ts-observe
Version:
A simple library to create wrapping middlewares for all sort of functions, classes, methods and accessors. This library enables you to decorate (wrap for standalone functions) with before and after middlewares. This approach let's you abstract from the co
196 lines • 9.44 kB
JavaScript
;
// ========================================================================
// 1. Core Wrapping Logic
// ========================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.observe = observe;
exports.ObserveMethod = ObserveMethod;
exports.ObserveAccessor = ObserveAccessor;
exports.ObserveClass = ObserveClass;
function observe(fn, executeBefore = [], executeAfter = [], context) {
if (!context) {
// Provide a default context if used standalone
context = { kind: 'function', name: fn.name || 'anonymous', target: {} };
}
const ctxInfo = `[${context.kind}:${String(context.name)}]`;
return function (...args) {
let processedArgs = args;
try {
executeBefore.forEach((beforeFn) => {
// Use .call() to pass 'this', 'context', and spread arguments correctly
processedArgs = beforeFn.call(this, context, ...processedArgs);
if (!Array.isArray(processedArgs)) {
console.warn(`Before middleware for '${ctxInfo}' did not return array. Reverting.`, { middleware: beforeFn.name });
processedArgs = args; // Keep original args if middleware failed
}
});
}
catch (e) {
console.error(`Error during 'before' middleware for '${ctxInfo}':`, e);
throw new Error(`Failed during 'before' middleware for '${ctxInfo}'`);
}
let result;
try {
// Call the original function using apply to preserve 'this' and pass processed args
result = fn.apply(this, processedArgs);
}
catch (e) {
console.error(`Error during original execution for '${ctxInfo}':`, e);
throw e; // Re-throw original error for better stack traces
}
try {
executeAfter.forEach((afterFn) => {
// .call() is correct here as MethodResultMiddleware expects (this, context, result)
result = afterFn.call(this, context, result);
});
}
catch (e) {
console.error(`Error during 'after' middleware for '${ctxInfo}':`, e);
throw new Error(`Failed during 'after' middleware for '${ctxInfo}'`);
}
return result;
};
}
// ========================================================================
// 3. Specific Decorator Factories
// ========================================================================
// 3.1. For Methods
function ObserveMethod(config = {}) {
const { beforeArgs = [], afterResult = [] } = config;
return function (target, propertyKey, descriptor) {
const originalMethod = descriptor.value;
if (typeof originalMethod === 'function') {
const context = {
kind: 'method',
name: propertyKey,
target: target
};
descriptor.value = observe(originalMethod, beforeArgs, afterResult, context);
return descriptor;
}
else {
console.warn(`ObserveMethod applied to non-method property: ${String(propertyKey)}`);
}
};
}
// 3.2 For Accessors
function ObserveAccessor(config = {}) {
const { beforeGet = [], afterGet = [], beforeSet = [], afterSet = [] } = config;
return function (target, propertyKey, descriptor) {
const originalGetter = descriptor.get;
const originalSetter = descriptor.set;
let newGetter = originalGetter;
let newSetter = originalSetter;
let changed = false;
// Wrap Getter
if (typeof originalGetter === 'function') {
const getterContext = { kind: 'getter', name: propertyKey, target: target };
// Getters have no 'beforeArgs' middleware, pass empty array
newGetter = observe(originalGetter, beforeGet, afterGet, getterContext);
changed = true;
}
// Wrap Setter
if (typeof originalSetter === 'function') {
const setterContext = { kind: 'setter', name: propertyKey, target: target };
// Adapt beforeSet middleware (which expect a single value)
// to the MethodArgsMiddleware signature (which expect ...args)
const adaptedBeforeSet = beforeSet.map(middleware => {
const adapter = function (ctx, ...args) {
const originalValue = args[0];
const processedValue = middleware.call(this, ctx, originalValue);
return [processedValue];
};
return adapter;
});
// Pass the adapted middleware to observe
newSetter = observe(originalSetter, adaptedBeforeSet, afterSet, setterContext);
changed = true;
}
// Assign back to descriptor if changed
if (changed) {
descriptor.get = newGetter;
descriptor.set = newSetter;
return descriptor;
}
else {
console.warn(`ObserveAccessor applied to non-accessor property: ${String(propertyKey)}`);
}
};
}
// 3.3. For Classes (Constructors)
function ObserveClass(config = {}) {
const { beforeArgs = [], afterInstance = [] } = config;
return function (OriginalConstructor) {
if (typeof OriginalConstructor !== 'function' || !OriginalConstructor.prototype) {
console.warn(`ObserveClass applied to non-class target.`);
return OriginalConstructor;
}
const classContext = {
kind: 'class',
name: OriginalConstructor.name,
target: OriginalConstructor
};
// Define the new class extending the original
const NewConstructor = class extends OriginalConstructor {
constructor(...args) {
let processedArgs = args;
// Process beforeArgs (BEFORE super call)
try {
beforeArgs.forEach(middleware => {
// Use apply for ConstructorArgsMiddleware (this is null)
processedArgs = middleware.apply(null, [classContext, ...processedArgs]);
if (!Array.isArray(processedArgs)) {
console.warn(`Constructor '${classContext.name.toString()}' beforeArgs middleware did not return array. Reverting.`);
processedArgs = args;
}
});
}
catch (e) {
console.error(`Error during constructor '${classContext.name.toString()}' 'beforeArgs' middleware:`, e);
throw new Error(`Failed during constructor '${classContext.name.toString()}' 'beforeArgs' middleware`);
}
// Call super() - invokes the OriginalConstructor logic
super(...processedArgs);
// Process afterInstance (AFTER super call) - 'this' is now available
try {
let instance = this;
afterInstance.forEach(middleware => {
// Use call for ConstructorInstanceMiddleware (this is the instance)
const potentiallyModifiedInstance = middleware.call(this, classContext, instance);
// Allow middleware to replace the instance if it returns a value
instance = potentiallyModifiedInstance !== undefined ? potentiallyModifiedInstance : instance;
});
// Note: If middleware replaced the instance, the original 'this' is discarded.
// This is advanced usage and generally not recommended unless necessary.
}
catch (e) {
console.error(`Error during constructor '${classContext.name.toString()}' 'afterInstance' middleware:`, e);
throw new Error(`Failed during constructor '${classContext.name.toString()}' 'afterInstance' middleware`);
}
}
};
// --- Preserve Static Members & Prototype Chain ---
function copyStatics(source, target) {
let currentSource = source;
const copiedKeys = new Set();
while (currentSource && currentSource !== Function.prototype) {
Reflect.ownKeys(currentSource).forEach(key => {
if (key !== 'prototype' && key !== 'length' && key !== 'name' &&
key !== 'arguments' && key !== 'caller' && !copiedKeys.has(key)) {
const descriptor = Object.getOwnPropertyDescriptor(currentSource, key);
if (descriptor && (descriptor.configurable || descriptor.writable)) {
Object.defineProperty(target, key, descriptor);
copiedKeys.add(key);
}
}
});
currentSource = Object.getPrototypeOf(currentSource);
}
}
copyStatics(OriginalConstructor, NewConstructor);
Object.setPrototypeOf(NewConstructor.prototype, OriginalConstructor.prototype);
Object.setPrototypeOf(NewConstructor, OriginalConstructor);
return NewConstructor;
};
}
//# sourceMappingURL=index.js.map