@nwire/hooks 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alex Gefter / 200apps Ltd.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # @nwire/hooks
2
+
3
+ > Universal dispatch primitive. One `hook()` accepts both `.use()` (sequential chain) and `.on()` (parallel listener) attachments.
4
+
5
+ ## What it is
6
+
7
+ Every middleware, lifecycle phase, event subscription, and plugin attachment in Nwire is built from one primitive: a named hook that runs a koa-compose chain first, then a parallel listener fan-out via `Promise.allSettled`. Built on [emittery](https://github.com/sindresorhus/emittery) (~3KB) plus a ~30 LOC composer. No other Nwire package required.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pnpm add @nwire/hooks
13
+ ```
14
+
15
+ ## Standalone use
16
+
17
+ For developers building a framework, a lifecycle host, or any system that needs both wrappers (tracing, auth, retry) AND observers (analytics, metrics) on the same signal.
18
+
19
+ ```ts
20
+ import { hook } from "@nwire/hooks";
21
+
22
+ interface RequestCtx {
23
+ url: string;
24
+ startTime?: number;
25
+ duration?: number;
26
+ error?: unknown;
27
+ }
28
+
29
+ const request = hook<RequestCtx>("http.request");
30
+
31
+ // MIDDLEWARE — sequential, can wrap, can short-circuit
32
+ request.use(async (ctx, next) => {
33
+ ctx.startTime = Date.now();
34
+ await next();
35
+ ctx.duration = Date.now() - ctx.startTime;
36
+ });
37
+
38
+ // LISTENER — parallel observer, fires after the chain
39
+ request.on(async (ctx) => {
40
+ await analytics.track(ctx);
41
+ });
42
+
43
+ await request.run({ url: "/hello" });
44
+ // 1. .use() chain in insertion order (each can next() or short-circuit)
45
+ // 2. .on() listeners in parallel via Promise.allSettled
46
+ // 3. If chain throws, ctx.error is set BEFORE listeners fire
47
+ ```
48
+
49
+ ## Within nwire-app
50
+
51
+ For developers using this package as part of the Nwire stack. You usually never call `hook()` yourself — `@nwire/app` lifecycle, `@nwire/forge` events, and every plugin's middleware all build on it. Touch this package when authoring a plugin that needs custom hooks.
52
+
53
+ ```ts
54
+ import { createHooks, hook } from "@nwire/hooks";
55
+
56
+ const lifecycle = createHooks({
57
+ boot: hook<EndpointCtx>("endpoint.boot"),
58
+ ready: hook<EndpointCtx>("endpoint.ready"),
59
+ shutdown: hook<EndpointCtx>("endpoint.shutdown"),
60
+ });
61
+
62
+ // Apply a plugin object — same hook can take chains AND listeners:
63
+ lifecycle.use({
64
+ boot: async (ctx, next) => {
65
+ logger.info("booting…");
66
+ await next();
67
+ },
68
+ shutdown: { on: (ctx) => logger.info("shut down", ctx) },
69
+ });
70
+ ```
71
+
72
+ ## The contract — the matrix
73
+
74
+ | Question | Behavior |
75
+ | ------------------------------------------------ | ------------------------------------------- |
76
+ | `.on()` runs if chain short-circuits (no throw)? | Yes — listeners observe outcome |
77
+ | `.on()` runs if chain throws? | Yes — `ctx.error` is set first |
78
+ | Can listeners mutate ctx? | No — `.on()` types ctx as `Readonly<Ctx>` |
79
+ | Listeners awaited by `.run()`? | Yes — parallel, `Promise.allSettled` |
80
+ | Listener throws → fails chain? | No — collected, routed to `onListenerError` |
81
+ | Listener ordering | Unordered (parallel) |
82
+ | Chain ordering | Insertion order (koa-compose) |
83
+
84
+ Escape hatch: `hook(name, { strictListeners: true })` re-throws the first listener error instead of routing it through `onListenerError`.
85
+
86
+ ## API
87
+
88
+ - `hook<Ctx>(name, options?)` — create one hook.
89
+ - `Hook<Ctx>.use(fn)` / `.on(fn)` / `.off(fn)` / `.run(ctx)`.
90
+ - `createHooks(record)` — typed bundle with `.hooks` + `.use(plugin)`.
91
+ - `compose(fns)` / `pipe(...fns)` / `withTimeout(ms, fn)` / `withRetry(opts, fn)` — composition helpers.
92
+
93
+ ## See also
94
+
95
+ - [Architecture sketch §06 — Hooks, the universal dispatch primitive](../../architecture-sketch.html#hooks)
96
+ - [Architecture sketch §07 — Hook contract matrix](../../architecture-sketch.html#hook-contract)
97
+ - Built on [`emittery`](https://github.com/sindresorhus/emittery)
98
+ - Sibling packages: [@nwire/endpoint](../nwire-endpoint), [@nwire/container](../nwire-container), [@nwire/app](../nwire-app)
@@ -0,0 +1,16 @@
1
+ /**
2
+ * koa-compose-shaped chain composer. ~30 LOC.
3
+ *
4
+ * Insertion order. Each fn wraps the next; first registered runs outermost.
5
+ * A fn that returns without calling `next()` short-circuits the rest of the
6
+ * chain — that's the outcome listeners observe.
7
+ *
8
+ * See architecture-sketch.html §07 (Chain ordering = insertion order).
9
+ */
10
+ import type { ChainFn } from "./types.js";
11
+ /**
12
+ * Compose an ordered list of chain functions into a single chain function.
13
+ * Throws if a middleware calls `next()` more than once (koa convention).
14
+ */
15
+ export declare function compose<Ctx>(fns: ReadonlyArray<ChainFn<Ctx>>): ChainFn<Ctx>;
16
+ //# sourceMappingURL=compose.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC;;;GAGG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAgB3E"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * koa-compose-shaped chain composer. ~30 LOC.
3
+ *
4
+ * Insertion order. Each fn wraps the next; first registered runs outermost.
5
+ * A fn that returns without calling `next()` short-circuits the rest of the
6
+ * chain — that's the outcome listeners observe.
7
+ *
8
+ * See architecture-sketch.html §07 (Chain ordering = insertion order).
9
+ */
10
+ /**
11
+ * Compose an ordered list of chain functions into a single chain function.
12
+ * Throws if a middleware calls `next()` more than once (koa convention).
13
+ */
14
+ export function compose(fns) {
15
+ return function composed(ctx, finalNext) {
16
+ let lastIndex = -1;
17
+ const dispatch = async (i) => {
18
+ if (i <= lastIndex) {
19
+ throw new Error("next() called multiple times in @nwire/hooks chain");
20
+ }
21
+ lastIndex = i;
22
+ const fn = i === fns.length ? finalNext : fns[i];
23
+ if (!fn)
24
+ return;
25
+ await fn(ctx, () => dispatch(i + 1));
26
+ };
27
+ return dispatch(0);
28
+ };
29
+ }
30
+ //# sourceMappingURL=compose.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compose.js","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH;;;GAGG;AACH,MAAM,UAAU,OAAO,CAAM,GAAgC;IAC3D,OAAO,SAAS,QAAQ,CAAC,GAAG,EAAE,SAAS;QACrC,IAAI,SAAS,GAAG,CAAC,CAAC,CAAC;QAEnB,MAAM,QAAQ,GAAG,KAAK,EAAE,CAAS,EAAiB,EAAE;YAClD,IAAI,CAAC,IAAI,SAAS,EAAE,CAAC;gBACnB,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;YACxE,CAAC;YACD,SAAS,GAAG,CAAC,CAAC;YACd,MAAM,EAAE,GAAG,CAAC,KAAK,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACjD,IAAI,CAAC,EAAE;gBAAE,OAAO;YAChB,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACvC,CAAC,CAAC;QAEF,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * `createHooks()` — typed host for a group of named hooks.
3
+ *
4
+ * Exposes the original `.hooks` record (so consumers can spread it into
5
+ * extended hosts: `createHooks({ ...lifecycle.hooks, request: hook(...) })`),
6
+ * and a plain-object `.use(plugin)` DX with full autocomplete on hook names.
7
+ *
8
+ * See architecture-sketch.html §06 ("Each consumer wires its own hooks").
9
+ */
10
+ import type { Hook } from "./hook.js";
11
+ import type { ChainFn, ListenerFn } from "./types.js";
12
+ /** Map of hook name → `Hook<Ctx>` for that hook. */
13
+ export type HookMap = Record<string, Hook<any>>;
14
+ /** Recover the Ctx parameter of a Hook. */
15
+ type CtxOf<H> = H extends Hook<infer C> ? C : never;
16
+ /**
17
+ * Per-hook plugin entry shape. Two convenience overloads:
18
+ * - a single chain fn, OR a list of chain fns (registered in order)
19
+ * - a tagged listener entry: `{ on: fn }` or `{ on: [fn1, fn2] }`
20
+ *
21
+ * Listeners and chains can coexist:
22
+ * { request: [chain1, chain2, { on: listener }] }
23
+ */
24
+ export type PluginEntry<Ctx> = ChainFn<Ctx> | {
25
+ on: ListenerFn<Ctx> | ReadonlyArray<ListenerFn<Ctx>>;
26
+ } | ReadonlyArray<ChainFn<Ctx> | {
27
+ on: ListenerFn<Ctx> | ReadonlyArray<ListenerFn<Ctx>>;
28
+ }>;
29
+ /** A plugin object keyed by hook name. All entries optional. */
30
+ export type Plugin<T extends HookMap> = {
31
+ [K in keyof T]?: PluginEntry<CtxOf<T[K]>>;
32
+ };
33
+ /** The typed host returned by {@link createHooks}. */
34
+ export interface Host<T extends HookMap> {
35
+ /** The underlying hook record. Spread to compose hosts. */
36
+ readonly hooks: T;
37
+ /** Apply a plugin object across multiple hooks at once. */
38
+ use(plugin: Plugin<T>): this;
39
+ }
40
+ /**
41
+ * Build a typed host over a record of hooks. Hook names are inferred from
42
+ * the keys; ctx types are inferred per-hook.
43
+ *
44
+ * @example
45
+ * const lifecycle = createHooks({
46
+ * boot: hook<EndpointCtx>("endpoint.boot"),
47
+ * shutdown: hook<EndpointCtx>("endpoint.shutdown"),
48
+ * });
49
+ * lifecycle.use({
50
+ * boot: async (ctx, next) => { console.log("booting…"); await next(); },
51
+ * shutdown: { on: (ctx) => console.log("shut down") },
52
+ * });
53
+ */
54
+ export declare function createHooks<T extends HookMap>(hooks: T): Host<T>;
55
+ export {};
56
+ //# sourceMappingURL=create-hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-hooks.d.ts","sourceRoot":"","sources":["../src/create-hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEnD,oDAAoD;AACpD,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAEhD,2CAA2C;AAC3C,KAAK,KAAK,CAAC,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAEpD;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,CAAC,GAAG,IACvB,OAAO,CAAC,GAAG,CAAC,GACZ;IAAE,EAAE,EAAE,UAAU,CAAC,GAAG,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAA;CAAE,GACxD,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG;IAAE,EAAE,EAAE,UAAU,CAAC,GAAG,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAA;CAAE,CAAC,CAAC;AAE3F,gEAAgE;AAChE,MAAM,MAAM,MAAM,CAAC,CAAC,SAAS,OAAO,IAAI;KACrC,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAC1C,CAAC;AAEF,sDAAsD;AACtD,MAAM,WAAW,IAAI,CAAC,CAAC,SAAS,OAAO;IACrC,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;IAClB,2DAA2D;IAC3D,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;CAC9B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,OAAO,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAehE"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * `createHooks()` — typed host for a group of named hooks.
3
+ *
4
+ * Exposes the original `.hooks` record (so consumers can spread it into
5
+ * extended hosts: `createHooks({ ...lifecycle.hooks, request: hook(...) })`),
6
+ * and a plain-object `.use(plugin)` DX with full autocomplete on hook names.
7
+ *
8
+ * See architecture-sketch.html §06 ("Each consumer wires its own hooks").
9
+ */
10
+ /**
11
+ * Build a typed host over a record of hooks. Hook names are inferred from
12
+ * the keys; ctx types are inferred per-hook.
13
+ *
14
+ * @example
15
+ * const lifecycle = createHooks({
16
+ * boot: hook<EndpointCtx>("endpoint.boot"),
17
+ * shutdown: hook<EndpointCtx>("endpoint.shutdown"),
18
+ * });
19
+ * lifecycle.use({
20
+ * boot: async (ctx, next) => { console.log("booting…"); await next(); },
21
+ * shutdown: { on: (ctx) => console.log("shut down") },
22
+ * });
23
+ */
24
+ export function createHooks(hooks) {
25
+ const host = {
26
+ hooks,
27
+ use(plugin) {
28
+ for (const key of Object.keys(plugin)) {
29
+ const entry = plugin[key];
30
+ if (entry === undefined)
31
+ continue;
32
+ const target = hooks[key];
33
+ if (!target)
34
+ continue;
35
+ applyEntry(target, entry);
36
+ }
37
+ return host;
38
+ },
39
+ };
40
+ return host;
41
+ }
42
+ function applyEntry(target, entry) {
43
+ if (Array.isArray(entry)) {
44
+ for (const sub of entry)
45
+ applyEntry(target, sub);
46
+ return;
47
+ }
48
+ if (typeof entry === "function") {
49
+ target.use(entry);
50
+ return;
51
+ }
52
+ if (entry && typeof entry === "object" && "on" in entry) {
53
+ const onFn = entry.on;
54
+ if (Array.isArray(onFn)) {
55
+ for (const fn of onFn)
56
+ target.on(fn);
57
+ }
58
+ else if (typeof onFn === "function") {
59
+ target.on(onFn);
60
+ }
61
+ }
62
+ }
63
+ //# sourceMappingURL=create-hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-hooks.js","sourceRoot":"","sources":["../src/create-hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAqCH;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,WAAW,CAAoB,KAAQ;IACrD,MAAM,IAAI,GAAY;QACpB,KAAK;QACL,GAAG,CAAC,MAAM;YACR,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAmB,EAAE,CAAC;gBACxD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;gBAC1B,IAAI,KAAK,KAAK,SAAS;oBAAE,SAAS;gBAClC,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;gBAC1B,IAAI,CAAC,MAAM;oBAAE,SAAS;gBACtB,UAAU,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;YAC5B,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;KACF,CAAC;IACF,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,UAAU,CAAM,MAAiB,EAAE,KAAuB;IACjE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,KAAK,MAAM,GAAG,IAAI,KAAK;YAAE,UAAU,CAAC,MAAM,EAAE,GAAuB,CAAC,CAAC;QACrE,OAAO;IACT,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,UAAU,EAAE,CAAC;QAChC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAClB,OAAO;IACT,CAAC;IACD,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,IAAI,IAAI,KAAK,EAAE,CAAC;QACxD,MAAM,IAAI,GAAG,KAAK,CAAC,EAAE,CAAC;QACtB,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACxB,KAAK,MAAM,EAAE,IAAI,IAAI;gBAAE,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QACvC,CAAC;aAAM,IAAI,OAAO,IAAI,KAAK,UAAU,EAAE,CAAC;YACtC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;QAClB,CAAC;IACH,CAAC;AACH,CAAC"}
package/dist/hook.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The core `hook()` factory and Hook class.
3
+ *
4
+ * One hook accepts BOTH `.use()` (chain) and `.on()` (parallel listener)
5
+ * attachments. `.run()` fires the chain first (sequential, can short-circuit),
6
+ * then fires listeners in parallel via Promise.allSettled.
7
+ *
8
+ * Behavior matrix is locked by architecture-sketch.html §07.
9
+ */
10
+ import type { ChainFn, HookOptions, ListenerFn } from "./types.js";
11
+ /** The public Hook surface. See architecture-sketch.html §06. */
12
+ export interface Hook<Ctx> {
13
+ /** Hook name. Stable for logging + telemetry. */
14
+ readonly name: string;
15
+ /** Attach a chain middleware (sequential, can short-circuit). */
16
+ use(fn: ChainFn<Ctx>): this;
17
+ /** Attach a listener (parallel, observes final ctx, cannot mutate). */
18
+ on(fn: ListenerFn<Ctx>): this;
19
+ /** Detach a previously attached `.use()` or `.on()` fn. No-op if unknown. */
20
+ off(fn: ChainFn<Ctx> | ListenerFn<Ctx>): this;
21
+ /** Run the chain, then fire listeners. Returns the (possibly mutated) ctx. */
22
+ run(ctx: Ctx): Promise<Ctx>;
23
+ }
24
+ /**
25
+ * Create a new hook.
26
+ *
27
+ * @example
28
+ * const request = hook<RequestCtx>("http.request");
29
+ * request.use(async (ctx, next) => { ctx.start = Date.now(); await next(); });
30
+ * request.on(async (ctx) => { await analytics.track(ctx); });
31
+ * await request.run(ctx);
32
+ */
33
+ export declare function hook<Ctx>(name: string, options?: HookOptions<Ctx>): Hook<Ctx>;
34
+ //# sourceMappingURL=hook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook.d.ts","sourceRoot":"","sources":["../src/hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAWhE,iEAAiE;AACjE,MAAM,WAAW,IAAI,CAAC,GAAG;IACvB,iDAAiD;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,iEAAiE;IACjE,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IAE5B,uEAAuE;IACvE,EAAE,CAAC,EAAE,EAAE,UAAU,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IAE9B,6EAA6E;IAC7E,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,UAAU,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IAE9C,8EAA8E;IAC9E,GAAG,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,wBAAgB,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,WAAW,CAAC,GAAG,CAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAEjF"}
package/dist/hook.js ADDED
@@ -0,0 +1,148 @@
1
+ /**
2
+ * The core `hook()` factory and Hook class.
3
+ *
4
+ * One hook accepts BOTH `.use()` (chain) and `.on()` (parallel listener)
5
+ * attachments. `.run()` fires the chain first (sequential, can short-circuit),
6
+ * then fires listeners in parallel via Promise.allSettled.
7
+ *
8
+ * Behavior matrix is locked by architecture-sketch.html §07.
9
+ */
10
+ import Emittery from "emittery";
11
+ import { compose } from "./compose.js";
12
+ /**
13
+ * Internal emittery event name. We piggy-back on emittery for typed
14
+ * subscription bookkeeping (and as the documented foundation of @nwire/hooks),
15
+ * but we iterate listeners ourselves via a parallel Set so we can apply the
16
+ * contract-mandated `Promise.allSettled` semantics — emittery's `.emit()`
17
+ * uses Promise.all which fails fast on the first rejection.
18
+ */
19
+ const RUN_EVENT = "__nwire_hooks_run__";
20
+ /**
21
+ * Create a new hook.
22
+ *
23
+ * @example
24
+ * const request = hook<RequestCtx>("http.request");
25
+ * request.use(async (ctx, next) => { ctx.start = Date.now(); await next(); });
26
+ * request.on(async (ctx) => { await analytics.track(ctx); });
27
+ * await request.run(ctx);
28
+ */
29
+ export function hook(name, options = {}) {
30
+ return new HookImpl(name, options);
31
+ }
32
+ class HookImpl {
33
+ name;
34
+ chain = [];
35
+ /** Iteration store — we run these in parallel via Promise.allSettled. */
36
+ listeners = new Set();
37
+ /** Bookkeeping mirror — emittery is the documented foundation. */
38
+ emitter = new Emittery();
39
+ /** Map user fn → emittery-shaped adapter, for `.off()`. */
40
+ adapters = new WeakMap();
41
+ strictListeners;
42
+ onListenerError;
43
+ constructor(name, options) {
44
+ this.name = name;
45
+ this.strictListeners = options.strictListeners === true;
46
+ this.onListenerError =
47
+ options.onListenerError ??
48
+ ((err, _ctx, hookName) => {
49
+ // Conservative default — log + continue. Hosts (createHooks)
50
+ // override this to route through their `lifecycle.error` hook.
51
+ // eslint-disable-next-line no-console
52
+ console.error(`[@nwire/hooks] listener error in "${hookName}":`, err);
53
+ });
54
+ }
55
+ use(fn) {
56
+ this.chain.push(fn);
57
+ return this;
58
+ }
59
+ on(fn) {
60
+ if (this.listeners.has(fn))
61
+ return this;
62
+ this.listeners.add(fn);
63
+ const adapter = async (ctx) => {
64
+ await fn(ctx);
65
+ };
66
+ this.adapters.set(fn, adapter);
67
+ this.emitter.on(RUN_EVENT, adapter);
68
+ return this;
69
+ }
70
+ off(fn) {
71
+ // Try chain first.
72
+ const chainIdx = this.chain.indexOf(fn);
73
+ if (chainIdx >= 0) {
74
+ this.chain.splice(chainIdx, 1);
75
+ return this;
76
+ }
77
+ // Otherwise treat as listener.
78
+ const listenerFn = fn;
79
+ if (this.listeners.delete(listenerFn)) {
80
+ const adapter = this.adapters.get(listenerFn);
81
+ if (adapter) {
82
+ this.emitter.off(RUN_EVENT, adapter);
83
+ this.adapters.delete(listenerFn);
84
+ }
85
+ }
86
+ return this;
87
+ }
88
+ async run(ctx) {
89
+ // 1. Run the chain. If it throws, capture before listeners fire so they
90
+ // can observe the failure mode via ctx.error.
91
+ const composed = compose(this.chain);
92
+ let chainError = undefined;
93
+ try {
94
+ await composed(ctx, async () => {
95
+ // Tail — nothing to do. The chain itself decides whether to call
96
+ // next() or short-circuit.
97
+ });
98
+ }
99
+ catch (err) {
100
+ chainError = err;
101
+ // Stash on ctx so listeners can observe failure mode. Mutating even
102
+ // when `Ctx` doesn't declare an `error` field is the documented
103
+ // contract (§07 row 2 + row 11).
104
+ ctx.error = err;
105
+ }
106
+ // 2. Fire listeners in parallel via Promise.allSettled — contract row 4.
107
+ if (this.listeners.size > 0) {
108
+ const snapshot = Array.from(this.listeners);
109
+ const results = await Promise.allSettled(snapshot.map(async (listener) => listener(ctx)));
110
+ const failures = results.filter((r) => r.status === "rejected");
111
+ if (failures.length > 0) {
112
+ if (this.strictListeners) {
113
+ // Strict mode: opt-in escape hatch (§07 closing note). Surface
114
+ // the first failure; attach the rest via `cause` for debug.
115
+ const primary = failures[0].reason;
116
+ if (failures.length > 1 &&
117
+ primary instanceof Error &&
118
+ primary.cause === undefined) {
119
+ Object.defineProperty(primary, "cause", {
120
+ value: failures.slice(1).map((f) => f.reason),
121
+ configurable: true,
122
+ writable: true,
123
+ });
124
+ }
125
+ throw primary;
126
+ }
127
+ // Default mode: report each, never fail the run.
128
+ for (const failure of failures) {
129
+ try {
130
+ this.onListenerError(failure.reason, ctx, this.name);
131
+ }
132
+ catch {
133
+ // Reporter itself blew up — swallow. Listener-error reporting
134
+ // must never crash the hook.
135
+ }
136
+ }
137
+ }
138
+ }
139
+ // 3. Propagate chain failure to the caller of `.run()`. Listeners have
140
+ // already observed it via ctx.error; this is the actionable signal
141
+ // for whatever called the hook.
142
+ if (chainError !== undefined) {
143
+ throw chainError;
144
+ }
145
+ return ctx;
146
+ }
147
+ }
148
+ //# sourceMappingURL=hook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook.js","sourceRoot":"","sources":["../src/hook.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @nwire/hooks — the universal dispatch primitive
3
+ *
4
+ * One `hook()` accepts BOTH `.use()` (chain) and `.on()` (parallel listener)
5
+ * attachments. Chain runs first (sequential, can short-circuit), then listeners
6
+ * fire in parallel via Promise.allSettled. See architecture-sketch.html §06–§07.
7
+ */
8
+ export { hook, type Hook } from "./hook.js";
9
+ export { createHooks, type Host, type HookMap, type Plugin, type PluginEntry } from "./create-hooks.js";
10
+ export { compose } from "./compose.js";
11
+ export { pipe, withTimeout, withRetry } from "./pipe.js";
12
+ export type { ChainFn, HookOptions, ListenerFn, RetryOpts } from "./types.js";
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,IAAI,EAAE,KAAK,IAAI,EAAE,MAAM,QAAQ,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,KAAK,IAAI,EAAE,KAAK,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AACrG,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACtD,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @nwire/hooks — the universal dispatch primitive
3
+ *
4
+ * One `hook()` accepts BOTH `.use()` (chain) and `.on()` (parallel listener)
5
+ * attachments. Chain runs first (sequential, can short-circuit), then listeners
6
+ * fire in parallel via Promise.allSettled. See architecture-sketch.html §06–§07.
7
+ */
8
+ export { hook } from "./hook.js";
9
+ export { createHooks } from "./create-hooks.js";
10
+ export { compose } from "./compose.js";
11
+ export { pipe, withTimeout, withRetry } from "./pipe.js";
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,IAAI,EAAa,MAAM,QAAQ,CAAC;AACzC,OAAO,EAAE,WAAW,EAA0D,MAAM,gBAAgB,CAAC;AACrG,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC"}
package/dist/pipe.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Composition helpers for chain functions.
3
+ *
4
+ * `pipe(...fns)` — compose multiple chain fns into one, koa-compose style.
5
+ * `withTimeout(ms, fn)` — wrap a chain fn so it rejects if it takes too long.
6
+ * `withRetry(opts, fn)` — wrap a chain fn so failures retry per the policy.
7
+ *
8
+ * These return `ChainFn<Ctx>` so they slot into `.use()` directly.
9
+ *
10
+ * See architecture-sketch.html §06.
11
+ */
12
+ import type { ChainFn, RetryOpts } from "./types.js";
13
+ /** Compose multiple chain fns into one. Insertion order. */
14
+ export declare function pipe<Ctx>(...fns: ChainFn<Ctx>[]): ChainFn<Ctx>;
15
+ /**
16
+ * Wrap a chain fn so it rejects with `Error("hook timeout: <ms>ms")` if it
17
+ * doesn't resolve in time. The downstream fn keeps running — there is no
18
+ * cancellation in JS — but the chain moves on.
19
+ */
20
+ export declare function withTimeout<Ctx>(ms: number, fn: ChainFn<Ctx>): ChainFn<Ctx>;
21
+ /**
22
+ * Wrap a chain fn so it retries on rejection per the policy. `next()` may
23
+ * be invoked more than once across retries — that's intentional; users
24
+ * who want next-only-once semantics should keep `next()` outside the
25
+ * retried fn (compose around it).
26
+ */
27
+ export declare function withRetry<Ctx>(opts: RetryOpts, fn: ChainFn<Ctx>): ChainFn<Ctx>;
28
+ //# sourceMappingURL=pipe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipe.d.ts","sourceRoot":"","sources":["../src/pipe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAElD,4DAA4D;AAC5D,wBAAgB,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,CAE9D;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAiB3E;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CA0B9E"}
package/dist/pipe.js ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Composition helpers for chain functions.
3
+ *
4
+ * `pipe(...fns)` — compose multiple chain fns into one, koa-compose style.
5
+ * `withTimeout(ms, fn)` — wrap a chain fn so it rejects if it takes too long.
6
+ * `withRetry(opts, fn)` — wrap a chain fn so failures retry per the policy.
7
+ *
8
+ * These return `ChainFn<Ctx>` so they slot into `.use()` directly.
9
+ *
10
+ * See architecture-sketch.html §06.
11
+ */
12
+ import { compose } from "./compose.js";
13
+ /** Compose multiple chain fns into one. Insertion order. */
14
+ export function pipe(...fns) {
15
+ return compose(fns);
16
+ }
17
+ /**
18
+ * Wrap a chain fn so it rejects with `Error("hook timeout: <ms>ms")` if it
19
+ * doesn't resolve in time. The downstream fn keeps running — there is no
20
+ * cancellation in JS — but the chain moves on.
21
+ */
22
+ export function withTimeout(ms, fn) {
23
+ if (!Number.isFinite(ms) || ms < 0) {
24
+ throw new RangeError(`withTimeout: ms must be a non-negative finite number, got ${ms}`);
25
+ }
26
+ return async function timed(ctx, next) {
27
+ let timer;
28
+ try {
29
+ await Promise.race([
30
+ Promise.resolve(fn(ctx, next)),
31
+ new Promise((_, reject) => {
32
+ timer = setTimeout(() => reject(new Error(`hook timeout: ${ms}ms`)), ms);
33
+ }),
34
+ ]);
35
+ }
36
+ finally {
37
+ if (timer !== undefined)
38
+ clearTimeout(timer);
39
+ }
40
+ };
41
+ }
42
+ /**
43
+ * Wrap a chain fn so it retries on rejection per the policy. `next()` may
44
+ * be invoked more than once across retries — that's intentional; users
45
+ * who want next-only-once semantics should keep `next()` outside the
46
+ * retried fn (compose around it).
47
+ */
48
+ export function withRetry(opts, fn) {
49
+ if (!Number.isInteger(opts.attempts) || opts.attempts < 1) {
50
+ throw new RangeError(`withRetry: attempts must be a positive integer, got ${opts.attempts}`);
51
+ }
52
+ const delayMs = opts.delayMs ?? 0;
53
+ const shouldRetry = opts.shouldRetry ?? (() => true);
54
+ return async function retried(ctx, next) {
55
+ let lastErr;
56
+ for (let attempt = 1; attempt <= opts.attempts; attempt++) {
57
+ try {
58
+ await fn(ctx, next);
59
+ return;
60
+ }
61
+ catch (err) {
62
+ lastErr = err;
63
+ if (attempt >= opts.attempts || !shouldRetry(err, attempt)) {
64
+ throw err;
65
+ }
66
+ if (delayMs > 0) {
67
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
68
+ }
69
+ }
70
+ }
71
+ // Unreachable — we either returned or threw above.
72
+ throw lastErr;
73
+ };
74
+ }
75
+ //# sourceMappingURL=pipe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipe.js","sourceRoot":"","sources":["../src/pipe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,4DAA4D;AAC5D,MAAM,UAAU,IAAI,CAAM,GAAG,GAAmB;IAC9C,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC;AACtB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAM,EAAU,EAAE,EAAgB;IAC3D,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,UAAU,CAAC,6DAA6D,EAAE,EAAE,CAAC,CAAC;IAC1F,CAAC;IACD,OAAO,KAAK,UAAU,KAAK,CAAC,GAAG,EAAE,IAAI;QACnC,IAAI,KAAgD,CAAC;QACrD,IAAI,CAAC;YACH,MAAM,OAAO,CAAC,IAAI,CAAC;gBACjB,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;gBAC9B,IAAI,OAAO,CAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE;oBAC/B,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,iBAAiB,EAAE,IAAI,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBAC3E,CAAC,CAAC;aACH,CAAC,CAAC;QACL,CAAC;gBAAS,CAAC;YACT,IAAI,KAAK,KAAK,SAAS;gBAAE,YAAY,CAAC,KAAK,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAM,IAAe,EAAE,EAAgB;IAC9D,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,GAAG,CAAC,EAAE,CAAC;QAC1D,MAAM,IAAI,UAAU,CAAC,uDAAuD,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC/F,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC;IAClC,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IAErD,OAAO,KAAK,UAAU,OAAO,CAAC,GAAG,EAAE,IAAI;QACrC,IAAI,OAAgB,CAAC;QACrB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,CAAC;YAC1D,IAAI,CAAC;gBACH,MAAM,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;gBACpB,OAAO;YACT,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,GAAG,GAAG,CAAC;gBACd,IAAI,OAAO,IAAI,IAAI,CAAC,QAAQ,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,CAAC;oBAC3D,MAAM,GAAG,CAAC;gBACZ,CAAC;gBACD,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;oBAChB,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;gBAC/D,CAAC;YACH,CAAC;QACH,CAAC;QACD,mDAAmD;QACnD,MAAM,OAAO,CAAC;IAChB,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Public types for `@nwire/hooks`. See architecture-sketch.html §06–§07.
3
+ */
4
+ /** A single link in a chain — koa-compose-shaped middleware. */
5
+ export type ChainFn<Ctx> = (ctx: Ctx, next: () => Promise<void>) => Promise<void> | void;
6
+ /** A listener — observes the final ctx state, never mutates it. */
7
+ export type ListenerFn<Ctx> = (ctx: Readonly<Ctx>) => Promise<void> | void;
8
+ /** Options for {@link createHook}/{@link hook}. */
9
+ export interface HookOptions<Ctx> {
10
+ /**
11
+ * If true, listener errors are re-thrown by `.run()` instead of being
12
+ * collected and surfaced via `onListenerError`. Default: false (conservative).
13
+ */
14
+ strictListeners?: boolean;
15
+ /**
16
+ * Called once per listener error. Defaults to `console.error`. A host
17
+ * (e.g. `createHooks(...)`) wires its `lifecycle.error` hook here.
18
+ */
19
+ onListenerError?: (err: unknown, ctx: Readonly<Ctx>, hookName: string) => void;
20
+ }
21
+ /** Retry policy for {@link withRetry}. */
22
+ export interface RetryOpts {
23
+ /** Total attempts (including the first). Must be >= 1. */
24
+ attempts: number;
25
+ /** Optional delay between attempts in ms (constant). Default: 0. */
26
+ delayMs?: number;
27
+ /** Optional predicate — return false to stop retrying a given error. */
28
+ shouldRetry?: (err: unknown, attempt: number) => boolean;
29
+ }
30
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,gEAAgE;AAChE,MAAM,MAAM,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAEzF,mEAAmE;AACnE,MAAM,MAAM,UAAU,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAE3E,mDAAmD;AACnD,MAAM,WAAW,WAAW,CAAC,GAAG;IAC9B;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CAChF;AAED,0CAA0C;AAC1C,MAAM,WAAW,SAAS;IACxB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wEAAwE;IACxE,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;CAC1D"}
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Public types for `@nwire/hooks`. See architecture-sketch.html §06–§07.
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@nwire/hooks",
3
+ "version": "0.7.0",
4
+ "description": "Nwire — the universal dispatch primitive. One hook() accepts both .use() (sequential koa-compose chain) and .on() (parallel listener) attachments. Built on emittery + a ~30 LOC composer. Standalone, in-process only, ~100 LOC of surface.",
5
+ "keywords": [
6
+ "dispatch",
7
+ "events",
8
+ "hooks",
9
+ "koa-compose",
10
+ "lifecycle",
11
+ "middleware",
12
+ "nwire"
13
+ ],
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "type": "module",
19
+ "main": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "import": "./dist/index.js",
24
+ "types": "./dist/index.d.ts"
25
+ }
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "dependencies": {
31
+ "emittery": "1.0.1"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^22.19.9",
35
+ "typescript": "^5.9.3",
36
+ "vitest": "^4.0.18"
37
+ },
38
+ "scripts": {
39
+ "build": "tsc && node ../../scripts/fix-dist-extensions.mjs dist",
40
+ "dev": "tsc --watch",
41
+ "typecheck": "tsc --noEmit",
42
+ "test": "vitest run"
43
+ }
44
+ }