# HG changeset patch # User cin # Date 2020-09-08 11:32:12 # Node ID a2fb9af6341ce6d0a853a7d2e4e59b7a983af529 # Parent c6d15f5d0e651830ecccfaff1f84e1eb625f40ec # Parent 7c11f56aa2ca342d1c9f15aa28c1918a0e4b7a68 Merge with ioc ts support diff --git a/.project b/.project --- a/.project +++ b/.project @@ -14,4 +14,15 @@ org.eclipse.buildship.core.gradleprojectnature + + + 1599549685358 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + diff --git a/.settings/org.eclipse.buildship.core.prefs b/.settings/org.eclipse.buildship.core.prefs --- a/.settings/org.eclipse.buildship.core.prefs +++ b/.settings/org.eclipse.buildship.core.prefs @@ -1,2 +1,13 @@ +arguments= +auto.sync=false +build.scans.enabled=false +connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) connection.project.dir= eclipse.preferences.version=1 +gradle.user.home= +java.home=/usr/lib64/jvm/java +jvm.arguments= +offline.mode=false +override.workspace.settings=true +show.console.view=true +show.executions.view=true diff --git a/build.gradle b/build.gradle --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.implab.gradle-typescript" version "1.3.0" + id "org.implab.gradle-typescript" version "1.3.3" id "org.implab.gradle-hg" id "ivy-publish" } @@ -49,10 +49,14 @@ typescript { compilerOptions { types = [] declaration = true + experimentalDecorators = true + strict = true + // dojo-typings are sick + skipLibCheck = true if(symbols != 'none') { sourceMap = true - sourceRoot = "_src" + sourceRoot = packageName } if (flavour == 'node') { diff --git a/docs/ru/observable.md b/docs/ru/observable.md --- a/docs/ru/observable.md +++ b/docs/ru/observable.md @@ -5,7 +5,7 @@ самостоятельных событий, например, связанных с действиями пользователя. Является реализацией классического шаблона наблюдателя с возможность сообщить -о коце потока событий. Данная реализация не содержит никаких дополнительных +о конце потока событий. Данная реализация не содержит никаких дополнительных функций, таких как фильтрация, канал с состоянием, преобразования сообщений и т.п. Это сделано специально, чтобы реализация оставалась максимально простой. @@ -20,7 +20,7 @@ var events = new Observable(async (notif notify(i); } // по окончании последовательности информируем, что событий больше не будет - compelte(); + complete(); }); // создаем окно с отображением хода событий @@ -49,9 +49,9 @@ let firstEvent = await events.next(); `Observable` можно создавать из событий другого объекта, например, виджета: ```ts -// клсс +// класс class Canvas { - readonly mouseMove: IObservable<[number,number]> + mouseMove: IObservable<[number,number]>; postCreate() { // превращаем события виджета в Observable @@ -98,7 +98,7 @@ class PositionTracker implements IDestro } ``` -Существует также несколько варинатов получения сообщений +Существует также несколько вариантов получения сообщений ```ts // регистрация метода для получений событий @@ -128,7 +128,7 @@ class Map { let evt = this.viewport.click.next(ct); // преобразуем позицию на экране в координаты карты - return this.clientToCoodinates([evt.clientx,evt.clientY]); + return this.clientToCoordinates([evt.clientX,evt.clientY]); } } @@ -142,8 +142,8 @@ let coords = await map.peekCoordinates() ## Observable и последовательности -Можно сичтать, что `Observable` это некоторая аналогия итератора только в -парадигме событийного (или реактивного) программировния. Следует также понимать, +Можно считать, что `Observable` это некоторая аналогия итератора только в +парадигме событийного (или реактивного) программирования. Следует также понимать, что при переходе от синхронного процедурного программирования к событийному так же меняется и направление управления (Inverse Of Control), что означает следующее: @@ -153,7 +153,7 @@ let coords = await map.peekCoordinates() * при работе с `Observable` клиенты вынуждены обрабатывать эти события по мере их поступления и не могут на это повлиять. -Последний пункт можно изменить применив, например, буффер или канал с +Последний пункт можно изменить применив, например, буфер или канал с состоянием, т.е. очередь, но данные механизмы выходят за рамки простого шаблона наблюдателя. @@ -179,4 +179,4 @@ events.on((data) => { // будет вызван для всех сообщений processEvent(data); }); -``` \ No newline at end of file +``` diff --git a/src/amd/js/data/StoreAdapter.js b/src/amd/js/data/StoreAdapter.js --- a/src/amd/js/data/StoreAdapter.js +++ b/src/amd/js/data/StoreAdapter.js @@ -71,7 +71,6 @@ function(declare, safe, when, QueryResul }); mapped.total = total; var results = new QueryResults(mapped); - console.log(results); return results; }); }, diff --git a/src/amd/ts/di/ResolverHelper.ts b/src/amd/ts/di/ResolverHelper.ts --- a/src/amd/ts/di/ResolverHelper.ts +++ b/src/amd/ts/di/ResolverHelper.ts @@ -12,7 +12,7 @@ const trace = TraceSource.get(m.id); trace.debug("globalRequire = {0}", globalRequire); class ModuleResolver { - _base: string; + _base: string | undefined; _require: Require; constructor(req: Require, base?: string) { diff --git a/src/amd/ts/log/trace.ts b/src/amd/ts/log/trace.ts --- a/src/amd/ts/log/trace.ts +++ b/src/amd/ts/log/trace.ts @@ -9,7 +9,7 @@ export = { cb = filter; filter = undefined; } - let test: Predicate; + let test: Predicate | undefined; if (filter instanceof RegExp) { test = chId => filter.test(chId); } else if (filter instanceof Function) { @@ -21,7 +21,7 @@ export = { if (test) { TraceSource.on(source => { source.level = this.level; - if (test(source.id)) + if (test && test(source.id)) source.events.on(cb); }); } else { diff --git a/src/amd/ts/text/TemplateCompiler.ts b/src/amd/ts/text/TemplateCompiler.ts --- a/src/amd/ts/text/TemplateCompiler.ts +++ b/src/amd/ts/text/TemplateCompiler.ts @@ -2,6 +2,7 @@ import * as format from "./format"; import { TraceSource, DebugLevel } from "../log/TraceSource"; import { ITemplateParser, TokenType } from "./TemplateParser"; import m = require("module"); +import { isKeyof } from "../safe"; const trace = TraceSource.get(m.id); @@ -21,7 +22,7 @@ const htmlEscaper = /[&<>"'\/]/g; // Escape a string for HTML interpolation. function escapeHtml(string: any) { - return ("" + string).replace(htmlEscaper, match => htmlEscapes[match]); + return ("" + string).replace(htmlEscaper, match => isKeyof(match, htmlEscapes) ? htmlEscapes[match] : ""); } export class TemplateCompiler { diff --git a/src/amd/ts/text/TemplateParser.ts b/src/amd/ts/text/TemplateParser.ts --- a/src/amd/ts/text/TemplateParser.ts +++ b/src/amd/ts/text/TemplateParser.ts @@ -38,7 +38,7 @@ export class TemplateParser implements I _tokens: string[]; _pos = -1; _type: TokenType; - _value: string; + _value: string | undefined; constructor(text: string) { argumentNotEmptyString(text, "text"); @@ -66,6 +66,8 @@ export class TemplateParser implements I } value() { + if (!this._value) + throw new Error("The current token doesn't have a value"); return this._value; } diff --git a/src/amd/ts/text/format.ts b/src/amd/ts/text/format.ts --- a/src/amd/ts/text/format.ts +++ b/src/amd/ts/text/format.ts @@ -2,29 +2,32 @@ import { format as dojoFormatNumber } fr import { format as dojoFormatDate } from "dojo/date/locale"; import { Formatter, compile as _compile } from "./StringFormat"; -import { isNumber, isNull } from "../safe"; +import { isNumber, isNull, get } from "../safe"; interface NumberFormatOptions { round?: number; pattern?: string; } -function convertNumber(value: any, pattern: string) { +function convertNumber(value: any, _pattern?: string) { if (isNumber(value)) { const nopt = {} as NumberFormatOptions; - if (pattern.indexOf("!") === 0) { + let pattern = _pattern; + if (pattern && pattern.indexOf("!") === 0) { nopt.round = -1; pattern = pattern.substr(1); } nopt.pattern = pattern; return dojoFormatNumber(value, nopt); + } else { + return ""; } } -function convertDate(value: any, pattern: string) { +function convertDate(value: any, pattern?: string) { if (value instanceof Date) { - const m = pattern.match(/^(\w+)-(\w+)$/); + const m = pattern && pattern.match(/^(\w+)-(\w+)$/); if (m) return dojoFormatDate(value, { selector: m[2], @@ -37,6 +40,8 @@ function convertDate(value: any, pattern selector: "date", datePattern: pattern }); + } else { + return ""; } } @@ -46,7 +51,7 @@ function format(msg: string, ...args: an return _formatter.format(msg, ...args); } -function _convert(value: any, pattern: string) { +function _convert(value: any, pattern?: string) { return _formatter.convert(value, pattern); } @@ -55,9 +60,9 @@ namespace format { export function compile(text: string) { const template = _compile(text); - return (...data) => { + return (...data: any[]) => { return template((name, pattern) => { - const value = data[name]; + const value = get(name, data); return !isNull(value) ? convert(value, pattern) : ""; }); }; diff --git a/src/amd/ts/text/template-compile.ts b/src/amd/ts/text/template-compile.ts --- a/src/amd/ts/text/template-compile.ts +++ b/src/amd/ts/text/template-compile.ts @@ -34,7 +34,7 @@ compile.load = (id: string, require: Req callback(cache[url]); } else { trace.debug("{0} -> {1}: load", id, url); - request(url).then(compile).then((tc: TemplateFn) => { + request(url).then(compile).then((tc: TemplateFn) => { trace.debug("{0}: compiled", url); callback(cache[url] = tc); }, (err: any) => { diff --git a/src/cjs/ts/di/ResolverHelper.ts b/src/cjs/ts/di/ResolverHelper.ts --- a/src/cjs/ts/di/ResolverHelper.ts +++ b/src/cjs/ts/di/ResolverHelper.ts @@ -4,10 +4,10 @@ import { TraceSource } from "../log/Trac const trace = TraceSource.get(module.id); const mainModule = require.main; -const mainRequire = (id: string) => mainModule.require(id); +const mainRequire = (id: string) => mainModule ? mainModule.require(id) : require; class ModuleResolver { - _base: string; + _base: string | undefined; _require: NodeRequireFunction; constructor(req: NodeRequireFunction, base?: string) { diff --git a/src/main/ts/Cancellation.ts b/src/main/ts/Cancellation.ts --- a/src/main/ts/Cancellation.ts +++ b/src/main/ts/Cancellation.ts @@ -3,7 +3,7 @@ import { argumentNotNull, destroyed } fr export class Cancellation implements ICancellation { private _reason: any; - private _cbs: Array<(e: any) => void>; + private _cbs: Array<(e: any) => void> | undefined; constructor(action: (cancel: (e?: any) => void) => void) { argumentNotNull(action, "action"); @@ -44,7 +44,7 @@ export class Cancellation implements ICa } } - private _unregister(cb) { + private _unregister(cb: any) { if (this._cbs) { const i = this._cbs.indexOf(cb); if (i >= 0) @@ -52,7 +52,7 @@ export class Cancellation implements ICa } } - private _cancel(reason) { + private _cancel(reason: any) { if (this._reason) return; @@ -60,7 +60,7 @@ export class Cancellation implements ICa if (this._cbs) { this._cbs.forEach(cb => cb(reason)); - this._cbs = null; + this._cbs = undefined; } } diff --git a/src/main/ts/EventProvider.ts b/src/main/ts/EventProvider.ts new file mode 100644 --- /dev/null +++ b/src/main/ts/EventProvider.ts @@ -0,0 +1,41 @@ +import { IDestroyable } from "./interfaces"; +import { Observable } from "./Observable"; + +/** + * Event proviers are used to produce events, throug this object you can feed + * the Observable with input events. Once the EventProvider is destroyed the + * bound obsevable is disconnected and marked as 'done'. + */ +export class EventProvider implements IDestroyable { + + _observable: Observable | undefined; + + _next: ((evt: T) => void) | undefined; + _done: (() => void) | undefined; + + constructor() { + this._observable = new Observable((next, _error, done) => { + this._next = next; + this._done = done; + }); + } + + destroy(): void { + if (this._observable) { + // break all references + this._observable = undefined; + this._next = undefined; + this._done = undefined; + } + } + post(event: T) { + return this._next && this._next(event); + } + + getObservable() { + if (!this._observable) + throw new Error("The object is destroyed"); + + return this._observable; + } +} diff --git a/src/main/ts/Observable.ts b/src/main/ts/Observable.ts --- a/src/main/ts/Observable.ts +++ b/src/main/ts/Observable.ts @@ -1,10 +1,10 @@ import { IObservable, IDestroyable, ICancellation, IObserver } from "./interfaces"; import { Cancellation } from "./Cancellation"; -import { argumentNotNull, destroyed } from "./safe"; +import { argumentNotNull } from "./safe"; type Handler = (x: T) => void; -type Initializer = (notify: Handler, error?: (e: any) => void, complete?: () => void) => void; +type Initializer = (notify: Handler, error: (e: any) => void, complete: () => void) => void; const noop = () => { }; @@ -17,7 +17,7 @@ export class Observable implements IO private _observers = new Array>(); - private _complete: boolean; + private _complete = false; private _error: any; @@ -46,7 +46,7 @@ export class Observable implements IO const me = this; const observer: IObserver & IDestroyable = { - next, + next: next.bind(null), error: error ? error.bind(null) : noop, complete: complete ? complete.bind(null) : noop, @@ -62,30 +62,21 @@ export class Observable implements IO subscribe(next: IObserver | Handler, error?: Handler, complete?: () => void): IDestroyable { if (isObserver(next)) { - const me = this; - const subscription = { - destroy() { - me._removeObserver(next); - } + this._addObserver(next); + return { + destroy: () => this._removeObserver(next) }; - this._addObserver(next); - return subscription; - } else if (next) { + } else { const observer = { - next, - error, - complete + next: next.bind(null), + error: error ? error.bind(null) : noop, + complete: complete ? complete.bind(null) : noop }; - const me = this; - const subscription = { - destroy() { - me._removeObserver(observer); - } + + this._addObserver(observer); + return { + destroy: () => this._removeObserver(observer) }; - this._addObserver(observer); - return subscription; - } else { - return destroyed; } } diff --git a/src/main/ts/ObservableValue.ts b/src/main/ts/ObservableValue.ts --- a/src/main/ts/ObservableValue.ts +++ b/src/main/ts/ObservableValue.ts @@ -17,8 +17,10 @@ export class ObservableValue extends } setValue(value: T) { - this._value = value; - this._notifyNext(value); + if (this._value !== value) { + this._value = value; + this._notifyNext(value); + } } on(next: Handler, error?: Handler, complete?: () => void): IDestroyable { diff --git a/src/main/ts/Uuid.ts b/src/main/ts/Uuid.ts --- a/src/main/ts/Uuid.ts +++ b/src/main/ts/Uuid.ts @@ -6,18 +6,25 @@ // Copyright (c) 2010-2012 Robert Kieffer // MIT License - http://opensource.org/licenses/mit-license.php +import { MapOf } from "./interfaces"; + declare const window: any; -declare const require; -declare const Buffer; +declare const require: any; +declare const Buffer: any; const _window: any = "undefined" !== typeof window ? window : null; +interface WritableArrayLike { + length: number; + [n: number]: T; +} + // Unique ID creation requires a high quality random # generator. We // feature // detect to determine the best RNG source, normalizing to a function // that // returns 128-bits of randomness, since that's what's usually required -let _rng; +let _rng: () => WritableArrayLike = () => []; function setupBrowser() { // Allow for MSIE11 msCrypto @@ -43,9 +50,9 @@ function setupBrowser() { // If all else fails, use Math.random(). It's fast, but is of // unspecified // quality. - const _rnds = new Array(16); + const _rnds = new Array(16); _rng = () => { - for (let i = 0, r; i < 16; i++) { + for (let i = 0, r = 0; i < 16; i++) { if ((i & 0x03) === 0) { r = Math.random() * 0x100000000; } @@ -84,22 +91,22 @@ if (_window) { const BufferClass = ("function" === typeof Buffer) ? Buffer : Array; // Maps for number <-> hex string conversion -const _byteToHex = []; -const _hexToByte = {}; +const _byteToHex: string[] = []; +const _hexToByte: MapOf = {}; for (let i = 0; i < 256; i++) { _byteToHex[i] = (i + 0x100).toString(16).substr(1); _hexToByte[_byteToHex[i]] = i; } // **`parse()` - Parse a UUID into it's component bytes** -function _parse(s, buf?, offset?): Array { +function _parse(s: string, buf: number[] = [], offset?: number): number[] { const i = (buf && offset) || 0; let ii = 0; - buf = buf || []; s.toLowerCase().replace(/[0-9a-f]{2}/g, oct => { if (ii < 16) { // Don't overflow! buf[i + ii++] = _hexToByte[oct]; } + return ""; }); // Zero out remaining bytes if string was short @@ -111,7 +118,7 @@ function _parse(s, buf?, offset?): Array } // **`unparse()` - Convert UUID byte array (ala parse()) into a string** -function _unparse(buf, offset?): string { +function _unparse(buf: ArrayLike, offset?: number): string { let i = offset || 0; const bth = _byteToHex; return bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + "-" + bth[buf[i++]] + bth[buf[i++]] + "-" + @@ -145,13 +152,20 @@ let _clockseq = (_seedBytes[6] << 8 | _s // Previous uuid creation time let _lastMSecs = 0; let _lastNSecs = 0; +interface V1Options { + clockseq?: number; + msecs?: number; + nsecs?: number; + node?: number[]; +} + // See https://github.com/broofa/node-uuid for API details -function _v1(options?, buf?, offset?): string { +function _v1(options?: V1Options): string; +function _v1(options: V1Options, buf: number[], offset?: number): number[]; +function _v1(options: V1Options = {}, buf?: number[], offset?: number): string | number[] { let i = buf && offset || 0; const b = buf || []; - options = options || {}; - let clockseq = (options.clockseq != null) ? options.clockseq : _clockseq; // UUID timestamps are 100 nano-second units since the Gregorian @@ -228,19 +242,21 @@ function _v1(options?, buf?, offset?): s return buf ? buf : _unparse(b); } +interface V4Opptions { + rng?: () => WritableArrayLike; + + random?: number[]; +} + // **`v4()` - Generate random UUID** // See https://github.com/broofa/node-uuid for API details -function _v4(options?, buf?, offset?): string { +function _v4(options?: V4Opptions): string; +function _v4(options: V4Opptions, buf: number[], offset?: number): number[]; +function _v4(options: V4Opptions = {}, buf?: number[], offset?: number): string | number[] { // Deprecated - 'format' argument, as supported in v1.2 const i = buf && offset || 0; - if (typeof (options) === "string") { - buf = (options === "binary") ? new BufferClass(16) : null; - options = null; - } - options = options || {}; - const rnds = options.random || (options.rng || _rng)(); // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` diff --git a/src/main/ts/components/ActivatableMixin.ts b/src/main/ts/components/ActivatableMixin.ts --- a/src/main/ts/components/ActivatableMixin.ts +++ b/src/main/ts/components/ActivatableMixin.ts @@ -9,15 +9,21 @@ const log = TraceSource.get("@implab/cor export function ActivatableMixin>(Base: TBase) { return class extends Base implements IActivatable { - _controller: IActivationController; + _controller: IActivationController | undefined; - _active: boolean; + _active = false; isActive() { return this._active; } + hasActivationController() { + return !!this._controller; + } + getActivationController() { + if (!this._controller) + throw Error("Activation controller isn't set"); return this._controller; } diff --git a/src/main/ts/components/AsyncComponent.ts b/src/main/ts/components/AsyncComponent.ts --- a/src/main/ts/components/AsyncComponent.ts +++ b/src/main/ts/components/AsyncComponent.ts @@ -2,8 +2,10 @@ import { Cancellation } from "../Cancell import { IAsyncComponent, ICancellation, ICancellable, IDestroyable } from "../interfaces"; import { destroy } from "../safe"; +const noop = () => void (0); + export class AsyncComponent implements IAsyncComponent, ICancellable { - _cancel: (e: any) => void; + _cancel: ((e: any) => void) = noop; _completion: Promise = Promise.resolve(); @@ -26,7 +28,7 @@ export class AsyncComponent implements I // after the operation is complete we need to cleanup the // resources destroy(h); - this._cancel = null; + this._cancel = noop; } }; @@ -34,7 +36,6 @@ export class AsyncComponent implements I } cancel(reason: any) { - if (this._cancel) - this._cancel(reason); + this._cancel(reason); } } 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 @@ -1,7 +1,8 @@ import { TraceSource } from "../log/TraceSource"; -import { argumentNotNull, argumentNotEmptyString, isPrimitive, each, isNull } from "../safe"; -import { Descriptor, ServiceMap } from "./interfaces"; +import { argumentNotEmptyString } from "../safe"; +import { Descriptor, ContainerServiceMap, ContainerKeys, TypeOfService, ILifetime } from "./interfaces"; import { Container } from "./Container"; +import { MapOf } from "../interfaces"; const trace = TraceSource.get("@implab/core/di/ActivationContext"); @@ -10,50 +11,85 @@ export interface ActivationContextInfo { service: string; - scope: ServiceMap; } -export class ActivationContext { - _cache: object; +let nextId = 1; - _services: ServiceMap; +/** This class is created once per `Container.resolve` method call and used to + * cache dependencies and to track created instances. The activation context + * tracks services with `context` activation type. + */ +export class ActivationContext { + _cache: MapOf; - _stack: ActivationContextInfo[]; + _services: ContainerServiceMap; - _visited: object; + _visited: MapOf; _name: string; - _localized: boolean; + _service: Descriptor; - container: Container; + _container: Container; + + _parent: ActivationContext | undefined; - constructor(container: Container, services: ServiceMap, name?: string, cache?: object, visited?) { - argumentNotNull(container, "container"); - argumentNotNull(services, "services"); - + /** Creates a new activation context with the specified parameters. + * @param container the container which starts the activation process + * @param services the initial service registrations + * @param name the name of the service being activated, this parameter is + * used for the debug purpose. + * @param service the service to activate, this parameter is used for the + * debug purpose. + */ + constructor(container: Container, services: ContainerServiceMap, name: string, service: Descriptor) { this._name = name; - this._visited = visited || {}; - this._stack = []; - this._cache = cache || {}; + this._service = service; + this._visited = {}; + this._cache = {}; this._services = services; - this.container = container; + this._container = container; } + /** the name of the current resolving dependency */ getName() { return this._name; } - resolve(name, def?): any { + /** Returns the container for which 'resolve' method was called */ + getContainer() { + return this._container; + } + + /** Resolves the specified dependency in the current context + * @param name The name of the dependency being resolved + */ + resolve>(name: K): TypeOfService; + /** Resolves the specified dependency with the specified default value if + * the dependency is missing. + * + * @param name The name of the dependency being resolved + * @param def A default value to return in case of the specified dependency + * is missing. + */ + resolve, T>(name: K, def: T): TypeOfService | T; + /** Resolves the specified dependency and returns undefined in case if the + * dependency is missing. + * + * @param name The name of the dependency being resolved + */ + resolve>(name: K, def: undefined): TypeOfService | undefined; + resolve, T>(name: K, def?: T): TypeOfService | T | undefined { const d = this._services[name]; - if (!d) + if (d !== undefined) { + return this.activate(d, name.toString()); + } else { if (arguments.length > 1) return def; else throw new Error(`Service ${name} not found`); - - return this.activate(d, name); + } } /** @@ -62,41 +98,36 @@ 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; + this._services[name] = service as any; } - clone() { - return new ActivationContext( - this.container, - this._services, - this._name, - this._cache, - this._visited - ); + createLifetime(): ILifetime { + const id = nextId++; + const me = this; + return { + initialize() { + }, + has() { + return id in me._cache; + }, + get() { + return me._cache[id]; + }, + store(item: any) { + me._cache[id] = item; + } + }; } - has(id: string) { - return id in this._cache; - } - - get(id: string) { - return this._cache[id]; - } - - store(id: string, value) { - 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); - this.leave(); + const ctx = this.enter(d, name); + const v = d.activate(ctx); if (trace.isLogEnabled()) trace.log(`leave ${name}`); @@ -110,23 +141,30 @@ export class ActivationContext { return count; } - getStack() { - return this._stack.slice().reverse(); + getStack(): ActivationContextInfo[] { + const stack = [{ + name: this._name, + service: this._service.toString() + }]; + + return this._parent ? + stack.concat(this._parent.getStack()) : + stack; } - private enter(name: string, service: string) { - this._stack.push({ - name, - service, - scope: this._services - }); - this._name = name; - this._services = Object.create(this._services); + private enter(service: Descriptor, name: string): this { + const clone = Object.create(this); + clone._name = name; + clone._services = Object.create(this._services); + clone._parent = this; + clone._service = service; + return clone; } - private leave() { - const ctx = this._stack.pop(); - this._services = ctx.scope; - this._name = ctx.name; + /** Creates a clone for the current context, used to protect it from modifications */ + clone(): this { + const clone = Object.create(this); + clone._services = Object.create(this._services); + return clone; } } 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) { + 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 @@ -1,37 +1,40 @@ -import { Descriptor, isDescriptor } from "./interfaces"; +import { Descriptor } from "./interfaces"; import { ActivationContext } from "./ActivationContext"; import { isPrimitive } from "../safe"; +import { isDescriptor } from "./traits"; -export class AggregateDescriptor implements Descriptor { - _value: object; +export class AggregateDescriptor implements Descriptor { + _value: any; - constructor(value: object) { + constructor(value: any) { this._value = value; } - activate(context: ActivationContext) { + activate(context: ActivationContext): T { return this._parse(this._value, context, "$value"); } - // TODO: make async - _parse(value, context: ActivationContext, path: string) { + _parse(value: any, context: ActivationContext, path: string): any { 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() { return "@walk"; } + + clone() { + return this; + } } 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 @@ -1,19 +1,12 @@ import { - ServiceRegistration, - TypeRegistration, - FactoryRegistration, - ServiceMap, - isDescriptor, - isDependencyRegistration, - DependencyRegistration, - ValueRegistration, + PartialServiceMap, ActivationType, - isValueRegistration, - isTypeRegistration, - isFactoryRegistration + ContainerKeys, + TypeOfService, + ILifetime } from "./interfaces"; -import { argumentNotEmptyString, isPrimitive, isPromise, delegate, argumentOfType, argumentNotNull, get } from "../safe"; +import { argumentNotEmptyString, isPrimitive, isPromise, delegate, argumentOfType, argumentNotNull, get, primitive } from "../safe"; import { AggregateDescriptor } from "./AggregateDescriptor"; import { ValueDescriptor } from "./ValueDescriptor"; import { Container } from "./Container"; @@ -25,10 +18,115 @@ import { ConfigError } from "./ConfigErr import { Cancellation } from "../Cancellation"; import { makeResolver } from "./ResolverHelper"; import { ICancellation } from "../interfaces"; +import { isDescriptor } from "./traits"; +import { LazyReferenceDescriptor } from "./LazyReferenceDescriptor"; +import { LifetimeManager } from "./LifetimeManager"; + +export interface RegistrationScope { + + /** сервисы, которые регистрируются в контексте активации и таким образом + * могут переопределять ранее зарегистрированные сервисы. за это свойство + * нужно платить, кроме того порядок активации будет влиять на результат + * разрешения зависимостей. + */ + services?: RegistrationMap; +} + +/** + * Базовый интерфейс конфигурации сервисов + */ +export interface ServiceRegistration extends RegistrationScope { + + activation?: ActivationType; + + params?: any; + + /** Специальный идентификатор используется при активации singleton, если + * не указан для TypeRegistration вычисляется как oid($type) + */ + typeId?: string; + + inject?: object | object[]; + + cleanup?: ((instance: T) => void) | string; +} + +export interface TypeRegistration any, S extends object> extends ServiceRegistration, S> { + $type: string | C; + params?: Registration, S>; +} + +export interface StrictTypeRegistration any, S extends object> extends ServiceRegistration, S> { + $type: C; + params?: Registration, S>; +} + +export interface FactoryRegistration any, S extends object> extends ServiceRegistration, S> { + $factory: string | F; +} + +export interface ValueRegistration { + $value: T; + parse?: boolean; +} + +export interface DependencyRegistration = ContainerKeys> extends RegistrationScope { + $dependency: K; + lazy?: boolean; + optional?: boolean; + default?: TypeOfService; +} + +export interface LazyDependencyRegistration = ContainerKeys> extends DependencyRegistration { + lazy: true; +} + +export type Registration = T extends primitive ? T : + ( + T | + { [k in keyof T]: Registration } | + TypeRegistration T, S> | + FactoryRegistration<(...args: any[]) => T, S> | + ValueRegistration | + DependencyRegistration + ); + +export type RegistrationMap = { + [k in keyof S]?: Registration; +}; + +const _activationTypes: { [k in ActivationType]: number; } = { + singleton: 1, + container: 2, + hierarchy: 3, + context: 4, + call: 5 +}; + +export function isTypeRegistration(x: any): x is TypeRegistration any, any> { + return (!isPrimitive(x)) && ("$type" in x); +} + +export function isFactoryRegistration(x: any): x is FactoryRegistration<() => any, any> { + return (!isPrimitive(x)) && ("$factory" in x); +} + +export function isValueRegistration(x: any): x is ValueRegistration { + return (!isPrimitive(x)) && ("$value" in x); +} + +export function isDependencyRegistration(x: any): x is DependencyRegistration { + return (!isPrimitive(x)) && ("$dependency" in x); +} + +export function isActivationType(x: string): x is ActivationType { + return typeof x === "string" && x in _activationTypes; +} const trace = TraceSource.get("@implab/core/di/Configuration"); - -async function mapAll(data: object | any[], map?: (v, k) => any): Promise { +async function mapAll(data: any[], map?: (v: any, k: number) => any): Promise; +async function mapAll(data: any, map?: (v: any, k: string) => any): Promise; +async function mapAll(data: any, map?: (v: any, k: any) => any): Promise { if (data instanceof Array) { return Promise.all(map ? data.map(map) : data); } else { @@ -47,21 +145,19 @@ async function mapAll(data: object | any export type ModuleResolver = (moduleName: string, ct?: ICancellation) => any; -type _key = string | number; - -export class Configuration { +export class Configuration { _hasInnerDescriptors = false; - _container: Container; + readonly _container: Container; - _path: Array<_key>; + _path: Array; - _configName: string; + _configName: string | undefined; - _require: ModuleResolver; + _require: ModuleResolver | undefined; - constructor(container: Container) { + constructor(container: Container) { argumentNotNull(container, "container"); this._container = container; this._path = []; @@ -78,7 +174,7 @@ export class Configuration { this._configName = moduleName; - const r = await makeResolver(null, contextRequire); + const r = await makeResolver(undefined, contextRequire); const config = await r(moduleName, ct); @@ -89,13 +185,14 @@ export class Configuration { ); } - async applyConfiguration(data: object, contextRequire?: any, ct = Cancellation.none) { + async applyConfiguration(data: RegistrationMap, opts: { contextRequire?: any; baseModule?: string }, ct = Cancellation.none) { argumentNotNull(data, "data"); + const _opts = opts || {}; - await this._applyConfiguration(data, await makeResolver(void (0), contextRequire), ct); + await this._applyConfiguration(data, await makeResolver(_opts.baseModule, _opts.contextRequire), ct); } - async _applyConfiguration(data: object, resolver?: ModuleResolver, ct = Cancellation.none) { + async _applyConfiguration(data: RegistrationMap, resolver?: ModuleResolver, ct = Cancellation.none) { trace.log("applyConfiguration"); this._configName = "$"; @@ -103,7 +200,7 @@ export class Configuration { if (resolver) this._require = resolver; - let services: ServiceMap; + let services: PartialServiceMap; try { services = await this._visitRegistrations(data, "$"); @@ -114,9 +211,9 @@ export class Configuration { this._container.register(services); } - _makeError(inner) { + _makeError(inner: any) { const e = new ConfigError("Failed to load configuration", inner); - e.configName = this._configName; + e.configName = this._configName || ""; e.path = this._makePath(); return e; } @@ -135,7 +232,15 @@ export class Configuration { trace.log("resolveType moduleName={0}, localName={1}", moduleName, localName); try { const m = await this._loadModule(moduleName); - return localName ? get(localName, m) : m; + if (localName) { + return get(localName, m); + } else { + if (m instanceof Function) + return m; + if ("default" in m) + return m.default; + return m; + } } catch (e) { trace.error("Failed to resolve type moduleName={0}, localName={1}", moduleName, localName); throw e; @@ -144,32 +249,31 @@ export class Configuration { _loadModule(moduleName: string) { trace.debug("loadModule {0}", moduleName); + if (!this._require) + throw new Error("Module loader isn't specified"); return this._require(moduleName); } - async _visitRegistrations(data, name: _key) { + async _visitRegistrations(data: RegistrationMap, name: string) { this._enter(name); if (data.constructor && data.constructor.prototype !== Object.prototype) throw new Error("Configuration must be a simple object"); - const o: ServiceMap = {}; - 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; + }) as PartialServiceMap; this._leave(); return services; } - _enter(name: _key) { - this._path.push(name); + _enter(name: string) { + this._path.push(name.toString()); trace.debug(">{0}", name); } @@ -178,11 +282,13 @@ export class Configuration { trace.debug("<{0}", name); } - async _visit(data, name: string) { - if (isPrimitive(data) || isDescriptor(data)) - return data; + _visit(data: any, name: string): Promise { + if (isPrimitive(data)) + return Promise.resolve(new ValueDescriptor(data)); + if (isDescriptor(data)) + return Promise.resolve(data); - if (isDependencyRegistration(data)) { + if (isDependencyRegistration(data)) { return this._visitDependencyRegistration(data, name); } else if (isValueRegistration(data)) { return this._visitValueRegistration(data, name); @@ -197,7 +303,7 @@ export class Configuration { return this._visitObject(data, name); } - async _visitObject(data: object, name: _key) { + async _visitObject(data: any, name: string) { if (data.constructor && data.constructor.prototype !== Object.prototype) return new ValueDescriptor(data); @@ -220,7 +326,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); @@ -233,9 +339,8 @@ export class Configuration { return v; } - _makeServiceParams(data: ServiceRegistration) { + _makeServiceParams(data: ServiceRegistration) { const opts: any = { - owner: this._container }; if (data.services) opts.services = this._visitRegistrations(data.services, "services"); @@ -257,30 +362,7 @@ export class Configuration { this._visit(data.params, "params"); if (data.activation) { - if (typeof (data.activation) === "string") { - switch (data.activation.toLowerCase()) { - case "singleton": - opts.activation = ActivationType.Singleton; - break; - case "container": - opts.activation = ActivationType.Container; - break; - case "hierarchy": - opts.activation = ActivationType.Hierarchy; - break; - case "context": - opts.activation = ActivationType.Context; - break; - case "call": - opts.activation = ActivationType.Call; - break; - default: - throw new Error("Unknown activation type: " + - data.activation); - } - } else { - opts.activation = Number(data.activation); - } + opts.activation = this._getLifetimeManager(data.activation, data.typeId); } if (data.cleanup) @@ -289,28 +371,28 @@ 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: _key) { + async _visitDependencyRegistration(data: DependencyRegistration, name: string) { argumentNotEmptyString(data && data.$dependency, "data.$dependency"); this._enter(name); - const d = new ReferenceDescriptor({ + const options = { name: data.$dependency, - lazy: data.lazy, optional: data.optional, default: data.default, services: data.services && await this._visitRegistrations(data.services, "services") - }); + }; + const d = data.lazy ? new LazyReferenceDescriptor(options) : new ReferenceDescriptor(options); this._leave(); return d; } - async _visitTypeRegistration(data: TypeRegistration, name: _key) { + async _visitTypeRegistration(data: TypeRegistration any, S>, name: string) { argumentNotNull(data.$type, "data.$type"); this._enter(name); @@ -319,10 +401,14 @@ export class Configuration { opts.type = data.$type; } else { const [moduleName, typeName] = data.$type.split(":", 2); - opts.type = this._resolveType(moduleName, typeName); + opts.type = this._resolveType(moduleName, typeName).then(t => { + if (!(t instanceof Function)) + throw Error("$type (" + data.$type + ") is not a constructable"); + return t; + }); } - const d = new TypeServiceDescriptor( + const d = new TypeServiceDescriptor( await mapAll(opts) ); @@ -331,18 +417,35 @@ export class Configuration { return d; } - async _visitFactoryRegistration(data: FactoryRegistration, name: _key) { + async _visitFactoryRegistration(data: FactoryRegistration<() => any, S>, name: string) { argumentOfType(data.$factory, Function, "data.$factory"); this._enter(name); const opts = this._makeServiceParams(data); opts.factory = data.$factory; - const d = new FactoryServiceDescriptor( + const d = new FactoryServiceDescriptor( await mapAll(opts) ); this._leave(); return d; } + + _getLifetimeManager(activation: ActivationType, typeId: string | undefined): ILifetime { + switch (activation) { + case "container": + return LifetimeManager.containerLifetime(this._container); + case "hierarchy": + return LifetimeManager.hierarchyLifetime(); + case "context": + return LifetimeManager.contextLifetime(); + case "singleton": + if (typeId === undefined) + throw Error("The singleton activation requires a typeId"); + return LifetimeManager.singletonLifetime(typeId); + default: + return LifetimeManager.empty(); + } + } } 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,40 @@ import { ActivationContext } from "./ActivationContext"; import { ValueDescriptor } from "./ValueDescriptor"; import { ActivationError } from "./ActivationError"; -import { isDescriptor, ServiceMap } from "./interfaces"; +import { ServiceMap, Descriptor, PartialServiceMap, ServiceLocator, ContainerServiceMap, ContainerKeys, TypeOfService } from "./interfaces"; import { TraceSource } from "../log/TraceSource"; -import { Configuration } from "./Configuration"; +import { Configuration, RegistrationMap } from "./Configuration"; import { Cancellation } from "../Cancellation"; +import { MapOf, IDestroyable } from "../interfaces"; +import { isDescriptor } from "./traits"; +import { LifetimeManager } from "./LifetimeManager"; +import { each } from "../safe"; +import { FluentRegistrations } from "./fluent/interfaces"; +import { FluentConfiguration } from "./fluent/FluentConfiguration"; const trace = TraceSource.get("@implab/core/di/ActivationContext"); -export class Container { - _services: ServiceMap; +export class Container implements ServiceLocator, IDestroyable { + readonly _services: ContainerServiceMap; - _cache: object; + readonly _lifetimeManager: LifetimeManager; + + 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._services.container = new ValueDescriptor(this) as any; + this._disposed = false; + this._lifetimeManager = new LifetimeManager(); } getRootContainer() { @@ -36,93 +45,105 @@ export class Container { return this._parent; } - resolve(name: string, def?) { + getLifetimeManager() { + return this._lifetimeManager; + } + + resolve>(name: K, def?: TypeOfService): TypeOfService { 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, String(name), d); + try { + return d.activate(context); + } 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?: TypeOfService) { + return this.resolve(name, def); } - register(nameOrCollection, service?) { + register(name: K, service: Descriptor): this; + register(services: PartialServiceMap): 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; + + each(data, (v, k) => this.register(k, v)); } else { if (!isDescriptor(service)) throw new Error("The service parameter must be a descriptor"); - this._services[nameOrCollection] = service; + this._services[nameOrCollection as K] = service as any; } return this; } - onDispose(callback) { + onDispose(callback: () => void) { if (!(callback instanceof Function)) throw new Error("The callback must be a function"); this._cleanup.push(callback); } + destroy() { + return this.dispose(); + } 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(); } /** * @param{String|Object} config - * The configuration of the contaier. Can be either a string or an object, + * The configuration of the container. 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. + * services which will be registered in the container. * * @param{Function} opts.contextRequire * The function which will be used to load a configuration or types for services. * */ - async configure(config: string | object, opts?: any, ct = Cancellation.none) { - const c = new Configuration(this); + async configure(config: string | RegistrationMap, opts?: { contextRequire: any; baseModule?: string }, ct = Cancellation.none) { + const _opts = Object.create(opts || null); if (typeof (config) === "string") { - return c.loadConfiguration(config, opts && opts.contextRequire, ct); + _opts.baseModule = config; + + const module = await import(config); + if (module && module.default && typeof (module.default.apply) === "function") + return module.default.apply(this); + else + return this._applyLegacyConfig(module, _opts, ct); } else { - return c.applyConfiguration(config, opts && opts.contextRequire, ct); + return this._applyLegacyConfig(config, _opts, ct); } } - createChildContainer() { - return new Container(this); - } - - has(id) { - return id in this._cache; + async _applyLegacyConfig(config: RegistrationMap, opts: { contextRequire: any; baseModule?: string }, ct = Cancellation.none) { + return new Configuration(this).applyConfiguration(config, opts); } - get(id) { - return this._cache[id]; + async fluent(config: FluentRegistrations, ct = Cancellation.none): Promise { + await new FluentConfiguration().register(config).apply(this, ct); + return this; } - store(id, value) { - return (this._cache[id] = value); + createChildContainer(): Container { + return new Container(this as any); } - } diff --git a/src/main/ts/di/FactoryServiceDescriptor.ts b/src/main/ts/di/FactoryServiceDescriptor.ts --- a/src/main/ts/di/FactoryServiceDescriptor.ts +++ b/src/main/ts/di/FactoryServiceDescriptor.ts @@ -1,23 +1,18 @@ import { ServiceDescriptor, ServiceDescriptorParams } from "./ServiceDescriptor"; -import { Factory } from "../interfaces"; import { argumentNotNull, oid } from "../safe"; -import { ActivationType } from "./interfaces"; -export interface FactoryServiceDescriptorParams extends ServiceDescriptorParams { - factory: Factory; +export interface FactoryServiceDescriptorParams extends ServiceDescriptorParams { + factory: (...args: P) => T; } -export class FactoryServiceDescriptor extends ServiceDescriptor { - constructor(opts: FactoryServiceDescriptorParams) { +export class FactoryServiceDescriptor extends ServiceDescriptor { + constructor(opts: FactoryServiceDescriptorParams) { super(opts); argumentNotNull(opts && opts.factory, "opts.factory"); // bind to null - this._factory = (...args) => opts.factory.apply(null, args); + this._factory = (...args) => opts.factory.apply(null, args as any); - if (opts.activation === ActivationType.Singleton) { - this._cacheId = oid(opts.factory); - } } } diff --git a/src/main/ts/di/LazyReferenceDescriptor.ts b/src/main/ts/di/LazyReferenceDescriptor.ts new file mode 100644 --- /dev/null +++ b/src/main/ts/di/LazyReferenceDescriptor.ts @@ -0,0 +1,83 @@ +import { argumentNotEmptyString, each } from "../safe"; +import { ActivationContext } from "./ActivationContext"; +import { Descriptor, PartialServiceMap, TypeOfService, ContainerKeys } from "./interfaces"; +import { ActivationError } from "./ActivationError"; + +export interface ReferenceDescriptorParams> { + name: K; + optional?: boolean; + default?: TypeOfService; + services?: PartialServiceMap; +} + +export class LazyReferenceDescriptor = ContainerKeys> + implements Descriptor) => TypeOfService)> { + + _name: K; + + _optional = false; + + _default: TypeOfService | undefined; + + _services: PartialServiceMap; + + constructor(opts: ReferenceDescriptorParams) { + argumentNotEmptyString(opts && opts.name, "opts.name"); + this._name = opts.name; + this._optional = !!opts.optional; + this._default = opts.default; + + this._services = (opts.services || {}) as PartialServiceMap; + } + + activate(context: ActivationContext) { + // добавляем сервисы + if (this._services) { + each(this._services, (v, k) => context.register(k, v)); + } + + const saved = context.clone(); + + return (cfg?: PartialServiceMap): any => { + // защищаем контекст на случай исключения в процессе + // активации + const ct = cfg ? saved.clone() : saved; + try { + if (cfg) { + each(cfg, (v, k) => ct.register(k, v)); + } + + return this._optional ? ct.resolve(this._name, this._default) : ct + .resolve(this._name); + } catch (error) { + throw new ActivationError(this._name.toString(), ct.getStack(), error); + } + }; + } + + toString() { + const opts = []; + if (this._optional) + opts.push("optional"); + + opts.push("lazy"); + + const parts = [ + "@ref " + ]; + if (opts.length) { + parts.push("{"); + parts.push(opts.join()); + parts.push("} "); + } + + parts.push(this._name.toString()); + + if (this._default !== undefined && this._default !== null) { + parts.push(" = "); + parts.push(String(this._default)); + } + + return parts.join(""); + } +} diff --git a/src/main/ts/di/LifetimeManager.ts b/src/main/ts/di/LifetimeManager.ts new file mode 100644 --- /dev/null +++ b/src/main/ts/di/LifetimeManager.ts @@ -0,0 +1,198 @@ +import { IDestroyable, MapOf } from "../interfaces"; +import { argumentNotNull, isDestroyable, primitive, isNull, argumentNotEmptyString } from "../safe"; +import { ILifetime } from "./interfaces"; +import { ActivationContext } from "./ActivationContext"; +import { Container } from "./Container"; + +function safeCall(item: () => void) { + try { + item(); + } catch { + // silence! + } +} + +const emptyLifetime: ILifetime = Object.freeze({ + has() { + return false; + }, + + initialize() { + + }, + + get() { + throw new Error("The specified item isn't registered with this lifetime manager"); + }, + + store() { + // does nothing + } + +}); + +const unknownLifetime: ILifetime = Object.freeze({ + has() { + return false; + }, + initialize() { + throw new Error("Can't call initialize on the unknown lifetime object"); + }, + get() { + throw new Error("The lifetime object isn't initialized"); + }, + store() { + throw new Error("Can't store a value in the unknown lifetime object"); + } +}); + +let nextId = 0; + +const singletons: { [k in keyof any]: any; } = {}; + +export class LifetimeManager implements IDestroyable { + private _cleanup: (() => void)[] = []; + private _cache: MapOf = {}; + private _destroyed = false; + + private _pending: MapOf = {}; + + create(): ILifetime { + const self = this; + const id = ++nextId; + return { + has() { + return (id in self._cache); + }, + + get() { + const t = self._cache[id]; + if (t === undefined) + throw new Error(`The item with with the key ${id} isn't found`); + return t; + }, + + initialize() { + if (self._pending[id]) + throw Error(`Cyclic reference detected: the item with the key ${id} is already activating.`); + self._pending[id] = true; + }, + + store(item: any, cleanup?: (item: any) => void) { + argumentNotNull(id, "id"); + argumentNotNull(item, "item"); + + if (this.has()) + throw new Error(`The item with with the key ${id} already registered with this lifetime manager`); + delete self._pending[id]; + + self._cache[id] = item; + + if (self._destroyed) + throw new Error("Lifetime manager is destroyed"); + if (cleanup) { + self._cleanup.push(() => cleanup(item)); + } else if (isDestroyable(item)) { + self._cleanup.push(() => item.destroy()); + } + } + }; + } + + destroy() { + if (!this._destroyed) { + this._destroyed = true; + this._cleanup.forEach(safeCall); + this._cleanup.length = 0; + } + } + + static empty(): ILifetime { + return emptyLifetime; + } + + static hierarchyLifetime(): ILifetime { + let _lifetime = unknownLifetime; + return { + initialize(context: ActivationContext) { + if (_lifetime !== unknownLifetime) + throw new Error("Cyclic reference activation detected"); + + _lifetime = context.getContainer().getLifetimeManager().create(); + }, + get() { + return _lifetime.get(); + }, + has() { + return _lifetime.has(); + }, + store(item: any, cleanup?: (item: any) => void) { + return _lifetime.store(item, cleanup); + } + }; + } + + static contextLifetime(): ILifetime { + let _lifetime = unknownLifetime; + return { + initialize(context: ActivationContext) { + if (_lifetime !== unknownLifetime) + throw new Error("Cyclic reference detected"); + _lifetime = context.createLifetime(); + }, + get() { + return _lifetime.get(); + }, + has() { + return _lifetime.has(); + }, + store(item: any) { + _lifetime.store(item); + } + }; + } + + static singletonLifetime(typeId: string): ILifetime { + argumentNotEmptyString(typeId, "typeId"); + let pending = false; + return { + has() { + return typeId in singletons; + }, + get() { + if (!this.has()) + throw new Error(`The instance ${typeId} doesn't exists`); + return singletons[typeId]; + }, + initialize() { + if (pending) + throw new Error("Cyclic reference detected"); + pending = true; + }, + store(item: any) { + singletons[typeId] = item; + pending = false; + } + }; + } + + static containerLifetime(container: Container) { + let _lifetime = unknownLifetime; + return { + initialize(context: ActivationContext) { + if (_lifetime !== unknownLifetime) + throw new Error("Cyclic reference detected"); + _lifetime = container.getLifetimeManager().create(); + }, + get() { + return _lifetime.get(); + }, + has() { + return _lifetime.has(); + }, + store(item: any) { + _lifetime.store(item); + } + }; + } +} 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,83 +1,71 @@ -import { isNull, argumentNotEmptyString, each } from "../safe"; +import { argumentNotEmptyString, each } from "../safe"; import { ActivationContext } from "./ActivationContext"; -import { ServiceMap, Descriptor } from "./interfaces"; -import { ActivationError } from "./ActivationError"; +import { Descriptor, PartialServiceMap, TypeOfService, ContainerKeys } from "./interfaces"; + +export interface ReferenceDescriptorParams> { + /** + * The name of the descriptor + */ + name: K; -export interface ReferenceDescriptorParams { - name: string; - lazy?: boolean; + /** + * The flag that indicates that the referenced service isn't required to exist. + * If the reference is optional and the referenced service doesn't exist, + * the undefined or a default value will be returned. + */ optional?: boolean; - default?; - services?: ServiceMap; + + /** + * a default value for the reference when the referenced service doesn't exist. + */ + default?: TypeOfService; + + /** + * The service overrides + */ + services?: PartialServiceMap; } -export class ReferenceDescriptor implements Descriptor { - _name: string; +export class ReferenceDescriptor = ContainerKeys> + implements Descriptor> { - _lazy = false; + _name: K; _optional = false; - _default: any; + _default: TypeOfService | undefined; - _services: ServiceMap; + _services: PartialServiceMap; - constructor(opts: ReferenceDescriptorParams) { + constructor(opts: ReferenceDescriptorParams) { argumentNotEmptyString(opts && opts.name, "opts.name"); this._name = opts.name; - this._lazy = !!opts.lazy; this._optional = !!opts.optional; this._default = opts.default; - this._services = opts.services; + + this._services = (opts.services || {}) as PartialServiceMap; } - activate(context: ActivationContext, name: string) { + /** This method activates the referenced service if one exists + * @param context activation context which is used during current activation + */ + activate(context: ActivationContext): any { // добавляем сервисы 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, v)); } - if (this._lazy) { - const saved = context.clone(); - - return (cfg: ServiceMap) => { - // защищаем контекст на случай исключения в процессе - // активации - const ct = saved.clone(); - try { - if (cfg) { - for (const k in cfg) - ct.register(k, cfg[k]); - } + const res = this._optional ? + context.resolve(this._name, this._default) : + context.resolve(this._name); - return this._optional ? ct.resolve(this._name, this._default) : ct - .resolve(this._name); - } catch (error) { - throw new ActivationError(this._name, ct.getStack(), error); - } - }; - } else { - // добавляем сервисы - if (this._services) { - for (const p of Object.keys(this._services)) - context.register(p, this._services[p]); - } - - const v = this._optional ? - context.resolve(this._name, this._default) : - context.resolve(this._name); - - return v; - } + return res; } toString() { const opts = []; if (this._optional) opts.push("optional"); - if (this._lazy) - opts.push("lazy"); const parts = [ "@ref " @@ -88,11 +76,11 @@ export class ReferenceDescriptor impleme parts.push("} "); } - parts.push(this._name); + parts.push(this._name.toString()); - if (!isNull(this._default)) { + if (this._default !== undefined && this._default !== null) { parts.push(" = "); - parts.push(this._default); + parts.push(String(this._default)); } return parts.join(""); diff --git a/src/main/ts/di/ServiceDescriptor.ts b/src/main/ts/di/ServiceDescriptor.ts --- a/src/main/ts/di/ServiceDescriptor.ts +++ b/src/main/ts/di/ServiceDescriptor.ts @@ -1,16 +1,17 @@ import { ActivationContext } from "./ActivationContext"; -import { Descriptor, ActivationType, ServiceMap, isDescriptor } from "./interfaces"; -import { Container } from "./Container"; -import { argumentNotNull, isPrimitive } from "../safe"; +import { Descriptor, ServiceMap, PartialServiceMap, ILifetime } from "./interfaces"; +import { isPrimitive, keys, isNull } from "../safe"; import { TraceSource } from "../log/TraceSource"; - -let cacheId = 0; +import { isDescriptor } from "./traits"; +import { LifetimeManager } from "./LifetimeManager"; +import { MatchingMemberKeys } from "../interfaces"; const trace = TraceSource.get("@implab/core/di/ActivationContext"); -function injectMethod(target, method, context, args) { +function injectMethod(target: T, method: M, context: ActivationContext, args: A) { + const m = target[method]; - if (!m) + if (!m || typeof m !== "function") throw new Error("Method '" + method + "' not found"); if (args instanceof Array) @@ -19,22 +20,22 @@ function injectMethod(target, method, co return m.call(target, _parse(args, context, "." + method)); } -function makeClenupCallback(target, method: ((instance) => void) | string) { - if (typeof (method) === "string") { - return () => { - target[method](); +function makeCleanupCallback(method: Cleaner) { + if (typeof (method) === "function") { + return (target: T) => { + method(target); }; } else { - return () => { - method(target); + return (target: T) => { + const m = target[method] as any; + m.apply(target); }; } } -// TODO: make async -function _parse(value, context: ActivationContext, path: string) { +function _parse(value: any, context: ActivationContext, path: string): any { if (isPrimitive(value)) - return value; + return value as any; trace.debug("parse {0}", path); @@ -42,178 +43,92 @@ function _parse(value, context: Activati return context.activate(value, path); if (value instanceof Array) - return value.map((x, i) => _parse(x, context, `${path}[${i}]`)); + return value.map((x, i) => _parse(x, context, `${path}[${i}]`)) as any; - const t = {}; - for (const p of Object.keys(value)) - t[p] = _parse(value[p], context, `${path}.${p}`); + const t: any = {}; + + keys(value).forEach(p => t[p] = _parse(value[p], context, `${path}.${p}`)); return t; } -export interface ServiceDescriptorParams { - activation?: ActivationType; +export type Cleaner = ((x: T) => void) | MatchingMemberKeys<() => void, T>; - owner: Container; +export type InjectionSpec = { + [m in keyof T]?: any; +}; - params?; +export interface ServiceDescriptorParams { + lifetime?: ILifetime; - inject?: object[]; + params?: P; - services?: ServiceMap; + inject?: InjectionSpec[]; - cleanup?: ((x) => void) | string; + services?: PartialServiceMap; + + cleanup?: Cleaner; } -export class ServiceDescriptor implements Descriptor { - _instance; - - _hasInstance = false; - - _activationType = ActivationType.Call; +export class ServiceDescriptor implements Descriptor { + _services: ServiceMap; - _services: ServiceMap; - - _params; + _params: P | undefined; - _inject: object[]; + _inject: InjectionSpec[]; - _cleanup: ((x) => void) | string; + _cleanup: ((item: T) => void) | undefined; - _cacheId: any; + _lifetime = LifetimeManager.empty(); - _owner: Container; + _objectLifetime: ILifetime | undefined; - constructor(opts: ServiceDescriptorParams) { - argumentNotNull(opts, "opts"); - argumentNotNull(opts.owner, "owner"); + constructor(opts: ServiceDescriptorParams) { - this._owner = opts.owner; + if (opts.lifetime) + this._lifetime = opts.lifetime; - if ("activation" in opts) - this._activationType = opts.activation; - - if ("params" in opts) + if (!isNull(opts.params)) this._params = opts.params; - if (opts.inject) - this._inject = opts.inject; + this._inject = opts.inject || []; - if (opts.services) - this._services = opts.services; + this._services = (opts.services || {}) as ServiceMap; 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._cleanup = makeCleanupCallback(opts.cleanup); } } - activate(context: ActivationContext) { - // if we have a local service records, register them first - let instance; - - // ensure we have a cache id - if (!this._cacheId) - this._cacheId = ++cacheId; - - 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 - const container = context.container.getRootContainer(); - - if (container.has(this._cacheId)) { - instance = container.get(this._cacheId); - } else { - instance = this._create(context); - container.store(this._cacheId, instance); - if (this._cleanup) - container.onDispose( - makeClenupCallback(instance, this._cleanup)); - } - - this._hasInstance = true; - return (this._instance = instance); - - case ActivationType.Container: // CONTAINER - // return a cached value - - if (this._hasInstance) - return this._instance; + activate(context: ActivationContext) { + const lifetime = this._lifetime; - // create an instance - instance = this._create(context); - - // the instance is bound to the container - if (this._cleanup) - this._owner.onDispose( - makeClenupCallback(instance, this._cleanup)); - - // cache and return the instance - this._hasInstance = true; - return (this._instance = instance); - 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)); - case ActivationType.Call: // CALL - // per-call created instances are controlled by callers - return this._create(context); - 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); - - if (this._cleanup) - context.container.onDispose(makeClenupCallback( - instance, - this._cleanup)); - - return context.container.store(this._cacheId, instance); - default: - throw new Error("Invalid activation type: " + this._activationType); + if (lifetime.has()) { + return lifetime.get(); + } else { + lifetime.initialize(context); + const instance = this._create(context); + lifetime.store(instance, this._cleanup); + return instance; } } - isInstanceCreated() { - return this._hasInstance; - } - - getInstance() { - return this._instance; - } - - _factory(...params: any[]): any { + _factory(...params: any[]): T { throw Error("Not implemented"); } - _create(context: ActivationContext) { + _create(context: ActivationContext) { trace.debug(`constructing ${context._name}`); - if (this._activationType !== ActivationType.Call && - context.visit(this._cacheId) > 0) - throw new Error("Recursion detected"); - if (this._services) { - for (const p in this._services) - context.register(p, this._services[p]); + keys(this._services).forEach(p => context.register(p, this._services[p])); } - let instance; + let instance: T; if (this._params === undefined) { instance = this._factory(); @@ -231,4 +146,9 @@ export class ServiceDescriptor implement } return instance; } + + clone() { + return Object.create(this); + } + } diff --git a/src/main/ts/di/TypeServiceDescriptor.ts b/src/main/ts/di/TypeServiceDescriptor.ts --- a/src/main/ts/di/TypeServiceDescriptor.ts +++ b/src/main/ts/di/TypeServiceDescriptor.ts @@ -1,15 +1,14 @@ import { ServiceDescriptor, ServiceDescriptorParams } from "./ServiceDescriptor"; -import { Constructor, Factory } from "../interfaces"; import { argumentNotNull, isPrimitive } from "../safe"; -export interface TypeServiceDescriptorParams extends ServiceDescriptorParams { - type: Constructor; +export interface TypeServiceDescriptorParams extends ServiceDescriptorParams { + type: new (...args: any[]) => T; } -export class TypeServiceDescriptor extends ServiceDescriptor { - _type: Constructor; +export class TypeServiceDescriptor extends ServiceDescriptor { + _type: new (...args: any[]) => T; - constructor(opts: TypeServiceDescriptorParams) { + constructor(opts: TypeServiceDescriptorParams) { super(opts); argumentNotNull(opts && opts.type, "opts.type"); @@ -18,9 +17,10 @@ export class TypeServiceDescriptor exten if (this._params) { if (this._params.length) { this._factory = (...args) => { - const t = Object.create(ctor.prototype); + /*const t = Object.create(ctor.prototype); const inst = ctor.apply(t, args); - return isPrimitive(inst) ? t : inst; + return isPrimitive(inst) ? t : inst;*/ + return new ctor(...args); }; } else { this._factory = arg => { diff --git a/src/main/ts/di/ValueDescriptor.ts b/src/main/ts/di/ValueDescriptor.ts --- a/src/main/ts/di/ValueDescriptor.ts +++ b/src/main/ts/di/ValueDescriptor.ts @@ -1,9 +1,9 @@ import { Descriptor } from "./interfaces"; -export class ValueDescriptor implements Descriptor { - _value; +export class ValueDescriptor implements Descriptor { + _value: T; - constructor(value) { + constructor(value: T) { this._value = value; } diff --git a/src/main/ts/di/fluent/DescriptorBuilder.ts b/src/main/ts/di/fluent/DescriptorBuilder.ts new file mode 100644 --- /dev/null +++ b/src/main/ts/di/fluent/DescriptorBuilder.ts @@ -0,0 +1,147 @@ +import { Resolver, RegistrationBuilder } from "./interfaces"; +import { Container } from "../Container"; +import { Descriptor, ILifetime, ActivationType, PartialServiceMap } from "../interfaces"; +import { DescriptorImpl } from "./DescriptorImpl"; +import { LifetimeManager } from "../LifetimeManager"; +import { isString, each, isPrimitive, isPromise, oid } from "../../safe"; + +export class DescriptorBuilder { + private readonly _container: Container; + private readonly _cb: (d: Descriptor) => void; + + private readonly _eb: (err: any) => void; + + private _lifetime = LifetimeManager.empty(); + + private _overrides?: PartialServiceMap; + + private _cleanup?: (item: T) => void; + + private _factory?: (resolve: Resolver) => T; + + private _pending = 1; + + private _failed = false; + + constructor(container: Container, cb: (d: Descriptor) => void, eb: (err: any) => void) { + this._container = container; + this._cb = cb; + this._eb = eb; + } + + build(): DescriptorBuilder { + this._defer(); + return new DescriptorBuilder(this._container, () => this._complete(), err => this._fail(err)); + } + + override(name: K, builder: RegistrationBuilder): this; + override(services: { [name in K]: RegistrationBuilder }): this; + override(nameOrServices: K | { [name in K]: RegistrationBuilder }, builder?: RegistrationBuilder): this { + const overrides: PartialServiceMap = this._overrides ? + this._overrides : + (this._overrides = {}); + + const guard = (v: void | Promise) => { + if (isPromise(v)) + v.catch(err => this._fail(err)); + }; + + if (isPrimitive(nameOrServices)) { + if (builder) { + this._defer(); + const d = new DescriptorBuilder( + this._container, + result => { + overrides[nameOrServices] = result; + this._complete(); + }, + err => this._fail(err) + ); + + try { + guard(builder(d)); + } catch (err) { + this._fail(err); + } + } + } else { + each(nameOrServices, (v, k) => this.override(k, v)); + } + return this; + } + + lifetime(lifetime: "singleton", typeId: string): this; + lifetime(lifetime: ILifetime | Exclude): this; + lifetime(lifetime: ILifetime | ActivationType, typeId?: string): this { + if (isString(lifetime)) { + this._lifetime = this._resolveLifetime(lifetime, typeId); + } else { + this._lifetime = lifetime; + } + return this; + } + + cleanup(cb: (item: T) => void): this { + this._cleanup = cb; + return this; + } + + factory(f: (resolve: Resolver) => T): void { + this._factory = f; + this._complete(); + } + + value(v: T): void { + this._cb({ + activate() { + return v; + } + }); + } + + _resolveLifetime(activation: ActivationType, typeId?: string | object) { + switch (activation) { + case "container": + return LifetimeManager.containerLifetime(this._container); + case "hierarchy": + return LifetimeManager.hierarchyLifetime(); + case "context": + return LifetimeManager.contextLifetime(); + case "singleton": + if (!typeId) + throw Error("The singleton activation requires a typeId"); + + const _oid = isString(typeId) ? typeId : oid(typeId); + + return LifetimeManager.singletonLifetime(_oid); + default: + return LifetimeManager.empty(); + } + } + + _defer() { + this._pending++; + } + + _complete() { + if (--this._pending === 0) { + if (!this._factory) + throw new Error("The factory must be specified"); + + this._cb(new DescriptorImpl({ + lifetime: this._lifetime, + factory: this._factory, + overrides: this._overrides, + cleanup: this._cleanup + })); + } + } + + _fail(err: any) { + if (!this._failed) { + this._failed = true; + this._eb.call(undefined, err); + } + } + +} diff --git a/src/main/ts/di/fluent/DescriptorImpl.ts b/src/main/ts/di/fluent/DescriptorImpl.ts new file mode 100644 --- /dev/null +++ b/src/main/ts/di/fluent/DescriptorImpl.ts @@ -0,0 +1,63 @@ +import { Descriptor, PartialServiceMap, ILifetime, ContainerKeys } from "../interfaces"; +import { ActivationContext } from "../ActivationContext"; +import { each } from "../../safe"; +import { DependencyOptions, LazyDependencyOptions, Resolver } from "./interfaces"; + +export interface DescriptorImplArgs { + lifetime: ILifetime; + + factory: (resolve: Resolver) => T; + + cleanup?: (item: T) => void; + + overrides?: PartialServiceMap; +} + +export class DescriptorImpl implements Descriptor { + + private readonly _overrides?: PartialServiceMap; + + private readonly _lifetime: ILifetime; + + private readonly _factory: (resolve: Resolver) => T; + + private readonly _cleanup?: (item: T) => void; + + constructor(args: DescriptorImplArgs) { + this._lifetime = args.lifetime; + this._factory = args.factory; + if (args.cleanup) + this._cleanup = args.cleanup; + if (args.overrides) + this._overrides = args.overrides; + } + + activate(context: ActivationContext): T { + + if (this._lifetime.has()) + return this._lifetime.get(); + + this._lifetime.initialize(context); + + if (this._overrides) + each(this._overrides, (v, k) => context.register(k, v)); + + const resolve = (name: ContainerKeys, opts?: DependencyOptions | LazyDependencyOptions) => { + if (opts && "lazy" in opts && opts.lazy) { + const c2 = context.clone(); + return () => { + return opts.optional ? c2.resolve(name, opts.default) : c2.resolve(name); + }; + } else { + return opts && opts.optional ? context.resolve(name, opts.default) : context.resolve(name); + } + }; + + const instance = this._factory.call(undefined, resolve); + + this._lifetime.store(instance, this._cleanup); + + return instance; + } + +} diff --git a/src/main/ts/di/fluent/FluentConfiguration.ts b/src/main/ts/di/fluent/FluentConfiguration.ts new file mode 100644 --- /dev/null +++ b/src/main/ts/di/fluent/FluentConfiguration.ts @@ -0,0 +1,60 @@ +import { Container } from "../Container"; +import { argumentNotNull, each, isPrimitive, isPromise } from "../../safe"; +import { DescriptorBuilder } from "./DescriptorBuilder"; +import { RegistrationBuilder, FluentRegistrations } from "./interfaces"; +import { Cancellation } from "../../Cancellation"; + +export class FluentConfiguration { + + _builders: { [k in keyof S]?: RegistrationBuilder } = {}; + + register(name: K, builder: RegistrationBuilder): FluentConfiguration>; + register(config: FluentRegistrations): FluentConfiguration>; + register(nameOrConfig: K | FluentRegistrations, builder?: RegistrationBuilder): FluentConfiguration> { + if (isPrimitive(nameOrConfig)) { + argumentNotNull(builder, "builder"); + this._builders[nameOrConfig] = builder; + } else { + each(nameOrConfig, (v, k) => this.register(k, v)); + } + + return this; + } + + apply(target: Container, ct = Cancellation.none) { + + let pending = 1; + + const _t2 = target as unknown as Container; + + return new Promise>((resolve, reject) => { + function guard(v: void | Promise) { + if (isPromise(v)) + v.catch(reject); + } + + function complete() { + if (!--pending) + resolve(_t2); + } + each(this._builders, (v, k) => { + pending++; + const d = new DescriptorBuilder(_t2, + result => { + _t2.register(k, result); + complete(); + }, + reject + ); + + try { + guard(v(d, ct)); + } catch (e) { + reject(e); + } + }); + complete(); + }); + } + +} diff --git a/src/main/ts/di/fluent/interfaces.ts b/src/main/ts/di/fluent/interfaces.ts new file mode 100644 --- /dev/null +++ b/src/main/ts/di/fluent/interfaces.ts @@ -0,0 +1,52 @@ +import { primitive } from "../../safe"; +import { TypeOfService, ContainerKeys, ActivationType, ILifetime } from "../interfaces"; +import { ICancellation } from "../../interfaces"; + +export interface DependencyOptions { + optional?: boolean; + default?: any; +} + +export interface LazyDependencyOptions extends DependencyOptions { + lazy: true; +} + +export type ExtractService = K extends keyof S ? S[K] : never; + +export type ExtractDependency = D extends { $dependency: infer K } ? + D extends { lazy: true } ? () => ExtractService : ExtractService : + D extends { $type: new (...args: any[]) => infer I } ? I : + D extends { $factory: (...args: any[]) => infer R } ? R : + WalkDependencies; + +export type WalkDependencies = D extends primitive ? D : + { [K in keyof D]: ExtractDependency }; + +export type InferReferenceType, O> = O extends { default: infer X } ? (TypeOfService | X) : + O extends { optional: true } ? (TypeOfService | undefined) : + TypeOfService; + +export interface Resolver { + , O extends LazyDependencyOptions>(this: void, name: K, opts: O): () => InferReferenceType; + , O extends DependencyOptions>(this: void, name: K, opts?: O): InferReferenceType; +} + +export interface DescriptorBuilder { + factory(f: (resolve: Resolver) => T): void; + + build(): DescriptorBuilder; + + override(name: K, builder: RegistrationBuilder): this; + override(services: { [name in K]: RegistrationBuilder }): this; + + lifetime(lifetime: "singleton", typeId: any): this; + lifetime(lifetime: ILifetime | Exclude): this; + + cleanup(cb: (item: T) => void): this; + + value(v: T): void; +} + +export type RegistrationBuilder = (d: DescriptorBuilder, ct?: ICancellation) => void | Promise; + +export type FluentRegistrations = { [k in K]: RegistrationBuilder }; diff --git a/src/main/ts/di/interfaces.ts b/src/main/ts/di/interfaces.ts --- a/src/main/ts/di/interfaces.ts +++ b/src/main/ts/di/interfaces.ts @@ -1,75 +1,53 @@ -import { isNull, isPrimitive } from "../safe"; import { ActivationContext } from "./ActivationContext"; -import { Constructor, Factory } from "../interfaces"; - -export interface Descriptor { - activate(context: ActivationContext, name?: string); -} - -export function isDescriptor(x): x is Descriptor { - return (!isPrimitive(x)) && - (x.activate instanceof Function); -} -export interface ServiceMap { - [s: string]: Descriptor; -} - -export enum ActivationType { - Singleton = 1, - Container, - Hierarchy, - Context, - Call -} - -export interface RegistrationWithServices { - services?: object; +export interface Descriptor { + activate(context: ActivationContext): T; } -export interface ServiceRegistration extends RegistrationWithServices { - - activation?: "singleton" | "container" | "hierarchy" | "context" | "call"; +export type ServiceMap = { + [k in keyof S]: Descriptor; +}; - params?; +export type ContainerKeys = keyof S | keyof ContainerProvided; - inject?: object | object[]; +export type TypeOfService = + K extends keyof ContainerProvided ? ContainerProvided[K] : + K extends keyof S ? S[K] : never; - cleanup?: (instance) => void | string; -} +export type ContainerServiceMap = { + [K in ContainerKeys]: Descriptor>; +}; -export interface TypeRegistration extends ServiceRegistration { - $type: string | Constructor; -} +export type PartialServiceMap = { + [k in keyof S]?: Descriptor; +}; -export interface FactoryRegistration extends ServiceRegistration { - $factory: string | Factory; +export interface ServiceLocator { + resolve>(name: K, def?: TypeOfService): TypeOfService; } -export interface ValueRegistration { - $value; - parse?: boolean; -} - -export interface DependencyRegistration extends RegistrationWithServices { - $dependency: string; - lazy?: boolean; - optional?: boolean; - default?; +export interface ContainerProvided { + container: ServiceLocator; } -export function isTypeRegistration(x): x is TypeRegistration { - return (!isPrimitive(x)) && ("$type" in x); -} +export type ContainerRegistered = /*{ + [K in Exclude>]: S[K]; +};*/ + Exclude>; -export function isFactoryRegistration(x): x is FactoryRegistration { - return (!isPrimitive(x)) && ("$factory" in x); -} +export type ActivationType = "singleton" | "container" | "hierarchy" | "context" | "call"; -export function isValueRegistration(x): x is ValueRegistration { - return (!isPrimitive(x)) && ("$value" in x); -} +/** + * Интерфейс для управления жизнью экземпляра объекта. Каждая регистрация имеет + * свой собственный объект `ILifetime`, который создается при первой активации + */ +export interface ILifetime { + /** Проверяет, что уже создан экземпляр объекта */ + has(): boolean; -export function isDependencyRegistration(x): x is DependencyRegistration { - return (!isPrimitive(x)) && ("$dependency" in x); + get(): any; + + initialize(context: ActivationContext): void; + + store(item: any, cleanup?: (item: any) => void): void; } diff --git a/src/main/ts/di/traits.ts b/src/main/ts/di/traits.ts new file mode 100644 --- /dev/null +++ b/src/main/ts/di/traits.ts @@ -0,0 +1,11 @@ +import { isPrimitive } from "../safe"; +import { Descriptor } from "./interfaces"; +import { FluentConfiguration } from "./fluent/FluentConfiguration"; +export function isDescriptor(x: any): x is Descriptor { + return (!isPrimitive(x)) && + (x.activate instanceof Function); +} + +export function fluent() { + return new FluentConfiguration(); +} diff --git a/src/main/ts/interfaces.ts b/src/main/ts/interfaces.ts --- a/src/main/ts/interfaces.ts +++ b/src/main/ts/interfaces.ts @@ -3,10 +3,20 @@ export interface Constructor { prototype: T; } +export type PromiseOrValue = T | PromiseLike; + export type Factory = (...args: any[]) => T; export type Predicate = (x: T) => boolean; +export type MatchingMemberKeys = { [K in keyof From]: From[K] extends T ? K : never}[keyof From]; + +export type NotMatchingMemberKeys = { [K in keyof From]: From[K] extends T ? never : K}[keyof From]; + +export type ExtractMembers = Pick>; + +export type ExcludeMembers = Pick>; + export interface MapOf { [key: string]: T; } @@ -55,7 +65,10 @@ export interface IActivatable { * can be activated and manages the active state of the * component */ - setActivationController(controller: IActivationController); + setActivationController(controller: IActivationController): void; + + /** Indicates whether this component has an activation controller */ + hasActivationController(): boolean; /** * Gets the current activation controller for this component @@ -76,6 +89,8 @@ export interface IActivationController { activate(component: IActivatable, ct?: ICancellation): Promise; + hasActive(): boolean; + getActive(): IActivatable; } diff --git a/src/main/ts/log/Registry.ts b/src/main/ts/log/Registry.ts --- a/src/main/ts/log/Registry.ts +++ b/src/main/ts/log/Registry.ts @@ -1,15 +1,15 @@ import { TraceSource } from "./TraceSource"; import { argumentNotNull } from "../safe"; -import { IDestroyable } from "../interfaces"; +import { IDestroyable, MapOf } from "../interfaces"; export class Registry { static readonly instance = new Registry(); - private _registry: object = new Object(); - private _listeners: object = new Object(); + private _registry: MapOf = {}; + private _listeners: MapOf<(source: TraceSource) => void> = {}; private _nextCookie: number = 1; - get(id: any): TraceSource { + get(id: string): TraceSource { argumentNotNull(id, "id"); if (this._registry[id]) diff --git a/src/main/ts/log/TraceEventData.ts b/src/main/ts/log/TraceEventData.ts --- a/src/main/ts/log/TraceEventData.ts +++ b/src/main/ts/log/TraceEventData.ts @@ -5,7 +5,7 @@ export class TraceEventData implements T source: TraceSource; level: number; message: any; - args?: any[]; + args: any[]; constructor(source: TraceSource, level: number, message: any, args: any[]) { this.source = source; diff --git a/src/main/ts/log/TraceSource.ts b/src/main/ts/log/TraceSource.ts --- a/src/main/ts/log/TraceSource.ts +++ b/src/main/ts/log/TraceSource.ts @@ -1,6 +1,7 @@ import { Observable } from "../Observable"; import { Registry } from "./Registry"; import { TraceEventData } from "./TraceEventData"; +import { EventProvider } from "../EventProvider"; export const DebugLevel = 400; @@ -25,22 +26,21 @@ export interface TraceEvent { export class TraceSource { readonly id: any; - level: number; + level = 0; readonly events: Observable; - _notifyNext: (arg: TraceEvent) => void; + private readonly _provider: EventProvider; - constructor(id: any) { + constructor(id?: any) { this.id = id || new Object(); - this.events = new Observable(next => { - this._notifyNext = next; - }); + this._provider = new EventProvider(); + this.events = this._provider.getObservable(); } - protected emit(level: number, message: any, args?: any[]) { - this._notifyNext(new TraceEventData(this, level, message, args)); + protected emit(level: number, message: any, args: any[]) { + this._provider.post(new TraceEventData(this, level, message, args)); } isDebugEnabled() { diff --git a/src/main/ts/log/writers/ConsoleLogger.ts b/src/main/ts/log/writers/ConsoleLogger.ts --- a/src/main/ts/log/writers/ConsoleLogger.ts +++ b/src/main/ts/log/writers/ConsoleLogger.ts @@ -32,7 +32,11 @@ export class ConsoleLogger implements ID this._writer.setLogLevel("error"); } this._writer.write("{0}: ", next.source.id); - this._writer.writeLine(next.message, ...next.args); + + if (next.args) + this._writer.writeLine(next.message, ...next.args); + else + this._writer.writeLine(next.message); } destroy() { diff --git a/src/main/ts/log/writers/ConsoleWriter.ts b/src/main/ts/log/writers/ConsoleWriter.ts --- a/src/main/ts/log/writers/ConsoleWriter.ts +++ b/src/main/ts/log/writers/ConsoleWriter.ts @@ -1,1 +1,1 @@ -export { ConsoleLogger as ConsoleWriter } from "./ConsoleLogger"; \ No newline at end of file +export { ConsoleLogger as ConsoleWriter } from "./ConsoleLogger"; 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 @@ -1,4 +1,4 @@ -import { ICancellable, Constructor } from "./interfaces"; +import { ICancellable, Constructor, IDestroyable, PromiseOrValue } from "./interfaces"; import { Cancellation } from "./Cancellation"; let _nextOid = 0; @@ -6,9 +6,11 @@ const _oid = typeof Symbol === "function Symbol("__implab__oid__") : "__implab__oid__"; -export function oid(instance: object): string { +export function oid(instance: null | undefined): undefined; +export function oid(instance: NonNullable): string; +export function oid(instance: any): string | undefined { if (isNull(instance)) - return null; + return undefined; if (_oid in instance) return instance[_oid]; @@ -16,6 +18,14 @@ 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 isKeyof(k: string, target: T): k is Extract { + return target && typeof target === "object" && k in target; +} + 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,11 +46,17 @@ export function argumentOfType(arg: any, throw new Error("The argument '" + name + "' type doesn't match"); } -export function isNull(val: any) { +export function isObject(val: any): val is object { + return typeof val === "object"; +} + +export function isNull(val: any): val is null | undefined { return (val === null || val === undefined); } -export function isPrimitive(val: any): val is string | number | boolean | undefined | null { +export type primitive = symbol | string | number | boolean | undefined | null; + +export function isPrimitive(val: any): val is primitive { return (val === null || val === undefined || typeof (val) === "string" || typeof (val) === "number" || typeof (val) === "boolean"); } @@ -57,7 +73,7 @@ export function isString(val: any): val return typeof (val) === "string" || val instanceof String; } -export function isPromise(val: any): val is PromiseLike { +export function isPromise(val: any): val is PromiseLike { return val && typeof val.then === "function"; } @@ -65,21 +81,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 || - ((typeof (val) === "string" || val instanceof String) && val.length === 0)) - return true; +export function isNullOrEmptyString(val: any): val is ("" | null | undefined) { + return (val === null || val === undefined || + ((typeof (val) === "string" || val instanceof String) && val.length === 0)); } -export function isNotEmptyArray(arg: any): arg is Array { +export function isNotEmptyArray(arg: any): arg is T[] { return (arg instanceof Array && arg.length > 0); } -function _isStrictMode() { +function _isStrictMode(this: any) { return !this; } -function _getNonStrictGlobal() { +function _getNonStrictGlobal(this: any) { return this; } @@ -116,24 +131,22 @@ export function get(member: string, cont * @param {Function} cb функция, вызываемая для каждого элемента * @param {Object} thisArg значение, которое будет передано в качестве * this в cb. - * @returns Результат вызова функции cb, либо undefined - * если достигнут конец массива. + * @returns {void} */ -export function each(obj, cb, thisArg?) { +export function each(obj: T, cb: (v: NonNullable, k: X) => void): void; +export function each(array: T[], cb: (v: T, i: number) => 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) { + let v: any; for (let i = 0; i < obj.length; i++) { - const x = cb.call(thisArg, obj[i], i); - if (x !== undefined) - return x; + v = obj[i]; + if (v !== undefined) + cb.call(thisArg, v, i); } } else { - const keys = Object.keys(obj); - for (const k of keys) { - const x = cb.call(thisArg, obj[k], k); - if (x !== undefined) - return x; - } + Object.keys(obj).forEach(k => obj[k] !== undefined && cb.call(thisArg, obj[k], k)); } } @@ -153,28 +166,27 @@ export function each(obj, cb, thisArg?) * own properties of the source are entirely copied to the destination. * */ -export function mixin(dest: T, source: S, template?: string[] | object): T & S { - argumentNotNull(dest, "to"); - const _res = dest as T & S; +export function mixin(dest: T, source: S, template?: keyof S[]): T & S; +export function mixin(dest: T, source: S, template: { [p in keyof S]?: keyof R; }): T & R; +export function mixin(dest: T, source: S, template?: any): any { + argumentNotNull(dest, "dest"); + const _res: any = dest as any; if (isPrimitive(source)) return _res; if (template instanceof Array) { - for (const p of template) { - if (p in source) + template.forEach(p => { + if (isKeyof(p, source)) _res[p] = source[p]; - } + }); } else if (template) { - const keys = Object.keys(source); - for (const p of keys) { - if (p in template) + keys(source).forEach(p => { + if (isKeyof(p, template)) _res[template[p]] = source[p]; - } + }); } else { - const keys = Object.keys(source); - for (const p of keys) - _res[p] = source[p]; + keys(source).forEach(p => _res[p] = source[p]); } return _res; @@ -184,7 +196,15 @@ export function mixin any, thisArg): (...args: any[]) => PromiseLike { +export function async T | PromiseLike>( + fn: F, + thisArg?: ThisParameterType +): (...args: Parameters) => PromiseLike; +export function async T | PromiseLike }>( + fn: M, + thisArg: O +): (...args: Parameters>) => PromiseLike; +export function async(_fn: any, thisArg: any): (...args: any[]) => PromiseLike { let fn = _fn; if (arguments.length === 2 && !(fn instanceof Function)) @@ -193,7 +213,7 @@ export function async(_fn: (...args: any if (fn == null) throw new Error("The function must be specified"); - function wrapresult(x, e?): PromiseLike { + function wrapresult(x: any, e?: any): PromiseLike { if (e) { return { then(cb, eb) { @@ -228,10 +248,16 @@ export function async(_fn: (...args: any }; } -type _AnyFn = (...args) => any; - -export function delegate(target: T, _method: (K | _AnyFn)) { - let method; +export function delegate any>( + target: T, + method: F +): OmitThisParameter; +export function delegate any; }>( + target: T, + method: M +): OmitThisParameter; +export function delegate(target: any, _method: any): (...args: any[]) => any { + let method: any; if (!(_method instanceof Function)) { argumentNotNull(target, "target"); method = target[_method]; @@ -262,6 +288,19 @@ export function delay(timeMs: number, ct }); } +/** Returns resolved promise, awaiting this method will cause the asynchronous + * completion of the rest of the code. + */ +export function fork() { + return Promise.resolve(); +} + +/** Always throws Error, can be used as a stub for the methods which should be + * assigned later and are required to be not null. + */ +export function notImplemented(): never { + throw new Error("Not implemeted"); +} /** * Iterates over the specified array of items and calls the callback `cb`, if * the result of the callback is a promise the next item from the array will be @@ -284,7 +323,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 +358,7 @@ export function pfor( let i = 0; - const next = () => { + const next = (): any => { while (i < items.length) { const r = cb(items[i], i); i++; @@ -336,7 +375,7 @@ export function first(sequence: Array export function first(sequence: PromiseLike>): PromiseLike; export function first( sequence: ArrayLike | PromiseLike>, - cb: (x: T) => void, + cb?: (x: T) => void, err?: (x: Error) => void ): void; /** @@ -358,7 +397,7 @@ export function first( err?: (x: Error) => void ) { if (isPromise(sequence)) { - return sequence.then(res => first(res, cb, err)); + return sequence.then(res => first(res, cb as any /* force to pass undefined cb */, err)); } else if (sequence && "length" in sequence) { if (sequence.length === 0) { if (err) @@ -400,7 +439,12 @@ export function firstWhere( err?: (x: Error) => any ) { if (isPromise(sequence)) { - return sequence.then(res => firstWhere(res, predicate, cb, err)); + return sequence.then(res => firstWhere( + res, + predicate as any /* force to pass undefined predicate */, + cb as any /* force to pass undefined cb */, + err) + ); } else if (sequence && "length" in sequence) { if (sequence.length === 0) { if (err) @@ -430,6 +474,12 @@ export function firstWhere( } } +export function isDestroyable(d: any): d is IDestroyable { + if (d && "destroy" in d && typeof (destroy) === "function") + return true; + return false; +} + export function destroy(d: any) { if (d && "destroy" in d) d.destroy(); diff --git a/src/main/ts/text/Converter.ts b/src/main/ts/text/Converter.ts --- a/src/main/ts/text/Converter.ts +++ b/src/main/ts/text/Converter.ts @@ -3,9 +3,9 @@ import { isPrimitive, isNull } from "../ export class Converter { static readonly default = new Converter(); - convert(value: any, pattern: string) { + convert(value: any, pattern?: string) { if (pattern && pattern.toLocaleLowerCase() === "json") { - const seen = []; + const seen: any[] = []; return JSON.stringify(value, (k, v) => { if (!isPrimitive(v)) { const id = seen.indexOf(v); diff --git a/src/main/ts/text/FormatCompiler.ts b/src/main/ts/text/FormatCompiler.ts --- a/src/main/ts/text/FormatCompiler.ts +++ b/src/main/ts/text/FormatCompiler.ts @@ -6,29 +6,37 @@ type CompiledPattern = (writer: TextWrit export class FormatCompiler { _scanner: FormatScanner; - _cache: MapOf = {}; + static _cache: MapOf = {}; + + _parts: Array; + + constructor(scanner: FormatScanner) { + this._scanner = scanner; + this._parts = []; + } - _parts: Array; + compile() { + this.visitText(); + const parts = this._parts; - compile(pattern: string) { + return (writer: TextWriter, args: any) => { + parts.forEach(x => { + if (isPrimitive(x)) + writer.writeValue(x); + else + writer.writeValue(get(x.name, args), x.format); + }); + }; + } + + static compile(pattern: string) { let compiledPattern = this._cache && this._cache[pattern]; if (!compiledPattern) { - this._scanner = new FormatScanner(pattern); - this._parts = []; - - this.visitText(); - const parts = this._parts; + const compiler = new this(new FormatScanner(pattern)); - compiledPattern = (writer: TextWriter, args: any) => { - parts.forEach(x => { - if (isPrimitive(x)) - writer.writeValue(x); - else - writer.writeValue(get(x.name, args), x.format); - }); - }; - if (this._cache) - this._cache[pattern] = compiledPattern; + compiledPattern = compiler.compile(); + + this._cache[pattern] = compiledPattern; } return compiledPattern; } @@ -73,7 +81,7 @@ export class FormatCompiler { this.dieUnexpectedToken("TEXT"); const fieldName = this._scanner.getTokenValue(); - const filedFormat = this.readColon() ? this.readFieldFormat() : null; + const filedFormat = this.readColon() ? this.readFieldFormat() : undefined; if (this._scanner.getTokenType() !== TokeType.CurlClose) this.dieUnexpectedToken("}"); @@ -104,7 +112,7 @@ export class FormatCompiler { return true; } - pushSubst(fieldName: string, filedFormat: string) { + pushSubst(fieldName: string, filedFormat?: string) { // console.log("pushSubst ", fieldName, filedFormat); this._parts.push({ name: fieldName, format: filedFormat }); } @@ -113,14 +121,14 @@ export class FormatCompiler { this._parts.push(text); } - dieUnexpectedToken(expected?: string) { + dieUnexpectedToken(expected?: string): never { throw new Error(isNullOrEmptyString(expected) ? `Unexpected token ${this._scanner.getTokenValue()}` : `Unexpected token ${this._scanner.getTokenValue()}, expected ${expected}` ); } - dieUnexpectedEnd(expected?: string) { + dieUnexpectedEnd(expected?: string): never { throw new Error(isNullOrEmptyString(expected) ? "Unexpected end of the string" : `Unexpected end of the string, expected ${expected}`); } } diff --git a/src/main/ts/text/FormatScanner.ts b/src/main/ts/text/FormatScanner.ts --- a/src/main/ts/text/FormatScanner.ts +++ b/src/main/ts/text/FormatScanner.ts @@ -16,8 +16,8 @@ const typeMap = { export class FormatScanner { private _text: string; - private _tokenType: TokeType; - private _tokenValue: string; + private _tokenType: TokeType | undefined; + private _tokenValue: string | undefined; private _rx = /[^{}:]+|(.)/g; constructor(text: string) { @@ -30,6 +30,9 @@ export class FormatScanner { return false; const match = this._rx.exec(this._text); + if (match === null) + return false; + this._tokenType = typeMap[match[1]] || TokeType.Text; this._tokenValue = match[0]; @@ -37,10 +40,15 @@ export class FormatScanner { } getTokenValue() { + if (this._tokenValue === undefined) + throw new Error("The scanner is before the first element"); return this._tokenValue; } getTokenType() { + + if (this._tokenType === undefined) + throw new Error("The scanner is before the first element"); return this._tokenType; } } diff --git a/src/main/ts/text/StringBuilder.ts b/src/main/ts/text/StringBuilder.ts --- a/src/main/ts/text/StringBuilder.ts +++ b/src/main/ts/text/StringBuilder.ts @@ -26,6 +26,6 @@ const sb = new StringBuilder(); export function format(format: string, ...args: any): string; export function format() { sb.clear(); - sb.write.apply(sb, arguments); + sb.write.apply(sb, arguments); return sb.toString(); } diff --git a/src/main/ts/text/StringFormat.ts b/src/main/ts/text/StringFormat.ts --- a/src/main/ts/text/StringFormat.ts +++ b/src/main/ts/text/StringFormat.ts @@ -1,8 +1,8 @@ -import { isPrimitive, isNull, each } from "../safe"; +import { isPrimitive, isNull, isKeyof, get } from "../safe"; import { MapOf } from "../interfaces"; type SubstFn = (name: string, format?: string) => string; -type TemplateFn = (subst: SubstFn) => string; +type TemplateFn = (subst: SubstFn) => string | undefined; type ConvertFn = (value: any, format?: string) => string; const map = { @@ -28,19 +28,19 @@ function espaceString(s: string) { function encode(s: string) { if (!s) return s; - return s.replace(/\\{|\\}|&|\\:|\n/g, m => map[m] || m); + return s.replace(/\\{|\\}|&|\\:|\n/g, m => isKeyof(m, map) ? map[m] : m); } function decode(s: string) { if (!s) return s; - return s.replace(/&(\w+);/g, (m, $1) => rev[$1] || m); + return s.replace(/&(\w+);/g, (m, $1) => isKeyof($1, rev) ? rev[$1] : m); } function subst(s: string) { const i = s.indexOf(":"); let name: string; - let pattern: string; + let pattern: string | undefined; if (i >= 0) { name = s.substr(0, i); pattern = s.substr(i + 1); @@ -51,7 +51,8 @@ function subst(s: string) { if (pattern) return [ espaceString(decode(name)), - espaceString(decode(pattern))]; + espaceString(decode(pattern)) + ]; else return [espaceString(decode(name))]; } @@ -103,9 +104,9 @@ export function compile(template: string return compiled; } -function defaultConverter(value: any, pattern: string) { +function defaultConverter(value: any, pattern?: string) { if (pattern && pattern.toLocaleLowerCase() === "json") { - const seen = []; + const seen: any = []; return JSON.stringify(value, (k, v) => { if (!isPrimitive(v)) { const id = seen.indexOf(v); @@ -113,10 +114,10 @@ function defaultConverter(value: any, pa return "@ref-" + id; else { seen.push(v); - return v; + return v.toString() as string; } } else { - return v; + return isNull(v) ? "" : v.toString(); } }, 2); } else if (isNull(value)) { @@ -124,7 +125,7 @@ function defaultConverter(value: any, pa } else if (value instanceof Date) { return value.toISOString(); } else { - return value.toString(); + return value.toString() as string; } } @@ -136,7 +137,7 @@ export class Formatter { this._converters.push(defaultConverter); } - convert(value: any, pattern: string) { + convert(value: any, pattern?: string) { for (const c of this._converters) { const res = c(value, pattern); if (!isNull(res)) @@ -149,7 +150,7 @@ export class Formatter { const template = compile(msg); return template((name, pattern) => { - const value = args[name]; + const value = get(name, args); return !isNull(value) ? this.convert(value, pattern) : ""; }); @@ -159,7 +160,7 @@ export class Formatter { const template = compile(msg); return (...args: any[]) => { return template((name, pattern) => { - const value = args[name]; + const value = get(name, args); return !isNull(value) ? this.convert(value, pattern) : ""; }); }; diff --git a/src/main/ts/text/TextWriterBase.ts b/src/main/ts/text/TextWriterBase.ts --- a/src/main/ts/text/TextWriterBase.ts +++ b/src/main/ts/text/TextWriterBase.ts @@ -3,8 +3,6 @@ import { FormatCompiler } from "./Format import { isString, argumentNotNull } from "../safe"; import { Converter } from "./Converter"; -const compiler = new FormatCompiler(); - export abstract class TextWriterBase implements TextWriter { private _converter: Converter; @@ -21,7 +19,7 @@ export abstract class TextWriterBase imp write(format: string, ...args: any[]): void; write(format: any, ...args: any[]): void { if (args.length) { - const compiled = compiler.compile(format); + const compiled = FormatCompiler.compile(format); compiled(this, args); } else { this.writeValue(format); @@ -32,11 +30,11 @@ export abstract class TextWriterBase imp writeLine(format: string, ...args: any[]): void; writeLine(): void { if (arguments.length) - this.write.apply(this, arguments); + this.write.apply(this, arguments); this.writeNewLine(); } - writeValue(value: any, spec?: string): void { + writeValue(value: any, spec?: string) { this.writeText( isString(value) ? value : @@ -44,5 +42,5 @@ export abstract class TextWriterBase imp ); } - abstract writeText(text: string); + abstract writeText(text: string): void; } diff --git a/src/test/ts/mock/Bar.ts b/src/test/ts/mock/Bar.ts --- a/src/test/ts/mock/Bar.ts +++ b/src/test/ts/mock/Bar.ts @@ -1,12 +1,43 @@ import { Foo } from "./Foo"; +/* export const service = annotate(); + +@service.wire({ + foo: dependency("foo"), + nested: { + lazy: dependency("foo", { lazy: true }) + }, + host: dependency("host") +}, "") */ export class Bar { - name = "bar"; + barName = "Twister"; + + _v: Foo | undefined; - foo: Foo; + constructor( + _opts: { + foo?: Foo; + nested?: { + lazy: () => Foo + }, + host: string + }, + s: string + ) { - constructor(_opts) { if (_opts && _opts.foo) - this.foo = _opts.foo; + this._v = _opts.foo; + if (s) + this.barName = s; + } + + setName(name: string) { + + } + + getFoo() { + if (this._v === undefined) + throw new Error("The foo isn't set"); + return this._v; } } diff --git a/src/test/ts/mock/Box.ts b/src/test/ts/mock/Box.ts new file mode 100644 --- /dev/null +++ b/src/test/ts/mock/Box.ts @@ -0,0 +1,28 @@ +import { Bar } from "./Bar"; + +// export service descriptor +// через service передается информация о типе зависимости +// даже если это шаблон. +// export const service = annotate>(); + +// @service.wire() +export class Box { + private _value: T | undefined; + + constructor(value?: T) { + this._value = value; + } + + // @service.inject(dependency("bar")) + setValue(value: T) { + this._value = value; + return value; + } + + getValue() { + if (this._value === undefined) + throw new Error("Trying to get a value from the empty box"); + + return this._value; + } +} diff --git a/src/test/ts/mock/Foo.ts b/src/test/ts/mock/Foo.ts --- a/src/test/ts/mock/Foo.ts +++ b/src/test/ts/mock/Foo.ts @@ -1,3 +1,3 @@ export class Foo { - name = "foo"; + fooName = "foo"; } diff --git a/src/test/ts/mock/MockActivationController.ts b/src/test/ts/mock/MockActivationController.ts --- a/src/test/ts/mock/MockActivationController.ts +++ b/src/test/ts/mock/MockActivationController.ts @@ -3,9 +3,15 @@ import { Cancellation } from "../Cancell export class MockActivationController implements IActivationController { - _active: IActivatable = null; + _active: IActivatable | null = null; + + hasActive() { + return !!this._active; + } getActive(): IActivatable { + if (!this._active) + throw new Error("No active component is set"); return this._active; } diff --git a/src/test/ts/mock/config.ts b/src/test/ts/mock/config.ts new file mode 100644 --- /dev/null +++ b/src/test/ts/mock/config.ts @@ -0,0 +1,14 @@ +import { Services } from "./services"; +import { fluent } from "../di/traits"; +import { Box } from "./Box"; + +export default fluent().register({ + host: it => it.value("example.com"), + + foo: it => import("./Foo").then(({ Foo }) => it + .factory(() => new Foo()) + ), + + box: it => it + .factory($dependency => new Box($dependency("foo"))) +}); diff --git a/src/test/ts/mock/services.ts b/src/test/ts/mock/services.ts new file mode 100644 --- /dev/null +++ b/src/test/ts/mock/services.ts @@ -0,0 +1,15 @@ +import { Foo } from "./Foo"; +import { Bar } from "./Bar"; +import { Box } from "./Box"; + +/** + * Сервисы доступные внутри контейнера + */ +export interface Services { + foo: Foo; + + box: Box; + + host: string; + +} diff --git a/src/test/ts/tests/ActivatableTests.ts b/src/test/ts/tests/ActivatableTests.ts --- a/src/test/ts/tests/ActivatableTests.ts +++ b/src/test/ts/tests/ActivatableTests.ts @@ -20,8 +20,19 @@ test("controller activation", async t => const c = new MockActivationController(); t.false(a.isActive(), "the component is not active by default"); - t.assert(c.getActive() == null, "the activation controller doesn't have an active component by default"); - t.assert(a.getActivationController() == null, "the component doesn't have an activation controller by default"); + t.false(c.hasActive(), "the activation controller doesn't have an active component by default"); + try { + c.getActive(); + t.fail("Should fail when no active component is set"); + } catch (e) { + } + + t.false(a.hasActivationController(), "the component doesn't have an activation controller by default"); + try { + a.getActivationController(); + t.fail("Should fail when no activation controller is set"); + } catch (e) { + } t.comment("Active the component through the controller"); await c.activate(a); @@ -33,7 +44,7 @@ test("controller activation", async t => await c.deactivate(); t.false(a.isActive(), "The component should successfully deactivate"); - t.equal(c.getActive(), null, "The controller shouldn't point to any component"); + t.false(c.hasActive(), "The controller shouldn't point to any component"); t.equal(a.getActivationController(), c, "The componet should point to it's controller"); }); diff --git a/src/test/ts/tests/CancellationTests.ts b/src/test/ts/tests/CancellationTests.ts --- a/src/test/ts/tests/CancellationTests.ts +++ b/src/test/ts/tests/CancellationTests.ts @@ -1,10 +1,10 @@ import { Cancellation } from "../Cancellation"; -import { delay } from "../safe"; +import { delay, notImplemented } from "../safe"; import { test } from "./TestTraits"; test("standalone cancellation", async t => { - let doCancel: (e) => void; + let doCancel: (e: any) => void = notImplemented; const ct = new Cancellation(cancel => { doCancel = cancel; diff --git a/src/test/ts/tests/ContainerTests.ts b/src/test/ts/tests/ContainerTests.ts --- a/src/test/ts/tests/ContainerTests.ts +++ b/src/test/ts/tests/ContainerTests.ts @@ -8,12 +8,19 @@ import { Bar } from "../mock/Bar"; import { isNull } from "../safe"; test("Container register/resolve tests", async t => { - const container = new Container(); + const container = new Container<{ + "bla-bla": string; + "connection": string; + "dbParams": { + timeout: number; + connection: string; + } + }>(); const connection1 = "db://localhost"; t.throws( - () => container.register("bla-bla", "bla-bla"), + () => container.register("bla-bla", "bla-bla" as any), "Do not allow to register anything other than descriptors" ); @@ -41,7 +48,12 @@ test("Container register/resolve tests", test("Container configure/resolve tests", async t => { - const container = new Container(); + const container = new Container<{ + foo: Foo; + box: Bar; + bar: Bar; + db: any; + }>(); await container.configure({ foo: { @@ -57,13 +69,13 @@ test("Container configure/resolve tests" bar: { $type: Bar, - params: { + params: [{ db: { provider: { $dependency: "db" } } - } + }] } }); t.pass("should configure from js object"); @@ -89,5 +101,6 @@ test("Load configuration from module", a const b1 = container.resolve("bar") as Bar; t.assert(!isNull(b1), "bar should not be null"); - t.assert(!isNull(b1.foo), "bar.foo should not be null"); + t.assert(!isNull(b1._v), "bar.foo should not be null"); + }); diff --git a/src/test/ts/tests/FluentContainerTests.ts b/src/test/ts/tests/FluentContainerTests.ts new file mode 100644 --- /dev/null +++ b/src/test/ts/tests/FluentContainerTests.ts @@ -0,0 +1,63 @@ +import { test } from "./TestTraits"; +import { fluent } from "../di/traits"; +import { Bar } from "../mock/Bar"; +import { Container } from "../di/Container"; +import { Foo } from "../mock/Foo"; +import { Box } from "../mock/Box"; +import { delay } from "../safe"; +import { Services } from "../mock/services"; + +test("Simple fluent config", async t => { + const config = fluent<{ host: string; bar: Bar; foo: Foo }>() + .register({ + host: it => it.value("example.com"), + bar: it => it.factory(resolve => new Bar({ host: resolve("host") }, "s-bar")), + foo: it => import("../mock/Foo").then(m => it.lifetime("container").factory(() => new m.Foo())) + }); + + const c1 = new Container<{}>(); + const container = await config.apply(c1); + + t.equal(container.resolve("host"), "example.com", "The value should be resolved"); + t.assert(container.resolve("bar"), "The service should de activated"); + t.equal(container.resolve("foo"), container.resolve("foo"), "The service should be activated once"); +}); + +test("Nested async configuration", async t => { + const container = await new Container<{ + foo: Foo; + box: Box + }>().fluent({ + foo: it => delay(0).then(() => it.factory(() => new Foo())), + box: it => it.lifetime("context").factory($dependency => new Box($dependency("foo"))) + }); + + t.assert(container.resolve("box").getValue(), "The dependency should be set"); + t.equals(container.resolve("box").getValue(), container.resolve("box").getValue(), "The service should be activated once") +}); + +test("Bad fluent config", async t => { + try { + await new Container<{ + foo: Foo; + box: Box + }>().fluent({ + foo: it => delay(0).then(() => it.factory(() => new Foo())), + box: it => it.lifetime("context") + .override("foo", () => { throw new Error("bad override"); }) + .factory($dependency => new Box($dependency("foo"))) + }); + t.fail("Should throw"); + } catch (e) { + t.pass("The configuration should fail"); + t.equal(e.message, "bad override", "the error should pass"); + } +}); + +test("Load fluent config", async t => { + const container = new Container(); + + await container.configure("../mock/config", { contextRequire: require }); + + t.assert(container.resolve("host"), "Should resolve simple value"); +}); diff --git a/src/test/ts/tests/ObservableTests.ts b/src/test/ts/tests/ObservableTests.ts --- a/src/test/ts/tests/ObservableTests.ts +++ b/src/test/ts/tests/ObservableTests.ts @@ -1,19 +1,19 @@ import { TraceSource } from "../log/TraceSource"; import { Observable } from "../Observable"; import { IObservable } from "../interfaces"; -import { delay } from "../safe"; +import { delay, fork } from "../safe"; import { test } from "./TestTraits"; const trace = TraceSource.get("ObservableTests"); test("events sequence example", async t => { - let events: IObservable; + let events: IObservable | undefined; const done = new Promise(resolve => { events = new Observable(async (notify, fail, finish) => { for (let i = 0; i < 10; i++) { - await delay(0); + await fork(); notify(i); } finish(); @@ -23,7 +23,9 @@ test("events sequence example", async t let count = 0; let complete = false; - events.on(x => count = count + x, null, () => complete = true); + if (!events) + throw new Error("events === undefined"); + events.on(x => count = count + x, undefined, () => complete = true); const first = await events.next(); @@ -37,11 +39,11 @@ test("events sequence example", async t }); test("event sequence termination", async t => { - let events: IObservable; + let events: IObservable | undefined; const done = new Promise(resolve => { events = new Observable(async (notify, fail, complete) => { - await delay(0); + await fork(); notify(1); complete(); notify(2); @@ -51,6 +53,9 @@ test("event sequence termination", async }); }); + if (!events) + throw new Error("events === undefined"); + let count = 0; events.on(() => {}, e => count++, () => count++); diff --git a/src/test/ts/tests/SafeTests.ts b/src/test/ts/tests/SafeTests.ts --- a/src/test/ts/tests/SafeTests.ts +++ b/src/test/ts/tests/SafeTests.ts @@ -1,5 +1,5 @@ import { Cancellation } from "../Cancellation"; -import { first, isPromise, firstWhere, delay, nowait } from "../safe"; +import { first, isPromise, firstWhere, delay, nowait, notImplemented } from "../safe"; import { test } from "./TestTraits"; test("await delay test", async t => { @@ -13,7 +13,7 @@ test("await delay test", async t => { t.pass("await delay"); // create cancellation token - let cancel: (e?: any) => void; + let cancel: (e?: any) => void = notImplemented; const ct = new Cancellation(c => cancel = c); // schedule delay @@ -40,14 +40,14 @@ test("await delay test", async t => { test("sequemce test", async t => { const sequence = ["a", "b", "c"]; - const empty = []; + const empty: string[] = []; // synchronous tests t.equals(first(sequence), "a", "Should return the first element"); t.equals(firstWhere(sequence, x => x === "b"), "b", "Should get the second element"); - let v: string; - let e: Error; + let v: string | undefined; + let e: Error | undefined; first(sequence, x => v = x); t.equal(v, "a", "The callback should be called for the first element"); firstWhere(sequence, x => x === "b", x => v = x); @@ -77,7 +77,7 @@ test("sequemce test", async t => { firstWhere(sequence, x => x === "z", x => v = x); }, "Should throw when the element isn't found"); - first(empty, null, x => e = x); + first(empty, undefined, x => e = x); t.true(e, "The errorback should be called for the empty sequence"); // async tests diff --git a/src/test/ts/tests/TestTraits.ts b/src/test/ts/tests/TestTraits.ts --- a/src/test/ts/tests/TestTraits.ts +++ b/src/test/ts/tests/TestTraits.ts @@ -8,7 +8,7 @@ export class TapeWriter implements IDest private readonly _tape: tape.Test; private readonly _subscriptions = new Array(); - private _destroyed; + private _destroyed = false; constructor(t: tape.Test) { argumentNotNull(t, "tape"); @@ -38,6 +38,9 @@ export class TapeWriter implements IDest } destroy() { + if (this._destroyed) + return; + this._destroyed = true; this._subscriptions.forEach(destroy); } } diff --git a/src/test/tsconfig.json b/src/test/tsconfig.json --- a/src/test/tsconfig.json +++ b/src/test/tsconfig.json @@ -1,8 +1,6 @@ { "extends": "../tsconfig", "compilerOptions": { - "rootDir": "ts", - "baseUrl": ".", "rootDirs": [ "ts", "../main/ts" diff --git a/src/testAmd/js/tests/plan.js b/src/testAmd/js/tests/plan.js --- a/src/testAmd/js/tests/plan.js +++ b/src/testAmd/js/tests/plan.js @@ -6,5 +6,6 @@ define([ "./ObservableTests", "./ContainerTests", "./SafeTests", - "./TextTests" + "./TextTests", + "./FluentContainerTests" ]); \ No newline at end of file diff --git a/src/testCjs/ts/tests/plan.ts b/src/testCjs/ts/tests/plan.ts --- a/src/testCjs/ts/tests/plan.ts +++ b/src/testCjs/ts/tests/plan.ts @@ -5,3 +5,4 @@ import "./ObservableTests"; import "./ContainerTests"; import "./SafeTests"; import "./TextTests"; +import "./FluentContainerTests"; diff --git a/src/tsconfig.json b/src/tsconfig.json --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -1,9 +1,12 @@ { "compilerOptions": { "moduleResolution": "node", + "experimentalDecorators": true, "noEmitOnError": true, "listFiles": true, + "strict": true, "types": [], + "target": "ES5", "lib": ["es5", "es2015.promise", "es2015.symbol", "es2015.iterable", "dom", "scripthost"] } } \ No newline at end of file