/* ------------------------------------------------------------------------- * 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) * * Only supports 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 function (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) Direct Usage Functions (no decorator support) * ------------------------------------------------------------------------ */ /** * `before`: * Direct usage => myFn = before(myFn, (ctx, args) => ...) */ export function before(fn: T, advice: BeforeAdvice): T { return SignalMap[SIGNALS.BEFORE](fn, advice); } /** * `after`: * Direct usage => myFn = after(myFn, (ctx, result, args) => ...) */ export function after(fn: T, advice: AfterAdvice): T { return SignalMap[SIGNALS.AFTER](fn, advice); } /** * `around`: * Direct usage => myFn = around(myFn, (proceed, ctx, args) => ...) */ export function around(fn: T, advice: AroundAdvice): T { return SignalMap[SIGNALS.AROUND](fn, advice); } /** * `error`: * Direct usage => myFn = error(myFn, (err, ctx, args) => ...) */ export function error(fn: T, advice: ErrorAdvice): T { return SignalMap[SIGNALS.ERROR](fn, advice); } /* ------------------------------------------------------------------------- * 7) Default Export * ------------------------------------------------------------------------ */ export default { SIGNALS, before, after, around, error, };