|
|
import { IDestroyable, ILifetimeContext, ILifetimeManager, ILifetimeSlot } from "./interfaces";
|
|
|
import { argumentNotNull, isDestroyable } from "./traits";
|
|
|
|
|
|
const safeCall = (item: () => void) => {
|
|
|
try {
|
|
|
item();
|
|
|
} catch {
|
|
|
// silence!
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const noop = () => { };
|
|
|
|
|
|
const fail = (message: string) => (): never => {
|
|
|
throw new Error(message);
|
|
|
};
|
|
|
|
|
|
const _emptySlot = Object.freeze({
|
|
|
has: () => false,
|
|
|
|
|
|
initialize: noop,
|
|
|
|
|
|
get: fail("The specified item isn't registered with a lifetime manager"),
|
|
|
|
|
|
store: noop,
|
|
|
|
|
|
remove: noop,
|
|
|
|
|
|
cleanup: noop,
|
|
|
});
|
|
|
|
|
|
const _destroy = (item: IDestroyable) => item.destroy();
|
|
|
|
|
|
const _makeCleanup = <T>(value: T, cleanup?: (item: T) => void) =>
|
|
|
cleanup ? () => cleanup(value) :
|
|
|
isDestroyable(value) ? () => _destroy(value) :
|
|
|
noop;
|
|
|
|
|
|
const newSlot = <T>(put: (item: ILifetimeSlot<T>) => void, remove: () => void): ILifetimeSlot<T> => ({
|
|
|
has: () => false,
|
|
|
|
|
|
initialize: () => put(pendingSlot(put, remove)),
|
|
|
|
|
|
get: fail("The slot doesn't hold a value"),
|
|
|
|
|
|
store: (value, cleanup) => put(valueSlot(value, cleanup, remove)),
|
|
|
|
|
|
remove: noop,
|
|
|
|
|
|
cleanup: noop,
|
|
|
});
|
|
|
|
|
|
const pendingSlot = <T>(put: (item: ILifetimeSlot<T>) => void, remove: () => void): ILifetimeSlot<T> => ({
|
|
|
has: () => false,
|
|
|
|
|
|
get: fail("The value in this slot doesn't exist"),
|
|
|
|
|
|
initialize: fail("Cyclic reference detected"),
|
|
|
|
|
|
store: (value, cleanup) => put(valueSlot(value, cleanup, remove)),
|
|
|
|
|
|
remove,
|
|
|
|
|
|
cleanup: noop
|
|
|
});
|
|
|
|
|
|
const valueSlot = <T>(value: T, cleanup: ((item: T) => void) | undefined, remove: () => void) => ({
|
|
|
has: () => true,
|
|
|
|
|
|
get: () => value,
|
|
|
|
|
|
initialize: fail("The slot already has a value"),
|
|
|
|
|
|
store: fail("The slot already has a value"),
|
|
|
|
|
|
cleanup: _makeCleanup(value, cleanup),
|
|
|
|
|
|
remove: remove
|
|
|
});
|
|
|
|
|
|
export class LifetimeManager implements ILifetimeManager {
|
|
|
private _destroyed = false;
|
|
|
|
|
|
private readonly _slots: Record<string, ILifetimeSlot<unknown>> = {};
|
|
|
|
|
|
slot<T>(cookie: string | number): ILifetimeSlot<T> {
|
|
|
if (cookie in this._slots)
|
|
|
return this._slots[cookie] as ILifetimeSlot<T>;
|
|
|
|
|
|
return newSlot<T>(this._put(cookie), this._remove(cookie));
|
|
|
}
|
|
|
|
|
|
private readonly _put = (id: string | number) => <T>(slot: ILifetimeSlot<T>) => {
|
|
|
this._assertNotDestroyed();
|
|
|
this._slots[id] = slot as ILifetimeSlot<unknown>;
|
|
|
};
|
|
|
|
|
|
private readonly _remove = (id: string | number) => () => {
|
|
|
this._assertNotDestroyed();
|
|
|
delete this._slots[id];
|
|
|
};
|
|
|
|
|
|
private _assertNotDestroyed() {
|
|
|
if (this._destroyed)
|
|
|
throw new Error("The lifetime manager is destroyed");
|
|
|
|
|
|
}
|
|
|
|
|
|
destroy() {
|
|
|
if (!this._destroyed) {
|
|
|
this._destroyed = true;
|
|
|
Object.values(this._slots).forEach(({ cleanup }) => safeCall(cleanup));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
export const emptyLifetime = <T>() => () => _emptySlot as ILifetimeSlot<T>;
|
|
|
|
|
|
let nextId = 1;
|
|
|
|
|
|
export const hierarchyLifetime = <T>() => {
|
|
|
const slotId = nextId++;
|
|
|
return (context: ILifetimeContext) =>
|
|
|
context.containerSlot<T>(slotId);
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Creates a lifetime instance bound to the current activation context. This
|
|
|
* lifetime will store the service instance per activation context. Every
|
|
|
* top level service resolution will create a new activation context. This
|
|
|
* context is propagated to subsequent service resolution thus all services
|
|
|
* with context lifetime will be shared among their consumers.
|
|
|
*
|
|
|
* @returns The instance of the lifetime.
|
|
|
*/
|
|
|
export const contextLifetime = <T>() => {
|
|
|
const slotId = nextId++;
|
|
|
|
|
|
return (context: ILifetimeContext) =>
|
|
|
context.contextSlot<T>(slotId);
|
|
|
};
|
|
|
|
|
|
const singletons = new LifetimeManager();
|
|
|
|
|
|
/**
|
|
|
* Creates the lifetime for the service which will allow existence only one
|
|
|
* instance with the specified {@linkcode typeId}. If there will be created
|
|
|
* several lifetime instances with same `typeId` in the runtime, they will
|
|
|
* share the same service instance.
|
|
|
*
|
|
|
* @param typeId The identified for the global instance, usually this is a
|
|
|
* fully qualified class name
|
|
|
* @returns The lifetime instance
|
|
|
*/
|
|
|
export const singletonLifetime = <T>(typeId: string) => {
|
|
|
argumentNotNull(typeId, "typeId");
|
|
|
|
|
|
return () => singletons.slot<T>(typeId);
|
|
|
};
|
|
|
|
|
|
/** Creates a lifetime bound to the specified container. Using this lifetime
|
|
|
* will create a single service instance per the specified container.
|
|
|
*
|
|
|
* @param container The container which will manage the lifetime for the service
|
|
|
*/
|
|
|
export const containerLifetime = <T>(manager: ILifetimeManager) => {
|
|
|
const slotId = nextId++;
|
|
|
return () => manager.slot<T>(slotId);
|
|
|
};
|
|
|
|