async-injection
Version:
A robust lightweight dependency injection library for TypeScript.
196 lines • 10 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ClassBasedProvider = void 0;
const bindable_provider_js_1 = require("./bindable-provider.js");
const constants_js_1 = require("./constants.js");
const decorators_js_1 = require("./decorators.js");
const state_js_1 = require("./state.js");
const utils_js_1 = require("./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).
*/
class ClassBasedProvider extends bindable_provider_js_1.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).
*/
resolveIfSingleton(asyncOnly) {
if ((!asyncOnly) || Reflect.getMetadata(constants_js_1.POSTCONSTRUCT_ASYNC_METADATA_KEY, this.maker))
return super.resolveIfSingleton(false);
return undefined;
}
/**
* Make a resolved or pending State that reflects any @PostConstruct annotations.
*/
makePostConstructState(obj) {
var _a, _b;
if (typeof obj === 'object' && (!Array.isArray(obj)) && obj.constructor) {
let maybeAsync = false;
let pcFn;
if (typeof this.successHandler === 'function') {
maybeAsync = true;
pcFn = () => {
return this.successHandler(obj, this.injector, this.id, this.maker);
};
}
else {
/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
// Check to see if there is a @PostConstruct annotation on a method of the class.
let postConstruct = Reflect.getMetadata(constants_js_1.POSTCONSTRUCT_SYNC_METADATA_KEY, obj.constructor);
if (!postConstruct) {
maybeAsync = true;
postConstruct = Reflect.getMetadata(constants_js_1.POSTCONSTRUCT_ASYNC_METADATA_KEY, obj.constructor);
}
if (postConstruct && obj.constructor.prototype[postConstruct] && typeof obj.constructor.prototype[postConstruct] === 'function')
pcFn = (_b = (_a = obj[postConstruct]).bind) === null || _b === void 0 ? void 0 : _b.call(_a, obj);
/* eslint-enable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
}
if (pcFn) {
let result;
try {
result = pcFn();
}
catch (err) {
// The post construction method threw while executing, give the errorHandler (if any) a crack at recovery.
try {
obj = this.queryErrorHandler(err, obj); // The returned obj is unlikely to be the original obj.
return state_js_1.State.MakeState(null, undefined, obj);
}
catch (e) {
// could not recover, propagate the error.
return state_js_1.State.MakeState(null, e, undefined);
}
}
// The post construction method says it will let us know when it's finished.
if (result && (result instanceof Promise || (maybeAsync && (0, utils_js_1.isPromise)(result)))) {
// Return a State that is pending (the other return statements in this method return a State which is resolved or rejected).
/* eslint-disable @typescript-eslint/no-unsafe-argument */
return state_js_1.State.MakeState(this.makePromiseForObj(result, () => obj));
}
}
}
// No PostConstruct, just return a resolved State
return state_js_1.State.MakeState(null, undefined, obj);
}
/**
* This method collects the States of all the constructor parameters for our target class.
*/
getConstructorParameterStates() {
const argTypes = Reflect.getMetadata(constants_js_1.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 = (0, decorators_js_1._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.
/* eslint-disable @typescript-eslint/no-unsafe-argument */
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 = (0, decorators_js_1._getOptionalDefaultAt)(this.maker, index);
if (md)
param = state_js_1.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((p, idx) => {
if (p.pending) {
return 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 = (0, decorators_js_1._getOptionalDefaultAt)(this.maker, idx);
if (!md)
throw err;
return md.value;
});
}
if (p.rejected)
return Promise.reject(p.rejected);
return Promise.resolve(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);
}
});
// Once the obj is resolved, then we need to check for PostConstruct and if it was async, wait for that too.
return state_js_1.State.MakeState(objPromise.then((obj) => {
const state = this.makePostConstructState(obj);
if (state.pending) {
return state.promise; // chain (aka wait some more).
}
else if (state.rejected) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return 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_js_1.State.MakeState(null, undefined, this.queryErrorHandler(err));
}
catch (e) {
// could not recover, propagate the error.
return state_js_1.State.MakeState(null, e, undefined);
}
}
}
}
}
exports.ClassBasedProvider = ClassBasedProvider;
//# sourceMappingURL=class-provider.js.map