@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
269 lines (248 loc) • 8.15 kB
text/typescript
// Copyright IBM Corp. and LoopBack contributors 2019,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
import debugFactory from 'debug';
import {BindingFilter} from './binding-filter';
import {BindingAddress} from './binding-key';
import {BindingComparator} from './binding-sorter';
import {Context} from './context';
import {InvocationResult} from './invocation';
import {transformValueOrPromise, ValueOrPromise} from './value-promise';
const debug = debugFactory('loopback:context:interceptor-chain');
/**
* Any type except `void`. We use this type to enforce that interceptor functions
* always return a value (including undefined or null).
*/
export type NonVoid = string | number | boolean | null | undefined | object;
/**
* The `next` function that can be used to invoke next generic interceptor in
* the chain
*/
export type Next = () => ValueOrPromise<NonVoid>;
/**
* An interceptor function to be invoked in a chain for the given context.
* It serves as the base interface for various types of interceptors, such
* as method invocation interceptor or request/response processing interceptor.
*
* @remarks
* We choose `NonVoid` as the return type to avoid possible bugs that an
* interceptor forgets to return the value from `next()`. For example, the code
* below will fail to compile.
*
* ```ts
* const myInterceptor: Interceptor = async (ctx, next) {
* // preprocessing
* // ...
*
* // There is a subtle bug that the result from `next()` is not further
* // returned back to the upstream interceptors
* const result = await next();
*
* // postprocessing
* // ...
* // We must have `return ...` here
* // either return `result` or another value if the interceptor decides to
* // have its own response
* }
* ```
*
* @typeParam C - `Context` class or a subclass of `Context`
* @param context - Context object
* @param next - A function to proceed with downstream interceptors or the
* target operation
*
* @returns The invocation result as a value (sync) or promise (async).
*/
export type GenericInterceptor<C extends Context = Context> = (
context: C,
next: Next,
) => ValueOrPromise<NonVoid>;
/**
* Interceptor function or a binding key that resolves a generic interceptor
* function
* @typeParam C - `Context` class or a subclass of `Context`
* @typeParam T - Return type of `next()`
*/
export type GenericInterceptorOrKey<C extends Context = Context> =
| BindingAddress<GenericInterceptor<C>>
| GenericInterceptor<C>;
/**
* Invocation state of an interceptor chain
*/
class InterceptorChainState<C extends Context = Context> {
private _index = 0;
/**
* Create a state for the interceptor chain
* @param interceptors - Interceptor functions or binding keys
* @param finalHandler - An optional final handler
*/
constructor(
public readonly interceptors: GenericInterceptorOrKey<C>[],
public readonly finalHandler: Next = () => undefined,
) {}
/**
* Get the index for the current interceptor
*/
get index() {
return this._index;
}
/**
* Check if the chain is done - all interceptors are invoked
*/
done() {
return this._index === this.interceptors.length;
}
/**
* Get the next interceptor to be invoked
*/
next() {
if (this.done()) {
throw new Error('No more interceptor is in the chain');
}
return this.interceptors[this._index++];
}
}
/**
* A chain of generic interceptors to be invoked for the given context
*
* @typeParam C - `Context` class or a subclass of `Context`
*/
export class GenericInterceptorChain<C extends Context = Context> {
/**
* A getter for an array of interceptor functions or binding keys
*/
protected getInterceptors: () => GenericInterceptorOrKey<C>[];
/**
* Create an invocation chain with a list of interceptor functions or
* binding keys
* @param context - Context object
* @param interceptors - An array of interceptor functions or binding keys
*/
constructor(context: C, interceptors: GenericInterceptorOrKey<C>[]);
/**
* Create an invocation interceptor chain with a binding filter and comparator.
* The interceptors are discovered from the context using the binding filter and
* sorted by the comparator (if provided).
*
* @param context - Context object
* @param filter - A binding filter function to select interceptors
* @param comparator - An optional comparator to sort matched interceptor bindings
*/
constructor(
context: C,
filter: BindingFilter,
comparator?: BindingComparator,
);
// Implementation
constructor(
private context: C,
interceptors: GenericInterceptorOrKey<C>[] | BindingFilter,
comparator?: BindingComparator,
) {
if (typeof interceptors === 'function') {
const interceptorsView = context.createView(interceptors, comparator);
this.getInterceptors = () => {
const bindings = interceptorsView.bindings;
if (comparator) {
bindings.sort(comparator);
}
return bindings.map(b => b.key);
};
} else if (Array.isArray(interceptors)) {
this.getInterceptors = () => interceptors;
}
}
/**
* Invoke the interceptor chain
*/
invokeInterceptors(finalHandler?: Next): ValueOrPromise<InvocationResult> {
// Create a state for each invocation to provide isolation
const state = new InterceptorChainState<C>(
this.getInterceptors(),
finalHandler,
);
return this.next(state);
}
/**
* Use the interceptor chain as an interceptor
*/
asInterceptor(): GenericInterceptor<C> {
return (ctx, next) => {
return this.invokeInterceptors(next);
};
}
/**
* Invoke downstream interceptors or the target method
*/
private next(
state: InterceptorChainState<C>,
): ValueOrPromise<InvocationResult> {
if (state.done()) {
// No more interceptors
return state.finalHandler();
}
// Invoke the next interceptor in the chain
return this.invokeNextInterceptor(state);
}
/**
* Invoke downstream interceptors
*/
private invokeNextInterceptor(
state: InterceptorChainState<C>,
): ValueOrPromise<InvocationResult> {
const index = state.index;
const interceptor = state.next();
const interceptorFn = this.loadInterceptor(interceptor);
return transformValueOrPromise(interceptorFn, fn => {
/* istanbul ignore if */
if (debug.enabled) {
debug('Invoking interceptor %d (%s) on %s', index, fn.name);
}
return fn(this.context, () => this.next(state));
});
}
/**
* Return the interceptor function or resolve the interceptor function as a binding
* from the context
*
* @param interceptor - Interceptor function or binding key
*/
private loadInterceptor(interceptor: GenericInterceptorOrKey<C>) {
if (typeof interceptor === 'function') return interceptor;
debug('Resolving interceptor binding %s', interceptor);
return this.context.getValueOrPromise(interceptor) as ValueOrPromise<
GenericInterceptor<C>
>;
}
}
/**
* Invoke a chain of interceptors with the context
* @param context - Context object
* @param interceptors - An array of interceptor functions or binding keys
*/
export function invokeInterceptors<
C extends Context = Context,
T = InvocationResult,
>(
context: C,
interceptors: GenericInterceptorOrKey<C>[],
): ValueOrPromise<T | undefined> {
const chain = new GenericInterceptorChain(context, interceptors);
return chain.invokeInterceptors();
}
/**
* Compose a list of interceptors as a single interceptor
* @param interceptors - A list of interceptor functions or binding keys
*/
export function composeInterceptors<C extends Context = Context>(
...interceptors: GenericInterceptorOrKey<C>[]
): GenericInterceptor<C> {
return (ctx, next) => {
const interceptor = new GenericInterceptorChain(
ctx,
interceptors,
).asInterceptor();
return interceptor(ctx, next);
};
}