diff --git a/src/main/ts/di/AggregateDescriptor.ts b/src/main/ts/di/AggregateDescriptor.ts --- a/src/main/ts/di/AggregateDescriptor.ts +++ b/src/main/ts/di/AggregateDescriptor.ts @@ -33,4 +33,8 @@ export class AggregateDescriptor implements Resolver { +export class Container implements Resolver, IDestroyable { readonly _services: ContainerServiceMap; readonly _cache: MapOf; @@ -93,6 +93,9 @@ export class Container opts.factory.apply(null, args as any); - if (opts.activation === "singleton") { - this._cacheId = oid(opts.factory); - } } } diff --git a/src/main/ts/di/LifetimeManager.ts b/src/main/ts/di/LifetimeManager.ts new file mode 100644 --- /dev/null +++ b/src/main/ts/di/LifetimeManager.ts @@ -0,0 +1,70 @@ +import { IDestroyable, MapOf } from "../interfaces"; +import { argumentNotNull, isDestroyable } from "../safe"; +import { ILifetimeManager } from "./interfaces"; + +function safeCall(item: () => void) { + try { + item(); + } catch { + // silence + } +} + +export class LifetimeManager implements IDestroyable, ILifetimeManager { + private _cleanup: (() => void)[] = []; + private _cache: MapOf = {}; + private _destroyed = false; + + has(id: string) { + return id in this._cache; + } + + get(id: string) { + const t = this._cache[id]; + if (t === undefined) + throw new Error(`The item with with the key ${id} isn't found`); + return t; + } + + register(id: string, item: any, cleanup?: (item: any) => void) { + argumentNotNull(id, "id"); + argumentNotNull(item, "item"); + if (this.has(id)) + throw new Error(`The item with with the key ${id} already registered with this lifetime manager`); + 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()); + } + } + + destroy() { + if (!this._destroyed) { + this._destroyed = true; + this._cleanup.forEach(safeCall); + this._cleanup.length = 0; + } + } + + static readonly empty: ILifetimeManager = { + has() { + return false; + }, + + get() { + throw new Error("The specified item isn't registered with this lifetime manager"); + }, + + register() { + // does nothing + }, + + destroy() { + throw new Error("Trying to destroy empty lifetime manager, this is a bug."); + } + }; +} diff --git a/src/main/ts/di/ServiceDescriptor.ts b/src/main/ts/di/ServiceDescriptor.ts --- a/src/main/ts/di/ServiceDescriptor.ts +++ b/src/main/ts/di/ServiceDescriptor.ts @@ -1,9 +1,10 @@ import { ActivationContext } from "./ActivationContext"; -import { Descriptor, ServiceMap, PartialServiceMap, ActivationType } from "./interfaces"; -import { Container } from "./Container"; +import { Descriptor, ServiceMap, PartialServiceMap, ActivationType, ILifetimeManager } from "./interfaces"; import { argumentNotNull, isPrimitive, keys, isNull } from "../safe"; import { TraceSource } from "../log/TraceSource"; import { isDescriptor } from "./traits"; +import { LifetimeManager } from "./LifetimeManager"; +import { MatchingMemberKeys } from "../interfaces"; let cacheId = 0; @@ -21,15 +22,14 @@ function injectMethod(target: T, method: Cleaner): () => void; -function makeClenupCallback(target: any, method: any) { - if (typeof (method) === "string") { - return () => { - target[method](); +function makeClenupCallback(method: Cleaner) { + if (typeof (method) === "function") { + return (target: T) => { + method(target); }; } else { - return () => { - method(target); + return (target: T) => { + (target[method] as any)(); }; } } @@ -53,16 +53,14 @@ function _parse(value: any, context: Act return t; } -export type Cleaner = ((x: T) => void) | keyof Extract void }>; +export type Cleaner = ((x: T) => void) | MatchingMemberKeys<() => void, T>; export type InjectionSpec = { [m in keyof T]?: any; }; export interface ServiceDescriptorParams { - activation?: ActivationType; - - owner: Container; + lifetime: ILifetimeManager; params?: P; @@ -74,32 +72,23 @@ export interface ServiceDescriptorParams } export class ServiceDescriptor implements Descriptor { - _instance: T | undefined; - - _hasInstance = false; - - _activationType: ActivationType = "call"; - _services: ServiceMap; _params: P | undefined; _inject: InjectionSpec[]; - _cleanup: Cleaner | undefined; + _cleanup: ((item: T) => void) | undefined; - _cacheId: any; + _cacheId = String(++cacheId); - _owner: Container; + _lifetime = LifetimeManager.empty; constructor(opts: ServiceDescriptorParams) { argumentNotNull(opts, "opts"); - argumentNotNull(opts.owner, "owner"); - this._owner = opts.owner; - - if (!isNull(opts.activation)) - this._activationType = opts.activation; + if (opts.lifetime) + this._lifetime = opts.lifetime; if (!isNull(opts.params)) this._params = opts.params; @@ -113,96 +102,22 @@ export class ServiceDescriptor) { - // if we have a local service records, register them first - let instance: T; - - // ensure we have a cache id - if (!this._cacheId) - this._cacheId = ++cacheId; - - switch (this._activationType) { - case "singleton": // SINGLETON - // if the value is cached return it - if (this._hasInstance) - return this._instance; - - // singletons are bound to the root container - const container = context.container.getRootContainer(); - - if (container.has(this._cacheId)) { - instance = container.get(this._cacheId); - } else { - instance = this._create(context); - container.store(this._cacheId, instance); - if (this._cleanup) - container.onDispose( - makeClenupCallback(instance, this._cleanup)); - } - - this._hasInstance = true; - return (this._instance = instance); - - case "container": // CONTAINER - // return a cached value - - if (this._hasInstance) - return this._instance; - - // create an instance - instance = this._create(context); + const lifetime = this._lifetime.initialize(this._cacheId, context); - // the instance is bound to the container - if (this._cleanup) - this._owner.onDispose( - makeClenupCallback(instance, this._cleanup)); - - // cache and return the instance - this._hasInstance = true; - return (this._instance = instance); - case "context": // CONTEXT - // return a cached value if one exists - - if (context.has(this._cacheId)) - return context.get(this._cacheId); - // context context activated instances are controlled by callers - return context.store(this._cacheId, this._create(context)); - case "call": // CALL - // per-call created instances are controlled by callers - return this._create(context); - case "hierarchy": // HIERARCHY - // hierarchy activated instances are behave much like container activated - // except they are created and bound to the child container - - // return a cached value - if (context.container.has(this._cacheId)) - return context.container.get(this._cacheId); - - instance = this._create(context); - - if (this._cleanup) - context.container.onDispose(makeClenupCallback( - instance, - this._cleanup)); - - return context.container.store(this._cacheId, instance); - default: - throw new Error("Invalid activation type: " + this._activationType); + if (lifetime.has()) { + return lifetime.get(); + } else { + const instance = this._create(context); + lifetime.store(this._cacheId, this._cleanup); + return instance; } } - isInstanceCreated() { - return this._hasInstance; - } - - getInstance() { - return this._instance; - } - _factory(...params: any[]): T { throw Error("Not implemented"); } @@ -210,10 +125,6 @@ export class ServiceDescriptor) { trace.debug(`constructing ${context._name}`); - if (this._activationType !== "call" && - context.visit(this._cacheId) > 0) - throw new Error("Recursion detected"); - if (this._services) { keys(this._services).forEach(p => context.register(p, this._services[p])); } @@ -236,4 +147,9 @@ export class ServiceDescriptor(builder: (t: ServiceRecordBuilder) => void): void; - } export interface RegistrationBuilder { diff --git a/src/main/ts/di/interfaces.ts b/src/main/ts/di/interfaces.ts --- a/src/main/ts/di/interfaces.ts +++ b/src/main/ts/di/interfaces.ts @@ -1,7 +1,10 @@ import { ActivationContext } from "./ActivationContext"; +import { IDestroyable } from "../interfaces"; export interface Descriptor { activate(context: ActivationContext): T; + + clone(): this; } export type ServiceMap = { @@ -36,3 +39,13 @@ export type ContainerRegistered>; export type ActivationType = "singleton" | "container" | "hierarchy" | "context" | "call"; + +export interface ILifetimeManager extends IDestroyable { + initialize(id: string, context: ActivationContext): ILifetime; +} + +export interface ILifetime { + has(): boolean; + get(): any; + store(item: any, cleanup?: (item: any) => void): void; +} \ No newline at end of file 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 @@ -9,6 +9,14 @@ export type Factory = (...args: export type Predicate = (x: T) => boolean; +export type MatchingMemberKeys = { [K in keyof From]: From[K] extends T ? K : never}[keyof From]; + +export type NotMatchingMemberKeys = { [K in keyof From]: From[K] extends T ? never : K}[keyof From]; + +export type ExtractMembers = Pick>; + +export type ExcludeMembers = Pick>; + export interface MapOf { [key: string]: T; } diff --git a/src/main/ts/safe.ts b/src/main/ts/safe.ts --- a/src/main/ts/safe.ts +++ b/src/main/ts/safe.ts @@ -1,4 +1,4 @@ -import { ICancellable, Constructor } from "./interfaces"; +import { ICancellable, Constructor, IDestroyable } from "./interfaces"; import { Cancellation } from "./Cancellation"; let _nextOid = 0; @@ -477,6 +477,12 @@ export function firstWhere( } } +export function isDestroyable(d: any): d is IDestroyable { + if (d && "destroy" in d && typeof(destroy) === "function") + return true; + return false; +} + export function destroy(d: any) { if (d && "destroy" in d) d.destroy();