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 {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){
|
||||||
|
|
|
@ -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
|
|
||||||
});
|
});
|
||||||
}
|
}
|
Loading…
Reference in New Issue