# HG changeset patch # User cin # Date 2025-11-02 12:55:40 # Node ID 3985e8405319cf27c02acadb912a87af1c06690d # Parent 3f8a82c8ce7388337215bfb9ba15addddb85845a sync diff --git a/build.gradle b/build.gradle --- a/build.gradle +++ b/build.gradle @@ -30,9 +30,8 @@ typescript { declaration = true experimentalDecorators = true strict = true - module = "commonjs" - target = "es5" - lib = ["es2015", "dom", "scripthost"] + module = "nodenext" + target = "ESNext" } tscCmd = "$projectDir/node_modules/.bin/tsc" tsLintCmd = "$projectDir/node_modules/.bin/tslint" diff --git a/src/main/ts/ActivationContext.ts b/src/main/ts/ActivationContext.ts --- a/src/main/ts/ActivationContext.ts +++ b/src/main/ts/ActivationContext.ts @@ -1,15 +1,8 @@ -import { ActivationError } from "./ActivationError"; -import { Descriptor, ILifetime, IActivationContext, DescriptorMap, ILifetimeManager, ILifetimeSlot } from "./interfaces"; -import { argumentNotNull, prototype } from "./traits"; - -export interface ActivationContextInfo { - name: string; - - service: string; - -} - -let nextId = 1; +import { ActivationError, ActivationItem } from "./ActivationError"; +import { IActivationContext, DescriptorMap, ILifetimeManager, ILifetimeSlot, ServiceLocator, IContainerBuilder } from "../typings/interfaces"; +import { argumentNotNull, each, prototype } from "./traits"; +import { LifetimeManager } from "./LifetimeManager"; +import { ContainerBuilder } from "./ContainerBuilder"; /** This object is created once per `Container.resolve` method call and used to * cache dependencies and to track created instances. The activation context @@ -21,17 +14,14 @@ let nextId = 1; * this activation context. */ export class ActivationContext implements IActivationContext { - private readonly _cache: Record; - private readonly _services: DescriptorMap; - - private readonly _name: string; + private readonly _container: ServiceLocator; - private readonly _service: Descriptor; + private readonly _contextScope: ILifetimeManager; - private readonly _lifetimeManagers: ILifetimeManager[]; + private _services: DescriptorMap; - private readonly _parent: ActivationContext | undefined; + private readonly _scope: ILifetimeManager[]; /** Creates a new activation context with the specified parameters. * @param containerLifetimeManager the container which starts the activation process @@ -41,31 +31,31 @@ export class ActivationContext implem * @param service the service to activate, this parameter is used for the * debug purpose. */ - constructor(lifetimeManagers: ILifetimeManager[], services: DescriptorMap, name: string, service: Descriptor, cache = {}) { - this._name = name; - this._service = service; - this._cache = cache; + constructor(container: ServiceLocator, scope: ILifetimeManager[], services: DescriptorMap, contextScope: ILifetimeManager = new LifetimeManager()) { + this._container = container; + this._contextScope = contextScope; this._services = services; - this._lifetimeManagers = lifetimeManagers; - } - - /** the name of the current resolving dependency */ - getName() { - return this._name; + this._scope = scope; } - resolve(name: K): NonNullable; - resolve(name: K, def: T): NonNullable | T; - resolve(name: K, def?: T): NonNullable | T | undefined { - const d = this._services[name]; + resolve(name: K, stack: ActivationItem[]): NonNullable; + resolve(name: K, stack: ActivationItem[], def: T): NonNullable | T; + resolve(name: K, stack: ActivationItem[], def?: T): NonNullable | T | undefined { + const service = this._services[name]; - if (d !== undefined) { - return this.activate(d, name.toString()); + if (service !== undefined) { + return service.activate( + this, + stack.concat({ + name: name.toString(), + descriptor: service.toString() + }) + ); } else { - if (arguments.length > 1) + if (arguments.length > 2) return def; else - throw new Error(`Service ${String(name)} not found`); + throw new Error("Service not found"); } } @@ -75,7 +65,7 @@ export class ActivationContext implem * @name{string} the name of the service * @service{string} the service descriptor to register */ - register(name: K, service: DescriptorMap[K]) { + private _register(name: K, service: DescriptorMap[K]) { argumentNotNull(name, "name"); const d = this._services[name]; @@ -85,69 +75,37 @@ export class ActivationContext implem this._services[name] = service; } - ownerSlot(slotId: string | number): ILifetimeSlot { - return this._lifetimeManagers[this._service.level].slot(slotId); + scopeSlot(level: number, slotId: string | number): ILifetimeSlot { + if (level < 0 || level >= this._scope.length) + throw new Error("The scope level is out of range"); + return this._scope[level].slot(slotId); } - containerSlot(slotId: string | number): ILifetimeSlot { - return this._lifetimeManagers[this._lifetimeManagers.length - 1].slot(slotId); + hierarchySlot(slotId: string | number): ILifetimeSlot { + return this._scope[this._scope.length - 1].slot(slotId); + } + + selfContainer(): ServiceLocator { + return this._container; + } + + createChildContainer(): IContainerBuilder { + return new ContainerBuilder(this._services, this._scope); } contextSlot(slotId: string | number): ILifetimeSlot { - - } - - createLifetime(): ILifetime { - const id = nextId++; - return { - initialize() { }, - has: () => id in this._cache, - get: () => { - const v = this._cache[id] as T; - if (v === undefined || v === null) - throw new Error("The value isn't present in the activation context"); - return v; - }, - store: item => { - this._cache[id] = item; - } - }; + return this._contextScope.slot(slotId); } - activate(d: Descriptor, name: string) { - // TODO: add logging - // if (trace.isLogEnabled()) - // trace.log("enter {0} {1}", name, d); - - const ctx = new ActivationContext( - this._containerLifetimeManager, - d.hasOverrides ? prototype(this._services) : this._services, - name, - d, - this._cache - ); - - const v = d.activate(ctx); - - // if (trace.isLogEnabled()) - // trace.log(`leave ${name}`); - - return v; - } - - getStack(): ActivationContextInfo[] { - const stack = [{ - name: this._name, - service: this._service.toString() - }]; - - return this._parent ? - stack.concat(this._parent.getStack()) : - stack; - } - - fail(innerException: unknown): never { - throw new ActivationError(this._name, this.getStack(), innerException); + withOverrides(overrides: DescriptorMap, action: () => X) { + const services = this._services; + this._services = prototype(this._services); + try { + each(overrides, (v, k) => this._register(k, v)); + return action(); + } finally { + this._services = services; + } } } diff --git a/src/main/ts/ActivationError.ts b/src/main/ts/ActivationError.ts --- a/src/main/ts/ActivationError.ts +++ b/src/main/ts/ActivationError.ts @@ -1,6 +1,6 @@ export interface ActivationItem { name: string; - service: string; + descriptor: string; } /** @@ -17,11 +17,6 @@ export class ActivationError { readonly activationStack: ActivationItem[]; /** - * The name of the failed service - */ - readonly service: string; - - /** * The exception which occurred during activation of the service */ readonly innerException: unknown; @@ -31,30 +26,32 @@ export class ActivationError { */ readonly message: string; - constructor(service: string, activationStack: ActivationItem[], innerException: unknown) { - this.message = "Failed to activate the service"; + constructor(message: string, activationStack: ActivationItem[], innerException?: unknown) { + this.message = message; this.activationStack = activationStack; - this.service = service; this.innerException = innerException; } toString() { const parts = [this.message]; - if (this.service) - parts.push(`when activating: ${String(this.service)}`); + + if (this.activationStack && this.activationStack.length) { + const [{ name, descriptor }, ...before] = this.activationStack; + + parts.push(`when activating: ${name}, ${descriptor}`); + + if (before) { + parts.push("at"); + parts.push.apply( + null, + before.map(({ name: name, descriptor: service }) => ` ${name} ${service}`) + ); + } + } if (this.innerException) parts.push(`caused by: ${String(this.innerException)}`); - if (this.activationStack) { - parts.push("at"); - parts.push.apply(null, - this.activationStack - .map(({ name, service }) => ` ${name} ${service}`) - ); - - } - return parts.join("\n"); } } diff --git a/src/main/ts/Container.ts b/src/main/ts/Container.ts --- a/src/main/ts/Container.ts +++ b/src/main/ts/Container.ts @@ -1,24 +1,35 @@ import { ActivationContext } from "./ActivationContext"; -import { ActivationError } from "./ActivationError"; import { ContainerBuilder } from "./ContainerBuilder"; -import { LifetimeManager } from "./LifetimeManager"; -import { DescriptorMap, IContainerBuilder, IDestroyable, ILifetimeManager, ServiceLocator } from "./interfaces"; +import { LifetimeManager, emptySlot } from "./LifetimeManager"; +import { DescriptorMap, IContainerBuilder, IDestroyable, ILifetimeManager, ILifetimeSlot, ServiceLocator } from "../typings/interfaces"; + +let nextId = 1; export class Container implements ServiceLocator, IDestroyable { private readonly _services: DescriptorMap; - private readonly _lifetimeManagers: ILifetimeManager[]; + private readonly _scope: ILifetimeManager[]; + + private readonly _containerId = `container-${nextId++}`; + + private readonly _slot: ILifetimeSlot; private _disposed: boolean; - private readonly _onDestroyed: () => void; - - constructor(services: DescriptorMap, lifetimeManagers: ILifetimeManager[], destroyed: () => void) { + constructor(services: DescriptorMap, parentScope: ILifetimeManager[]) { this._services = services; this._disposed = false; - this._lifetimeManagers = lifetimeManagers.concat(new LifetimeManager()); - this._onDestroyed = destroyed; - + this._scope = parentScope.concat(new LifetimeManager()); + + // If this container is created inside the parent container scope, + // allocated lifetime slot + this._slot = parentScope.length ? + parentScope[parentScope.length - 1].slot(this._containerId) : + emptySlot(); + + // store the container reference in the lifetime slot + this._slot.store(this); + } private _assertNotDestroyed() { @@ -29,9 +40,7 @@ export class Container implements Ser createChildBuilder(): IContainerBuilder { this._assertNotDestroyed(); - const lifetime = this._lifetimeManager.create(); - - return new ContainerBuilder(this._services, lifetime); + return new ContainerBuilder(this._services, this._scope); } resolve(name: K): NonNullable; @@ -39,26 +48,24 @@ export class Container implements Ser resolve(name: K, def?: T) { this._assertNotDestroyed(); - const d = this._services[name]; - if (d === undefined) { - if (arguments.length > 1) - return def; - else - throw new Error(`Service '${String(name)}' isn't found`); - } else { - const context = new ActivationContext(this._lifetimeManagers, this._services, String(name), d); - try { - return d.activate(context); - } catch (error) { - throw new ActivationError(name.toString(), context.getStack(), error); - } - } + const context = new ActivationContext(this, this._scope, this._services); + + return arguments.length === 1 ? + context.resolve(name, []) : + context.resolve(name, [], def); } + destroy() { if (this._disposed) return; this._disposed = true; - this._lifetimeManager.destroy(); - (0,this._onDestroyed)(); + + // destroy own scope + this._scope[this._scope.length - 1].destroy(); + + // release lifetime slot if the container is destroyed before the parent + // container. If this container is destroyed during the parent container + // cleanup procedure this call will have no effect. + this._slot.remove(); } } diff --git a/src/main/ts/ContainerBuilder.ts b/src/main/ts/ContainerBuilder.ts --- a/src/main/ts/ContainerBuilder.ts +++ b/src/main/ts/ContainerBuilder.ts @@ -1,9 +1,6 @@ import { Container } from "./Container"; import { DescriptorBuilder } from "./DescriptorBuilder"; -import { containerSelfDescriptor } from "./DescriptorImpl"; -import { Descriptor, IContainerBuilder, IDescriptorBuilder, DescriptorMap, ServiceLocator, ILifetime, IDestroyable, ContainerServices, ContainerServicesConstraint } from "./interfaces"; -import { emptyLifetime, LifetimeManager } from "./LifetimeManager"; -import { isDestroyable, prototype } from "./traits"; +import { Descriptor, IContainerBuilder, IDescriptorBuilder, DescriptorMap, ServiceLocator, ILifetimeManager } from "../typings/interfaces"; /** * Container builder used to prepare service descriptors and create a IoC container @@ -13,20 +10,20 @@ export class ContainerBuilder>; + private readonly _services: DescriptorMap; - private readonly _lifetimeManager = new LifetimeManager(); + private readonly _scope: ILifetimeManager[]; - private readonly _lifetime: ILifetime; + private readonly _level: number; - constructor(parentServices: DescriptorMap | null = null, lifetime?: ILifetime) { + constructor(parentServices: DescriptorMap | null, scope: ILifetimeManager[] = []) { this._services = { ...parentServices }; // create a copy - this._lifetime = lifetime ?? emptyLifetime(); + this._level = scope.length; + this._scope = scope; } - createServiceBuilder(name: K): - IDescriptorBuilder, U> { + createServiceBuilder(name: K): IDescriptorBuilder, U> { - return new DescriptorBuilder(this._lifetimeManager, this._register(name), this._fail); + return new DescriptorBuilder(this._level, String(name), this._register(name), this._fail); } @@ -35,13 +32,7 @@ export class ContainerBuilder(name: K) => diff --git a/src/main/ts/DescriptorBuilder.ts b/src/main/ts/DescriptorBuilder.ts --- a/src/main/ts/DescriptorBuilder.ts +++ b/src/main/ts/DescriptorBuilder.ts @@ -1,7 +1,7 @@ -import { BuildDescriptorFn, IDescriptorBuilder, DepsMap, Resolve, DescriptorMap } from "./interfaces"; -import { Descriptor, ILifetime, ActivationType } from "./interfaces"; +import { BuildDescriptorFn, IDescriptorBuilder, DepsMap, Resolve, DescriptorMap } from "../typings/interfaces"; +import { Descriptor, ILifetime, ActivationType } from "../typings/interfaces"; import { DescriptorImpl } from "./DescriptorImpl"; -import { containerLifetime, contextLifetime, emptyLifetime, hierarchyLifetime, LifetimeManager, singletonLifetime } from "./LifetimeManager"; +import { contextLifetime, emptyLifetime, hierarchyLifetime, scopeLifetime, singletonLifetime } from "./LifetimeManager"; import { each, isPromise, isString, key, oid } from "./traits"; /** @@ -15,6 +15,10 @@ export class DescriptorBuilder; + private readonly _level: number; + + private readonly _instanceId: string | number; + private _lifetime: ILifetime = emptyLifetime(); private _overrides: DescriptorMap; @@ -36,11 +40,13 @@ export class DescriptorBuilder) => void, eb: (err: unknown) => void) { + constructor(level: number, instanceId: string | number, cb: (d: Descriptor) => void, eb: (err: unknown) => void) { this._cb = cb; this._eb = eb; this._overrides = {}; this._refs = {}; + this._level = level; + this._instanceId = instanceId; } /** Declares dependencies to be consumed in the factory method */ @@ -85,6 +91,8 @@ export class DescriptorBuilder, object, U>( + this._level, + String(nameOrServices), result => { this._overrides[nameOrServices] = result; this._complete(); @@ -145,11 +153,11 @@ export class DescriptorBuilder(activation: ActivationType, typeId?: string | object): ILifetime { switch (activation) { case "container": - return containerLifetime(); + return scopeLifetime(this._level, this._instanceId); case "hierarchy": - return hierarchyLifetime(); + return hierarchyLifetime(this._instanceId); case "context": - return contextLifetime(); + return contextLifetime(this._instanceId); case "singleton": { if (!typeId) throw Error("The singleton activation requires a typeId"); diff --git a/src/main/ts/DescriptorImpl.ts b/src/main/ts/DescriptorImpl.ts --- a/src/main/ts/DescriptorImpl.ts +++ b/src/main/ts/DescriptorImpl.ts @@ -1,9 +1,10 @@ -import { Descriptor, ILifetime, DepsMap, IActivationContext, DescriptorMap } from "./interfaces"; +import { Descriptor, ILifetime, IActivationContext, DescriptorMap, DescriptorDepsMap } from "../typings/interfaces"; +import { ActivationError, ActivationItem } from "./ActivationError"; import { each, key } from "./traits"; export interface DescriptorImplArgs { - readonly lifetime: ILifetime>; + readonly lifetime: ILifetime; readonly factory: (refs: Record) => NonNullable; @@ -11,90 +12,73 @@ export interface DescriptorImplArgs; - readonly dependencies?: DepsMap; + readonly dependencies?: DescriptorDepsMap; } -export const containerSelfDescriptor = () => Object.freeze({ - level: 0, - activate(context: IActivationContext) { - return context.createChildContainer(); - } -}); - - export class DescriptorImpl implements Descriptor { - private readonly _overrides?: DescriptorMap; + private readonly _overrides: DescriptorMap | undefined; - private readonly _lifetime: ILifetime>; + private readonly _lifetime: ILifetime; private readonly _factory: (refs: Record) => NonNullable; - private readonly _cleanup?: (item: NonNullable) => void; + private readonly _cleanup: (item: NonNullable) => void; - private readonly _deps?: DepsMap; - - readonly hasOverrides: boolean; + private readonly _dependencies: DescriptorDepsMap | undefined; constructor({ lifetime, factory, cleanup, overrides, dependencies }: DescriptorImplArgs) { this._lifetime = lifetime; this._factory = factory; - if (cleanup) - this._cleanup = cleanup; - if (overrides) - this._overrides = overrides; - if (dependencies) - this._deps = dependencies; - - this.hasOverrides = !!overrides; + this._cleanup = cleanup ? cleanup : () => { }; + this._overrides = overrides; + this._dependencies = dependencies; } - activate(context: IActivationContext): NonNullable { + activate(context: IActivationContext, stack: ActivationItem[]): NonNullable { - const { has, get, initialize, store } = this._lifetime(context); + const slot = this._lifetime(context); + if (slot.has()) + return slot.get(); + + if (!slot.initialize()) + throw new Error("Cyclic reference detected"); - if (has()) - return get(); + const instance = this._overrides ? + context.withOverrides(this._overrides, () => this._activate(context, stack)) : + this._activate(context, stack); - initialize(); + slot.store(instance, this._cleanup); - if (this._overrides) - each(this._overrides, (v, k) => context.register(k, v)); + return instance; + } - const resolve = ({ name, lazy, ...opts }: { name: K; lazy?: boolean; default?: S[K] | null; }) => { - if (lazy) { - return "default" in opts ? - () => context.resolve(name, opts.default) : - () => context.resolve(name); - } else { - return "default" in opts ? - context.resolve(name, opts.default) : - context.resolve(name); - } - }; + private _activate(context: IActivationContext, stack: ActivationItem[]) { + const refs: Record = {}; + if (this._dependencies) { + const resolve = ({ name, lazy, ...opts }: { name: K; lazy?: boolean; default?: S[K]; }) => { + if (lazy) { + return "default" in opts ? + () => context.resolve(name, stack, opts.default) : + () => context.resolve(name, stack); + } else { + return "default" in opts ? + context.resolve(name, stack, opts.default) : + context.resolve(name, stack); + } + }; - const deps = this._deps; - - const refs = deps ? - Object.keys(deps) - .map(k => { - const ref = deps[k]; - return typeof ref !== "object" ? - { [k]: resolve({ name: ref }) } : - { [k]: resolve(ref) }; - }) - .reduce((a, p) => ({ ...a, ...p }), {}) : - {}; + // can throw activation exception + each(this._dependencies, (v, k) => { + refs[k] = resolve(v); + }); + } try { // call the factory method - const instance = (0, this._factory)(refs); - - // store the instance - store(instance, this._cleanup); - return instance; - } catch (err) { - context.fail(err); + return (0, this._factory)(refs); + } catch (e) { + throw new ActivationError("Error creating instance", stack, e); } } diff --git a/src/main/ts/FluentConfiguration.ts b/src/main/ts/FluentConfiguration.ts --- a/src/main/ts/FluentConfiguration.ts +++ b/src/main/ts/FluentConfiguration.ts @@ -1,4 +1,4 @@ -import { ConfigurationMap, ConfigurationMapConstraint, ContainerServices, ContainerServicesConstraint, ExtractRequiredKeys, IContainerBuilder } from "./interfaces"; +import { ConfigurationMap, ConfigurationMapConstraint, ContainerServices, ContainerServicesConstraint, ExtractRequiredKeys, IContainerBuilder } from "../typings/interfaces"; import { argumentNotNull, each, isKey } from "./traits"; type ContainerExtensionConstraint = ContainerServicesConstraint>; 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,13 +1,6 @@ -import { IDestroyable, ILifetimeContext, ILifetimeManager, ILifetimeSlot } from "./interfaces"; -import { argumentNotNull, isDestroyable } from "./traits"; - -const safeCall = (item: () => void) => { - try { - item(); - } catch { - // silence! - } -}; +import { ILifetime, ILifetimeContext, ILifetimeManager, ILifetimeSlot } from "../typings/interfaces"; +import { LifetimeSlot } from "./LifetimeSlot"; +import { argumentNotNull } from "./traits"; const noop = () => { }; @@ -18,7 +11,7 @@ const fail = (message: string) => (): ne const _emptySlot = Object.freeze({ has: () => false, - initialize: noop, + initialize: () => false, get: fail("The specified item isn't registered with a lifetime manager"), @@ -29,105 +22,48 @@ const _emptySlot = Object.freeze({ 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() { + slot(slotId: string | number): ILifetimeSlot { if (this._destroyed) throw new Error("The lifetime manager is destroyed"); + if (slotId in this._slots) + return this._slots[slotId] as ILifetimeSlot; + + return this._slots[slotId] = new LifetimeSlot(() => delete this._slots[slotId]); } + destroy() { if (!this._destroyed) { this._destroyed = true; - Object.values(this._slots).forEach(({ cleanup }) => safeCall(cleanup)); + Object.values(this._slots).forEach(slot => { + try { + slot.cleanup(); + } catch { + // ignore + } + }); } } } -export const emptyLifetime = () => () => _emptySlot as ILifetimeSlot; +export const emptySlot = () => _emptySlot as ILifetimeSlot; -let nextId = 1; +export const emptyLifetime = () => emptySlot as ILifetime; -export const containerLifetime = () => { - const slotId = nextId ++; - return (context: ILifetimeContext) => - context.ownerSlot(slotId); -}; +export const scopeLifetime = (level: number, slotId: string | number) => + (context: ILifetimeContext) => + context.scopeSlot(level, slotId); -export const hierarchyLifetime = () => { - const slotId = nextId++; - return (context: ILifetimeContext) => - context.containerSlot(slotId); -}; +export const hierarchyLifetime = (slotId: string | number) => + (context: ILifetimeContext) => + context.hierarchySlot(slotId); /** * Creates a lifetime instance bound to the current activation context. This @@ -138,12 +74,9 @@ export const hierarchyLifetime = () = * * @returns The instance of the lifetime. */ -export const contextLifetime = () => { - const slotId = nextId++; - - return (context: ILifetimeContext) => +export const contextLifetime = (slotId: string | number) => + (context: ILifetimeContext) => context.contextSlot(slotId); -}; const singletons = new LifetimeManager(); @@ -162,13 +95,3 @@ export const singletonLifetime = (typ 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); -}; diff --git a/src/main/ts/LifetimeSlot.ts b/src/main/ts/LifetimeSlot.ts new file mode 100644 --- /dev/null +++ b/src/main/ts/LifetimeSlot.ts @@ -0,0 +1,61 @@ +import { ILifetimeSlot } from "../typings/interfaces"; +import { isDestroyable } from "./traits"; + +const _destroy = (item: unknown) => () => isDestroyable(item) && item.destroy(); + +enum State { + New, + + Initialized, + + Destroyed +} + +export class LifetimeSlot implements ILifetimeSlot { + private _value: NonNullable | undefined = undefined; + + private _state = State.New; + + cleanup: () => void; + + readonly remove: () => void; + + constructor(remove: () => void) { + this.remove = this._finalizer(remove); + this.cleanup = this._finalizer(() => { }); + } + + has(): boolean { + this._assertNotDisposed(); + return this._value !== undefined; + } + get(): NonNullable { + this._assertNotDisposed(); + if (this._value === undefined) + throw new Error("The slot doesn't have a value"); + return this._value; + } + + initialize(): boolean { + return this._state === State.New ? + (this._state = State.Initialized, true) : + false; + } + + store(item: NonNullable, cleanup: ((item: NonNullable) => void) | undefined = _destroy): void { + this._assertNotDisposed(); + this._value = item; + if (cleanup) + this.cleanup = this._finalizer(() => cleanup(item)); + } + + private _finalizer(cb: () => void) { + return () => this._state !== State.Destroyed ? (this._state = State.Destroyed, cb()) : void (0); + } + + private _assertNotDisposed() { + if (this._state === State.Destroyed) + throw new Error("The slot is disposed"); + } + +} \ No newline at end of file diff --git a/src/main/ts/traits.ts b/src/main/ts/traits.ts --- a/src/main/ts/traits.ts +++ b/src/main/ts/traits.ts @@ -1,5 +1,5 @@ import { FluentConfiguration } from "./FluentConfiguration"; -import { ContainerServices, ContainerServicesConstraint, IDestroyable } from "./interfaces"; +import { ContainerServicesConstraint, IDestroyable } from "../typings/interfaces"; export function fluent>() { return new FluentConfiguration(); diff --git a/src/main/ts/interfaces.ts b/src/main/typings/interfaces.d.ts rename from src/main/ts/interfaces.ts rename to src/main/typings/interfaces.d.ts --- a/src/main/ts/interfaces.ts +++ b/src/main/typings/interfaces.d.ts @@ -1,5 +1,5 @@ -import { ContainerBuilder } from "./ContainerBuilder"; -import { key } from "./traits"; +import { ActivationItem } from "../ts/ActivationError"; +import { key } from "../ts/traits"; export interface IDestroyable { destroy(): void; @@ -32,6 +32,10 @@ export type DepsMap = { [k in key]: Refs | keyof S; }; +export type DescriptorDepsMap = { + [k in key]: Refs; +}; + export type Refs = { [k in keyof S]: Ref; }[keyof S]; @@ -47,7 +51,7 @@ export type Ref = { * When specified the dependency becomes optional, the default value can be * `null` or `undefined` */ - default?: D | null + default?: D; }; export type Lazy = L extends true ? () => T : T; @@ -55,14 +59,14 @@ export type Lazy = /** Возвращает тип свойства `default` в типе {@link T} */ export type InferDefault = T extends { default: infer D } ? D : never; -export type InferLazy = R extends { lazy: infer L } ? +export type IsLazy = R extends { lazy: infer L } ? L extends true ? true : false : false; export type Resolve = R extends keyof S ? NonNullable : R extends Ref ? K extends keyof S ? - Lazy | InferDefault, InferLazy> : + Lazy | InferDefault, IsLazy> : never : never; @@ -161,34 +165,38 @@ export interface Descriptor { /** This flags indicates that this registration can be replaced or overridden. */ readonly configurable?: boolean; - /** If specified signals the activation context that a new service scope - * should be created to isolate service overrides. - */ - readonly hasOverrides?: boolean; - - activate(context: IActivationContext): NonNullable; + activate(context: IActivationContext, stack: ActivationItem[]): NonNullable; } /** The context used to initialize lifetime instance {@linkcode ILifetime} */ export interface ILifetimeContext { - ownerSlot(slotId: string | number): ILifetimeSlot; - contextSlot(slotId: string | number): ILifetimeSlot; - containerSlot(slotId: string | number): ILifetimeSlot; + /** + * + * @param slotId + */ + scopeSlot(level: number, slotId: string | number): ILifetimeSlot; + hierarchySlot(slotId: string | number): ILifetimeSlot; } -export interface IActivationContext extends ILifetimeContext, ServiceLocator { +export interface IActivationContext extends ILifetimeContext { - register(name: K, service: DescriptorMap[K]): void; - - fail(error: unknown): never; + /** + * @param overrides + * @param action + * @throws + */ + withOverrides(overrides: DescriptorMap, action: () => X): X; selfContainer(): ServiceLocator; - createChildContainer(): IContainerBuilder>; + createChildContainer(): IContainerBuilder; + + resolve(name: K, stack: ActivationItem[]): NonNullable; + resolve(name: K, stack: ActivationItem[], def: T): NonNullable | T; } /** @@ -202,7 +210,7 @@ export type DescriptorMap = { [k in keyof S]?: Descriptor; }; -type ContainerKeys = keyof ContainerProvided; +export type ContainerKeys = keyof ContainerProvided; export type ContainerProvided = { container: ServiceLocator>; @@ -251,17 +259,28 @@ export type ActivationType = "singleton" export type ILifetime = (context: ILifetimeContext) => ILifetimeSlot; export interface ILifetimeSlot { - readonly has: () => boolean; + has(): boolean; + + get(): NonNullable; - readonly get: () => T; - - readonly initialize: () => void; + /** + * Initializes the slot, if the slot is already initialized returns false, + * otherwise returns true. This method is used to detect cyclic references. + */ + initialize(): boolean; - readonly store: (item: T, cleanup?: (item: T) => void) => void; + store(item: NonNullable, cleanup?: (item: NonNullable) => void): void; - readonly remove: () => void; + /** + * Removes the slot from the lifetime manager. The inner value will not be + * disposed. If the slot is cleaned up calling this method has no effect. + */ + remove(): void; - readonly cleanup: () => void; + /** + * Disposed inner value of this slot. + */ + cleanup(): void; } export interface ILifetimeManager extends IDestroyable { diff --git a/src/tsconfig.json b/src/tsconfig.json --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -6,7 +6,6 @@ "listFiles": true, "strict": true, "types": [], - "target": "ES2018", - "lib": ["ES2018", "DOM", "ScriptHost"] + "target": "ESNext" } } \ No newline at end of file