async-injection
Version:
A robust lightweight dependency injection library for TypeScript.
294 lines • 14.3 kB
JavaScript
import { BindableProvider } from './bindable-provider.js';
import { POSTCONSTRUCT_ASYNC_METADATA_KEY, POSTCONSTRUCT_SYNC_METADATA_KEY, REFLECT_PARAMS } from './constants.js';
import { _getInjectedIdAt, _getInjectedIdForMethod, _getOptionalDefaultAt, _getOptionalDefaultForMethod } from './decorators.js';
import { State } from './state.js';
import { isPromise } from './utils.js';
/**
* @inheritDoc
* This specialization invokes it's configured class constructor synchronously and then scans for (and invokes) any @PostConstruct (which may be synchronous or asynchronous).
*/
export class ClassBasedProvider extends BindableProvider {
constructor(injector, id, maker) {
super(injector, id, maker);
}
/**
* @inheritDoc
* @see the class description for this Provider.
* This method is just a singleton guard, the real work is done by provideAsStateImpl.
*/
provideAsState() {
let retVal = this.singleton;
if (!retVal) {
retVal = this.provideAsStateImpl();
}
if (this.singleton === null)
this.singleton = retVal;
return retVal;
}
/**
* @inheritDoc
* This specialization returns undefined if 'asyncOnly' is true **and** there is no asynchronous PostConstruct annotation (since class constructors can never by asynchronous),
* **unless** the @PostConstruct method has injectable parameters, which may themselves require async resolution.
*/
resolveIfSingleton(asyncOnly) {
if (!asyncOnly || Reflect.getMetadata(POSTCONSTRUCT_ASYNC_METADATA_KEY, this.maker) || this.postConstructHasParams())
return super.resolveIfSingleton(false);
return undefined;
}
/**
* Returns true if the @PostConstruct method (if any) has at least one parameter.
* Any parameter may require async resolution, so this class must participate in resolveSingletons.
*/
postConstructHasParams() {
var _a;
const pcMethod = (_a = Reflect.getMetadata(POSTCONSTRUCT_SYNC_METADATA_KEY, this.maker)) !== null && _a !== void 0 ? _a : Reflect.getMetadata(POSTCONSTRUCT_ASYNC_METADATA_KEY, this.maker);
if (!pcMethod)
return false;
const paramTypes = Reflect.getMetadata(REFLECT_PARAMS, this.maker.prototype, pcMethod);
return Array.isArray(paramTypes) && paramTypes.length > 0;
}
/**
* Make a resolved or pending State that reflects any @PostConstruct annotations and/or onSuccess handler.
* Any @PostConstruct method (with any injected parameters) runs first; the onSuccess handler runs after.
*/
makePostConstructState(obj) {
if (obj === null || typeof obj !== 'object' || Array.isArray(obj) || !obj.constructor) {
return State.MakeState(null, undefined, obj);
}
const ctor = obj.constructor;
// Look up the @PostConstruct method name (sync or async).
let pcMaybeAsync = false;
let pcMethodName = Reflect.getMetadata(POSTCONSTRUCT_SYNC_METADATA_KEY, ctor);
if (!pcMethodName) {
pcMethodName = Reflect.getMetadata(POSTCONSTRUCT_ASYNC_METADATA_KEY, ctor);
pcMaybeAsync = !!pcMethodName;
}
const pcValid = !!(pcMethodName && typeof ctor.prototype[pcMethodName] === 'function');
const hasSuccess = typeof this.successHandler === 'function';
if (!pcValid && !hasSuccess) {
return State.MakeState(null, undefined, obj);
}
// Resolve any injectable parameters declared on the @PostConstruct method.
const paramStates = pcValid ? this.getMethodParameterStates(ctor, pcMethodName) : [];
// A synchronously rejected param (with no @Optional fallback) is treated as a PostConstruct error.
const firstRejected = paramStates.find(p => !p.pending && p.rejected);
if (firstRejected) {
try {
obj = this.queryErrorHandler(firstRejected.rejected, obj);
return State.MakeState(null, undefined, obj);
}
catch (e) {
return State.MakeState(null, e, undefined);
}
}
if (paramStates.some(p => p.pending)) {
// One or more params require async resolution — wait for them, then invoke.
const paramsPromise = Promise.all(paramStates.map(async (p, idx) => {
if (p.pending) {
try {
return await p.promise;
}
catch (err) {
const md = _getOptionalDefaultForMethod(ctor.prototype, pcMethodName, idx);
if (!md)
throw err;
return md.value;
}
}
return p.fulfilled;
}));
return State.MakeState((async () => {
let args;
try {
args = await paramsPromise;
}
catch (err) {
return this.queryErrorHandler(err, obj);
}
try {
const pcResult = obj[pcMethodName](...args);
if (pcResult && (pcResult instanceof Promise || (pcMaybeAsync && isPromise(pcResult))))
await pcResult;
if (hasSuccess) {
const sResult = this.successHandler(obj, this.injector, this.id, this.maker);
if (sResult && isPromise(sResult))
await sResult;
}
return obj;
}
catch (err) {
return this.queryErrorHandler(err, obj);
}
})());
}
// All params are synchronously available (or there are no params).
const pcArgs = paramStates.map(p => p.fulfilled);
const maybeAsync = pcMaybeAsync || hasSuccess;
// Build a single function that calls PostConstruct (with resolved args) then onSuccess.
let pcFn;
if (pcValid) {
pcFn = () => {
const pcResult = obj[pcMethodName](...pcArgs);
if (pcResult && (pcResult instanceof Promise || (pcMaybeAsync && isPromise(pcResult)))) {
// PostConstruct is async — chain onSuccess after it resolves.
return hasSuccess
? pcResult.then(() => this.successHandler(obj, this.injector, this.id, this.maker))
: pcResult;
}
// PostConstruct is sync — call onSuccess immediately.
if (hasSuccess)
return this.successHandler(obj, this.injector, this.id, this.maker);
return pcResult;
};
}
else {
// No PostConstruct — just call onSuccess.
pcFn = () => this.successHandler(obj, this.injector, this.id, this.maker);
}
let result;
try {
result = pcFn();
}
catch (err) {
try {
obj = this.queryErrorHandler(err, obj);
return State.MakeState(null, undefined, obj);
}
catch (e) {
return State.MakeState(null, e, undefined);
}
}
if (result && (result instanceof Promise || (maybeAsync && isPromise(result)))) {
return State.MakeState(this.makePromiseForObj(result, () => obj));
}
return State.MakeState(null, undefined, obj);
}
/**
* Collects the resolved States for all injectable parameters of a @PostConstruct method.
* Uses the same resolution rules as constructor parameters: the reflected type (or an explicit @Inject token) is used to look up the binding, and an error is thrown if the type cannot be determined.
* Use @Optional() on a parameter to supply a fallback when no binding is found.
* Returns an empty array if the method has no parameters.
*/
getMethodParameterStates(ctor, methodName) {
const argTypes = Reflect.getMetadata(REFLECT_PARAMS, ctor.prototype, methodName);
if (!Array.isArray(argTypes) || argTypes.length === 0)
return [];
return argTypes.map((argType, index) => {
const overrideToken = _getInjectedIdForMethod(ctor.prototype, methodName, index);
const actualToken = overrideToken !== undefined ? overrideToken : argType;
if (actualToken == null) {
throw new Error(`Injection error. Unable to determine parameter ${index} type/value of ${ctor.name}.${methodName}`);
}
let param = this.injector.resolveState(actualToken);
if (!param.pending && param.rejected) {
const optionalDefault = _getOptionalDefaultForMethod(ctor.prototype, methodName, index);
if (optionalDefault)
param = State.MakeState(null, undefined, optionalDefault.value);
}
return param;
});
}
/**
* This method collects the States of all the constructor parameters for our target class.
*/
getConstructorParameterStates() {
const argTypes = Reflect.getMetadata(REFLECT_PARAMS, this.maker);
if (argTypes === undefined || !Array.isArray(argTypes)) {
return [];
}
return argTypes.map((argType, index) => {
// The reflect-metadata API fails on circular dependencies returning undefined instead.
// Additionally, it cannot return generic types (no runtime type info).
// If an Inject annotation precedes the parameter, then that is what should get injected.
const overrideToken = _getInjectedIdAt(this.maker, index);
// If there was no Inject annotation, we might still be able to determine what to inject using the 'argType' (aka Reflect design:paramtypes).
const actualToken = overrideToken === undefined ? argType : overrideToken;
if (actualToken === undefined) {
// No Inject annotation, and the type is not known.
throw new Error(`Injection error. Unable to determine parameter ${index} type/value of ${this.maker.toString()} constructor`);
}
// Ask our container to resolve the parameter.
let param = this.injector.resolveState(actualToken);
// If the parameter could not be resolved, see if there is an @Optional annotation
if ((!param.pending) && param.rejected) {
const md = _getOptionalDefaultAt(this.maker, index);
if (md)
param = State.MakeState(null, undefined, md.value);
}
return param;
});
}
/**
* Gather the needed constructor parameters, invoke the constructor, and figure out what post construction needs done.
*/
provideAsStateImpl() {
const params = this.getConstructorParameterStates();
// If any of the params are in a rejected state, we cannot construct.
const firstRejectedParam = params.find((p) => {
return (!p.pending) && p.rejected;
});
if (firstRejectedParam)
return firstRejectedParam;
if (params.some(p => p.pending)) {
// Some of the parameters needed for construction are not yet available, wait for them and then attempt construction.
// We do this by mapping each param to a Promise (pending or not), and then awaiting them all.
// This might create some unnecessary (but immediately resolved) Promise objects,
// BUT, it allows us to chain for failure *and* substitute the Optional (if one exists).
const objPromise = this.makePromiseForObj(Promise.all(params.map(async (p, idx) => {
if (p.pending) {
try {
return await p.promise;
}
catch (err) {
// This was a promised param that failed to resolve.
// If there is an Optional decorator, use that, otherwise, failure is failure.
const md = _getOptionalDefaultAt(this.maker, idx);
if (!md)
throw err;
return md.value;
}
}
return p.fulfilled;
})), (values) => {
if (values) {
// All the parameters are now available, instantiate the class.
// If this throws, it will be handled by our caller.
return Reflect.construct(this.maker, values);
}
return undefined;
});
// Once the obj is resolved, then we need to check for PostConstruct and if it was async, wait for that too.
return State.MakeState((async () => {
const obj = await objPromise;
const state = this.makePostConstructState(obj);
if (state.pending) {
return await state.promise; // chain (aka wait some more).
}
else if (state.rejected) {
throw state.rejected; // error
}
else {
return state.fulfilled; // value (aka obj).
}
})());
}
else {
// All parameters needed for construction are available, instantiate the object.
try {
const newObj = Reflect.construct(this.maker, params.map((p) => p.fulfilled));
return this.makePostConstructState(newObj);
}
catch (err) {
// There was an error, give the errorHandler (if any) a crack at recovery.
try {
return State.MakeState(null, undefined, this.queryErrorHandler(err));
}
catch (e) {
// could not recover, propagate the error.
return State.MakeState(null, e, undefined);
}
}
}
}
}
//# sourceMappingURL=class-provider.js.map