contexify
Version:
A TypeScript library providing a powerful dependency injection container with context-based IoC capabilities, inspired by LoopBack's Context system.
252 lines • 7.8 kB
JavaScript
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
import { DecoratorFactory } from "metarize";
import createDebugger from "../utils/debug.js";
import { tryWithFinally } from "../utils/value-promise.js";
const debugSession = createDebugger("contexify:resolver:session");
const getTargetName = DecoratorFactory.getTargetName;
function isBinding(element) {
return element != null && element.type === "binding";
}
__name(isBinding, "isBinding");
function isInjection(element) {
return element != null && element.type === "injection";
}
__name(isInjection, "isInjection");
class ResolutionSession {
static {
__name(this, "ResolutionSession");
}
/**
* A stack of bindings for the current resolution session. It's used to track
* the path of dependency resolution and detect circular dependencies.
*/
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 === void 0) return void 0;
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()) {
session.pushBinding(binding);
return 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 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) {
if (debugSession.enabled) {
debugSession("Enter injection:", ResolutionSession.describeInjection(injection));
}
this.stack.push({
type: "injection",
value: injection
});
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;
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 void 0;
}
/**
* 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 void 0;
}
/**
* Enter the resolution of the given binding. If
* @param binding - Binding
*/
pushBinding(binding) {
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
});
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;
if (debugSession.enabled) {
debugSession("Exit binding:", 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();
}
}
function describe(e) {
switch (e.type) {
case "injection":
return "@" + ResolutionSession.describeInjection(e.value).targetName;
case "binding":
return e.value.key;
}
}
__name(describe, "describe");
function asResolutionOptions(optionsOrSession) {
if (optionsOrSession instanceof ResolutionSession) {
return {
session: optionsOrSession
};
}
return optionsOrSession ?? {};
}
__name(asResolutionOptions, "asResolutionOptions");
class ResolutionError extends Error {
static {
__name(this, "ResolutionError");
}
resolutionCtx;
constructor(message, resolutionCtx) {
super(ResolutionError.buildMessage(message, resolutionCtx)), this.resolutionCtx = resolutionCtx;
this.name = ResolutionError.name;
}
static buildDetails(resolutionCtx) {
return {
context: resolutionCtx.context?.name ?? "",
binding: resolutionCtx.binding?.key ?? "",
resolutionPath: resolutionCtx.options?.session?.getResolutionPath() ?? ""
};
}
/**
* 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 = ResolutionError.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(", ");
}
}
export {
ResolutionError,
ResolutionSession,
asResolutionOptions
};
//# sourceMappingURL=resolution-session.js.map