ServiceDescriptor.ts
288 lines
| 8.3 KiB
| video/mp2t
|
TypeScriptLexer
|
|
r34 | import { ActivationContext } from "./ActivationContext"; | ||
|
|
r41 | import { Descriptor, ActivationType, ServiceMap, isDescriptor } from "./interfaces"; | ||
|
|
r34 | import { Container } from "./Container"; | ||
|
|
r39 | import { argumentNotNull, isPrimitive, oid, isPromise } from "../safe"; | ||
| import { Constructor, Factory } from "../interfaces"; | ||||
|
|
r41 | import { TraceSource } from "../log/TraceSource"; | ||
|
|
r34 | |||
| let cacheId = 0; | ||||
|
|
r41 | const trace = TraceSource.get("@implab/core/di/ActivationContext"); | ||
|
|
r34 | function injectMethod(target, method, context, args) { | ||
|
|
r38 | const m = target[method]; | ||
|
|
r34 | if (!m) | ||
| throw new Error("Method '" + method + "' not found"); | ||||
| if (args instanceof Array) | ||||
|
|
r39 | return m.apply(target, context.parse(args, "." + method)); | ||
|
|
r34 | else | ||
|
|
r39 | return m.call(target, context.parse(args, "." + method)); | ||
|
|
r38 | } | ||
|
|
r34 | |||
|
|
r39 | function makeClenupCallback(target, method: ((instance) => void) | string) { | ||
|
|
r34 | if (typeof (method) === "string") { | ||
|
|
r38 | return () => { | ||
|
|
r34 | target[method](); | ||
| }; | ||||
| } else { | ||||
|
|
r38 | return () => { | ||
|
|
r34 | method(target); | ||
| }; | ||||
| } | ||||
|
|
r38 | } | ||
|
|
r34 | |||
|
|
r41 | // TODO: make async | ||
| function _parse(value, context: ActivationContext, path: string) { | ||||
| if (isPrimitive(value)) | ||||
| return value; | ||||
|
|
r42 | trace.debug("parse {0}", path); | ||
|
|
r41 | if (isDescriptor(value)) | ||
| return context.activate(value, path); | ||||
| if (value instanceof Array) | ||||
|
|
r42 | return value.map((x, i) => _parse(x, context, `${path}[${i}]`)); | ||
|
|
r41 | |||
| const t = {}; | ||||
| for (const p of Object.keys(value)) | ||||
|
|
r42 | t[p] = _parse(value[p], context, `${path}.${p}`); | ||
|
|
r41 | return t; | ||
| } | ||||
|
|
r39 | export interface ServiceDescriptorParams { | ||
|
|
r38 | activation?: ActivationType; | ||
|
|
r34 | |||
|
|
r38 | owner: Container; | ||
|
|
r34 | |||
|
|
r39 | type?: Constructor; | ||
|
|
r34 | |||
|
|
r39 | factory?: Factory; | ||
|
|
r34 | |||
|
|
r38 | params?; | ||
|
|
r34 | |||
|
|
r39 | inject?: object[]; | ||
|
|
r34 | |||
|
|
r38 | services?: ServiceMap; | ||
|
|
r34 | |||
|
|
r39 | cleanup?: ((x) => void) | string; | ||
|
|
r34 | } | ||
|
|
r39 | export class ServiceDescriptor implements Descriptor { | ||
| _instance; | ||||
|
|
r34 | |||
|
|
r38 | _hasInstance = false; | ||
|
|
r34 | |||
|
|
r38 | _activationType = ActivationType.Call; | ||
|
|
r34 | |||
|
|
r38 | _services: ServiceMap; | ||
|
|
r34 | |||
|
|
r39 | _type: Constructor = null; | ||
|
|
r34 | |||
|
|
r39 | _factory: Factory = null; | ||
|
|
r34 | |||
|
|
r38 | _params; | ||
|
|
r34 | |||
|
|
r39 | _inject: object[]; | ||
|
|
r34 | |||
|
|
r39 | _cleanup: ((x) => void) | string; | ||
|
|
r34 | |||
|
|
r38 | _cacheId: any; | ||
|
|
r34 | |||
|
|
r38 | _owner: Container; | ||
|
|
r34 | |||
|
|
r39 | constructor(opts: ServiceDescriptorParams) { | ||
|
|
r34 | argumentNotNull(opts, "opts"); | ||
| argumentNotNull(opts.owner, "owner"); | ||||
| this._owner = opts.owner; | ||||
| if (!(opts.type || opts.factory)) | ||||
| throw new Error( | ||||
| "Either a type or a factory must be specified"); | ||||
| if (opts.activation) | ||||
| this._activationType = opts.activation; | ||||
| if (opts.type) | ||||
| this._type = opts.type; | ||||
| if (opts.params) | ||||
| this._params = opts.params; | ||||
| if (opts.inject) | ||||
|
|
r39 | this._inject = opts.inject; | ||
|
|
r34 | |||
| if (opts.services) | ||||
| this._services = opts.services; | ||||
| if (opts.factory) | ||||
| this._factory = opts.factory; | ||||
| 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; | ||||
| } | ||||
|
|
r38 | if (this._activationType === ActivationType.Singleton) { | ||
| const tof = this._type || this._factory; | ||||
|
|
r34 | |||
| // create the persistent cache identifier for the type | ||||
| if (isPrimitive(tof)) | ||||
| this._cacheId = tof; | ||||
| else | ||||
| this._cacheId = oid(tof); | ||||
| } else { | ||||
| this._cacheId = ++cacheId; | ||||
| } | ||||
| } | ||||
|
|
r41 | activate(context: ActivationContext) { | ||
|
|
r34 | // if we have a local service records, register them first | ||
| let instance; | ||||
| switch (this._activationType) { | ||||
|
|
r38 | case ActivationType.Singleton: // SINGLETON | ||
|
|
r34 | // if the value is cached return it | ||
| if (this._hasInstance) | ||||
| return this._instance; | ||||
| // singletons are bound to the root container | ||||
|
|
r38 | const container = context.container.getRootContainer(); | ||
|
|
r34 | |||
| if (container.has(this._cacheId)) { | ||||
| instance = container.get(this._cacheId); | ||||
| } else { | ||||
|
|
r41 | instance = this._create(context); | ||
|
|
r34 | container.store(this._cacheId, instance); | ||
| if (this._cleanup) | ||||
| container.onDispose( | ||||
| makeClenupCallback(instance, this._cleanup)); | ||||
| } | ||||
| this._hasInstance = true; | ||||
| return (this._instance = instance); | ||||
|
|
r38 | case ActivationType.Container: // CONTAINER | ||
| // return a cached value | ||||
|
|
r34 | if (this._hasInstance) | ||
| return this._instance; | ||||
| // create an instance | ||||
|
|
r41 | instance = this._create(context); | ||
|
|
r34 | |||
| // 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); | ||||
|
|
r38 | case ActivationType.Context: // CONTEXT | ||
| // return a cached value if one exists | ||||
|
|
r34 | if (context.has(this._cacheId)) | ||
| return context.get(this._cacheId); | ||||
|
|
r38 | // context context activated instances are controlled by callers | ||
|
|
r41 | return context.store(this._cacheId, this._create(context)); | ||
|
|
r38 | case ActivationType.Call: // CALL | ||
|
|
r34 | // per-call created instances are controlled by callers | ||
|
|
r41 | return this._create(context); | ||
|
|
r38 | case ActivationType.Hierarchy: // HIERARCHY | ||
|
|
r34 | // 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); | ||||
|
|
r41 | instance = this._create(context); | ||
|
|
r34 | |||
| if (this._cleanup) | ||||
| context.container.onDispose(makeClenupCallback( | ||||
| instance, | ||||
| this._cleanup)); | ||||
| return context.container.store(this._cacheId, instance); | ||||
| default: | ||||
|
|
r38 | throw new Error("Invalid activation type: " + this._activationType); | ||
|
|
r34 | } | ||
| } | ||||
| isInstanceCreated() { | ||||
| return this._hasInstance; | ||||
| } | ||||
| getInstance() { | ||||
| return this._instance; | ||||
| } | ||||
|
|
r41 | _create(context: ActivationContext) { | ||
| trace.debug(`constructing ${context._name}`); | ||||
|
|
r34 | |||
|
|
r38 | if (this._activationType !== ActivationType.Call && | ||
|
|
r34 | context.visit(this._cacheId) > 0) | ||
| throw new Error("Recursion detected"); | ||||
| if (this._services) { | ||||
|
|
r38 | for (const p in this._services) | ||
|
|
r34 | context.register(p, this._services[p]); | ||
| } | ||||
|
|
r38 | let instance; | ||
|
|
r34 | |||
| if (!this._factory) { | ||||
|
|
r38 | const ctor = this._type; | ||
|
|
r39 | if (this._params && this._params.length) { | ||
| this._factory = (...args) => { | ||||
| const t = Object.create(ctor.prototype); | ||||
| const inst = ctor.apply(t, args); | ||||
| return isPrimitive(inst) ? t : inst; | ||||
| }; | ||||
| } else { | ||||
| this._factory = () => { | ||||
| return new ctor(); | ||||
| }; | ||||
| } | ||||
|
|
r34 | } | ||
| if (this._params === undefined) { | ||||
| instance = this._factory(); | ||||
| } else if (this._params instanceof Array) { | ||||
|
|
r41 | instance = this._factory.apply(this, _parse(this._params, context, "args")); | ||
|
|
r34 | } else { | ||
|
|
r41 | instance = this._factory(_parse(this._params, context, "args")); | ||
|
|
r34 | } | ||
| if (this._inject) { | ||||
|
|
r38 | this._inject.forEach(spec => { | ||
| for (const m in spec) | ||||
|
|
r34 | injectMethod(instance, m, context, spec[m]); | ||
| }); | ||||
| } | ||||
| return instance; | ||||
| } | ||||
| // @constructor {singleton} foo/bar/Baz | ||||
| // @factory {singleton} | ||||
| toString() { | ||||
|
|
r38 | const parts = []; | ||
|
|
r34 | |||
| parts.push(this._type ? "@constructor" : "@factory"); | ||||
| parts.push(ActivationType[this._activationType]); | ||||
| if (typeof (this._type) === "string") | ||||
| parts.push(this._type); | ||||
| return parts.join(" "); | ||||
| } | ||||
|
|
r38 | } | ||
