diff --git a/src/main/ts/di/ActivationContext.ts b/src/main/ts/di/ActivationContext.ts --- a/src/main/ts/di/ActivationContext.ts +++ b/src/main/ts/di/ActivationContext.ts @@ -2,37 +2,38 @@ import { TraceSource } from "../log/Trac import { argumentNotNull, argumentNotEmptyString, isPrimitive, each, isNull } from "../safe"; import { Descriptor, ServiceMap } from "./interfaces"; import { Container } from "./Container"; +import { MapOf } from "../interfaces"; const trace = TraceSource.get("@implab/core/di/ActivationContext"); -export interface ActivationContextInfo { +export interface ActivationContextInfo { name: string; service: string; - scope: ServiceMap; + scope: ServiceMap; } -export class ActivationContext { - _cache: object; +export class ActivationContext { + _cache: MapOf; - _services: ServiceMap; + _services: ServiceMap; - _stack: ActivationContextInfo[]; + _stack: ActivationContextInfo[]; - _visited: object; + _visited: MapOf; _name: string; - _localized: boolean; + _localized: boolean = false; - container: Container; + container: Container; - constructor(container: Container, services: ServiceMap, name?: string, cache?: object, visited?) { + constructor(container: Container, services: ServiceMap, name?: string, cache?: object, visited?: MapOf) { argumentNotNull(container, "container"); argumentNotNull(services, "services"); - this._name = name; + this._name = name || ""; this._visited = visited || {}; this._stack = []; this._cache = cache || {}; @@ -44,16 +45,17 @@ export class ActivationContext { return this._name; } - resolve(name, def?): any { + resolve(name: K, def?: T) { const d = this._services[name]; - if (!d) - if (arguments.length > 1) + if (d !== undefined) { + return this.activate(d as Descriptor, name.toString()); + } else { + if (def !== undefined && def !== null) return def; else throw new Error(`Service ${name} not found`); - - return this.activate(d, name); + } } /** @@ -62,7 +64,7 @@ export class ActivationContext { * @name{string} the name of the service * @service{string} the service descriptor to register */ - register(name: string, service: Descriptor) { + register(name: K, service: Descriptor) { argumentNotEmptyString(name, "name"); this._services[name] = service; @@ -82,20 +84,20 @@ export class ActivationContext { return id in this._cache; } - get(id: string) { + get(id: string) { return this._cache[id]; } - store(id: string, value) { + store(id: string, value: any) { return (this._cache[id] = value); } - activate(d: Descriptor, name: string) { + activate(d: Descriptor, name: string) { if (trace.isLogEnabled()) trace.log(`enter ${name} ${d}`); this.enter(name, d.toString()); - const v = d.activate(this); + const v = d.activate(this); this.leave(); if (trace.isLogEnabled()) @@ -126,7 +128,11 @@ export class ActivationContext { private leave() { const ctx = this._stack.pop(); - this._services = ctx.scope; - this._name = ctx.name; + if (ctx) { + this._services = ctx.scope; + this._name = ctx.name; + } else { + trace.error("Trying to leave the last activation scope"); + } } } diff --git a/src/main/ts/di/ActivationError.ts b/src/main/ts/di/ActivationError.ts --- a/src/main/ts/di/ActivationError.ts +++ b/src/main/ts/di/ActivationError.ts @@ -1,7 +1,10 @@ -import { ActivationContextInfo } from "./ActivationContext"; +export interface ActivationItem { + name: string; + service: string; +} export class ActivationError { - activationStack: ActivationContextInfo[]; + activationStack: ActivationItem[]; service: string; @@ -9,7 +12,7 @@ export class ActivationError { message: string; - constructor(service: string, activationStack: ActivationContextInfo[], innerException: any) { + constructor(service: string, activationStack: ActivationItem[], innerException: any) { this.message = "Failed to activate the service"; this.activationStack = activationStack; this.service = service; 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 @@ -2,33 +2,35 @@ import { Descriptor, isDescriptor } from import { ActivationContext } from "./ActivationContext"; import { isPrimitive } from "../safe"; -export class AggregateDescriptor implements Descriptor { +type Parse = T extends Descriptor ? V : + T extends {} ? { [K in keyof T]: Parse } : + T; + +export class AggregateDescriptor implements Descriptor> { _value: T; constructor(value: T) { this._value = value; } - activate(context: ActivationContext) { + activate(context: ActivationContext) { return this._parse(this._value, context, "$value"); } - // TODO: make async - _parse(value: T, context: ActivationContext, path: string) { + _parse(value: V, context: ActivationContext, path: string): Parse { if (isPrimitive(value)) - return value; + return value as any; if (isDescriptor(value)) return context.activate(value, path); if (value instanceof Array) - return value.map((x, i) => this._parse(x, context, `${path}[${i}]`)); + return value.map((x, i) => this._parse(x, context, `${path}[${i}]`)) as any; - const t = {}; - for (const p of Object.keys(value)) + const t: any = {}; + for (const p in value) t[p] = this._parse(value[p], context, `${path}.${p}`); return t; - } toString() { diff --git a/src/main/ts/di/Annotations.ts b/src/main/ts/di/Annotations.ts --- a/src/main/ts/di/Annotations.ts +++ b/src/main/ts/di/Annotations.ts @@ -4,22 +4,40 @@ export interface InjectOptions { lazy?: boolean; } +interface Dependency { + $dependency: K; + + lazy?: boolean; +} + +interface Lazy extends Dependency { + lazy: true; +} + type Setter = (v: T) => void; type Compatible = T1 extends T2 ? any : never; -type SetterType = T extends (v: infer V) => void ? V : never; - type ExtractService = K extends keyof S ? S[K] : K; type ExtractDependency = D extends { $dependency: infer K } ? D extends { lazy: true } ? () => ExtractService : ExtractService : VisitDependency; type VisitDependency = D extends {} ? { [K in keyof D]: ExtractDependency } : D; +interface Config { + dependency(name: K): Dependency; + + lazy(name: K): Lazy; + + build(): Builder; +} + +export declare function services(): Config; + export class Builder { consume

(...args: P) { return ) => T>(constructor: C) => { - return constructor as typeof constructor & { service: () => T }; + return constructor; }; } @@ -39,3 +57,5 @@ export class Builder { } } + + diff --git a/src/main/ts/di/ConfigError.ts b/src/main/ts/di/ConfigError.ts --- a/src/main/ts/di/ConfigError.ts +++ b/src/main/ts/di/ConfigError.ts @@ -1,11 +1,11 @@ export class ConfigError extends Error { - inner: any; + inner?: {}; - path: string; + path?: string; - configName: string; + configName?: string; - constructor(message: string, inner?: any) { + constructor(message: string, inner?: {}) { super(message); this.inner = inner; } diff --git a/src/main/ts/di/Configuration.ts b/src/main/ts/di/Configuration.ts --- a/src/main/ts/di/Configuration.ts +++ b/src/main/ts/di/Configuration.ts @@ -28,7 +28,7 @@ import { ICancellation } from "../interf const trace = TraceSource.get("@implab/core/di/Configuration"); -async function mapAll(data: any | any[], map?: (v: any, k: keyof any) => any): Promise { +async function mapAll(data: any | any[], map?: (v: any, k: number | string) => any): Promise { if (data instanceof Array) { return Promise.all(map ? data.map(map) : data); } else { @@ -47,21 +47,19 @@ async function mapAll(data: any | any[], export type ModuleResolver = (moduleName: string, ct?: ICancellation) => any; -type _key = string | number; - -export class Configuration { +export class Configuration { _hasInnerDescriptors = false; - _container: Container; + _container: Container; - _path: Array<_key>; + _path: Array; _configName: string | undefined; _require: ModuleResolver | undefined; - constructor(container: Container) { + constructor(container: Container) { argumentNotNull(container, "container"); this._container = container; this._path = []; @@ -150,7 +148,7 @@ export class Configuration { return this._require(moduleName); } - async _visitRegistrations(data: any, name: _key) { + async _visitRegistrations(data: any, name: string) { this._enter(name); if (data.constructor && @@ -161,7 +159,7 @@ export class Configuration { const keys = Object.keys(data); const services = await mapAll(data, async (v, k) => { - const d = await this._visit(v, k); + const d = await this._visit(v, k.toString()); return isDescriptor(d) ? d : new AggregateDescriptor(d); }) as ServiceMap; @@ -180,11 +178,11 @@ export class Configuration { trace.debug("<{0}", name); } - async _visit(data: T, name: keyof T) { + async _visit(data: T, name: string) { if (isPrimitive(data) || isDescriptor(data)) return data; - if (isDependencyRegistration(data)) { + if (isDependencyRegistration(data)) { return this._visitDependencyRegistration(data, name); } else if (isValueRegistration(data)) { return this._visitValueRegistration(data, name); @@ -196,10 +194,10 @@ export class Configuration { return this._visitArray(data, name); } - return this._visitObject(data, name); + return this._visitObject(data as T & object, name); } - async _visitObject(data: object, name: _key) { + async _visitObject(data: T, name: string) { if (data.constructor && data.constructor.prototype !== Object.prototype) return new ValueDescriptor(data); @@ -222,7 +220,7 @@ export class Configuration { return v; } - async _visitArray(data: any[], name: _key) { + async _visitArray(data: any[], name: string) { if (data.constructor && data.constructor.prototype !== Array.prototype) return new ValueDescriptor(data); @@ -235,7 +233,7 @@ export class Configuration { return v; } - _makeServiceParams(data: ServiceRegistration) { + _makeServiceParams(data: ServiceRegistration) { const opts: any = { owner: this._container }; @@ -291,14 +289,14 @@ export class Configuration { return opts; } - async _visitValueRegistration(data: ValueRegistration, name: _key) { + async _visitValueRegistration(data: ValueRegistration, name: string) { this._enter(name); const d = data.parse ? new AggregateDescriptor(data.$value) : new ValueDescriptor(data.$value); this._leave(); return d; } - async _visitDependencyRegistration(data: DependencyRegistration, name: keyof S) { + async _visitDependencyRegistration(data: DependencyRegistration, name: string) { argumentNotEmptyString(data && data.$dependency, "data.$dependency"); this._enter(name); const d = new ReferenceDescriptor({ @@ -312,7 +310,7 @@ export class Configuration { return d; } - async _visitTypeRegistration(data: TypeRegistration, name: _key) { + async _visitTypeRegistration(data: TypeRegistration, name: string) { argumentNotNull(data.$type, "data.$type"); this._enter(name); @@ -333,7 +331,7 @@ export class Configuration { return d; } - async _visitFactoryRegistration(data: FactoryRegistration, name: _key) { + async _visitFactoryRegistration(data: FactoryRegistration, name: string) { argumentOfType(data.$factory, Function, "data.$factory"); this._enter(name); diff --git a/src/main/ts/di/Container.ts b/src/main/ts/di/Container.ts --- a/src/main/ts/di/Container.ts +++ b/src/main/ts/di/Container.ts @@ -1,31 +1,35 @@ import { ActivationContext } from "./ActivationContext"; import { ValueDescriptor } from "./ValueDescriptor"; import { ActivationError } from "./ActivationError"; -import { isDescriptor, ServiceMap } from "./interfaces"; +import { isDescriptor, ServiceMap, Descriptor } from "./interfaces"; import { TraceSource } from "../log/TraceSource"; import { Configuration } from "./Configuration"; import { Cancellation } from "../Cancellation"; +import { MapOf } from "../interfaces"; const trace = TraceSource.get("@implab/core/di/ActivationContext"); -export class Container { - _services: ServiceMap; +export class Container }> { + readonly _services: ServiceMap; - _cache: object; + readonly _cache: MapOf; + + readonly _cleanup: (() => void)[]; - _cleanup: (() => void)[]; + readonly _root: Container; - _root: Container; + readonly _parent?: Container; - _parent: Container; + _disposed: boolean; - constructor(parent?: Container) { + constructor(parent?: Container) { this._parent = parent; this._services = parent ? Object.create(parent._services) : {}; this._cache = {}; this._cleanup = []; this._root = parent ? parent.getRootContainer() : this; this._services.container = new ValueDescriptor(this); + this._disposed = false; } getRootContainer() { @@ -36,57 +40,63 @@ export class Container { return this._parent; } - resolve(name: string, def?) { + resolve(name: K, def?: T) { trace.debug("resolve {0}", name); const d = this._services[name]; if (d === undefined) { - if (arguments.length > 1) + if (def !== undefined) return def; else throw new Error("Service '" + name + "' isn't found"); - } + } else { - const context = new ActivationContext(this, this._services); - try { - return context.activate(d, name); - } catch (error) { - throw new ActivationError(name, context.getStack(), error); + const context = new ActivationContext(this, this._services); + try { + return context.activate(d as Descriptor, name.toString()); + } catch (error) { + throw new ActivationError(name.toString(), context.getStack(), error); + } } } /** * @deprecated use resolve() method */ - getService() { - return this.resolve.apply(this, arguments); + getService(name: K, def?: S[K]) { + return this.resolve(name, def); } - register(nameOrCollection, service?) { + register(name: K, service: Descriptor): this; + register(services: ServiceMap): this; + register(nameOrCollection: K | ServiceMap, service?: Descriptor) { if (arguments.length === 1) { - const data = nameOrCollection; - for (const name in data) - this.register(name, data[name]); + const data = nameOrCollection as ServiceMap; + for (const name in data) { + if (Object.prototype.hasOwnProperty.call(data, name)) { + this.register(name, data[name] as Descriptor); + } + } } else { if (!isDescriptor(service)) throw new Error("The service parameter must be a descriptor"); - this._services[nameOrCollection] = service; + this._services[nameOrCollection as K] = service; } return this; } - onDispose(callback) { + onDispose(callback: () => void) { if (!(callback instanceof Function)) throw new Error("The callback must be a function"); this._cleanup.push(callback); } dispose() { - if (this._cleanup) { - for (const f of this._cleanup) - f(); - this._cleanup = null; - } + if (this._disposed) + return; + this._disposed = true; + for (const f of this._cleanup) + f(); } /** @@ -109,8 +119,8 @@ export class Container { } } - createChildContainer() { - return new Container(this); + createChildContainer } = S>(): Container { + return new Container(this as any); } has(id: string | number) { diff --git a/src/main/ts/di/ReferenceDescriptor.ts b/src/main/ts/di/ReferenceDescriptor.ts --- a/src/main/ts/di/ReferenceDescriptor.ts +++ b/src/main/ts/di/ReferenceDescriptor.ts @@ -1,4 +1,4 @@ -import { isNull, argumentNotEmptyString, each } from "../safe"; +import { isNull, argumentNotEmptyString, each, keys } from "../safe"; import { ActivationContext } from "./ActivationContext"; import { ServiceMap, Descriptor } from "./interfaces"; import { ActivationError } from "./ActivationError"; @@ -11,6 +11,12 @@ export interface ReferenceDescriptorPara services?: ServiceMap; } +function def(v: T) { + if (v === undefined) + throw Error(); + return v; +} + export class ReferenceDescriptor implements Descriptor { _name: K; @@ -20,7 +26,7 @@ export class ReferenceDescriptor; constructor(opts: ReferenceDescriptorParams) { argumentNotEmptyString(opts && opts.name, "opts.name"); @@ -32,24 +38,22 @@ export class ReferenceDescriptor) { // добавляем сервисы if (this._services) { - for (const p of Object.keys(this._services)) - context.register(p, this._services[p]); + each(this._services, (v, k) => context.register(k, def(v))); } if (this._lazy) { const saved = context.clone(); - return (cfg: ServiceMap) => { + return (cfg: Partial>) => { // защищаем контекст на случай исключения в процессе // активации const ct = saved.clone(); try { if (cfg) { - for (const k in cfg) - ct.register(k, cfg[k]); + each(cfg, (v, k) => ct.register(k, v || {})); } return this._optional ? ct.resolve(this._name, this._default) : ct @@ -59,12 +63,6 @@ export class ReferenceDescriptor { - activate(context: ActivationContext, name?: string): T; + activate(context: ActivationContext): T; } export function isDescriptor(x: any): x is Descriptor { @@ -70,6 +70,6 @@ export function isValueRegistration(x: a return (!isPrimitive(x)) && ("$value" in x); } -export function isDependencyRegistration(x: any): x is DependencyRegistration { +export function isDependencyRegistration(x: any): x is DependencyRegistration { return (!isPrimitive(x)) && ("$dependency" in x); } 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 @@ -6,9 +6,9 @@ const _oid = typeof Symbol === "function Symbol("__implab__oid__") : "__implab__oid__"; -export function oid(instance: object): string { +export function oid(instance: any): string | undefined { if (isNull(instance)) - return null; + return undefined; if (_oid in instance) return instance[_oid]; @@ -16,6 +16,10 @@ export function oid(instance: object): s return (instance[_oid] = "oid_" + (++_nextOid)); } +export function keys(arg: T): (Extract)[] { + return isObject(arg) && arg ? Object.keys(arg) as (Extract)[] : []; +} + export function argumentNotNull(arg: any, name: string) { if (arg === null || arg === undefined) throw new Error("The argument " + name + " can't be null or undefined"); @@ -36,6 +40,10 @@ export function argumentOfType(arg: any, throw new Error("The argument '" + name + "' type doesn't match"); } +export function isObject(val: any): val is object { + return typeof val === "object"; +} + export function isNull(val: any) { return (val === null || val === undefined); } @@ -65,21 +73,20 @@ export function isCancellable(val: any): return val && typeof val.cancel === "function"; } -export function isNullOrEmptyString(val: any): val is string | null | undefined { - if (val === null || val === undefined || +export function isNullOrEmptyString(val: any): val is (string | null | undefined) { + return (val === null || val === undefined || ((typeof (val) === "string" || val instanceof String) && val.length === 0)) - return true; } export function isNotEmptyArray(arg: any): arg is Array { return (arg instanceof Array && arg.length > 0); } -function _isStrictMode() { +function _isStrictMode(this: any) { return !this; } -function _getNonStrictGlobal() { +function _getNonStrictGlobal(this: any) { return this; } @@ -119,7 +126,9 @@ export function get(member: string, cont * @returns Результат вызова функции cb, либо undefined * если достигнут конец массива. */ -export function each(obj, cb, thisArg?) { +export function each(obj: T, cb: (v: T[keyof T], k: keyof T) => void): void; +export function each(obj: any, cb: any, thisArg?: any): any; +export function each(obj: any, cb: any, thisArg?: any) { argumentNotNull(cb, "cb"); if (obj instanceof Array) { for (let i = 0; i < obj.length; i++) { @@ -128,8 +137,8 @@ export function each(obj, cb, thisArg?) return x; } } else { - const keys = Object.keys(obj); - for (const k of keys) { + const _keys = Object.keys(obj); + for (const k of _keys) { const x = cb.call(thisArg, obj[k], k); if (x !== undefined) return x; @@ -166,8 +175,8 @@ export function mixin any, thisArg): (...args: any[]) => PromiseLike { +export function async(_fn: (...args: any[]) => any, thisArg: any): (...args: any[]) => PromiseLike { let fn = _fn; if (arguments.length === 2 && !(fn instanceof Function)) @@ -284,7 +293,7 @@ export function pmap( let i = 0; const result = new Array(); - const next = () => { + const next = (): any => { while (i < items.length) { const r = cb(items[i], i); const ri = i; @@ -319,7 +328,7 @@ export function pfor( let i = 0; - const next = () => { + const next = (): any => { while (i < items.length) { const r = cb(items[i], i); i++; diff --git a/src/test/ts/mock/Box.ts b/src/test/ts/mock/Box.ts --- a/src/test/ts/mock/Box.ts +++ b/src/test/ts/mock/Box.ts @@ -1,6 +1,13 @@ -import { config } from "./config"; +import { services } from "../di/Annotations"; +import { Bar } from "./Bar"; -const service = config.build("barBox"); +// declare required dependencies +const config = services<{ + bar: Bar; +}>(); + +// export service descriptor +export const service = config.build>(); @service.consume(config.dependency("bar")) export class Box { @@ -25,4 +32,4 @@ export class Box { return this._value; } -} \ No newline at end of file +}