##// END OF EJS Templates
Refactoring, working on services lifetime
cin -
r131:674555892e14 ioc ts support
parent child
Show More
@@ -1,138 +1,123
1 1 import { TraceSource } from "../log/TraceSource";
2 2 import { argumentNotNull, argumentNotEmptyString } from "../safe";
3 import { Descriptor, ContainerProvided, ContainerServiceMap, ContainerKeys, ContainerResolve } from "./interfaces";
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<S extends object> {
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?: string, cache?: object, visited?: MapOf<any>) {
33 argumentNotNull(container, "container");
34 argumentNotNull(services, "services");
35
36 this._name = name || "<unnamed>";
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(name, d.toString());
100 const v = d.activate(this);
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 name,
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 context.activate(d, name.toString());
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, context: ActivationContext<any>): ILifetime {
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 false;
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, ActivationType, ILifetimeManager } from "./interfaces";
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