@loopback/context
Version:
Facilities to manage artifacts and their dependencies in your Node.js applications. The module exposes TypeScript/JavaScript APIs and decorators to register artifacts, declare dependencies, and resolve artifacts by keys. It also serves as an IoC container
274 lines • 9.99 kB
JavaScript
// Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved.
// Node module: @loopback/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.ResolutionError = exports.asResolutionOptions = exports.ResolutionSession = void 0;
const tslib_1 = require("tslib");
const metadata_1 = require("@loopback/metadata");
const debug_1 = tslib_1.__importDefault(require("debug"));
const value_promise_1 = require("./value-promise");
const debugSession = (0, debug_1.default)('loopback: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;
}
/**
* 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 = new ResolutionSession()) {
// Start to resolve a binding within the session
session.pushBinding(binding);
return (0, value_promise_1.tryWithFinally)(() => action(session), () => session.popBinding());
}
/**
* 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 = new ResolutionSession()) {
session.pushInjection(injection);
return (0, value_promise_1.tryWithFinally)(() => action(session), () => session.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.stack.filter(isBinding).map(describe).join(' --> ');
}
/**
* Get the injection path as `injectionA --> injectionB --> injectionC`.
*/
getInjectionPath() {
return this.injectionStack
.map(i => ResolutionSession.describeInjection(i).targetName)
.join(' --> ');
}
/**
* Get the resolution path including bindings and injections, for example:
* `bindingA --> @ClassA[0] --> bindingB --> @ClassB.prototype.prop1
* --> bindingC`.
*/
getResolutionPath() {
return this.stack.map(describe).join(' --> ');
}
toString() {
return this.getResolutionPath();
}
}
exports.ResolutionSession = ResolutionSession;
function describe(e) {
switch (e.type) {
case 'injection':
return '@' + ResolutionSession.describeInjection(e.value).targetName;
case 'binding':
return e.value.key;
}
}
/**
* 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;
/**
* Error for context binding resolutions and dependency injections
*/
class ResolutionError extends Error {
constructor(message, resolutionCtx) {
super(ResolutionError.buildMessage(message, resolutionCtx));
this.resolutionCtx = resolutionCtx;
this.name = ResolutionError.name;
}
static buildDetails(resolutionCtx) {
var _a, _b, _c, _d, _e, _f, _g;
return {
context: (_b = (_a = resolutionCtx.context) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : '',
binding: (_d = (_c = resolutionCtx.binding) === null || _c === void 0 ? void 0 : _c.key) !== null && _d !== void 0 ? _d : '',
resolutionPath: (_g = (_f = (_e = resolutionCtx.options) === null || _e === void 0 ? void 0 : _e.session) === null || _f === void 0 ? void 0 : _f.getResolutionPath()) !== null && _g !== void 0 ? _g : '',
};
}
/**
* Build the error message for the resolution to include more contextual data
* @param reason - Cause of the error
* @param resolutionCtx - Resolution context
*/
static buildMessage(reason, resolutionCtx) {
const info = this.describeResolutionContext(resolutionCtx);
const message = `${reason} (${info})`;
return message;
}
static describeResolutionContext(resolutionCtx) {
const details = ResolutionError.buildDetails(resolutionCtx);
const items = [];
for (const [name, val] of Object.entries(details)) {
if (val !== '') {
items.push(`${name}: ${val}`);
}
}
return items.join(', ');
}
}
exports.ResolutionError = ResolutionError;
//# sourceMappingURL=resolution-session.js.map
;