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: unknown) => () => isDestroyable(item) && item.destroy() ; const _makeCleanup = (value: T, cleanup?: (item: T) => void) => cleanup ? () => cleanup(value) : _destroy(value); const newSlot = (put: (item: ILifetimeSlot) => void, remove: () => void): ILifetimeSlot => ({ 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 = (put: (item: ILifetimeSlot) => void, remove: () => void): ILifetimeSlot => ({ 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 = (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> = {}; slot(cookie: string | number): ILifetimeSlot { if (cookie in this._slots) return this._slots[cookie] as ILifetimeSlot; return newSlot(this._put(cookie), this._remove(cookie)); } private readonly _put = (id: string | number) => (slot: ILifetimeSlot) => { this._assertNotDestroyed(); this._slots[id] = slot as ILifetimeSlot; }; 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 = () => () => _emptySlot as ILifetimeSlot; let nextId = 1; export const containerLifetime = () => { const slotId = nextId ++; return (context: ILifetimeContext) => context.ownerSlot(slotId); }; export const hierarchyLifetime = () => { const slotId = nextId++; return (context: ILifetimeContext) => context.containerSlot(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 = () => { const slotId = nextId++; return (context: ILifetimeContext) => context.contextSlot(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 = (typeId: string) => { argumentNotNull(typeId, "typeId"); return () => singletons.slot(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 = (manager: ILifetimeManager) => { const slotId = nextId++; return () => manager.slot(slotId); };