contexify
Version:
A TypeScript library providing a powerful dependency injection container with context-based IoC capabilities, inspired by LoopBack's Context system.
122 lines (112 loc) • 3.54 kB
text/typescript
import { Context } from '../context/context.js';
import { ResolutionSession } from '../resolution/resolution-session.js';
import type { ValueOrPromise } from '../utils/value-promise.js';
import { invokeMethodWithInterceptors } from './interceptor.js';
import type { InvocationArgs, InvocationSource } from './invocation.js';
/**
* Create the Promise type for `T`. If `T` extends `Promise`, the type is `T`,
* otherwise the type is `ValueOrPromise<T>`.
*/
export type AsValueOrPromise<T> = T extends Promise<unknown>
? T
: ValueOrPromise<T>;
/**
* The intercepted variant of a function to return `ValueOrPromise<T>`.
* If `T` is not a function, the type is `T`.
*/
export type AsInterceptedFunction<T> = T extends (
...args: InvocationArgs
) => infer R
? (...args: Parameters<T>) => AsValueOrPromise<R>
: T;
/**
* The proxy type for `T`. The return type for any method of `T` with original
* return type `R` becomes `ValueOrPromise<R>` if `R` does not extend `Promise`.
* Property types stay untouched.
*
* @example
* ```ts
* class MyController {
* name: string;
*
* greet(name: string): string {
* return `Hello, ${name}`;
* }
*
* async hello(name: string) {
* return `Hello, ${name}`;
* }
* }
* ```
*
* `AsyncProxy<MyController>` will be:
* ```ts
* {
* name: string; // the same as MyController
* greet(name: string): ValueOrPromise<string>; // the return type becomes `ValueOrPromise<string>`
* hello(name: string): Promise<string>; // the same as MyController
* }
* ```
*/
export type AsyncProxy<T> = { [P in keyof T]: AsInterceptedFunction<T[P]> };
/**
* Invocation source for injected proxies. It wraps a snapshot of the
* `ResolutionSession` that tracks the binding/injection stack.
*/
export class ProxySource implements InvocationSource<ResolutionSession> {
type = 'proxy';
constructor(readonly value: ResolutionSession) {}
toString() {
return this.value.getBindingPath();
}
}
/**
* A proxy handler that applies interceptors
*
* See https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Proxy
*/
export class InterceptionHandler<T extends object> implements ProxyHandler<T> {
constructor(
private context = new Context(),
private session?: ResolutionSession,
private source?: InvocationSource
) {}
get(target: T, propertyName: PropertyKey, _receiver: unknown) {
const targetObj = target as Record<PropertyKey, unknown>;
if (typeof propertyName !== 'string') return targetObj[propertyName];
const propertyOrMethod = targetObj[propertyName];
if (typeof propertyOrMethod === 'function') {
return (...args: InvocationArgs) => {
return invokeMethodWithInterceptors(
this.context,
target,
propertyName,
args,
{
source:
this.source ?? (this.session && new ProxySource(this.session)),
}
);
};
}
return propertyOrMethod;
}
}
/**
* Create a proxy that applies interceptors for method invocations
* @param target - Target class or object
* @param context - Context object
* @param session - Resolution session
* @param source - Invocation source
*/
export function createProxyWithInterceptors<T extends object>(
target: T,
context?: Context,
session?: ResolutionSession,
source?: InvocationSource
): AsyncProxy<T> {
return new Proxy(
target,
new InterceptionHandler(context, ResolutionSession.fork(session), source)
) as AsyncProxy<T>;
}