From e02cdfe733a46432b30582ddaaa8ce9a84909555 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Mon, 6 Oct 2014 11:36:22 -0400 Subject: [PATCH] feat(injector): handle in-progress async construction --- modules/di/src/injector.js | 192 ++++++++++++++++++++++--------- modules/di/test/di/async_spec.js | 47 ++++++-- 2 files changed, 175 insertions(+), 64 deletions(-) diff --git a/modules/di/src/injector.js b/modules/di/src/injector.js index 6003e1949f..9c6d6abef2 100644 --- a/modules/di/src/injector.js +++ b/modules/di/src/injector.js @@ -6,7 +6,16 @@ import {Type, isPresent, isBlank} from 'facade/lang'; import {Future, FutureWrapper} from 'facade/async'; import {Key} from './key'; -var _creating = new Object(); +class _InProgress { + constructor(future:Future) { + this.future = future; + } +} +var _isInProgressSync = new _InProgress(null); +function _isInProgress(obj) { + return obj instanceof _InProgress; +} + export class Injector { constructor(bindings:List) { @@ -14,16 +23,9 @@ export class Injector { this._bindings = this._createListOfBindings(flatten); this._instances = this._createInstances(); this._parent = null; //TODO: vsavkin make a parameter - } - _createListOfBindings(flattenBindings):List { - var bindings = ListWrapper.createFixedSize(Key.numberOfKeys() + 1); - MapWrapper.forEach(flattenBindings, (keyId, v) => bindings[keyId] = v); - return bindings; - } - - _createInstances():List { - return ListWrapper.createFixedSize(Key.numberOfKeys() + 1); + this._asyncStrategy = new _AsyncInjectorStrategy(this); + this._syncStrategy = new _SyncInjectorStrategy(this); } get(token) { @@ -42,86 +44,164 @@ export class Injector { return this._getByKey(key, true); } - _getByKey(key:Key, returnFuture) { - var keyId = key.id; - if (key.token === Injector) return this._injector(returnFuture); - - var instance = this._get(this._instances, keyId); - if (instance === _creating) { - throw new CyclicDependencyError(key); - } - if (isPresent(instance)) return instance; - - var binding = this._get(this._bindings, keyId); - if (isPresent(binding)) { - return this._instantiate(key, binding, returnFuture); - } - if (isPresent(this._parent)) { - return this._parent._getByKey(key, returnFuture); - } - - throw new NoProviderError(key); - } - createChild(bindings:List):Injector { var inj = new Injector(bindings); inj._parent = this; //TODO: vsavkin: change it when optional parameters are working return inj; } - _injector(returnFuture){ - return returnFuture ? FutureWrapper.value(this) : this; + + _createListOfBindings(flattenBindings):List { + var bindings = ListWrapper.createFixedSize(Key.numberOfKeys() + 1); + MapWrapper.forEach(flattenBindings, (keyId, v) => bindings[keyId] = v); + return bindings; } - _get(list:List, index){ - if (list.length <= index) return null; - return ListWrapper.get(list, index); + _createInstances():List { + return ListWrapper.createFixedSize(Key.numberOfKeys() + 1); } - _instantiate(key:Key, binding:Binding, returnFuture) { - ListWrapper.set(this._instances, key.id, _creating); + _getByKey(key:Key, returnFuture) { + var strategy = returnFuture ? this._asyncStrategy : this._syncStrategy; - if (binding.providedAsFuture && !returnFuture) { - throw new AsyncBindingError(key); + var instance = strategy.readFromCache(key); + if (isPresent(instance)) return instance; + + instance = strategy.instantiate(key); + if (isPresent(instance)) return instance; + + if (isPresent(this._parent)) { + return this._parent._getByKey(key, returnFuture); + } + throw new NoProviderError(key); + } + + _getInstance(key:Key){ + if (this._instances.length <= key.id) return null; + return ListWrapper.get(this._instances, key.id); + } + + _setInstance(key:Key, obj){ + ListWrapper.set(this._instances, key.id, obj); + } + + _getBinding(key:Key){ + if (this._bindings.length <= key.id) return null; + return ListWrapper.get(this._bindings, key.id); + } +} + + +class _SyncInjectorStrategy { + constructor(injector:Injector) { + this.injector = injector; + } + + readFromCache(key:Key) { + if (key.token === Injector) { + return this.injector; } - if (returnFuture) { - return this._instantiateAsync(key, binding); + var instance = this.injector._getInstance(key); + + // skip async in-progress + if (instance === _isInProgressSync) { + throw new CyclicDependencyError(key); + } else if (isPresent(instance) && !_isInProgress(instance)) { + return instance; } else { - return this._instantiateSync(key, binding); + return null; } } - _instantiateSync(key:Key, binding:Binding) { - var deps; + instantiate(key:Key) { + var binding = this.injector._getBinding(key); + if (isBlank(binding)) return null; + + if (binding.providedAsFuture) throw new AsyncBindingError(key); + + //add a marker so we can detect cyclic dependencies + this.injector._setInstance(key, _isInProgressSync); + + var deps = this._resolveDependencies(key, binding); + return this._createInstance(key, binding, deps); + } + + _resolveDependencies(key:Key, binding:Binding) { try { - deps = ListWrapper.map(binding.dependencies, d => this._getByKey(d.key, d.asFuture)); + var getDependency = d => this.injector._getByKey(d.key, d.asFuture); + return ListWrapper.map(binding.dependencies, getDependency); } catch (e) { if (e instanceof ProviderError) e.addKey(key); throw e; } + } + _createInstance(key:Key, binding:Binding, deps:List) { try { var instance = binding.factory(deps); - ListWrapper.set(this._instances, key.id, instance); + this.injector._setInstance(key, instance); return instance; } catch (e) { throw new InstantiationError(e, key); } } +} - _instantiateAsync(key:Key, binding:Binding):Future { - var instances = this._createInstances(); - var futures = ListWrapper.map(binding.dependencies, d => this._getByKey(d.key, true)); - return FutureWrapper.wait(futures). - then(binding.factory). - then(function(instance) { - ListWrapper.set(instances, key.id, instance); - return instance - }); + + +class _AsyncInjectorStrategy { + constructor(injector:Injector) { + this.injector = injector; + } + + readFromCache(key:Key) { + if (key.token === Injector) { + return FutureWrapper.value(this.injector); + } + + var instance = this.injector._getInstance(key); + if (_isInProgress(instance)) { + return instance.future; + } else if (isPresent(instance)) { + return FutureWrapper.value(instance); + } else { + return null; + } + } + + instantiate(key:Key) { + var binding = this.injector._getBinding(key); + if (isBlank(binding)) return null; + + var deps = this._resolveDependencies(key, binding); + var future = FutureWrapper.wait(deps). + then(deps => this._findOrCreate(key, binding, deps)). + then(instance => this._cacheInstance(key, instance)); + + this.injector._setInstance(key, new _InProgress(future)); + return future; + } + + _resolveDependencies(key:Key, binding:Binding):List { + var getDependency = d => this.injector._getByKey(d.key, true); + return ListWrapper.map(binding.dependencies, getDependency); + } + + _findOrCreate(key:Key, binding: Binding, deps:List) { + var instance = this.injector._getInstance(key); + if (! _isInProgress(instance)) return instance; + return binding.factory(deps); + } + + _cacheInstance(key, instance) { + this.injector._setInstance(key, instance); + return instance } } + + function _flattenBindings(bindings:List) { var res = {}; ListWrapper.forEach(bindings, function (b){ diff --git a/modules/di/test/di/async_spec.js b/modules/di/test/di/async_spec.js index 3a9d89e26d..0265656983 100644 --- a/modules/di/test/di/async_spec.js +++ b/modules/di/test/di/async_spec.js @@ -61,6 +61,45 @@ export function main () { done(); }); }); + + it("should return a future when the binding is sync from cache", function () { + var injector = new Injector([ + UserList + ]); + expect(injector.get(UserList)).toBeAnInstanceOf(UserList); + expect(injector.asyncGet(UserList)).toBeFuture(); + }); + + it("should create only one instance (async + async)", function (done) { + var injector = new Injector([ + bind(UserList).toAsyncFactory([], fetchUsers) + ]); + + var ul1 = injector.asyncGet(UserList); + var ul2 = injector.asyncGet(UserList); + + FutureWrapper.wait([ul1, ul2]).then(function (uls) { + expect(uls[0]).toBe(uls[1]); + done(); + }); + }); + + it("should create only one instance (sync + async)", function (done) { + var injector = new Injector([ + UserList + ]); + + var future = injector.asyncGet(UserList); + var ul = injector.get(UserList); + + expect(future).toBeFuture(); + expect(ul).toBeAnInstanceOf(UserList); + + future.then(function (ful) { + expect(ful).toBe(ul); + done(); + }); + }); }); describe("get", function () { @@ -105,13 +144,5 @@ export function main () { expect(controller.userList).toBeFuture(); }); }); - - - // InjectFuture toFactory([@AsyncInject(UserList)], (userListFuutre)] - // InjectFuture toFactory((@AsyncInject(UsrList) userListFuutre) => ] - - // resolve exceptions and async - // do not construct two instances of the same async dependency if there is one in progress - // cycles }); } \ No newline at end of file