# HG changeset patch # User cin # Date 2018-12-03 14:27:19 # Node ID d3813a6cdb36199787a91237c49c8205f8a38082 # Parent 4325b0f90ccc99c1ae65ec542ae7617019e4fcb0 working on IoC container diff --git a/.vscode/settings.json b/.vscode/settings.json --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,15 @@ { "java.configuration.updateBuildConfiguration": "disabled", - "tslint.enable": true + "tslint.enable": true, + "search.exclude": { + "**/node_modules": true, + "**/bower_components": true, + "/build": true + }, + "files.watcherExclude": { + "**/.git/objects/**": true, + "**/.git/subtree-cache/**": true, + "**/node_modules/**": true, + "/build": true + } } \ No newline at end of file diff --git a/src/ts/components/ActivatableMixin.ts b/src/ts/components/ActivatableMixin.ts --- a/src/ts/components/ActivatableMixin.ts +++ b/src/ts/components/ActivatableMixin.ts @@ -1,11 +1,11 @@ -import { IActivationController, IActivatable, ICancellation } from '../interfaces'; -import { AsyncComponent } from './AsyncComponent'; -import { Cancellation } from '../Cancellation'; -import { TraceSource } from '../log/TraceSource'; +import { IActivationController, IActivatable, ICancellation } from "../interfaces"; +import { AsyncComponent } from "./AsyncComponent"; +import { Cancellation } from "../Cancellation"; +import { TraceSource } from "../log/TraceSource"; type Constructor = new (...args: any[]) => T; -const log = TraceSource.get('@implab/core/components/ActivatableMixin'); +const log = TraceSource.get("@implab/core/components/ActivatableMixin"); export function ActivatableMixin>(Base: TBase) { return class extends Base implements IActivatable { @@ -77,7 +77,7 @@ export function ActivatableMixin implements Descriptor { - _value: T + _value: T; constructor(value: T) { @@ -10,7 +10,7 @@ export class AggregateDescriptor impl activate(context: ActivationContext, name: string) { context.enter(name); - let v = context.parse(this._value, ".params"); + const v = context.parse(this._value, ".params"); context.leave(); return v; } diff --git a/src/ts/di/Container.ts b/src/ts/di/Container.ts --- a/src/ts/di/Container.ts +++ b/src/ts/di/Container.ts @@ -1,28 +1,26 @@ -declare function require(modules: string[], cb?: (...args: any[]) => any) : void; - -declare function define(name:string, modules: string[], cb?: (...args: any[]) => any) : void; - -import { Uuid } from "../Uuid"; import { ActivationContext } from "./ActivationContext"; import { ValueDescriptor } from "./ValueDescriptor"; import { ActivationError } from "./ActivationError"; -import { isDescriptor, ActivationType } from "./interfaces"; +import { isDescriptor, ActivationType, ServiceMap, isDependencyRegistration, isValueRegistration, ServiceRegistration } from "./interfaces"; import { AggregateDescriptor } from "./AggregateDescriptor"; import { isPrimitive } from "../safe"; import { ReferenceDescriptor } from "./ReferenceDescriptor"; -import { ServiceDescriptor } from "./ServiceDescriptor"; - +import { ServiceDescriptor, ServiceDescriptorParams } from "./ServiceDescriptor"; +import { ModuleResolverBase } from "./ModuleResolverBase"; +import format = require("../text/format"); export class Container { - _services + _services: ServiceMap; - _cache + _cache: object; + + _cleanup: (() => void)[]; - _cleanup: any[] + _root: Container; - _root: Container + _parent: Container; - _parent: Container + _resolver: ModuleResolverBase; constructor(parent?: Container) { this._parent = parent; @@ -42,7 +40,7 @@ export class Container { } getService(name: string, def?: T) { - let d = this._services[name]; + const d = this._services[name]; if (!d) if (arguments.length > 1) return def; @@ -50,21 +48,21 @@ export class Container { throw new Error("Service '" + name + "' isn't found"); if (d.isInstanceCreated()) - return d.getInstance(); + return d.getInstance() as T; - var context = new ActivationContext(this, this._services); + const context = new ActivationContext(this, this._services); try { - return d.activate(context, name); + return d.activate(context, name) as T; } catch (error) { throw new ActivationError(name, context.getStack(), error); } } register(nameOrCollection, service?) { - if (arguments.length == 1) { - var data = nameOrCollection; - for (let name in data) + if (arguments.length === 1) { + const data = nameOrCollection; + for (const name in data) this.register(name, data[name]); } else { if (!(isDescriptor(service))) @@ -82,8 +80,8 @@ export class Container { dispose() { if (this._cleanup) { - for (var i = 0; i < this._cleanup.length; i++) - this._cleanup[i].call(null); + for (const f of this._cleanup) + f(); this._cleanup = null; } } @@ -93,41 +91,18 @@ export class Container { * The configuration of the contaier. Can be either a string or an object, * if the configuration is an object it's treated as a collection of * services which will be registed in the contaier. - * + * * @param{Function} opts.contextRequire * The function which will be used to load a configuration or types for services. - * + * */ - async configure(config, opts) { - var me = this, - contextRequire = (opts && opts.contextRequire); - + async configure(config: string | object, opts?: object) { if (typeof (config) === "string") { - let args; - if (!contextRequire) { - var shim = [config, Uuid()].join(config.indexOf("/") != -1 ? "-" : "/"); - args = await new Promise((resolve, reject) => { - define(shim, ["require", config], function (ctx, data) { - resolve([data, { - contextRequire: ctx - }]); - }) - }); - require([shim]); - } else { - // TODO how to get correct contextRequire for the relative config module? - args = await new Promise((resolve, reject) => { - contextRequire([config], function (data) { - resolve([data, { - contextRequire: contextRequire - }]); - }); - }); - } - - return me._configure.apply(me, args); + const resolver = await this._resolver.createResolver(config, opts); + const data = await this._resolver.loadModule(config); + return this._configure(data, { resolver }); } else { - return me._configure(config, opts); + return this._configure(config); } } @@ -147,109 +122,108 @@ export class Container { return (this._cache[id] = value); } - async _configure(data, opts) { - var typemap = {}, - me = this, - p, - contextRequire = (opts && opts.contextRequire) || require; + async _configure(data: object, opts?: { resolver: ModuleResolverBase }) { + const resolver = (opts && opts.resolver) || this._resolver; + + const services: ServiceMap = {}; - var services = {}; + resolver.beginBatch(); - for (p in data) { - var service = me._parse(data[p], typemap); - if (!(isDescriptor(service))) - service = new AggregateDescriptor(service); - services[p] = service; + async function parse(k) { + services[k] = await this._parse(data[k], resolver); } - me.register(services); + const batch = Object.keys(data).map(parse); - var names = []; - - for (p in typemap) - names.push(p); + resolver.completeBatch(); - return new Promise((resolve, reject) => { - if (names.length) { - contextRequire(names, function () { - for (var i = 0; i < names.length; i++) - typemap[names[i]] = arguments[i]; - resolve(me); - }); - } else { - resolve(me); - } - }); + await Promise.all(batch); + this.register(services); } - _parse(data, typemap) { - if (isPrimitive(data) || isDescriptor(data)) - return data; - if (data.$dependency) { + async _parse(registration: any, resolver: ModuleResolverBase) { + if (isPrimitive(registration) || isDescriptor(registration)) + return registration; + + if (isDependencyRegistration(registration)) { + return new ReferenceDescriptor( - data.$dependency, - data.lazy, - data.optional, - data["default"], - data.services && this._parseObject(data.services, typemap)); - } else if (data.$value) { - return !data.parse ? - new ValueDescriptor(data.$value) : - new AggregateDescriptor(this._parse(data.$value, typemap)); - } else if (data.$type || data.$factory) { - return this._parseService(data, typemap); - } else if (data instanceof Array) { - return this._parseArray(data, typemap); + registration.$dependency, + registration.lazy, + registration.optional, + registration["default"], + registration.services && this._parseObject(registration.services, resolver) + ); + + } else if (isValueRegistration(registration)) { + + return !registration.parse ? + new ValueDescriptor(registration.$value) : + new AggregateDescriptor(this._parse(registration.$value, resolver)); + + } else if (registration.$type || registration.$factory) { + return this._parseService(registration, resolver); + } else if (registration instanceof Array) { + return this._parseArray(registration, resolver); } - return this._parseObject(data, typemap); + return this._parseObject(registration, resolver); } - _parseService(data, typemap) { - var me = this, - opts: any = { - owner: this - }; - if (data.$type) { + async _parseService(data: ServiceRegistration, resolver: ModuleResolverBase) { + const opts: ServiceDescriptorParams = { + owner: this + }; - opts.type = data.$type; - - if (typeof (data.$type) === "string") { - typemap[data.$type] = null; - opts.typeMap = typemap; - } + function guard(fn: () => PromiseLike) { + return fn(); } - if (data.$factory) - opts.factory = data.$factory; + if (data.$type) { + if (data.$type instanceof Function) + opts.type = data.$type; + else if (typeof data.$type === "string") + opts.type = await resolver.resolve(data.$type); + else + throw new Error(format("Unsupported type specification: {0:json}", data.$type)); + } else { + if (data.$factory instanceof Function) + opts.factory = data.$factory; + else if (typeof data.$factory === "string") + opts.factory = await resolver.resolve(data.$factory); + else + throw new Error(format("Unsupported factory specification: {0:json}", data.$factory)); + } if (data.services) - opts.services = me._parseObject(data.services, typemap); - if (data.inject) - opts.inject = data.inject instanceof Array ? data.inject.map(function (x) { - return me._parseObject(x, typemap); - }) : me._parseObject(data.inject, typemap); + opts.services = await this._parseObject(data.services, resolver); + + if (data.inject instanceof Array) + opts.inject = await Promise.all(data.inject.map(x => this._parseObject(x, resolver))); + else + opts.inject = this._parseObject(data.inject, resolver); + if (data.params) - opts.params = me._parse(data.params, typemap); + opts.params = this._parse(data.params, resolver); if (data.activation) { if (typeof (data.activation) === "string") { switch (data.activation.toLowerCase()) { case "singleton": - opts.activation = ActivationType.SINGLETON; + opts.activation = ActivationType.Singleton; break; case "container": - opts.activation = ActivationType.CONTAINER; + opts.activation = ActivationType.Container; break; case "hierarchy": - opts.activation = ActivationType.HIERARCHY; + opts.activation = ActivationType.Hierarchy; break; case "context": - opts.activation = ActivationType.CONTEXT; + opts.activation = ActivationType.Context; break; case "call": - opts.activation = ActivationType.CALL; + opts.activation = ActivationType.Call; break; default: throw new Error("Unknown activation type: " + @@ -266,14 +240,14 @@ export class Container { return new ServiceDescriptor(opts); } - _parseObject(data, typemap) { + _parseObject(data: any, typemap) { if (data.constructor && data.constructor.prototype !== Object.prototype) return new ValueDescriptor(data); - var o = {}; + const o = {}; - for (var p in data) + for (const p in data) o[p] = this._parse(data[p], typemap); return o; @@ -284,9 +258,6 @@ export class Container { data.constructor.prototype !== Array.prototype) return new ValueDescriptor(data); - var me = this; - return data.map(function (x) { - return me._parse(x, typemap); - }); + return data.map(x => this._parse(x, typemap)); } -} \ No newline at end of file +} diff --git a/src/ts/di/ModuleResolverBase.ts b/src/ts/di/ModuleResolverBase.ts --- a/src/ts/di/ModuleResolverBase.ts +++ b/src/ts/di/ModuleResolverBase.ts @@ -2,16 +2,21 @@ import { argumentNotEmptyString, get } f export abstract class ModuleResolverBase { - async resolve(typeName: string) { argumentNotEmptyString(typeName, "typeName"); - let [moduleName, localName] = typeName.split("#", 2); + const [moduleName, localName] = typeName.split("#", 2); - let moduleObject = await this.loadModule(moduleName); + const moduleObject = await this.loadModule(moduleName); return localName ? get(localName, moduleObject) : moduleObject; } - abstract loadModule(moduleName: string): PromiseLike + beginBatch() { + } - abstract createResolver(moduleName: string): PromiseLike -} \ No newline at end of file + completeBatch() { + } + + abstract loadModule(moduleName: string): PromiseLike; + + abstract createResolver(moduleName: string, opts?: object): PromiseLike; +} diff --git a/src/ts/di/RequireJsResolver.ts b/src/ts/di/RequireJsResolver.ts --- a/src/ts/di/RequireJsResolver.ts +++ b/src/ts/di/RequireJsResolver.ts @@ -7,18 +7,18 @@ declare function require(modules: string declare function define(name: string, modules: string[], cb?: (...args: any[]) => any): void; -class RequireJsResolverParams { - contextRequire: (modules: string[], cb?: (...args: any[]) => any) => void +interface RequireJsResolverParams { + contextRequire: (modules: string[], cb?: (...args: any[]) => any) => void; - base: string + base: string; } TraceSource.get("RequireJsResolver"); export class RequireJsResolver extends ModuleResolverBase { - _contextRequire = require + _contextRequire = require; - _base: string + _base: string; constructor(opts) { super(); @@ -29,7 +29,7 @@ export class RequireJsResolver extends M this._contextRequire = opts.contextRequire; if (opts.base) { - if (opts.base.indexOf("./") == 0) + if (opts.base.indexOf("./") === 0) throw new Error(`A module id should be an absolute: '${opts.base}'`); this._base = opts.base; } @@ -40,36 +40,35 @@ export class RequireJsResolver extends M async createResolver(moduleName: string): Promise { argumentNotEmptyString(moduleName, "moduleName"); - let parts = moduleName.split("/"); - if (parts[0] == ".") { + const parts = moduleName.split("/"); + if (parts[0] === ".") { if (this._base) parts[0] = this._base; else throw new Error(`Can't resolve a relative module '${moduleName}'`); } - if(parts.length > 1) - parts.splice(-1,1,Uuid()); + if (parts.length > 1) + parts.splice(-1, 1, Uuid()); else parts.push(Uuid()); - var shim = parts.join('/'); + const shim = parts.join("/"); - let contextRequire = await new Promise((resolve, reject) => { - define(shim, ["require"], function (ctx) { - resolve(ctx); - }) - }); + const contextRequire = await new Promise( + resolve => define(shim, ["require"], resolve) + ); return new RequireJsResolver({ - base: parts.slice(0,-1).join('/'), - contextRequire: contextRequire + base: parts.slice(0, -1).join("/"), + contextRequire }); } - async loadModule(moduleName: string): Promise { - return new Promise((resolve) => this._contextRequire.call(null, [moduleName], resolve) + async loadModule(moduleName: string): Promise { + return new Promise( + resolve => this._contextRequire.call(null, [moduleName], resolve) ); } -} \ No newline at end of file +} diff --git a/src/ts/di/ServiceDescriptor.ts b/src/ts/di/ServiceDescriptor.ts --- a/src/ts/di/ServiceDescriptor.ts +++ b/src/ts/di/ServiceDescriptor.ts @@ -2,11 +2,12 @@ import { ActivationContext } from "./Act import { Descriptor, ActivationType, ServiceMap, Constructor, Factory } from "./interfaces"; import { Container } from "./Container"; import { argumentNotNull, isPrimitive, oid } from "../safe"; +import { ClientResponse } from "http"; let cacheId = 0; function injectMethod(target, method, context, args) { - var m = target[method]; + const m = target[method]; if (!m) throw new Error("Method '" + method + "' not found"); @@ -14,60 +15,60 @@ function injectMethod(target, method, co m.apply(target, context.parse(args, "." + method)); else m.call(target, context.parse(args, "." + method)); -}; +} -function makeClenupCallback(target, method) { +function makeClenupCallback(target, method: (instance) => void | string) { if (typeof (method) === "string") { - return function () { + return () => { target[method](); }; } else { - return function () { + return () => { method(target); }; } -}; +} -export interface ServiceDescriptorParams { - activation: ActivationType +export interface ServiceDescriptorParams { + activation?: ActivationType; - owner: Container + owner: Container; - type: Constructor + type?: Constructor; - factory: Factory + factory?: Factory; - params + params?; - inject + inject?; - services: ServiceMap + services?: ServiceMap; - cleanup: (instance: T) => void + cleanup?: (instance: T) => void | string; } -export class ServiceDescriptor implements Descriptor { - _instance: T = null +export class ServiceDescriptor implements Descriptor { + _instance: T = null; - _hasInstance = false + _hasInstance = false; - _activationType = ActivationType.CALL + _activationType = ActivationType.Call; - _services: ServiceMap + _services: ServiceMap; - _type: Constructor = null + _type: Constructor = null; - _factory: Factory = null + _factory: Factory = null; - _params + _params; - _inject: Array + _inject: Array; - _cleanup: (instance: T) => void + _cleanup: (instance: T) => void; - _cacheId: any + _cacheId: any; - _owner: Container + _owner: Container; constructor(opts: ServiceDescriptorParams) { argumentNotNull(opts, "opts"); @@ -105,8 +106,8 @@ export class ServiceDescriptor implem this._cleanup = opts.cleanup; } - if (this._activationType == ActivationType.SINGLETON) { - let tof = this._type || this._factory; + if (this._activationType === ActivationType.Singleton) { + const tof = this._type || this._factory; // create the persistent cache identifier for the type if (isPrimitive(tof)) @@ -123,13 +124,13 @@ export class ServiceDescriptor implem let instance; switch (this._activationType) { - case ActivationType.SINGLETON: // SINGLETON + case ActivationType.Singleton: // SINGLETON // if the value is cached return it if (this._hasInstance) return this._instance; // singletons are bound to the root container - let container = context.container.getRootContainer(); + const container = context.container.getRootContainer(); if (container.has(this._cacheId)) { instance = container.get(this._cacheId); @@ -144,8 +145,9 @@ export class ServiceDescriptor implem this._hasInstance = true; return (this._instance = instance); - case ActivationType.CONTAINER: // CONTAINER - //return a cached value + case ActivationType.Container: // CONTAINER + // return a cached value + if (this._hasInstance) return this._instance; @@ -160,18 +162,19 @@ export class ServiceDescriptor implem // cache and return the instance this._hasInstance = true; return (this._instance = instance); - case ActivationType.CONTEXT: // CONTEXT - //return a cached value if one exists + case ActivationType.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 + // context context activated instances are controlled by callers return context.store(this._cacheId, this._create( context, name)); - case ActivationType.CALL: // CALL + case ActivationType.Call: // CALL // per-call created instances are controlled by callers return this._create(context, name); - case ActivationType.HIERARCHY: // HIERARCHY + case ActivationType.Hierarchy: // HIERARCHY // hierarchy activated instances are behave much like container activated // except they are created and bound to the child container @@ -188,7 +191,7 @@ export class ServiceDescriptor implem return context.container.store(this._cacheId, instance); default: - throw "Invalid activation type: " + this._activationType; + throw new Error("Invalid activation type: " + this._activationType); } } @@ -203,35 +206,22 @@ export class ServiceDescriptor implem _create(context, name) { context.enter(name, this, Boolean(this._services)); - if (this._activationType != ActivationType.CALL && + if (this._activationType !== ActivationType.Call && context.visit(this._cacheId) > 0) throw new Error("Recursion detected"); if (this._services) { - for (var p in this._services) + for (const p in this._services) context.register(p, this._services[p]); } - var instance; + let instance; if (!this._factory) { - var ctor = this._type; - - if (this._params === undefined) { - this._factory = function () { - return new ctor(); - }; - } else if (this._params instanceof Array) { - this._factory = function () { - var inst = Object.create(ctor.prototype); - var ret = ctor.apply(inst, arguments); - return typeof (ret) === "object" ? ret : inst; - }; - } else { - this._factory = function (param) { - return new ctor(param); - }; - } + const ctor = this._type; + this._factory = (...args) => { + return new ctor(...args); + }; } if (this._params === undefined) { @@ -247,8 +237,8 @@ export class ServiceDescriptor implem } if (this._inject) { - this._inject.forEach(function (spec) { - for (var m in spec) + this._inject.forEach(spec => { + for (const m in spec) injectMethod(instance, m, context, spec[m]); }); } @@ -261,7 +251,7 @@ export class ServiceDescriptor implem // @constructor {singleton} foo/bar/Baz // @factory {singleton} toString() { - var parts = []; + const parts = []; parts.push(this._type ? "@constructor" : "@factory"); @@ -272,4 +262,4 @@ export class ServiceDescriptor implem return parts.join(" "); } -} \ No newline at end of file +} diff --git a/src/ts/di/interfaces.ts b/src/ts/di/interfaces.ts --- a/src/ts/di/interfaces.ts +++ b/src/ts/di/interfaces.ts @@ -3,26 +3,69 @@ import { ActivationContext } from "./Act export interface Descriptor { activate(context: ActivationContext, name?: string); + isInstanceCreated(): boolean; + getInstance(); } export type Constructor = new (...args: any[]) => T; - export type Factory = (...args: any[]) => T; export function isDescriptor(instance): instance is Descriptor { - return (!isNull(instance)) && - ('activate' in instance); + return (!isNull(instance)) && + ("activate" in instance); } export interface ServiceMap { - [s: string] : Descriptor + [s: string]: Descriptor; } export enum ActivationType { - SINGLETON, - CONTAINER, - HIERARCHY, - CONTEXT, - CALL -} \ No newline at end of file + Singleton, + Container, + Hierarchy, + Context, + Call +} + +export interface RegistrationWithServices { + services?: object; +} + +export interface ServiceRegistration extends RegistrationWithServices { + $type?: string | Constructor; + + $factory?: string | Factory; + + activation?: "singleton" | "container" | "hierarchy" | "context" | "call"; + + params?; + + inject?: object | object[]; + + cleanup: (instance) => void | string; +} + +export interface ValueRegistration { + $value; + parse?: boolean; +} + +export interface DependencyRegistration extends RegistrationWithServices { + $dependency: string; + lazy?: boolean; + optional?: boolean; + default?; +} + +export function isServiceRegistration(x): x is ServiceRegistration { + return x && ("$type" in x || "$factory" in x); +} + +export function isValueRegistration(x): x is ValueRegistration { + return x && "$value" in x; +} + +export function isDependencyRegistration(x): x is DependencyRegistration { + return x && "$depdendency" in x; +}