import { getParamNames } from './reflect'; import debug from 'debug'; import { isPromiseLike } from './promiseHelpers'; import { EventEmitter } from 'events'; var log = debug('akala:core:injector'); function ctorToFunction(this: new () => any) { var args = [null]; for (var i = 0; i < arguments.length; i++) args[i + 1] = arguments[i]; return new (Function.prototype.bind.apply(this, args)); } export type Injected = (instance?: any) => T; export type Injectable = (...args: any[]) => T; export type InjectableWithTypedThis = (this: U, ...args: any[]) => T; export type InjectableAsync = (...args: any[]) => PromiseLike; export type InjectableAsyncWithTypedThis = (this: U, ...args: any[]) => PromiseLike; export class Injector { constructor(protected parent?: Injector) { if (this.parent == null) this.parent = defaultInjector; this.register('$injector', this); } private notifier = new EventEmitter(); public setInjectables(value: { [key: string]: any }) { this.injectables = value; } public keys() { return Object.keys(this.injectables); } public merge(i: Injector) { var self = this; Object.getOwnPropertyNames(i.injectables).forEach(function (property) { if (property != '$injector') self.registerDescriptor(property, Object.getOwnPropertyDescriptor(i.injectables, property)); }) } protected notify(name: string, value?: PropertyDescriptor) { if (typeof value == 'undefined') value = Object.getOwnPropertyDescriptor(this.injectables, name); if (this.notifier.listenerCount(name) > 0) this.notifier.emit(name, value); if (this.parent) this.parent.notify(name, value); } public onResolve(name: string): PromiseLike public onResolve(name: string, handler: (value: T) => void): void public onResolve(name: string, handler?: (value: T) => void) { if (!handler) return new Promise((resolve, reject) => { this.onResolve(name, resolve); }) var value = this.resolve(name); if (value !== undefined && value !== null) { handler(value); return; } this.notifier.once(name, (prop: PropertyDescriptor) => { if (prop.get) handler(prop.get()); else handler(prop.value); }); if (this.parent) this.parent.onResolve(name, handler); } public inject(a: Injectable): Injected public inject(...a: string[]): (b: TypedPropertyDescriptor>) => void public inject(a: Injectable | string, ...b: string[]): Injected | ((b: TypedPropertyDescriptor>) => void) public inject(a: Injectable | string, ...b: string[]) { if (typeof a == 'function') return this.injectWithName(a['$inject'] || getParamNames(a), a); var self = this; return function (c: TypedPropertyDescriptor>) { if (typeof b == 'undefined') b = []; b.unshift(a); var oldf = self.injectWithName(b, c.value); c.value = function () { return oldf.apply(this, arguments); } } } public injectAsync(a: Injectable) public injectAsync(...a: string[]) public injectAsync(a: Injectable | string, ...b: string[]) { if (typeof a == 'function') return this.injectWithNameAsync(a['$inject'] || getParamNames(a), a) if (typeof b == 'undefined') b = []; b.unshift(a); var self = this; return function (c: TypedPropertyDescriptor>) { var f = c.value; c.value = function () { return self.injectWithNameAsync(b, f); } } } public injectNew(ctor: Injectable) { return this.inject(ctorToFunction.bind(ctor)); } public resolve(param: string): T { log('resolving ' + param); if (typeof (this.injectables[param]) != 'undefined') { log(`resolved ${param}`); log.extend('verbose')(`resolved ${param} to ${this.injectables[param]}`); return this.injectables[param]; } var indexOfDot = param.indexOf('.'); if (~indexOfDot) { var keys = param.split('.') return keys.reduce((result, key, i) => { if (result instanceof Proxy) return result[key]; if (result instanceof Injector) return result.resolve(key); if (isPromiseLike(result)) return result.then((result) => { return result[key] }); if (result === this.injectables && typeof (result[key]) == 'undefined' && this.parent) { return this.parent.resolve(key); } return result && result[key]; }, this.injectables); } if (this.parent) { log('trying parent injector'); return this.parent.resolve(param); } return null; } public resolveAsync(name: string): T | PromiseLike { return this.onResolve(name); log('resolving ' + name); if (typeof (this.injectables[name]) != 'undefined') { log('resolved ' + name + ' to %o', this.injectables[name]); return this.injectables[name]; } if (this.parent) { log('trying parent injector'); return this.parent.resolveAsync(name); } return this.onResolve(name); } private inspecting: boolean = false; public inspect() { if (this.inspecting) return; this.inspecting = true; console.log(this.injectables); this.inspecting = false; } private browsingForJSON = false; public toJSON() { console.log(arguments); var wasBrowsingForJSON = this.browsingForJSON; this.browsingForJSON = true; if (!wasBrowsingForJSON) return this.injectables; this.browsingForJSON = wasBrowsingForJSON; return undefined; } public injectNewWithName(toInject: string[], ctor: Function) { return this.injectWithName(toInject, ctorToFunction.bind(ctor)); } public injectWithNameAsync(toInject: string[], a: InjectableAsync | Injectable): PromiseLike { if (!toInject || toInject.length == 0) return Promise.resolve(a()); var paramNames = getParamNames(a); var self = this; var wait = false; return new Promise((resolve, reject) => { if (paramNames.length == toInject.length || paramNames.length == 0) { if (toInject.length == paramNames.length && paramNames.length == 0) resolve(a.call(null)); else { var args = []; for (var param of toInject) { args[args.length] = self.resolveAsync(param); if (isPromiseLike(args[args.length - 1])) wait = true; } if (wait) return Promise.all(args.map(function (v) { if (isPromiseLike(v)) return v; return Promise.resolve(v); })).then((args) => { resolve(a.apply(null, args)) }); else resolve(a.apply(null, args)); } } else reject('the number of arguments does not match the number of injected parameters'); }); } public injectWithName(toInject: string[], a: Injectable): Injected { var self = this; if (toInject && toInject.length > 0) { var paramNames = getParamNames(a); if (paramNames.length == toInject.length || paramNames.length == 0) { if (toInject.length == paramNames.length && paramNames.length == 0) return >a; return function (instance?: any) { var args = []; for (var param of toInject) { args[args.length] = self.resolve(param) } return a.apply(instance, args); } } } return function (instance?: any, ...otherArgs: any[]) { var args = []; var unknownArgIndex = 0; for (var param of toInject) { var resolved = self.resolve(param); if (resolved && paramNames && paramNames.indexOf(param) == args.length) args[args.length] = resolved; else if (typeof (otherArgs[unknownArgIndex]) != 'undefined') args[args.length] = otherArgs[unknownArgIndex++]; else args[args.length] = resolved; } if (otherArgs && otherArgs.length > unknownArgIndex) { args.concat(otherArgs.slice(unknownArgIndex)); } return a.apply(instance, args); } } public exec(...toInject: string[]) { var self = this; return function (f: Injectable) { return self.injectWithName(toInject, f)(this); } } private injectables = {}; public unregister(name: string) { var registration = Object.getOwnPropertyDescriptor(this.injectables, name); if (registration) delete this.injectables[name]; } public register(name: string, value: T, override?: boolean) { if (typeof (value) != 'undefined' && value !== null) this.registerDescriptor(name, { value: value, enumerable: true, configurable: true }, override); return value; } public registerFactory(name: string, value: () => T, override?: boolean) { this.register(name + 'Factory', value, override); this.registerDescriptor(name, { get: function () { return value(); }, enumerable: true, configurable: true }, override); return value; } public factory(name: string, override?: boolean) { var inj = this; return function (fact: () => T) { return inj.registerFactory(name, fact, override); } } public service(name: string, ...toInject: string[]) public service(name: string, override?: boolean, ...toInject: string[]) public service(name: string, override?: boolean | string, ...toInject: string[]) { var inj = this; var singleton; if (typeof toInject == 'undefined') toInject = []; if (typeof override == 'string') { toInject.unshift(override) override = false; } return function (fact: new (...args: any[]) => T) { inj.registerDescriptor(name, { get() { if (singleton) return singleton; return singleton = inj.injectNewWithName(toInject, fact)(); } }) } } public registerDescriptor(name: string, value: PropertyDescriptor, override?: boolean) { log('registering ' + name); if (!override && typeof (this.injectables[name]) != 'undefined') throw new Error('There is already a registered item for ' + name); if (typeof (this.injectables[name]) !== 'undefined') this.unregister(name); Object.defineProperty(this.injectables, name, value); this.notify(name, value); } } export var defaultInjector = new Injector();