ServiceDescriptor.ts
239 lines
| 7.2 KiB
| video/mp2t
|
TypeScriptLexer
|
|
r49 | import { ActivationContext } from "./ActivationContext"; | ||
|
|
r118 | import { Descriptor, ServiceMap, PartialServiceMap, ActivationType } from "./interfaces"; | ||
|
|
r49 | import { Container } from "./Container"; | ||
|
|
r118 | import { argumentNotNull, isPrimitive, keys, isNull } from "../safe"; | ||
|
|
r49 | import { TraceSource } from "../log/TraceSource"; | ||
|
|
r118 | import { isDescriptor } from "./traits"; | ||
|
|
r49 | |||
| let cacheId = 0; | ||||
| const trace = TraceSource.get("@implab/core/di/ActivationContext"); | ||||
|
|
r115 | function injectMethod<T, M extends keyof T, S, A>(target: T, method: M, context: ActivationContext<S>, args: A) { | ||
|
|
r49 | const m = target[method]; | ||
|
|
r115 | if (!m || typeof m !== "function") | ||
|
|
r49 | throw new Error("Method '" + method + "' not found"); | ||
| if (args instanceof Array) | ||||
|
|
r65 | return m.apply(target, _parse(args, context, "." + method)); | ||
|
|
r49 | else | ||
|
|
r65 | return m.call(target, _parse(args, context, "." + method)); | ||
|
|
r49 | } | ||
|
|
r115 | function makeClenupCallback<T>(target: T, method: Cleaner<T>): () => void; | ||
| function makeClenupCallback(target: any, method: any) { | ||||
|
|
r49 | if (typeof (method) === "string") { | ||
| return () => { | ||||
| target[method](); | ||||
| }; | ||||
| } else { | ||||
| return () => { | ||||
| method(target); | ||||
| }; | ||||
| } | ||||
| } | ||||
|
|
r118 | function _parse(value: any, context: ActivationContext<any>, path: string): any { | ||
|
|
r49 | if (isPrimitive(value)) | ||
|
|
r115 | return value as any; | ||
|
|
r49 | |||
| trace.debug("parse {0}", path); | ||||
| if (isDescriptor(value)) | ||||
| return context.activate(value, path); | ||||
| if (value instanceof Array) | ||||
|
|
r115 | return value.map((x, i) => _parse(x, context, `${path}[${i}]`)) as any; | ||
|
|
r49 | |||
|
|
r115 | const t: any = {}; | ||
| keys(value).forEach(p => t[p] = _parse(value[p], context, `${path}.${p}`)); | ||||
|
|
r49 | |||
| return t; | ||||
| } | ||||
|
|
r115 | export type Cleaner<T> = ((x: T) => void) | keyof Extract<T, { [M in keyof T]: () => void }>; | ||
| export type InjectionSpec<T> = { | ||||
| [m in keyof T]?: any; | ||||
| }; | ||||
| export interface ServiceDescriptorParams<S, T, P extends any[]> { | ||||
|
|
r49 | activation?: ActivationType; | ||
|
|
r115 | owner: Container<S>; | ||
|
|
r49 | |||
|
|
r115 | params?: P; | ||
|
|
r49 | |||
|
|
r115 | inject?: InjectionSpec<T>[]; | ||
|
|
r49 | |||
|
|
r115 | services?: PartialServiceMap<S>; | ||
|
|
r49 | |||
|
|
r115 | cleanup?: Cleaner<T>; | ||
|
|
r49 | } | ||
|
|
r115 | export class ServiceDescriptor<S, T, P extends any[]> implements Descriptor<S, T> { | ||
| _instance: T | undefined; | ||||
|
|
r49 | |||
| _hasInstance = false; | ||||
|
|
r118 | _activationType: ActivationType = "call"; | ||
|
|
r49 | |||
|
|
r115 | _services: ServiceMap<S>; | ||
|
|
r49 | |||
|
|
r115 | _params: P | undefined; | ||
|
|
r49 | |||
|
|
r115 | _inject: InjectionSpec<T>[]; | ||
|
|
r49 | |||
|
|
r115 | _cleanup: Cleaner<T> | undefined; | ||
|
|
r49 | |||
| _cacheId: any; | ||||
|
|
r115 | _owner: Container<S>; | ||
|
|
r49 | |||
|
|
r115 | constructor(opts: ServiceDescriptorParams<S, T, P>) { | ||
|
|
r49 | argumentNotNull(opts, "opts"); | ||
| argumentNotNull(opts.owner, "owner"); | ||||
| this._owner = opts.owner; | ||||
|
|
r115 | if (!isNull(opts.activation)) | ||
|
|
r49 | this._activationType = opts.activation; | ||
|
|
r115 | if (!isNull(opts.params)) | ||
|
|
r49 | this._params = opts.params; | ||
|
|
r115 | this._inject = opts.inject || []; | ||
|
|
r49 | |||
|
|
r115 | this._services = (opts.services || {}) as ServiceMap<S>; | ||
|
|
r49 | |||
| if (opts.cleanup) { | ||||
| if (!(typeof (opts.cleanup) === "string" || opts.cleanup instanceof Function)) | ||||
| throw new Error( | ||||
| "The cleanup parameter must be either a function or a function name"); | ||||
| this._cleanup = opts.cleanup; | ||||
| } | ||||
| } | ||||
|
|
r115 | activate(context: ActivationContext<S>) { | ||
|
|
r49 | // if we have a local service records, register them first | ||
|
|
r115 | let instance: T; | ||
|
|
r49 | |||
| // ensure we have a cache id | ||||
| if (!this._cacheId) | ||||
| this._cacheId = ++cacheId; | ||||
| switch (this._activationType) { | ||||
|
|
r118 | case "singleton": // SINGLETON | ||
|
|
r49 | // 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); | ||||
|
|
r118 | case "container": // CONTAINER | ||
|
|
r49 | // return a cached value | ||
| if (this._hasInstance) | ||||
| return this._instance; | ||||
| // create an instance | ||||
| instance = this._create(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); | ||||
|
|
r118 | case "context": // CONTEXT | ||
|
|
r49 | // 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)); | ||||
|
|
r118 | case "call": // CALL | ||
|
|
r49 | // per-call created instances are controlled by callers | ||
| return this._create(context); | ||||
|
|
r118 | case "hierarchy": // HIERARCHY | ||
|
|
r49 | // 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); | ||||
| } | ||||
| } | ||||
| isInstanceCreated() { | ||||
| return this._hasInstance; | ||||
| } | ||||
| getInstance() { | ||||
| return this._instance; | ||||
| } | ||||
|
|
r115 | _factory(...params: any[]): T { | ||
|
|
r49 | throw Error("Not implemented"); | ||
| } | ||||
|
|
r115 | _create(context: ActivationContext<S>) { | ||
|
|
r49 | trace.debug(`constructing ${context._name}`); | ||
|
|
r118 | if (this._activationType !== "call" && | ||
|
|
r49 | context.visit(this._cacheId) > 0) | ||
| throw new Error("Recursion detected"); | ||||
| if (this._services) { | ||||
|
|
r115 | keys(this._services).forEach(p => context.register(p, this._services[p])); | ||
|
|
r49 | } | ||
|
|
r115 | let instance: T; | ||
|
|
r49 | |||
| if (this._params === undefined) { | ||||
| instance = this._factory(); | ||||
| } else if (this._params instanceof Array) { | ||||
| instance = this._factory.apply(this, _parse(this._params, context, "args")); | ||||
| } else { | ||||
| instance = this._factory(_parse(this._params, context, "args")); | ||||
| } | ||||
| if (this._inject) { | ||||
| this._inject.forEach(spec => { | ||||
| for (const m in spec) | ||||
| injectMethod(instance, m, context, spec[m]); | ||||
| }); | ||||
| } | ||||
| return instance; | ||||
| } | ||||
| } | ||||
