UNPKG

12.6 kBPlain TextView Raw
1import { getParamNames } from './reflect';
2import debug from 'debug';
3import { isPromiseLike } from './promiseHelpers';
4import { EventEmitter } from 'events';
5
6var log = debug('akala:core:injector');
7
8function ctorToFunction(this: new () => any)
9{
10 var args = [null];
11 for (var i = 0; i < arguments.length; i++)
12 args[i + 1] = arguments[i];
13 return new (Function.prototype.bind.apply(this, args));
14}
15
16export type Injected<T> = (instance?: any) => T;
17export type Injectable<T> = (...args: any[]) => T;
18export type InjectableWithTypedThis<T, U> = (this: U, ...args: any[]) => T;
19export type InjectableAsync<T> = (...args: any[]) => PromiseLike<T>;
20export type InjectableAsyncWithTypedThis<T, U> = (this: U, ...args: any[]) => PromiseLike<T>;
21
22
23export class Injector
24{
25 constructor(protected parent?: Injector)
26 {
27 if (this.parent == null)
28 this.parent = defaultInjector;
29
30 this.register('$injector', this);
31 }
32
33 private notifier = new EventEmitter();
34
35 public setInjectables(value: { [key: string]: any })
36 {
37 this.injectables = value;
38 }
39
40 public keys()
41 {
42 return Object.keys(this.injectables);
43 }
44
45 public merge(i: Injector)
46 {
47 var self = this;
48 Object.getOwnPropertyNames(i.injectables).forEach(function (property)
49 {
50 if (property != '$injector')
51 self.registerDescriptor(property, Object.getOwnPropertyDescriptor(i.injectables, property));
52 })
53 }
54
55 protected notify<T>(name: string, value?: PropertyDescriptor)
56 {
57 if (typeof value == 'undefined')
58 value = Object.getOwnPropertyDescriptor(this.injectables, name);
59 if (this.notifier.listenerCount(name) > 0)
60 this.notifier.emit(name, value);
61 if (this.parent)
62 this.parent.notify(name, value);
63 }
64
65 public onResolve<T = any>(name: string): PromiseLike<T>
66 public onResolve<T = any>(name: string, handler: (value: T) => void): void
67 public onResolve<T = any>(name: string, handler?: (value: T) => void)
68 {
69 if (!handler)
70 return new Promise<T>((resolve, reject) =>
71 {
72 this.onResolve(name, resolve);
73 })
74
75 var value = this.resolve(name);
76 if (value !== undefined && value !== null)
77 {
78 handler(value);
79 return;
80 }
81
82 this.notifier.once(name, (prop: PropertyDescriptor) =>
83 {
84 if (prop.get)
85 handler(prop.get());
86 else
87 handler(prop.value);
88 });
89 if (this.parent)
90 this.parent.onResolve(name, handler);
91 }
92
93 public inject<T>(a: Injectable<T>): Injected<T>
94 public inject<T>(...a: string[]): (b: TypedPropertyDescriptor<Injectable<T>>) => void
95 public inject<T>(a: Injectable<T> | string, ...b: string[]): Injected<T> | ((b: TypedPropertyDescriptor<Injectable<T>>) => void)
96 public inject<T>(a: Injectable<T> | string, ...b: string[])
97 {
98 if (typeof a == 'function')
99 return this.injectWithName(a['$inject'] || getParamNames(a), a);
100 var self = this;
101 return function (c: TypedPropertyDescriptor<Injectable<T>>)
102 {
103 if (typeof b == 'undefined')
104 b = [];
105 b.unshift(a);
106 var oldf = self.injectWithName(b, c.value);
107 c.value = function ()
108 {
109 return oldf.apply(this, arguments);
110 }
111 }
112 }
113
114 public injectAsync<T>(a: Injectable<T>)
115 public injectAsync<T>(...a: string[])
116 public injectAsync<T>(a: Injectable<T> | string, ...b: string[])
117 {
118 if (typeof a == 'function')
119 return this.injectWithNameAsync(a['$inject'] || getParamNames(a), a)
120
121 if (typeof b == 'undefined')
122 b = [];
123 b.unshift(a);
124 var self = this;
125
126 return function <U>(c: TypedPropertyDescriptor<InjectableAsync<U>>)
127 {
128 var f = c.value;
129 c.value = function ()
130 {
131 return self.injectWithNameAsync(b, f);
132 }
133 }
134 }
135
136 public injectNew<T>(ctor: Injectable<T>)
137 {
138 return this.inject(ctorToFunction.bind(ctor));
139 }
140
141 public resolve<T = any>(param: string): T
142 {
143 log('resolving ' + param);
144
145 if (typeof (this.injectables[param]) != 'undefined')
146 {
147 log(`resolved ${param}`);
148 log.extend('verbose')(`resolved ${param} to ${this.injectables[param]}`);
149 return this.injectables[param];
150 }
151 var indexOfDot = param.indexOf('.');
152
153 if (~indexOfDot)
154 {
155 var keys = param.split('.')
156 return keys.reduce((result, key, i) =>
157 {
158 if (result instanceof Proxy)
159 return result[key];
160 if (result instanceof Injector)
161 return result.resolve(key);
162 if (isPromiseLike(result))
163 return result.then((result) => { return result[key] });
164 if (result === this.injectables && typeof (result[key]) == 'undefined' && this.parent)
165 {
166 return this.parent.resolve(key);
167 }
168 return result && result[key];
169 }, this.injectables);
170
171 }
172 if (this.parent)
173 {
174 log('trying parent injector');
175 return this.parent.resolve<T>(param);
176 }
177 return null;
178 }
179
180 public resolveAsync<T = any>(name: string): T | PromiseLike<T>
181 {
182 return this.onResolve<T>(name);
183 log('resolving ' + name);
184 if (typeof (this.injectables[name]) != 'undefined')
185 {
186 log('resolved ' + name + ' to %o', this.injectables[name]);
187 return this.injectables[name];
188 }
189 if (this.parent)
190 {
191 log('trying parent injector');
192 return this.parent.resolveAsync(name);
193 }
194 return this.onResolve<T>(name);
195 }
196
197
198
199 private inspecting: boolean = false;
200
201 public inspect()
202 {
203 if (this.inspecting)
204 return;
205 this.inspecting = true;
206 console.log(this.injectables);
207 this.inspecting = false;
208 }
209
210 private browsingForJSON = false;
211
212 public toJSON()
213 {
214 console.log(arguments);
215 var wasBrowsingForJSON = this.browsingForJSON;
216 this.browsingForJSON = true;
217 if (!wasBrowsingForJSON)
218 return this.injectables;
219 this.browsingForJSON = wasBrowsingForJSON;
220 return undefined;
221 }
222
223 public injectNewWithName(toInject: string[], ctor: Function)
224 {
225 return this.injectWithName(toInject, ctorToFunction.bind(ctor));
226 }
227
228 public injectWithNameAsync<T>(toInject: string[], a: InjectableAsync<T> | Injectable<T>): PromiseLike<T>
229 {
230 if (!toInject || toInject.length == 0)
231 return Promise.resolve<T>(a());
232 var paramNames = <string[]>getParamNames(a);
233 var self = this;
234 var wait = false;
235
236 return new Promise<T>((resolve, reject) =>
237 {
238 if (paramNames.length == toInject.length || paramNames.length == 0)
239 {
240 if (toInject.length == paramNames.length && paramNames.length == 0)
241 resolve(a.call(null));
242 else
243 {
244 var args = [];
245 for (var param of toInject)
246 {
247 args[args.length] = self.resolveAsync(param);
248 if (isPromiseLike(args[args.length - 1]))
249 wait = true;
250 }
251 if (wait)
252 return Promise.all(args.map(function (v)
253 {
254 if (isPromiseLike(v))
255 return v;
256 return Promise.resolve(v);
257 })).then((args) => { resolve(a.apply(null, args)) });
258 else
259 resolve(a.apply(null, args));
260 }
261 }
262 else
263 reject('the number of arguments does not match the number of injected parameters');
264 });
265 }
266
267
268 public injectWithName<T>(toInject: string[], a: Injectable<T>): Injected<T>
269 {
270 var self = this;
271 if (toInject && toInject.length > 0)
272 {
273 var paramNames = <string[]>getParamNames(a);
274 if (paramNames.length == toInject.length || paramNames.length == 0)
275 {
276 if (toInject.length == paramNames.length && paramNames.length == 0)
277 return <Injectable<T>>a;
278 return function (instance?: any)
279 {
280 var args = [];
281 for (var param of toInject)
282 {
283 args[args.length] = self.resolve(param)
284 }
285 return a.apply(instance, args);
286 }
287 }
288 }
289 return function (instance?: any, ...otherArgs: any[])
290 {
291 var args = [];
292 var unknownArgIndex = 0;
293 for (var param of toInject)
294 {
295 var resolved = self.resolve(param);
296 if (resolved && paramNames && paramNames.indexOf(param) == args.length)
297 args[args.length] = resolved;
298 else if (typeof (otherArgs[unknownArgIndex]) != 'undefined')
299 args[args.length] = otherArgs[unknownArgIndex++];
300 else
301 args[args.length] = resolved;
302 }
303 if (otherArgs && otherArgs.length > unknownArgIndex)
304 {
305 args.concat(otherArgs.slice(unknownArgIndex));
306 }
307 return a.apply(instance, args);
308 }
309 }
310
311 public exec<T>(...toInject: string[])
312 {
313 var self = this;
314 return function (f: Injectable<T>)
315 {
316 return self.injectWithName(toInject, f)(this);
317 }
318 }
319
320 private injectables = {};
321
322 public unregister(name: string)
323 {
324 var registration = Object.getOwnPropertyDescriptor(this.injectables, name);
325 if (registration)
326 delete this.injectables[name];
327 }
328
329 public register<T>(name: string, value: T, override?: boolean)
330 {
331 if (typeof (value) != 'undefined' && value !== null)
332 this.registerDescriptor(name, { value: value, enumerable: true, configurable: true }, override);
333 return value;
334 }
335 public registerFactory<T>(name: string, value: () => T, override?: boolean)
336 {
337 this.register(name + 'Factory', value, override);
338 this.registerDescriptor(name, {
339 get: function ()
340 {
341 return value();
342 }, enumerable: true, configurable: true
343 }, override);
344 return value;
345 }
346
347 public factory(name: string, override?: boolean)
348 {
349 var inj = this;
350 return function <T>(fact: () => T)
351 {
352 return inj.registerFactory(name, fact, override);
353 }
354 }
355
356 public service(name: string, ...toInject: string[])
357 public service(name: string, override?: boolean, ...toInject: string[])
358 public service(name: string, override?: boolean | string, ...toInject: string[])
359 {
360 var inj = this;
361 var singleton;
362
363 if (typeof toInject == 'undefined')
364 toInject = [];
365
366 if (typeof override == 'string')
367 {
368 toInject.unshift(override)
369 override = false;
370 }
371
372 return function <T>(fact: new (...args: any[]) => T)
373 {
374 inj.registerDescriptor(name, {
375 get()
376 {
377 if (singleton)
378 return singleton;
379 return singleton = inj.injectNewWithName(toInject, fact)();
380 }
381 })
382 }
383 }
384
385 public registerDescriptor(name: string, value: PropertyDescriptor, override?: boolean)
386 {
387 log('registering ' + name);
388 if (!override && typeof (this.injectables[name]) != 'undefined')
389 throw new Error('There is already a registered item for ' + name);
390 if (typeof (this.injectables[name]) !== 'undefined')
391 this.unregister(name);
392 Object.defineProperty(this.injectables, name, value);
393 this.notify(name, value);
394 }
395}
396
397export var defaultInjector = new Injector();
398
\No newline at end of file