| @@ -0,0 +1,70 | |||
|
|
1 | import { IDestroyable, MapOf } from "../interfaces"; | |
|
|
2 | import { argumentNotNull, isDestroyable } from "../safe"; | |
|
|
3 | import { ILifetimeManager } from "./interfaces"; | |
|
|
4 | ||
|
|
5 | function safeCall(item: () => void) { | |
|
|
6 | try { | |
|
|
7 | item(); | |
|
|
8 | } catch { | |
|
|
9 | // silence | |
|
|
10 | } | |
|
|
11 | } | |
|
|
12 | ||
|
|
13 | export class LifetimeManager implements IDestroyable, ILifetimeManager { | |
|
|
14 | private _cleanup: (() => void)[] = []; | |
|
|
15 | private _cache: MapOf<any> = {}; | |
|
|
16 | private _destroyed = false; | |
|
|
17 | ||
|
|
18 | has(id: string) { | |
|
|
19 | return id in this._cache; | |
|
|
20 | } | |
|
|
21 | ||
|
|
22 | get(id: string) { | |
|
|
23 | const t = this._cache[id]; | |
|
|
24 | if (t === undefined) | |
|
|
25 | throw new Error(`The item with with the key ${id} isn't found`); | |
|
|
26 | return t; | |
|
|
27 | } | |
|
|
28 | ||
|
|
29 | register(id: string, item: any, cleanup?: (item: any) => void) { | |
|
|
30 | argumentNotNull(id, "id"); | |
|
|
31 | argumentNotNull(item, "item"); | |
|
|
32 | if (this.has(id)) | |
|
|
33 | throw new Error(`The item with with the key ${id} already registered with this lifetime manager`); | |
|
|
34 | this._cache[id] = item; | |
|
|
35 | ||
|
|
36 | if (this._destroyed) | |
|
|
37 | throw new Error("Lifetime manager is destroyed"); | |
|
|
38 | if (cleanup) { | |
|
|
39 | this._cleanup.push(() => cleanup(item)); | |
|
|
40 | } else if (isDestroyable(item)) { | |
|
|
41 | this._cleanup.push(() => item.destroy()); | |
|
|
42 | } | |
|
|
43 | } | |
|
|
44 | ||
|
|
45 | destroy() { | |
|
|
46 | if (!this._destroyed) { | |
|
|
47 | this._destroyed = true; | |
|
|
48 | this._cleanup.forEach(safeCall); | |
|
|
49 | this._cleanup.length = 0; | |
|
|
50 | } | |
|
|
51 | } | |
|
|
52 | ||
|
|
53 | static readonly empty: ILifetimeManager = { | |
|
|
54 | has() { | |
|
|
55 | return false; | |
|
|
56 | }, | |
|
|
57 | ||
|
|
58 | get() { | |
|
|
59 | throw new Error("The specified item isn't registered with this lifetime manager"); | |
|
|
60 | }, | |
|
|
61 | ||
|
|
62 | register() { | |
|
|
63 | // does nothing | |
|
|
64 | }, | |
|
|
65 | ||
|
|
66 | destroy() { | |
|
|
67 | throw new Error("Trying to destroy empty lifetime manager, this is a bug."); | |
|
|
68 | } | |
|
|
69 | }; | |
|
|
70 | } | |
| @@ -33,4 +33,8 export class AggregateDescriptor<S exten | |||
|
|
33 | 33 | toString() { |
|
|
34 | 34 | return "@walk"; |
|
|
35 | 35 | } |
|
|
36 | ||
|
|
37 | clone() { | |
|
|
38 | return this; | |
|
|
36 | 39 | } |
|
|
40 | } | |
| @@ -5,12 +5,12 import { ServiceMap, Descriptor, Partial | |||
|
|
5 | 5 | import { TraceSource } from "../log/TraceSource"; |
|
|
6 | 6 | import { Configuration, RegistrationMap } from "./Configuration"; |
|
|
7 | 7 | import { Cancellation } from "../Cancellation"; |
|
|
8 | import { MapOf } from "../interfaces"; | |
|
|
8 | import { MapOf, IDestroyable } from "../interfaces"; | |
|
|
9 | 9 | import { isDescriptor } from "./traits"; |
|
|
10 | 10 | |
|
|
11 | 11 | const trace = TraceSource.get("@implab/core/di/ActivationContext"); |
|
|
12 | 12 | |
|
|
13 | export class Container<S extends object = any> implements Resolver<S> { | |
|
|
13 | export class Container<S extends object = any> implements Resolver<S>, IDestroyable { | |
|
|
14 | 14 | readonly _services: ContainerServiceMap<S>; |
|
|
15 | 15 | |
|
|
16 | 16 | readonly _cache: MapOf<any>; |
| @@ -93,6 +93,9 export class Container<S extends object | |||
|
|
93 | 93 | this._cleanup.push(callback); |
|
|
94 | 94 | } |
|
|
95 | 95 | |
|
|
96 | destroy() { | |
|
|
97 | return this.dispose(); | |
|
|
98 | } | |
|
|
96 | 99 | dispose() { |
|
|
97 | 100 | if (this._disposed) |
|
|
98 | 101 | return; |
| @@ -14,8 +14,5 export class FactoryServiceDescriptor<S | |||
|
|
14 | 14 | // bind to null |
|
|
15 | 15 | this._factory = (...args) => opts.factory.apply(null, args as any); |
|
|
16 | 16 | |
|
|
17 | if (opts.activation === "singleton") { | |
|
|
18 | this._cacheId = oid(opts.factory); | |
|
|
19 | 17 |
|
|
|
20 | 18 | } |
|
|
21 | } | |
| @@ -1,9 +1,10 | |||
|
|
1 | 1 | import { ActivationContext } from "./ActivationContext"; |
|
|
2 | import { Descriptor, ServiceMap, PartialServiceMap, ActivationType } from "./interfaces"; | |
|
|
3 | import { Container } from "./Container"; | |
|
|
2 | import { Descriptor, ServiceMap, PartialServiceMap, ActivationType, ILifetimeManager } from "./interfaces"; | |
|
|
4 | 3 | import { argumentNotNull, isPrimitive, keys, isNull } from "../safe"; |
|
|
5 | 4 | import { TraceSource } from "../log/TraceSource"; |
|
|
6 | 5 | import { isDescriptor } from "./traits"; |
|
|
6 | import { LifetimeManager } from "./LifetimeManager"; | |
|
|
7 | import { MatchingMemberKeys } from "../interfaces"; | |
|
|
7 | 8 | |
|
|
8 | 9 | let cacheId = 0; |
|
|
9 | 10 | |
| @@ -21,15 +22,14 function injectMethod<T, M extends keyof | |||
|
|
21 | 22 | return m.call(target, _parse(args, context, "." + method)); |
|
|
22 | 23 | } |
|
|
23 | 24 | |
|
|
24 |
function makeClenupCallback<T>( |
|
|
|
25 | function makeClenupCallback(target: any, method: any) { | |
|
|
26 | if (typeof (method) === "string") { | |
|
|
27 | return () => { | |
|
|
28 | target[method](); | |
|
|
25 | function makeClenupCallback<T>(method: Cleaner<T>) { | |
|
|
26 | if (typeof (method) === "function") { | |
|
|
27 | return (target: T) => { | |
|
|
28 | method(target); | |
|
|
29 | 29 | }; |
|
|
30 | 30 | } else { |
|
|
31 | return () => { | |
|
|
32 |
|
|
|
|
31 | return (target: T) => { | |
|
|
32 | (target[method] as any)(); | |
|
|
33 | 33 | }; |
|
|
34 | 34 | } |
|
|
35 | 35 | } |
| @@ -53,16 +53,14 function _parse(value: any, context: Act | |||
|
|
53 | 53 | return t; |
|
|
54 | 54 | } |
|
|
55 | 55 | |
|
|
56 |
export type Cleaner<T> = ((x: T) => void) | |
|
|
|
56 | export type Cleaner<T> = ((x: T) => void) | MatchingMemberKeys<() => void, T>; | |
|
|
57 | 57 | |
|
|
58 | 58 | export type InjectionSpec<T> = { |
|
|
59 | 59 | [m in keyof T]?: any; |
|
|
60 | 60 | }; |
|
|
61 | 61 | |
|
|
62 | 62 | export interface ServiceDescriptorParams<S extends object, T, P extends any[]> { |
|
|
63 | activation?: ActivationType; | |
|
|
64 | ||
|
|
65 | owner: Container<S>; | |
|
|
63 | lifetime: ILifetimeManager; | |
|
|
66 | 64 | |
|
|
67 | 65 | params?: P; |
|
|
68 | 66 | |
| @@ -74,32 +72,23 export interface ServiceDescriptorParams | |||
|
|
74 | 72 | } |
|
|
75 | 73 | |
|
|
76 | 74 | export class ServiceDescriptor<S extends object, T, P extends any[]> implements Descriptor<S, T> { |
|
|
77 | _instance: T | undefined; | |
|
|
78 | ||
|
|
79 | _hasInstance = false; | |
|
|
80 | ||
|
|
81 | _activationType: ActivationType = "call"; | |
|
|
82 | ||
|
|
83 | 75 | _services: ServiceMap<S>; |
|
|
84 | 76 | |
|
|
85 | 77 | _params: P | undefined; |
|
|
86 | 78 | |
|
|
87 | 79 | _inject: InjectionSpec<T>[]; |
|
|
88 | 80 | |
|
|
89 |
_cleanup: |
|
|
|
81 | _cleanup: ((item: T) => void) | undefined; | |
|
|
90 | 82 | |
|
|
91 | _cacheId: any; | |
|
|
83 | _cacheId = String(++cacheId); | |
|
|
92 | 84 | |
|
|
93 | _owner: Container<S>; | |
|
|
85 | _lifetime = LifetimeManager.empty; | |
|
|
94 | 86 | |
|
|
95 | 87 | constructor(opts: ServiceDescriptorParams<S, T, P>) { |
|
|
96 | 88 | argumentNotNull(opts, "opts"); |
|
|
97 | argumentNotNull(opts.owner, "owner"); | |
|
|
98 | 89 | |
|
|
99 | this._owner = opts.owner; | |
|
|
100 | ||
|
|
101 | if (!isNull(opts.activation)) | |
|
|
102 | this._activationType = opts.activation; | |
|
|
90 | if (opts.lifetime) | |
|
|
91 | this._lifetime = opts.lifetime; | |
|
|
103 | 92 | |
|
|
104 | 93 | if (!isNull(opts.params)) |
|
|
105 | 94 | this._params = opts.params; |
| @@ -113,96 +102,22 export class ServiceDescriptor<S extends | |||
|
|
113 | 102 | throw new Error( |
|
|
114 | 103 | "The cleanup parameter must be either a function or a function name"); |
|
|
115 | 104 | |
|
|
116 | this._cleanup = opts.cleanup; | |
|
|
105 | this._cleanup = makeClenupCallback(opts.cleanup); | |
|
|
117 | 106 | } |
|
|
118 | 107 | } |
|
|
119 | 108 | |
|
|
120 | 109 | activate(context: ActivationContext<S>) { |
|
|
121 | // if we have a local service records, register them first | |
|
|
122 | let instance: T; | |
|
|
123 | ||
|
|
124 | // ensure we have a cache id | |
|
|
125 | if (!this._cacheId) | |
|
|
126 | this._cacheId = ++cacheId; | |
|
|
127 | ||
|
|
128 | switch (this._activationType) { | |
|
|
129 | case "singleton": // SINGLETON | |
|
|
130 | // if the value is cached return it | |
|
|
131 | if (this._hasInstance) | |
|
|
132 | return this._instance; | |
|
|
133 | ||
|
|
134 | // singletons are bound to the root container | |
|
|
135 | const container = context.container.getRootContainer(); | |
|
|
136 | ||
|
|
137 | if (container.has(this._cacheId)) { | |
|
|
138 | instance = container.get(this._cacheId); | |
|
|
139 | } else { | |
|
|
140 | instance = this._create(context); | |
|
|
141 | container.store(this._cacheId, instance); | |
|
|
142 | if (this._cleanup) | |
|
|
143 | container.onDispose( | |
|
|
144 | makeClenupCallback(instance, this._cleanup)); | |
|
|
145 | } | |
|
|
146 | ||
|
|
147 | this._hasInstance = true; | |
|
|
148 | return (this._instance = instance); | |
|
|
149 | ||
|
|
150 | case "container": // CONTAINER | |
|
|
151 | // return a cached value | |
|
|
152 | ||
|
|
153 | if (this._hasInstance) | |
|
|
154 | return this._instance; | |
|
|
155 | ||
|
|
156 | // create an instance | |
|
|
157 | instance = this._create(context); | |
|
|
110 | const lifetime = this._lifetime.initialize(this._cacheId, context); | |
|
|
158 | 111 | |
|
|
159 | // the instance is bound to the container | |
|
|
160 | if (this._cleanup) | |
|
|
161 | this._owner.onDispose( | |
|
|
162 | makeClenupCallback(instance, this._cleanup)); | |
|
|
163 | ||
|
|
164 |
|
|
|
|
165 | this._hasInstance = true; | |
|
|
166 | return (this._instance = instance); | |
|
|
167 | case "context": // CONTEXT | |
|
|
168 | // return a cached value if one exists | |
|
|
169 | ||
|
|
170 | if (context.has(this._cacheId)) | |
|
|
171 | return context.get(this._cacheId); | |
|
|
172 | // context context activated instances are controlled by callers | |
|
|
173 | return context.store(this._cacheId, this._create(context)); | |
|
|
174 | case "call": // CALL | |
|
|
175 | // per-call created instances are controlled by callers | |
|
|
176 | return this._create(context); | |
|
|
177 | case "hierarchy": // HIERARCHY | |
|
|
178 | // hierarchy activated instances are behave much like container activated | |
|
|
179 | // except they are created and bound to the child container | |
|
|
180 | ||
|
|
181 | // return a cached value | |
|
|
182 | if (context.container.has(this._cacheId)) | |
|
|
183 | return context.container.get(this._cacheId); | |
|
|
184 | ||
|
|
185 | instance = this._create(context); | |
|
|
186 | ||
|
|
187 | if (this._cleanup) | |
|
|
188 | context.container.onDispose(makeClenupCallback( | |
|
|
189 | instance, | |
|
|
190 | this._cleanup)); | |
|
|
191 | ||
|
|
192 | return context.container.store(this._cacheId, instance); | |
|
|
193 | default: | |
|
|
194 | throw new Error("Invalid activation type: " + this._activationType); | |
|
|
112 | if (lifetime.has()) { | |
|
|
113 | return lifetime.get(); | |
|
|
114 | } else { | |
|
|
115 | const instance = this._create(context); | |
|
|
116 | lifetime.store(this._cacheId, this._cleanup); | |
|
|
117 | return instance; | |
|
|
195 | 118 | } |
|
|
196 | 119 | } |
|
|
197 | 120 | |
|
|
198 | isInstanceCreated() { | |
|
|
199 | return this._hasInstance; | |
|
|
200 | } | |
|
|
201 | ||
|
|
202 | getInstance() { | |
|
|
203 | return this._instance; | |
|
|
204 | } | |
|
|
205 | ||
|
|
206 | 121 | _factory(...params: any[]): T { |
|
|
207 | 122 | throw Error("Not implemented"); |
|
|
208 | 123 | } |
| @@ -210,10 +125,6 export class ServiceDescriptor<S extends | |||
|
|
210 | 125 | _create(context: ActivationContext<S>) { |
|
|
211 | 126 | trace.debug(`constructing ${context._name}`); |
|
|
212 | 127 | |
|
|
213 | if (this._activationType !== "call" && | |
|
|
214 | context.visit(this._cacheId) > 0) | |
|
|
215 | throw new Error("Recursion detected"); | |
|
|
216 | ||
|
|
217 | 128 | if (this._services) { |
|
|
218 | 129 | keys(this._services).forEach(p => context.register(p, this._services[p])); |
|
|
219 | 130 | } |
| @@ -236,4 +147,9 export class ServiceDescriptor<S extends | |||
|
|
236 | 147 | } |
|
|
237 | 148 | return instance; |
|
|
238 | 149 | } |
|
|
150 | ||
|
|
151 | clone() { | |
|
|
152 | return Object.create(this); | |
|
|
239 | 153 | } |
|
|
154 | ||
|
|
155 | } | |
| @@ -46,8 +46,6 export interface RegistrationVisitor<S e | |||
|
|
46 | 46 | |
|
|
47 | 47 | visitFactoryRegistration(): void; |
|
|
48 | 48 | |
|
|
49 | visitBuilder<T>(builder: (t: ServiceRecordBuilder<T, S>) => void): void; | |
|
|
50 | ||
|
|
51 | 49 | } |
|
|
52 | 50 | |
|
|
53 | 51 | export interface RegistrationBuilder<T, S extends object> { |
| @@ -1,7 +1,10 | |||
|
|
1 | 1 | import { ActivationContext } from "./ActivationContext"; |
|
|
2 | import { IDestroyable } from "../interfaces"; | |
|
|
2 | 3 | |
|
|
3 | 4 | export interface Descriptor<S extends object = any, T = any> { |
|
|
4 | 5 | activate(context: ActivationContext<S>): T; |
|
|
6 | ||
|
|
7 | clone(): this; | |
|
|
5 | 8 | } |
|
|
6 | 9 | |
|
|
7 | 10 | export type ServiceMap<S extends object> = { |
| @@ -36,3 +39,13 export type ContainerRegistered<S extend | |||
|
|
36 | 39 | Exclude<S, ContainerProvided<S>>; |
|
|
37 | 40 | |
|
|
38 | 41 | export type ActivationType = "singleton" | "container" | "hierarchy" | "context" | "call"; |
|
|
42 | ||
|
|
43 | export interface ILifetimeManager extends IDestroyable { | |
|
|
44 | initialize(id: string, context: ActivationContext<any>): ILifetime; | |
|
|
45 | } | |
|
|
46 | ||
|
|
47 | export interface ILifetime { | |
|
|
48 | has(): boolean; | |
|
|
49 | get(): any; | |
|
|
50 | store(item: any, cleanup?: (item: any) => void): void; | |
|
|
51 | } No newline at end of file | |
| @@ -9,6 +9,14 export type Factory<T = {}> = (...args: | |||
|
|
9 | 9 | |
|
|
10 | 10 | export type Predicate<T = any> = (x: T) => boolean; |
|
|
11 | 11 | |
|
|
12 | export type MatchingMemberKeys<T, From> = { [K in keyof From]: From[K] extends T ? K : never}[keyof From]; | |
|
|
13 | ||
|
|
14 | export type NotMatchingMemberKeys<T, From> = { [K in keyof From]: From[K] extends T ? never : K}[keyof From]; | |
|
|
15 | ||
|
|
16 | export type ExtractMembers<T, From> = Pick<From, MatchingMemberKeys<T, From>>; | |
|
|
17 | ||
|
|
18 | export type ExcludeMembers<T, From> = Pick<From, NotMatchingMemberKeys<T, From>>; | |
|
|
19 | ||
|
|
12 | 20 | export interface MapOf<T> { |
|
|
13 | 21 | [key: string]: T; |
|
|
14 | 22 | } |
| @@ -1,4 +1,4 | |||
|
|
1 | import { ICancellable, Constructor } from "./interfaces"; | |
|
|
1 | import { ICancellable, Constructor, IDestroyable } from "./interfaces"; | |
|
|
2 | 2 | import { Cancellation } from "./Cancellation"; |
|
|
3 | 3 | |
|
|
4 | 4 | let _nextOid = 0; |
| @@ -477,6 +477,12 export function firstWhere<T>( | |||
|
|
477 | 477 | } |
|
|
478 | 478 | } |
|
|
479 | 479 | |
|
|
480 | export function isDestroyable(d: any): d is IDestroyable { | |
|
|
481 | if (d && "destroy" in d && typeof(destroy) === "function") | |
|
|
482 | return true; | |
|
|
483 | return false; | |
|
|
484 | } | |
|
|
485 | ||
|
|
480 | 486 | export function destroy(d: any) { |
|
|
481 | 487 | if (d && "destroy" in d) |
|
|
482 | 488 | d.destroy(); |
General Comments 0
You need to be logged in to leave comments.
Login now
