feat(injector): handle in-progress async construction
This commit is contained in:
parent
4d6c7481ad
commit
e02cdfe733
|
@ -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);
|
||||
}
|
||||
|
||||
if (returnFuture) {
|
||||
return this._instantiateAsync(key, binding);
|
||||
_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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
|
||||
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){
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue