@inward/context
Version:
LoopBack's container for Inversion of Control
254 lines • 9.04 kB
JavaScript
;
// Copyright IBM Corp. 2018,2020. All Rights Reserved.
// Node module: @inward/context
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
Object.defineProperty(exports, "__esModule", { value: true });
exports.asResolutionOptions = exports.ResolutionSession = void 0;
const tslib_1 = require("tslib");
const metadata_1 = require("@inward/metadata");
const debug_1 = tslib_1.__importDefault(require("debug"));
const value_promise_1 = require("./value-promise");
const debugSession = debug_1.default('inward:context:resolver:session');
const getTargetName = metadata_1.DecoratorFactory.getTargetName;
/**
* Type guard for binding elements
* @param element - A resolution element
*/
function isBinding(element) {
return element != null && element.type === 'binding';
}
/**
* Type guard for injection elements
* @param element - A resolution element
*/
function isInjection(element) {
return element != null && element.type === 'injection';
}
/**
* Object to keep states for a session to resolve bindings and their
* dependencies within a context
*/
class ResolutionSession {
constructor() {
/**
* A stack of bindings for the current resolution session. It's used to track
* the path of dependency resolution and detect circular dependencies.
*/
this.stack = [];
}
/**
* Fork the current session so that a new one with the same stack can be used
* in parallel or future resolutions, such as multiple method arguments,
* multiple properties, or a getter function
* @param session - The current session
*/
static fork(session) {
if (session === undefined)
return undefined;
const copy = new ResolutionSession();
copy.stack.push(...session.stack);
return copy;
}
/**
* Start to resolve a binding within the session
* @param binding - The current binding
* @param session - The current resolution session
*/
static enterBinding(binding, session) {
session = session !== null && session !== void 0 ? session : new ResolutionSession();
session.pushBinding(binding);
return session;
}
/**
* Run the given action with the given binding and session
* @param action - A function to do some work with the resolution session
* @param binding - The current binding
* @param session - The current resolution session
*/
static runWithBinding(action, binding, session) {
const resolutionSession = ResolutionSession.enterBinding(binding, session);
return value_promise_1.tryWithFinally(() => action(resolutionSession), () => resolutionSession.popBinding());
}
/**
* Push an injection into the session
* @param injection - The current injection
* @param session - The current resolution session
*/
static enterInjection(injection, session) {
session = session !== null && session !== void 0 ? session : new ResolutionSession();
session.pushInjection(injection);
return session;
}
/**
* Run the given action with the given injection and session
* @param action - A function to do some work with the resolution session
* @param binding - The current injection
* @param session - The current resolution session
*/
static runWithInjection(action, injection, session) {
const resolutionSession = ResolutionSession.enterInjection(injection, session);
return value_promise_1.tryWithFinally(() => action(resolutionSession), () => resolutionSession.popInjection());
}
/**
* Describe the injection for debugging purpose
* @param injection - Injection object
*/
static describeInjection(injection) {
const name = getTargetName(injection.target, injection.member, injection.methodDescriptorOrParameterIndex);
return {
targetName: name,
bindingSelector: injection.bindingSelector,
metadata: injection.metadata,
};
}
/**
* Push the injection onto the session
* @param injection - Injection The current injection
*/
pushInjection(injection) {
/* istanbul ignore if */
if (debugSession.enabled) {
debugSession('Enter injection:', ResolutionSession.describeInjection(injection));
}
this.stack.push({ type: 'injection', value: injection });
/* istanbul ignore if */
if (debugSession.enabled) {
debugSession('Resolution path:', this.getResolutionPath());
}
}
/**
* Pop the last injection
*/
popInjection() {
const top = this.stack.pop();
if (!isInjection(top)) {
throw new Error('The top element must be an injection');
}
const injection = top.value;
/* istanbul ignore if */
if (debugSession.enabled) {
debugSession('Exit injection:', ResolutionSession.describeInjection(injection));
debugSession('Resolution path:', this.getResolutionPath() || '<empty>');
}
return injection;
}
/**
* Getter for the current injection
*/
get currentInjection() {
for (let i = this.stack.length - 1; i >= 0; i--) {
const element = this.stack[i];
if (isInjection(element))
return element.value;
}
return undefined;
}
/**
* Getter for the current binding
*/
get currentBinding() {
for (let i = this.stack.length - 1; i >= 0; i--) {
const element = this.stack[i];
if (isBinding(element))
return element.value;
}
return undefined;
}
/**
* Enter the resolution of the given binding. If
* @param binding - Binding
*/
pushBinding(binding) {
/* istanbul ignore if */
if (debugSession.enabled) {
debugSession('Enter binding:', binding.toJSON());
}
if (this.stack.find(i => isBinding(i) && i.value === binding)) {
const msg = `Circular dependency detected: ` +
`${this.getResolutionPath()} --> ${binding.key}`;
debugSession(msg);
throw new Error(msg);
}
this.stack.push({ type: 'binding', value: binding });
/* istanbul ignore if */
if (debugSession.enabled) {
debugSession('Resolution path:', this.getResolutionPath());
}
}
/**
* Exit the resolution of a binding
*/
popBinding() {
const top = this.stack.pop();
if (!isBinding(top)) {
throw new Error('The top element must be a binding');
}
const binding = top.value;
/* istanbul ignore if */
if (debugSession.enabled) {
debugSession('Exit binding:', binding === null || binding === void 0 ? void 0 : binding.toJSON());
debugSession('Resolution path:', this.getResolutionPath() || '<empty>');
}
return binding;
}
/**
* Getter for bindings on the stack
*/
get bindingStack() {
return this.stack.filter(isBinding).map(e => e.value);
}
/**
* Getter for injections on the stack
*/
get injectionStack() {
return this.stack.filter(isInjection).map(e => e.value);
}
/**
* Get the binding path as `bindingA --> bindingB --> bindingC`.
*/
getBindingPath() {
return this.bindingStack.map(b => b.key).join(' --> ');
}
/**
* Get the injection path as `injectionA --> injectionB --> injectionC`.
*/
getInjectionPath() {
return this.injectionStack
.map(i => ResolutionSession.describeInjection(i).targetName)
.join(' --> ');
}
static describe(e) {
switch (e.type) {
case 'injection':
return '@' + ResolutionSession.describeInjection(e.value).targetName;
case 'binding':
return e.value.key;
}
}
/**
* Get the resolution path including bindings and injections, for example:
* `bindingA --> @ClassA[0] --> bindingB --> @ClassB.prototype.prop1
* --> bindingC`.
*/
getResolutionPath() {
return this.stack.map(i => ResolutionSession.describe(i)).join(' --> ');
}
toString() {
return this.getResolutionPath();
}
}
exports.ResolutionSession = ResolutionSession;
/**
* Normalize ResolutionOptionsOrSession to ResolutionOptions
* @param optionsOrSession - resolution options or session
*/
function asResolutionOptions(optionsOrSession) {
// backwards compatibility
if (optionsOrSession instanceof ResolutionSession) {
return { session: optionsOrSession };
}
return optionsOrSession !== null && optionsOrSession !== void 0 ? optionsOrSession : {};
}
exports.asResolutionOptions = asResolutionOptions;
//# sourceMappingURL=resolution-session.js.map