# HG changeset patch # User cin # Date 2018-12-03 05:58:31 # Node ID bf1098a8d03176adda3cc394f621b23061b007f7 # Parent 58f57bdfc29545d2a2a1befd94034ffdd9b4841a ported IoC container to typescript added di/ModuleResolverBase and di/RequireJsResolver added tslint.json ./safe.ts linted diff --git a/build.gradle b/build.gradle --- a/build.gradle +++ b/build.gradle @@ -57,7 +57,7 @@ task _packageMeta(type: Copy) { } } -task build(dependsOn: [_npmInstall, _buildTs, _legacyJs, _packageMeta]) { +task build(dependsOn: [_legacyJs, _npmInstall, _buildTs, _packageMeta]) { } diff --git a/src/js/di/ActivationContext.js b/src/js/di/ActivationContext.js deleted file mode 100644 --- a/src/js/di/ActivationContext.js +++ /dev/null @@ -1,138 +0,0 @@ -define([ - "../declare", - "../safe", - "./Descriptor", - "./ValueDescriptor", - "../log/trace!" -], function (declare, safe, Descriptor, Value, trace) { - var Context = declare(null, { - - _cache: null, - - _services: null, - - _stack: null, - - _visited: null, - - container: null, - - _trace: true, - - constructor: function (container, services, cache, visited) { - safe.argumentNotNull(container, "container"); - safe.argumentNotNull(services, "services"); - - this._visited = visited || {}; - this._stack = []; - this._cache = cache || {}; - this._services = services; - this.container = container; - }, - - getService: function (name, def) { - var d = this._services[name]; - - if (!d) - if (arguments.length > 1) - return def; - else - throw new Error("Service '" + name + "' not found"); - - return d.activate(this, name); - }, - - /** - * registers services local to the the activation context - * - * @name{string} the name of the service - * @service{string} the service descriptor to register - */ - register: function (name, service) { - safe.argumentNotEmptyString(name, "name"); - - if (!(service instanceof Descriptor)) - service = new Value(service, true); - this._services[name] = service; - }, - - clone: function () { - return new Context( - this.container, - Object.create(this._services), - this._cache, - this._visited - ); - - }, - - has: function (id) { - return id in this._cache; - }, - - get: function (id) { - return this._cache[id]; - }, - - store: function (id, value) { - return (this._cache[id] = value); - }, - - parse: function (data, name) { - var me = this; - if (safe.isPrimitive(data)) - return data; - - if (data instanceof Descriptor) { - return data.activate(this, name); - } else if (data instanceof Array) { - me.enter(name); - var v = data.map(function (x, i) { - return me.parse(x, "." + i); - }); - me.leave(); - return v; - } else { - me.enter(name); - var result = {}; - for (var p in data) - result[p] = me.parse(data[p], "." + p); - me.leave(); - return result; - } - }, - - visit: function (id) { - var count = this._visited[id] || 0; - this._visited[id] = count + 1; - return count; - }, - - getStack: function () { - return this._stack.slice().reverse(); - }, - - enter: function (name, d, localize) { - if (this._trace) - trace.log("enter " + name + " " + (d || "") + - (localize ? " localize" : "")); - this._stack.push({ - name: name, - service: d, - scope: this._services - }); - if (localize) - this._services = Object.create(this._services); - }, - - leave: function () { - var ctx = this._stack.pop(); - this._services = ctx.scope; - - if (this._trace) - trace.log("leave " + ctx.name + " " + (ctx.service || "")); - } - }); - - return Context; -}); \ No newline at end of file diff --git a/src/js/di/ActivationError.js b/src/js/di/ActivationError.js deleted file mode 100644 --- a/src/js/di/ActivationError.js +++ /dev/null @@ -1,39 +0,0 @@ -define([ - "../declare" -], function (declare) { - return declare(null, { - activationStack: null, - - service: null, - - innerException: null, - - message: null, - - constructor: function (service, activationStack, innerException) { - this.message = "Failed to activate the service"; - this.activationStack = activationStack; - this.service = service; - this.innerException = innerException; - }, - - toString: function () { - var parts = [this.message]; - if (this.service) - parts.push("when activating: " + this.service.toString()); - - if (this.innerException) - parts.push("caused by: " + this.innerException.toString()); - - if (this.activationStack) { - parts.push("at"); - this.activationStack.forEach(function (x) { - parts.push(" " + x.name + " " + - (x.service ? x.service.toString() : "")); - }); - } - - return parts.join("\n"); - } - }); -}); \ No newline at end of file diff --git a/src/js/di/Container.js b/src/js/di/Container.js deleted file mode 100644 --- a/src/js/di/Container.js +++ /dev/null @@ -1,299 +0,0 @@ -define([ - "../declare", - "../safe", - "../Uuid", - "../Deferred", - "./ActivationContext", - "./Descriptor", - "./ValueDescriptor", - "./ReferenceDescriptor", - "./ServiceDescriptor", - "./ActivationError" -], function ( - declare, - safe, - Uuid, - Deferred, - ActivationContext, - Descriptor, - Value, - Reference, - Service, - ActivationError) { - var Container = declare(null, { - _services: null, - _cache: null, - _cleanup: null, - _root: null, - _parent: null, - - constructor: function (parent) { - this._parent = parent; - this._services = parent ? Object.create(parent._services) : {}; - this._cache = {}; - this._cleanup = []; - this._root = parent ? parent.getRootContainer() : this; - this._services.container = new Value(this, true); - }, - - getRootContainer: function () { - return this._root; - }, - - getParent: function () { - return this._parent; - }, - - /** - * - */ - getService: function (name, def) { - var d = this._services[name]; - if (!d) - if (arguments.length > 1) - return def; - else - throw new Error("Service '" + name + "' isn't found"); - if (d.isInstanceCreated()) - return d.getInstance(); - - var context = new ActivationContext(this, this._services); - - try { - return d.activate(context, name); - } catch (error) { - throw new ActivationError(name, context.getStack(), error); - } - }, - - register: function (name, service) { - if (arguments.length == 1) { - var data = name; - for (name in data) - this.register(name, data[name]); - } else { - if (!(service instanceof Descriptor)) - service = new Value(service, true); - this._services[name] = service; - } - return this; - }, - - onDispose: function (callback) { - if (!(callback instanceof Function)) - throw new Error("The callback must be a function"); - this._cleanup.push(callback); - }, - - dispose: function () { - if (this._cleanup) { - for (var i = 0; i < this._cleanup.length; i++) - this._cleanup[i].call(null); - this._cleanup = null; - } - }, - - /** - * @param{String|Object} config - * 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. - * - */ - configure: function (config, opts) { - var p, me = this, - contextRequire = (opts && opts.contextRequire); - - if (typeof (config) === "string") { - p = new Deferred(); - if (!contextRequire) { - var shim = [config, Uuid()].join(config.indexOf("/") != -1 ? "-" : "/"); - define(shim, ["require", config], function (ctx, data) { - p.resolve([data, { - contextRequire: ctx - }]); - }); - require([shim]); - } else { - // TODO how to get correct contextRequire for the relative config module? - contextRequire([config], function (data) { - p.resolve([data, { - contextRequire: contextRequire - }]); - }); - } - - return p.then(function (args) { - return me._configure.apply(me, args); - }); - } else { - return me._configure(config, opts); - } - }, - - createChildContainer: function () { - return new Container(this); - }, - - has: function (id) { - return id in this._cache; - }, - - get: function (id) { - return this._cache[id]; - }, - - store: function (id, value) { - return (this._cache[id] = value); - }, - - _configure: function (data, opts) { - var typemap = {}, - d = new Deferred(), - me = this, - p, - contextRequire = (opts && opts.contextRequire) || require; - - var services = {}; - - for (p in data) { - var service = me._parse(data[p], typemap); - if (!(service instanceof Descriptor)) - service = new Value(service, false); - services[p] = service; - } - - me.register(services); - - var names = []; - - for (p in typemap) - names.push(p); - - if (names.length) { - contextRequire(names, function () { - for (var i = 0; i < names.length; i++) - typemap[names[i]] = arguments[i]; - d.resolve(me); - }); - } else { - d.resolve(me); - } - return d.promise; - }, - - _parse: function (data, typemap) { - if (safe.isPrimitive(data) || data instanceof Descriptor) - return data; - if (data.$dependency) - return new Reference( - data.$dependency, - data.lazy, - data.optional, - data["default"], - data.services && this._parseObject(data.services, typemap)); - if (data.$value) { - var raw = !data.parse; - return new Value(raw ? data.$value : this._parse( - data.$value, - typemap), raw); - } - if (data.$type || data.$factory) - return this._parseService(data, typemap); - if (data instanceof Array) - return this._parseArray(data, typemap); - - return this._parseObject(data, typemap); - }, - - _parseService: function (data, typemap) { - var me = this, - opts = { - owner: this - }; - if (data.$type) { - - opts.type = data.$type; - - if (typeof (data.$type) === "string") { - typemap[data.$type] = null; - opts.typeMap = typemap; - } - } - - if (data.$factory) - opts.factory = 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); - if (data.params) - opts.params = me._parse(data.params, typemap); - - if (data.activation) { - if (typeof (data.activation) === "string") { - switch (data.activation.toLowerCase()) { - case "singleton": - opts.activation = Service.SINGLETON; - break; - case "container": - opts.activation = Service.CONTAINER; - break; - case "hierarchy": - opts.activation = Service.HIERARCHY; - break; - case "context": - opts.activation = Service.CONTEXT; - break; - case "call": - opts.activation = Service.CALL; - break; - default: - throw new Error("Unknown activation type: " + - data.activation); - } - } else { - opts.activation = Number(data.activation); - } - } - - if (data.cleanup) - opts.cleanup = data.cleanup; - - return new Service(opts); - }, - - _parseObject: function (data, typemap) { - if (data.constructor && - data.constructor.prototype !== Object.prototype) - return new Value(data, true); - - var o = {}; - - for (var p in data) - o[p] = this._parse(data[p], typemap); - - return o; - }, - - _parseArray: function (data, typemap) { - if (data.constructor && - data.constructor.prototype !== Array.prototype) - return new Value(data, true); - - var me = this; - return data.map(function (x) { - return me._parse(x, typemap); - }); - } - - }); - - return Container; -}); \ No newline at end of file diff --git a/src/js/di/Descriptor.js b/src/js/di/Descriptor.js deleted file mode 100644 --- a/src/js/di/Descriptor.js +++ /dev/null @@ -1,4 +0,0 @@ -define([], function() { - // abstract base type for descriptros - return function() {}; -}); \ No newline at end of file diff --git a/src/js/di/ReferenceDescriptor.js b/src/js/di/ReferenceDescriptor.js deleted file mode 100644 --- a/src/js/di/ReferenceDescriptor.js +++ /dev/null @@ -1,90 +0,0 @@ -define([ - "../declare", "../safe", "./Descriptor", "./ActivationError", "./ValueDescriptor" -], - -function(declare, safe, Descriptor, ActivationError, Value) { - return declare(Descriptor, { - _name : null, - _lazy : false, - _optional : false, - _default : undefined, - - constructor : function(name, lazy, optional, def, services) { - safe.argumentNotEmptyString(name, "name"); - this._name = name; - this._lazy = Boolean(lazy); - this._optional = Boolean(optional); - this._default = def; - this._services = services; - }, - - activate : function(context, name) { - var me = this; - - context.enter(name, this, true); - - // добавляем сервисы - if (me._services) { - for ( var p in me._services) { - var sv = me._services[p]; - context.register(p, sv instanceof Descriptor ? sv : new Value(sv, false)); - } - } - - if (me._lazy) { - // сохраняем контекст активации - context = context.clone(); - return function(cfg) { - // защищаем контекст на случай исключения в процессе - // активации - var ct = context.clone(); - try { - if (cfg) - safe.each(cfg, function(v, k) { - ct.register(k, v instanceof Descriptor ? v : new Value(v, false)); - }); - return me._optional ? ct.getService(me._name, me._default) : ct - .getService(me._name); - } catch (error) { - throw new ActivationError(me._name, ct.getStack(), error); - } - }; - } - - var v = me._optional ? context.getService(me._name, me._default) : context - .getService(me._name); - context.leave(me); - return v; - }, - - isInstanceCreated : function() { - return false; - }, - - toString : function() { - var opts = []; - if (this._optional) - opts.push("optional"); - if (this._lazy) - opts.push("lazy"); - - var parts = [ - "@ref " - ]; - if (opts.length) { - parts.push("{"); - parts.push(opts.join()); - parts.push("} "); - } - - parts.push(this._name); - - if (!safe.isNull(this._default)) { - parts.push(" = "); - parts.push(this._default); - } - - return parts.join(""); - } - }); -}); \ No newline at end of file diff --git a/src/js/di/ServiceDescriptor.js b/src/js/di/ServiceDescriptor.js deleted file mode 100644 --- a/src/js/di/ServiceDescriptor.js +++ /dev/null @@ -1,289 +0,0 @@ -define( - [ - "../declare", - "../safe", - "./Descriptor", - "./ValueDescriptor" - ], - - function (declare, safe, Descriptor, Value) { - var SINGLETON_ACTIVATION = 1, - CONTAINER_ACTIVATION = 2, - CONTEXT_ACTIVATION = 3, - CALL_ACTIVATION = 4, - HIERARCHY_ACTIVATION = 5; - - var injectMethod = function (target, method, context, args) { - var m = target[method]; - if (!m) - throw new Error("Method '" + method + "' not found"); - - if (args instanceof Array) - m.apply(target, context.parse(args, "." + method)); - else - m.call(target, context.parse(args, "." + method)); - }; - - var makeClenupCallback = function (target, method) { - if (typeof (method) === "string") { - return function () { - target[method](); - }; - } else { - return function () { - method(target); - }; - } - }; - - var cacheId = 0; - - var cls = declare( - Descriptor, { - _instance: null, - _hasInstance: false, - _activationType: CALL_ACTIVATION, - _services: null, - _type: null, - _typeMap: null, - _factory: null, - _params: undefined, - _inject: null, - _cleanup: null, - _cacheId: null, - _owner: null, - - constructor: function (opts) { - safe.argumentNotNull(opts, "opts"); - safe.argumentNotNull(opts.owner, "opts.owner"); - - this._owner = opts.owner; - - if (!(opts.type || opts.factory)) - throw new Error( - "Either a type or a factory must be specified"); - - if (typeof (opts.type) === "string" && !opts.typeMap) - throw new Error( - "The typeMap is required when the type is specified by its name"); - - if (opts.activation) - this._activationType = opts.activation; - if (opts.type) - this._type = opts.type; - if (opts.params) - this._params = opts.params; - if (opts.inject) - this._inject = opts.inject instanceof Array ? opts.inject : [opts.inject]; - if (opts.services) - this._services = opts.services; - if (opts.factory) - this._factory = opts.factory; - if (opts.typeMap) - this._typeMap = opts.typeMap; - 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; - } - - this._cacheId = ++cacheId; - }, - - activate: function (context, name) { - - // if we have a local service records, register them first - - var instance; - - switch (this._activationType) { - case 1: // SINGLETON - // if the value is cached return it - if (this._hasInstance) - return this._instance; - - var tof = this._type || this._factory; - - // create the persistent cache identifier for the type - if (safe.isPrimitive(tof)) - this._cacheId = this._type; - else - this._cacheId = safe.oid(tof); - - // singletons are bound to the root container - var container = context.container.getRootContainer(); - - if (container.has(this._cacheId)) { - instance = container.get(this._cacheId); - } else { - instance = this._create(context, name); - container.store(this._cacheId, instance); - if (this._cleanup) - container.onDispose( - makeClenupCallback(instance, this._cleanup)); - } - - this._hasInstance = true; - return (this._instance = instance); - - case 2: // CONTAINER - //return a cached value - if (this._hasInstance) - return this._instance; - - // create an instance - instance = this._create(context, name); - - // 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 3: // 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, - name)); - case 4: // CALL - // per-call created instances are controlled by callers - return this._create(context, name); - case 5: // 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, name); - - if (this._cleanup) - context.container.onDispose(makeClenupCallback( - instance, - this._cleanup)); - - return context.container.store(this._cacheId, instance); - default: - throw "Invalid activation type: " + this._activationType; - } - }, - - isInstanceCreated: function () { - return this._hasInstance; - }, - - getInstance: function () { - return this._instance; - }, - - _create: function (context, name) { - context.enter(name, this, Boolean(this._services)); - - if (this._activationType != CALL_ACTIVATION && - context.visit(this._cacheId) > 0) - throw new Error("Recursion detected"); - - if (this._services) { - for (var p in this._services) { - var sv = this._services[p]; - context.register(p, sv instanceof Descriptor ? sv : new Value(sv, false)); - } - } - - var instance; - - if (!this._factory) { - var ctor, type = this._type; - - if (typeof (type) === "string") { - ctor = this._typeMap[type]; - if (!ctor) - throw new Error("Failed to resolve the type '" + - type + "'"); - } else { - ctor = 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); - }; - } - } - - if (this._params === undefined) { - instance = this._factory(); - } else if (this._params instanceof Array) { - instance = this._factory.apply(this, context.parse( - this._params, - ".params")); - } else { - instance = this._factory(context.parse( - this._params, - ".params")); - } - - if (this._inject) { - this._inject.forEach(function (spec) { - for (var m in spec) - injectMethod(instance, m, context, spec[m]); - }); - } - - context.leave(); - - return instance; - }, - - // @constructor {singleton} foo/bar/Baz - // @factory {singleton} - toString: function () { - var parts = []; - - parts.push(this._type ? "@constructor" : "@factory"); - - parts.push(activationNames[this._activationType]); - - if (typeof (this._type) === "string") - parts.push(this._type); - - return parts.join(" "); - } - - }); - - cls.SINGLETON = SINGLETON_ACTIVATION; - cls.CONTAINER = CONTAINER_ACTIVATION; - cls.CONTEXT = CONTEXT_ACTIVATION; - cls.CALL = CALL_ACTIVATION; - cls.HIERARCHY = HIERARCHY_ACTIVATION; - - var activationNames = [ - "", - "{singleton}", - "{container}", - "{context}", - "{call}", - "{hierarchy}" - ]; - - return cls; - }); \ No newline at end of file diff --git a/src/js/di/ValueDescriptor.js b/src/js/di/ValueDescriptor.js deleted file mode 100644 --- a/src/js/di/ValueDescriptor.js +++ /dev/null @@ -1,38 +0,0 @@ -define([ "../declare", "./Descriptor", "../safe" ], - -function(declare, Descriptor, safe) { - return declare(Descriptor, { - _value : undefined, - _raw : false, - constructor : function(value, raw) { - this._value = value; - this._raw = Boolean(raw); - }, - - activate : function(context, name) { - context.enter(name, this); - var v = this._raw ? this._value : context.parse( - this._value, - ".params"); - context.leave(this); - return v; - }, - - isInstanceCreated : function() { - return this._raw; - }, - - getInstance : function() { - if (!this._raw) - throw new Error("The instance isn't constructed"); - return this._value; - }, - - toString : function() { - if (this._raw) - return "@value {raw}"; - else - return safe.isNull(this._value) ? "@value " : "@value"; - } - }); -}); \ No newline at end of file diff --git a/src/ts/Uuid.ts b/src/ts/Uuid.ts --- a/src/ts/Uuid.ts +++ b/src/ts/Uuid.ts @@ -258,7 +258,7 @@ function _v4(options?, buf?, offset?): s } export function Uuid() { - + return _v4(); } export namespace Uuid { diff --git a/src/ts/di.ts b/src/ts/di.ts --- a/src/ts/di.ts +++ b/src/ts/di.ts @@ -1,126 +0,0 @@ -import { TraceSource } from "./log/TraceSource"; -import { argumentNotNull, argumentNotEmptyString, isPrimitive, each, isNull } from "./safe"; -import { Uuid } from './Uuid'; -import {ActivationContext} from "./di/ActivationContext"; - -let trace = TraceSource.get("di"); - -export interface Descriptor { - activate(context: ActivationContext, name: string): any -} - -export function isDescriptor(value: any): value is Descriptor { - return ("activate" in value); -} - -export interface ServiceMap { - [s: string] : Descriptor -} - -export class ActivationContextInfo { - name: string - - service: string - - scope: ServiceMap -} - -export class ActivationError { - activationStack: ActivationContextInfo[] - - service: string - - innerException: any - - message: string - - constructor(service: string, activationStack: ActivationContextInfo[], innerException) { - this.message = "Failed to activate the service"; - this.activationStack = activationStack; - this.service = service; - this.innerException = innerException; - } - - toString() { - var parts = [this.message]; - if (this.service) - parts.push("when activating: " + this.service.toString()); - - if (this.innerException) - parts.push("caused by: " + this.innerException.toString()); - - if (this.activationStack) { - parts.push("at"); - this.activationStack.forEach(function (x) { - parts.push(" " + x.name + " " + - (x.service ? x.service.toString() : "")); - }); - } - - return parts.join("\n"); - } -} - - -export enum ActivationType { - SINGLETON, - CONTAINER, - HIERARCHY, - CONTEXT, - CALL -} - - - -interface ServiceDescriptorParams { - -} - -class ServiceDescriptor extends Descriptor { - constructor(opts: ServiceDescriptorParams) { - super(); - } - - activate(context: ActivationContext, name: string) { - throw new Error("Method not implemented."); - } - isInstanceCreated(): boolean { - throw new Error("Method not implemented."); - } - getInstance() { - throw new Error("Method not implemented."); - } -} - - - -class AggregateDescriptor extends Descriptor { - constructor(value: T) { - super(); - } - - activate(context: ActivationContext, name: string) { - throw new Error("Method not implemented."); - } - isInstanceCreated(): boolean { - throw new Error("Method not implemented."); - } - getInstance(): T { - throw new Error("Method not implemented."); - } -} - -class ValueDescriptor implements Descriptor { - activate(context: ActivationContext, name: string) { - throw new Error("Method not implemented."); - } - isInstanceCreated(): boolean { - throw new Error("Method not implemented."); - } - getInstance(): T { - throw new Error("Method not implemented."); - } - constructor(value: T) { - - } -} \ No newline at end of file diff --git a/src/ts/di/ActivationContext.ts b/src/ts/di/ActivationContext.ts --- a/src/ts/di/ActivationContext.ts +++ b/src/ts/di/ActivationContext.ts @@ -1,10 +1,18 @@ import { TraceSource } from "../log/TraceSource"; import { argumentNotNull, argumentNotEmptyString, isPrimitive, each, isNull } from "../safe"; -import { Uuid } from '../Uuid'; -import { Container, ActivationContextInfo, ServiceMap, Descriptor, isDescriptor } from "../di"; +import { Descriptor, ServiceMap, isDescriptor } from "./interfaces"; +import { Container } from "./Container"; let trace = TraceSource.get("di"); +export class ActivationContextInfo { + name: string + + service: string + + scope: ServiceMap +} + export class ActivationContext { _cache: object diff --git a/src/ts/di/ActivationError.ts b/src/ts/di/ActivationError.ts new file mode 100644 --- /dev/null +++ b/src/ts/di/ActivationError.ts @@ -0,0 +1,37 @@ +import { ActivationContextInfo } from "./ActivationContext"; + +export class ActivationError { + activationStack: ActivationContextInfo[] + + service: string + + innerException: any + + message: string + + constructor(service: string, activationStack: ActivationContextInfo[], innerException) { + this.message = "Failed to activate the service"; + this.activationStack = activationStack; + this.service = service; + this.innerException = innerException; + } + + toString() { + var parts = [this.message]; + if (this.service) + parts.push("when activating: " + this.service.toString()); + + if (this.innerException) + parts.push("caused by: " + this.innerException.toString()); + + if (this.activationStack) { + parts.push("at"); + this.activationStack.forEach(function (x) { + parts.push(" " + x.name + " " + + (x.service ? x.service.toString() : "")); + }); + } + + return parts.join("\n"); + } +} \ No newline at end of file diff --git a/src/ts/di/AggregateDescriptor.ts b/src/ts/di/AggregateDescriptor.ts new file mode 100644 --- /dev/null +++ b/src/ts/di/AggregateDescriptor.ts @@ -0,0 +1,24 @@ +import { Descriptor } from "./interfaces"; +import { ActivationContext } from "./ActivationContext"; + +export class AggregateDescriptor implements Descriptor { + _value: T + + constructor(value: T) { + + } + + activate(context: ActivationContext, name: string) { + context.enter(name); + let v = context.parse(this._value, ".params"); + context.leave(); + return v; + } + + isInstanceCreated(): boolean { + return false; + } + getInstance(): T { + throw new Error("Not supported exception"); + } +} 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,6 +1,16 @@ +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 { ActivationError } from "../di"; +import { ValueDescriptor } from "./ValueDescriptor"; +import { ActivationError } from "./ActivationError"; +import { isDescriptor, ActivationType } from "./interfaces"; +import { AggregateDescriptor } from "./AggregateDescriptor"; +import { isPrimitive } from "../safe"; +import { ReferenceDescriptor } from "./ReferenceDescriptor"; +import { ServiceDescriptor } from "./ServiceDescriptor"; export class Container { @@ -57,7 +67,7 @@ export class Container { for (let name in data) this.register(name, data[name]); } else { - if (!(service instanceof Descriptor)) + if (!(isDescriptor(service))) service = new ValueDescriptor(service); this._services[nameOrCollection] = service; } @@ -147,7 +157,7 @@ export class Container { for (p in data) { var service = me._parse(data[p], typemap); - if (!(service instanceof Descriptor)) + if (!(isDescriptor(service))) service = new AggregateDescriptor(service); services[p] = service; } @@ -174,7 +184,7 @@ export class Container { } _parse(data, typemap) { - if (isPrimitive(data) || data instanceof Descriptor) + if (isPrimitive(data) || isDescriptor(data)) return data; if (data.$dependency) { return new ReferenceDescriptor( diff --git a/src/ts/di/Descriptor.ts b/src/ts/di/Descriptor.ts deleted file mode 100644 --- a/src/ts/di/Descriptor.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ActivationContext } from "./ActivationContext"; -import { isNull } from "../safe"; - -export interface Descriptor { - activate(context: ActivationContext, name?: string); -} - -export function isDescriptor(instance): instance is Descriptor { - return (!isNull(instance)) && - ('activate' in instance); -} \ No newline at end of file diff --git a/src/ts/di/ModuleResolverBase.ts b/src/ts/di/ModuleResolverBase.ts new file mode 100644 --- /dev/null +++ b/src/ts/di/ModuleResolverBase.ts @@ -0,0 +1,17 @@ +import { argumentNotEmptyString, get } from "../safe"; + +export abstract class ModuleResolverBase { + + + async resolve(typeName: string) { + argumentNotEmptyString(typeName, "typeName"); + let [moduleName, localName] = typeName.split("#", 2); + + let moduleObject = await this.loadModule(moduleName); + return localName ? get(localName, moduleObject) : moduleObject; + } + + abstract loadModule(moduleName: string): PromiseLike + + abstract createResolver(moduleName: string): PromiseLike +} \ No newline at end of file diff --git a/src/ts/di/ReferenceDescriptor.ts b/src/ts/di/ReferenceDescriptor.ts --- a/src/ts/di/ReferenceDescriptor.ts +++ b/src/ts/di/ReferenceDescriptor.ts @@ -1,6 +1,9 @@ import { isNull, argumentNotEmptyString, each } from "../safe"; +import { ActivationContext } from "./ActivationContext"; +import { ServiceMap, Descriptor } from "./interfaces"; +import { ActivationError } from "./ActivationError"; -class ReferenceDescriptor extends Descriptor { +export class ReferenceDescriptor implements Descriptor { _name: string _lazy = false @@ -9,10 +12,9 @@ class ReferenceDescriptor extends Descri _default: any - _services: object + _services: ServiceMap - constructor(name: string, lazy: boolean, optional: boolean, def, services: object) { - super(); + constructor(name: string, lazy: boolean, optional: boolean, def, services: ServiceMap) { argumentNotEmptyString(name, "name"); this._name = name; this._lazy = Boolean(lazy); @@ -30,22 +32,22 @@ class ReferenceDescriptor extends Descri if (me._services) { for (var p in me._services) { var sv = me._services[p]; - context.register(p, sv instanceof Descriptor ? sv : new AggregateDescriptor(sv)); + context.register(p, sv); } } if (me._lazy) { // сохраняем контекст активации context = context.clone(); - return function (cfg) { + return function (cfg: ServiceMap) { // защищаем контекст на случай исключения в процессе // активации var ct = context.clone(); try { if (cfg) - each(cfg, function (v, k) { - ct.register(k, v instanceof Descriptor ? v : new AggregateDescriptor(v)); - }); + for(let k in cfg) + ct.register(k, cfg[v]); + return me._optional ? ct.getService(me._name, me._default) : ct .getService(me._name); } catch (error) { diff --git a/src/ts/di/RequireJsResolver.ts b/src/ts/di/RequireJsResolver.ts new file mode 100644 --- /dev/null +++ b/src/ts/di/RequireJsResolver.ts @@ -0,0 +1,75 @@ +import { ModuleResolverBase } from "./ModuleResolverBase"; +import { Uuid } from "../Uuid"; +import { argumentNotEmptyString } from "../safe"; +import { TraceSource } from "../log/TraceSource"; + +declare function require(modules: string[], cb?: (...args: any[]) => any): void; + +declare function define(name: string, modules: string[], cb?: (...args: any[]) => any): void; + +class RequireJsResolverParams { + contextRequire: (modules: string[], cb?: (...args: any[]) => any) => void + + base: string +} + +TraceSource.get("RequireJsResolver"); + +export class RequireJsResolver extends ModuleResolverBase { + _contextRequire = require + + _base: string + + constructor(opts) { + super(); + + if (opts) { + + if (opts.contextRequire) + this._contextRequire = opts.contextRequire; + + if (opts.base) { + if (opts.base.indexOf("./") == 0) + throw new Error(`A module id should be an absolute: '${opts.base}'`); + this._base = opts.base; + } + } + + } + + async createResolver(moduleName: string): Promise { + argumentNotEmptyString(moduleName, "moduleName"); + + let 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()); + else + parts.push(Uuid()); + + var shim = parts.join('/'); + + let contextRequire = await new Promise((resolve, reject) => { + define(shim, ["require"], function (ctx) { + resolve(ctx); + }) + }); + + return new RequireJsResolver({ + base: parts.slice(0,-1).join('/'), + contextRequire: contextRequire + }); + } + + 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 new file mode 100644 --- /dev/null +++ b/src/ts/di/ServiceDescriptor.ts @@ -0,0 +1,275 @@ +import { ActivationContext } from "./ActivationContext"; +import { Descriptor, ActivationType, ServiceMap, Constructor, Factory } from "./interfaces"; +import { Container } from "./Container"; +import { argumentNotNull, isPrimitive, oid } from "../safe"; + +let cacheId = 0; + +function injectMethod(target, method, context, args) { + var m = target[method]; + if (!m) + throw new Error("Method '" + method + "' not found"); + + if (args instanceof Array) + m.apply(target, context.parse(args, "." + method)); + else + m.call(target, context.parse(args, "." + method)); +}; + +function makeClenupCallback(target, method) { + if (typeof (method) === "string") { + return function () { + target[method](); + }; + } else { + return function () { + method(target); + }; + } +}; + +export interface ServiceDescriptorParams { + activation: ActivationType + + owner: Container + + type: Constructor + + factory: Factory + + params + + inject + + services: ServiceMap + + cleanup: (instance: T) => void +} + +export class ServiceDescriptor implements Descriptor { + _instance: T = null + + _hasInstance = false + + _activationType = ActivationType.CALL + + _services: ServiceMap + + _type: Constructor = null + + _factory: Factory = null + + _params + + _inject: Array + + _cleanup: (instance: T) => void + + _cacheId: any + + _owner: Container + + constructor(opts: ServiceDescriptorParams) { + 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) + this._inject = opts.inject instanceof Array ? opts.inject : [opts.inject]; + + 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; + } + + if (this._activationType == ActivationType.SINGLETON) { + let tof = this._type || this._factory; + + // create the persistent cache identifier for the type + if (isPrimitive(tof)) + this._cacheId = tof; + else + this._cacheId = oid(tof); + } else { + this._cacheId = ++cacheId; + } + } + + activate(context: ActivationContext, name: string) { + // if we have a local service records, register them first + let instance; + + switch (this._activationType) { + 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(); + + if (container.has(this._cacheId)) { + instance = container.get(this._cacheId); + } else { + instance = this._create(context, name); + container.store(this._cacheId, instance); + if (this._cleanup) + container.onDispose( + makeClenupCallback(instance, this._cleanup)); + } + + this._hasInstance = true; + return (this._instance = instance); + + case ActivationType.CONTAINER: // CONTAINER + //return a cached value + if (this._hasInstance) + return this._instance; + + // create an instance + instance = this._create(context, name); + + // 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 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 + return context.store(this._cacheId, this._create( + context, + name)); + case ActivationType.CALL: // CALL + // per-call created instances are controlled by callers + return this._create(context, name); + case ActivationType.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, name); + + if (this._cleanup) + context.container.onDispose(makeClenupCallback( + instance, + this._cleanup)); + + return context.container.store(this._cacheId, instance); + default: + throw "Invalid activation type: " + this._activationType; + } + } + + isInstanceCreated() { + return this._hasInstance; + } + + getInstance() { + return this._instance; + } + + _create(context, name) { + context.enter(name, this, Boolean(this._services)); + + if (this._activationType != ActivationType.CALL && + context.visit(this._cacheId) > 0) + throw new Error("Recursion detected"); + + if (this._services) { + for (var p in this._services) + context.register(p, this._services[p]); + } + + var 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); + }; + } + } + + if (this._params === undefined) { + instance = this._factory(); + } else if (this._params instanceof Array) { + instance = this._factory.apply(this, context.parse( + this._params, + ".params")); + } else { + instance = this._factory(context.parse( + this._params, + ".params")); + } + + if (this._inject) { + this._inject.forEach(function (spec) { + for (var m in spec) + injectMethod(instance, m, context, spec[m]); + }); + } + + context.leave(); + + return instance; + } + + // @constructor {singleton} foo/bar/Baz + // @factory {singleton} + toString() { + var parts = []; + + parts.push(this._type ? "@constructor" : "@factory"); + + parts.push(ActivationType[this._activationType]); + + if (typeof (this._type) === "string") + parts.push(this._type); + + return parts.join(" "); + } +} \ No newline at end of file diff --git a/src/ts/di/ValueDescriptor.ts b/src/ts/di/ValueDescriptor.ts new file mode 100644 --- /dev/null +++ b/src/ts/di/ValueDescriptor.ts @@ -0,0 +1,23 @@ +import { Descriptor } from "./interfaces"; +import { ActivationContext } from "./ActivationContext"; + +export class ValueDescriptor implements Descriptor { + _value: T + + constructor(value: T) { + this._value = value; + } + + activate(context: ActivationContext, name: string) { + context.enter(name); + let v = this._value; + context.leave(); + return v; + } + isInstanceCreated(): boolean { + return true; + } + getInstance(): T { + return this._value; + } +} \ No newline at end of file diff --git a/src/ts/di/interfaces.ts b/src/ts/di/interfaces.ts new file mode 100644 --- /dev/null +++ b/src/ts/di/interfaces.ts @@ -0,0 +1,28 @@ +import { isNull } from "../safe"; +import { ActivationContext } from "./ActivationContext"; + +export interface Descriptor { + activate(context: ActivationContext, name?: string); +} + +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); +} + +export interface ServiceMap { + [s: string] : Descriptor +} + +export enum ActivationType { + SINGLETON, + CONTAINER, + HIERARCHY, + CONTEXT, + CALL +} \ No newline at end of file diff --git a/src/ts/safe.ts b/src/ts/safe.ts --- a/src/ts/safe.ts +++ b/src/ts/safe.ts @@ -1,3 +1,16 @@ +let _nextOid = 0; +const _oid = Symbol("__oid"); + +export function oid(instance: object) { + if (isNull(instance)) + return null; + + if (_oid in instance) + return instance[_oid]; + else + return (instance[_oid] = "oid_" + (++_nextOid)); +} + export function argumentNotNull(arg, name) { if (arg === null || arg === undefined) throw new Error("The argument " + name + " can't be null or undefined"); @@ -28,20 +41,20 @@ export function isPrimitive(arg) { } export function isInteger(arg) { - return parseInt(arg) == arg; + return parseInt(arg, 10) === arg; } export function isNumber(arg) { - return parseFloat(arg) == arg; + return parseFloat(arg) === arg; } export function isString(val) { - return typeof (val) == "string" || val instanceof String; + return typeof (val) === "string" || val instanceof String; } export function isNullOrEmptyString(str) { if (str === null || str === undefined || - ((typeof (str) == "string" || str instanceof String) && str.length === 0)) + ((typeof (str) === "string" || str instanceof String) && str.length === 0)) return true; } @@ -49,11 +62,28 @@ export function isNotEmptyArray(arg): ar return (arg instanceof Array && arg.length > 0); } +export function getGlobal() { + return this; +} + +export function get(member: string, context?: object) { + argumentNotEmptyString(member, "member"); + let that = context || getGlobal(); + const parts = member.split("."); + for (const m of parts) { + if (!m) + continue; + if (isNull(that = that[m])) + break; + } + return that; +} + /** * Выполняет метод для каждого элемента массива, останавливается, когда * либо достигнут конец массива, либо функция cb вернула * значение. - * + * * @param {Array | Object} obj массив элементов для просмотра * @param {Function} cb функция, вызываемая для каждого элемента * @param {Object} thisArg значение, которое будет передано в качестве @@ -63,18 +93,16 @@ export function isNotEmptyArray(arg): ar */ export function each(obj, cb, thisArg?) { argumentNotNull(cb, "cb"); - var i, x; if (obj instanceof Array) { - for (i = 0; i < obj.length; i++) { - x = cb.call(thisArg, obj[i], i); + for (let i = 0; i < obj.length; i++) { + const x = cb.call(thisArg, obj[i], i); if (x !== undefined) return x; } } else { - var keys = Object.keys(obj); - for (i = 0; i < keys.length; i++) { - var k = keys[i]; - x = cb.call(thisArg, obj[k], k); + const keys = Object.keys(obj); + for (const k of keys) { + const x = cb.call(thisArg, obj[k], k); if (x !== undefined) return x; } @@ -83,7 +111,7 @@ export function each(obj, cb, thisArg?) /** Copies property values from a source object to the destination and returns * the destination onject. - * + * * @param dest The destination object into which properties from the source * object will be copied. * @param source The source of values which will be copied to the destination @@ -95,31 +123,27 @@ export function each(obj, cb, thisArg?) * where keys are property names in the source and the values are property * names in the destination object. If the template isn't specified then the * own properties of the source are entirely copied to the destination. - * + * */ -export function mixin(dest: T, source: S, template?: string[] | object) : T & S { +export function mixin(dest: T, source: S, template?: string[] | object): T & S { argumentNotNull(dest, "to"); - let _res = dest; + const _res = dest as T & S; if (template instanceof Array) { - for(let i = 0; i < template.length; i++) { - let p = template[i]; + for (const p of template) { if (p in source) _res[p] = source[p]; } } else if (template) { - let keys = Object.keys(source); - for(let i = 0; i < keys.length; i++) { - let p = keys[i]; + const keys = Object.keys(source); + for (const p of keys) { if (p in template) _res[template[p]] = source[p]; } } else { - let keys = Object.keys(source); - for(let i = 0; i < keys.length; i++) { - let p = keys[i]; + const keys = Object.keys(source); + for (const p of keys) _res[p] = source[p]; - } } return _res; @@ -132,7 +156,7 @@ export function mixin(dest: T, sour export function async(_fn: (...args: any[]) => any, thisArg): (...args: any[]) => PromiseLike { let fn = _fn; - if (arguments.length == 2 && !(fn instanceof Function)) + if (arguments.length === 2 && !(fn instanceof Function)) fn = thisArg[fn]; if (fn == null) @@ -141,7 +165,7 @@ export function async(_fn: (...args: any function wrapresult(x, e?): PromiseLike { if (e) { return { - then: function (cb, eb) { + then(cb, eb) { try { return eb ? wrapresult(eb(e)) : this; } catch (e2) { @@ -153,7 +177,7 @@ export function async(_fn: (...args: any if (x && x.then) return x; return { - then: function (cb) { + then(cb) { try { return cb ? wrapresult(cb(x)) : this; } catch (e2) { @@ -164,16 +188,18 @@ export function async(_fn: (...args: any } } - return function () { + return (...args) => { try { - return wrapresult(fn.apply(thisArg, arguments)); + return wrapresult(fn.apply(thisArg, args)); } catch (e) { return wrapresult(null, e); } }; } -export function delegate(target: T, _method: (K | Function)) { +type _AnyFn = (...args) => any; + +export function delegate(target: T, _method: (K | _AnyFn)) { let method; if (!(_method instanceof Function)) { @@ -185,18 +211,18 @@ export function delegate { + return method.apply(target, args); }; } /** * Для каждого элемента массива вызывает указанную функцию и сохраняет * возвращенное значение в массиве результатов. - * + * * @remarks cb может выполняться асинхронно, при этом одновременно будет * только одна операция. - * + * * @async */ export function pmap(items, cb) { diff --git a/tslint.json b/tslint.json new file mode 100644 --- /dev/null +++ b/tslint.json @@ -0,0 +1,40 @@ +{ + "extends": "tslint:recommended", + "rules": { + "align": [ + true, + "parameters", + "statements" + ], + "interface-name": [false], + "max-line-length": [ true, 185 ], + "member-access": false, + "member-ordering": [ + false, + "variables-before-functions" + ], + "no-bitwise": false, + "no-empty": false, + "no-namespace": false, + "no-string-literal": false, + "ordered-imports": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-whitespace" + ], + "object-literal-sort-keys": false, + "trailing-comma": [ + true, + { + "singleline": "never", + "multiline": "never" + } + ], + "variable-name": false, + "curly": false, + "array-type": false, + "arrow-parens": [true, "ban-single-arg-parens"] + } +} \ No newline at end of file