Skip to content

@nwire/hooks — alone

The universal dispatch primitive. Two compositions — listeners (parallel, fire-and-forget) and chains (series, can short-circuit) — behind one tense-driven API.

What it does

  • bus.on("EventName", handler) registers a listener.
  • bus.dispatch("EventName", payload, { mode }) invokes them.
  • Four mode choices map to the four common dispatch shapes:
modeSemanticsCommon use
"parallel"Run all listeners concurrently; ignore return values."X has happened" — past-tense events.
"series"Run listeners in order; ignore return values.Ordered side effects (audit log → metric → notify).
"series-bail"Run in order; first thrown error stops the chain."Before X" — guard-style validations.
"chain"Pass the payload through each listener; return the final value.Middleware-style transforms.

Install

bash
pnpm add @nwire/hooks

Use it for your own events

ts
import { createBus } from "@nwire/hooks"

const bus = createBus<{
  "UserSignedUp": { id: string; email: string }
  "UserAboutToDelete": { id: string }
}>()

// Parallel side effects
bus.on("UserSignedUp", async (u) => sendWelcomeEmail(u))
bus.on("UserSignedUp", async (u) => track("signup", { id: u.id }))

await bus.dispatch("UserSignedUp", { id: "u1", email: "a@b.c" }, { mode: "parallel" })

// Series-bail guard
bus.on("UserAboutToDelete", async (u) => {
  if (await hasActiveSubscription(u.id)) throw new Error("Has active subscription")
})

await bus.dispatch("UserAboutToDelete", { id: "u1" }, { mode: "series-bail" })
// throws if the guard fails

Use it for middleware chains

ts
const pipeline = createBus<{ "Request": { url: string; headers: Record<string, string> } }>()

pipeline.on("Request", (req) => ({ ...req, headers: { ...req.headers, "x-trace": uuid() } }))
pipeline.on("Request", (req) => ({ ...req, url: req.url.toLowerCase() }))

const final = await pipeline.dispatch("Request", { url: "/HELLO", headers: {} }, { mode: "chain" })
// → { url: "/hello", headers: { "x-trace": "..." } }

Why this exists

Every framework reinvents the listener half (emittery, EventEmitter, mitt) and the chain half (koa-compose, express middleware, hapi extensions) separately. @nwire/hooks is the smallest surface that covers both, with typed payloads, async by default, and tense-driven dispatch modes that match the way you'd describe the event in English.

What's underneath

  • The listener half wraps emittery.
  • The chain half is ~30 LOC of koa-compose-shaped composition.

No reflect-metadata, no decorators, no global state.

MIT licensed.