# HG changeset patch # User cin # Date 2023-08-29 19:48:00 # Node ID 99a53d63fd6f14a71cf5c77f0676a6f45cb61275 # Parent 988f0f6aab67cc8c5b539399e8a96aa97331ccf5 intoruced initial work on ILifetimeSlot diff --git a/src/main/ts/LifetimeManager.ts b/src/main/ts/LifetimeManager.ts --- a/src/main/ts/LifetimeManager.ts +++ b/src/main/ts/LifetimeManager.ts @@ -1,4 +1,4 @@ -import { IActivationContext, ILifetime, ILifetimeManager } from "./interfaces"; +import { IActivationContext, ILifetime, ILifetimeContext, ILifetimeManager, ILifetimeSlot } from "./interfaces"; import { ActivationContext } from "./ActivationContext"; import { argumentNotNull, isDestroyable } from "./traits"; @@ -10,44 +10,29 @@ const safeCall = (item: () => void) => { } }; -const _emptyLifetime = Object.freeze({ - has() { - return false; - }, +const _emptySlot = Object.freeze({ + has: () => false, - initialize() { - }, + initialize: () => void (0), - get() { + get: () => { throw new Error("The specified item isn't registered with this lifetime manager"); }, - store() { - // does nothing - }, - - toString() { - return `[object EmptyLifetime]`; - } - + store: () => void (0) }); -const _unknownLifetime = Object.freeze({ - has() { - return false; - }, - initialize() { +const _unknonwSlot = Object.freeze({ + has: () => false, + + initialize: () => { throw new Error("Can't call initialize on the unknown lifetime object"); }, - get() { + get: () => { throw new Error("The lifetime object isn't initialized"); }, - store() { + store: () => { throw new Error("Can't store a value in the unknown lifetime object"); - }, - - toString() { - return `[object UnknownLifetime]`; } }); @@ -55,56 +40,73 @@ 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 _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, + private readonly _slots: Record> = {}; - 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; - }, + slot(cookie: string): ILifetimeSlot { + if (cookie in this._slots) + return this._slots[cookie] as ILifetimeSlot; - 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; + const store = (item: T, cleanup?: (item: T) => void) => { + this._assertNotDestroyed(); + this._slots[cookie] = valueSlot( + item, + cleanup ?? isDestroyable(item) ? () => item.destroy() : noop + ); + }; - if (cleanup) { - this._cleanup.push(() => cleanup(item)); - } else if (isDestroyable(item)) { - this._cleanup.push(() => item.destroy()); - } + return { + has: () => false, + get: () => { + throw new Error("The value isn't stored in this slot"); }, - - remove: () => { - if (this._pending[id]) - throw new Error(`The item '${id}' can't be removed before it has been stored`); - delete this._cache[id]; + 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; @@ -122,7 +124,7 @@ export const emptyLifetime = (): ILif export const hierarchyLifetime = (): ILifetime => { let _lifetime: ILifetime = _unknownLifetime; return { - initialize(context: IActivationContext) { + initialize(context: ILifetimeContext) { if (_lifetime !== _unknownLifetime) throw new Error("Cyclic reference activation detected"); @@ -143,10 +145,19 @@ export const hierarchyLifetime = (): }; }; +/** + * 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: ActivationContext) { + initialize(context: ILifetimeContext) { if (_lifetime !== _unknownLifetime) throw new Error("Cyclic reference detected"); _lifetime = context.createLifetime(); @@ -166,6 +177,16 @@ export const contextLifetime = (): IL }; }; +/** + * 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; @@ -193,6 +214,11 @@ export const singletonLifetime = (typ }; }; +/** 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 { @@ -207,8 +233,8 @@ export const containerLifetime = (con has() { return _lifetime.has(); }, - store(item: NonNullable) { - _lifetime.store(item); + store(item: NonNullable, cleanup?: (item: NonNullable) => void) { + _lifetime.store(item, cleanup); }, toString() { return `[object ContainerLifetime, has=${String(_lifetime.has())}]`; diff --git a/src/main/ts/interfaces.ts b/src/main/ts/interfaces.ts --- a/src/main/ts/interfaces.ts +++ b/src/main/ts/interfaces.ts @@ -168,11 +168,16 @@ export interface Descriptor { activate(context: IActivationContext): NonNullable; } -export interface IActivationContext extends ServiceLocator { +/** The context used to initialize lifetime instance {@linkcode ILifetime} */ +export interface ILifetimeContext { createLifetime(): ILifetime; createContainerLifetime(): ILifetime; +} + +export interface IActivationContext extends ILifetimeContext, ServiceLocator { + register(name: K, service: DescriptorMap[K]): void; } @@ -242,13 +247,23 @@ export interface ILifetime { get(): NonNullable; - initialize(context: IActivationContext): void; + initialize(context: ILifetimeContext): void; store(item: NonNullable, cleanup?: (item: NonNullable) => void): void; toString(): string; } +export interface ILifetimeSlot { + has: () => boolean; + + get: () => T; + + initialize: () => void; + + store(item: T, cleanup?: (item: T) => void): void; +} + export interface ILifetimeManager extends IDestroyable { create(): ILifetime; }