| @@ -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 | toString() { |
|
33 | toString() { | |
| 34 | return "@walk"; |
|
34 | return "@walk"; | |
| 35 | } |
|
35 | } | |
|
|
36 | ||||
|
|
37 | clone() { | |||
|
|
38 | return this; | |||
|
|
39 | } | |||
| 36 | } |
|
40 | } | |
| @@ -5,12 +5,12 import { ServiceMap, Descriptor, Partial | |||||
| 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"; | |
| 8 | import { MapOf } from "../interfaces"; |
|
8 | import { MapOf, IDestroyable } from "../interfaces"; | |
| 9 | import { isDescriptor } from "./traits"; |
|
9 | import { isDescriptor } from "./traits"; | |
| 10 |
|
10 | |||
| 11 | const trace = TraceSource.get("@implab/core/di/ActivationContext"); |
|
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 | readonly _services: ContainerServiceMap<S>; |
|
14 | readonly _services: ContainerServiceMap<S>; | |
| 15 |
|
15 | |||
| 16 | readonly _cache: MapOf<any>; |
|
16 | readonly _cache: MapOf<any>; | |
| @@ -93,6 +93,9 export class Container<S extends object | |||||
| 93 | this._cleanup.push(callback); |
|
93 | this._cleanup.push(callback); | |
| 94 | } |
|
94 | } | |
| 95 |
|
95 | |||
|
|
96 | destroy() { | |||
|
|
97 | return this.dispose(); | |||
|
|
98 | } | |||
| 96 | dispose() { |
|
99 | dispose() { | |
| 97 | if (this._disposed) |
|
100 | if (this._disposed) | |
| 98 | return; |
|
101 | return; | |
| @@ -14,8 +14,5 export class FactoryServiceDescriptor<S | |||||
| 14 | // bind to null |
|
14 | // bind to null | |
| 15 | this._factory = (...args) => opts.factory.apply(null, args as any); |
|
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 | } |
|
|||
| 20 | } |
|
17 | } | |
| 21 | } |
|
18 | } | |
| @@ -1,9 +1,10 | |||||
| 1 | import { ActivationContext } from "./ActivationContext"; |
|
1 | import { ActivationContext } from "./ActivationContext"; | |
| 2 | import { Descriptor, ServiceMap, PartialServiceMap, ActivationType } from "./interfaces"; |
|
2 | import { Descriptor, ServiceMap, PartialServiceMap, ActivationType, ILifetimeManager } from "./interfaces"; | |
| 3 | import { Container } from "./Container"; |
|
|||
| 4 | import { argumentNotNull, isPrimitive, keys, isNull } from "../safe"; |
|
3 | import { argumentNotNull, isPrimitive, keys, isNull } from "../safe"; | |
| 5 | import { TraceSource } from "../log/TraceSource"; |
|
4 | import { TraceSource } from "../log/TraceSource"; | |
| 6 | import { isDescriptor } from "./traits"; |
|
5 | import { isDescriptor } from "./traits"; | |
|
|
6 | import { LifetimeManager } from "./LifetimeManager"; | |||
|
|
7 | import { MatchingMemberKeys } from "../interfaces"; | |||
| 7 |
|
8 | |||
| 8 | let cacheId = 0; |
|
9 | let cacheId = 0; | |
| 9 |
|
10 | |||
| @@ -21,15 +22,14 function injectMethod<T, M extends keyof | |||||
| 21 | return m.call(target, _parse(args, context, "." + method)); |
|
22 | return m.call(target, _parse(args, context, "." + method)); | |
| 22 | } |
|
23 | } | |
| 23 |
|
24 | |||
| 24 |
function makeClenupCallback<T>( |
|
25 | function makeClenupCallback<T>(method: Cleaner<T>) { | |
| 25 | function makeClenupCallback(target: any, method: any) { |
|
26 | if (typeof (method) === "function") { | |
| 26 | if (typeof (method) === "string") { |
|
27 | return (target: T) => { | |
| 27 | return () => { |
|
28 | method(target); | |
| 28 | target[method](); |
|
|||
| 29 | }; |
|
29 | }; | |
| 30 | } else { |
|
30 | } else { | |
| 31 | return () => { |
|
31 | return (target: T) => { | |
| 32 |
|
|
32 | (target[method] as any)(); | |
| 33 | }; |
|
33 | }; | |
| 34 | } |
|
34 | } | |
| 35 | } |
|
35 | } | |
| @@ -53,16 +53,14 function _parse(value: any, context: Act | |||||
| 53 | return t; |
|
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 | export type InjectionSpec<T> = { |
|
58 | export type InjectionSpec<T> = { | |
| 59 | [m in keyof T]?: any; |
|
59 | [m in keyof T]?: any; | |
| 60 | }; |
|
60 | }; | |
| 61 |
|
61 | |||
| 62 | export interface ServiceDescriptorParams<S extends object, T, P extends any[]> { |
|
62 | export interface ServiceDescriptorParams<S extends object, T, P extends any[]> { | |
| 63 | activation?: ActivationType; |
|
63 | lifetime: ILifetimeManager; | |
| 64 |
|
||||
| 65 | owner: Container<S>; |
|
|||
| 66 |
|
64 | |||
| 67 | params?: P; |
|
65 | params?: P; | |
| 68 |
|
66 | |||
| @@ -74,32 +72,23 export interface ServiceDescriptorParams | |||||
| 74 | } |
|
72 | } | |
| 75 |
|
73 | |||
| 76 | export class ServiceDescriptor<S extends object, T, P extends any[]> implements Descriptor<S, T> { |
|
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 | _services: ServiceMap<S>; |
|
75 | _services: ServiceMap<S>; | |
| 84 |
|
76 | |||
| 85 | _params: P | undefined; |
|
77 | _params: P | undefined; | |
| 86 |
|
78 | |||
| 87 | _inject: InjectionSpec<T>[]; |
|
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 | constructor(opts: ServiceDescriptorParams<S, T, P>) { |
|
87 | constructor(opts: ServiceDescriptorParams<S, T, P>) { | |
| 96 | argumentNotNull(opts, "opts"); |
|
88 | argumentNotNull(opts, "opts"); | |
| 97 | argumentNotNull(opts.owner, "owner"); |
|
|||
| 98 |
|
89 | |||
| 99 | this._owner = opts.owner; |
|
90 | if (opts.lifetime) | |
| 100 |
|
91 | this._lifetime = opts.lifetime; | ||
| 101 | if (!isNull(opts.activation)) |
|
|||
| 102 | this._activationType = opts.activation; |
|
|||
| 103 |
|
92 | |||
| 104 | if (!isNull(opts.params)) |
|
93 | if (!isNull(opts.params)) | |
| 105 | this._params = opts.params; |
|
94 | this._params = opts.params; | |
| @@ -113,96 +102,22 export class ServiceDescriptor<S extends | |||||
| 113 | throw new Error( |
|
102 | throw new Error( | |
| 114 | "The cleanup parameter must be either a function or a function name"); |
|
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 | activate(context: ActivationContext<S>) { |
|
109 | activate(context: ActivationContext<S>) { | |
| 121 | // if we have a local service records, register them first |
|
110 | const lifetime = this._lifetime.initialize(this._cacheId, context); | |
| 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); |
|
|||
| 158 |
|
111 | |||
| 159 | // the instance is bound to the container |
|
112 | if (lifetime.has()) { | |
| 160 | if (this._cleanup) |
|
113 | return lifetime.get(); | |
| 161 | this._owner.onDispose( |
|
114 | } else { | |
| 162 | makeClenupCallback(instance, this._cleanup)); |
|
115 | const instance = this._create(context); | |
| 163 |
|
116 | lifetime.store(this._cacheId, this._cleanup); | ||
| 164 |
|
|
117 | return instance; | |
| 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); |
|
|||
| 195 | } |
|
118 | } | |
| 196 | } |
|
119 | } | |
| 197 |
|
120 | |||
| 198 | isInstanceCreated() { |
|
|||
| 199 | return this._hasInstance; |
|
|||
| 200 | } |
|
|||
| 201 |
|
||||
| 202 | getInstance() { |
|
|||
| 203 | return this._instance; |
|
|||
| 204 | } |
|
|||
| 205 |
|
||||
| 206 | _factory(...params: any[]): T { |
|
121 | _factory(...params: any[]): T { | |
| 207 | throw Error("Not implemented"); |
|
122 | throw Error("Not implemented"); | |
| 208 | } |
|
123 | } | |
| @@ -210,10 +125,6 export class ServiceDescriptor<S extends | |||||
| 210 | _create(context: ActivationContext<S>) { |
|
125 | _create(context: ActivationContext<S>) { | |
| 211 | trace.debug(`constructing ${context._name}`); |
|
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 | if (this._services) { |
|
128 | if (this._services) { | |
| 218 | keys(this._services).forEach(p => context.register(p, this._services[p])); |
|
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 | return instance; |
|
148 | return instance; | |
| 238 | } |
|
149 | } | |
|
|
150 | ||||
|
|
151 | clone() { | |||
|
|
152 | return Object.create(this); | |||
|
|
153 | } | |||
|
|
154 | ||||
| 239 | } |
|
155 | } | |
| @@ -46,8 +46,6 export interface RegistrationVisitor<S e | |||||
| 46 |
|
46 | |||
| 47 | visitFactoryRegistration(): void; |
|
47 | visitFactoryRegistration(): void; | |
| 48 |
|
48 | |||
| 49 | visitBuilder<T>(builder: (t: ServiceRecordBuilder<T, S>) => void): void; |
|
|||
| 50 |
|
||||
| 51 | } |
|
49 | } | |
| 52 |
|
50 | |||
| 53 | export interface RegistrationBuilder<T, S extends object> { |
|
51 | export interface RegistrationBuilder<T, S extends object> { | |
| @@ -1,7 +1,10 | |||||
| 1 | import { ActivationContext } from "./ActivationContext"; |
|
1 | import { ActivationContext } from "./ActivationContext"; | |
|
|
2 | import { IDestroyable } from "../interfaces"; | |||
| 2 |
|
3 | |||
| 3 | export interface Descriptor<S extends object = any, T = any> { |
|
4 | export interface Descriptor<S extends object = any, T = any> { | |
| 4 | activate(context: ActivationContext<S>): T; |
|
5 | activate(context: ActivationContext<S>): T; | |
|
|
6 | ||||
|
|
7 | clone(): this; | |||
| 5 | } |
|
8 | } | |
| 6 |
|
9 | |||
| 7 | export type ServiceMap<S extends object> = { |
|
10 | export type ServiceMap<S extends object> = { | |
| @@ -36,3 +39,13 export type ContainerRegistered<S extend | |||||
| 36 | Exclude<S, ContainerProvided<S>>; |
|
39 | Exclude<S, ContainerProvided<S>>; | |
| 37 |
|
40 | |||
| 38 | export type ActivationType = "singleton" | "container" | "hierarchy" | "context" | "call"; |
|
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 | export type Predicate<T = any> = (x: T) => boolean; |
|
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 | export interface MapOf<T> { |
|
20 | export interface MapOf<T> { | |
| 13 | [key: string]: T; |
|
21 | [key: string]: T; | |
| 14 | } |
|
22 | } | |
| @@ -1,4 +1,4 | |||||
| 1 | import { ICancellable, Constructor } from "./interfaces"; |
|
1 | import { ICancellable, Constructor, IDestroyable } from "./interfaces"; | |
| 2 | import { Cancellation } from "./Cancellation"; |
|
2 | import { Cancellation } from "./Cancellation"; | |
| 3 |
|
3 | |||
| 4 | let _nextOid = 0; |
|
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 | export function destroy(d: any) { |
|
486 | export function destroy(d: any) { | |
| 481 | if (d && "destroy" in d) |
|
487 | if (d && "destroy" in d) | |
| 482 | d.destroy(); |
|
488 | d.destroy(); | |
General Comments 0
You need to be logged in to leave comments.
Login now
