diff --git a/.vscode/settings.json b/.vscode/settings.json --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,6 @@ { - "java.configuration.updateBuildConfiguration": "automatic" + "java.configuration.updateBuildConfiguration": "automatic", + "cSpell.words": [ + "linkcode" + ] } \ No newline at end of file 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,5 +1,4 @@ -import { Descriptor, ILifetime, RegistrationMap, LifetimeContainer, ConfigurableKeys } from "./interfaces"; -import { LifetimeManager } from "./LifetimeManager"; +import { Descriptor, ILifetime, IActivationContext, DescriptorMap, ILifetimeManager } from "./interfaces"; import { argumentNotNull } from "./traits"; export interface ActivationContextInfo { @@ -11,20 +10,25 @@ export interface ActivationContextInfo { let nextId = 1; -/** This class is created once per `Container.resolve` method call and used to +/** This object is created once per `Container.resolve` method call and used to * cache dependencies and to track created instances. The activation context * tracks services with `context` activation type. + * + * @template S The service map used in the activation context, services from + * this map are available to resolution. + * @template U A set of keys from the service map which can be overridden in + * this activation context. */ -export class ActivationContext { +export class ActivationContext implements IActivationContext { private readonly _cache: Record; - private readonly _services: Partial>; + private readonly _services: DescriptorMap; private readonly _name: string; private readonly _service: Descriptor; - private readonly _containerLifetimeManager: LifetimeManager; + private readonly _containerLifetimeManager: ILifetimeManager; private readonly _parent: ActivationContext | undefined; @@ -36,7 +40,7 @@ export class ActivationContext>, name: string, service: Descriptor, cache = {}) { + constructor(containerLifetimeManager: ILifetimeManager, services: DescriptorMap, name: string, service: Descriptor, cache = {}) { this._name = name; this._service = service; this._cache = cache; @@ -53,19 +57,9 @@ export class ActivationContext(); } - /** Resolves the specified dependency in the current context - * @param name The name of the dependency being resolved - */ resolve(name: K): NonNullable; - /** Resolves the specified dependency with the specified default value if - * the dependency is missing. - * - * @param name The name of the dependency being resolved - * @param def A default value to return in case of the specified dependency - * is missing. - */ resolve(name: K, def: T): NonNullable | T; - resolve(name: K, def?: T): S[K] | T | undefined { + resolve(name: K, def?: T): NonNullable | T | undefined { const d = this._services[name]; if (d !== undefined) { @@ -84,18 +78,27 @@ export class ActivationContext>(name: K, service: RegistrationMap[K]) { + register(name: K, service: DescriptorMap[K]) { argumentNotNull(name, "name"); + const d = this._services[name]; + if (d !== undefined && !d.configurable) + throw new Error(`Service ${String(name)} can't be overridden`); + this._services[name] = service; } createLifetime(): ILifetime { const id = nextId++; return { - initialize() {}, + initialize() { }, has: () => id in this._cache, - get: () => this._cache[id] as T, + 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; } @@ -130,7 +133,9 @@ export class ActivationContext, name: string) { return new ActivationContext( this._containerLifetimeManager, - Object.create(this._services) as typeof this._services, + service.hasOverrides ? + Object.create(this._services) as typeof this._services : + this._services, name, service, this._cache 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 @@ -3,13 +3,32 @@ export interface ActivationItem { service: string; } +/** + * Contains information about the error which occurred during service activation. + * + * Information about activation error includes original exception which has + * occurred, the name of the service being activated and activation stack of + * services. + */ export class ActivationError { + /** + * Stack of services being activating + */ readonly activationStack: ActivationItem[]; + /** + * The name of the failed service + */ readonly service: string; + /** + * The exception which occurred during activation of the service + */ readonly innerException: unknown; + /** + * Error message + */ readonly message: string; constructor(service: string, activationStack: ActivationItem[], innerException: unknown) { 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,33 +1,54 @@ import { ActivationContext } from "./ActivationContext"; import { ActivationError } from "./ActivationError"; import { ContainerBuilder } from "./ContainerBuilder"; -import { RegistrationMap, ServiceContainer, ContainerServices, ServiceLocator, IContainerBuilder, Configurable, ContainerKeys} from "./interfaces"; -import { LifetimeManager } from "./LifetimeManager"; +import { DescriptorMap, IContainerBuilder, IDestroyable, ILifetimeManager, ServiceLocator } from "./interfaces"; -export class Container> implements ServiceContainer { - private readonly _services: Partial>>; +export class Container implements ServiceLocator, IDestroyable { + private readonly _services: DescriptorMap; - private readonly _lifetimeManager: LifetimeManager; + private readonly _lifetimeManager: ILifetimeManager; private _disposed: boolean; - constructor(services: Partial>>, lifetimeManager: LifetimeManager) { - this._services = services; - this._services.container = { activate: () => this as ContainerServices["container"]}; - this._services.childContainer = { activate: () => this.createChildContainer() as ContainerServices["childContainer"] }; + private readonly _onDestroyed: () => void; + + constructor(services: DescriptorMap, lifetimeManager: ILifetimeManager, destroyed: () => void) { + this._services = { + ...services, + container: { + configurable: false, + activate: () => this + }, + childContainer: { + configurable: false, + activate: () => this.createChildBuilder(), + } + }; + this._disposed = false; this._lifetimeManager = lifetimeManager; + this._onDestroyed = destroyed; + } - createChildContainer(): IContainerBuilder { - return new ContainerBuilder(this._services); + private _assertNotDestroyed() { + if (this._disposed) + throw new Error("The container is destroyed"); } - resolve>(name: K): NonNullable[K]>; - resolve, T>(name: K, def: T): NonNullable[K]> | T; - resolve, T>(name: K, def?: T) { - // TODO: add logging - // trace.debug("resolve {0}", name); + createChildBuilder(): IContainerBuilder { + this._assertNotDestroyed(); + + const lifetime = this._lifetimeManager.create(); + + return new ContainerBuilder(this._services, lifetime); + } + + resolve(name: K): NonNullable; + resolve(name: K, def: T): NonNullable | T; + resolve(name: K, def?: T) { + this._assertNotDestroyed(); + const d = this._services[name]; if (d === undefined) { if (arguments.length > 1) @@ -35,7 +56,6 @@ export class Container> implements IContainerBuilder{ +/** + * Container builder used to prepare service descriptors and create a IoC container + */ +export class ContainerBuilder implements + IContainerBuilder { private _pending = 1; - private readonly _services: Partial>>; + private readonly _services: DescriptorMap; private readonly _lifetimeManager = new LifetimeManager(); - constructor(parentServices?: object) { - this._services = Object.create(parentServices ? parentServices : null) as object; + private readonly _lifetime: ILifetime; + + constructor(parentServices?: DescriptorMap, lifetime?: ILifetime) { + this._services = Object.create(parentServices ? parentServices : null) as DescriptorMap; + this._lifetimeManager = new LifetimeManager(); + this._lifetime = lifetime ?? emptyLifetime(); } + createServiceBuilder(name: K): + IDescriptorBuilder, U> { - createServiceBuilder(name: K): IDescriptorBuilder, NonNullable, object, keyof S> { return new DescriptorBuilder(this._lifetimeManager, this._register(name), this._fail); + } - build(): ServiceContainer { + build(): ServiceLocator { this._assertBuilding(); - if(!this._complete()) + if (!this._complete()) throw new Error("The configuration didn't complete."); - return new Container(this._services, this._lifetimeManager); + + const lifetime = this._lifetime; + + const detach = isDestroyable(lifetime) ? () => lifetime.destroy() : () => void (0); + + const container = new Container(this._services, this._lifetimeManager, detach); + lifetime.store(container); + + return container; } - private readonly _register = >(name: K) => (descriptor: Descriptor>) => { - this._complete(); - this._services[name] = descriptor; - }; + private readonly _register = (name: K) => + (descriptor: Descriptor) => { + this._complete(); + this._services[name] = descriptor; + }; private readonly _fail = (ex: unknown) => { - + throw ex; }; private _assertBuilding() { - throw new Error("The descriptor builder is finalized"); + if (!this._pending) + throw new Error("The descriptor builder is finalized"); } private _complete() { 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,28 +1,28 @@ -import { RegistrationBuilder, LifetimeContainer, ConfigurableKeys, IDescriptorBuilder, Ref, Resolved, DepsMap } from "./interfaces"; +import { BuildDescriptorFn, IDescriptorBuilder, DepsMap, Resolve, DescriptorMap } from "./interfaces"; import { Descriptor, ILifetime, ActivationType } from "./interfaces"; -import { DescriptorImpl, RegistrationOverridesMap } from "./DescriptorImpl"; -import { LifetimeManager } from "./LifetimeManager"; -import { each, isKey, isPromise, isString, key, oid } from "./traits"; +import { DescriptorImpl } from "./DescriptorImpl"; +import { contextLifetime, emptyLifetime, hierarchyLifetime, LifetimeManager, singletonLifetime } from "./LifetimeManager"; +import { each, isPromise, isString, key, oid } from "./traits"; /** * @template {S} Карта доступных зависимостей, как правило `ContainerServices` * @template {T} Тип сервиса */ -export class DescriptorBuilder implements IDescriptorBuilder { +export class DescriptorBuilder implements IDescriptorBuilder { private readonly _lifetimeManager: LifetimeManager; private readonly _cb: (d: Descriptor) => void; private readonly _eb: (err: unknown) => void; - private readonly _refs: DepsMap; + private readonly _refs: DepsMap; - private _lifetime = LifetimeManager.empty(); + private _lifetime = emptyLifetime(); - private _overrides: RegistrationOverridesMap; + private _overrides: DescriptorMap; private _cleanup?: (item: T) => void; - private _factory?: (refs: R) => T; + private _factory?: (refs: R) => NonNullable; private _pending = 1; @@ -45,24 +45,20 @@ export class DescriptorBuilder]: keyof S | Ref; }>(refs: X): - IDescriptorBuilder : - X[k] extends Ref ? Resolved : - never; - }, O> { + + /** Declares dependencies to be consumed in the factory method */ + wants & Record>(refs: X): + IDescriptorBuilder; }, U> { each(refs, (v, k) => this._refs[k] = v); return this as IDescriptorBuilder : - X[k] extends Ref ? Resolved : - never; - }, O>; + [k in keyof X]: Resolve; + }, U>; } - factory(f: (refs: R) => T): void { + + /** Registers a factory method for the service */ + factory(f: (refs: R) => NonNullable): void { this._assertBuilding(); this._factory = f; this._finalize(); @@ -79,19 +75,19 @@ export class DescriptorBuilder(name: K, builder: RegistrationBuilder>): this; - override(services: { [k in K]: RegistrationBuilder> }): this; - override(nameOrServices: K | { [name in K]: RegistrationBuilder> }, builder?: RegistrationBuilder>): this { + override(name: K, builder: BuildDescriptorFn, U>): this; + override(services: { [k in K]: BuildDescriptorFn, U> }): this; + override(nameOrServices: K | { [name in K]: BuildDescriptorFn, U> }, builder?: BuildDescriptorFn, U>): this { this._assertBuilding(); const guard = (v: void | Promise) => { if (isPromise(v)) v.catch(err => this._fail(err)); }; - if (isKey(nameOrServices)) { + if (typeof nameOrServices !== "object") { if (builder) { this._defer(); - const d = new DescriptorBuilder, object, O>( + const d = new DescriptorBuilder, object, U>( this._lifetimeManager, result => { this._overrides[nameOrServices] = result; @@ -112,7 +108,13 @@ export class DescriptorBuilder} + * object or {@linkcode ActivationType} literal. + * @param lifetime + */ lifetime(lifetime: ILifetime | Exclude): this; lifetime(lifetime: ILifetime | ActivationType, typeId?: string): this { this._assertBuilding(); @@ -124,13 +126,17 @@ export class DescriptorBuilder void): this { this._assertBuilding(); this._cleanup = cb; return this; } - value(v: T): void { + /** Registers a value as the instance of the service */ + value(v: NonNullable): void { this._assertBuilding(); this._cb({ activate() { @@ -145,19 +151,17 @@ export class DescriptorBuilder({ lifetime: this._lifetime, - factory: this._factory, + factory: this._factory as (refs: Record) => NonNullable, overrides: this._overrides, cleanup: this._cleanup })); 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,32 +1,32 @@ -import { Descriptor, ILifetime, DepsMap, Ref, IActivationContext } from "./interfaces"; -import { each, isKey, key } from "./traits"; +import { Descriptor, ILifetime, DepsMap, IActivationContext, DescriptorMap } from "./interfaces"; +import { each, key } from "./traits"; -export type RegistrationOverridesMap = { [k in keyof S]?: Descriptor> }; - -export interface DescriptorImplArgs { +export interface DescriptorImplArgs { lifetime: ILifetime; - factory: (refs: Record) => T; + factory: (refs: Record) => NonNullable; - cleanup?: (item: T) => void; + cleanup?: (item: NonNullable) => void; - overrides?: RegistrationOverridesMap; + overrides?: DescriptorMap; - dependencies?: DepsMap; + dependencies?: DepsMap; } -export class DescriptorImpl implements Descriptor { +export class DescriptorImpl implements Descriptor { - private readonly _overrides?: RegistrationOverridesMap; + private readonly _overrides?: DescriptorMap; private readonly _lifetime: ILifetime; - private readonly _factory: (refs: Record) => T; + private readonly _factory: (refs: Record) => NonNullable; + + private readonly _cleanup?: (item: NonNullable) => void; - private readonly _cleanup?: (item: T) => void; + private readonly _deps?: DepsMap; - private readonly _deps?: DepsMap; + readonly hasOverrides: boolean; constructor({ lifetime, factory, cleanup, overrides, dependencies }: DescriptorImplArgs) { this._lifetime = lifetime; @@ -37,9 +37,11 @@ export class DescriptorImpl): T { + activate(context: IActivationContext): NonNullable { if (this._lifetime.has()) return this._lifetime.get(); @@ -49,7 +51,7 @@ export class DescriptorImpl context.register(k, v)); - const resolve = ({ name, lazy, ...opts }: Ref) => { + 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 { @@ -61,12 +63,12 @@ export class DescriptorImpl { const ref = deps[k]; - return isKey(ref) ? + return typeof ref !== "object" ? { [k]: resolve({ name: ref }) } : { [k]: resolve(ref) }; }) - .reduce((a, p) => ({ ...a, ...p }), {} ) as Record: - {} as Record; + .reduce((a, p) => ({ ...a, ...p }), {} ): + {}; const instance = this._factory.call(undefined, makeRefs(this._deps)); 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,19 +1,19 @@ -import { DescriptorBuilder } from "./DescriptorBuilder"; -import { ConfigurableKeys, ContainerServices, RegistrationBuildersMap, ExtractRequired, IContainerBuilder } from "./interfaces"; -import { ServiceContainer } from "./interfaces"; +import { ConfigurationMap, ConfigurationMapConstraint, ContainerServices, ContainerServicesConstraint, ExtractRequiredKeys, IContainerBuilder } from "./interfaces"; import { argumentNotNull, each, isKey } from "./traits"; -export class FluentConfiguration = ConfigurableKeys> { +type ContainerExtensionConstraint = ContainerServicesConstraint>; - private _builders: Partial> = {}; +export class FluentConfiguration, Y extends keyof S = keyof S> { + + private _builders: Partial, keyof S, keyof S>> = {}; /** Adds a declaration of the services to the current config. * * @template D The map of the services * @returns self */ - declare>(): FluentConfiguration> { - return this as FluentConfiguration>; + declare>(): FluentConfiguration { + return this as unknown as FluentConfiguration; } /** Adds compile-time information about the already provided services @@ -22,7 +22,7 @@ export class FluentConfiguration>(): FluentConfiguration> { - return this as FluentConfiguration>; + return this as unknown as FluentConfiguration>; } /** Register the service. @@ -31,13 +31,13 @@ export class FluentConfiguration(name: K, builder: RegistrationBuildersMap[K]): FluentConfiguration>; + register(name: K, builder: ConfigurationMap,Y, keyof S>[K]): FluentConfiguration>; /** Registers the collection of services * @param config The collection of services to register. * @returns self */ - register(config: RegistrationBuildersMap): FluentConfiguration>; - register(nameOrConfig: K | RegistrationBuildersMap, builder?: RegistrationBuildersMap[K]) { + register, Y, keyof X>>(config: X): FluentConfiguration>; + register(nameOrConfig: K | ConfigurationMap, K, keyof S>, builder?: ConfigurationMap, Y, keyof S>[K]) { if (isKey(nameOrConfig)) { argumentNotNull(builder, "builder"); this._builders[nameOrConfig] = builder; @@ -45,7 +45,7 @@ export class FluentConfiguration this.register(k, v)); } - return this as FluentConfiguration>; + return this; // as FluentConfiguration>; } /** @@ -57,15 +57,16 @@ export class FluentConfiguration>(missing: M) { + done(...args: ExtractRequiredKeys extends never ? [] : [services: {[ k in ExtractRequiredKeys]: "required"}]) { + //done() { return this; } - configure>(builder: T) { + configure, keyof S>>(builder: C) { each(this._builders, (v, k) => { v(builder.createServiceBuilder(k)); }); - builder.build() as T & ServiceContainer; + return builder.build(); } } 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, IDestroyable, ILifetime } from "./interfaces"; +import { IActivationContext, ILifetime, ILifetimeManager } from "./interfaces"; import { ActivationContext } from "./ActivationContext"; import { argumentNotNull, isDestroyable } from "./traits"; @@ -10,7 +10,7 @@ const safeCall = (item: () => void) => { } }; -const emptyLifetime = Object.freeze({ +const _emptyLifetime = Object.freeze({ has() { return false; }, @@ -32,7 +32,7 @@ const emptyLifetime = Object.freeze({ }); -const unknownLifetime = Object.freeze({ +const _unknownLifetime = Object.freeze({ has() { return false; }, @@ -45,6 +45,7 @@ const unknownLifetime = Object.freeze({ store() { throw new Error("Can't store a value in the unknown lifetime object"); }, + toString() { return `[object UnknownLifetime]`; } @@ -52,25 +53,25 @@ const unknownLifetime = Object.freeze({ let nextId = 0; -const singletons: { [K:string]: unknown} = {}; +const singletons: { [K: string]: unknown } = {}; -export class LifetimeManager implements IDestroyable { +export class LifetimeManager implements ILifetimeManager { private _cleanup: (() => void)[] = []; - private readonly _cache: {[K: string]: unknown} = {}; + private readonly _cache: { [K: string]: unknown } = {}; private _destroyed = false; - private readonly _pending: {[K: string]: unknown} = {}; + private readonly _pending: { [K: string]: unknown } = {}; - create(): ILifetime { + create(): ILifetime & { remove(): void; } { const id = ++nextId; return { has: () => id in this._cache, get: () => { - const t = this._cache[id]; - if (t === undefined) + 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 as T; + return t; }, initialize: () => { @@ -79,23 +80,27 @@ export class LifetimeManager implements this._pending[id] = true; }, - store: (item: T, cleanup?: (item: T) => void) => { - argumentNotNull(id, "id"); - argumentNotNull(item, "item"); - + 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; - if (this._destroyed) - throw new Error("Lifetime manager is destroyed"); if (cleanup) { this._cleanup.push(() => cleanup(item)); } else if (isDestroyable(item)) { this._cleanup.push(() => item.destroy()); } + }, + + remove: () => { + if (this._pending[id]) + throw new Error(`The item '${id}' can't be removed before it has been stored`); + delete this._cache[id]; } }; } @@ -108,104 +113,105 @@ export class LifetimeManager implements } } - static empty(): ILifetime { - return emptyLifetime; - } +} + +export const emptyLifetime = (): ILifetime => { + return _emptyLifetime; +}; - static hierarchyLifetime() { - let _lifetime: ILifetime = unknownLifetime; - return { - initialize(context: IActivationContext) { - if (_lifetime !== unknownLifetime) - throw new Error("Cyclic reference activation detected"); +export const hierarchyLifetime = (): ILifetime => { + let _lifetime: ILifetime = _unknownLifetime; + return { + initialize(context: IActivationContext) { + if (_lifetime !== _unknownLifetime) + throw new Error("Cyclic reference activation detected"); - _lifetime = context.createContainerLifetime(); - }, - get() { - return _lifetime.get(); - }, - has() { - return _lifetime.has(); - }, - store(item: T, cleanup?: (item: T) => void) { - return _lifetime.store(item, cleanup); - }, - toString() { - return `[object HierarchyLifetime, has=${String(this.has())}]`; - } - }; - } - - static contextLifetime() { - let _lifetime: ILifetime = 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: T) { - _lifetime.store(item); - }, - toString() { - return `[object ContextLifetime, has=${String(this.has())}]`; - } - }; - } + _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())}]`; + } + }; +}; - static singletonLifetime(typeId: string) { - 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 T; - }, - initialize() { - if (pending) - throw new Error("Cyclic reference detected"); - pending = true; - }, - store(item: T) { - singletons[typeId] = item; - pending = false; - }, - toString() { - return `[object SingletonLifetime, has=${String(this.has())}, typeId=${typeId}]`; - } - }; - } +export const contextLifetime = (): ILifetime => { + let _lifetime: ILifetime = _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: NonNullable) { + _lifetime.store(item); + }, + toString() { + return `[object ContextLifetime, has=${String(this.has())}]`; + } + }; +}; - static 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: T) { - _lifetime.store(item); - }, - toString() { - return `[object ContainerLifetime, has=${String(_lifetime.has())}]`; - } - }; - } -} +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}]`; + } + }; +}; + +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) { + _lifetime.store(item); + }, + 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 @@ -11,8 +11,8 @@ export interface Resolver { /** * Функция для разрешения зависимостей, поддерживает создание фабричных методов, * отложенную активацию и значение по-умолчанию для сервисов - * @template K Ключ сервиса из {@link S} - * @template O Тип параметра {@link opts} используется для выведения типа + * @template K Ключ сервиса из {@linkcode S} + * @template O Тип параметра {@linkcode opts} используется для выведения типа * возвращаемого значения. * @param name Ключ сервиса, который будет разрешен. * @param {boolean=} opts.lazy Признак того, что требуется отложенная активация, @@ -23,8 +23,8 @@ export interface Resolver { * @returns Либо фабричный метод для получения зависимости, либо значение зависимости * @throws Error Если зависимость не найдена и не предоставлено значение по-умолчанию */ - (name: K, opts?: O): () => (O extends { default: infer T } ? T : never) | NonNullable; - (name: K, opts?: O): (O extends { default: infer T } ? T : never) | NonNullable; + (name: K, opts?: O): () => NonNullable | InferDefault; + (name: K, opts?: O): NonNullable | InferDefault; } export type DepsMap = { @@ -32,26 +32,52 @@ export type DepsMap = { }; export type Refs = { - [k in keyof S]: Ref; + [k in keyof S]: Ref; }[keyof S]; -export type Ref = { name: K, lazy?: L, default?: D | null }; +export type Ref = { + /** The name of the service */ + name: K; + + /** Make a lazy reference, the resolved dependency will be a function */ + lazy?: boolean; + + /** The default value for the case where the service isn't defined. + * When specified the dependency becomes optional, the default value can be + * `null` or `undefined` + */ + default?: D | null +}; export type Lazy = L extends true ? () => T : T; +/** Возвращает тип свойства `default` в типе {@link T} */ export type InferDefault = T extends { default: infer D } ? D : never; +export type InferLazy = 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, L> : + R extends Ref ? + K extends keyof S ? + Lazy | InferDefault, InferLazy> : never : never; /** - * Интерфейс для конфигурирования сервиса в контейнере + * Интерфейс для конфигурирования сервиса в контейнере. Конфигурирование сервиса + * состоит из настройки различных параметров вызовами методов {@linkcode wants}, + * {@linkcode lifetime}, {@linkcode override}, {@linkcode cleanup}. Завершение настройки + * сервиса осуществляется вызовом одного из методов {@linkcode factory} либо + * {@linkcode value}. + * + * @template S Карта сервисов контейнера, доступных при описании дескриптора + * @template T Тип сервиса + * @template R Карта зависимостей, которая передается параметром фабрике + * @template U Имена пользовательских сервисов, доступных для переопределения */ -export interface IDescriptorBuilder { +export interface IDescriptorBuilder { /** Указывает фабрика для создания экземпляра сервиса, фабрика передается * в виде параметра. При вызове фабрике будет передан объект с зависимостями, @@ -59,9 +85,9 @@ export interface IDescriptorBuilder T): void; + factory(f: (refs: R) => NonNullable): void; /** * Используется для указания зависимостей, которые потребуются фабричному @@ -78,10 +104,10 @@ export interface IDescriptorBuilder & Record>(refs: X): IDescriptorBuilder; - }, O> + }, U> - override(name: K, builder: RegistrationBuilder>): this; - override(services: { [name in K]: RegistrationBuilder> }): this; + override(name: K, builder: BuildDescriptorFn): this; + override>(services: X): this; lifetime(lifetime: "singleton", typeId: string | number | object): this; lifetime(lifetime: ILifetime | Exclude): this; @@ -96,49 +122,97 @@ export interface IDescriptorBuilder): void; } -export type RegistrationBuilder = (d: IDescriptorBuilder>) => void; +export type BuildDescriptorFn = (d: IDescriptorBuilder, U>) => void; -export type RegistrationBuildersMap, K extends keyof S = keyof S> = { - [k in K]-?: RegistrationBuilder, NonNullable> +/** + * Конфигурация контейнера, состоит из набора функций, которые выполняют конфигурацию. + * + * Все параметры конфигурации являются обязательными, если требуется ввести + * необязательные параметры, то нужно ограничить параметр типа {@linkcode K} + * + * @template S Сервисы доступные в контейнере + * @template K Сервисы участвующие в конфигурации + */ +export type ConfigurationMap = { + [k in K]-?: BuildDescriptorFn +}; + +export type ConfigurationMapConstraint = { + [k in X]-?: k extends U ? BuildDescriptorFn : never; +}; + +/** + * The type constraint useful to restrict type parameters to prevent defining + * the services with the {@link ContainerKeys} names. + * + * The constraint doesn't exclude using this keys but declares them as `never` + * which effectively will lead using this keys to the error. + */ +export type ContainerServicesConstraint = { + [k in keyof S]: k extends ContainerKeys ? never : S[k]; }; export interface Descriptor { - activate(context: IActivationContext): T; + + /** 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; } export interface IActivationContext extends ServiceLocator { createLifetime(): ILifetime; createContainerLifetime(): ILifetime; -} -export type RegistrationMap = { - [k in K]-?: Descriptor; -}; - -export interface ContainerProvided { - container: ServiceLocator>; - - childContainer: IContainerBuilder; + register(name: K, service: DescriptorMap[K]): void; } -export type Configurable = { [k in keyof S]: k extends ProvidedKeys ? never : S[k]; }; +/** + * Descriptors map for the specified services {@linkcode S}. All entries are + * optional regardless the required or optional services in the original map. + * + * @template S Сервисы контекста активации + * @template U Карта сервисов которые создаются дескрипторами + */ +export type DescriptorMap = { + [k in keyof S]?: Descriptor; +}; -export type ProvidedKeys = keyof ContainerProvided; +type ContainerKeys = keyof ContainerProvided; + +export type ContainerProvided> = { + container: ServiceLocator>; + + childContainer: IContainerBuilder, Exclude>; +}; -export type ContainerServices = - { [k in keyof S as k extends ProvidedKeys ? never: k]: S[k] } & - ContainerProvided; -export type ConfigurableKeys = Exclude; +/** + * Таблица сервисов, которые предоставляет контейнер. + * + * Сервисы, предоставляемые контейнером не могут быть null или undefined. + */ +export type ContainerServices> = { + [k in keyof S | ContainerKeys]: + k extends ContainerKeys ? ContainerProvided[k] : + k extends keyof S ? S[k] : never +}; -export type ConfigurableServices = Pick>; -export type ContainerKeys = keyof S | ProvidedKeys; - +/** + * Returns the service declared in the type map {@link S}. + * + * + */ export interface ServiceLocator { resolve(name: K): NonNullable; resolve(name: K, def: T): NonNullable | T; @@ -148,17 +222,11 @@ export interface LifetimeContainer { createLifetime(): ILifetime; } -export interface ServiceContainer extends - ServiceLocator>, - IDestroyable { +export interface IContainerBuilder { + createServiceBuilder(name: K): + IDescriptorBuilder, U>; - createChildContainer(): IContainerBuilder; -} - -export interface IContainerBuilder { - createServiceBuilder(name: K): IDescriptorBuilder, object, keyof S>; - - build(): ServiceContainer; + build(): ServiceLocator; } @@ -172,12 +240,19 @@ export interface ILifetime { /** Проверяет, что уже создан экземпляр объекта */ has(): boolean; - get(): T; + get(): NonNullable; + + initialize(context: IActivationContext): void; + + store(item: NonNullable, cleanup?: (item: NonNullable) => void): void; - initialize(context: IActivationContext): void; + toString(): string; +} - store(item: T, cleanup?: (item: T) => void): void; +export interface ILifetimeManager extends IDestroyable { + create(): ILifetime; } export type ExtractRequired = { [p in K as (undefined extends T[p] ? never : p)]-?: T[p] }; +export type ExtractRequiredKeys = { [p in K]-?: undefined extends T[p] ? never : p }[K]; \ 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,7 +1,7 @@ import { FluentConfiguration } from "./FluentConfiguration"; -import { IDestroyable } from "./interfaces"; +import { ContainerServices, ContainerServicesConstraint, IDestroyable } from "./interfaces"; -export function fluent() { +export function fluent>() { return new FluentConfiguration(); } diff --git a/src/test/ts/t/container.ts b/src/test/ts/t/container.ts --- a/src/test/ts/t/container.ts +++ b/src/test/ts/t/container.ts @@ -2,7 +2,7 @@ import { describe, it } from "mocha"; import { Container } from "../Container"; import { ContainerBuilder } from "../ContainerBuilder"; -import { ConfigurableKeys, ContainerProvided, ContainerServices, DepsMap, Refs, Resolver } from "../interfaces"; +import { ContainerServices, DepsMap, IContainerBuilder, Refs, Resolver } from "../interfaces"; import { fluent } from "../traits"; class Foo { @@ -22,7 +22,9 @@ interface Services { baz: Foo; - container: string; + box?: Foo; + + //container: string; } interface ServicesB { @@ -36,9 +38,9 @@ interface ServicesB { declare const resolver: Resolver; -const foo = resolver("foo", {lazy: true}); +const foo = resolver("foo", { lazy: true, default: null }); -const mmap = >(m: X) => {}; +const mmap = >(m: X) => { }; declare const refs: Refs; @@ -52,8 +54,8 @@ x.container.resolve("container"); mmap({ - fooz: {name: "foo", lazy: false, default: undefined }, - ooz: "bar" + fooz: { name: "foo", lazy: false, default: undefined }, + ooz: "bar" }); interface SharedServices { @@ -64,37 +66,42 @@ interface SharedServices { baz: Bar; } -const config = fluent() - .declare() +const config = fluent() .declare() .register({ - zoo: it => {}, + zoo: it => it.value(new Foo()), bar: it => it .lifetime("context") // тип активации, время жизни .wants({ - zoo: "zoo", // зависимость + self: "container", + childContainer: "childContainer", bar: "bar", - - $zoo: { name: "foo", lazy: true, } // отложенная активация, - //фабричный метод + foo$: { name: "foo", lazy: true } // отложенная активация, фабричный метод }) - .wants({ - zoom: "bar" + .override({ // переопределение сервиса + box: it => it.factory(() => new Foo()) + }) - .factory(({ $zoo, zoo }) => // фабрика получает объект с именованными зависимостями - // удобно для деструктурирования - new Bar($zoo) // создается экземпляр сервиса + .factory(({ foo$, bar, self, childContainer }) => // фабрика получает объект с именованными зависимостями + new Bar(foo$) // создается экземпляр сервиса ), foo: it => it.factory(() => new Foo()), - baz: it => it.value(new Foo()) + baz: it => it.value(new Foo()), + //box: it => it.factory(() => new Foo()) }) - .done({}); + .done(); + +declare const container: IContainerBuilder, keyof Services>; -declare const container: ContainerBuilder<{}>; +const v = container.build().resolve("foo"); +if (v) { + // noop +} + const c2 = config.configure(container); c2.resolve("foo"); -declare const m :ContainerServices<{foo: Foo}>["container"]; +declare const m: ContainerServices<{ foo?: Foo }>["container"]; m.resolve("container").resolve("container").resolve("foo"); \ No newline at end of file