UNPKG

contexify

Version:

A TypeScript library providing a powerful dependency injection container with context-based IoC capabilities, inspired by LoopBack's Context system.

212 lines 8.46 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); import assert from "assert"; import { ClassDecoratorFactory, DecoratorFactory, MetadataAccessor, MetadataInspector, MethodDecoratorFactory } from "metarize"; import { injectable } from "../binding/binding-decorator.js"; import { createBindingFromClass, isProviderClass } from "../binding/binding-inspector.js"; import { BindingKey } from "../binding/binding-key.js"; import { sortBindingsByPhase } from "../binding/binding-sorter.js"; import createDebugger from "../utils/debug.js"; import { ContextBindings, ContextTags, GLOBAL_INTERCEPTOR_NAMESPACE, LOCAL_INTERCEPTOR_NAMESPACE } from "../utils/keys.js"; import { tryWithFinally } from "../utils/value-promise.js"; import { invokeInterceptors } from "./interceptor-chain.js"; import { InvocationContext } from "./invocation.js"; const debug = createDebugger("contexify:interceptor"); class InterceptedInvocationContext extends InvocationContext { static { __name(this, "InterceptedInvocationContext"); } /** * Discover all binding keys for global interceptors (tagged by * ContextTags.GLOBAL_INTERCEPTOR) */ getGlobalInterceptorBindingKeys() { let bindings = this.findByTag(ContextTags.GLOBAL_INTERCEPTOR); bindings = bindings.filter((binding) => ( // Only include interceptors that match the source type of the invocation this.applicableTo(binding) )); this.sortGlobalInterceptorBindings(bindings); const keys = bindings.map((b) => b.key); if (debug.enabled) { debug("Global interceptor binding keys:", keys); } return keys; } /** * Check if the binding for a global interceptor matches the source type * of the invocation * @param binding - Binding */ applicableTo(binding) { const sourceType = this.source?.type; if (sourceType == null) return true; const allowedSource = binding.tagMap[ContextTags.GLOBAL_INTERCEPTOR_SOURCE]; return ( // No tag, always apply allowedSource == null || // source matched allowedSource === sourceType || // source included in the string[] Array.isArray(allowedSource) && allowedSource.includes(sourceType) ); } /** * Sort global interceptor bindings by `globalInterceptorGroup` tags * @param bindings - An array of global interceptor bindings */ sortGlobalInterceptorBindings(bindings) { const orderedGroups = this.getSync(ContextBindings.GLOBAL_INTERCEPTOR_ORDERED_GROUPS, { optional: true }) ?? []; return sortBindingsByPhase(bindings, ContextTags.GLOBAL_INTERCEPTOR_GROUP, orderedGroups); } /** * Load all interceptors for the given invocation context. It adds * interceptors from possibly three sources: * 1. method level `@intercept` * 2. class level `@intercept` * 3. global interceptors discovered in the context */ loadInterceptors() { let interceptors = MetadataInspector.getMethodMetadata(INTERCEPT_METHOD_KEY, this.target, this.methodName) ?? []; const targetClass = typeof this.target === "function" ? this.target : this.target.constructor; const classInterceptors = MetadataInspector.getClassMetadata(INTERCEPT_CLASS_KEY, targetClass) ?? []; interceptors = mergeInterceptors(classInterceptors, interceptors); const globalInterceptors = this.getGlobalInterceptorBindingKeys(); interceptors = mergeInterceptors(globalInterceptors, interceptors); if (debug.enabled) { debug("Interceptors for %s", this.targetName, interceptors); } return interceptors; } } function asGlobalInterceptor(group) { return (binding) => { binding.tag(ContextTags.GLOBAL_INTERCEPTOR).tag({ [ContextTags.NAMESPACE]: GLOBAL_INTERCEPTOR_NAMESPACE }); if (group) binding.tag({ [ContextTags.GLOBAL_INTERCEPTOR_GROUP]: group }); }; } __name(asGlobalInterceptor, "asGlobalInterceptor"); function globalInterceptor(group, ...specs) { return injectable(asGlobalInterceptor(group), ...specs); } __name(globalInterceptor, "globalInterceptor"); const INTERCEPT_METHOD_KEY = MetadataAccessor.create("intercept:method"); function mergeInterceptors(interceptorsFromSpec, existingInterceptors) { const interceptorsToApply = new Set(interceptorsFromSpec); const appliedInterceptors = new Set(existingInterceptors); for (const i of interceptorsToApply) { if (appliedInterceptors.has(i)) { interceptorsToApply.delete(i); } } for (const i of appliedInterceptors) { interceptorsToApply.add(i); } return Array.from(interceptorsToApply); } __name(mergeInterceptors, "mergeInterceptors"); const INTERCEPT_CLASS_KEY = MetadataAccessor.create("intercept:class"); let InterceptClassDecoratorFactory = class InterceptClassDecoratorFactory2 extends ClassDecoratorFactory { static { __name(this, "InterceptClassDecoratorFactory"); } mergeWithOwn(ownMetadata, _target) { ownMetadata = ownMetadata || []; return mergeInterceptors(this.spec, ownMetadata); } }; let InterceptMethodDecoratorFactory = class InterceptMethodDecoratorFactory2 extends MethodDecoratorFactory { static { __name(this, "InterceptMethodDecoratorFactory"); } mergeWithOwn(ownMetadata, _target, methodName, _methodDescriptor) { ownMetadata = ownMetadata || {}; const interceptors = ownMetadata[methodName] || []; ownMetadata[methodName] = mergeInterceptors(this.spec, interceptors); return ownMetadata; } }; function intercept(...interceptorOrKeys) { return /* @__PURE__ */ __name(function interceptDecoratorForClassOrMethod(target, method, methodDescriptor) { if (method && methodDescriptor) { return InterceptMethodDecoratorFactory.createDecorator(INTERCEPT_METHOD_KEY, interceptorOrKeys, { decoratorName: "@intercept" })(target, method, methodDescriptor); } if (typeof target === "function" && !method && !methodDescriptor) { return InterceptClassDecoratorFactory.createDecorator(INTERCEPT_CLASS_KEY, interceptorOrKeys, { decoratorName: "@intercept" })(target); } throw new Error("@intercept cannot be used on a property: " + DecoratorFactory.getTargetName(target, method, methodDescriptor)); }, "interceptDecoratorForClassOrMethod"); } __name(intercept, "intercept"); function invokeMethodWithInterceptors(context, target, methodName, args, options = {}) { assert(!options.skipInterceptors, "skipInterceptors is not allowed"); const invocationCtx = new InterceptedInvocationContext(context, target, methodName, args, options.source); invocationCtx.assertMethodExists(); return tryWithFinally(() => { const interceptors = invocationCtx.loadInterceptors(); const targetMethodInvoker = /* @__PURE__ */ __name(() => invocationCtx.invokeTargetMethod(options), "targetMethodInvoker"); interceptors.push(targetMethodInvoker); return invokeInterceptors(invocationCtx, interceptors); }, () => invocationCtx.close()); } __name(invokeMethodWithInterceptors, "invokeMethodWithInterceptors"); function registerInterceptor(ctx, interceptor, options = {}) { let { global } = options; const { group, source } = options; if (group != null || source != null) { global = global !== false; } const namespace = options.namespace ?? options.defaultNamespace ?? global ? GLOBAL_INTERCEPTOR_NAMESPACE : LOCAL_INTERCEPTOR_NAMESPACE; let binding; if (isProviderClass(interceptor)) { binding = createBindingFromClass(interceptor, { defaultNamespace: namespace, ...options }); if (binding.tagMap[ContextTags.GLOBAL_INTERCEPTOR]) { global = true; } ctx.add(binding); } else { let key = options.key; if (!key) { const name = options.name ?? interceptor.name; if (!name) { key = BindingKey.generate(namespace).key; } else { key = `${namespace}.${name}`; } } binding = ctx.bind(key).to(interceptor); } if (global) { binding.apply(asGlobalInterceptor(group)); if (source) { binding.tag({ [ContextTags.GLOBAL_INTERCEPTOR_SOURCE]: source }); } } return binding; } __name(registerInterceptor, "registerInterceptor"); export { INTERCEPT_CLASS_KEY, INTERCEPT_METHOD_KEY, InterceptedInvocationContext, asGlobalInterceptor, globalInterceptor, intercept, invokeMethodWithInterceptors, mergeInterceptors, registerInterceptor }; //# sourceMappingURL=interceptor.js.map