mono/packages/core/dist/aspects.js
2025-01-28 13:42:22 +01:00

188 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* -------------------------------------------------------------------------
* 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 var SIGNALS;
(function (SIGNALS) {
SIGNALS["BEFORE"] = "BEFORE";
SIGNALS["AFTER"] = "AFTER";
SIGNALS["AROUND"] = "AROUND";
SIGNALS["ERROR"] = "ERROR";
})(SIGNALS || (SIGNALS = {}));
/* -------------------------------------------------------------------------
* 5) The SignalMap Implementation
* - This is where the actual "wrapping" logic lives.
* ------------------------------------------------------------------------ */
const SignalMap = {
/**
* 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, advice) {
return function (...args) {
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);
});
}
else {
const finalArgs = Array.isArray(maybeNewArgs) ? maybeNewArgs : args;
return original.apply(this, finalArgs);
}
};
},
/**
* AFTER:
* - Possibly modifies the return value
* - If original is async, we chain on its promise
* - Advice can be sync or async
*/
[SIGNALS.AFTER](original, advice) {
return function (...args) {
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;
});
}
else {
const maybeNewResult = advice(this, result, args);
if (maybeNewResult instanceof Promise) {
return maybeNewResult.then(r => r);
}
return maybeNewResult;
}
};
},
/**
* AROUND:
* - Full control over invocation
* - Typically you do: proceed(...args)
* - If you want to skip or call multiple times, you can
*/
[SIGNALS.AROUND](original, advice) {
return function (...args) {
const proceed = (...innerArgs) => original.apply(this, innerArgs);
return advice(proceed, this, args);
};
},
/**
* ERROR:
* - Intercepts errors thrown by the original function or a rejected Promise
* - Optionally returns a fallback or rethrows
*/
[SIGNALS.ERROR](original, advice) {
return function (...args) {
try {
const result = original.apply(this, args);
if (result instanceof Promise) {
// Handle async rejections
return result.catch((err) => {
return advice(err, this, args);
});
}
return result;
}
catch (err) {
// Synchronous error
return advice(err, this, args);
}
};
},
};
/* -------------------------------------------------------------------------
* 6) Decorator Helper
* ------------------------------------------------------------------------ */
/** Checks if were decorating a class method. */
function isMethod(_target, descriptor) {
return !!descriptor && typeof descriptor.value === 'function';
}
/* -------------------------------------------------------------------------
* 7) Wrapped Helpers (cutMethod, cut, aspect)
* ------------------------------------------------------------------------ */
/** Strictly typed wrapping for class methods. */
function cutMethod(descriptor, advice, type) {
const original = descriptor.value;
descriptor.value = SignalMap[type](original, advice); // Cast `any` or refine further
return descriptor;
}
/** Strictly typed wrapping for direct function usage. */
function cut(target, advice, type) {
return SignalMap[type](target, advice);
}
/**
* The core aspect(...) function
* - Returns a decorator if used in that style
* - Otherwise, can wrap a function directly
*/
function aspect({ type, advice }) {
// If type is invalid, produce a no-op decorator
if (!(type in SignalMap)) {
return function crosscut(target, _name, descriptor) {
return descriptor || target;
};
}
// Return a decorator function
return function crosscut(target, _name, descriptor) {
// 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);
};
}
export function before(arg1, arg2) {
if (typeof arg1 === 'function' && typeof arg2 === 'function') {
return SignalMap[SIGNALS.BEFORE](arg1, arg2);
}
return aspect({ type: SIGNALS.BEFORE, advice: arg1 });
}
export function after(arg1, arg2) {
if (typeof arg1 === 'function' && typeof arg2 === 'function') {
return SignalMap[SIGNALS.AFTER](arg1, arg2);
}
return aspect({ type: SIGNALS.AFTER, advice: arg1 });
}
export function around(arg1, arg2) {
if (typeof arg1 === 'function' && typeof arg2 === 'function') {
return SignalMap[SIGNALS.AROUND](arg1, arg2);
}
return aspect({ type: SIGNALS.AROUND, advice: arg1 });
}
export function error(arg1, arg2) {
if (typeof arg1 === 'function' && typeof arg2 === 'function') {
return SignalMap[SIGNALS.ERROR](arg1, arg2);
}
return aspect({ type: SIGNALS.ERROR, advice: arg1 });
}
/* -------------------------------------------------------------------------
* 9) Default Export
* ------------------------------------------------------------------------ */
export default {
SIGNALS,
before,
after,
around,
error,
aspect,
};
//# sourceMappingURL=aspects.js.map