@nwire/hooks 0.7.1 → 0.8.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/README.md +663 -33
- package/dist/hook.d.ts +32 -15
- package/dist/hook.d.ts.map +1 -1
- package/dist/hook.js +196 -96
- package/dist/hook.js.map +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/dist/record.d.ts +50 -0
- package/dist/record.d.ts.map +1 -0
- package/dist/record.js +86 -0
- package/dist/record.js.map +1 -0
- package/dist/registry.d.ts +54 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +103 -0
- package/dist/registry.js.map +1 -0
- package/dist/types.d.ts +97 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +8 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/record.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recording + replay.
|
|
3
|
+
*
|
|
4
|
+
* Recording a hook run produces a {@link Recording} — the full step trace
|
|
5
|
+
* plus user-supplied ctx snapshots taken before and after. Replays the
|
|
6
|
+
* recording later (or in another process) and assert step-by-step shape
|
|
7
|
+
* matches. This is the substrate Studio's "replay this trace" button and
|
|
8
|
+
* the deterministic debug flow are built on.
|
|
9
|
+
*
|
|
10
|
+
* Two scopes:
|
|
11
|
+
*
|
|
12
|
+
* record(hook, ctx, opts?) one-shot: run the hook, snapshot ctx, return Recording
|
|
13
|
+
* replay(hook, recording, opts) re-run with the recorded ctxIn; compare shapes
|
|
14
|
+
*
|
|
15
|
+
* The clone strategy is pluggable; default is `structuredClone`. Pass
|
|
16
|
+
* `clone: (x) => JSON.parse(JSON.stringify(x))` for non-clonable shapes.
|
|
17
|
+
*/
|
|
18
|
+
const defaultClone = (ctx) => {
|
|
19
|
+
if (typeof structuredClone === "function")
|
|
20
|
+
return structuredClone(ctx);
|
|
21
|
+
// Last-resort fallback for environments without structuredClone.
|
|
22
|
+
return JSON.parse(JSON.stringify(ctx));
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Run the hook once, capture ctx-in/out + every observation. Result is a
|
|
26
|
+
* self-contained {@link Recording} that can be persisted (JSON-safe if the
|
|
27
|
+
* caller's clone yields JSON-safe shapes), shipped to Studio, or fed back
|
|
28
|
+
* into `replay()`.
|
|
29
|
+
*/
|
|
30
|
+
export async function record(hook, ctx, opts = {}) {
|
|
31
|
+
const clone = opts.clone ?? defaultClone;
|
|
32
|
+
const ctxIn = clone(ctx);
|
|
33
|
+
const result = await hook.runDetailed(ctx, opts);
|
|
34
|
+
const ctxOut = clone(result.ctx);
|
|
35
|
+
return {
|
|
36
|
+
hookId: "id" in hook ? hook.id : "unknown",
|
|
37
|
+
hookName: hook.name,
|
|
38
|
+
runId: result.runId,
|
|
39
|
+
outcome: result.outcome,
|
|
40
|
+
steps: result.steps,
|
|
41
|
+
ctxIn,
|
|
42
|
+
ctxOut,
|
|
43
|
+
capturedAt: new Date().toISOString(),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Replay a recording. Re-runs the hook with `recording.ctxIn` (cloned),
|
|
48
|
+
* compares the resulting step sequence + outcome against the recording,
|
|
49
|
+
* and reports drift. This is observational — it does not stub side effects.
|
|
50
|
+
* Pure / idempotent chains should match; non-deterministic chains report
|
|
51
|
+
* their drift so a human can decide whether it's expected.
|
|
52
|
+
*/
|
|
53
|
+
export async function replay(hook, recording, opts = {}) {
|
|
54
|
+
const clone = opts.clone ?? defaultClone;
|
|
55
|
+
const inputCtx = clone(recording.ctxIn);
|
|
56
|
+
const result = await hook.runDetailed(inputCtx, opts);
|
|
57
|
+
const drift = [];
|
|
58
|
+
if (result.outcome !== recording.outcome) {
|
|
59
|
+
drift.push(`outcome: recorded=${recording.outcome} replayed=${result.outcome}`);
|
|
60
|
+
}
|
|
61
|
+
if (result.steps.length !== recording.steps.length) {
|
|
62
|
+
drift.push(`step count: recorded=${recording.steps.length} replayed=${result.steps.length}`);
|
|
63
|
+
}
|
|
64
|
+
// Compare step sequences by (stepKind, stepName, phase). Durations + ts
|
|
65
|
+
// are inherently non-deterministic — we exclude them from the compare.
|
|
66
|
+
const lim = Math.min(result.steps.length, recording.steps.length);
|
|
67
|
+
for (let i = 0; i < lim; i++) {
|
|
68
|
+
const r = recording.steps[i];
|
|
69
|
+
const p = result.steps[i];
|
|
70
|
+
if (r.stepKind !== p.stepKind || r.stepName !== p.stepName || r.phase !== p.phase) {
|
|
71
|
+
drift.push(`step[${i}]: recorded=${r.stepKind}/${r.stepName ?? ""}/${r.phase} ` +
|
|
72
|
+
`replayed=${p.stepKind}/${p.stepName ?? ""}/${p.phase}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
matches: drift.length === 0,
|
|
77
|
+
drift,
|
|
78
|
+
recorded: recording,
|
|
79
|
+
replayed: {
|
|
80
|
+
ctx: result.ctx,
|
|
81
|
+
stepCount: result.steps.length,
|
|
82
|
+
outcome: result.outcome,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=record.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record.js","sourceRoot":"","sources":["../src/record.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-wide registry + topology helpers.
|
|
3
|
+
*
|
|
4
|
+
* Everything that needs to be ambient — the "current run" context that
|
|
5
|
+
* lets nested `.run()` calls link as children, plus the introspection
|
|
6
|
+
* list `nwire scan` and Studio consume — lives here. One module, no
|
|
7
|
+
* deps beyond `node:async_hooks` (stdlib).
|
|
8
|
+
*/
|
|
9
|
+
import type { Hook } from "./hook.js";
|
|
10
|
+
import type { SourceLocation } from "./types.js";
|
|
11
|
+
/** Internal — called by `hook()` after instantiation. */
|
|
12
|
+
export declare function registerHook(hook: Hook<unknown>, source?: SourceLocation): void;
|
|
13
|
+
/**
|
|
14
|
+
* Metadata for every hook created in this process. Scan + Studio call
|
|
15
|
+
* this to enumerate the world; it returns a snapshot, mutating it has
|
|
16
|
+
* no effect on the registry.
|
|
17
|
+
*/
|
|
18
|
+
export declare function listHooks(): ReadonlyArray<{
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly chain: number;
|
|
22
|
+
readonly listeners: number;
|
|
23
|
+
readonly source?: SourceLocation;
|
|
24
|
+
}>;
|
|
25
|
+
/** Clear the registry. Test-only — never call in production paths. */
|
|
26
|
+
export declare function __resetRegistryForTests(): void;
|
|
27
|
+
/**
|
|
28
|
+
* Walk the V8 stack, skip frames that originate inside `@nwire/*` packages,
|
|
29
|
+
* return the first user-code frame. `skipFrames` lets call sites that are
|
|
30
|
+
* one or two helpers deep aim past their own wrapper.
|
|
31
|
+
*/
|
|
32
|
+
export declare function captureSourceLocation(skipFrames?: number): SourceLocation | undefined;
|
|
33
|
+
interface RunContext {
|
|
34
|
+
readonly runId: string;
|
|
35
|
+
readonly hookId: string;
|
|
36
|
+
readonly parentRunId?: string;
|
|
37
|
+
}
|
|
38
|
+
/** Monotonic id for the next `.run()`. Shared across module instances. */
|
|
39
|
+
export declare function nextRunId(): string;
|
|
40
|
+
/** Monotonic id assigned to each `hook()` at construction. */
|
|
41
|
+
export declare function nextHookId(): string;
|
|
42
|
+
/**
|
|
43
|
+
* Run a function with a {@link RunContext} active for the duration. Nested
|
|
44
|
+
* `.run()` calls inside `fn` see this context via {@link currentRun} and
|
|
45
|
+
* pick up `runId` as their `parentRunId`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function withRunContext<T>(ctx: RunContext, fn: () => Promise<T>): Promise<T>;
|
|
48
|
+
/**
|
|
49
|
+
* The active run context, if any. Returns undefined when called outside
|
|
50
|
+
* a hook run — that's the normal case for top-level dispatch.
|
|
51
|
+
*/
|
|
52
|
+
export declare function currentRun(): RunContext | undefined;
|
|
53
|
+
export {};
|
|
54
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AA6B9C,yDAAyD;AACzD,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,EAAE,cAAc,GAAG,IAAI,CAE/E;AAED;;;;GAIG;AACH,wBAAgB,SAAS,IAAI,aAAa,CAAC;IACzC,QAAQ,CAAC,EAAE,EAAQ,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAM,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAK,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAC,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAG,cAAc,CAAC;CACnC,CAAC,CAQD;AAED,sEAAsE;AACtE,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C;AAaD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,SAAI,GAAG,cAAc,GAAG,SAAS,CAehF;AAID,UAAU,UAAU;IAClB,QAAQ,CAAC,KAAK,EAAS,MAAM,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAQ,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAID,0EAA0E;AAC1E,wBAAgB,SAAS,IAAI,MAAM,CAGlC;AAED,8DAA8D;AAC9D,wBAAgB,UAAU,IAAI,MAAM,CAGnC;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAEnF;AAED;;;GAGG;AACH,wBAAgB,UAAU,IAAI,UAAU,GAAG,SAAS,CAEnD"}
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-wide registry + topology helpers.
|
|
3
|
+
*
|
|
4
|
+
* Everything that needs to be ambient — the "current run" context that
|
|
5
|
+
* lets nested `.run()` calls link as children, plus the introspection
|
|
6
|
+
* list `nwire scan` and Studio consume — lives here. One module, no
|
|
7
|
+
* deps beyond `node:async_hooks` (stdlib).
|
|
8
|
+
*/
|
|
9
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
10
|
+
// Pin to globalThis so every instance of @nwire/hooks loaded into this
|
|
11
|
+
// process (Node loader + jiti + tsx + …) shares one registry. Without
|
|
12
|
+
// this, scan-time `listHooks()` ran against a different module graph
|
|
13
|
+
// than user code and saw an empty list.
|
|
14
|
+
const GLOBAL_KEY = "__nwire_hooks_registry__";
|
|
15
|
+
const g = globalThis;
|
|
16
|
+
const slot = (g[GLOBAL_KEY] ??= {
|
|
17
|
+
registry: [],
|
|
18
|
+
hookCounter: { value: 0 },
|
|
19
|
+
runCounter: { value: 0 },
|
|
20
|
+
});
|
|
21
|
+
const registry = slot.registry;
|
|
22
|
+
/** Internal — called by `hook()` after instantiation. */
|
|
23
|
+
export function registerHook(hook, source) {
|
|
24
|
+
registry.push({ hook, source });
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Metadata for every hook created in this process. Scan + Studio call
|
|
28
|
+
* this to enumerate the world; it returns a snapshot, mutating it has
|
|
29
|
+
* no effect on the registry.
|
|
30
|
+
*/
|
|
31
|
+
export function listHooks() {
|
|
32
|
+
return registry.map(({ hook, source }) => ({
|
|
33
|
+
id: hook.id,
|
|
34
|
+
name: hook.name,
|
|
35
|
+
chain: hook.stepCounts().chain,
|
|
36
|
+
listeners: hook.stepCounts().listeners,
|
|
37
|
+
source,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
/** Clear the registry. Test-only — never call in production paths. */
|
|
41
|
+
export function __resetRegistryForTests() {
|
|
42
|
+
registry.length = 0;
|
|
43
|
+
}
|
|
44
|
+
// ─── Source-location capture ──────────────────────────────────────────
|
|
45
|
+
const PAREN_RE = /\(([^()]+):(\d+):(\d+)\)\s*$/;
|
|
46
|
+
const PLAIN_RE = /\s+at\s+([^()\s]+):(\d+):(\d+)\s*$/;
|
|
47
|
+
// Skip frames inside installed @nwire packages (node_modules) and inside
|
|
48
|
+
// framework-source paths — but NOT inside test files under those packages,
|
|
49
|
+
// so package-local tests can assert on captured user-code locations.
|
|
50
|
+
// `[\\/]` accepts both POSIX `/` and Windows `\` separators because V8
|
|
51
|
+
// stack frames emit native path syntax per-platform.
|
|
52
|
+
const NWIRE_FRAME_RE = /(node_modules[\\/](@nwire[\\/]|\.pnpm[\\/]@nwire\+)|packages[\\/]nwire-[^\\/]+[\\/](src|dist)[\\/](?!__tests__[\\/]))/;
|
|
53
|
+
/**
|
|
54
|
+
* Walk the V8 stack, skip frames that originate inside `@nwire/*` packages,
|
|
55
|
+
* return the first user-code frame. `skipFrames` lets call sites that are
|
|
56
|
+
* one or two helpers deep aim past their own wrapper.
|
|
57
|
+
*/
|
|
58
|
+
export function captureSourceLocation(skipFrames = 1) {
|
|
59
|
+
const err = new Error();
|
|
60
|
+
const stack = err.stack;
|
|
61
|
+
if (!stack)
|
|
62
|
+
return undefined;
|
|
63
|
+
const lines = stack.split("\n");
|
|
64
|
+
// First line is "Error" header; the +1 skips it.
|
|
65
|
+
for (let i = skipFrames + 1; i < lines.length; i++) {
|
|
66
|
+
const line = lines[i];
|
|
67
|
+
if (NWIRE_FRAME_RE.test(line))
|
|
68
|
+
continue;
|
|
69
|
+
const m = PAREN_RE.exec(line) ?? PLAIN_RE.exec(line);
|
|
70
|
+
if (!m)
|
|
71
|
+
continue;
|
|
72
|
+
const [, file, lineStr, colStr] = m;
|
|
73
|
+
return { file: file, line: Number(lineStr), column: Number(colStr) };
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const runContext = new AsyncLocalStorage();
|
|
78
|
+
/** Monotonic id for the next `.run()`. Shared across module instances. */
|
|
79
|
+
export function nextRunId() {
|
|
80
|
+
slot.runCounter.value += 1;
|
|
81
|
+
return `r${slot.runCounter.value.toString(36)}-${Date.now().toString(36)}`;
|
|
82
|
+
}
|
|
83
|
+
/** Monotonic id assigned to each `hook()` at construction. */
|
|
84
|
+
export function nextHookId() {
|
|
85
|
+
slot.hookCounter.value += 1;
|
|
86
|
+
return `h${slot.hookCounter.value.toString(36)}`;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Run a function with a {@link RunContext} active for the duration. Nested
|
|
90
|
+
* `.run()` calls inside `fn` see this context via {@link currentRun} and
|
|
91
|
+
* pick up `runId` as their `parentRunId`.
|
|
92
|
+
*/
|
|
93
|
+
export function withRunContext(ctx, fn) {
|
|
94
|
+
return runContext.run(ctx, fn);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The active run context, if any. Returns undefined when called outside
|
|
98
|
+
* a hook run — that's the normal case for top-level dispatch.
|
|
99
|
+
*/
|
|
100
|
+
export function currentRun() {
|
|
101
|
+
return runContext.getStore();
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,23 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Public types for `@nwire/hooks`.
|
|
2
|
+
* Public types for `@nwire/hooks`.
|
|
3
|
+
*
|
|
4
|
+
* The two function shapes:
|
|
5
|
+
* - ChainFn — koa-compose middleware. Wraps next(); can short-circuit.
|
|
6
|
+
* - ListenerFn — parallel observer. Reads the final ctx; never mutates.
|
|
7
|
+
*
|
|
8
|
+
* The observability surface (StepObservation / RunResult / Recording) is the
|
|
9
|
+
* contract every consumer relies on for tracing, scan, Studio, OTel, replay.
|
|
3
10
|
*/
|
|
4
11
|
/** A single link in a chain — koa-compose-shaped middleware. */
|
|
5
12
|
export type ChainFn<Ctx> = (ctx: Ctx, next: () => Promise<void>) => Promise<void> | void;
|
|
6
13
|
/** A listener — observes the final ctx state, never mutates it. */
|
|
7
14
|
export type ListenerFn<Ctx> = (ctx: Readonly<Ctx>) => Promise<void> | void;
|
|
8
|
-
/**
|
|
15
|
+
/** Where this hook (or step) was authored. Captured at attach time. */
|
|
16
|
+
export interface SourceLocation {
|
|
17
|
+
readonly file: string;
|
|
18
|
+
readonly line: number;
|
|
19
|
+
readonly column?: number;
|
|
20
|
+
}
|
|
21
|
+
/** Options for `hook()` / `createHook()`. */
|
|
9
22
|
export interface HookOptions<Ctx> {
|
|
10
23
|
/**
|
|
11
24
|
* If true, listener errors are re-thrown by `.run()` instead of being
|
|
12
|
-
* collected and surfaced via `onListenerError`. Default: false
|
|
25
|
+
* collected and surfaced via `onListenerError`. Default: false.
|
|
13
26
|
*/
|
|
14
27
|
strictListeners?: boolean;
|
|
15
28
|
/**
|
|
16
|
-
* Called once per listener error. Defaults to
|
|
17
|
-
*
|
|
29
|
+
* Called once per listener error. Defaults to console.error. Hosts can
|
|
30
|
+
* route this through their own logger / framework-event bus.
|
|
18
31
|
*/
|
|
19
32
|
onListenerError?: (err: unknown, ctx: Readonly<Ctx>, hookName: string) => void;
|
|
20
33
|
}
|
|
34
|
+
/** Optional per-step metadata supplied at `.use()` / `.on()` time. */
|
|
35
|
+
export interface UseOptions {
|
|
36
|
+
/** Human label — shown in tap events, OTel spans, Studio. */
|
|
37
|
+
readonly name?: string;
|
|
38
|
+
/** Higher runs first (= outermost in chain). Default 0. */
|
|
39
|
+
readonly priority?: number;
|
|
40
|
+
}
|
|
41
|
+
export interface OnOptions extends UseOptions {
|
|
42
|
+
/**
|
|
43
|
+
* Filter when this listener fires relative to the chain outcome.
|
|
44
|
+
* "always" — fire on completed + prevented + failed (default).
|
|
45
|
+
* "success" — fire only when the chain completed without throw.
|
|
46
|
+
* "failure" — fire only when the chain threw.
|
|
47
|
+
*/
|
|
48
|
+
readonly when?: "always" | "success" | "failure";
|
|
49
|
+
}
|
|
21
50
|
/** Retry policy for {@link withRetry}. */
|
|
22
51
|
export interface RetryOpts {
|
|
23
52
|
/** Total attempts (including the first). Must be >= 1. */
|
|
@@ -27,4 +56,67 @@ export interface RetryOpts {
|
|
|
27
56
|
/** Optional predicate — return false to stop retrying a given error. */
|
|
28
57
|
shouldRetry?: (err: unknown, attempt: number) => boolean;
|
|
29
58
|
}
|
|
59
|
+
export type StepKind = "chain" | "listener";
|
|
60
|
+
export type StepPhase = "start" | "end" | "error";
|
|
61
|
+
export type RunOutcome = "completed" | "prevented" | "failed";
|
|
62
|
+
/** One observation emitted to taps. Tagged union by `phase`. */
|
|
63
|
+
export interface StepObservation {
|
|
64
|
+
/** Hook name, stable across runs. */
|
|
65
|
+
readonly hookName: string;
|
|
66
|
+
/** Hook instance id — unique per `hook()` call in this process. */
|
|
67
|
+
readonly hookId: string;
|
|
68
|
+
/** Run id — unique per `.run()` invocation. */
|
|
69
|
+
readonly runId: string;
|
|
70
|
+
/** Parent run id when this `.run()` was started inside another. */
|
|
71
|
+
readonly parentRunId?: string;
|
|
72
|
+
/** Monotonic step id within the hook (insertion order). */
|
|
73
|
+
readonly stepId: number;
|
|
74
|
+
readonly stepKind: StepKind;
|
|
75
|
+
/** Optional human label from {@link UseOptions.name} / {@link OnOptions.name}. */
|
|
76
|
+
readonly stepName?: string;
|
|
77
|
+
readonly phase: StepPhase;
|
|
78
|
+
/** performance.now() timestamp at observation. */
|
|
79
|
+
readonly ts: number;
|
|
80
|
+
/** Set on `end` and `error`. Milliseconds since matching `start`. */
|
|
81
|
+
readonly durationMs?: number;
|
|
82
|
+
/** Set on `error`. */
|
|
83
|
+
readonly error?: unknown;
|
|
84
|
+
}
|
|
85
|
+
/** Subscriber to a hook's per-step observations. */
|
|
86
|
+
export type TapFn = (obs: StepObservation) => void;
|
|
87
|
+
/** Per-run options. */
|
|
88
|
+
export interface RunOptions {
|
|
89
|
+
/** Abort the run. Listeners may inspect; chains may bail mid-flight. */
|
|
90
|
+
readonly signal?: AbortSignal;
|
|
91
|
+
/** Explicit parent. When omitted, picks up the ambient parent. */
|
|
92
|
+
readonly parentRunId?: string;
|
|
93
|
+
}
|
|
94
|
+
/** Detailed result returned by {@link Hook.runDetailed}. */
|
|
95
|
+
export interface RunResult<Ctx> {
|
|
96
|
+
readonly ctx: Ctx;
|
|
97
|
+
readonly outcome: RunOutcome;
|
|
98
|
+
readonly runId: string;
|
|
99
|
+
readonly parentRunId?: string;
|
|
100
|
+
readonly steps: ReadonlyArray<StepObservation>;
|
|
101
|
+
readonly durationMs: number;
|
|
102
|
+
/** Set when outcome === "failed". */
|
|
103
|
+
readonly error?: unknown;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* A recorded run, complete enough to replay step-by-step.
|
|
107
|
+
*
|
|
108
|
+
* `ctxIn` / `ctxOut` are snapshots provided by the caller — the recorder
|
|
109
|
+
* does not serialize ctx itself (ctx shapes are domain-specific). Callers
|
|
110
|
+
* usually pass `structuredClone(ctx)` before and after.
|
|
111
|
+
*/
|
|
112
|
+
export interface Recording {
|
|
113
|
+
readonly hookId: string;
|
|
114
|
+
readonly hookName: string;
|
|
115
|
+
readonly runId: string;
|
|
116
|
+
readonly outcome: RunOutcome;
|
|
117
|
+
readonly steps: ReadonlyArray<StepObservation>;
|
|
118
|
+
readonly ctxIn: unknown;
|
|
119
|
+
readonly ctxOut: unknown;
|
|
120
|
+
readonly capturedAt: string;
|
|
121
|
+
}
|
|
30
122
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Public types for `@nwire/hooks`.
|
|
2
|
+
* Public types for `@nwire/hooks`.
|
|
3
|
+
*
|
|
4
|
+
* The two function shapes:
|
|
5
|
+
* - ChainFn — koa-compose middleware. Wraps next(); can short-circuit.
|
|
6
|
+
* - ListenerFn — parallel observer. Reads the final ctx; never mutates.
|
|
7
|
+
*
|
|
8
|
+
* The observability surface (StepObservation / RunResult / Recording) is the
|
|
9
|
+
* contract every consumer relies on for tracing, scan, Studio, OTel, replay.
|
|
3
10
|
*/
|
|
4
11
|
export {};
|
|
5
12
|
//# sourceMappingURL=types.js.map
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nwire/hooks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
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
5
|
"keywords": [
|
|
6
6
|
"dispatch",
|