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

ctxx

v0.1.0

Published

Typed AsyncLocalStorage context for Node.js apps

Downloads

6

Readme

ctxx — Typed AsyncLocalStorage context for Node.js

Lightweight, typed context built on Node's AsyncLocalStorage. Manage request-scoped state, propagate values across async boundaries, and integrate with frameworks like Express.

Install

npm i ctxx

Requires Node >= 18.17.

Why ctxx?

  • Typed: Generic store with strong TypeScript types.
  • Simple: run, get, set, with, use—small API that mirrors React-like ergonomics.
  • Interop: bind and bindEmitter ensure callbacks/listeners observe the right context.
  • Express-ready: One-line middleware to start a context per request.

Quick start

import { createContext } from "ctxx";

type Store = {
  requestId?: string;
  userId?: string;
};

export const ctx = createContext<Store>();

// Start a context scope
ctx.run({ requestId: "req-123" }, () => {
  doWork();
});

function doWork() {
  // Read values anywhere within the scope
  const rid = ctx.get("requestId");

  // Update state (shallow-merged by default)
  ctx.set({ userId: "u_42" });

  // Strongly-typed strict accessor (throws if used outside a scope)
  const store = ctx.use();
  console.log(store.requestId, store.userId);
}

API

Create a context

import { createContext } from "ctxx";

type Store = Record<string, unknown>;

const ctx = createContext<Store>({
  // optional: customize merge behavior for set()/with() (default is shallow assign)
  merge(prev, patch) {
    return Object.assign({}, prev, patch);
  },
});

At a glance

| Method | Signature | Returns | Notes | | --- | --- | --- | --- | | has | has() | boolean | Is there an active store? | | get | get() / get(key) | store | value | undefined | Optional accessor; safe outside a scope | | set | set(patch) | void | Shallow-merge by default (customizable via merge) | | run | run(initial, fn) | R | Start a new async context scope | | with | with(patch, fn) | R | Nested scope with merged store | | bind | bind(fn, opts?) | F | Run fn with the bound context; live by default | | bindEmitter | bindEmitter(emitter, opts?) | void | Listener methods run with bound context; live by default | | use | use() | store | Strict accessor; throws outside a scope | | middleware.express | express(opts?) | RequestHandler | Starts a scope per request |

Method snippets

// has / get / set
if (!ctx.has()) {
  ctx.run({ a: 1 }, () => {/* ... */});
}

ctx.run({ a: 1, nested: { x: 1 } }, () => {
  ctx.get();         // { a: 1, nested: { x: 1 } }
  ctx.get("a");      // 1
  ctx.set({ a: 2 }); // { a: 2, nested: { x: 1 } }
});

// with (temporary shadow)
ctx.run({ a: 1, b: 2 }, () => {
  ctx.with({ b: 3 }, () => {
    ctx.get("b"); // 3
  });
  ctx.get("b");   // 2
});

// use (strict)
ctx.run({ a: 1 }, () => {
  const store = ctx.use();
  store.a; // 1
});

// bind (callbacks)
let bound: () => number;
ctx.run({ x: 1 }, () => {
  bound = ctx.bind(() => ctx.get("x") ?? -1); // live by default
  ctx.set({ x: 2 });
});
bound(); // 2

// bindEmitter (EventEmitter)
import { EventEmitter } from "node:events";
const emitter = new EventEmitter();
ctx.run({ x: 1 }, () => {
  ctx.bindEmitter(emitter); // live by default
  emitter.on("tick", () => {
    console.log(ctx.get("x")); // 2
  });
  ctx.set({ x: 2 });
});

Merge behavior

By default, set and with use shallow assign:

const ctx = createContext<{ a: number; nested?: { x?: number } }>();

ctx.run({ a: 1, nested: { x: 1 } }, () => {
  ctx.set({ nested: { x: 2 } });
  // nested replaced entirely → { a: 1, nested: { x: 2 } }
});

Provide a custom merge to implement deep merge if desired:

import { createContext } from "ctxx";

const ctx = createContext<{ nested: { x?: number; y?: number } }>({
  merge(prev, patch) {
    return {
      ...prev,
      nested: { ...prev.nested, ...patch.nested },
    };
  },
});

Express middleware

import express from "express";
import { createContext } from "ctxx";

type Store = { reqId?: string; userId?: string };
const ctx = createContext<Store>();

const app = express();

app.use(
  ctx.middleware.express({
    async init({ req }) {
      return { reqId: req.headers["x-request-id"] as string };
    },
    onStart({ store, req }) {
      req.log = (msg: string) => console.log(`[${store.reqId}]`, msg);
    },
    onFinish({ store }) {
      console.log("completed", store.reqId);
    },
  })
);

app.get("/hello", (_req, res) => {
  // Context is available during the request
  res.json({ reqId: ctx.get("reqId") });
});

app.listen(3000);

Notes:

  • The middleware runs init to create an initial store per request.
  • onStart is called inside the request context right after it begins.
  • onFinish is attached to finish/close and runs with a bound context.

Binding callbacks and emitters

const ctx = createContext<{ x: number }>();

let bound: () => number;
ctx.run({ x: 1 }, () => {
  const fn = () => ctx.get("x") ?? -1;
  // live by default: if ctx.set({ x: 2 }) happens later, bound sees 2
  bound = ctx.bind(fn);
});

bound(); // 1 now, 2 if updated before invocation

EventEmitter integration:

import { EventEmitter } from "node:events";

const emitter = new EventEmitter();
ctx.run({ x: 1 }, () => {
  ctx.bindEmitter(emitter); // live binding by default
  emitter.on("tick", () => {
    console.log(ctx.get("x"));
  });
  ctx.set({ x: 2 });
});

emitter.emit("tick"); // prints 2

If you want snapshot semantics, compute a snapshot store and call ctx.bind(fn, { live: true }) on that snapshot, or copy the store before binding.

Error handling

ctx.use() throws with a helpful message when called outside of a running context scope. Prefer ctx.get() when you want an optional access pattern.

Testing

This repo uses Vitest. Run tests with:

npm test

TypeScript config

No special configuration is required. The package ships ESM/CJS builds and type definitions.

License

MIT