import { IActivationContext, ILifetime, ILifetimeContext, ILifetimeManager, ILifetimeSlot } from "./interfaces"; import { ActivationContext } from "./ActivationContext"; import { argumentNotNull, isDestroyable } from "./traits"; const safeCall = (item: () => void) => { try { item(); } catch { // silence! } }; const _emptySlot = Object.freeze({ has: () => false, initialize: () => void (0), get: () => { throw new Error("The specified item isn't registered with this lifetime manager"); }, store: () => void (0) }); const _unknonwSlot = Object.freeze({ has: () => false, initialize: () => { throw new Error("Can't call initialize on the unknown lifetime object"); }, get: () => { throw new Error("The lifetime object isn't initialized"); }, store: () => { throw new Error("Can't store a value in the unknown lifetime object"); } }); let nextId = 0; const singletons: { [K: string]: unknown } = {}; const pendingSlot = (store: (item: T) => void) => ({ has: () => false, get: () => { throw new Error("The value in this slot doesn't exist"); }, initialize: () => { throw new Error("Cyclic reference detected"); }, store }); const noop = () => void(0); const valueSlot = (value: T, cleanup: (item: T) => void) => ({ has: () => true, get: () => value, initialize: () => { throw new Error("The slot already has a value"); }, store: () => { throw new Error("The slot already has a value"); } }); export class LifetimeManager implements ILifetimeManager { private _destroyed = false; private readonly _slots: Record> = {}; slot(cookie: string): ILifetimeSlot { if (cookie in this._slots) return this._slots[cookie] as ILifetimeSlot; const store = (item: T, cleanup?: (item: T) => void) => { this._assertNotDestroyed(); this._slots[cookie] = valueSlot( item, cleanup ?? isDestroyable(item) ? () => item.destroy() : noop ); }; return { has: () => false, get: () => { throw new Error("The value isn't stored in this slot"); }, store, initialize: () => { this._assertNotDestroyed(); this._slots[cookie] = pendingSlot(store); } }; } private _assertNotDestroyed() { if (this._destroyed) throw new Error("The lifetime manager is destroyed"); } destroy() { if (!this._destroyed) { this._destroyed = true; this._cleanup.forEach(safeCall); this._cleanup.length = 0; } } } export const emptyLifetime = (): ILifetime => { return _emptyLifetime; }; export const hierarchyLifetime = (): ILifetime => { let _lifetime: ILifetime = _unknownLifetime; return { initialize(context: ILifetimeContext) { if (_lifetime !== _unknownLifetime) throw new Error("Cyclic reference activation detected"); _lifetime = context.createContainerLifetime(); }, get() { return _lifetime.get(); }, has() { return _lifetime.has(); }, store(item: NonNullable, cleanup?: (item: NonNullable) => void) { return _lifetime.store(item, cleanup); }, toString() { return `[object HierarchyLifetime, has=${String(this.has())}]`; } }; }; /** * 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 = (): ILifetime => { let _lifetime: ILifetime = _unknownLifetime; return { initialize(context: ILifetimeContext) { if (_lifetime !== _unknownLifetime) throw new Error("Cyclic reference detected"); _lifetime = context.createLifetime(); }, get() { return _lifetime.get(); }, has() { return _lifetime.has(); }, store(item: NonNullable) { _lifetime.store(item); }, toString() { return `[object ContextLifetime, has=${String(this.has())}]`; } }; }; /** * 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 = (typeId: string): ILifetime => { argumentNotNull(typeId, "typeId"); let pending = false; return { has() { return typeId in singletons; }, get() { if (!this.has()) throw new Error(`The instance ${typeId} doesn't exists`); return singletons[typeId] as NonNullable; }, initialize() { if (pending) throw new Error("Cyclic reference detected"); pending = true; }, store(item: NonNullable) { singletons[typeId] = item; pending = false; }, toString() { return `[object SingletonLifetime, has=${String(this.has())}, typeId=${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 = (container: { createLifetime(): ILifetime }) => { let _lifetime: ILifetime = _unknownLifetime; return { initialize() { if (_lifetime !== _unknownLifetime) throw new Error("Cyclic reference detected"); _lifetime = container.createLifetime(); }, get() { return _lifetime.get(); }, has() { return _lifetime.has(); }, store(item: NonNullable, cleanup?: (item: NonNullable) => void) { _lifetime.store(item, cleanup); }, toString() { return `[object ContainerLifetime, has=${String(_lifetime.has())}]`; } }; };