| @@ -0,0 +1,63 | |||||
|
|
1 | import { Descriptor, PartialServiceMap, ILifetime, ContainerKeys } from "../interfaces"; | |||
|
|
2 | import { ActivationContext } from "../ActivationContext"; | |||
|
|
3 | import { each } from "../../safe"; | |||
|
|
4 | import { DependencyOptions, LazyDependencyOptions, Resolver } from "./interfaces"; | |||
|
|
5 | ||||
|
|
6 | export interface DescriptorImplArgs<S extends object, T> { | |||
|
|
7 | lifetime: ILifetime; | |||
|
|
8 | ||||
|
|
9 | factory: (resolve: Resolver<S>) => T; | |||
|
|
10 | ||||
|
|
11 | cleanup?: (item: T) => void; | |||
|
|
12 | ||||
|
|
13 | overrides?: PartialServiceMap<S>; | |||
|
|
14 | } | |||
|
|
15 | ||||
|
|
16 | export class DescriptorImpl<S extends object, T> implements Descriptor<S, T> { | |||
|
|
17 | ||||
|
|
18 | private readonly _overrides?: PartialServiceMap<S>; | |||
|
|
19 | ||||
|
|
20 | private readonly _lifetime: ILifetime; | |||
|
|
21 | ||||
|
|
22 | private readonly _factory: (resolve: Resolver<S>) => T; | |||
|
|
23 | ||||
|
|
24 | private readonly _cleanup?: (item: T) => void; | |||
|
|
25 | ||||
|
|
26 | constructor(args: DescriptorImplArgs<S, T>) { | |||
|
|
27 | this._lifetime = args.lifetime; | |||
|
|
28 | this._factory = args.factory; | |||
|
|
29 | if (args.cleanup) | |||
|
|
30 | this._cleanup = args.cleanup; | |||
|
|
31 | if (args.overrides) | |||
|
|
32 | this._overrides = args.overrides; | |||
|
|
33 | } | |||
|
|
34 | ||||
|
|
35 | activate(context: ActivationContext<S>): T { | |||
|
|
36 | ||||
|
|
37 | if (this._lifetime.has()) | |||
|
|
38 | return this._lifetime.get(); | |||
|
|
39 | ||||
|
|
40 | this._lifetime.initialize(context); | |||
|
|
41 | ||||
|
|
42 | if (this._overrides) | |||
|
|
43 | each(this._overrides, (v, k) => context.register(k, v)); | |||
|
|
44 | ||||
|
|
45 | const resolve = (name: ContainerKeys<S>, opts?: DependencyOptions | LazyDependencyOptions) => { | |||
|
|
46 | if (opts && "lazy" in opts && opts.lazy) { | |||
|
|
47 | const c2 = context.clone(); | |||
|
|
48 | return () => { | |||
|
|
49 | return opts.optional ? c2.resolve(name, opts.default) : c2.resolve(name); | |||
|
|
50 | }; | |||
|
|
51 | } else { | |||
|
|
52 | return opts && opts.optional ? context.resolve(name, opts.default) : context.resolve(name); | |||
|
|
53 | } | |||
|
|
54 | }; | |||
|
|
55 | ||||
|
|
56 | const instance = this._factory.call(undefined, resolve); | |||
|
|
57 | ||||
|
|
58 | this._lifetime.store(instance, this._cleanup); | |||
|
|
59 | ||||
|
|
60 | return instance; | |||
|
|
61 | } | |||
|
|
62 | ||||
|
|
63 | } | |||
| @@ -0,0 +1,58 | |||||
|
|
1 | import { Container } from "../Container"; | |||
|
|
2 | import { argumentNotNull, each, isPrimitive, isPromise } from "../../safe"; | |||
|
|
3 | import { DescriptorBuilder } from "./DescriptorBuilder"; | |||
|
|
4 | import { RegistrationBuilder, FluentRegistrations } from "./interfaces"; | |||
|
|
5 | import { Cancellation } from "../../Cancellation"; | |||
|
|
6 | ||||
|
|
7 | export class FluentConfiguration<S extends object, Y extends keyof S = keyof S> { | |||
|
|
8 | ||||
|
|
9 | _builders: { [k in keyof S]?: RegistrationBuilder<S, S[k]> } = {}; | |||
|
|
10 | ||||
|
|
11 | register<K extends Y>(name: K, builder: RegistrationBuilder<S, S[K]>): FluentConfiguration<S, Exclude<Y, K>>; | |||
|
|
12 | register<K extends Y>(config: FluentRegistrations<K, S>): FluentConfiguration<S, Exclude<Y, K>>; | |||
|
|
13 | register<K extends Y>(nameOrConfig: K | FluentRegistrations<K, S>, builder?: RegistrationBuilder<S, S[K]>): FluentConfiguration<S, Exclude<Y, K>> { | |||
|
|
14 | if (isPrimitive(nameOrConfig)) { | |||
|
|
15 | argumentNotNull(builder, "builder"); | |||
|
|
16 | this._builders[nameOrConfig] = builder; | |||
|
|
17 | } else { | |||
|
|
18 | each(nameOrConfig, (v, k) => this.register(k, v)); | |||
|
|
19 | } | |||
|
|
20 | ||||
|
|
21 | return this; | |||
|
|
22 | } | |||
|
|
23 | ||||
|
|
24 | apply(target: Container<S>, ct = Cancellation.none) { | |||
|
|
25 | ||||
|
|
26 | let pending = 1; | |||
|
|
27 | ||||
|
|
28 | return new Promise((resolve, reject) => { | |||
|
|
29 | function guard(v: void | Promise<void>) { | |||
|
|
30 | if (isPromise(v)) | |||
|
|
31 | v.catch(reject); | |||
|
|
32 | } | |||
|
|
33 | ||||
|
|
34 | function complete() { | |||
|
|
35 | if (!--pending) | |||
|
|
36 | resolve(); | |||
|
|
37 | } | |||
|
|
38 | each(this._builders, (v, k) => { | |||
|
|
39 | pending++; | |||
|
|
40 | const d = new DescriptorBuilder<S, any>(target, | |||
|
|
41 | result => { | |||
|
|
42 | target.register(k, result); | |||
|
|
43 | complete(); | |||
|
|
44 | }, | |||
|
|
45 | reject | |||
|
|
46 | ); | |||
|
|
47 | ||||
|
|
48 | try { | |||
|
|
49 | guard(v(d, ct)); | |||
|
|
50 | } catch (e) { | |||
|
|
51 | reject(e); | |||
|
|
52 | } | |||
|
|
53 | }); | |||
|
|
54 | complete(); | |||
|
|
55 | }); | |||
|
|
56 | } | |||
|
|
57 | ||||
|
|
58 | } | |||
| @@ -0,0 +1,54 | |||||
|
|
1 | import { test } from "./TestTraits"; | |||
|
|
2 | import { fluent } from "../di/traits"; | |||
|
|
3 | import { Bar } from "../mock/Bar"; | |||
|
|
4 | import { Container } from "../di/Container"; | |||
|
|
5 | import { Foo } from "../mock/Foo"; | |||
|
|
6 | import { Box } from "../mock/Box"; | |||
|
|
7 | import { delay } from "../safe"; | |||
|
|
8 | ||||
|
|
9 | test("Simple fluent config", async t => { | |||
|
|
10 | const config = fluent<{ host: string; bar: Bar; foo: Foo }>() | |||
|
|
11 | .register({ | |||
|
|
12 | host: it => it.value("example.com"), | |||
|
|
13 | bar: it => it.factory(resolve => new Bar({ host: resolve("host") }, "s-bar")), | |||
|
|
14 | foo: it => import("../mock/Foo").then(m => it.lifetime("container").factory(() => new m.Foo())) | |||
|
|
15 | }); | |||
|
|
16 | ||||
|
|
17 | const container = new Container<{ host: string; bar: Bar; foo: Foo; }>(); | |||
|
|
18 | await config.apply(container); | |||
|
|
19 | ||||
|
|
20 | t.equal(container.resolve("host"), "example.com", "The value should be resolved"); | |||
|
|
21 | t.assert(container.resolve("bar"), "The service should de activated"); | |||
|
|
22 | t.equal(container.resolve("foo"), container.resolve("foo"), "The service should be activated once"); | |||
|
|
23 | }); | |||
|
|
24 | ||||
|
|
25 | test("Nested async configuration", async t => { | |||
|
|
26 | const container = await new Container<{ | |||
|
|
27 | foo: Foo; | |||
|
|
28 | box: Box<Foo> | |||
|
|
29 | }>().fluent({ | |||
|
|
30 | foo: it => delay(0).then(() => it.factory(() => new Foo())), | |||
|
|
31 | box: it => it.lifetime("context").factory($dependency => new Box($dependency("foo"))) | |||
|
|
32 | }); | |||
|
|
33 | ||||
|
|
34 | t.assert(container.resolve("box").getValue(), "The dependency should be set"); | |||
|
|
35 | t.equals(container.resolve("box").getValue(), container.resolve("box").getValue(), "The service should be activated once") | |||
|
|
36 | }); | |||
|
|
37 | ||||
|
|
38 | test("Bad fluent config", async t => { | |||
|
|
39 | try { | |||
|
|
40 | await new Container<{ | |||
|
|
41 | foo: Foo; | |||
|
|
42 | box: Box<Foo> | |||
|
|
43 | }>().fluent({ | |||
|
|
44 | foo: it => delay(0).then(() => it.factory(() => new Foo())), | |||
|
|
45 | box: it => it.lifetime("context") | |||
|
|
46 | .override("foo", () => { throw new Error("bad override"); }) | |||
|
|
47 | .factory($dependency => new Box($dependency("foo"))) | |||
|
|
48 | }); | |||
|
|
49 | t.fail("Should throw"); | |||
|
|
50 | } catch (e) { | |||
|
|
51 | t.pass("The configuration should fail"); | |||
|
|
52 | t.equal(e.message, "bad override", "the error should pass"); | |||
|
|
53 | } | |||
|
|
54 | }); | |||
| @@ -15,6 +15,10 export interface ActivationContextInfo { | |||||
| 15 |
|
15 | |||
| 16 | let nextId = 1; |
|
16 | let nextId = 1; | |
| 17 |
|
17 | |||
|
|
18 | /** This class is created once per `Container.resolve` method call and used to | |||
|
|
19 | * cache dependencies and to track created instances. The activation context | |||
|
|
20 | * tracks services with `context` activation type. | |||
|
|
21 | */ | |||
| 18 | export class ActivationContext<S extends object> { |
|
22 | export class ActivationContext<S extends object> { | |
| 19 | _cache: MapOf<any>; |
|
23 | _cache: MapOf<any>; | |
| 20 |
|
24 | |||
| @@ -30,6 +34,14 export class ActivationContext<S extends | |||||
| 30 |
|
34 | |||
| 31 | _parent: ActivationContext<S> | undefined; |
|
35 | _parent: ActivationContext<S> | undefined; | |
| 32 |
|
36 | |||
|
|
37 | /** Creates a new activation context with the specified parameters. | |||
|
|
38 | * @param container the container which starts the activation process | |||
|
|
39 | * @param services the initial service registrations | |||
|
|
40 | * @param name the name of the service being activated, this parameter is | |||
|
|
41 | * used for the debug purpose. | |||
|
|
42 | * @param service the service to activate, this parameter is used for the | |||
|
|
43 | * debug purpose. | |||
|
|
44 | */ | |||
| 33 | constructor(container: Container<S>, services: ContainerServiceMap<S>, name: string, service: Descriptor<S, any>) { |
|
45 | constructor(container: Container<S>, services: ContainerServiceMap<S>, name: string, service: Descriptor<S, any>) { | |
| 34 | this._name = name; |
|
46 | this._name = name; | |
| 35 | this._service = service; |
|
47 | this._service = service; | |
| @@ -39,16 +51,33 export class ActivationContext<S extends | |||||
| 39 | this._container = container; |
|
51 | this._container = container; | |
| 40 | } |
|
52 | } | |
| 41 |
|
53 | |||
|
|
54 | /** the name of the current resolving dependency */ | |||
| 42 | getName() { |
|
55 | getName() { | |
| 43 | return this._name; |
|
56 | return this._name; | |
| 44 | } |
|
57 | } | |
| 45 |
|
58 | |||
|
|
59 | /** Returns the container for which 'resolve' method was called */ | |||
| 46 | getContainer() { |
|
60 | getContainer() { | |
| 47 | return this._container; |
|
61 | return this._container; | |
| 48 | } |
|
62 | } | |
| 49 |
|
63 | |||
|
|
64 | /** Resolves the specified dependency in the current context | |||
|
|
65 | * @param name The name of the dependency being resolved | |||
|
|
66 | */ | |||
| 50 | resolve<K extends ContainerKeys<S>>(name: K): TypeOfService<S, K>; |
|
67 | resolve<K extends ContainerKeys<S>>(name: K): TypeOfService<S, K>; | |
|
|
68 | /** Resolves the specified dependency with the specified default value if | |||
|
|
69 | * the dependency is missing. | |||
|
|
70 | * | |||
|
|
71 | * @param name The name of the dependency being resolved | |||
|
|
72 | * @param def A default value to return in case of the specified dependency | |||
|
|
73 | * is missing. | |||
|
|
74 | */ | |||
| 51 | resolve<K extends ContainerKeys<S>, T>(name: K, def: T): TypeOfService<S, K> | T; |
|
75 | resolve<K extends ContainerKeys<S>, T>(name: K, def: T): TypeOfService<S, K> | T; | |
|
|
76 | /** Resolves the specified dependency and returns undefined in case if the | |||
|
|
77 | * dependency is missing. | |||
|
|
78 | * | |||
|
|
79 | * @param name The name of the dependency being resolved | |||
|
|
80 | */ | |||
| 52 | resolve<K extends ContainerKeys<S>>(name: K, def: undefined): TypeOfService<S, K> | undefined; |
|
81 | resolve<K extends ContainerKeys<S>>(name: K, def: undefined): TypeOfService<S, K> | undefined; | |
| 53 | resolve<K extends ContainerKeys<S>, T>(name: K, def?: T): TypeOfService<S, K> | T | undefined { |
|
82 | resolve<K extends ContainerKeys<S>, T>(name: K, def?: T): TypeOfService<S, K> | T | undefined { | |
| 54 | const d = this._services[name]; |
|
83 | const d = this._services[name]; | |
| @@ -92,6 +121,7 export class ActivationContext<S extends | |||||
| 92 | } |
|
121 | } | |
| 93 | }; |
|
122 | }; | |
| 94 | } |
|
123 | } | |
|
|
124 | ||||
| 95 | activate<T>(d: Descriptor<S, T>, name: string) { |
|
125 | activate<T>(d: Descriptor<S, T>, name: string) { | |
| 96 | if (trace.isLogEnabled()) |
|
126 | if (trace.isLogEnabled()) | |
| 97 | trace.log(`enter ${name} ${d}`); |
|
127 | trace.log(`enter ${name} ${d}`); | |
| @@ -1,7 +1,7 | |||||
| 1 | import { ActivationContext } from "./ActivationContext"; |
|
1 | import { ActivationContext } from "./ActivationContext"; | |
| 2 | import { ValueDescriptor } from "./ValueDescriptor"; |
|
2 | import { ValueDescriptor } from "./ValueDescriptor"; | |
| 3 | import { ActivationError } from "./ActivationError"; |
|
3 | import { ActivationError } from "./ActivationError"; | |
| 4 |
import { ServiceMap, Descriptor, PartialServiceMap, |
|
4 | import { ServiceMap, Descriptor, PartialServiceMap, ServiceLocator, ContainerServiceMap, ContainerKeys, TypeOfService } from "./interfaces"; | |
| 5 | import { TraceSource } from "../log/TraceSource"; |
|
5 | import { TraceSource } from "../log/TraceSource"; | |
| 6 | import { Configuration, RegistrationMap } from "./Configuration"; |
|
6 | import { Configuration, RegistrationMap } from "./Configuration"; | |
| 7 | import { Cancellation } from "../Cancellation"; |
|
7 | import { Cancellation } from "../Cancellation"; | |
| @@ -9,13 +9,15 import { MapOf, IDestroyable } from "../ | |||||
| 9 | import { isDescriptor } from "./traits"; |
|
9 | import { isDescriptor } from "./traits"; | |
| 10 | import { LifetimeManager } from "./LifetimeManager"; |
|
10 | import { LifetimeManager } from "./LifetimeManager"; | |
| 11 | import { each } from "../safe"; |
|
11 | import { each } from "../safe"; | |
|
|
12 | import { FluentRegistrations } from "./fluent/interfaces"; | |||
|
|
13 | import { FluentConfiguration } from "./fluent/FluentConfiguration"; | |||
| 12 |
|
14 | |||
| 13 | const trace = TraceSource.get("@implab/core/di/ActivationContext"); |
|
15 | const trace = TraceSource.get("@implab/core/di/ActivationContext"); | |
| 14 |
|
16 | |||
| 15 | export class Container<S extends object = any> implements ServiceLocator<S>, IDestroyable { |
|
17 | export class Container<S extends object = any> implements ServiceLocator<S>, IDestroyable { | |
| 16 | readonly _services: ContainerServiceMap<S>; |
|
18 | readonly _services: ContainerServiceMap<S>; | |
| 17 |
|
19 | |||
| 18 |
readonly _lifetimeManager: |
|
20 | readonly _lifetimeManager: LifetimeManager; | |
| 19 |
|
21 | |||
| 20 | readonly _cleanup: (() => void)[]; |
|
22 | readonly _cleanup: (() => void)[]; | |
| 21 |
|
23 | |||
| @@ -126,6 +128,11 export class Container<S extends object | |||||
| 126 | } |
|
128 | } | |
| 127 | } |
|
129 | } | |
| 128 |
|
130 | |||
|
|
131 | async fluent<K extends keyof S>(config: FluentRegistrations<K, S>, ct = Cancellation.none): Promise<this> { | |||
|
|
132 | await new FluentConfiguration<S>().register(config).apply(this, ct); | |||
|
|
133 | return this; | |||
|
|
134 | } | |||
|
|
135 | ||||
| 129 | createChildContainer<S2 extends object = S>(): Container<S & S2> { |
|
136 | createChildContainer<S2 extends object = S>(): Container<S & S2> { | |
| 130 | return new Container<S & S2>(this as any); |
|
137 | return new Container<S & S2>(this as any); | |
| 131 | } |
|
138 | } | |
| @@ -1,6 +1,6 | |||||
| 1 | import { IDestroyable, MapOf } from "../interfaces"; |
|
1 | import { IDestroyable, MapOf } from "../interfaces"; | |
| 2 | import { argumentNotNull, isDestroyable } from "../safe"; |
|
2 | import { argumentNotNull, isDestroyable, primitive, isNull, argumentNotEmptyString } from "../safe"; | |
| 3 |
import { |
|
3 | import { ILifetime } from "./interfaces"; | |
| 4 | import { ActivationContext } from "./ActivationContext"; |
|
4 | import { ActivationContext } from "./ActivationContext"; | |
| 5 | import { Container } from "./Container"; |
|
5 | import { Container } from "./Container"; | |
| 6 |
|
6 | |||
| @@ -12,7 +12,7 function safeCall(item: () => void) { | |||||
| 12 | } |
|
12 | } | |
| 13 | } |
|
13 | } | |
| 14 |
|
14 | |||
| 15 | const emptyLifetime: ILifetime = { |
|
15 | const emptyLifetime: ILifetime = Object.freeze({ | |
| 16 | has() { |
|
16 | has() { | |
| 17 | return false; |
|
17 | return false; | |
| 18 | }, |
|
18 | }, | |
| @@ -29,11 +29,11 const emptyLifetime: ILifetime = { | |||||
| 29 | // does nothing |
|
29 | // does nothing | |
| 30 | } |
|
30 | } | |
| 31 |
|
31 | |||
| 32 | }; |
|
32 | }); | |
| 33 |
|
33 | |||
| 34 | const unknownLifetime: ILifetime = { |
|
34 | const unknownLifetime: ILifetime = Object.freeze({ | |
| 35 | has() { |
|
35 | has() { | |
| 36 | throw new Error("The lifetime is unknown"); |
|
36 | return false; | |
| 37 | }, |
|
37 | }, | |
| 38 | initialize() { |
|
38 | initialize() { | |
| 39 | throw new Error("Can't call initialize on the unknown lifetime object"); |
|
39 | throw new Error("Can't call initialize on the unknown lifetime object"); | |
| @@ -44,11 +44,13 const unknownLifetime: ILifetime = { | |||||
| 44 | store() { |
|
44 | store() { | |
| 45 | throw new Error("Can't store a value in the unknown lifetime object"); |
|
45 | throw new Error("Can't store a value in the unknown lifetime object"); | |
| 46 | } |
|
46 | } | |
| 47 | } |
|
47 | }); | |
| 48 |
|
48 | |||
| 49 | let nextId = 0; |
|
49 | let nextId = 0; | |
| 50 |
|
50 | |||
| 51 | export class LifetimeManager implements IDestroyable, ILifetimeManager { |
|
51 | const singletons: { [k in keyof any]: any; } = {}; | |
|
|
52 | ||||
|
|
53 | export class LifetimeManager implements IDestroyable { | |||
| 52 | private _cleanup: (() => void)[] = []; |
|
54 | private _cleanup: (() => void)[] = []; | |
| 53 | private _cache: MapOf<any> = {}; |
|
55 | private _cache: MapOf<any> = {}; | |
| 54 | private _destroyed = false; |
|
56 | private _destroyed = false; | |
| @@ -116,7 +118,7 export class LifetimeManager implements | |||||
| 116 | if (_lifetime !== unknownLifetime) |
|
118 | if (_lifetime !== unknownLifetime) | |
| 117 | throw new Error("Cyclic reference activation detected"); |
|
119 | throw new Error("Cyclic reference activation detected"); | |
| 118 |
|
120 | |||
| 119 |
_lifetime = context.getContainer().getLifetimeManager().create( |
|
121 | _lifetime = context.getContainer().getLifetimeManager().create(); | |
| 120 | }, |
|
122 | }, | |
| 121 | get() { |
|
123 | get() { | |
| 122 | return _lifetime.get(); |
|
124 | return _lifetime.get(); | |
| @@ -151,7 +153,27 export class LifetimeManager implements | |||||
| 151 | } |
|
153 | } | |
| 152 |
|
154 | |||
| 153 | static singletonLifetime(typeId: string): ILifetime { |
|
155 | static singletonLifetime(typeId: string): ILifetime { | |
| 154 | return emptyLifetime; |
|
156 | argumentNotEmptyString(typeId, "typeId"); | |
|
|
157 | let pending = false; | |||
|
|
158 | return { | |||
|
|
159 | has() { | |||
|
|
160 | return typeId in singletons; | |||
|
|
161 | }, | |||
|
|
162 | get() { | |||
|
|
163 | if (!this.has()) | |||
|
|
164 | throw new Error(`The instance ${typeId} doesn't exists`); | |||
|
|
165 | return singletons[typeId]; | |||
|
|
166 | }, | |||
|
|
167 | initialize() { | |||
|
|
168 | if (pending) | |||
|
|
169 | throw new Error("Cyclic reference detected"); | |||
|
|
170 | pending = true; | |||
|
|
171 | }, | |||
|
|
172 | store(item: any) { | |||
|
|
173 | singletons[typeId] = item; | |||
|
|
174 | pending = false; | |||
|
|
175 | } | |||
|
|
176 | }; | |||
| 155 | } |
|
177 | } | |
| 156 |
|
178 | |||
| 157 | static containerLifetime(container: Container<any>) { |
|
179 | static containerLifetime(container: Container<any>) { | |
| @@ -160,7 +182,7 export class LifetimeManager implements | |||||
| 160 | initialize(context: ActivationContext<any>) { |
|
182 | initialize(context: ActivationContext<any>) { | |
| 161 | if (_lifetime !== unknownLifetime) |
|
183 | if (_lifetime !== unknownLifetime) | |
| 162 | throw new Error("Cyclic reference detected"); |
|
184 | throw new Error("Cyclic reference detected"); | |
| 163 |
_lifetime = container.getLifetimeManager().create( |
|
185 | _lifetime = container.getLifetimeManager().create(); | |
| 164 | }, |
|
186 | }, | |
| 165 | get() { |
|
187 | get() { | |
| 166 | return _lifetime.get(); |
|
188 | return _lifetime.get(); | |
| @@ -1,5 +1,5 | |||||
| 1 | import { ActivationContext } from "./ActivationContext"; |
|
1 | import { ActivationContext } from "./ActivationContext"; | |
| 2 |
import { Descriptor, ServiceMap, PartialServiceMap, |
|
2 | import { Descriptor, ServiceMap, PartialServiceMap, ILifetime } from "./interfaces"; | |
| 3 | import { isPrimitive, keys, isNull } from "../safe"; |
|
3 | import { isPrimitive, keys, isNull } from "../safe"; | |
| 4 | import { TraceSource } from "../log/TraceSource"; |
|
4 | import { TraceSource } from "../log/TraceSource"; | |
| 5 | import { isDescriptor } from "./traits"; |
|
5 | import { isDescriptor } from "./traits"; | |
| @@ -1,46 +1,94 | |||||
| 1 |
import { Resolver, |
|
1 | import { Resolver, RegistrationBuilder } from "./interfaces"; | |
| 2 | import { Container } from "../Container"; |
|
2 | import { Container } from "../Container"; | |
| 3 |
import { Descriptor, ILifetime, |
|
3 | import { Descriptor, ILifetime, ActivationType, PartialServiceMap } from "../interfaces"; | |
| 4 | import { ActivationContext } from "../ActivationContext"; |
|
4 | import { DescriptorImpl } from "./DescriptorImpl"; | |
|
|
5 | import { LifetimeManager } from "../LifetimeManager"; | |||
|
|
6 | import { isString, each, isPrimitive, isPromise, oid } from "../../safe"; | |||
|
|
7 | ||||
|
|
8 | export class DescriptorBuilder<S extends object, T> { | |||
|
|
9 | private readonly _container: Container<S>; | |||
|
|
10 | private readonly _cb: (d: Descriptor<S, T>) => void; | |||
|
|
11 | ||||
|
|
12 | private readonly _eb: (err: any) => void; | |||
| 5 |
|
13 | |||
| 6 | export class DescriptorBuilder<T, S extends object> { |
|
14 | private _lifetime = LifetimeManager.empty(); | |
| 7 | readonly _container: Container<S>; |
|
15 | ||
| 8 | readonly _cb: (d: Descriptor<S, T>) => void; |
|
16 | private _overrides?: PartialServiceMap<S>; | |
|
|
17 | ||||
|
|
18 | private _cleanup?: (item: T) => void; | |||
| 9 |
|
19 | |||
| 10 | constructor(container: Container<S>, cb: (d: Descriptor<S, T>) => void) { |
|
20 | private _factory?: (resolve: Resolver<S>) => T; | |
|
|
21 | ||||
|
|
22 | private _pending = 1; | |||
|
|
23 | ||||
|
|
24 | private _failed = false; | |||
|
|
25 | ||||
|
|
26 | constructor(container: Container<S>, cb: (d: Descriptor<S, T>) => void, eb: (err: any) => void) { | |||
| 11 | this._container = container; |
|
27 | this._container = container; | |
| 12 | this._cb = cb; |
|
28 | this._cb = cb; | |
|
|
29 | this._eb = eb; | |||
|
|
30 | } | |||
|
|
31 | ||||
|
|
32 | build<T2>(): DescriptorBuilder<S, T2> { | |||
|
|
33 | this._defer(); | |||
|
|
34 | return new DescriptorBuilder<S, T2>(this._container, () => this._complete(), err => this._fail(err)); | |||
| 13 | } |
|
35 | } | |
| 14 |
|
36 | |||
| 15 | factory(f: (resolve: Resolver<S>, activate: (lifetime: ILifetime, factory: () => any, cleanup?: (item: any) => void) => any) => T): void { |
|
37 | override<K extends keyof S>(name: K, builder: RegistrationBuilder<S, S[K]>): this; | |
| 16 | this._cb({ |
|
38 | override<K extends keyof S>(services: { [name in K]: RegistrationBuilder<S, S[K]> }): this; | |
| 17 | activate(context: ActivationContext<S>) { |
|
39 | override<K extends keyof S>(nameOrServices: K | { [name in K]: RegistrationBuilder<S, S[K]> }, builder?: RegistrationBuilder<S, S[K]>): this { | |
| 18 | const resolve = (name: ContainerKeys<S>, opts?: DependencyOptions | LazyDependencyOptions) => { |
|
40 | const overrides: PartialServiceMap<S> = this._overrides ? | |
| 19 | if (opts && "lazy" in opts && opts.lazy) { |
|
41 | this._overrides : | |
| 20 | const c2 = context.clone(); |
|
42 | (this._overrides = {}); | |
| 21 | return () => { |
|
43 | ||
| 22 | return opts.optional ? c2.resolve(name, opts.default) : c2.resolve(name); |
|
44 | const guard = (v: void | Promise<void>) => { | |
| 23 | }; |
|
45 | if (isPromise(v)) | |
| 24 | } else { |
|
46 | v.catch(err => this._fail(err)); | |
| 25 | return opts && opts.optional ? context.resolve(name, opts.default) : context.resolve(name); |
|
|||
| 26 | } |
|
|||
| 27 |
|
|
47 | }; | |
| 28 |
|
48 | |||
| 29 | const activate = (lifetime: ILifetime, factory: () => any, cleanup?: (item: any) => void) => { |
|
49 | if (isPrimitive(nameOrServices)) { | |
| 30 |
|
|
50 | if (builder) { | |
| 31 | return lifetime.get(); |
|
51 | this._defer(); | |
|
|
52 | const d = new DescriptorBuilder<S, S[K]>( | |||
|
|
53 | this._container, | |||
|
|
54 | result => { | |||
|
|
55 | overrides[nameOrServices] = result; | |||
|
|
56 | this._complete(); | |||
|
|
57 | }, | |||
|
|
58 | err => this._fail(err) | |||
|
|
59 | ); | |||
|
|
60 | ||||
|
|
61 | try { | |||
|
|
62 | guard(builder(d)); | |||
|
|
63 | } catch (err) { | |||
|
|
64 | this._fail(err); | |||
|
|
65 | } | |||
|
|
66 | } | |||
| 32 |
|
|
67 | } else { | |
| 33 | lifetime.initialize(context); |
|
68 | each(nameOrServices, (v, k) => this.override(k, v)); | |
| 34 | const instance = factory(); |
|
69 | } | |
| 35 | lifetime.store(instance, cleanup); |
|
70 | return this; | |
| 36 | return instance; |
|
|||
| 37 | } |
|
71 | } | |
| 38 |
|
72 | |||
| 39 | }; |
|
73 | lifetime(lifetime: "singleton", typeId: string): this; | |
|
|
74 | lifetime(lifetime: ILifetime | Exclude<ActivationType, "singleton">): this; | |||
|
|
75 | lifetime(lifetime: ILifetime | ActivationType, typeId?: string): this { | |||
|
|
76 | if (isString(lifetime)) { | |||
|
|
77 | this._lifetime = this._resolveLifetime(lifetime, typeId); | |||
|
|
78 | } else { | |||
|
|
79 | this._lifetime = lifetime; | |||
|
|
80 | } | |||
|
|
81 | return this; | |||
|
|
82 | } | |||
| 40 |
|
83 | |||
| 41 | return f(resolve, activate); |
|
84 | cleanup(cb: (item: T) => void): this { | |
|
|
85 | this._cleanup = cb; | |||
|
|
86 | return this; | |||
| 42 | } |
|
87 | } | |
| 43 | }); |
|
88 | ||
|
|
89 | factory(f: (resolve: Resolver<S>) => T): void { | |||
|
|
90 | this._factory = f; | |||
|
|
91 | this._complete(); | |||
| 44 | } |
|
92 | } | |
| 45 |
|
93 | |||
| 46 | value(v: T): void { |
|
94 | value(v: T): void { | |
| @@ -51,4 +99,49 export class DescriptorBuilder<T, S exte | |||||
| 51 | }); |
|
99 | }); | |
| 52 | } |
|
100 | } | |
| 53 |
|
101 | |||
|
|
102 | _resolveLifetime(activation: ActivationType, typeId?: string | object) { | |||
|
|
103 | switch (activation) { | |||
|
|
104 | case "container": | |||
|
|
105 | return LifetimeManager.containerLifetime(this._container); | |||
|
|
106 | case "hierarchy": | |||
|
|
107 | return LifetimeManager.hierarchyLifetime(); | |||
|
|
108 | case "context": | |||
|
|
109 | return LifetimeManager.contextLifetime(); | |||
|
|
110 | case "singleton": | |||
|
|
111 | if (!typeId) | |||
|
|
112 | throw Error("The singleton activation requires a typeId"); | |||
|
|
113 | ||||
|
|
114 | const _oid = isString(typeId) ? typeId : oid(typeId); | |||
|
|
115 | ||||
|
|
116 | return LifetimeManager.singletonLifetime(_oid); | |||
|
|
117 | default: | |||
|
|
118 | return LifetimeManager.empty(); | |||
| 54 | } |
|
119 | } | |
|
|
120 | } | |||
|
|
121 | ||||
|
|
122 | _defer() { | |||
|
|
123 | this._pending++; | |||
|
|
124 | } | |||
|
|
125 | ||||
|
|
126 | _complete() { | |||
|
|
127 | if (--this._pending === 0) { | |||
|
|
128 | if (!this._factory) | |||
|
|
129 | throw new Error("The factory must be specified"); | |||
|
|
130 | ||||
|
|
131 | this._cb(new DescriptorImpl<S, T>({ | |||
|
|
132 | lifetime: this._lifetime, | |||
|
|
133 | factory: this._factory, | |||
|
|
134 | overrides: this._overrides, | |||
|
|
135 | cleanup: this._cleanup | |||
|
|
136 | })); | |||
|
|
137 | } | |||
|
|
138 | } | |||
|
|
139 | ||||
|
|
140 | _fail(err: any) { | |||
|
|
141 | if (!this._failed) { | |||
|
|
142 | this._failed = true; | |||
|
|
143 | this._eb.call(undefined, err); | |||
|
|
144 | } | |||
|
|
145 | } | |||
|
|
146 | ||||
|
|
147 | } | |||
| @@ -1,6 +1,6 | |||||
| 1 | import { primitive } from "../../safe"; |
|
1 | import { primitive } from "../../safe"; | |
| 2 | import { AnnotationBuilder } from "../Annotations"; |
|
2 | import { TypeOfService, ContainerKeys, ActivationType, ILifetime } from "../interfaces"; | |
| 3 |
import { I |
|
3 | import { ICancellation } from "../../interfaces"; | |
| 4 |
|
4 | |||
| 5 | export interface DependencyOptions { |
|
5 | export interface DependencyOptions { | |
| 6 | optional?: boolean; |
|
6 | optional?: boolean; | |
| @@ -22,10 +22,6 export type ExtractDependency<D, S> = D | |||||
| 22 | export type WalkDependencies<D, S> = D extends primitive ? D : |
|
22 | export type WalkDependencies<D, S> = D extends primitive ? D : | |
| 23 | { [K in keyof D]: ExtractDependency<D[K], S> }; |
|
23 | { [K in keyof D]: ExtractDependency<D[K], S> }; | |
| 24 |
|
24 | |||
| 25 | export type ServiceModule<T, S extends object, M extends keyof any = "service"> = { |
|
|||
| 26 | [m in M]: AnnotationBuilder<T, S>; |
|
|||
| 27 | }; |
|
|||
| 28 |
|
||||
| 29 | export type InferReferenceType<S extends object, K extends ContainerKeys<S>, O> = O extends { default: infer X } ? (TypeOfService<S, K> | X) : |
|
25 | export type InferReferenceType<S extends object, K extends ContainerKeys<S>, O> = O extends { default: infer X } ? (TypeOfService<S, K> | X) : | |
| 30 | O extends { optional: true } ? (TypeOfService<S, K> | undefined) : |
|
26 | O extends { optional: true } ? (TypeOfService<S, K> | undefined) : | |
| 31 | TypeOfService<S, K>; |
|
27 | TypeOfService<S, K>; | |
| @@ -35,14 +31,22 export interface Resolver<S extends obje | |||||
| 35 | <K extends ContainerKeys<S>, O extends DependencyOptions>(this: void, name: K, opts?: O): InferReferenceType<S, K, O>; |
|
31 | <K extends ContainerKeys<S>, O extends DependencyOptions>(this: void, name: K, opts?: O): InferReferenceType<S, K, O>; | |
| 36 | } |
|
32 | } | |
| 37 |
|
33 | |||
| 38 |
export interface DescriptorBuilder< |
|
34 | export interface DescriptorBuilder<S extends object, T> { | |
| 39 | service(service: AnnotationBuilder<T, S> | ServiceModule<T, S>): void; |
|
35 | factory(f: (resolve: Resolver<S>) => T): void; | |
|
|
36 | ||||
|
|
37 | build<T2>(): DescriptorBuilder<S, T2>; | |||
| 40 |
|
38 | |||
| 41 | factory(f: (resolve: Resolver<S>, activate: <T2>(lifetime: ILifetime, factory: () => T2, cleanup?: (item: T2) => void) => T2) => T): void; |
|
39 | override<K extends keyof S>(name: K, builder: RegistrationBuilder<S, S[K]>): this; | |
|
|
40 | override<K extends keyof S>(services: { [name in K]: RegistrationBuilder<S, S[K]> }): this; | |||
|
|
41 | ||||
|
|
42 | lifetime(lifetime: "singleton", typeId: any): this; | |||
|
|
43 | lifetime(lifetime: ILifetime | Exclude<ActivationType, "singleton">): this; | |||
|
|
44 | ||||
|
|
45 | cleanup(cb: (item: T) => void): this; | |||
| 42 |
|
46 | |||
| 43 | value(v: T): void; |
|
47 | value(v: T): void; | |
| 44 | } |
|
48 | } | |
| 45 |
|
49 | |||
| 46 | export interface Configuration<S extends object, Y extends keyof S = keyof S> { |
|
50 | export type RegistrationBuilder<S extends object, T> = (d: DescriptorBuilder<S, T>, ct?: ICancellation) => void | Promise<void>; | |
| 47 | register<K extends Y>(name: K, builder: (d: DescriptorBuilder<S[K], S>) => void): Configuration<S, Exclude<Y, K>>; |
|
51 | ||
| 48 | } |
|
52 | export type FluentRegistrations<K extends keyof S, S extends object> = { [k in K]: RegistrationBuilder<S, S[k]> }; | |
| @@ -37,10 +37,6 export type ContainerRegistered<S extend | |||||
| 37 |
|
37 | |||
| 38 | export type ActivationType = "singleton" | "container" | "hierarchy" | "context" | "call"; |
|
38 | export type ActivationType = "singleton" | "container" | "hierarchy" | "context" | "call"; | |
| 39 |
|
39 | |||
| 40 | export interface ILifetimeManager { |
|
|||
| 41 | create(context: ActivationContext<any>): ILifetime; |
|
|||
| 42 | } |
|
|||
| 43 |
|
||||
| 44 |
|
|
40 | /** | |
| 45 | * Интерфейс для управления жизнью экземпляра объекта. Каждая регистрация имеет |
|
41 | * Интерфейс для управления жизнью экземпляра объекта. Каждая регистрация имеет | |
| 46 | * свой собственный объект `ILifetime`, который создается при первой активации |
|
42 | * свой собственный объект `ILifetime`, который создается при первой активации | |
| @@ -1,12 +1,11 | |||||
| 1 | import { isPrimitive } from "../safe"; |
|
1 | import { isPrimitive } from "../safe"; | |
| 2 | import { Descriptor } from "./interfaces"; |
|
2 | import { Descriptor } from "./interfaces"; | |
| 3 | import { Configuration } from "./fluent/Configuration"; |
|
3 | import { FluentConfiguration } from "./fluent/FluentConfiguration"; | |
| 4 |
|
||||
| 5 | export function isDescriptor(x: any): x is Descriptor { |
|
4 | export function isDescriptor(x: any): x is Descriptor { | |
| 6 | return (!isPrimitive(x)) && |
|
5 | return (!isPrimitive(x)) && | |
| 7 | (x.activate instanceof Function); |
|
6 | (x.activate instanceof Function); | |
| 8 | } |
|
7 | } | |
| 9 |
|
8 | |||
| 10 |
export function |
|
9 | export function fluent<S extends object>() { | |
| 11 | return new Configuration<S>(); |
|
10 | return new FluentConfiguration<S>(); | |
| 12 | } |
|
11 | } | |
| @@ -1,4 +1,4 | |||||
| 1 | import { ICancellable, Constructor, IDestroyable } from "./interfaces"; |
|
1 | import { ICancellable, Constructor, IDestroyable, PromiseOrValue } from "./interfaces"; | |
| 2 | import { Cancellation } from "./Cancellation"; |
|
2 | import { Cancellation } from "./Cancellation"; | |
| 3 |
|
3 | |||
| 4 | let _nextOid = 0; |
|
4 | let _nextOid = 0; | |
| @@ -6,6 +6,8 const _oid = typeof Symbol === "function | |||||
| 6 | Symbol("__implab__oid__") : |
|
6 | Symbol("__implab__oid__") : | |
| 7 | "__implab__oid__"; |
|
7 | "__implab__oid__"; | |
| 8 |
|
8 | |||
|
|
9 | export function oid(instance: null | undefined): undefined; | |||
|
|
10 | export function oid(instance: NonNullable<any>): string; | |||
| 9 | export function oid(instance: any): string | undefined { |
|
11 | export function oid(instance: any): string | undefined { | |
| 10 | if (isNull(instance)) |
|
12 | if (isNull(instance)) | |
| 11 | return undefined; |
|
13 | return undefined; | |
| @@ -1,16 +1,19 | |||||
| 1 | import { Services } from "./services"; |
|
1 | import { Services } from "./services"; | |
| 2 |
import { |
|
2 | import { fluent } from "../di/traits"; | |
| 3 | import { LifetimeManager } from "../di/LifetimeManager"; |
|
3 | ||
|
|
4 | export default fluent<Services>().register({ | |||
|
|
5 | host: it => it.value("example.com"), | |||
| 4 |
|
6 | |||
| 5 | export const config = configure<Services>() |
|
7 | bar2: it => Promise.all([import("./Foo"), import("./Bar")]) | |
| 6 | .register("host", s => s.value("example.com")) |
|
8 | .then(([{ Foo }, { Bar }]) => it | |
| 7 | .register("bar2", bar2 => Promise.all([import("./Foo"), import("./Bar")]) |
|
9 | .lifetime("container") | |
| 8 | .then(([{ Foo }, { Bar }]) => { |
|
10 | .override({ | |
| 9 | const lifetime = LifetimeManager.hierarchyLifetime(); |
|
11 | host: it2 => it2.value("simple.org"), | |
| 10 |
|
12 | foo: it2 => it2.value(new Foo()) | ||
| 11 | bar2.factory((resolve, activate) => { |
|
13 | }) | |
|
|
14 | .factory(resolve => { | |||
| 12 | const bar = new Bar({ |
|
15 | const bar = new Bar({ | |
| 13 |
foo: |
|
16 | foo: new Foo(), | |
| 14 | nested: { |
|
17 | nested: { | |
| 15 | lazy: resolve("foo", { lazy: true }) |
|
18 | lazy: resolve("foo", { lazy: true }) | |
| 16 | }, |
|
19 | }, | |
| @@ -18,6 +21,6 export const config = configure<Services | |||||
| 18 | }, "some text"); |
|
21 | }, "some text"); | |
| 19 | bar.setName(resolve("host")); |
|
22 | bar.setName(resolve("host")); | |
| 20 | return bar; |
|
23 | return bar; | |
|
|
24 | }) | |||
|
|
25 | ) | |||
| 21 | }); |
|
26 | }); | |
| 22 | }) |
|
|||
| 23 | ); |
|
|||
| @@ -6,7 +6,6 import { ValueDescriptor } from "../di/V | |||||
| 6 | import { Foo } from "../mock/Foo"; |
|
6 | import { Foo } from "../mock/Foo"; | |
| 7 | import { Bar } from "../mock/Bar"; |
|
7 | import { Bar } from "../mock/Bar"; | |
| 8 | import { isNull } from "../safe"; |
|
8 | import { isNull } from "../safe"; | |
| 9 | import { Box } from "ts/mock/Box"; |
|
|||
| 10 |
|
9 | |||
| 11 | test("Container register/resolve tests", async t => { |
|
10 | test("Container register/resolve tests", async t => { | |
| 12 | const container = new Container<{ |
|
11 | const container = new Container<{ | |
| @@ -1,8 +1,6 | |||||
| 1 | { |
|
1 | { | |
| 2 | "extends": "../tsconfig", |
|
2 | "extends": "../tsconfig", | |
| 3 | "compilerOptions": { |
|
3 | "compilerOptions": { | |
| 4 | //"rootDir": "ts", |
|
|||
| 5 | "baseUrl": ".", |
|
|||
| 6 | "rootDirs": [ |
|
4 | "rootDirs": [ | |
| 7 | "ts", |
|
5 | "ts", | |
| 8 | "../main/ts" |
|
6 | "../main/ts" | |
| @@ -6,5 +6,6 define([ | |||||
| 6 | "./ObservableTests", |
|
6 | "./ObservableTests", | |
| 7 | "./ContainerTests", |
|
7 | "./ContainerTests", | |
| 8 | "./SafeTests", |
|
8 | "./SafeTests", | |
| 9 | "./TextTests" |
|
9 | "./TextTests", | |
|
|
10 | "./FluentContainerTests" | |||
| 10 | ]); No newline at end of file |
|
11 | ]); | |
| 1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
| 1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
General Comments 0
You need to be logged in to leave comments.
Login now
