npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@thuum/decor

v0.0.3

Published

Type-safe function decorator toolkit for aspect-oriented programming — build reusable, composable patterns for observability, resilience, and flow control in TypeScript

Readme

@thuum/decor

Type-safe function decorator toolkit for aspect-oriented programming — build reusable, composable patterns for observability, resilience, and flow control in TypeScript.

Installation

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

Overview

@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.

Decorator Functions

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

Decorator Factories

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

Features

  • 🔍 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

Motivation

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.

Core Use Cases

These are the primary reasons @thuum/decor exists.

Observability

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);
      }
    };
  });

Precondition Guards

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

Resilience

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;
    }
  });
};

Recipes

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

Lifecycle Hooks

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

Flow Control

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"]

Caching

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();
});

Composability

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" }

Choosing a Decorator

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

Capability Comparison

| Capability | decorate | decorator | middleware | probe | |---|:---:|:---:|:---:|:---:| | 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 |

Design Intent

| | decorate | decorator | middleware | probe | |---|---|---|---|---| | Mental model | Ad-hoc wrapping — you decorate one specific function | Decorator factory — you create a reusable wrapper for many functions | Flow control gate — you decide whether to proceed | Passive observer — you watch the function | | Responsibility | You call fn(), you handle the result | You call fn(), you handle the result | You call next() to proceed | The framework calls the target for you | | Power level | 🔴 Maximum | 🔴 Maximum | 🟡 Medium | 🟢 Minimum | | Scope | Single function | Any compatible function | Any function | Any compatible function | | Typical use cases | One-off logging, argument clamping, ad-hoc memoization, quick inline wrapping | Reusable memoization, retry, argument validation/transformation, trampolines, access control | Feature flags, timing, before/after hooks, guards, conditional execution | Logging, tracing, metrics, auditing, assertions |

When to Choose Which

  • 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.

API Reference

decorate(fn, wrapper)

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;
});

transform(fn, transformer)

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"

decorator(wrapper)

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

middleware(mw)

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();
  });

attempt(fn)

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 }

probe(probeFn)

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);
  };
});

Async Variants

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.

scheduler(next) / continuation(seed?)

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));

Result<T>

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.


License

ISC