| @@ -1,138 +1,123 | |||
|
|
1 | 1 | import { TraceSource } from "../log/TraceSource"; |
|
|
2 | 2 | import { argumentNotNull, argumentNotEmptyString } from "../safe"; |
|
|
3 |
import { Descriptor |
|
|
|
3 | import { Descriptor, ContainerServiceMap, ContainerKeys, ContainerResolve } from "./interfaces"; | |
|
|
4 | 4 | import { Container } from "./Container"; |
|
|
5 | 5 | import { MapOf } from "../interfaces"; |
|
|
6 | 6 | |
|
|
7 | 7 | const trace = TraceSource.get("@implab/core/di/ActivationContext"); |
|
|
8 | 8 | |
|
|
9 |
export interface ActivationContextInfo |
|
|
|
9 | export interface ActivationContextInfo { | |
|
|
10 | 10 | name: string; |
|
|
11 | 11 | |
|
|
12 | 12 | service: string; |
|
|
13 | 13 | |
|
|
14 | scope: ContainerServiceMap<S>; | |
|
|
15 | 14 | } |
|
|
16 | 15 | |
|
|
17 | 16 | export class ActivationContext<S extends object> { |
|
|
18 | 17 | _cache: MapOf<any>; |
|
|
19 | 18 | |
|
|
20 | 19 | _services: ContainerServiceMap<S>; |
|
|
21 | 20 | |
|
|
22 | _stack: ActivationContextInfo<S>[]; | |
|
|
23 | ||
|
|
24 | 21 | _visited: MapOf<any>; |
|
|
25 | 22 | |
|
|
26 | 23 | _name: string; |
|
|
27 | 24 | |
|
|
28 | _localized: boolean = false; | |
|
|
25 | _service: Descriptor<S, any>; | |
|
|
29 | 26 | |
|
|
30 | container: Container<S>; | |
|
|
27 | _container: Container<S>; | |
|
|
28 | ||
|
|
29 | _parent: ActivationContext<S> | undefined; | |
|
|
31 | 30 | |
|
|
32 |
constructor(container: Container<S>, services: ContainerServiceMap<S>, name |
|
|
|
33 | argumentNotNull(container, "container"); | |
|
|
34 |
|
|
|
|
35 | ||
|
|
36 |
this._ |
|
|
|
37 | this._visited = visited || {}; | |
|
|
38 | this._stack = []; | |
|
|
39 | this._cache = cache || {}; | |
|
|
31 | constructor(container: Container<S>, services: ContainerServiceMap<S>, name: string, service: Descriptor<S, any>) { | |
|
|
32 | this._name = name; | |
|
|
33 | this._service = service; | |
|
|
34 | this._visited = {}; | |
|
|
35 | this._cache = {}; | |
|
|
40 | 36 | this._services = services; |
|
|
41 | this.container = container; | |
|
|
37 | this._container = container; | |
|
|
42 | 38 | } |
|
|
43 | 39 | |
|
|
44 | 40 | getName() { |
|
|
45 | 41 | return this._name; |
|
|
46 | 42 | } |
|
|
47 | 43 | |
|
|
44 | getContainer() { | |
|
|
45 | return this._container; | |
|
|
46 | } | |
|
|
47 | ||
|
|
48 | 48 | resolve<K extends ContainerKeys<S>>(name: K, def?: ContainerResolve<S, K>) { |
|
|
49 | 49 | const d = this._services[name]; |
|
|
50 | 50 | |
|
|
51 | 51 | if (d !== undefined) { |
|
|
52 | 52 | return this.activate(d, name.toString()); |
|
|
53 | 53 | } else { |
|
|
54 | 54 | if (def !== undefined && def !== null) |
|
|
55 | 55 | return def; |
|
|
56 | 56 | else |
|
|
57 | 57 | throw new Error(`Service ${name} not found`); |
|
|
58 | 58 | } |
|
|
59 | 59 | } |
|
|
60 | 60 | |
|
|
61 | 61 | /** |
|
|
62 | 62 | * registers services local to the the activation context |
|
|
63 | 63 | * |
|
|
64 | 64 | * @name{string} the name of the service |
|
|
65 | 65 | * @service{string} the service descriptor to register |
|
|
66 | 66 | */ |
|
|
67 | 67 | register<K extends keyof S>(name: K, service: Descriptor<S, S[K]>) { |
|
|
68 | 68 | argumentNotEmptyString(name, "name"); |
|
|
69 | 69 | |
|
|
70 | 70 | this._services[name] = service as any; |
|
|
71 | 71 | } |
|
|
72 | 72 | |
|
|
73 | clone() { | |
|
|
74 | return new ActivationContext<S>( | |
|
|
75 | this.container, | |
|
|
76 | this._services, | |
|
|
77 | this._name, | |
|
|
78 | this._cache, | |
|
|
79 | this._visited | |
|
|
80 | ); | |
|
|
81 | } | |
|
|
82 | ||
|
|
83 | 73 | has(id: string) { |
|
|
84 | 74 | return id in this._cache; |
|
|
85 | 75 | } |
|
|
86 | 76 | |
|
|
87 | 77 | get<T>(id: string) { |
|
|
88 | 78 | return this._cache[id]; |
|
|
89 | 79 | } |
|
|
90 | 80 | |
|
|
91 | 81 | store(id: string, value: any) { |
|
|
92 | 82 | return (this._cache[id] = value); |
|
|
93 | 83 | } |
|
|
94 | 84 | |
|
|
95 | 85 | activate<T>(d: Descriptor<S, T>, name: string) { |
|
|
96 | 86 | if (trace.isLogEnabled()) |
|
|
97 | 87 | trace.log(`enter ${name} ${d}`); |
|
|
98 | 88 | |
|
|
99 |
this.enter( |
|
|
|
100 |
const v = d.activate( |
|
|
|
101 | this.leave(); | |
|
|
89 | const ctx = this.enter(d, name); | |
|
|
90 | const v = d.activate(ctx); | |
|
|
102 | 91 | |
|
|
103 | 92 | if (trace.isLogEnabled()) |
|
|
104 | 93 | trace.log(`leave ${name}`); |
|
|
105 | 94 | |
|
|
106 | 95 | return v; |
|
|
107 | 96 | } |
|
|
108 | 97 | |
|
|
109 | 98 | visit(id: string) { |
|
|
110 | 99 | const count = this._visited[id] || 0; |
|
|
111 | 100 | this._visited[id] = count + 1; |
|
|
112 | 101 | return count; |
|
|
113 | 102 | } |
|
|
114 | 103 | |
|
|
115 | getStack() { | |
|
|
116 | return this._stack.slice().reverse(); | |
|
|
104 | getStack(): ActivationContextInfo[] { | |
|
|
105 | const stack = [{ | |
|
|
106 | name: this._name, | |
|
|
107 | service: this._service.toString() | |
|
|
108 | }]; | |
|
|
109 | ||
|
|
110 | return this._parent ? | |
|
|
111 | stack.concat(this._parent.getStack()) : | |
|
|
112 | stack; | |
|
|
117 | 113 | } |
|
|
118 | 114 | |
|
|
119 | private enter(name: string, service: string) { | |
|
|
120 | this._stack.push({ | |
|
|
121 |
|
|
|
|
122 | service, | |
|
|
123 | scope: this._services | |
|
|
124 | }); | |
|
|
125 | this._name = name; | |
|
|
126 | this._services = Object.create(this._services); | |
|
|
127 | } | |
|
|
128 | ||
|
|
129 | private leave() { | |
|
|
130 | const ctx = this._stack.pop(); | |
|
|
131 | if (ctx) { | |
|
|
132 | this._services = ctx.scope; | |
|
|
133 | this._name = ctx.name; | |
|
|
134 | } else { | |
|
|
135 | trace.error("Trying to leave the last activation scope"); | |
|
|
136 | } | |
|
|
115 | private enter(service: Descriptor<S, any>, name: string): this { | |
|
|
116 | const clone = Object.create(this); | |
|
|
117 | clone._name = name; | |
|
|
118 | clone._services = Object.create(this._services); | |
|
|
119 | clone._parent = this; | |
|
|
120 | clone._service = service; | |
|
|
121 | return clone; | |
|
|
137 | 122 | } |
|
|
138 | 123 | } |
| @@ -1,143 +1,135 | |||
|
|
1 | 1 | import { ActivationContext } from "./ActivationContext"; |
|
|
2 | 2 | import { ValueDescriptor } from "./ValueDescriptor"; |
|
|
3 | 3 | import { ActivationError } from "./ActivationError"; |
|
|
4 | import { ServiceMap, Descriptor, PartialServiceMap, ContainerProvided, Resolver, ContainerServiceMap, ContainerKeys, ContainerResolve } from "./interfaces"; | |
|
|
4 | import { ServiceMap, Descriptor, PartialServiceMap, ContainerProvided, Resolver, ContainerServiceMap, ContainerKeys, ContainerResolve, ILifetimeManager } from "./interfaces"; | |
|
|
5 | 5 | import { TraceSource } from "../log/TraceSource"; |
|
|
6 | 6 | import { Configuration, RegistrationMap } from "./Configuration"; |
|
|
7 | 7 | import { Cancellation } from "../Cancellation"; |
|
|
8 | 8 | import { MapOf, IDestroyable } from "../interfaces"; |
|
|
9 | 9 | import { isDescriptor } from "./traits"; |
|
|
10 | import { LifetimeManager } from "./LifetimeManager"; | |
|
|
11 | import { each } from "../safe"; | |
|
|
10 | 12 | |
|
|
11 | 13 | const trace = TraceSource.get("@implab/core/di/ActivationContext"); |
|
|
12 | 14 | |
|
|
13 | 15 | export class Container<S extends object = any> implements Resolver<S>, IDestroyable { |
|
|
14 | 16 | readonly _services: ContainerServiceMap<S>; |
|
|
15 | 17 | |
|
|
16 | 18 | readonly _cache: MapOf<any>; |
|
|
17 | 19 | |
|
|
20 | readonly _lifetimeManager: ILifetimeManager; | |
|
|
21 | ||
|
|
18 | 22 | readonly _cleanup: (() => void)[]; |
|
|
19 | 23 | |
|
|
20 | 24 | readonly _root: Container<S>; |
|
|
21 | 25 | |
|
|
22 | 26 | readonly _parent?: Container<S>; |
|
|
23 | 27 | |
|
|
24 | 28 | _disposed: boolean; |
|
|
25 | 29 | |
|
|
26 | 30 | constructor(parent?: Container<S>) { |
|
|
27 | 31 | this._parent = parent; |
|
|
28 | 32 | this._services = parent ? Object.create(parent._services) : {}; |
|
|
29 | 33 | this._cache = {}; |
|
|
30 | 34 | this._cleanup = []; |
|
|
31 | 35 | this._root = parent ? parent.getRootContainer() : this; |
|
|
32 | 36 | this._services.container = new ValueDescriptor(this) as any; |
|
|
33 | 37 | this._disposed = false; |
|
|
38 | this._lifetimeManager = new LifetimeManager(); | |
|
|
34 | 39 | } |
|
|
35 | 40 | |
|
|
36 | 41 | getRootContainer() { |
|
|
37 | 42 | return this._root; |
|
|
38 | 43 | } |
|
|
39 | 44 | |
|
|
40 | 45 | getParent() { |
|
|
41 | 46 | return this._parent; |
|
|
42 | 47 | } |
|
|
43 | 48 | |
|
|
49 | getLifetimeManager() { | |
|
|
50 | return this._lifetimeManager; | |
|
|
51 | } | |
|
|
52 | ||
|
|
44 | 53 | resolve<K extends ContainerKeys<S>>(name: K, def?: ContainerResolve<S, K>): ContainerResolve<S, K> { |
|
|
45 | 54 | trace.debug("resolve {0}", name); |
|
|
46 | 55 | const d = this._services[name]; |
|
|
47 | 56 | if (d === undefined) { |
|
|
48 | 57 | if (def !== undefined) |
|
|
49 | 58 | return def; |
|
|
50 | 59 | else |
|
|
51 | 60 | throw new Error("Service '" + name + "' isn't found"); |
|
|
52 | 61 | } else { |
|
|
53 | 62 | |
|
|
54 | const context = new ActivationContext<S>(this, this._services); | |
|
|
63 | const context = new ActivationContext<S>(this, this._services, String(name), d); | |
|
|
55 | 64 | try { |
|
|
56 |
return |
|
|
|
65 | return d.activate(context); | |
|
|
57 | 66 | } catch (error) { |
|
|
58 | 67 | throw new ActivationError(name.toString(), context.getStack(), error); |
|
|
59 | 68 | } |
|
|
60 | 69 | } |
|
|
61 | 70 | } |
|
|
62 | 71 | |
|
|
63 | 72 | /** |
|
|
64 | 73 | * @deprecated use resolve() method |
|
|
65 | 74 | */ |
|
|
66 | 75 | getService<K extends ContainerKeys<S>>(name: K, def?: ContainerResolve<S, K>) { |
|
|
67 | 76 | return this.resolve(name, def); |
|
|
68 | 77 | } |
|
|
69 | 78 | |
|
|
70 | 79 | register<K extends keyof S>(name: K, service: Descriptor<S, S[K]>): this; |
|
|
71 | 80 | register(services: PartialServiceMap<S>): this; |
|
|
72 | 81 | register<K extends keyof S>(nameOrCollection: K | ServiceMap<S>, service?: Descriptor<S, S[K]>) { |
|
|
73 | 82 | if (arguments.length === 1) { |
|
|
74 | 83 | const data = nameOrCollection as ServiceMap<S>; |
|
|
75 | 84 | |
|
|
76 | for (const name in data) { | |
|
|
77 | if (Object.prototype.hasOwnProperty.call(data, name)) { | |
|
|
78 | this.register(name, data[name] as Descriptor<S, S[keyof S]>); | |
|
|
79 | } | |
|
|
80 | } | |
|
|
85 | each(data, (v, k) => this.register(k, v)); | |
|
|
81 | 86 | } else { |
|
|
82 | 87 | if (!isDescriptor(service)) |
|
|
83 | 88 | throw new Error("The service parameter must be a descriptor"); |
|
|
84 | 89 | |
|
|
85 | 90 | this._services[nameOrCollection as K] = service as any; |
|
|
86 | 91 | } |
|
|
87 | 92 | return this; |
|
|
88 | 93 | } |
|
|
89 | 94 | |
|
|
90 | 95 | onDispose(callback: () => void) { |
|
|
91 | 96 | if (!(callback instanceof Function)) |
|
|
92 | 97 | throw new Error("The callback must be a function"); |
|
|
93 | 98 | this._cleanup.push(callback); |
|
|
94 | 99 | } |
|
|
95 | 100 | |
|
|
96 | 101 | destroy() { |
|
|
97 | 102 | return this.dispose(); |
|
|
98 | 103 | } |
|
|
99 | 104 | dispose() { |
|
|
100 | 105 | if (this._disposed) |
|
|
101 | 106 | return; |
|
|
102 | 107 | this._disposed = true; |
|
|
103 | 108 | for (const f of this._cleanup) |
|
|
104 | 109 | f(); |
|
|
105 | 110 | } |
|
|
106 | 111 | |
|
|
107 | 112 | /** |
|
|
108 | 113 | * @param{String|Object} config |
|
|
109 | 114 | * The configuration of the contaier. Can be either a string or an object, |
|
|
110 | 115 | * if the configuration is an object it's treated as a collection of |
|
|
111 | 116 | * services which will be registed in the contaier. |
|
|
112 | 117 | * |
|
|
113 | 118 | * @param{Function} opts.contextRequire |
|
|
114 | 119 | * The function which will be used to load a configuration or types for services. |
|
|
115 | 120 | * |
|
|
116 | 121 | */ |
|
|
117 | 122 | async configure(config: string | RegistrationMap<S>, opts?: any, ct = Cancellation.none) { |
|
|
118 | 123 | const c = new Configuration<S>(this); |
|
|
119 | 124 | |
|
|
120 | 125 | if (typeof (config) === "string") { |
|
|
121 | 126 | return c.loadConfiguration(config, opts && opts.contextRequire, ct); |
|
|
122 | 127 | } else { |
|
|
123 | 128 | return c.applyConfiguration(config, opts && opts.contextRequire, ct); |
|
|
124 | 129 | } |
|
|
125 | 130 | } |
|
|
126 | 131 | |
|
|
127 | 132 | createChildContainer<S2 extends object = S>(): Container<S & S2> { |
|
|
128 | 133 | return new Container<S & S2>(this as any); |
|
|
129 | 134 | } |
|
|
130 | ||
|
|
131 | has(id: string | number) { | |
|
|
132 | return id in this._cache; | |
|
|
133 | } | |
|
|
134 | ||
|
|
135 | get(id: string | number) { | |
|
|
136 | return this._cache[id]; | |
|
|
137 | } | |
|
|
138 | ||
|
|
139 | store(id: string | number, value: any) { | |
|
|
140 | return (this._cache[id] = value); | |
|
|
141 | } | |
|
|
142 | ||
|
|
143 | 135 | } |
| @@ -1,92 +1,141 | |||
|
|
1 | 1 | import { IDestroyable, MapOf } from "../interfaces"; |
|
|
2 | 2 | import { argumentNotNull, isDestroyable } from "../safe"; |
|
|
3 | 3 | import { ILifetimeManager, ILifetime } from "./interfaces"; |
|
|
4 | 4 | import { ActivationContext } from "./ActivationContext"; |
|
|
5 | 5 | |
|
|
6 | 6 | function safeCall(item: () => void) { |
|
|
7 | 7 | try { |
|
|
8 | 8 | item(); |
|
|
9 | 9 | } catch { |
|
|
10 | // silence | |
|
|
10 | // silence! | |
|
|
11 | 11 | } |
|
|
12 | 12 | } |
|
|
13 | 13 | |
|
|
14 | const emptyLifetime: ILifetime = { | |
|
|
15 | has() { | |
|
|
16 | return false; | |
|
|
17 | }, | |
|
|
18 | ||
|
|
19 | enter() { | |
|
|
20 | ||
|
|
21 | }, | |
|
|
22 | ||
|
|
23 | get() { | |
|
|
24 | throw new Error("The specified item isn't registered with this lifetime manager"); | |
|
|
25 | }, | |
|
|
26 | ||
|
|
27 | store() { | |
|
|
28 | // does nothing | |
|
|
29 | } | |
|
|
30 | ||
|
|
31 | }; | |
|
|
32 | ||
|
|
14 | 33 | export class LifetimeManager implements IDestroyable, ILifetimeManager { |
|
|
15 | 34 | private _cleanup: (() => void)[] = []; |
|
|
16 | 35 | private _cache: MapOf<any> = {}; |
|
|
17 | 36 | private _destroyed = false; |
|
|
18 | 37 | |
|
|
19 |
initialize(id: string |
|
|
|
38 | initialize(id: string): ILifetime { | |
|
|
20 | 39 | const self = this; |
|
|
21 | 40 | let pending = false; |
|
|
22 | 41 | return { |
|
|
23 | 42 | has() { |
|
|
24 | 43 | return (id in self._cache); |
|
|
25 | 44 | }, |
|
|
26 | 45 | |
|
|
27 | 46 | get() { |
|
|
28 | 47 | const t = self._cache[id]; |
|
|
29 | 48 | if (t === undefined) |
|
|
30 | 49 | throw new Error(`The item with with the key ${id} isn't found`); |
|
|
31 | 50 | return t; |
|
|
32 | 51 | }, |
|
|
33 | 52 | |
|
|
34 | 53 | enter() { |
|
|
35 | 54 | if (pending) |
|
|
36 | 55 | throw Error(`Cyclic reference detected: the item with the key ${id} is already activating.`); |
|
|
37 | 56 | pending = true; |
|
|
38 | 57 | }, |
|
|
39 | 58 | |
|
|
40 | 59 | store(item: any, cleanup?: (item: any) => void) { |
|
|
41 | 60 | argumentNotNull(id, "id"); |
|
|
42 | 61 | argumentNotNull(item, "item"); |
|
|
43 | 62 | |
|
|
44 | 63 | if (this.has()) |
|
|
45 | 64 | throw new Error(`The item with with the key ${id} already registered with this lifetime manager`); |
|
|
46 | 65 | pending = false; |
|
|
47 | 66 | |
|
|
48 | 67 | self._cache[id] = item; |
|
|
49 | 68 | |
|
|
50 | 69 | if (self._destroyed) |
|
|
51 | 70 | throw new Error("Lifetime manager is destroyed"); |
|
|
52 | 71 | if (cleanup) { |
|
|
53 | 72 | self._cleanup.push(() => cleanup(item)); |
|
|
54 | 73 | } else if (isDestroyable(item)) { |
|
|
55 | 74 | self._cleanup.push(() => item.destroy()); |
|
|
56 | 75 | } |
|
|
57 | 76 | } |
|
|
58 | 77 | }; |
|
|
59 | 78 | } |
|
|
60 | 79 | |
|
|
61 | ||
|
|
62 | ||
|
|
63 | ||
|
|
64 | ||
|
|
65 | ||
|
|
66 | ||
|
|
67 | 80 | destroy() { |
|
|
68 | 81 | if (!this._destroyed) { |
|
|
69 | 82 | this._destroyed = true; |
|
|
70 | 83 | this._cleanup.forEach(safeCall); |
|
|
71 | 84 | this._cleanup.length = 0; |
|
|
72 | 85 | } |
|
|
73 | 86 | } |
|
|
74 | 87 | |
|
|
75 | 88 | static readonly empty: ILifetimeManager = { |
|
|
76 | has() { | |
|
|
77 |
return |
|
|
|
89 | initialize(): ILifetime { | |
|
|
90 | return emptyLifetime; | |
|
|
78 | 91 | }, |
|
|
92 | destroy() { | |
|
|
93 | throw new Error("Trying to destroy empty lifetime manager, this is a bug."); | |
|
|
94 | } | |
|
|
95 | ||
|
|
96 | }; | |
|
|
97 | ||
|
|
98 | static readonly hierarchyLifetime: ILifetimeManager = { | |
|
|
99 | initialize(id: string, context: ActivationContext<any>): ILifetime { | |
|
|
100 | return context.getContainer().getLifetimeManager().initialize(id, context); | |
|
|
101 | }, | |
|
|
102 | destroy() { | |
|
|
103 | throw new Error("Trying to destroy hierarchy lifetime manager, this is a bug."); | |
|
|
104 | } | |
|
|
105 | }; | |
|
|
79 | 106 | |
|
|
80 | get() { | |
|
|
81 | throw new Error("The specified item isn't registered with this lifetime manager"); | |
|
|
107 | static readonly singletonLifetime: ILifetimeManager = { | |
|
|
108 | initialize(id: string): ILifetime { | |
|
|
109 | return singletonLifetimeManager.initialize(id); | |
|
|
82 | 110 | }, |
|
|
111 | destroy() { | |
|
|
112 | throw new Error("Trying to destroy singleton lifetime manager, this is a bug."); | |
|
|
113 | } | |
|
|
114 | }; | |
|
|
83 | 115 | |
|
|
84 | register() { | |
|
|
85 | // does nothing | |
|
|
116 | static readonly contextLifetime: ILifetimeManager = { | |
|
|
117 | initialize(id: string, context: ActivationContext<any>): ILifetime { | |
|
|
118 | return { | |
|
|
119 | enter() { | |
|
|
120 | if (context.visit(id)) | |
|
|
121 | throw new Error("Cyclic reference detected"); | |
|
|
122 | }, | |
|
|
123 | get() { | |
|
|
124 | return context.get(id); | |
|
|
125 | }, | |
|
|
126 | has() { | |
|
|
127 | return context.has(id); | |
|
|
128 | }, | |
|
|
129 | store(item: any) { | |
|
|
130 | context.store(id, item); | |
|
|
131 | } | |
|
|
132 | ||
|
|
133 | }; | |
|
|
86 | 134 |
|
|
|
87 | ||
|
|
88 | 135 | destroy() { |
|
|
89 | 136 | throw new Error("Trying to destroy empty lifetime manager, this is a bug."); |
|
|
90 | 137 | } |
|
|
91 | 138 | }; |
|
|
92 | 139 | } |
|
|
140 | ||
|
|
141 | const singletonLifetimeManager = new LifetimeManager(); | |
| @@ -1,157 +1,164 | |||
|
|
1 | 1 | import { ActivationContext } from "./ActivationContext"; |
|
|
2 |
import { Descriptor, ServiceMap, PartialServiceMap, |
|
|
|
2 | import { Descriptor, ServiceMap, PartialServiceMap, ILifetimeManager } from "./interfaces"; | |
|
|
3 | 3 | import { argumentNotNull, isPrimitive, keys, isNull } from "../safe"; |
|
|
4 | 4 | import { TraceSource } from "../log/TraceSource"; |
|
|
5 | 5 | import { isDescriptor } from "./traits"; |
|
|
6 | 6 | import { LifetimeManager } from "./LifetimeManager"; |
|
|
7 | 7 | import { MatchingMemberKeys } from "../interfaces"; |
|
|
8 | import { Container } from "./Container"; | |
|
|
8 | 9 | |
|
|
9 | 10 | let cacheId = 0; |
|
|
10 | 11 | |
|
|
11 | 12 | const trace = TraceSource.get("@implab/core/di/ActivationContext"); |
|
|
12 | 13 | |
|
|
13 | 14 | function injectMethod<T, M extends keyof T, S extends object, A>(target: T, method: M, context: ActivationContext<S>, args: A) { |
|
|
14 | 15 | |
|
|
15 | 16 | const m = target[method]; |
|
|
16 | 17 | if (!m || typeof m !== "function") |
|
|
17 | 18 | throw new Error("Method '" + method + "' not found"); |
|
|
18 | 19 | |
|
|
19 | 20 | if (args instanceof Array) |
|
|
20 | 21 | return m.apply(target, _parse(args, context, "." + method)); |
|
|
21 | 22 | else |
|
|
22 | 23 | return m.call(target, _parse(args, context, "." + method)); |
|
|
23 | 24 | } |
|
|
24 | 25 | |
|
|
25 | 26 | function makeCleanupCallback<T>(method: Cleaner<T>) { |
|
|
26 | 27 | if (typeof (method) === "function") { |
|
|
27 | 28 | return (target: T) => { |
|
|
28 | 29 | method(target); |
|
|
29 | 30 | }; |
|
|
30 | 31 | } else { |
|
|
31 | 32 | return (target: T) => { |
|
|
32 | 33 | const m = target[method] as any; |
|
|
33 | 34 | m.apply(target); |
|
|
34 | 35 | }; |
|
|
35 | 36 | } |
|
|
36 | 37 | } |
|
|
37 | 38 | |
|
|
38 | 39 | function _parse(value: any, context: ActivationContext<any>, path: string): any { |
|
|
39 | 40 | if (isPrimitive(value)) |
|
|
40 | 41 | return value as any; |
|
|
41 | 42 | |
|
|
42 | 43 | trace.debug("parse {0}", path); |
|
|
43 | 44 | |
|
|
44 | 45 | if (isDescriptor(value)) |
|
|
45 | 46 | return context.activate(value, path); |
|
|
46 | 47 | |
|
|
47 | 48 | if (value instanceof Array) |
|
|
48 | 49 | return value.map((x, i) => _parse(x, context, `${path}[${i}]`)) as any; |
|
|
49 | 50 | |
|
|
50 | 51 | const t: any = {}; |
|
|
51 | 52 | |
|
|
52 | 53 | keys(value).forEach(p => t[p] = _parse(value[p], context, `${path}.${p}`)); |
|
|
53 | 54 | |
|
|
54 | 55 | return t; |
|
|
55 | 56 | } |
|
|
56 | 57 | |
|
|
57 | 58 | export type Cleaner<T> = ((x: T) => void) | MatchingMemberKeys<() => void, T>; |
|
|
58 | 59 | |
|
|
59 | 60 | export type InjectionSpec<T> = { |
|
|
60 | 61 | [m in keyof T]?: any; |
|
|
61 | 62 | }; |
|
|
62 | 63 | |
|
|
63 | 64 | export interface ServiceDescriptorParams<S extends object, T, P extends any[]> { |
|
|
64 | lifetime: ILifetimeManager; | |
|
|
65 | owner: Container<S>; | |
|
|
66 | ||
|
|
67 | lifetime?: ILifetimeManager; | |
|
|
65 | 68 | |
|
|
66 | 69 | params?: P; |
|
|
67 | 70 | |
|
|
68 | 71 | inject?: InjectionSpec<T>[]; |
|
|
69 | 72 | |
|
|
70 | 73 | services?: PartialServiceMap<S>; |
|
|
71 | 74 | |
|
|
72 | 75 | cleanup?: Cleaner<T>; |
|
|
73 | 76 | } |
|
|
74 | 77 | |
|
|
75 | 78 | export class ServiceDescriptor<S extends object, T, P extends any[]> implements Descriptor<S, T> { |
|
|
76 | 79 | _services: ServiceMap<S>; |
|
|
77 | 80 | |
|
|
78 | 81 | _params: P | undefined; |
|
|
79 | 82 | |
|
|
80 | 83 | _inject: InjectionSpec<T>[]; |
|
|
81 | 84 | |
|
|
82 | 85 | _cleanup: ((item: T) => void) | undefined; |
|
|
83 | 86 | |
|
|
84 | 87 | _cacheId = String(++cacheId); |
|
|
85 | 88 | |
|
|
86 | 89 | _lifetime = LifetimeManager.empty; |
|
|
87 | 90 | |
|
|
91 | _owner: Container<S>; | |
|
|
92 | ||
|
|
88 | 93 | constructor(opts: ServiceDescriptorParams<S, T, P>) { |
|
|
89 | argumentNotNull(opts, "opts"); | |
|
|
94 | argumentNotNull(opts && opts.owner, "opts.owner"); | |
|
|
95 | ||
|
|
96 | this._owner = opts.owner; | |
|
|
90 | 97 | |
|
|
91 | 98 | if (opts.lifetime) |
|
|
92 | 99 | this._lifetime = opts.lifetime; |
|
|
93 | 100 | |
|
|
94 | 101 | if (!isNull(opts.params)) |
|
|
95 | 102 | this._params = opts.params; |
|
|
96 | 103 | |
|
|
97 | 104 | this._inject = opts.inject || []; |
|
|
98 | 105 | |
|
|
99 | 106 | this._services = (opts.services || {}) as ServiceMap<S>; |
|
|
100 | 107 | |
|
|
101 | 108 | if (opts.cleanup) { |
|
|
102 | 109 | if (!(typeof (opts.cleanup) === "string" || opts.cleanup instanceof Function)) |
|
|
103 | 110 | throw new Error( |
|
|
104 | 111 | "The cleanup parameter must be either a function or a function name"); |
|
|
105 | 112 | |
|
|
106 | 113 | this._cleanup = makeCleanupCallback(opts.cleanup); |
|
|
107 | 114 | } |
|
|
108 | 115 | } |
|
|
109 | 116 | |
|
|
110 | 117 | activate(context: ActivationContext<S>) { |
|
|
111 | 118 | const lifetime = this._lifetime.initialize(this._cacheId, context); |
|
|
112 | 119 | |
|
|
113 | 120 | if (lifetime.has()) { |
|
|
114 | 121 | return lifetime.get(); |
|
|
115 | 122 | } else { |
|
|
116 | 123 | lifetime.enter(); |
|
|
117 | 124 | const instance = this._create(context); |
|
|
118 | 125 | lifetime.store(this._cacheId, this._cleanup); |
|
|
119 | 126 | return instance; |
|
|
120 | 127 | } |
|
|
121 | 128 | } |
|
|
122 | 129 | |
|
|
123 | 130 | _factory(...params: any[]): T { |
|
|
124 | 131 | throw Error("Not implemented"); |
|
|
125 | 132 | } |
|
|
126 | 133 | |
|
|
127 | 134 | _create(context: ActivationContext<S>) { |
|
|
128 | 135 | trace.debug(`constructing ${context._name}`); |
|
|
129 | 136 | |
|
|
130 | 137 | if (this._services) { |
|
|
131 | 138 | keys(this._services).forEach(p => context.register(p, this._services[p])); |
|
|
132 | 139 | } |
|
|
133 | 140 | |
|
|
134 | 141 | let instance: T; |
|
|
135 | 142 | |
|
|
136 | 143 | if (this._params === undefined) { |
|
|
137 | 144 | instance = this._factory(); |
|
|
138 | 145 | } else if (this._params instanceof Array) { |
|
|
139 | 146 | instance = this._factory.apply(this, _parse(this._params, context, "args")); |
|
|
140 | 147 | } else { |
|
|
141 | 148 | instance = this._factory(_parse(this._params, context, "args")); |
|
|
142 | 149 | } |
|
|
143 | 150 | |
|
|
144 | 151 | if (this._inject) { |
|
|
145 | 152 | this._inject.forEach(spec => { |
|
|
146 | 153 | for (const m in spec) |
|
|
147 | 154 | injectMethod(instance, m, context, spec[m]); |
|
|
148 | 155 | }); |
|
|
149 | 156 | } |
|
|
150 | 157 | return instance; |
|
|
151 | 158 | } |
|
|
152 | 159 | |
|
|
153 | 160 | clone() { |
|
|
154 | 161 | return Object.create(this); |
|
|
155 | 162 | } |
|
|
156 | 163 | |
|
|
157 | 164 | } |
| @@ -1,55 +1,53 | |||
|
|
1 | 1 | import { ActivationContext } from "./ActivationContext"; |
|
|
2 | 2 | import { IDestroyable } from "../interfaces"; |
|
|
3 | 3 | |
|
|
4 | 4 | export interface Descriptor<S extends object = any, T = any> { |
|
|
5 | 5 | activate(context: ActivationContext<S>): T; |
|
|
6 | ||
|
|
7 | clone(): this; | |
|
|
8 | 6 | } |
|
|
9 | 7 | |
|
|
10 | 8 | export type ServiceMap<S extends object> = { |
|
|
11 | 9 | [k in keyof S]: Descriptor<S, S[k]>; |
|
|
12 | 10 | }; |
|
|
13 | 11 | |
|
|
14 | 12 | export type ContainerKeys<S extends object> = keyof S | keyof ContainerProvided<S>; |
|
|
15 | 13 | |
|
|
16 | 14 | export type ContainerResolve<S extends object, K> = |
|
|
17 | 15 | K extends keyof ContainerProvided<S> ? ContainerProvided<S>[K] : |
|
|
18 | 16 | K extends keyof S ? S[K] : never; |
|
|
19 | 17 | |
|
|
20 | 18 | export type ContainerServiceMap<S extends object> = { |
|
|
21 | 19 | [K in ContainerKeys<S>]: Descriptor<S, ContainerResolve<S, K>>; |
|
|
22 | 20 | }; |
|
|
23 | 21 | |
|
|
24 | 22 | export type PartialServiceMap<S extends object> = { |
|
|
25 | 23 | [k in keyof S]?: Descriptor<S, S[k]>; |
|
|
26 | 24 | }; |
|
|
27 | 25 | |
|
|
28 | 26 | export interface Resolver<S extends object> { |
|
|
29 | 27 | resolve<K extends ContainerKeys<S>>(name: K, def?: ContainerResolve<S, K>): ContainerResolve<S, K>; |
|
|
30 | 28 | } |
|
|
31 | 29 | |
|
|
32 | 30 | export interface ContainerProvided<S extends object> { |
|
|
33 | 31 | container: Resolver<S>; |
|
|
34 | 32 | } |
|
|
35 | 33 | |
|
|
36 | 34 | export type ContainerRegistered<S extends object> = /*{ |
|
|
37 | 35 | [K in Exclude<keyof S, keyof ContainerProvided<S>>]: S[K]; |
|
|
38 | 36 | };*/ |
|
|
39 | 37 | Exclude<S, ContainerProvided<S>>; |
|
|
40 | 38 | |
|
|
41 | 39 | export type ActivationType = "singleton" | "container" | "hierarchy" | "context" | "call"; |
|
|
42 | 40 | |
|
|
43 | 41 | export interface ILifetimeManager extends IDestroyable { |
|
|
44 | 42 | initialize(id: string, context: ActivationContext<any>): ILifetime; |
|
|
45 | 43 | } |
|
|
46 | 44 | |
|
|
47 | 45 | export interface ILifetime { |
|
|
48 | 46 | has(): boolean; |
|
|
49 | 47 | |
|
|
50 | 48 | get(): any; |
|
|
51 | 49 | |
|
|
52 | 50 | enter(): void; |
|
|
53 | 51 | |
|
|
54 | 52 | store(item: any, cleanup?: (item: any) => void): void; |
|
|
55 | 53 | } No newline at end of file |
General Comments 0
You need to be logged in to leave comments.
Login now
