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 noop = () => {}; const fail = (message: string) => { 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 }); 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"); } }); 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 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"); } }); const singletons: { [K: string]: unknown } = {}; 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); } }; } remove(cookie: string) { delete this._slots[cookie]; } 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(({clean}) => ) } } } export const emptyLifetime = (): ILifetime => { return _emptyLifetime; }; export const hierarchyLifetime = (): ILifetime => { // TODO: вот здесь ошибка, при первой активации сервиса будет получен и // привязан lifetime из дочернего контейнера, при активации через второй // дочерний контейнера это приведет к ошибке, точнее будет взят экземпляр // из первого контейнера. 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())}]`; } }; };