import { IActivationContext, ILifetime, ILifetimeManager } from "./interfaces"; import { ActivationContext } from "./ActivationContext"; import { argumentNotNull, isDestroyable } from "./traits"; const safeCall = (item: () => void) => { try { item(); } catch { // silence! } }; const _emptyLifetime = Object.freeze({ has() { return false; }, initialize() { }, get() { throw new Error("The specified item isn't registered with this lifetime manager"); }, store() { // does nothing }, toString() { return `[object EmptyLifetime]`; } }); const _unknownLifetime = Object.freeze({ has() { return 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"); }, toString() { return `[object UnknownLifetime]`; } }); let nextId = 0; const singletons: { [K: string]: unknown } = {}; export class LifetimeManager implements ILifetimeManager { private _cleanup: (() => void)[] = []; private readonly _cache: { [K: string]: unknown } = {}; private _destroyed = false; private readonly _pending: { [K: string]: unknown } = {}; create(): ILifetime & { remove(): void; } { const id = ++nextId; return { has: () => id in this._cache, get: () => { const t = this._cache[id] as T; if (t === undefined || t === null) throw new Error(`The item with with the key ${id} isn't found`); return t; }, initialize: () => { if (this._pending[id]) throw Error(`Cyclic reference detected: the item with the key ${id} is already activating.`); this._pending[id] = true; }, store: (item: NonNullable, cleanup?: (item: NonNullable) => void) => { if (id in this._cache) throw new Error(`The item with with the key ${id} already registered with this lifetime manager`); if (this._destroyed) throw new Error("Lifetime manager is destroyed"); delete this._pending[id]; this._cache[id] = item; if (cleanup) { this._cleanup.push(() => cleanup(item)); } else if (isDestroyable(item)) { this._cleanup.push(() => item.destroy()); } }, remove: () => { if (this._pending[id]) throw new Error(`The item '${id}' can't be removed before it has been stored`); delete this._cache[id]; } }; } 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: IActivationContext) { 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())}]`; } }; }; export const contextLifetime = (): ILifetime => { let _lifetime: ILifetime = _unknownLifetime; return { initialize(context: ActivationContext) { 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())}]`; } }; }; 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}]`; } }; }; 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) { _lifetime.store(item); }, toString() { return `[object ContainerLifetime, has=${String(_lifetime.has())}]`; } }; };