ts-observe
v1.0.7
Published
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
Maintainers
Readme
ts-observe: Function/Method/Class Observation and Middleware
ts-observe provides a flexible way to wrap functions, methods, accessors, and class constructors with middleware logic in TypeScript. This allows you to execute code before and after the original logic runs, enabling Aspect-Oriented Programming (AOP) patterns like logging, validation, caching, performance monitoring, and more, primarily through the use of decorators.
Development
From your terminal run:
npm i ts-observeto install the package.
If you are using typescript, make sure to include the following in your tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Core Concept: The observe Function
At its heart, ts-observe uses the observe function to wrap a target function.
import { observe, MiddlewareContext } from 'ts-observe';
// Example Middleware
const logBefore: (context: MiddlewareContext, ...args: any[]) => any[] = (context, ...args) => {
console.log(`[${context.kind}:${String(context.name)}] Calling with args:`, args);
return args; // Must return the (potentially modified) arguments array
};
const logAfter: (context: MiddlewareContext, result: any) => any = (context, result) => {
console.log(`[${context.kind}:${String(context.name)}] Returned:`, result);
return result; // Must return the (potentially modified) result
};
// Original function
function add(a: number, b: number): number {
console.log('Executing add...');
return a + b;
}
// Wrap the function
const observedAdd = observe(add, [logBefore], [logAfter]);
// Call the wrapped function
const sum = observedAdd(5, 3);
// Output:
// [function:add] Calling with args: [ 5, 3 ]
// Executing add...
// [function:add] Returned: 8
console.log('Final Sum:', sum); // Output: Final Sum: 8While observe can be used directly, the primary way to use ts-observe is through decorators for classes, methods, and accessors.
Decorators
ObserveMethod
Use @ObserveMethod to apply middleware to class methods.
import { ObserveMethod, MethodArgsMiddleware, MethodResultMiddleware, MiddlewareContext } from 'ts-observe';
// Middleware specific to methods
const validatePositiveArgs: MethodArgsMiddleware<number[]> = function(this: any, context, ...args) {
console.log(`[${context.kind}:${String(context.name)}] Validating args on instance:`, this);
if (args.some(arg => arg <= 0)) {
throw new Error("Arguments must be positive!");
}
return args; // Return original or modified args
};
const doubleResult: MethodResultMiddleware<number> = function(this: any, context, result) {
console.log(`[${context.kind}:${String(context.name)}] Doubling result on instance:`, this);
return result * 2;
};
class Calculator {
id = Math.random();
@ObserveMethod({
beforeArgs: [validatePositiveArgs],
afterResult: [doubleResult]
})
multiply(a: number, b: number): number {
console.log('Executing multiply...');
return a * b;
}
}
const calc = new Calculator();
const product = calc.multiply(4, 5);
// Output:
// [method:multiply] Validating args on instance: Calculator { id: 0.123... }
// Executing multiply...
// [method:multiply] Doubling result on instance: Calculator { id: 0.123... }
console.log('Final Product:', product); // Output: Final Product: 40
try {
calc.multiply(4, -5);
} catch (e: any) {
console.error(e.message); // Output: Arguments must be positive!
}ObserveAccessor
Use @ObserveAccessor to apply middleware to class property getters and setters.
import { ObserveAccessor, AccessorGetResultMiddleware, AccessorSetArgMiddleware, AccessorSetResultMiddleware, MiddlewareContext } from 'ts-observe';
// Middleware specific to accessors
const logGet: AccessorGetResultMiddleware<string> = function(this: any, context, result) {
console.log(`[${context.kind}:${String(context.name)}] Got value: '${result}' on instance:`, this);
return result.toUpperCase(); // Modify the result
};
const validateSet: AccessorSetArgMiddleware<string> = function(this: any, context, value) {
console.log(`[${context.kind}:${String(context.name)}] Validating set value: '${value}' on instance:`, this);
if (value.length < 3) {
throw new Error("Name must be at least 3 characters long.");
}
return value; // Return original or modified value
};
const logSetComplete: AccessorSetResultMiddleware = function(this: any, context, result) {
// Note: Setters return void, so 'result' is undefined here.
console.log(`[${context.kind}:${String(context.name)}] Set operation complete on instance:`, this);
// Setters don't return a value, so we don't return anything either.
};
class User {
private _name: string = 'Default';
id = Math.random();
@ObserveAccessor({
afterGet: [logGet],
beforeSet: [validateSet],
afterSet: [logSetComplete]
})
get name(): string {
console.log('Executing getter...');
return this._name;
}
set name(newName: string) {
console.log('Executing setter...');
this._name = newName;
}
}
const user = new User();
// Using the setter
try {
user.name = 'Alice';
// Output:
// [setter:name] Validating set value: 'Alice' on instance: User { _name: 'Default', id: 0.456... }
// Executing setter...
// [setter:name] Set operation complete on instance: User { _name: 'Alice', id: 0.456... }
user.name = 'Bo';
} catch (e: any) {
console.error(e.message); // Output: Name must be at least 3 characters long.
}
// Using the getter
const currentName = user.name;
// Output:
// Executing getter...
// [getter:name] Got value: 'Alice' on instance: User { _name: 'Alice', id: 0.456... }
console.log('Current Name:', currentName); // Output: Current Name: ALICEObserveClass
Use @ObserveClass to apply middleware to the class constructor.
beforeArgs: Runs before thesuper()call within the constructor wrapper. Useful for validating or transforming constructor arguments.thisis not available yet.afterInstance: Runs after thesuper()call.thisrefers to the newly created instance. Useful for logging instance creation or performing post-initialization logic.
import { ObserveClass, ConstructorArgsMiddleware, ConstructorInstanceMiddleware, MiddlewareContext } from './observe';
// Middleware specific to constructors
const logConstructorArgs: ConstructorArgsMiddleware<any[]> = (context, ...args) => {
console.log(`[${context.kind}:${context.name}] Constructing with args:`, args);
// Modify args if needed: return [args[0].toUpperCase(), ...args.slice(1)];
return args;
};
const logInstanceCreation: ConstructorInstanceMiddleware<any> = function(this: any, context, instance) {
console.log(`[${context.kind}:${context.name}] Instance created:`, instance);
// Can modify the instance here, or even return a different object (advanced)
// this.initialized = true;
return instance; // Return the instance (or a modified/replaced one)
};
@ObserveClass({
beforeArgs: [logConstructorArgs],
afterInstance: [logInstanceCreation]
})
class Greeter {
greeting: string;
name: string;
constructor(message: string, name: string) {
console.log('Executing original constructor...');
this.greeting = message;
this.name = name;
}
greet() {
return `${this.greeting}, ${this.name}!`;
}
static staticMethod() {
console.log("Static method called!");
}
}
// Call the constructor
const greeter = new Greeter('Hello', 'World');
// Output:
// [class:Greeter] Constructing with args: [ 'Hello', 'World' ]
// Executing original constructor...
// [class:Greeter] Instance created: Greeter { greeting: 'Hello', name: 'World' }
console.log(greeter.greet()); // Output: Hello, World!
// Static members are preserved
Greeter.staticMethod(); // Output: Static method called!Middleware Context
All middleware functions receive a MiddlewareContext object as their first argument (after this where applicable). This object provides information about the element being observed:
kind:'function','method','getter','setter', or'class'.name: The name of the function, method, property, or class.target: The object the member belongs to (prototype for instance members, constructor for static members/class).
Middleware Types
ts-observe provides specific types for middleware functions to ensure type safety:
MethodArgsMiddleware<TArgs>: ForbeforeArgsin@ObserveMethod. Receivesthis,context, and method arguments (...args). Must return an array of arguments.MethodResultMiddleware<TReturn>: ForafterResultin@ObserveMethod. Receivesthis,context, and the method result. Must return a result of the same type.AccessorGetResultMiddleware<TReturn>: ForafterGetin@ObserveAccessor. Receivesthis,context, and the getter result. Must return a result of the same type.AccessorSetArgMiddleware<TVal>: ForbeforeSetin@ObserveAccessor. Receivesthis,context, and the value being set. Must return a value of the same type.AccessorSetResultMiddleware: ForafterSetin@ObserveAccessor. Receivesthis,context, andundefined(as setters returnvoid). Should not return a value.ConstructorArgsMiddleware<TArgs>: ForbeforeArgsin@ObserveClass. Receivesnull(forthis),context, and constructor arguments (...args). Must return an array of arguments.ConstructorInstanceMiddleware<TInstance>: ForafterInstancein@ObserveClass. Receives the newinstance(forthis),context, and theinstanceagain. Can optionally return a modified or replacementinstance.
Error Handling
Errors occurring within middleware functions are caught and logged to the console.
- Errors in
beforemiddleware prevent the original function/method/constructor from executing and re-throw a new error. - Errors in the original function execution are caught, logged, and re-thrown, preserving the original stack trace.
- Errors in
aftermiddleware are caught, logged, and re-throw a new error after the original function has already executed.
Middleware functions that are supposed to return arguments (like beforeArgs) but return a non-array value will cause a warning, and the original arguments will be used instead to prevent unexpected behavior.
