import { IDestroyable, MapOf } from "../interfaces"; import { argumentNotNull, isDestroyable, argumentNotEmptyString, isRemovable } from "../safe"; import { ILifetime, ServiceContainer } from "./interfaces"; import { ActivationContext } from "./ActivationContext"; function safeCall(item: () => void) { try { item(); } catch { // silence! } } const emptyLifetime: ILifetime = 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: ILifetime = 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: any = {}; export class LifetimeManager implements IDestroyable { private _cleanup: (() => void)[] = []; private _cache: MapOf = {}; private _destroyed = false; private _pending: MapOf = {}; create(): ILifetime { const self = this; const id = ++nextId; return { has() { return (id in self._cache); }, get() { const t = self._cache[id]; if (t === undefined) throw new Error(`The item with with the key ${id} isn't found`); return t; }, initialize() { if (self._pending[id]) throw Error(`Cyclic reference detected: the item with the key ${id} is already activating.`); self._pending[id] = true; }, store(item: any, cleanup?: (item: any) => void) { argumentNotNull(id, "id"); argumentNotNull(item, "item"); if (this.has()) throw new Error(`The item with with the key ${id} already registered with this lifetime manager`); delete self._pending[id]; self._cache[id] = item; if (self._destroyed) throw new Error("Lifetime manager is destroyed"); if (cleanup) { self._cleanup.push(() => cleanup(item)); } else if (isDestroyable(item)) { self._cleanup.push(() => item.destroy()); } } }; } destroy() { if (!this._destroyed) { this._destroyed = true; this._cleanup.forEach(safeCall); this._cleanup.length = 0; } } static empty(): ILifetime { return emptyLifetime; } static hierarchyLifetime() { let _lifetime = unknownLifetime; return { initialize(context: ActivationContext) { if (_lifetime !== unknownLifetime) throw new Error("Cyclic reference activation detected"); _lifetime = context.getContainer().getLifetimeManager().create(); }, get() { return _lifetime.get(); }, has() { return _lifetime.has(); }, store(item: any, cleanup?: (item: any) => void) { return _lifetime.store(item, cleanup); }, toString() { return `[object HierarchyLifetime, has=${this.has()}]`; } }; } static contextLifetime() { let _lifetime = 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: any) { _lifetime.store(item); }, toString() { return `[object ContextLifetime, has=${this.has()}]`; } }; } static singletonLifetime(typeId: string) { argumentNotEmptyString(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]; }, initialize() { if (pending) throw new Error("Cyclic reference detected"); pending = true; }, store(item: any) { singletons[typeId] = item; pending = false; }, toString() { return `[object SingletonLifetime, has=${this.has()}, typeId=${typeId}]`; } }; } static containerLifetime(container: ServiceContainer) { let _lifetime = unknownLifetime; return { initialize(context: ActivationContext) { if (_lifetime !== unknownLifetime) throw new Error("Cyclic reference detected"); _lifetime = container.getLifetimeManager().create(); }, get() { return _lifetime.get(); }, has() { return _lifetime.has(); }, store(item: any) { _lifetime.store(item); }, toString() { return `[object ContainerLifetime, has=${_lifetime.has()}]`; } }; } }