async-injection
Version:
A robust lightweight dependency injection library for TypeScript.
294 lines • 13.3 kB
JavaScript
import { AsyncFactoryBasedProvider } from './async-factory-provider.js';
import { BindableProvider } from './bindable-provider.js';
import { ClassBasedProvider } from './class-provider.js';
import { ConstantProvider } from './constant-provider.js';
import { INJECTABLE_METADATA_KEY } from './constants.js';
import { State } from './state.js';
import { FactoryBasedProvider } from './sync-factory-provider.js';
import { isPromise } from './utils.js';
/**
* Helper class to ensure we can distinguish between Error instances legitimately returned from Providers, and Errors thrown by Providers.
*
* @see resolveSingletons.
*/
class ReasonWrapper {
constructor(reason) {
this.reason = reason;
}
}
/**
* Injector (aka Container) to handle (a)synchronous dependency management.
*/
export class Container {
/**
* Create a new Container, with an optional parent Injector which will be searched if any given InjectableId is not bound within this Container.
*/
constructor(parent) {
this.parent = parent;
this.providers = new Map();
}
/**
* @inheritDoc
*/
isIdKnown(id, ascending) {
if (this.providers.has(id))
return true;
if (ascending && this.parent)
return this.parent.isIdKnown(id, true);
return false;
}
/**
* @inheritDoc
*/
get(id) {
const provider = this.providers.get(id);
if (!provider) {
if (this.parent)
return this.parent.get(id);
throw new Error('Symbol not bound: ' + id.toString());
}
const state = provider.provideAsState();
if (state.pending)
throw new Error('Synchronous request on unresolved asynchronous dependency tree: ' + id.toString());
if (state.rejected)
throw state.rejected;
return state.fulfilled;
}
/**
* @inheritDoc
*/
resolve(id) {
const state = this.resolveState(id);
if (isPromise(state.promise)) {
return state.promise;
}
if (state.rejected) {
return Promise.reject(state.rejected);
}
return Promise.resolve(state.fulfilled);
}
/**
* Removes a binding from this Container (and optionally its ancestor chain).
* Most useful in unit tests to replace or clear bindings between test cases.
* **Caution:** Removing a binding after initialization may have unexpected consequences if other parts of the application still hold or expect to obtain an instance of the removed id.
*
* @param id The id of the binding to remove.
* @param ascending If true, the binding is also removed from every Container in the parent chain.
* @param releaseIfSingleton If true, Provider.releaseIfSingleton is called before removal.
*/
removeBinding(id, ascending, releaseIfSingleton) {
if (releaseIfSingleton) {
const p = this.providers.get(id);
if (p)
p.releaseIfSingleton();
}
this.providers.delete(id);
if (ascending && this.parent instanceof Container) {
this.parent.removeBinding(id, true, releaseIfSingleton);
}
}
/**
* Alias for {@link isIdKnown}. Familiar to users migrating from TypeDI.
*/
has(id, ascending) {
return this.isIdKnown(id, ascending);
}
/**
* Alias for {@link removeBinding}. Familiar to users migrating from InversifyJS.
*/
unbind(id, ascending, releaseIfSingleton) {
this.removeBinding(id, ascending, releaseIfSingleton);
}
/**
* Creates a new child Container that inherits unbound ids from this Container.
* Familiar to users migrating from TSyringe.
*/
createChildContainer() {
return new Container(this);
}
/**
* Descriptor-based binding dispatching to the appropriate bindXXX method.
* Familiar to users migrating from TSyringe.
* Returns a {@link BindAs} chain for class and factory bindings; returns undefined for value bindings (which are always singletons).
*/
register(id, descriptor) {
if ('useClass' in descriptor)
return this.bindClass(id, descriptor.useClass);
if ('useValue' in descriptor) {
this.bindConstant(id, descriptor.useValue);
return undefined;
}
if ('useFactory' in descriptor)
return this.bindFactory(id, descriptor.useFactory);
// useAsyncFactory
return this.bindAsyncFactory(id, descriptor.useAsyncFactory);
}
/**
* Binds a class as a singleton in one step.
* Shorthand for {@link bindClass}(...).{@link BindAs.asSingleton asSingleton}().
* Familiar to users migrating from TSyringe.
*/
registerSingleton(id, constructor) {
this.bindClass(id, constructor).asSingleton();
}
/**
* @inheritDoc
*/
bindConstant(id, value) {
this.providers.set(id, new ConstantProvider(value));
return value;
}
bindClass(id, constructor) {
if (typeof constructor === 'undefined') {
constructor = id;
}
if (!Reflect.getMetadata(INJECTABLE_METADATA_KEY, constructor)) {
throw new Error('Class not decorated with @Injectable [' + constructor.toString() + ']');
}
const provider = new ClassBasedProvider(this, id, constructor);
this.providers.set(id, provider);
return provider.makeBindAs();
}
/**
* @inheritDoc
*/
bindFactory(id, factory) {
const provider = new FactoryBasedProvider(this, id, factory);
this.providers.set(id, provider);
return provider.makeBindAs();
}
/**
* @inheritDoc
*/
bindAsyncFactory(id, factory) {
const provider = new AsyncFactoryBasedProvider(this, id, factory);
this.providers.set(id, provider);
return provider.makeBindAs();
}
/**
* @inheritDoc
*/
resolveSingletons(asyncOnly, parentRecursion) {
const makePromiseToResolve = () => {
return new Promise((resolve, reject) => {
const pending = new Map();
// Ask each provider to resolve itself *IF* it is a singleton.
this.providers.forEach((value, key) => {
// If the provider is a singleton *and* if resolution is being handled asynchronously, the provider will return a completion promise.
const p = value.resolveIfSingleton(asyncOnly !== null && asyncOnly !== void 0 ? asyncOnly : false);
if (p !== null && typeof p !== 'undefined')
pending.set(key, p);
});
// The contract for this method is that it behaves somewhat like Promise.allSettled (e.g. won't complete until all pending Singletons have settled).
// Further the contract states that if any of the asynchronous Singletons rejected, that we will also return a rejected Promise, and that the rejection reason will be a Map of the InjectableId's that did not resolve, and the Error they emitted.
const pp = Array.from(pending.values());
const keys = Array.from(pending.keys());
// Mapping the catch is an alternate version of Promise.allSettled (e.g. keeps Promise.all from short-circuiting).
Promise.all(pp
.map(p => p.catch(e => new ReasonWrapper(e))))
.then((results) => {
const rejects = new Map();
// Check the results. Since we don't export ReasonWrapper, it is safe to assume that an instance of that was produced by our map => catch code above, so it's a rejected Singleton error.
results.forEach((result, idx) => {
if (result instanceof ReasonWrapper) {
rejects.set(keys[idx], result.reason);
}
});
// If we had rejections, notify our caller what they were.
if (rejects.size > 0)
reject(rejects);
else
resolve(); // All good.
});
});
};
if (parentRecursion && this.parent instanceof Container) {
return this.parent.resolveSingletons(asyncOnly, parentRecursion).then(() => {
return makePromiseToResolve().then(() => this);
});
}
return makePromiseToResolve().then(() => this);
}
/**
* As implied by the name prefix, this is a factored out method invoked only by the 'resolve' method.
* It makes searching our parent (if it exists) easier (and quicker) IF our parent is a fellow instance of Container.
*/
resolveState(id) {
const provider = this.providers.get(id);
if (!provider) {
if (this.parent) {
if (this.parent instanceof Container) {
return this.parent.resolveState(id);
}
// This code (below) will only ever execute if the creator of this container passes in their own implementation of an Injector.
/* istanbul ignore next */
try {
return State.MakeState(this.parent.resolve(id), undefined, undefined);
}
catch (err) {
return State.MakeState(null, err);
}
}
return State.MakeState(null, new Error('Symbol not bound: ' + id.toString()));
}
return provider.provideAsState();
}
/**
* Convenience method to assist in releasing non-garbage-collectable resources that Singletons in this Container may have allocated.
* It will walk through all registered Providers (of this Container only), and invoke their Provider.releaseIfSingleton method.
* This method is not part of the Injector interface, because you normally only create (and release) from Containers.
* NOTE:
* This *only* releases active/pending Singleton's that have already been created by this Container.
* The most likely use of this method would be when you have created a new child Container for a limited-duration transaction, and you want to easily clean up temporary resources.
* For example, your service object may need to know when it should unsubscribe from an RxJs stream (failure to do so can result in your Singleton not being garbage collected at the end of a transaction).
* In theory, you could handle all unsubscription and cleanup yourself, but the @Release decorator and this method are meant to simply make that easier.
*/
releaseSingletons() {
this.providers.forEach((value) => {
value.releaseIfSingleton();
});
}
/**
* Releases a Singleton instance if it exists.
* However, it does **not** remove the binding, so if you call get or resolve (directly or indirectly) on the id, a new Singleton will be created.
* If not a singleton, this method returns undefined.
* If the singleton has been resolved, it is returned, otherwise null is returned.
* If the singleton is pending resolution, a Promise for the singleton (or for null) is returned.
* Note that if a singleton is returned, its Release method will already have been invoked.
*/
releaseSingleton(id) {
const provider = this.providers.get(id);
if (provider)
return provider.releaseIfSingleton();
return undefined;
}
/**
* Make a copy of this Container.
* This is an experimental feature!
* I have not thought through all the dark corners, so use at your own peril!
* Here are some notes:
* The injector parameter for SyncFactory and AsyncFactory callbacks will be the Container invoking the factory.
* So a factory that uses a parent closure instead of the supplied injector may get unexpected results.
* The injector parameter for OnSuccess and OnError callbacks will be the Container performing the resolution.
* Singletons are cloned at their *existing* state..
* If resolved in "this" container, they will not be re-resolved for the clone.
* If released by the clone, they will be considered released by "this" container.
* If a singleton is currently being asynchronously constructed any callbacks will reference "this" Container, however both Containers should have no problem awaiting resolution.
* If a singleton is not resolved when the container is cloned, then if both containers resolve, you will create *two* "singletons".
* The way to avoid this last effect is to call resolveSingletons first
*/
clone(clazz) {
if (!clazz)
clazz = Container;
const retVal = new clazz(this.parent);
this.providers.forEach((v, k) => {
if (v instanceof BindableProvider) {
v = Object.assign(Object.create(Object.getPrototypeOf(v)), v);
v.injector = retVal;
}
retVal.providers.set(k, v);
});
return retVal;
}
}
//# sourceMappingURL=container.js.map