/* ------------------------------------------------------------------------- * aspects.ts * * A robust “aspect” system supporting: * - before: optionally modifies arguments (sync or async) * - after: optionally modifies return value (sync or async) * - around: complete control over function invocation * - error: intercept errors (sync or async) * * Works as both: * 1) Decorators for class methods (e.g. @before(...)) * 2) Direct function wrappers (e.g. fn = before(fn, ...)). * ------------------------------------------------------------------------ */ /* ------------------------------------------------------------------------- * 1) SIGNALS Enum (string-based to avoid symbol issues) * ------------------------------------------------------------------------ */ export enum SIGNALS { BEFORE = 'BEFORE', AFTER = 'AFTER', AROUND = 'AROUND', ERROR = 'ERROR', } /* ------------------------------------------------------------------------- * 2) Basic Types * ------------------------------------------------------------------------ */ type AnyFunction = (...args: any[]) => any; /** * If a function returns a Promise, then its awaited type is T. * Otherwise, it's just ReturnType. */ type AwaitedReturn = T extends (...args: any[]) => Promise ? U : ReturnType; /* ------------------------------------------------------------------------- * 3) Advice Signatures * ------------------------------------------------------------------------ */ /** * BEFORE advice: * - Receives `context` (the `this` of the function) * - Receives the original `args` * - Can return either nothing (`void`) or new arguments (`Parameters`) * - Can be async, returning a Promise that resolves to new args or `void` */ export type BeforeAdvice = ( context: ThisParameterType, args: Parameters ) => void | Parameters | Promise>; /** * AFTER advice: * - Receives `context`, the original function’s final (awaited) result, * and the original arguments * - Can return a new result (sync or async) */ export type AfterAdvice = ( context: ThisParameterType, result: AwaitedReturn, args: Parameters ) => AwaitedReturn | Promise>; /** * AROUND advice: * - Provides a `proceed(...args)` function that calls the original method * - You can call `proceed` any number of times, or skip it * - Supports both sync and async usage */ export type AroundAdvice = ( proceed: (...args: Parameters) => ReturnType, context: ThisParameterType, args: Parameters ) => ReturnType; /** * ERROR advice: * - Intercepts errors thrown by the original method (sync or async) * - Can return a fallback result or rethrow */ export type ErrorAdvice = ( error: unknown, context: ThisParameterType, args: Parameters ) => ReturnType | void; /* ------------------------------------------------------------------------- * 4) ISignalMap: Each signal has a distinct function wrapper signature * ------------------------------------------------------------------------ */ interface ISignalMap { [SIGNALS.BEFORE]: (original: T, advice: BeforeAdvice) => T; [SIGNALS.AFTER]: (original: T, advice: AfterAdvice) => T; [SIGNALS.AROUND]: (original: T, advice: AroundAdvice) => T; [SIGNALS.ERROR]: (original: T, advice: ErrorAdvice) => T; } /* ------------------------------------------------------------------------- * 5) The SignalMap Implementation * - This is where the actual "wrapping" logic lives. * ------------------------------------------------------------------------ */ const SignalMap: ISignalMap = { /** * BEFORE: * - Possibly modifies arguments * - If returns a Promise, we await it before calling original * - If returns an array, we use that as new arguments */ [SIGNALS.BEFORE](original: T, advice: BeforeAdvice): T { return function (this: ThisParameterType, ...args: Parameters): ReturnType { const maybeNewArgs = advice(this, args); if (maybeNewArgs instanceof Promise) { return maybeNewArgs.then((resolvedArgs) => { const finalArgs = resolvedArgs || args; const result = original.apply(this, finalArgs); return (result instanceof Promise) ? result : Promise.resolve(result); }) as ReturnType; } else { const finalArgs = Array.isArray(maybeNewArgs) ? maybeNewArgs : args; return original.apply(this, finalArgs); } } as T; }, /** * AFTER: * - Possibly modifies the return value * - If original is async, we chain on its promise * - Advice can be sync or async */ [SIGNALS.AFTER](original: T, advice: AfterAdvice): T { return function (this: ThisParameterType, ...args: Parameters): ReturnType { const result = original.apply(this, args); if (result instanceof Promise) { return result.then((unwrapped) => { const maybeNewResult = advice(this, unwrapped, args); return (maybeNewResult instanceof Promise) ? maybeNewResult : maybeNewResult; }) as ReturnType; } else { const maybeNewResult = advice(this, result as AwaitedReturn, args); if (maybeNewResult instanceof Promise) { return maybeNewResult.then(r => r) as ReturnType; } return maybeNewResult as ReturnType; } } as T; }, /** * AROUND: * - Full control over invocation * - Typically you do: proceed(...args) * - If you want to skip or call multiple times, you can */ [SIGNALS.AROUND](original: T, advice: AroundAdvice): T { return function (this: ThisParameterType, ...args: Parameters): ReturnType { const proceed = (...innerArgs: Parameters) => original.apply(this, innerArgs); return advice(proceed, this, args); } as T; }, /** * ERROR: * - Intercepts errors thrown by the original function or a rejected Promise * - Optionally returns a fallback or rethrows */ [SIGNALS.ERROR](original: T, advice: ErrorAdvice): T { return function (this: ThisParameterType, ...args: Parameters): ReturnType { try { const result = original.apply(this, args); if (result instanceof Promise) { // Handle async rejections return result.catch((err: unknown) => { return advice(err, this, args); }) as ReturnType; } return result; } catch (err) { // Synchronous error return advice(err, this, args) as ReturnType; } } as T; }, }; /* ------------------------------------------------------------------------- * 6) Decorator Helper * ------------------------------------------------------------------------ */ /** Checks if we’re decorating a class method. */ function isMethod( _target: any, descriptor?: PropertyDescriptor ): descriptor is PropertyDescriptor & { value: AnyFunction } { return !!descriptor && typeof descriptor.value === 'function'; } /* ------------------------------------------------------------------------- * 7) Wrapped Helpers (cutMethod, cut, aspect) * ------------------------------------------------------------------------ */ /** Strictly typed wrapping for class methods. */ function cutMethod( descriptor: PropertyDescriptor & { value: T }, advice: A, type: SIGNALS ): PropertyDescriptor { const original = descriptor.value; descriptor.value = SignalMap[type](original, advice as any); // Cast `any` or refine further return descriptor; } /** Strictly typed wrapping for direct function usage. */ function cut(target: T, advice: A, type: SIGNALS): T { return SignalMap[type](target, advice as any); } interface AspectOptions { type: T; advice: A; } /** * The core aspect(...) function * - Returns a decorator if used in that style * - Otherwise, can wrap a function directly */ function aspect({ type, advice }: AspectOptions) { // If type is invalid, produce a no-op decorator if (!(type in SignalMap)) { return function crosscut( target: any, _name?: string, descriptor?: PropertyDescriptor ) { return descriptor || target; }; } // Return a decorator function return function crosscut( target: any, _name?: string, descriptor?: PropertyDescriptor ): any { // If used on a method if (isMethod(target, descriptor)) { return cutMethod(descriptor!, advice, type); } // If used directly on a function or something else return cut(target, advice, type); }; } /* ------------------------------------------------------------------------- * 8) Overloaded Decorator/Function Wrappers * - Each can be used as a decorator or direct wrapper * ------------------------------------------------------------------------ */ /** * `before`: * Decorator usage => @before((ctx, args) => ...) * Direct usage => myFn = before(myFn, (ctx, args) => ...) */ export function before( advice: BeforeAdvice ): (target: any, name?: string, descriptor?: PropertyDescriptor) => any; export function before(fn: T, advice: BeforeAdvice): T; export function before( arg1: T | BeforeAdvice, arg2?: BeforeAdvice ): any { if (typeof arg1 === 'function' && typeof arg2 === 'function') { return SignalMap[SIGNALS.BEFORE](arg1, arg2); } return aspect({ type: SIGNALS.BEFORE, advice: arg1 as BeforeAdvice }); } /** * `after`: * Decorator usage => @after((ctx, result, args) => ...) * Direct usage => myFn = after(myFn, (ctx, result, args) => ...) */ export function after( advice: AfterAdvice ): (target: any, name?: string, descriptor?: PropertyDescriptor) => any; export function after(fn: T, advice: AfterAdvice): T; export function after( arg1: T | AfterAdvice, arg2?: AfterAdvice ): any { if (typeof arg1 === 'function' && typeof arg2 === 'function') { return SignalMap[SIGNALS.AFTER](arg1, arg2); } return aspect({ type: SIGNALS.AFTER, advice: arg1 as AfterAdvice }); } /** * `around`: * Decorator usage => @around((proceed, ctx, args) => ...) * Direct usage => myFn = around(myFn, (proceed, ctx, args) => ...) */ export function around( advice: AroundAdvice ): (target: any, name?: string, descriptor?: PropertyDescriptor) => any; export function around(fn: T, advice: AroundAdvice): T; export function around( arg1: T | AroundAdvice, arg2?: AroundAdvice ): any { if (typeof arg1 === 'function' && typeof arg2 === 'function') { return SignalMap[SIGNALS.AROUND](arg1, arg2); } return aspect({ type: SIGNALS.AROUND, advice: arg1 as AroundAdvice }); } /** * `error`: * Decorator usage => @error((err, ctx, args) => ...) * Direct usage => myFn = error(myFn, (err, ctx, args) => ...) */ export function error( advice: ErrorAdvice ): (target: any, name?: string, descriptor?: PropertyDescriptor) => any; export function error(fn: T, advice: ErrorAdvice): T; export function error( arg1: T | ErrorAdvice, arg2?: ErrorAdvice ): any { if (typeof arg1 === 'function' && typeof arg2 === 'function') { return SignalMap[SIGNALS.ERROR](arg1, arg2); } return aspect({ type: SIGNALS.ERROR, advice: arg1 as ErrorAdvice }); } /* ------------------------------------------------------------------------- * 9) Default Export * ------------------------------------------------------------------------ */ export default { SIGNALS, before, after, around, error, aspect, };