feat(injector): handle in-progress async construction

This commit is contained in:
vsavkin 2014-10-06 11:36:22 -04:00
parent 4d6c7481ad
commit e02cdfe733
2 changed files with 175 additions and 64 deletions

View File

@ -6,7 +6,16 @@ import {Type, isPresent, isBlank} from 'facade/lang';
import {Future, FutureWrapper} from 'facade/async'; import {Future, FutureWrapper} from 'facade/async';
import {Key} from './key'; 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 { export class Injector {
constructor(bindings:List) { constructor(bindings:List) {
@ -14,16 +23,9 @@ export class Injector {
this._bindings = this._createListOfBindings(flatten); this._bindings = this._createListOfBindings(flatten);
this._instances = this._createInstances(); this._instances = this._createInstances();
this._parent = null; //TODO: vsavkin make a parameter this._parent = null; //TODO: vsavkin make a parameter
}
_createListOfBindings(flattenBindings):List { this._asyncStrategy = new _AsyncInjectorStrategy(this);
var bindings = ListWrapper.createFixedSize(Key.numberOfKeys() + 1); this._syncStrategy = new _SyncInjectorStrategy(this);
MapWrapper.forEach(flattenBindings, (keyId, v) => bindings[keyId] = v);
return bindings;
}
_createInstances():List {
return ListWrapper.createFixedSize(Key.numberOfKeys() + 1);
} }
get(token) { get(token) {
@ -42,86 +44,164 @@ export class Injector {
return this._getByKey(key, true); 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 { createChild(bindings:List):Injector {
var inj = new Injector(bindings); var inj = new Injector(bindings);
inj._parent = this; //TODO: vsavkin: change it when optional parameters are working inj._parent = this; //TODO: vsavkin: change it when optional parameters are working
return inj; 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){ _createInstances():List {
if (list.length <= index) return null; return ListWrapper.createFixedSize(Key.numberOfKeys() + 1);
return ListWrapper.get(list, index);
} }
_instantiate(key:Key, binding:Binding, returnFuture) { _getByKey(key:Key, returnFuture) {
ListWrapper.set(this._instances, key.id, _creating); var strategy = returnFuture ? this._asyncStrategy : this._syncStrategy;
if (binding.providedAsFuture && !returnFuture) { var instance = strategy.readFromCache(key);
throw new AsyncBindingError(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) { var instance = this.injector._getInstance(key);
return this._instantiateAsync(key, binding);
// skip async in-progress
if (instance === _isInProgressSync) {
throw new CyclicDependencyError(key);
} else if (isPresent(instance) && !_isInProgress(instance)) {
return instance;
} else { } else {
return this._instantiateSync(key, binding); return null;
} }
} }
_instantiateSync(key:Key, binding:Binding) { instantiate(key:Key) {
var deps; 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 { 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) { } catch (e) {
if (e instanceof ProviderError) e.addKey(key); if (e instanceof ProviderError) e.addKey(key);
throw e; throw e;
} }
}
_createInstance(key:Key, binding:Binding, deps:List) {
try { try {
var instance = binding.factory(deps); var instance = binding.factory(deps);
ListWrapper.set(this._instances, key.id, instance); this.injector._setInstance(key, instance);
return instance; return instance;
} catch (e) { } catch (e) {
throw new InstantiationError(e, key); 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)); class _AsyncInjectorStrategy {
return FutureWrapper.wait(futures). constructor(injector:Injector) {
then(binding.factory). this.injector = injector;
then(function(instance) { }
ListWrapper.set(instances, key.id, instance);
return instance 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) { function _flattenBindings(bindings:List) {
var res = {}; var res = {};
ListWrapper.forEach(bindings, function (b){ ListWrapper.forEach(bindings, function (b){

View File

@ -61,6 +61,45 @@ export function main () {
done(); 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 () { describe("get", function () {
@ -105,13 +144,5 @@ export function main () {
expect(controller.userList).toBeFuture(); 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
}); });
} }