@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 +21 -0
- package/README.md +98 -0
- package/dist/compose.d.ts +16 -0
- package/dist/compose.d.ts.map +1 -0
- package/dist/compose.js +30 -0
- package/dist/compose.js.map +1 -0
- package/dist/create-hooks.d.ts +56 -0
- package/dist/create-hooks.d.ts.map +1 -0
- package/dist/create-hooks.js +63 -0
- package/dist/create-hooks.js.map +1 -0
- package/dist/hook.d.ts +34 -0
- package/dist/hook.d.ts.map +1 -0
- package/dist/hook.js +148 -0
- package/dist/hook.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/pipe.d.ts +28 -0
- package/dist/pipe.d.ts.map +1 -0
- package/dist/pipe.js +75 -0
- package/dist/pipe.js.map +1 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +44 -0
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"}
|
package/dist/compose.js
ADDED
|
@@ -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
|
package/dist/hook.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook.js","sourceRoot":"","sources":["../src/hook.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
package/dist/pipe.js.map
ADDED
|
@@ -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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
}
|