UNPKG

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
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