Skip to content

@thuum/decor

Terminal window
npm install @thuum/decor
# or
bun add @thuum/decor

@thuum/decor provides higher-order functions that wrap existing functions with cross-cutting concerns — without modifying the original implementation. It ships the building blocks; users build the patterns.

One-shot functions that decorate a specific target function, returning a new version:

  • decorate(fn, wrapper) — Decorates a single function with a wrapper, preserving its signature
  • transform(fn, transformer) — Transforms a function into a new one with a potentially different signature

Produce reusable decorators that can be applied to many compatible functions:

  • attempt — Wraps a function in try-catch, returning a Result<T> instead of throwing
  • decorator(wrapper) — Creates a reusable, type-safe function decorator with full interception power
  • middleware(mw) — Creates a decorator using a middleware pattern with a next() callback
  • probe(probeFn) — Creates a decorator for tracing function execution (arguments and results)

The async variants (available at @thuum/decor/async) include all factories above plus:

  • scheduler(next) — Creates a decorator that routes every invocation through a scheduling strategy
  • continuation(seed?) — Creates a sequential FIFO scheduler for serializing async execution
  • 🔍 Observability — Trace, log, and measure function calls without modifying behavior
  • 🛡️ Precondition Guards — Prevent execution when contracts are unmet
  • 🔄 Lifecycle Hooks — Trigger setup or teardown around execution
  • 💪 Resilience — Retry, timeout, circuit-break, and fallback on failure
  • Flow Control — Throttle, debounce, serialize, and limit concurrency
  • 📦 Caching — Memoize and replay results to avoid redundant work
  • 🧩 Composability — Stack decorators declaratively, type-safe and zero-dependency

The patterns below appear in virtually every production codebase. @thuum/decor provides primitives that make building them trivial — without coupling your business logic to cross-cutting infrastructure.

These are the primary reasons @thuum/decor exists.

See what happens without changing what happens.

Emit structured telemetry around invocation. May wrap in try/catch with rethrow. Never alters behavior or return values.

import { probe } from "@thuum/decor";
// Structured logging — observe arguments and results
const withLogging = (name: string) =>
probe((...args: unknown[]) => {
console.log(`[${name}] called with:`, args);
return (result) => {
if (result.ok) {
console.log(`[${name}] returned:`, result.value);
} else {
console.error(`[${name}] threw:`, result.error);
}
};
});
const add = withLogging("add")((a: number, b: number) => a + b);
add(2, 3);
// [add] called with: [2, 3]
// [add] returned: 5
import { middleware } from "@thuum/decor";
// Execution timing — measure duration without altering flow
const withTiming = (label: string) =>
middleware((next) => {
const start = performance.now();
next();
const elapsed = performance.now() - start;
console.log(`[${label}] ${elapsed.toFixed(2)}ms`);
});
const compute = withTiming("compute")((x: number) => x * x);
compute(42); // [compute] 0.01ms
import { probe } from "@thuum/decor";
// Call counting — track invocation frequency
const withCounter = (name: string) => {
let count = 0;
return probe((..._args: unknown[]) => {
count++;
console.log(`[${name}] invocation #${count}`);
});
};
const greet = withCounter("greet")((name: string) => `Hello, ${name}!`);
greet("Alice"); // [greet] invocation #1
greet("Bob"); // [greet] invocation #2
import { probe } from "@thuum/decor";
// Tracing spans — emit span-like context for distributed tracing
const withTracing = (spanName: string) =>
probe((...args: unknown[]) => {
const traceId = crypto.randomUUID();
console.log(`[trace:${traceId}] → ${spanName}`, args);
return (result) => {
if (result.ok) {
console.log(`[trace:${traceId}] ← ${spanName} OK`);
} else {
console.log(`[trace:${traceId}] ← ${spanName} ERROR`, result.error);
}
};
});

Prevent execution when a contract is unmet.

Short-circuit with a throw or early return. Guards protect invariants — they do not produce Result<T>.

import { decorator } from "@thuum/decor";
// Disposed check — prevent use-after-dispose
const guardDisposed = (isDisposed: () => boolean) =>
decorator((fn, ...args: unknown[]) => {
if (isDisposed()) {
throw new Error("Cannot invoke: resource is disposed");
}
return fn(...args);
});
let disposed = false;
const send = guardDisposed(() => disposed)((msg: string) => {
console.log("Sending:", msg);
});
send("hello"); // Sending: hello
disposed = true;
send("world"); // throws: Cannot invoke: resource is disposed
import { middleware } from "@thuum/decor";
// Already-initialized / idempotency — execute only once
const once = (() => {
let initialized = false;
return middleware((next) => {
if (initialized) return;
initialized = true;
next();
});
})();
const setup = once(() => {
console.log("Initializing...");
});
setup(); // Initializing...
setup(); // (no-op)
import { decorator } from "@thuum/decor";
// Input validation — reject invalid arguments
const validatePositive = decorator((fn, n: number) => {
if (n < 0) throw new RangeError(`Expected positive number, got ${n}`);
return fn(n);
});
const sqrt = validatePositive(Math.sqrt);
sqrt(16); // 4
sqrt(-1); // throws RangeError: Expected positive number, got -1
import { decorator } from "@thuum/decor";
// Invariant assertion — fail fast on violated assumptions
const assertNonNull = <T>(label: string) =>
decorator((fn, value: T | null | undefined) => {
if (value == null) {
throw new TypeError(`Invariant violation: ${label} must not be null`);
}
return fn(value);
});
const processUser = assertNonNull<{ name: string }>("user")((user) => {
return user.name.toUpperCase();
});
processUser({ name: "Alice" }); // "ALICE"
processUser(null); // throws TypeError

Control failure modes declaratively.

Protect against transient and systemic failures. Includes the Result pattern (attempt), retry, timeout, and circuit breaker strategies.

import { attempt } from "@thuum/decor";
// Result pattern — convert throws into values
const divide = (a: number, b: number) => {
if (b === 0) throw new Error("Divide by zero");
return a / b;
};
const safeDivide = attempt(divide);
const result = safeDivide(10, 0);
if (!result.ok) {
console.error("Failed:", result.error); // Failed: Error: Divide by zero
} else {
console.log("Result:", result.value);
}
import { decorator } from "@thuum/decor/async";
// Retry with exponential backoff — rethrows on exhaustion
const withRetry = (maxAttempts: number, baseDelayMs: number) =>
decorator(async (fn, ...args: unknown[]) => {
let lastError: unknown;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn(...args);
} catch (error) {
lastError = error;
if (attempt < maxAttempts) {
const delay = baseDelayMs * Math.pow(2, attempt - 1);
await new Promise((r) => setTimeout(r, delay));
}
}
}
throw lastError;
});
const fetchUser = withRetry(3, 500)(async (id: number) => {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
});
await fetchUser(1); // retries up to 3 times with 500ms, 1000ms, 2000ms delays
import { decorator } from "@thuum/decor/async";
// Timeout — reject after deadline
const withTimeout = (ms: number) =>
decorator(async (fn, ...args: unknown[]) => {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), ms);
try {
return await fn(...args);
} finally {
clearTimeout(timeout);
}
});
const fetchData = withTimeout(5000)(async (url: string) => {
const res = await fetch(url);
return res.json();
});
import { decorator } from "@thuum/decor/async";
// Circuit breaker — fast-fail after threshold
const withCircuitBreaker = (threshold: number, cooldownMs: number) => {
let failures = 0;
let openUntil = 0;
return decorator(async (fn, ...args: unknown[]) => {
if (Date.now() < openUntil) {
throw new Error("Circuit is open — request rejected");
}
try {
const result = await fn(...args);
failures = 0;
return result;
} catch (error) {
failures++;
if (failures >= threshold) {
openUntil = Date.now() + cooldownMs;
}
throw error;
}
});
};

High-value patterns that demonstrate @thuum/decor’s expressiveness.

Trigger setup or teardown around execution.

import { middleware } from "@thuum/decor/async";
// Ensure-initialized — lazily initialize a resource before first use
const ensureInitialized = (init: () => Promise<void>) => {
let initialized = false;
return middleware(async (next) => {
if (!initialized) {
await init();
initialized = true;
}
await next();
});
};
const withDb = ensureInitialized(async () => {
console.log("Connecting to database...");
await connectToDatabase();
});
const query = withDb(async (sql: string) => {
return db.execute(sql);
});
await query("SELECT 1"); // Connecting to database... (first call only)
await query("SELECT 2"); // (already initialized)
import { middleware } from "@thuum/decor";
// Before/after hooks — resource acquire/release
const withHooks = (before: () => void, after: () => void) =>
middleware((next) => {
before();
try {
next();
} finally {
after();
}
});
const withTransaction = withHooks(
() => console.log("BEGIN"),
() => console.log("COMMIT"),
);
const save = withTransaction((data: string) => {
console.log("Saving:", data);
});
save("record");
// BEGIN
// Saving: record
// COMMIT

Govern when and how often execution happens.

import { decorator } from "@thuum/decor";
// Throttle — limit invocation rate (browser events)
const throttle = (limitMs: number) => {
let lastCall = 0;
return decorator((fn, ...args: unknown[]) => {
const now = Date.now();
if (now - lastCall < limitMs) return undefined as ReturnType<typeof fn>;
lastCall = now;
return fn(...args);
});
};
const onScroll = throttle(100)((e: Event) => {
console.log("Scroll event processed");
});
import { decorator } from "@thuum/decor";
// Debounce — delay until activity settles (browser events)
const debounce = (waitMs: number) => {
let timer: ReturnType<typeof setTimeout> | null = null;
return decorator((fn, ...args: unknown[]) => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => fn(...args), waitMs);
return undefined as ReturnType<typeof fn>;
});
};
const onInput = debounce(300)((value: string) => {
console.log("Search:", value);
});
import { scheduler, continuation } from "@thuum/decor/async";
// Serialization (FIFO) — ensure sequential execution
const next = continuation();
const sequential = scheduler(next);
const process = sequential(async (id: string) => {
await new Promise((r) => setTimeout(r, Math.random() * 50));
return `${id} done`;
});
// Calls execute one-at-a-time, in FIFO order
const results = await Promise.all(["a", "b", "c"].map(process));
// ["a done", "b done", "c done"]

Avoid redundant computation or I/O.

import { decorator } from "@thuum/decor";
// Memoization — cache results by arguments
const memoize = () => {
const cache = new Map<string, unknown>();
return decorator((fn, ...args: unknown[]) => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key) as ReturnType<typeof fn>;
const result = fn(...args);
cache.set(key, result);
return result;
});
};
const fibonacci = memoize()((n: number): number => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
fibonacci(50); // instant
import { decorator } from "@thuum/decor/async";
// TTL cache — expire results after a duration
const withTTLCache = (ttlMs: number) => {
const cache = new Map<string, { value: unknown; expires: number }>();
return decorator(async (fn, ...args: unknown[]) => {
const key = JSON.stringify(args);
const cached = cache.get(key);
if (cached && Date.now() < cached.expires) {
return cached.value as ReturnType<typeof fn>;
}
const result = await fn(...args);
cache.set(key, { value: result, expires: Date.now() + ttlMs });
return result;
});
};
const fetchUser = withTTLCache(60_000)(async (id: number) => {
const res = await fetch(`/api/users/${id}`);
return res.json();
});

All decorators produced by @thuum/decor are regular functions — compose them by stacking. The outermost decorator runs first.

import { decorator } from "@thuum/decor/async";
import { probe } from "@thuum/decor/async";
const withLogging = (name: string) =>
probe((...args: unknown[]) => {
console.log(`[${name}] →`, args);
return (result) => {
if (result.ok) console.log(`[${name}] ←`, result.value);
else console.error(`[${name}] ✗`, result.error);
};
});
const withRetry = (attempts: number) =>
decorator(async (fn, ...args: unknown[]) => {
let lastError: unknown;
for (let i = 1; i <= attempts; i++) {
try { return await fn(...args); }
catch (e) { lastError = e; }
}
throw lastError;
});
const withTimeout = (ms: number) =>
decorator(async (fn, ...args: unknown[]) => {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), ms);
try { return await fn(...args); }
finally { clearTimeout(t); }
});
// Stack: logging (outermost) → retry → timeout → target
const fetchUser = withLogging("fetchUser")(
withRetry(3)(
withTimeout(5000)(async (id: number) => {
const res = await fetch(`/api/users/${id}`);
return res.json();
})
)
);
await fetchUser(42);
// [fetchUser] → [42]
// (retries with timeout on each attempt)
// [fetchUser] ← { id: 42, name: "Alice" }

The package offers four functions with full or partial interception power — decorate, decorator, middleware, and probe — at different levels of reusability and control.

Capabilitydecoratedecoratormiddlewareprobe
Read arguments✅ receives ...args✅ receives ...args❌ only receives next✅ receives ...args
Modify arguments✅ can pass different values to fn()✅ can pass different values to fn()❌ no access❌ target always called with original args
Read return value✅ captures fn() result✅ captures fn() result❌ no access✅ via Result<T> callback (read-only)
Modify return value✅ can return something else✅ can return something else❌ no access❌ original value always returned
Prevent target execution✅ simply don’t call fn()✅ simply don’t call fn()✅ simply don’t call next()⚠️ only by throwing before
Call target multiple times✅ (e.g. retry)✅ (e.g. retry)✅ can call next() multiple times❌ always called exactly once
Code before target
Code after target✅ via optional callback
Observe errors✅ with try/catch around fn()✅ with try/catch around fn()⚠️ indirectly (next throws)✅ via Result { ok: false, error }
Preserve this binding✅ auto-bound✅ auto-bound✅ auto-applied✅ auto-applied
Composable (stackable)⚠️ via nesting decorate() calls
Type-safe wrapper signature✅ typed to target params✅ typed to target params✅ generic over any target✅ typed to probe params
Reusable across functions❌ one-shot, bound to specific fn✅ returns a reusable decorator✅ returns a reusable decorator✅ returns a reusable decorator
decoratedecoratormiddlewareprobe
Mental modelAd-hoc wrapping — you decorate one specific functionDecorator factory — you create a reusable wrapper for many functionsFlow control gate — you decide whether to proceedPassive observer — you watch the function
ResponsibilityYou call fn(), you handle the resultYou call fn(), you handle the resultYou call next() to proceedThe framework calls the target for you
Power level🔴 Maximum🔴 Maximum🟡 Medium🟢 Minimum
ScopeSingle functionAny compatible functionAny functionAny compatible function
Typical use casesOne-off logging, argument clamping, ad-hoc memoization, quick inline wrappingReusable memoization, retry, argument validation/transformation, trampolines, access controlFeature flags, timing, before/after hooks, guards, conditional executionLogging, tracing, metrics, auditing, assertions
  • decorate — You want to wrap one specific function with full interception power. Ideal for ad-hoc, inline decoration where the wrapper logic is specific to that function and doesn’t need to be reusable.
  • probe — You just want to observe without interfering. The target always runs, you optionally inspect the outcome. Ideal for telemetry, logging, and lightweight precondition guards (that throw).
  • middleware — You need to control whether the target runs and/or wrap it with before/after logic, but you don’t need to touch the arguments or return value. Familiar Express/Koa pattern.
  • decorator — You need full control and reusability: create a wrapper once and apply it to many functions. Transform inputs, transform outputs, call the target conditionally or repeatedly, or replace its behavior entirely.

Decorates a single target function with a wrapper, returning a new function with the same signature.

The wrapper receives:

  1. fn — the original function (with this already bound)
  2. ...args — the arguments passed to the decorated function

It must return the same type as the original function.

import { decorate } from "@thuum/decor";
function greet(name: string) {
return `Hello, ${name}!`;
}
const loggedGreet = decorate(greet, (fn, name) => {
console.log(`greet called with "${name}"`);
const result = fn(name);
console.log(`greet returned "${result}"`);
return result;
});

Transforms a function into a new one with a potentially different signature (arguments and/or return type).

import { transform } from "@thuum/decor";
const add = (a: number, b: number) => a + b;
const addStrings = transform(add, (fn, a: string, b: string) => {
return fn(Number(a), Number(b)).toString();
});
addStrings("2", "3"); // "5"

Creates a reusable, type-safe function decorator. The wrapper receives fn and ...args, and must return the same type as the target.

import { decorator } from "@thuum/decor";
const withLogging = decorator((fn, ...args: unknown[]) => {
console.log("called with:", args);
const result = fn(...args);
console.log("returned:", result);
return result;
});
const add = withLogging((a: number, b: number) => a + b);
add(2, 3); // logs arguments and result

Creates a decorator using a middleware pattern. Call next() to execute the target; skip it to short-circuit.

import { middleware } from "@thuum/decor";
const withGuard = (condition: () => boolean) =>
middleware((next) => {
if (!condition()) throw new Error("Guard failed");
next();
});

Decorates a function so it returns a Result<T> instead of throwing.

import { attempt } from "@thuum/decor";
const safeParse = attempt(JSON.parse);
const result = safeParse("not json");
// { ok: false, error: SyntaxError }

Creates a decorator that observes function execution without interfering. The probeFn receives call arguments and optionally returns a callback that receives the Result<T>.

import { probe } from "@thuum/decor";
const trace = probe((...args: unknown[]) => {
console.log("", args);
return (result) => {
if (result.ok) console.log("", result.value);
else console.error("", result.error);
};
});

All factories have async versions at @thuum/decor/async:

import { decorator, middleware, probe, attempt } from "@thuum/decor/async";
import { scheduler, continuation } from "@thuum/decor/async";

Async factories work identically to their sync counterparts but handle Promise-returning functions and await within wrappers.

Async-only. Creates a decorator that routes invocations through a scheduling strategy.

import { scheduler, continuation } from "@thuum/decor/async";
const next = continuation();
const sequential = scheduler(next);
const process = sequential(async (id: string) => {
await someAsyncWork(id);
return `${id} done`;
});
// Executes one-at-a-time in FIFO order
await Promise.all(["a", "b", "c"].map(process));
type Result<T> =
| { ok: true; value: T; error?: never }
| { ok: false; value?: never; error: unknown };

A discriminated union representing either a successful computation or a failed one. Use result.ok to narrow the type.


ISC