@rnw-community/decorators-core
v2.28.0
Published
Framework-agnostic interceptor primitive for building method decorators (Log, metrics, lock, retry, ...). Pure middleware composition. Dual ESM+CJS. Built on TypeScript's experimentalDecorators.
Maintainers
Readme
decorators-core
Framework-agnostic middleware primitive for building method decorators (log, metrics, lock, retry, …). Depends only on @rnw-community/shared for its MethodDecoratorType<K> primitive. TypeScript experimentalDecorators. Dual ESM + CJS.
The primitive
createInterceptor
createInterceptor({ middleware }: CreateInterceptorOptionsInterface) wraps a method descriptor with a single middleware function and returns a MethodDecoratorType<AnyFn>:
import { createInterceptor } from '@rnw-community/decorators-core';
import type { InterceptorMiddleware } from '@rnw-community/decorators-core';
const passthrough: InterceptorMiddleware = (_context, next) => next();
const Passthrough = createInterceptor({ middleware: passthrough });
class Service {
@Passthrough
run(): void { /* ... */ }
}next() invokes the original method. The middleware decides what to do before, after, around, or instead of that call — observe timings, retry, short-circuit, log, acquire a lock — and returns whatever shape the method returns (sync value, Promise<T>, Observable<T>, or anything else). The engine is transport-agnostic and result-shape-agnostic; the middleware owns both.
Stack multiple concerns by stacking decorators at the call site, not by composing internal arrays:
class OrderService {
@Log(...)
@HistogramMetric(...)
@SequentialLock(...)
async placeOrder(id: string): Promise<Receipt> { /* ... */ }
}Exports
InterceptorMiddleware<TArgs, TResult>
Function type: (context: ExecutionContextInterface<TArgs>, next: () => TResult) => TResult. See "Build your own decorator" above for a full implementation.
import type { InterceptorMiddleware } from '@rnw-community/decorators-core';
const timingMiddleware: InterceptorMiddleware<[id: string]> = (context, next) => {
const start = performance.now();
return next();
};ExecutionContextInterface<TArgs>
{ className, methodName, args, logContext } — stable identity per invocation, passed as the first argument to every InterceptorMiddleware. See "Execution context" below for field semantics.
import type { ExecutionContextInterface } from '@rnw-community/decorators-core';
const describe = (context: ExecutionContextInterface): string => context.logContext;CreateInterceptorOptionsInterface<TArgs, TResult>
{ middleware: InterceptorMiddleware<TArgs, TResult> } — the sole option accepted by createInterceptor.
import { createInterceptor } from '@rnw-community/decorators-core';
import type { CreateInterceptorOptionsInterface, InterceptorMiddleware } from '@rnw-community/decorators-core';
declare const middleware: InterceptorMiddleware;
const options: CreateInterceptorOptionsInterface<readonly unknown[], unknown> = { middleware };
const withMiddleware = createInterceptor(options);MethodDecoratorType<K> comes from @rnw-community/shared — import it directly, not through this package.
Build your own decorator
import { createInterceptor } from '@rnw-community/decorators-core';
import type { InterceptorMiddleware } from '@rnw-community/decorators-core';
const logMiddleware: InterceptorMiddleware = (context, next) => {
const start = performance.now();
console.log(`[enter] ${context.logContext}`, context.args);
try {
const result = next();
console.log(`[ok] ${context.logContext} (${(performance.now() - start).toFixed(2)}ms)`, result);
return result;
} catch (error) {
console.error(`[err] ${context.logContext} (${(performance.now() - start).toFixed(2)}ms)`, error);
throw error;
}
};
const withLog = createInterceptor({ middleware: logMiddleware });
class UserService {
@withLog
getUser(id: string): { id: string } { return { id }; }
}For Promise or Observable return types the middleware attaches callbacks to the settled/completion signal — the engine does not special-case them. See @rnw-community/log-decorator, @rnw-community/histogram-metric-decorator, and @rnw-community/lock-decorator for working Promise- and Observable-aware middleware implementations.
Execution context
buildContext(this, fallbackClassName, methodName, args) produces one ExecutionContextInterface per invocation:
className— resolved fromthis.constructor.namewhenthisis a class instance, fromthis.namewhen the method is static (sothisis the class constructor), and falls back to the target's name at decoration time whenthisis detached orglobalThis. Anonymous classes fall back to'Object'.methodName— the property key as a string.args— the original method arguments (tuple-typed viaTArgs).logContext— convenience format`${className}::${methodName}`for consumer transports.
The context identity is stable across a single invocation — middleware can capture context in closures (timers, resource handles) without worrying about mutation.
Engine guarantees
- Non-function descriptors (getters, setters, value properties) pass through unchanged — the engine returns the input descriptor.
- The middleware is called exactly once per decorated-method invocation.
next()is called exactly once inside the middleware unless the middleware short-circuits (and MUST NOT be called more than once; the engine does not re-enter).- The engine does not catch middleware-thrown errors — they propagate to the caller unchanged. Wrap in
try/catchinside the middleware if you need to transform or observe failures.
