@demlik/tea 0.15.0 → 0.16.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.
Files changed (115) hide show
  1. package/README.md +30 -7
  2. package/dist/agent/index.d.ts +49 -37
  3. package/dist/agent/index.js +12 -8
  4. package/dist/chunk-4LPY67E4.js +18 -0
  5. package/dist/chunk-4LPY67E4.js.map +1 -0
  6. package/dist/chunk-5XU6ZJO2.js +533 -0
  7. package/dist/chunk-5XU6ZJO2.js.map +1 -0
  8. package/dist/{chunk-AYS5N6WY.js → chunk-6NG4V5SB.js} +62 -172
  9. package/dist/chunk-6NG4V5SB.js.map +1 -0
  10. package/dist/{chunk-3B5X7SBB.js → chunk-7MGXQZPW.js} +11 -9
  11. package/dist/chunk-7MGXQZPW.js.map +1 -0
  12. package/dist/{chunk-V46HSVCK.js → chunk-BCTEOVOU.js} +3 -3
  13. package/dist/{chunk-V46HSVCK.js.map → chunk-BCTEOVOU.js.map} +1 -1
  14. package/dist/chunk-CH5KTOEZ.js +710 -0
  15. package/dist/chunk-CH5KTOEZ.js.map +1 -0
  16. package/dist/chunk-CIHSHLQX.js +64 -0
  17. package/dist/chunk-CIHSHLQX.js.map +1 -0
  18. package/dist/{chunk-TCLBCERH.js → chunk-EHUDXAKC.js} +238 -212
  19. package/dist/chunk-EHUDXAKC.js.map +1 -0
  20. package/dist/chunk-GOX2CYIM.js +380 -0
  21. package/dist/chunk-GOX2CYIM.js.map +1 -0
  22. package/dist/{chunk-22POWGFA.js → chunk-IVJP3GKJ.js} +46 -20
  23. package/dist/chunk-IVJP3GKJ.js.map +1 -0
  24. package/dist/{chunk-QPEVCJ5Q.js → chunk-MVZS5FDM.js} +13 -6
  25. package/dist/chunk-MVZS5FDM.js.map +1 -0
  26. package/dist/{chunk-VTQI2NHP.js → chunk-MYNLY3HP.js} +9 -8
  27. package/dist/chunk-MYNLY3HP.js.map +1 -0
  28. package/dist/{chunk-Q5CQTBFA.js → chunk-NSIAORRY.js} +20 -30
  29. package/dist/chunk-NSIAORRY.js.map +1 -0
  30. package/dist/{chunk-MYXFNWAJ.js → chunk-QNGYQMZB.js} +4 -4
  31. package/dist/chunk-QNGYQMZB.js.map +1 -0
  32. package/dist/chunk-WT5PBPLB.js +98 -0
  33. package/dist/chunk-WT5PBPLB.js.map +1 -0
  34. package/dist/{core-DnzqWy6u.d.ts → core-BbgtiXdg.d.ts} +231 -139
  35. package/dist/do/index.d.ts +37 -33
  36. package/dist/do/index.js +21 -14
  37. package/dist/do/index.js.map +1 -1
  38. package/dist/{durable-effects-CWzA-o2B.d.ts → durable-effects-7MTK_jnj.d.ts} +1 -1
  39. package/dist/effect/index.d.ts +151 -0
  40. package/dist/effect/index.js +98 -0
  41. package/dist/effect/index.js.map +1 -0
  42. package/dist/extension/index.d.ts +6 -7
  43. package/dist/extension/index.js +3 -2
  44. package/dist/extension/index.js.map +1 -1
  45. package/dist/flow/index.d.ts +68 -59
  46. package/dist/flow/index.js +32 -27
  47. package/dist/flow/index.js.map +1 -1
  48. package/dist/idempotency/index.d.ts +6 -7
  49. package/dist/idempotency/index.js +4 -2
  50. package/dist/{index-CSyMYDAB.d.ts → index-7vV8xmsD.d.ts} +1 -1
  51. package/dist/{index-KXqTRJyb.d.ts → index-Bz5Rh9Ga.d.ts} +19 -96
  52. package/dist/index-CjFDUBeq.d.ts +207 -0
  53. package/dist/{index-CuNO9m8i.d.ts → index-CkFYA9Y6.d.ts} +14 -16
  54. package/dist/{index-CA_7I5g9.d.ts → index-Cou86-GX.d.ts} +137 -246
  55. package/dist/{index-DrIRb_Ch.d.ts → index-DU6JpQ8G.d.ts} +1 -1
  56. package/dist/{index-HSYgG2Yt.d.ts → index-Xufl55W8.d.ts} +247 -211
  57. package/dist/{index-C2DwkjlP.d.ts → index-_dOR3xqA.d.ts} +57 -45
  58. package/dist/index.d.ts +121 -385
  59. package/dist/index.js +2 -1
  60. package/dist/jev/index.d.ts +133 -97
  61. package/dist/jev/index.js +205 -161
  62. package/dist/jev/index.js.map +1 -1
  63. package/dist/{machine-DGlPWCWI.d.ts → machine-BQzTV8On.d.ts} +171 -288
  64. package/dist/machine-viz/index.d.ts +5 -5
  65. package/dist/machine-viz/index.js +8 -4
  66. package/dist/machine-viz/index.js.map +1 -1
  67. package/dist/mem/index.d.ts +3 -4
  68. package/dist/mem/index.js +2 -1
  69. package/dist/mem/index.js.map +1 -1
  70. package/dist/node/index.d.ts +119 -78
  71. package/dist/node/index.js +52 -38
  72. package/dist/node/index.js.map +1 -1
  73. package/dist/paginate/index.d.ts +27 -22
  74. package/dist/paginate/index.js +6 -4
  75. package/dist/parity/index.d.ts +4 -5
  76. package/dist/parity/index.js +3 -2
  77. package/dist/parity/index.js.map +1 -1
  78. package/dist/pbt/index.d.ts +2 -2
  79. package/dist/pbt/index.js +2 -1
  80. package/dist/pbt/index.js.map +1 -1
  81. package/dist/persistence/index.d.ts +6 -7
  82. package/dist/persistence/index.js +5 -3
  83. package/dist/promise/index.d.ts +186 -0
  84. package/dist/promise/index.js +5 -0
  85. package/dist/promise/index.js.map +1 -0
  86. package/dist/{protocol-D8VbH-VH.d.ts → protocol-ClRZBhHQ.d.ts} +7 -8
  87. package/dist/react/index.d.ts +36 -17
  88. package/dist/react/index.js +27 -4
  89. package/dist/react/index.js.map +1 -1
  90. package/dist/resilience/index.d.ts +63 -456
  91. package/dist/resilience/index.js +37 -483
  92. package/dist/resilience/index.js.map +1 -1
  93. package/dist/{runtime-types-CMF13BBh.d.ts → runtime-types-CiVpC0HN.d.ts} +131 -388
  94. package/dist/testing/index.d.ts +22 -16
  95. package/dist/testing/index.js +15 -7
  96. package/dist/testing/index.js.map +1 -1
  97. package/dist/timing/index.d.ts +25 -22
  98. package/dist/timing/index.js +4 -3
  99. package/dist/timing/index.js.map +1 -1
  100. package/dist/{types-BseNPIhr.d.ts → types-LxZxGYda.d.ts} +2 -2
  101. package/dist/work-queue/index.d.ts +5 -6
  102. package/package.json +18 -5
  103. package/dist/chunk-22POWGFA.js.map +0 -1
  104. package/dist/chunk-3B5X7SBB.js.map +0 -1
  105. package/dist/chunk-66PY5WYF.js +0 -1816
  106. package/dist/chunk-66PY5WYF.js.map +0 -1
  107. package/dist/chunk-AYS5N6WY.js.map +0 -1
  108. package/dist/chunk-L3TND7LA.js +0 -97
  109. package/dist/chunk-L3TND7LA.js.map +0 -1
  110. package/dist/chunk-MYXFNWAJ.js.map +0 -1
  111. package/dist/chunk-Q5CQTBFA.js.map +0 -1
  112. package/dist/chunk-QPEVCJ5Q.js.map +0 -1
  113. package/dist/chunk-TCLBCERH.js.map +0 -1
  114. package/dist/chunk-VTQI2NHP.js.map +0 -1
  115. package/dist/index-ZCMhnVKN.d.ts +0 -272
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/internal/journal/index.ts","../../src/node/index.ts"],"names":["handle"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @packageDocumentation\n * internal/journal — an append-only, ordered record log for `@demlik/tea`.\n * Internal since #49 — not published on any subpath; `fileJournal` on\n * `@demlik/tea/node` is the one public route.\n *\n * The log a \"durable, replayable state machine\" replays from. tea already ships\n * the log's neighbours — `fileStore`/`memoryStore` (`Store<S>`: snapshot with\n * whole-load / whole-save), `recorder` (JSONL debug traces), `durable-effects`\n * (`foldLedger` over an owed-effect ledger) — but not the log itself, so every\n * consumer hand-rolled it. This is that log, domain-blind: a `Journal<R>` over\n * an opaque record `R`, with no pipeline/ledger vocabulary of its own.\n *\n * Three operations carry the contract:\n * - `append(stream, record)` — durable before it returns, assigning a\n * per-stream total-order `seq` (1, 2, 3, …). Concurrent appenders to one\n * stream converge on one order; the lock's only job is an honest order key.\n * - `list(stream)` — every entry of a stream in `seq` order.\n * - `changes(listener)` — a subscription over appends made through this\n * instance after subscribing.\n *\n * `withLock(stream, fx)` runs `fx` with exclusive access to a stream, so a\n * read-decide-append sequence is atomic against other writers. It is\n * re-entrant for the same stream: an `append` inside `fx` does not deadlock on\n * the lock `fx` already holds.\n *\n * The substrate is a `JournalBackend<R>` — how a stream is read, how one record\n * is appended durably, and how a stream is locked across processes.\n * `makeJournal` wires the domain-blind orchestration (per-stream serialisation,\n * `seq` assignment, `changes` fan-out) over any backend. `memoryJournal` is the\n * in-process backend that mirrors `memoryStore` for tests; the Node file\n * backend (`fileJournal`) homes in `@demlik/tea/node` beside `fileStore`.\n *\n * Interface shape follows Effect `unstable/eventlog` `EventJournal`\n * (`entries`/`write`/`changes`/`withLock`) as prior art, not a dependency.\n */\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\n/** A per-stream total-order key. Starts at 1 and increments by one per append. */\nexport type Seq = number;\n\n/** One appended record, tagged with its stream and its per-stream `seq`. */\nexport interface JournalEntry<R> {\n readonly stream: string;\n readonly seq: Seq;\n readonly record: R;\n}\n\n/**\n * An append-only, ordered record log over an opaque record `R`.\n *\n * Domain-blind: it knows nothing of what a record means. Ordering, durability\n * and the honest order key are its whole contract.\n */\nexport interface Journal<R> {\n /**\n * Append `record` to `stream`, durable before the promise resolves, and\n * resolve with the `seq` it was assigned — the next integer in the stream's\n * total order.\n */\n append(stream: string, record: R): Promise<{ seq: Seq }>;\n /** Every entry of `stream`, in `seq` order. Empty for an unwritten stream. */\n list(stream: string): Promise<ReadonlyArray<JournalEntry<R>>>;\n /**\n * Subscribe to appends made through this instance after this call. Returns an\n * unsubscribe. Cross-process appends are not delivered — a cross-process\n * subscription is a polling concern this log leaves to its consumer.\n */\n changes(listener: (entry: JournalEntry<R>) => void): () => void;\n /**\n * Run `fx` with exclusive access to `stream` — the in-process mutex against\n * other callers here, the backend lock against other processes. Re-entrant\n * for the same stream, so an `append` (or nested `withLock`) inside `fx` runs\n * under the lock this call already holds rather than deadlocking on it.\n */\n withLock<A>(stream: string, fx: () => Promise<A>): Promise<A>;\n}\n\n/**\n * The substrate a `Journal<R>` is built over. Everything host-specific lives\n * here; `makeJournal` owns the host-blind rest.\n *\n * `append` is always invoked under `lock` for the same stream, so a backend\n * assigns `seq` from `read().length + 1` without racing itself.\n */\nexport interface JournalBackend<R> {\n /** Every entry of `stream` in `seq` order; empty for an unwritten stream. */\n read(stream: string): Promise<ReadonlyArray<JournalEntry<R>>>;\n /**\n * Persist `record` as the next entry of `stream`, durable before resolving,\n * and resolve with the entry it wrote. Invoked only while `lock` holds the\n * stream, so `seq` is safely the current length plus one.\n */\n append(stream: string, record: R): Promise<JournalEntry<R>>;\n /** Run `fx` while holding `stream`'s cross-process lock. */\n lock<A>(stream: string, fx: () => Promise<A>): Promise<A>;\n}\n\n/**\n * A promise-chained mutex: each `run` waits for the prior one to settle, so the\n * bodies never interleave. The order key is honest because assignment happens\n * inside a body, never across the `await` that another body could slip through.\n */\nclass AsyncLock {\n private tail: Promise<unknown> = Promise.resolve();\n\n run<A>(fx: () => Promise<A>): Promise<A> {\n const result = this.tail.then(fx, fx);\n this.tail = result.then(\n () => undefined,\n () => undefined,\n );\n return result;\n }\n}\n\n/**\n * Build a `Journal<R>` over `backend`. Owns the host-blind half: per-stream\n * serialisation, re-entrant exclusion, and the `changes` fan-out. `append` and\n * `withLock` both route through `exclusive`, which holds the in-process mutex\n * and the backend lock for one stream and is re-entrant for that stream within\n * a single call chain.\n */\nexport function makeJournal<R>(backend: JournalBackend<R>): Journal<R> {\n const mutexes = new Map<string, AsyncLock>();\n // The streams this async call chain holds. Kept in AsyncLocalStorage, not an\n // instance-wide Set, because re-entrancy is a property of the CALL CHAIN, not\n // of the instance: a genuine re-entrant append (nested inside an `fx` that\n // already holds the stream) and an INDEPENDENT top-level append that merely\n // arrives while another chain holds the lock are indistinguishable to a\n // shared Set — so the shared Set waves the independent call through inline,\n // past the mutex, to run concurrently with the holder. That is a lost record:\n // in `fileJournal` the two both read the same length and compute the same\n // `seq`, and the second `rename` clobbers the first. A per-chain store only\n // ever reports the streams THIS chain entered, so an independent call sees an\n // empty (or unrelated) store and correctly queues on the mutex.\n const heldStreams = new AsyncLocalStorage<ReadonlySet<string>>();\n const listeners = new Set<(entry: JournalEntry<R>) => void>();\n\n const mutexFor = (stream: string): AsyncLock => {\n let mutex = mutexes.get(stream);\n if (!mutex) {\n mutex = new AsyncLock();\n mutexes.set(stream, mutex);\n }\n return mutex;\n };\n\n const exclusive = <A>(stream: string, fx: () => Promise<A>): Promise<A> => {\n const held = heldStreams.getStore();\n // This call chain already holds this stream's lock: run inline, or a\n // re-entrant append/withLock would block forever on a lock it owns.\n if (held?.has(stream)) return fx();\n return mutexFor(stream).run(() =>\n backend.lock(stream, () => {\n // Scope the augmented held-set to `fx` and its async continuations. The\n // store is torn down when `fx` settles — no finally clause to leak it.\n const nested = new Set(held).add(stream);\n return heldStreams.run(nested, fx);\n }),\n );\n };\n\n return {\n append(stream, record) {\n return exclusive(stream, async () => {\n const entry = await backend.append(stream, record);\n for (const listener of listeners) listener(entry);\n return { seq: entry.seq };\n });\n },\n list(stream) {\n return Promise.resolve(backend.read(stream));\n },\n changes(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n withLock(stream, fx) {\n return exclusive(stream, fx);\n },\n };\n}\n\n/**\n * An in-process `Journal<R>` — the standard test substrate, mirroring\n * `memoryStore` from `@demlik/tea/mem`. Reference semantics: a record handed to\n * `append` is stored by reference, exactly as `memoryStore` stores state. No\n * cross-process lock is needed — a single process owns the whole log — so the\n * backend lock is identity and `makeJournal`'s in-process mutex is the only\n * ordering guarantee.\n */\nexport function memoryJournal<R>(): Journal<R> {\n const streams = new Map<string, JournalEntry<R>[]>();\n\n return makeJournal<R>({\n async read(stream) {\n return (streams.get(stream) ?? []).slice();\n },\n async append(stream, record) {\n const log = streams.get(stream) ?? [];\n const entry: JournalEntry<R> = { stream, seq: log.length + 1, record };\n log.push(entry);\n streams.set(stream, log);\n return entry;\n },\n async lock(_stream, fx) {\n return fx();\n },\n });\n}\n\n// The remote-journal + per-remote-cursor half of the journal feature (#31): the\n// remote a client pushes to and pulls from, the local cursor that resumes sync,\n// and the sync client that joins a local `Journal` to a remote so two local\n// journals meet in one order. It builds ON `memoryJournal` above.\nexport * from \"./remote\";\n","/**\n * @packageDocumentation\n * @demlik/tea/node — Node host adapter for `@demlik/tea`.\n *\n * Every host adapter ships two halves (mirrors `@demlik/tea/do`):\n *\n * 1. `fileStore<S>(path, parse)` — a `Store<S>` over a JSON file on disk.\n * Atomic save (temp + rename). Absent file → `load()` resolves `null`\n * (fresh boot). Structurally malformed JSON throws at `load()` (an infra\n * error), matching `doStore`; a shape mismatch is NOT a throw — the\n * substrate calls `migrate`, which returns `null` and boots fresh.\n * Use for resumable scripts / local repro of prod machines. Ephemeral\n * CLIs can skip persistence with `memoryStore` from `@demlik/tea/mem`.\n *\n * 2. `nodeSubscribe<M, Ctx>()` — handler registry for the three node-native\n * Sub types this package owns:\n * - `node_ws` — opens a `ws` WebSocket; routes open/message/close/\n * error into Msgs. Registers the live socket in\n * `ctx.wsRegistry` so a Cmd handler can write to it\n * (see `sendToWebSocket`). Cleanup closes the socket\n * for ANY non-CLOSED state.\n * - `node_timer` — setTimeout / setInterval; cleanup clears it.\n * - `node_signal` — a process signal (SIGINT/SIGTERM/…) → Msg; cleanup\n * detaches the listener.\n *\n * Unlike tea-do — whose `alarm()` / `webSocketMessage()` are DO lifecycle\n * methods the handler can't observe from the outside, hence its registries —\n * a node process is continuously alive: each handler holds its resource in\n * closure and the returned cleanup closes it. The substrate's subscription\n * reconciler runs that cleanup the instant `subscriptions(state)` stops listing\n * the Sub — end-then-start choreography for free (Rule 9, [invariant 4]).\n * A lifecycle-bound resource (a socket, a timer) becomes impossible to leak\n * from a reducer cell, because no reducer cell owns its teardown.\n *\n * `ctx.wsRegistry` is the one shared surface: `node_ws` populates it; a Cmd\n * interpreter reads it to write to the open socket. Same shape as tea-do's\n * `wsRegistry`.\n */\n\nimport {\n mkdir,\n open,\n readFile,\n rename,\n unlink,\n writeFile,\n} from \"node:fs/promises\";\nimport { createRequire } from \"node:module\";\nimport { dirname, join } from \"node:path\";\nimport type WebSocket from \"ws\";\nimport type { RawData } from \"ws\";\nimport type { FencedStore, Store, Sub, SubId } from \"../index\";\nimport { StoreConflictError } from \"../index\";\nimport {\n type Journal,\n type JournalEntry,\n makeJournal,\n type Seq,\n} from \"../internal/journal\";\nimport { dispatchIfPresent } from \"../subs/types\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// fileStore — `Store<S>` over a JSON file.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Persist a machine's state to a JSON file — pass the `path` and a `parse` that\n * validates what comes back, get a `Store<S>` you hand to `run` so the next run\n * resumes where this one stopped.\n *\n * `parse` is REQUIRED because the file is a real serialization boundary — the\n * bytes that come back through `JSON.parse` are structurally `unknown`, and the\n * caller (who owns `S`) is the only party that can validate the shape.\n * Returning `null` from `parse` means \"no usable persisted state\" — the\n * substrate boots `init` with `loaded = null`. `parse` must NOT throw per the\n * `Store<S>.migrate` contract.\n *\n * Pass `{ fenced: true }` to get a `FencedStore<S>` instead — see\n * {@link FileStoreOptions}.\n */\nexport function fileStore<S>(\n path: string,\n parse: (raw: unknown) => S | null,\n): Store<S>;\nexport function fileStore<S>(\n path: string,\n parse: (raw: unknown) => S | null,\n options: FileStoreOptions & { readonly fenced: true },\n): FencedStore<S>;\nexport function fileStore<S>(\n path: string,\n parse: (raw: unknown) => S | null,\n options?: FileStoreOptions,\n): Store<S> | FencedStore<S>;\nexport function fileStore<S>(\n path: string,\n parse: (raw: unknown) => S | null,\n options: FileStoreOptions = {},\n): Store<S> | FencedStore<S> {\n const base: Store<S> = {\n async load(): Promise<unknown> {\n let raw: string;\n try {\n raw = await readFile(path, \"utf8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw err;\n }\n // JSON.parse throws on structural malformation — propagate (infra error),\n // matching `doStore`. The decoded value is intentionally returned as\n // `unknown`; the substrate's `migrate` callback is the boundary parse.\n return JSON.parse(raw);\n },\n async save(state: S): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n // Write to a pid-scoped temp then atomically rename. The rename gives\n // torn-write safety (a reader sees the old file or the new one, never a\n // partial) and survives process crash / Ctrl-C — the only failure a local\n // resumable run resumes from. fsync (file + dir) would add power-loss\n // durability the use case doesn't need; the substrate serializes saves\n // through run()'s queue, so there's never a second concurrent writer\n // WITHIN one run. Between two runs there is nothing here refusing one —\n // that is what `{ fenced: true }` below adds (#143).\n const tmp = `${path}.${process.pid}.tmp`;\n await writeFile(tmp, JSON.stringify(state), \"utf8\");\n await rename(tmp, path);\n },\n migrate(raw: unknown): S | null {\n return parse(raw);\n },\n };\n return options.fenced === true ? fenceFileStore(base, path) : base;\n}\n\n/** Options for {@link fileStore}. */\nexport interface FileStoreOptions {\n /**\n * Refuse a second live writer (#143). With `{ fenced: true }` the returned\n * store is a `FencedStore<S>`: it carries a version stamp beside the state\n * file, and `run` compare-and-swaps against it on every save. A process that\n * boots reads the current version and takes the fence; the older live writer\n * that started from the same version is refused with a `StoreConflictError`\n * at its next save. Omit it and the store is exactly as it was — the last\n * writer wins, and single-writer is the caller's precondition to keep.\n */\n readonly fenced?: true;\n}\n\n/**\n * The fencing half of {@link fileStore}. The state file itself is unchanged, so\n * a file written unfenced resumes fenced and back again; the version lives in a\n * sidecar `<path>.fence` holding a decimal integer, and an absent stamp is `0`.\n *\n * The compare-and-swap is taken under the same `wx` lock file `fileJournal`\n * uses, because `open(…, \"wx\")` is the one primitive that is atomic ACROSS\n * PROCESSES. Without it the read-compare-write is a TOCTOU race and two writers\n * both pass their own check. Rename-over-tmp still does the torn-write work for\n * each of the two files; the lock does the exclusion no rename could.\n */\nfunction fenceFileStore<S>(base: Store<S>, path: string): FencedStore<S> {\n const fencePath = `${path}.fence`;\n const lockPath = `${path}.fence.lock`;\n\n async function readVersion(): Promise<number> {\n let raw: string;\n try {\n raw = await readFile(fencePath, \"utf8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return 0;\n throw err;\n }\n const version = Number.parseInt(raw.trim(), 10);\n // A stamp we cannot read is not a version we may assume: defaulting it to 0\n // would let a fresh writer win a fence against a live one.\n if (!Number.isSafeInteger(version) || version < 0) {\n throw new Error(\n `fileStore: version stamp at \"${fencePath}\" is not an integer: ${raw}`,\n );\n }\n return version;\n }\n\n return {\n ...base,\n fenced: true,\n async loadFenced(): Promise<{ raw: unknown; version: number }> {\n // Version FIRST, bytes second: a writer landing between the two reads\n // leaves us with a version OLDER than the bytes, so our next save is\n // refused. Reading bytes first could hand back a version newer than them,\n // and that save would silently win.\n const version = await readVersion();\n return { raw: await base.load(), version };\n },\n async saveFenced(state: S, expectedVersion: number): Promise<number> {\n await mkdir(dirname(path), { recursive: true });\n await acquireFileLock(dirname(path), lockPath);\n try {\n const actual = await readVersion();\n if (actual !== expectedVersion) {\n throw new StoreConflictError(expectedVersion, actual);\n }\n const next = expectedVersion + 1;\n // State before stamp. A crash between the two leaves a stamp BEHIND the\n // bytes, which reads as a conflict for the next writer — the safe\n // direction. The reverse advertises a write that never landed.\n await base.save(state);\n const tmp = `${fencePath}.${process.pid}.tmp`;\n await writeFile(tmp, String(next), \"utf8\");\n await rename(tmp, fencePath);\n return next;\n } finally {\n await unlink(lockPath).catch(() => {});\n }\n },\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// fileJournal — a `Journal<R>` over one on-disk JSONL file per stream.\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst LOCK_SPIN_MS = 10;\n\n/**\n * The Node file substrate for the journal (`src/internal/journal/`), beside\n * `fileStore`. A\n * stream is one JSONL file under `dir` (`<encoded-stream>.jsonl`), one\n * `{\"seq\":n,\"record\":…}` object per line — readable and greppable on disk, no\n * SQLite. `parse` is REQUIRED for the same reason `fileStore` requires it: the\n * bytes read back through `JSON.parse` are structurally `unknown`, and the\n * caller who owns `R` is the only party that can validate a record's shape.\n *\n * `append` rewrites the whole stream file via a pid-scoped temp + `rename`,\n * exactly as `fileStore.save` does — a reader sees the old file or the new one,\n * never a torn line, and the write survives a crash mid-append. The append is\n * O(n) in the stream's length; a durable record log a machine replays is read\n * whole anyway, so the atomic-rewrite guarantee is worth the cost.\n *\n * The ordering key is taken under a `wx` lock file (`<file>.lock`) holding the\n * writer's PID: a second writer racing the same stream blocks on the lock, so\n * `seq` is a single total order across processes. A crashed writer's lock is\n * stolen once its PID proves dead (`process.kill(pid, 0)` → `ESRCH`), so a\n * crash never wedges the stream. This is the lock phoenix spike #6673 proved:\n * its only job is an honest order key, not the win.\n *\n * `changes` fires for appends made through THIS instance; a cross-process\n * append lands on disk but is not broadcast here (that is a polling concern the\n * log leaves to its consumer, and the remote-journal work downstream).\n */\nexport function fileJournal<R>(\n dir: string,\n parse: (raw: unknown) => R,\n): Journal<R> {\n const streamPath = (stream: string): string =>\n join(dir, `${encodeURIComponent(stream)}.jsonl`);\n\n const readEntries = async (stream: string): Promise<JournalEntry<R>[]> => {\n const path = streamPath(stream);\n let raw: string;\n try {\n raw = await readFile(path, \"utf8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return [];\n throw err;\n }\n return raw\n .split(\"\\n\")\n .filter((line) => line.length > 0)\n .map((line) => {\n const row = JSON.parse(line) as { seq: Seq; record: unknown };\n return { stream, seq: row.seq, record: parse(row.record) };\n });\n };\n\n return makeJournal<R>({\n read: readEntries,\n async append(stream, record) {\n const entries = await readEntries(stream);\n const entry: JournalEntry<R> = {\n stream,\n seq: entries.length + 1,\n record,\n };\n const path = streamPath(stream);\n const lines = [...entries, entry]\n .map((e) => JSON.stringify({ seq: e.seq, record: e.record }))\n .join(\"\\n\");\n await mkdir(dir, { recursive: true });\n const tmp = `${path}.${process.pid}.tmp`;\n await writeFile(tmp, `${lines}\\n`, \"utf8\");\n await rename(tmp, path);\n return entry;\n },\n async lock(stream, fx) {\n const lockPath = `${streamPath(stream)}.lock`;\n await acquireFileLock(dir, lockPath);\n try {\n return await fx();\n } finally {\n await unlink(lockPath).catch(() => {});\n }\n },\n });\n}\n\n/**\n * Acquire an exclusive lock by creating `lockPath` with `wx` (O_CREAT|O_EXCL) —\n * the one primitive that is atomic across processes. On contention, steal the\n * lock only once its holder's PID proves dead; otherwise spin. `process.kill(pid,\n * 0)` sends no signal — it tests existence: it throws `ESRCH` for a dead PID,\n * and `EPERM` for a live one this process may not signal (still alive, so hold\n * off). A lock that vanishes between the failed create and the read is already\n * free, so retry.\n */\nasync function acquireFileLock(dir: string, lockPath: string): Promise<void> {\n await mkdir(dir, { recursive: true });\n for (;;) {\n try {\n const handle = await open(lockPath, \"wx\");\n try {\n await handle.writeFile(String(process.pid));\n } finally {\n await handle.close();\n }\n return;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"EEXIST\") throw err;\n if (await stealIfStale(lockPath)) continue;\n await new Promise((resolve) => setTimeout(resolve, LOCK_SPIN_MS));\n }\n }\n}\n\nasync function stealIfStale(lockPath: string): Promise<boolean> {\n let holder: string;\n try {\n holder = await readFile(lockPath, \"utf8\");\n } catch (err) {\n // Vanished between the failed create and this read — already free.\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return true;\n throw err;\n }\n const pid = Number(holder.trim());\n if (!Number.isInteger(pid) || pid <= 0) return false;\n if (isProcessAlive(pid)) return false;\n await unlink(lockPath).catch(() => {});\n return true;\n}\n\nfunction isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n return (err as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// NodeSub — the three node-native subscription variants.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * A node WebSocket sub. The substrate diffs subs by `id`; the same `id` across\n * transitions = the same socket (kept open). Emit a DIFFERENT `id` to force a\n * reconnect.\n *\n * Only `onMessage` returns `M | null` (null drops the frame) because inbound\n * frames are the boundary that needs a parse-and-maybe-discard seam. `onOpen`/\n * `onClose`/`onError` always dispatch — a spurious one is dropped in the reducer\n * (the cell ignores it), not here.\n */\nexport type NodeWsSub<M> = {\n id: SubId;\n type: \"node_ws\";\n url: string;\n onMessage: (data: string) => M | null;\n onOpen?: () => M;\n onClose?: (code: number, reason: string) => M;\n onError?: (message: string) => M;\n};\n\n/**\n * A node timer sub. One-shot `setTimeout` by default; `repeat: true` →\n * `setInterval`. Cleanup clears the timer — a timer keyed to a phase is\n * cancelled the moment the machine leaves that phase.\n */\nexport type NodeTimerSub<M> = {\n id: SubId;\n type: \"node_timer\";\n delayMs: number;\n msg: M;\n repeat?: boolean;\n};\n\n/**\n * A process-signal sub: `signal` (SIGINT/SIGTERM/…) → `msg`. Cleanup detaches\n * the listener. Lets graceful shutdown be a real machine transition rather than\n * an out-of-band `process.on` that the reducer can't see.\n */\nexport type NodeSignalSub<M> = {\n id: SubId;\n type: \"node_signal\";\n signal: NodeJS.Signals;\n msg: M;\n};\n\nexport type NodeSub<M> = NodeWsSub<M> | NodeTimerSub<M> | NodeSignalSub<M>;\n\n// Compile-time guarantee: `NodeSub<M>` is structurally a `Sub` (has `id`+`type`).\n// Erased at runtime — the `extends Sub` constraint on the alias's type\n// parameter fails to compile if `NodeSub` ever drifts off the `Sub` shape.\nexport type AssertNodeSubIsSub<T extends Sub = NodeSub<unknown>> = T;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Ctx — what the node Subs read / populate.\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type NodeWsRegistry = Map<SubId, WebSocket>;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// ws — loaded on first use, never at module load.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * `ws` is an OPTIONAL peer: the `node_ws` Sub needs it, `fileStore` and\n * `fileJournal` — everything the tutorial uses — do not. A static\n * `import … from \"ws\"` puts the dependency on the module's load path, so a\n * consumer who installed only the tutorial's three packages could not import\n * this door at all (`ERR_MODULE_NOT_FOUND`). The type import above is erased,\n * and the value arrives through this loader at the moment a socket is actually\n * opened — so the cost of an optional peer is paid by the feature that needs\n * it, not by the door it happens to share.\n *\n * `createRequire` rather than `await import(\"ws\")` because `subscribe`'s\n * contract is synchronous — the handler must return its cleanup on the same\n * tick the substrate installs it, and a deferred socket would give cleanup\n * nothing to close.\n */\ntype NodeWsConstructor = new (url: string) => WebSocket;\n\nlet cachedWs: NodeWsConstructor | undefined;\n\nfunction loadWebSocket(): NodeWsConstructor {\n if (cachedWs) return cachedWs;\n let mod: unknown;\n try {\n mod = createRequire(import.meta.url)(\"ws\");\n } catch (cause) {\n throw new Error(\n 'The `node_ws` Sub needs the optional peer dependency \"ws\". Install it: `npm install ws`.',\n { cause },\n );\n }\n const ctor = (mod as { default?: NodeWsConstructor }).default ?? mod;\n cachedWs = ctor as NodeWsConstructor;\n return cachedWs;\n}\n\n/**\n * `WebSocket.OPEN`, inlined. Reading it off the class would drag `ws` onto the\n * load path of `sendToWebSocket`, which is a Cmd-side helper a caller can reach\n * without ever opening a socket. The value is fixed by the WHATWG WebSocket\n * spec, not by `ws`.\n */\nconst WS_OPEN = 1;\n\n/** `WebSocket.CLOSED`, inlined for the same reason as {@link WS_OPEN}. */\nconst WS_CLOSED = 3;\n\n/**\n * The Ctx fields the node Subs depend on. A consumer's Ctx must extend this so\n * `node_ws` can register the live socket and Cmd handlers can reach it.\n *\n * Deliberately NOT parameterized by Msg: the registry holds raw `WebSocket`s,\n * and each Sub variant carries its own Msg-typed callbacks. Trip-wire: if a\n * future node Sub holds a Msg-typed callback on ctx, this becomes\n * `NodeSubscribeCtx<M>`.\n */\nexport interface NodeSubscribeCtx {\n wsRegistry: NodeWsRegistry;\n}\n\n/**\n * Write a frame to a registered `node_ws` socket. No-op (returns `false`) when\n * the socket is absent, not OPEN, or `send()` throws. The Cmd-side companion to\n * the `node_ws` Sub: the Sub owns the socket's lifetime; this writes to it.\n *\n * The contract is a boolean no-op, never a throw: the socket can transition out\n * of OPEN between the `readyState` check and `send()` (or `ws` can throw on an\n * errored underlying connection), and a Cmd interpreter must not crash on that.\n */\nexport function sendToWebSocket(\n ctx: NodeSubscribeCtx,\n id: SubId,\n data: string,\n): boolean {\n const socket = ctx.wsRegistry.get(id);\n if (!socket || socket.readyState !== WS_OPEN) return false;\n try {\n socket.send(data);\n return true;\n } catch {\n return false;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// nodeSubscribe — the handler registry.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Build the `subscribe` handlers for `node_ws`, `node_timer`, and\n * `node_signal`. Matches `Machine#subscribe`'s\n * `[K in U[\"type\"]]: (sub, ctx, dispatch) => () => void` contract for the\n * `NodeSub<M>` variants. Generic `Ctx` must extend `NodeSubscribeCtx` so the\n * `node_ws` handler can register its socket.\n */\nexport function nodeSubscribe<M, Ctx extends NodeSubscribeCtx>(): {\n node_ws: (\n sub: NodeWsSub<M>,\n ctx: Ctx,\n dispatch: (msg: M) => void,\n ) => () => void;\n node_timer: (\n sub: NodeTimerSub<M>,\n ctx: Ctx,\n dispatch: (msg: M) => void,\n ) => () => void;\n node_signal: (\n sub: NodeSignalSub<M>,\n ctx: Ctx,\n dispatch: (msg: M) => void,\n ) => () => void;\n} {\n return {\n node_ws: (sub, ctx, dispatch) => {\n const socket = new (loadWebSocket())(sub.url);\n ctx.wsRegistry.set(sub.id, socket);\n\n socket.on(\"open\", () => {\n if (sub.onOpen) dispatch(sub.onOpen());\n });\n socket.on(\"message\", (data: RawData) => {\n dispatchIfPresent(dispatch, sub.onMessage(data.toString()));\n });\n socket.on(\"close\", (code: number, reason: Buffer) => {\n if (sub.onClose) dispatch(sub.onClose(code, reason.toString()));\n });\n socket.on(\"error\", (err: Error) => {\n if (sub.onError) dispatch(sub.onError(err.message));\n });\n\n return () => {\n ctx.wsRegistry.delete(sub.id);\n socket.removeAllListeners();\n // Swallow teardown-induced errors: terminate()-ing a CONNECTING socket\n // emits an 'error' (\"closed before the connection was established\"),\n // and with our listeners gone that would be an unhandled 'error' that\n // crashes the process. A no-op listener absorbs it.\n socket.on(\"error\", () => {});\n // Close for ANY non-closed state: a graceful 1000 when OPEN, a hard\n // terminate() otherwise (CONNECTING/CLOSING) so a socket caught mid-\n // handshake can't dangle. This is the teardown the hand-driven Cmd-pair\n // approach (`cleanup_browser` closing the ws only `if OPEN`) could not\n // guarantee.\n if (socket.readyState === WS_OPEN) {\n socket.close(1000, \"tea-node sub cleanup\");\n } else if (socket.readyState !== WS_CLOSED) {\n socket.terminate();\n }\n };\n },\n\n node_timer: (sub, _ctx, dispatch) => {\n // Two reconcile lifecycles under one variant: a one-shot (`setTimeout`)\n // that `subscriptions(state)` should drop on the render after it fires,\n // vs a repeating (`setInterval`) one the state keeps listing across\n // renders. Same Sub.type; the `repeat` flag picks the install + cleanup.\n if (sub.repeat) {\n const handle = setInterval(() => dispatch(sub.msg), sub.delayMs);\n return () => clearInterval(handle);\n }\n const handle = setTimeout(() => dispatch(sub.msg), sub.delayMs);\n return () => clearTimeout(handle);\n },\n\n node_signal: (sub, _ctx, dispatch) => {\n const handler = () => dispatch(sub.msg);\n process.on(sub.signal, handler);\n return () => {\n process.off(sub.signal, handler);\n };\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/internal/journal/index.ts","../../src/node/index.ts"],"names":["handle"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @packageDocumentation\n * internal/journal — an append-only, ordered record log for `@demlik/tea`.\n * Internal since #49 — not published on any subpath; `fileJournal` on\n * `@demlik/tea/node` is the one public route.\n *\n * The log a \"durable, replayable state machine\" replays from. tea already ships\n * the log's neighbours — `fileStore`/`memoryStore` (`Store<S>`: snapshot with\n * whole-load / whole-save), `recorder` (JSONL debug traces), `durable-effects`\n * (`foldLedger` over an owed-effect ledger) — but not the log itself, so every\n * consumer hand-rolled it. This is that log, domain-blind: a `Journal<R>` over\n * an opaque record `R`, with no pipeline/ledger vocabulary of its own.\n *\n * Three operations carry the contract:\n * - `append(stream, record)` — durable before it returns, assigning a\n * per-stream total-order `seq` (1, 2, 3, …). Concurrent appenders to one\n * stream converge on one order; the lock's only job is an honest order key.\n * - `list(stream)` — every entry of a stream in `seq` order.\n * - `changes(listener)` — a subscription over appends made through this\n * instance after subscribing.\n *\n * `withLock(stream, fx)` runs `fx` with exclusive access to a stream, so a\n * read-decide-append sequence is atomic against other writers. It is\n * re-entrant for the same stream: an `append` inside `fx` does not deadlock on\n * the lock `fx` already holds.\n *\n * The substrate is a `JournalBackend<R>` — how a stream is read, how one record\n * is appended durably, and how a stream is locked across processes.\n * `makeJournal` wires the domain-blind orchestration (per-stream serialisation,\n * `seq` assignment, `changes` fan-out) over any backend. `memoryJournal` is the\n * in-process backend that mirrors `memoryStore` for tests; the Node file\n * backend (`fileJournal`) homes in `@demlik/tea/node` beside `fileStore`.\n *\n * Interface shape follows Effect `unstable/eventlog` `EventJournal`\n * (`entries`/`write`/`changes`/`withLock`) as prior art, not a dependency.\n */\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\n/** A per-stream total-order key. Starts at 1 and increments by one per append. */\nexport type Seq = number;\n\n/** One appended record, tagged with its stream and its per-stream `seq`. */\nexport interface JournalEntry<R> {\n readonly stream: string;\n readonly seq: Seq;\n readonly record: R;\n}\n\n/**\n * An append-only, ordered record log over an opaque record `R`.\n *\n * Domain-blind: it knows nothing of what a record means. Ordering, durability\n * and the honest order key are its whole contract.\n */\nexport interface Journal<R> {\n /**\n * Append `record` to `stream`, durable before the promise resolves, and\n * resolve with the `seq` it was assigned — the next integer in the stream's\n * total order.\n */\n append(stream: string, record: R): Promise<{ seq: Seq }>;\n /** Every entry of `stream`, in `seq` order. Empty for an unwritten stream. */\n list(stream: string): Promise<ReadonlyArray<JournalEntry<R>>>;\n /**\n * Subscribe to appends made through this instance after this call. Returns an\n * unsubscribe. Cross-process appends are not delivered — a cross-process\n * subscription is a polling concern this log leaves to its consumer.\n */\n changes(listener: (entry: JournalEntry<R>) => void): () => void;\n /**\n * Run `fx` with exclusive access to `stream` — the in-process mutex against\n * other callers here, the backend lock against other processes. Re-entrant\n * for the same stream, so an `append` (or nested `withLock`) inside `fx` runs\n * under the lock this call already holds rather than deadlocking on it.\n */\n withLock<A>(stream: string, fx: () => Promise<A>): Promise<A>;\n}\n\n/**\n * The substrate a `Journal<R>` is built over. Everything host-specific lives\n * here; `makeJournal` owns the host-blind rest.\n *\n * `append` is always invoked under `lock` for the same stream, so a backend\n * assigns `seq` from `read().length + 1` without racing itself.\n */\nexport interface JournalBackend<R> {\n /** Every entry of `stream` in `seq` order; empty for an unwritten stream. */\n read(stream: string): Promise<ReadonlyArray<JournalEntry<R>>>;\n /**\n * Persist `record` as the next entry of `stream`, durable before resolving,\n * and resolve with the entry it wrote. Invoked only while `lock` holds the\n * stream, so `seq` is safely the current length plus one.\n */\n append(stream: string, record: R): Promise<JournalEntry<R>>;\n /** Run `fx` while holding `stream`'s cross-process lock. */\n lock<A>(stream: string, fx: () => Promise<A>): Promise<A>;\n}\n\n/**\n * A promise-chained mutex: each `run` waits for the prior one to settle, so the\n * bodies never interleave. The order key is honest because assignment happens\n * inside a body, never across the `await` that another body could slip through.\n */\nclass AsyncLock {\n private tail: Promise<unknown> = Promise.resolve();\n\n run<A>(fx: () => Promise<A>): Promise<A> {\n const result = this.tail.then(fx, fx);\n this.tail = result.then(\n () => undefined,\n () => undefined,\n );\n return result;\n }\n}\n\n/**\n * Build a `Journal<R>` over `backend`. Owns the host-blind half: per-stream\n * serialisation, re-entrant exclusion, and the `changes` fan-out. `append` and\n * `withLock` both route through `exclusive`, which holds the in-process mutex\n * and the backend lock for one stream and is re-entrant for that stream within\n * a single call chain.\n */\nexport function makeJournal<R>(backend: JournalBackend<R>): Journal<R> {\n const mutexes = new Map<string, AsyncLock>();\n // The streams this async call chain holds. Kept in AsyncLocalStorage, not an\n // instance-wide Set, because re-entrancy is a property of the CALL CHAIN, not\n // of the instance: a genuine re-entrant append (nested inside an `fx` that\n // already holds the stream) and an INDEPENDENT top-level append that merely\n // arrives while another chain holds the lock are indistinguishable to a\n // shared Set — so the shared Set waves the independent call through inline,\n // past the mutex, to run concurrently with the holder. That is a lost record:\n // in `fileJournal` the two both read the same length and compute the same\n // `seq`, and the second `rename` clobbers the first. A per-chain store only\n // ever reports the streams THIS chain entered, so an independent call sees an\n // empty (or unrelated) store and correctly queues on the mutex.\n const heldStreams = new AsyncLocalStorage<ReadonlySet<string>>();\n const listeners = new Set<(entry: JournalEntry<R>) => void>();\n\n const mutexFor = (stream: string): AsyncLock => {\n let mutex = mutexes.get(stream);\n if (!mutex) {\n mutex = new AsyncLock();\n mutexes.set(stream, mutex);\n }\n return mutex;\n };\n\n const exclusive = <A>(stream: string, fx: () => Promise<A>): Promise<A> => {\n const held = heldStreams.getStore();\n // This call chain already holds this stream's lock: run inline, or a\n // re-entrant append/withLock would block forever on a lock it owns.\n if (held?.has(stream)) return fx();\n return mutexFor(stream).run(() =>\n backend.lock(stream, () => {\n // Scope the augmented held-set to `fx` and its async continuations. The\n // store is torn down when `fx` settles — no finally clause to leak it.\n const nested = new Set(held).add(stream);\n return heldStreams.run(nested, fx);\n }),\n );\n };\n\n return {\n append(stream, record) {\n return exclusive(stream, async () => {\n const entry = await backend.append(stream, record);\n for (const listener of listeners) listener(entry);\n return { seq: entry.seq };\n });\n },\n list(stream) {\n return Promise.resolve(backend.read(stream));\n },\n changes(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n withLock(stream, fx) {\n return exclusive(stream, fx);\n },\n };\n}\n\n/**\n * An in-process `Journal<R>` — the standard test substrate, mirroring\n * `memoryStore` from `@demlik/tea/mem`. Reference semantics: a record handed to\n * `append` is stored by reference, exactly as `memoryStore` stores state. No\n * cross-process lock is needed — a single process owns the whole log — so the\n * backend lock is identity and `makeJournal`'s in-process mutex is the only\n * ordering guarantee.\n */\nexport function memoryJournal<R>(): Journal<R> {\n const streams = new Map<string, JournalEntry<R>[]>();\n\n return makeJournal<R>({\n async read(stream) {\n return (streams.get(stream) ?? []).slice();\n },\n async append(stream, record) {\n const log = streams.get(stream) ?? [];\n const entry: JournalEntry<R> = { stream, seq: log.length + 1, record };\n log.push(entry);\n streams.set(stream, log);\n return entry;\n },\n async lock(_stream, fx) {\n return fx();\n },\n });\n}\n\n// The remote-journal + per-remote-cursor half of the journal feature (#31): the\n// remote a client pushes to and pulls from, the local cursor that resumes sync,\n// and the sync client that joins a local `Journal` to a remote so two local\n// journals meet in one order. It builds ON `memoryJournal` above.\nexport * from \"./remote\";\n","/**\n * @packageDocumentation\n * @demlik/tea/node — Node host adapter for `@demlik/tea`.\n *\n * Every host adapter ships two halves (mirrors `@demlik/tea/do`):\n *\n * 1. `fileStore<S>(path, parse)` — a `Store<S>` over a JSON file on disk.\n * Atomic save (temp + rename). Absent file → `load()` resolves `null`\n * (fresh boot). Structurally malformed JSON throws at `load()` (an infra\n * error), matching `doStore`; a shape mismatch is NOT a throw — the\n * substrate calls `migrate`, which returns `null` and boots fresh.\n * Use for resumable scripts / local repro of prod machines. Ephemeral\n * CLIs can skip persistence with `memoryStore` from `@demlik/tea/mem`.\n *\n * 2. `nodeSubscribe<M, Ctx>({ ws })` — the runners for the three node-native\n * Sub types this package owns, handed to `run` as its `subscribe`:\n * - `node_ws` — deps `{ key, url }`. Opens a `ws` WebSocket; routes\n * open/message/close/error into Msgs through the `ws`\n * handlers. Registers the live socket in\n * `ctx.wsRegistry` under `key` so a Cmd handler can\n * write to it (see `sendToWebSocket`). Cleanup closes\n * the socket for ANY non-CLOSED state.\n * - `node_timer` — deps `{ delayMs, msg, repeat? }`. setTimeout /\n * setInterval; cleanup clears it.\n * - `node_signal` — deps `{ signal, msg }`. A process signal\n * (SIGINT/SIGTERM/…) → Msg; cleanup detaches the\n * listener.\n *\n * Unlike tea-do — whose `alarm()` / `webSocketMessage()` are DO lifecycle\n * methods the handler can't observe from the outside, hence its registries —\n * a node process is continuously alive: each runner holds its resource in\n * closure and the returned cleanup closes it. The engine runs that cleanup the\n * instant the Sub's `deps` go null, and a changed `deps` value stops the old\n * runner before starting the new one — end-then-start choreography for free\n * (Rule 9, [invariant 4]). A lifecycle-bound resource (a socket, a timer)\n * becomes impossible to leak from a reducer cell, because no reducer cell owns\n * its teardown.\n *\n * `ctx.wsRegistry` is the one shared surface: `node_ws` populates it, keyed by\n * the Sub's `deps.key`; a Cmd interpreter reads it to write to the open\n * socket. Same shape as tea-do's `wsRegistry`.\n */\n\nimport {\n mkdir,\n open,\n readFile,\n rename,\n unlink,\n writeFile,\n} from \"node:fs/promises\";\nimport { createRequire } from \"node:module\";\nimport { dirname, join } from \"node:path\";\nimport type WebSocket from \"ws\";\nimport type { RawData } from \"ws\";\nimport type { Dispose, FencedStore, Store, Sub } from \"../index\";\nimport { StoreConflictError } from \"../index\";\nimport {\n type Journal,\n type JournalEntry,\n makeJournal,\n type Seq,\n} from \"../internal/journal\";\nimport { dispatchIfPresent } from \"../subs/types\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// fileStore — `Store<S>` over a JSON file.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Persist a machine's state to a JSON file — pass the `path` and a `parse` that\n * validates what comes back, get a `Store<S>` you hand to `run` so the next run\n * resumes where this one stopped.\n *\n * `parse` is REQUIRED because the file is a real serialization boundary — the\n * bytes that come back through `JSON.parse` are structurally `unknown`, and the\n * caller (who owns `S`) is the only party that can validate the shape.\n * Returning `null` from `parse` means \"no usable persisted state\" — the\n * substrate boots `init` with `loaded = null`. `parse` must NOT throw per the\n * `Store<S>.migrate` contract.\n *\n * Pass `{ fenced: true }` to get a `FencedStore<S>` instead — see\n * {@link FileStoreOptions}.\n */\nexport function fileStore<S>(\n path: string,\n parse: (raw: unknown) => S | null,\n): Store<S>;\nexport function fileStore<S>(\n path: string,\n parse: (raw: unknown) => S | null,\n options: FileStoreOptions & { readonly fenced: true },\n): FencedStore<S>;\nexport function fileStore<S>(\n path: string,\n parse: (raw: unknown) => S | null,\n options?: FileStoreOptions,\n): Store<S> | FencedStore<S>;\nexport function fileStore<S>(\n path: string,\n parse: (raw: unknown) => S | null,\n options: FileStoreOptions = {},\n): Store<S> | FencedStore<S> {\n const base: Store<S> = {\n async load(): Promise<unknown> {\n let raw: string;\n try {\n raw = await readFile(path, \"utf8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw err;\n }\n // JSON.parse throws on structural malformation — propagate (infra error),\n // matching `doStore`. The decoded value is intentionally returned as\n // `unknown`; the substrate's `migrate` callback is the boundary parse.\n return JSON.parse(raw);\n },\n async save(state: S): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n // Write to a pid-scoped temp then atomically rename. The rename gives\n // torn-write safety (a reader sees the old file or the new one, never a\n // partial) and survives process crash / Ctrl-C — the only failure a local\n // resumable run resumes from. fsync (file + dir) would add power-loss\n // durability the use case doesn't need; the substrate serializes saves\n // through run()'s queue, so there's never a second concurrent writer\n // WITHIN one run. Between two runs there is nothing here refusing one —\n // that is what `{ fenced: true }` below adds (#143).\n const tmp = `${path}.${process.pid}.tmp`;\n await writeFile(tmp, JSON.stringify(state), \"utf8\");\n await rename(tmp, path);\n },\n migrate(raw: unknown): S | null {\n return parse(raw);\n },\n };\n return options.fenced === true ? fenceFileStore(base, path) : base;\n}\n\n/** Options for {@link fileStore}. */\nexport interface FileStoreOptions {\n /**\n * Refuse a second live writer (#143). With `{ fenced: true }` the returned\n * store is a `FencedStore<S>`: it carries a version stamp beside the state\n * file, and `run` compare-and-swaps against it on every save. A process that\n * boots reads the current version and takes the fence; the older live writer\n * that started from the same version is refused with a `StoreConflictError`\n * at its next save. Omit it and the store is exactly as it was — the last\n * writer wins, and single-writer is the caller's precondition to keep.\n */\n readonly fenced?: true;\n}\n\n/**\n * The fencing half of {@link fileStore}. The state file itself is unchanged, so\n * a file written unfenced resumes fenced and back again; the version lives in a\n * sidecar `<path>.fence` holding a decimal integer, and an absent stamp is `0`.\n *\n * The compare-and-swap is taken under the same `wx` lock file `fileJournal`\n * uses, because `open(…, \"wx\")` is the one primitive that is atomic ACROSS\n * PROCESSES. Without it the read-compare-write is a TOCTOU race and two writers\n * both pass their own check. Rename-over-tmp still does the torn-write work for\n * each of the two files; the lock does the exclusion no rename could.\n */\nfunction fenceFileStore<S>(base: Store<S>, path: string): FencedStore<S> {\n const fencePath = `${path}.fence`;\n const lockPath = `${path}.fence.lock`;\n\n async function readVersion(): Promise<number> {\n let raw: string;\n try {\n raw = await readFile(fencePath, \"utf8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return 0;\n throw err;\n }\n const version = Number.parseInt(raw.trim(), 10);\n // A stamp we cannot read is not a version we may assume: defaulting it to 0\n // would let a fresh writer win a fence against a live one.\n if (!Number.isSafeInteger(version) || version < 0) {\n throw new Error(\n `fileStore: version stamp at \"${fencePath}\" is not an integer: ${raw}`,\n );\n }\n return version;\n }\n\n return {\n ...base,\n fenced: true,\n async loadFenced(): Promise<{ raw: unknown; version: number }> {\n // Version FIRST, bytes second: a writer landing between the two reads\n // leaves us with a version OLDER than the bytes, so our next save is\n // refused. Reading bytes first could hand back a version newer than them,\n // and that save would silently win.\n const version = await readVersion();\n return { raw: await base.load(), version };\n },\n async saveFenced(state: S, expectedVersion: number): Promise<number> {\n await mkdir(dirname(path), { recursive: true });\n await acquireFileLock(dirname(path), lockPath);\n try {\n const actual = await readVersion();\n if (actual !== expectedVersion) {\n throw new StoreConflictError(expectedVersion, actual);\n }\n const next = expectedVersion + 1;\n // State before stamp. A crash between the two leaves a stamp BEHIND the\n // bytes, which reads as a conflict for the next writer — the safe\n // direction. The reverse advertises a write that never landed.\n await base.save(state);\n const tmp = `${fencePath}.${process.pid}.tmp`;\n await writeFile(tmp, String(next), \"utf8\");\n await rename(tmp, fencePath);\n return next;\n } finally {\n await unlink(lockPath).catch(() => {});\n }\n },\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// fileJournal — a `Journal<R>` over one on-disk JSONL file per stream.\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst LOCK_SPIN_MS = 10;\n\n/**\n * The Node file substrate for the journal (`src/internal/journal/`), beside\n * `fileStore`. A\n * stream is one JSONL file under `dir` (`<encoded-stream>.jsonl`), one\n * `{\"seq\":n,\"record\":…}` object per line — readable and greppable on disk, no\n * SQLite. `parse` is REQUIRED for the same reason `fileStore` requires it: the\n * bytes read back through `JSON.parse` are structurally `unknown`, and the\n * caller who owns `R` is the only party that can validate a record's shape.\n *\n * `append` rewrites the whole stream file via a pid-scoped temp + `rename`,\n * exactly as `fileStore.save` does — a reader sees the old file or the new one,\n * never a torn line, and the write survives a crash mid-append. The append is\n * O(n) in the stream's length; a durable record log a machine replays is read\n * whole anyway, so the atomic-rewrite guarantee is worth the cost.\n *\n * The ordering key is taken under a `wx` lock file (`<file>.lock`) holding the\n * writer's PID: a second writer racing the same stream blocks on the lock, so\n * `seq` is a single total order across processes. A crashed writer's lock is\n * stolen once its PID proves dead (`process.kill(pid, 0)` → `ESRCH`), so a\n * crash never wedges the stream. This is the lock phoenix spike #6673 proved:\n * its only job is an honest order key, not the win.\n *\n * `changes` fires for appends made through THIS instance; a cross-process\n * append lands on disk but is not broadcast here (that is a polling concern the\n * log leaves to its consumer, and the remote-journal work downstream).\n */\nexport function fileJournal<R>(\n dir: string,\n parse: (raw: unknown) => R,\n): Journal<R> {\n const streamPath = (stream: string): string =>\n join(dir, `${encodeURIComponent(stream)}.jsonl`);\n\n const readEntries = async (stream: string): Promise<JournalEntry<R>[]> => {\n const path = streamPath(stream);\n let raw: string;\n try {\n raw = await readFile(path, \"utf8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return [];\n throw err;\n }\n return raw\n .split(\"\\n\")\n .filter((line) => line.length > 0)\n .map((line) => {\n const row = JSON.parse(line) as { seq: Seq; record: unknown };\n return { stream, seq: row.seq, record: parse(row.record) };\n });\n };\n\n return makeJournal<R>({\n read: readEntries,\n async append(stream, record) {\n const entries = await readEntries(stream);\n const entry: JournalEntry<R> = {\n stream,\n seq: entries.length + 1,\n record,\n };\n const path = streamPath(stream);\n const lines = [...entries, entry]\n .map((e) => JSON.stringify({ seq: e.seq, record: e.record }))\n .join(\"\\n\");\n await mkdir(dir, { recursive: true });\n const tmp = `${path}.${process.pid}.tmp`;\n await writeFile(tmp, `${lines}\\n`, \"utf8\");\n await rename(tmp, path);\n return entry;\n },\n async lock(stream, fx) {\n const lockPath = `${streamPath(stream)}.lock`;\n await acquireFileLock(dir, lockPath);\n try {\n return await fx();\n } finally {\n await unlink(lockPath).catch(() => {});\n }\n },\n });\n}\n\n/**\n * Acquire an exclusive lock by creating `lockPath` with `wx` (O_CREAT|O_EXCL) —\n * the one primitive that is atomic across processes. On contention, steal the\n * lock only once its holder's PID proves dead; otherwise spin. `process.kill(pid,\n * 0)` sends no signal — it tests existence: it throws `ESRCH` for a dead PID,\n * and `EPERM` for a live one this process may not signal (still alive, so hold\n * off). A lock that vanishes between the failed create and the read is already\n * free, so retry.\n */\nasync function acquireFileLock(dir: string, lockPath: string): Promise<void> {\n await mkdir(dir, { recursive: true });\n for (;;) {\n try {\n const handle = await open(lockPath, \"wx\");\n try {\n await handle.writeFile(String(process.pid));\n } finally {\n await handle.close();\n }\n return;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"EEXIST\") throw err;\n if (await stealIfStale(lockPath)) continue;\n await new Promise((resolve) => setTimeout(resolve, LOCK_SPIN_MS));\n }\n }\n}\n\nasync function stealIfStale(lockPath: string): Promise<boolean> {\n let holder: string;\n try {\n holder = await readFile(lockPath, \"utf8\");\n } catch (err) {\n // Vanished between the failed create and this read — already free.\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return true;\n throw err;\n }\n const pid = Number(holder.trim());\n if (!Number.isInteger(pid) || pid <= 0) return false;\n if (isProcessAlive(pid)) return false;\n await unlink(lockPath).catch(() => {});\n return true;\n}\n\nfunction isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n return (err as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// NodeSub — the three node-native subscription variants.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The `deps` of a `node_ws` Sub: which socket (`key`) and where it connects\n * (`url`). Both are plain data, so the engine derives the Sub's id from them —\n * the same `{ key, url }` across transitions is the same socket (kept open),\n * and a changed one closes it and opens a fresh one.\n *\n * `key` is the author's name for the socket: the Cmd side addresses it through\n * {@link sendToWebSocket} by that key, since the derived Sub id is not\n * something a Cmd handler can know. It must be unique among the machine's live\n * `node_ws` Subs.\n */\nexport type NodeWsDeps = { readonly key: string; readonly url: string };\n\n/**\n * A node WebSocket sub. Its callbacks are not data, so they live on the runner\n * side — {@link NodeWsHandlers}, handed to {@link nodeSubscribe}.\n */\nexport type NodeWsSub = Sub<\"node_ws\", NodeWsDeps>;\n\n/**\n * The `deps` of a `node_timer` Sub. One-shot `setTimeout` by default;\n * `repeat: true` → `setInterval`.\n */\nexport type NodeTimerDeps<M> = {\n readonly delayMs: number;\n readonly msg: M;\n readonly repeat?: boolean;\n};\n\n/**\n * A node timer sub. Cleanup clears the timer — a timer keyed to a phase is\n * cancelled the moment the machine leaves that phase, and a changed\n * `delayMs` / `msg` / `repeat` is a new id, so the timer restarts. For a plain\n * one-shot, the engine's built-in `timer` Sub needs no runner at all.\n */\nexport type NodeTimerSub<M> = Sub<\"node_timer\", NodeTimerDeps<M>>;\n\n/** The `deps` of a `node_signal` Sub: `signal` (SIGINT/SIGTERM/…) → `msg`. */\nexport type NodeSignalDeps<M> = {\n readonly signal: NodeJS.Signals;\n readonly msg: M;\n};\n\n/**\n * A process-signal sub. Cleanup detaches the listener. Lets graceful shutdown\n * be a real machine transition rather than an out-of-band `process.on` that the\n * reducer can't see.\n */\nexport type NodeSignalSub<M> = Sub<\"node_signal\", NodeSignalDeps<M>>;\n\nexport type NodeSub<M> = NodeWsSub | NodeTimerSub<M> | NodeSignalSub<M>;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Ctx — what the node Subs read / populate.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** The live `node_ws` sockets, keyed by each Sub's `deps.key`. */\nexport type NodeWsRegistry = Map<string, WebSocket>;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// ws — loaded on first use, never at module load.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * `ws` is an OPTIONAL peer: the `node_ws` Sub needs it, `fileStore` and\n * `fileJournal` — everything the tutorial uses — do not. A static\n * `import … from \"ws\"` puts the dependency on the module's load path, so a\n * consumer who installed only the tutorial's three packages could not import\n * this door at all (`ERR_MODULE_NOT_FOUND`). The type import above is erased,\n * and the value arrives through this loader at the moment a socket is actually\n * opened — so the cost of an optional peer is paid by the feature that needs\n * it, not by the door it happens to share.\n *\n * `createRequire` rather than `await import(\"ws\")` because a runner's\n * contract is synchronous — it must return its cleanup on the same tick the\n * engine starts it, and a deferred socket would give cleanup nothing to close.\n */\ntype NodeWsConstructor = new (url: string) => WebSocket;\n\nlet cachedWs: NodeWsConstructor | undefined;\n\nfunction loadWebSocket(): NodeWsConstructor {\n if (cachedWs) return cachedWs;\n let mod: unknown;\n try {\n mod = createRequire(import.meta.url)(\"ws\");\n } catch (cause) {\n throw new Error(\n 'The `node_ws` Sub needs the optional peer dependency \"ws\". Install it: `npm install ws`.',\n { cause },\n );\n }\n const ctor = (mod as { default?: NodeWsConstructor }).default ?? mod;\n cachedWs = ctor as NodeWsConstructor;\n return cachedWs;\n}\n\n/**\n * `WebSocket.OPEN`, inlined. Reading it off the class would drag `ws` onto the\n * load path of `sendToWebSocket`, which is a Cmd-side helper a caller can reach\n * without ever opening a socket. The value is fixed by the WHATWG WebSocket\n * spec, not by `ws`.\n */\nconst WS_OPEN = 1;\n\n/** `WebSocket.CLOSED`, inlined for the same reason as {@link WS_OPEN}. */\nconst WS_CLOSED = 3;\n\n/**\n * The Ctx fields the `node_ws` runner depends on. A consumer's Ctx must extend\n * this so the runner can register the live socket and Cmd handlers can reach\n * it.\n *\n * Deliberately NOT parameterized by Msg: the registry holds raw `WebSocket`s,\n * and the Msg-typed callbacks live in the {@link NodeWsHandlers} handed to\n * `nodeSubscribe`. Trip-wire: if a future node Sub holds a Msg-typed callback\n * on ctx, this becomes `NodeSubscribeCtx<M>`.\n */\nexport interface NodeSubscribeCtx {\n wsRegistry: NodeWsRegistry;\n}\n\n/**\n * Write a frame to the `node_ws` socket whose `deps.key` is `key`. No-op\n * (returns `false`) when the socket is absent, not OPEN, or `send()` throws.\n * The Cmd-side companion to the `node_ws` Sub: the Sub owns the socket's\n * lifetime; this writes to it.\n *\n * The contract is a boolean no-op, never a throw: the socket can transition out\n * of OPEN between the `readyState` check and `send()` (or `ws` can throw on an\n * errored underlying connection), and a Cmd interpreter must not crash on that.\n */\nexport function sendToWebSocket(\n ctx: NodeSubscribeCtx,\n key: string,\n data: string,\n): boolean {\n const socket = ctx.wsRegistry.get(key);\n if (!socket || socket.readyState !== WS_OPEN) return false;\n try {\n socket.send(data);\n return true;\n } catch {\n return false;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// nodeSubscribe — the runner table.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * What a `node_ws` socket's events become. Functions cannot be `deps`, so they\n * live here, on the runner side; each receives the running Sub so one handler\n * table can branch on `sub.deps` (its `key` or `url`) when a machine runs more\n * than one socket.\n *\n * Only `onMessage` returns `M | null` (null drops the frame) because inbound\n * frames are the boundary that needs a parse-and-maybe-discard seam. `onOpen`/\n * `onClose`/`onError` always dispatch — a spurious one is dropped in the reducer\n * (the cell ignores it), not here. An omitted one dispatches nothing.\n */\nexport interface NodeWsHandlers<M> {\n onMessage(data: string, sub: NodeWsSub): M | null;\n onOpen?(sub: NodeWsSub): M;\n onClose?(code: number, reason: string, sub: NodeWsSub): M;\n onError?(message: string, sub: NodeWsSub): M;\n}\n\n/** The options of {@link nodeSubscribe}: the `node_ws` handlers, when used. */\nexport interface NodeSubscribeOpts<M> {\n readonly ws: NodeWsHandlers<M>;\n}\n\n/** The `node_timer` + `node_signal` runners — what every `nodeSubscribe` returns. */\nexport interface NodeSubscribeBase<M> {\n node_timer: (\n sub: NodeTimerSub<M>,\n ctx: unknown,\n dispatch: (msg: M) => void,\n ) => Dispose;\n node_signal: (\n sub: NodeSignalSub<M>,\n ctx: unknown,\n dispatch: (msg: M) => void,\n ) => Dispose;\n}\n\n/** The full runner table, with `node_ws` — `nodeSubscribe({ ws })`. */\nexport interface NodeSubscribeWithWs<M, Ctx extends NodeSubscribeCtx>\n extends NodeSubscribeBase<M> {\n node_ws: (sub: NodeWsSub, ctx: Ctx, dispatch: (msg: M) => void) => Dispose;\n}\n\n/**\n * Build the `subscribe` runners for the node Sub types, to hand to `run`:\n * `run(machine, { subscribe: nodeSubscribe<M, Ctx>({ ws: { onMessage } }), ctx })`.\n *\n * `node_timer` and `node_signal` read everything off `sub.deps`, so they need\n * no options. `node_ws` needs its {@link NodeWsHandlers}; called without them,\n * the table has no `node_ws` runner, so a machine that declares `NodeWsSub`\n * fails to typecheck at `run` rather than opening a socket nobody listens to.\n * `Ctx` must extend {@link NodeSubscribeCtx} so the `node_ws` runner can\n * register its socket.\n */\nexport function nodeSubscribe<M>(): NodeSubscribeBase<M>;\nexport function nodeSubscribe<M, Ctx extends NodeSubscribeCtx>(\n opts: NodeSubscribeOpts<M>,\n): NodeSubscribeWithWs<M, Ctx>;\nexport function nodeSubscribe<M, Ctx extends NodeSubscribeCtx>(\n opts?: NodeSubscribeOpts<M>,\n): NodeSubscribeBase<M> | NodeSubscribeWithWs<M, Ctx> {\n const base: NodeSubscribeBase<M> = {\n node_timer: (sub, _ctx, dispatch) => {\n // Two reconcile lifecycles under one type: a one-shot (`setTimeout`)\n // whose `deps` should go null on the state after it fires, vs a\n // repeating (`setInterval`) one the state keeps on. The `repeat` flag\n // picks the install + cleanup.\n const { delayMs, msg, repeat } = sub.deps;\n if (repeat) {\n const handle = setInterval(() => dispatch(msg), delayMs);\n return () => clearInterval(handle);\n }\n const handle = setTimeout(() => dispatch(msg), delayMs);\n return () => clearTimeout(handle);\n },\n\n node_signal: (sub, _ctx, dispatch) => {\n const { signal, msg } = sub.deps;\n const handler = () => dispatch(msg);\n process.on(signal, handler);\n return () => {\n process.off(signal, handler);\n };\n },\n };\n if (opts === undefined) return base;\n return { ...base, node_ws: nodeWsRunner<M, Ctx>(opts.ws) };\n}\n\nfunction nodeWsRunner<M, Ctx extends NodeSubscribeCtx>(\n handlers: NodeWsHandlers<M>,\n): NodeSubscribeWithWs<M, Ctx>[\"node_ws\"] {\n return (sub, ctx, dispatch) => {\n const { key, url } = sub.deps;\n // Two live sockets under one key would make `sendToWebSocket` ambiguous,\n // and the first one's cleanup would unregister the second. The engine\n // stops a Sub before it starts its replacement, so a key still held here\n // is two concurrent Subs sharing it — an authoring bug, refused loudly.\n if (ctx.wsRegistry.has(key)) {\n throw new Error(\n `node_ws: a live socket is already registered under key \"${key}\" — each running node_ws Sub needs its own \\`deps.key\\`.`,\n );\n }\n const socket = new (loadWebSocket())(url);\n ctx.wsRegistry.set(key, socket);\n\n socket.on(\"open\", () => {\n if (handlers.onOpen) dispatch(handlers.onOpen(sub));\n });\n socket.on(\"message\", (data: RawData) => {\n dispatchIfPresent(dispatch, handlers.onMessage(data.toString(), sub));\n });\n socket.on(\"close\", (code: number, reason: Buffer) => {\n if (handlers.onClose)\n dispatch(handlers.onClose(code, reason.toString(), sub));\n });\n socket.on(\"error\", (err: Error) => {\n if (handlers.onError) dispatch(handlers.onError(err.message, sub));\n });\n\n return () => {\n ctx.wsRegistry.delete(key);\n socket.removeAllListeners();\n // Swallow teardown-induced errors: terminate()-ing a CONNECTING socket\n // emits an 'error' (\"closed before the connection was established\"),\n // and with our listeners gone that would be an unhandled 'error' that\n // crashes the process. A no-op listener absorbs it.\n socket.on(\"error\", () => {});\n // Close for ANY non-closed state: a graceful 1000 when OPEN, a hard\n // terminate() otherwise (CONNECTING/CLOSING) so a socket caught mid-\n // handshake can't dangle. This is the teardown the hand-driven Cmd-pair\n // approach (`cleanup_browser` closing the ws only `if OPEN`) could not\n // guarantee.\n if (socket.readyState === WS_OPEN) {\n socket.close(1000, \"tea-node sub cleanup\");\n } else if (socket.readyState !== WS_CLOSED) {\n socket.terminate();\n }\n };\n };\n}\n"]}
@@ -1,10 +1,9 @@
1
- import { a as ResilientState, f as ResilientTimerMsg, R as RunCmd, F as FailMsg, S as SucceedMsg, C as CircuitConfig, b as RateLimitConfig, D as DeadlineConfig, c as ResilientPorts, d as ResilientHandlers } from '../index-CA_7I5g9.js';
2
- import { C as Cmd } from '../core-DnzqWy6u.js';
1
+ import { C as Cmd, b as CmdDef, T as TimerDeps } from '../core-BbgtiXdg.js';
3
2
  import { RetryPolicy } from '../retry-backoff/index.js';
4
- import { a as DeadlineSub } from '../index-HSYgG2Yt.js';
5
- export { b as deadlineSub, s as subscribeDeadline } from '../index-HSYgG2Yt.js';
6
- import 'zod';
7
- import '../types-BseNPIhr.js';
3
+ import { a as DeadlineSub } from '../index-Xufl55W8.js';
4
+ import { a as ResilientState, b as ResilientTimerMsg, R as RunCmd, F as FailMsg, S as SucceedMsg, f as CircuitConfig, l as RateLimitConfig, m as DeadlineConfig } from '../index-Cou86-GX.js';
5
+ import '@standard-schema/spec';
6
+ import '../types-LxZxGYda.js';
8
7
 
9
8
  /**
10
9
  * internal/paginate/paginator — the cursor/offset/page-token walk loop as pure
@@ -350,21 +349,15 @@ interface PaginatedWalkState<Cursor, Page> {
350
349
  /**
351
350
  * The page-fetch effect this knob emits: the inherited `resilient_run` Cmd from
352
351
  * resilient-call, whose `input` is the `Cursor` to fetch and whose `key` is the
353
- * fixed `PAGE_KEY`. The consumer's `handlers(ports)` interprets it.
352
+ * fixed `PAGE_KEY`. It is `Cmd.define`d; the handler you write for it fetches
353
+ * one page and returns `ok(page)` or `err({ _tag: "port_rejected", … })`.
354
354
  */
355
355
  type FetchPageCmd<Cursor> = RunCmd<Cursor>;
356
- /** Page-settled Msgs the `handlers` port dispatches back (inherited verbatim). */
356
+ /** The page-settled Msgs the engine mints from that handler's outcome. */
357
357
  type PageOkMsg<Page> = SucceedMsg<Page>;
358
358
  type PageErrMsg = FailMsg;
359
359
  /** The retry / deadline timer Msg — inherited from resilient-call. */
360
360
  type PaginatedWalkTimerMsg = ResilientTimerMsg;
361
- /**
362
- * Ports the consumer supplies to `handlers`. `run(cursor)` fetches ONE page for
363
- * the given cursor; throwing routes to `pageErr` (and thus the resilience
364
- * backoff). Same shape as resilient-call's port, specialized to the page fetch.
365
- */
366
- interface PaginatedWalkPorts<Cursor, Page> extends ResilientPorts<Cursor, Page> {
367
- }
368
361
  /** The single resilient-call key every page fetch runs under. */
369
362
  declare const PAGE_KEY = "page";
370
363
  /**
@@ -373,24 +366,36 @@ declare const PAGE_KEY = "page";
373
366
  * defaults to `Math.random` (read at the verb boundary inside resilient-call,
374
367
  * never in a `paginated-walk` verb). Inherited straight from resilient-call.
375
368
  *
376
- * Returns the uniform L2 knob contract: `init()`, the verbs `start` / `pageOk`
377
- * / `pageErr` / `onTimer`, `subs(state)`, and `handlers(ports)`. `Cursor` is
369
+ * Returns plain functions: `init()`, the verbs `start` / `pageOk` / `pageErr`
370
+ * / `onTimer`, `deadlines(state)` / `timer(state)`, and the page-fetch Cmd def
371
+ * (`fetch`) to list in the machine's `cmds`. `Cursor` is
378
372
  * the page handle, `Page` the fetch result, `EmittedCmd` the consumer's
379
373
  * per-page Cmds plus the inherited `FetchPageCmd`.
380
374
  */
381
375
  declare function createPaginatedWalk<Cursor, Page, EmittedCmd extends Cmd = Cmd>(config: PaginatedWalkConfig<Cursor, Page, EmittedCmd>, rng?: () => number): {
376
+ /** The page-fetch Cmd def — list it in the machine's `cmds`. */
377
+ fetch: CmdDef<"resilient_run", {
378
+ readonly key: string;
379
+ readonly input: Cursor;
380
+ }, Page, {
381
+ readonly [detail: string]: unknown;
382
+ readonly _tag: "deadline_exceeded";
383
+ } | {
384
+ readonly [detail: string]: unknown;
385
+ readonly _tag: "port_rejected";
386
+ }>;
382
387
  init: () => PaginatedWalkState<Cursor, Page>;
383
388
  start: (s: PaginatedWalkState<Cursor, Page>, at: number) => readonly [PaginatedWalkState<Cursor, Page>, readonly (EmittedCmd | FetchPageCmd<Cursor>)[]];
384
- pageOk: (s: PaginatedWalkState<Cursor, Page>, page: Page, at: number) => readonly [PaginatedWalkState<Cursor, Page>, readonly (EmittedCmd | FetchPageCmd<Cursor>)[]];
385
- pageErr: (s: PaginatedWalkState<Cursor, Page>, error: unknown, at: number) => readonly [PaginatedWalkState<Cursor, Page>, readonly (EmittedCmd | FetchPageCmd<Cursor>)[]];
389
+ pageOk: (s: PaginatedWalkState<Cursor, Page>, msg: PageOkMsg<Page>) => readonly [PaginatedWalkState<Cursor, Page>, readonly (EmittedCmd | FetchPageCmd<Cursor>)[]];
390
+ pageErr: (s: PaginatedWalkState<Cursor, Page>, msg: PageErrMsg) => readonly [PaginatedWalkState<Cursor, Page>, readonly (EmittedCmd | FetchPageCmd<Cursor>)[]];
386
391
  onTimer: (s: PaginatedWalkState<Cursor, Page>, msg: PaginatedWalkTimerMsg) => readonly [PaginatedWalkState<Cursor, Page>, readonly (EmittedCmd | FetchPageCmd<Cursor>)[]];
387
392
  drain: (s: PaginatedWalkState<Cursor, Page>, n: number) => readonly [PaginatedWalkState<Cursor, Page>, readonly (EmittedCmd | FetchPageCmd<Cursor>)[]];
388
393
  resume: (s: PaginatedWalkState<Cursor, Page>, at: number) => readonly [PaginatedWalkState<Cursor, Page>, readonly (EmittedCmd | FetchPageCmd<Cursor>)[]];
389
394
  isComplete: (s: PaginatedWalkState<Cursor, Page>) => boolean;
390
395
  failure: (s: PaginatedWalkState<Cursor, Page>) => unknown;
391
396
  isStuck: (s: PaginatedWalkState<Cursor, Page>) => boolean;
392
- subs: (s: PaginatedWalkState<Cursor, Page>) => readonly DeadlineSub[];
393
- handlers: (ports: PaginatedWalkPorts<Cursor, Page>) => ResilientHandlers<Cursor, Page, "resilient">;
397
+ deadlines: (s: PaginatedWalkState<Cursor, Page>) => readonly DeadlineSub[];
398
+ timer: (s: PaginatedWalkState<Cursor, Page>) => TimerDeps<ResilientTimerMsg<"resilient">> | null;
394
399
  };
395
400
  /**
396
401
  * Lift a knob result `[slice, cmds]` into a host `[State, cmds]` where the slice
@@ -402,4 +407,4 @@ declare function liftWalk<S extends {
402
407
  walk: PaginatedWalkState<Cursor, Page>;
403
408
  }, Cursor, Page, C extends Cmd>(state: S, [slice, cmds]: readonly [PaginatedWalkState<Cursor, Page>, readonly C[]]): readonly [S, readonly C[]];
404
409
 
405
- export { DeadlineSub, type FetchPageCmd, PAGE_KEY, type PageErrMsg, type PageOkMsg, type PaginatedWalkConfig, type PaginatedWalkPorts, type PaginatedWalkState, type PaginatedWalkTimerMsg, type PaginatorPolicy, type PaginatorState, createPaginatedWalk, defaultPaginatorPolicy, drain, initPaginator, isDone, liftWalk, recordPage, resume, start };
410
+ export { type FetchPageCmd, PAGE_KEY, type PageErrMsg, type PageOkMsg, type PaginatedWalkConfig, type PaginatedWalkState, type PaginatedWalkTimerMsg, type PaginatorPolicy, type PaginatorState, createPaginatedWalk, defaultPaginatorPolicy, drain, initPaginator, isDone, liftWalk, recordPage, resume, start };
@@ -1,7 +1,9 @@
1
- export { PAGE_KEY, createPaginatedWalk, defaultPaginatorPolicy, drain, initPaginator, isDone, liftWalk, recordPage, resume, start } from '../chunk-Q5CQTBFA.js';
2
- import '../chunk-AYS5N6WY.js';
1
+ export { PAGE_KEY, createPaginatedWalk, defaultPaginatorPolicy, drain, initPaginator, isDone, liftWalk, recordPage, resume, start } from '../chunk-NSIAORRY.js';
2
+ import '../chunk-6NG4V5SB.js';
3
3
  import '../chunk-VBDU4JL6.js';
4
- export { deadlineSub, subscribeDeadline } from '../chunk-22POWGFA.js';
5
- import '../chunk-66PY5WYF.js';
4
+ import '../chunk-4LPY67E4.js';
5
+ import '../chunk-IVJP3GKJ.js';
6
+ import '../chunk-GOX2CYIM.js';
7
+ import '../chunk-CH5KTOEZ.js';
6
8
  //# sourceMappingURL=index.js.map
7
9
  //# sourceMappingURL=index.js.map
@@ -1,8 +1,7 @@
1
- import { B as BootingRuntime } from '../runtime-types-CMF13BBh.js';
2
- import { C as Cmd, S as Sub, M as Machine } from '../core-DnzqWy6u.js';
3
- import { T as Trace, R as RecorderOptions } from '../index-DrIRb_Ch.js';
4
- import 'better-result';
5
- import 'zod';
1
+ import { B as BootingRuntime } from '../runtime-types-CiVpC0HN.js';
2
+ import { C as Cmd, S as Sub, M as Machine } from '../core-BbgtiXdg.js';
3
+ import { T as Trace, R as RecorderOptions } from '../index-DU6JpQ8G.js';
4
+ import '@standard-schema/spec';
6
5
 
7
6
  /**
8
7
  * the record → replay → normalized-diff go/no-go gate.
@@ -1,6 +1,7 @@
1
- import { recorder, deepEqual } from '../chunk-V46HSVCK.js';
1
+ import { recorder, deepEqual } from '../chunk-BCTEOVOU.js';
2
+ import '../chunk-GOX2CYIM.js';
3
+ import { replay } from '../chunk-CH5KTOEZ.js';
2
4
  import '../chunk-HWVW4AXC.js';
3
- import { replay } from '../chunk-66PY5WYF.js';
4
5
 
5
6
  // src/parity/index.ts
6
7
  function recordRun(runtime, opts) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/parity/index.ts"],"names":["key"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * the record → replay → normalized-diff go/no-go gate.\n *\n * The parity spike proved the mechanism already composes from in-tree\n * primitives: `recorder` captures a run's Msg sequence (the recording), `replay`\n * re-folds it purely, and `trace-replay`'s order-insensitive deep-equal diffs\n * the result. Non-determinism is a RECORD-TIME property — ids/timestamps freeze\n * into the recording, `replay` never re-mints them — so the same recording\n * replays byte-identical every time.\n *\n * This module is the thin packaging over that substrate plus the one genuinely\n * new helper, `normalizeForParity`: the finding-normalizer that strips\n * record-time non-determinism (generated ids/timestamps) and stable-key-sorts\n * collections, so an OLD-engine golden run and a NEW-engine run diff as equal\n * exactly when they are behaviourally equivalent. Every domain (audit, auth,\n * ingest, …) gets its old-vs-new parity check without reinventing the capture,\n * the normalization, and the diff.\n *\n * ```text\n * recordRun(runtime) → Recording // wraps the recorder\n * goldenReplay(machine, rec) → State // wraps replay\n * normalizeForParity(schema) → (finding) => norm // strip + stable-sort\n * parityEqual(a, b) → boolean // normalized deep-equal\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { BootingRuntime, Cmd, Machine, Sub } from \"../index\";\nimport { replay } from \"../index\";\nimport {\n type RecorderOptions,\n recorder,\n type Trace,\n} from \"../internal/persistence/recorder\";\nimport { deepEqual } from \"../internal/persistence/trace-replay\";\n\n/**\n * The two recorder types this module's own signatures name — `Trace` is what\n * {@link Recording.trace} returns and what {@link goldenReplay} accepts bare,\n * `RecorderOptions` is what {@link recordRun} takes. The recorder itself is\n * internal, so this door is the only place either type can be named from\n * outside the package.\n */\nexport type { RecorderOptions, Trace } from \"../internal/persistence/recorder\";\n\n/**\n * A live parity recording attached to a Runtime — the go/no-go gate's golden\n * artifact. Wraps a {@link recorder}: drive the runtime, then snapshot with\n * {@link Recording.trace} (or hand the live `Recording` straight to\n * {@link goldenReplay}, which snapshots for you).\n *\n * @typeParam S - the machine's State type.\n * @typeParam M - the machine's Msg union.\n */\nexport interface Recording<S, M> {\n /**\n * Snapshot the recording so far as a replayable {@link Trace} — `loaded`\n * (boot state), the ordered non-null `msgs`, and `finalState`. Sufficient to\n * reproduce the run via {@link goldenReplay}. Fresh object each call.\n */\n trace(): Trace<S, M>;\n /** Serialize the recording as JSONL (see {@link recorder}'s `toJSONL`). */\n toJSONL(): string;\n /** Detach the underlying observer. Idempotent. */\n stop(): void;\n}\n\n/**\n * Attach a parity recording to a Runtime, reusing the `recorder`/`historyTracker`\n * capture (the boot observe for `loaded` + every non-null msg in order) rather\n * than a fresh observer path.\n *\n * Attach on the synchronous `run()` handle so the boot transition is captured\n * as `loaded`; drive the runtime; then `trace()` (or pass the handle to\n * {@link goldenReplay}). See {@link RecorderOptions} for `captureSteps` (needed\n * only for the per-step / resume-cache-prefix inspection, not for replay).\n */\nexport function recordRun<S, M extends { type: string }>(\n runtime: BootingRuntime<S, M>,\n opts?: RecorderOptions,\n): Recording<S, M> {\n const rec = recorder(runtime, opts);\n return {\n trace: () => rec.dump(),\n toJSONL: () => rec.toJSONL(),\n stop: () => rec.stop(),\n };\n}\n\n/**\n * Reproduce a recording's final state by folding its msgs through the pure\n * `replay` — `init(loaded, ctx)` then `update` per msg, never `interpret`,\n * never a `Store`, never a live subscription. Accepts a live {@link Recording}\n * (snapshotted here) or a bare {@link Trace}.\n *\n * `ctx` is the ENVIRONMENT supplied fresh at replay time (the trace is the\n * INPUT: `loaded` + `msgs`), mirroring `replayTrace`. It is optional so a\n * void-ctx machine can call `goldenReplay(machine, rec)`; a machine that reads\n * ctx passes it explicitly.\n */\nexport function goldenReplay<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n recording: Recording<S, M> | Trace<S, M>,\n ctx?: Ctx,\n): S {\n const trace = asTrace(recording);\n const { state } = replay(machine, {\n msgs: trace.msgs,\n ctx: ctx as Ctx,\n loaded: trace.loaded,\n });\n return state;\n}\n\n/** Narrow a `Recording | Trace` to a `Trace`, snapshotting a live recording. */\nfunction asTrace<S, M>(recording: Recording<S, M> | Trace<S, M>): Trace<S, M> {\n return typeof (recording as Recording<S, M>).trace === \"function\"\n ? (recording as Recording<S, M>).trace()\n : (recording as Trace<S, M>);\n}\n\n/** Configuration for {@link normalizeForParity}. */\nexport interface ParitySchema {\n /**\n * Object keys stripped wherever they appear — the record-time\n * non-determinism `replay` never re-mints. Default: `id`, `runId`,\n * `timestamp`.\n */\n readonly stripKeys?: readonly string[];\n /**\n * Candidate keys used to stable-sort arrays of objects. For each array the\n * first candidate present (as a primitive) on the items decides the order;\n * arrays with no candidate fall back to canonical-JSON order. Default:\n * `ruleId`, `selector`, `url`.\n */\n readonly sortKeys?: readonly string[];\n}\n\n// Defaults proven by the spike: strip the generated id/runId/timestamp,\n// stable-key-sort by ruleId | selector | url. Fan-out ordering is already\n// deterministic on this substrate (array order); the sort only defends against\n// intra-effect concurrency (Promise.all / Set).\nconst DEFAULT_STRIP_KEYS: readonly string[] = [\"id\", \"runId\", \"timestamp\"];\nconst DEFAULT_SORT_KEYS: readonly string[] = [\"ruleId\", \"selector\", \"url\"];\n\n/**\n * Build a finding-normalizer: a pure `(value) => normalized` that strips the\n * schema's `stripKeys`, recursively sorts object keys, and stable-key-sorts\n * arrays by the schema's `sortKeys`. Two runs that differ only in generated\n * ids/timestamps or intra-effect ordering normalize to a byte-identical value,\n * so {@link parityEqual} on the normalized pair reflects behavioural (not incidental)\n * difference.\n */\nexport function normalizeForParity(\n schema?: ParitySchema,\n): (value: unknown) => unknown {\n const stripKeys = new Set(schema?.stripKeys ?? DEFAULT_STRIP_KEYS);\n const sortKeys = schema?.sortKeys ?? DEFAULT_SORT_KEYS;\n\n const normalize = (value: unknown): unknown => {\n if (Array.isArray(value)) {\n const items = value.map(normalize);\n // Decorate-sort-undecorate: primary key = first present sortKey value,\n // secondary = canonical JSON (items are already normalized so keys are\n // sorted), original index last for a stable total order.\n return items\n .map((item, index) => ({\n item,\n index,\n primary: primarySortKey(item, sortKeys),\n json: JSON.stringify(item) ?? \"\",\n }))\n .sort(\n (a, b) =>\n compareStrings(a.primary, b.primary) ||\n compareStrings(a.json, b.json) ||\n a.index - b.index,\n )\n .map((decorated) => decorated.item);\n }\n if (isPlainObject(value)) {\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(value)\n .filter((key) => !stripKeys.has(key))\n .sort()) {\n out[key] = normalize(value[key]);\n }\n return out;\n }\n return value;\n };\n\n return normalize;\n}\n\n/** The first present `sortKeys` candidate on `item` as a string, or `\"\"`. */\nfunction primarySortKey(item: unknown, sortKeys: readonly string[]): string {\n if (!isPlainObject(item)) return \"\";\n for (const key of sortKeys) {\n const candidate = item[key];\n if (\n typeof candidate === \"string\" ||\n typeof candidate === \"number\" ||\n typeof candidate === \"boolean\"\n ) {\n return String(candidate);\n }\n }\n return \"\";\n}\n\nfunction compareStrings(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * The parity verdict: `true` iff `a` and `b` are structurally equal. Reuses\n * `trace-replay`'s order-insensitive deep-equal (the comparator the\n * record/replay lane owns) rather than adding another boolean deep-compare —\n * feed it the pair produced by {@link normalizeForParity} for the\n * normalized-JSON equality the gate rests on.\n */\nexport function parityEqual(a: unknown, b: unknown): boolean {\n return deepEqual(a, b);\n}\n"]}
1
+ {"version":3,"sources":["../../src/parity/index.ts"],"names":["key"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * the record → replay → normalized-diff go/no-go gate.\n *\n * The parity spike proved the mechanism already composes from in-tree\n * primitives: `recorder` captures a run's Msg sequence (the recording), `replay`\n * re-folds it purely, and `trace-replay`'s order-insensitive deep-equal diffs\n * the result. Non-determinism is a RECORD-TIME property — ids/timestamps freeze\n * into the recording, `replay` never re-mints them — so the same recording\n * replays byte-identical every time.\n *\n * This module is the thin packaging over that substrate plus the one genuinely\n * new helper, `normalizeForParity`: the finding-normalizer that strips\n * record-time non-determinism (generated ids/timestamps) and stable-key-sorts\n * collections, so an OLD-engine golden run and a NEW-engine run diff as equal\n * exactly when they are behaviourally equivalent. Every domain (audit, auth,\n * ingest, …) gets its old-vs-new parity check without reinventing the capture,\n * the normalization, and the diff.\n *\n * ```text\n * recordRun(runtime) → Recording // wraps the recorder\n * goldenReplay(machine, rec) → State // wraps replay\n * normalizeForParity(schema) → (finding) => norm // strip + stable-sort\n * parityEqual(a, b) → boolean // normalized deep-equal\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { BootingRuntime, Cmd, Machine, Sub } from \"../index\";\nimport { replay } from \"../index\";\nimport {\n type RecorderOptions,\n recorder,\n type Trace,\n} from \"../internal/persistence/recorder\";\nimport { deepEqual } from \"../internal/persistence/trace-replay\";\n\n/**\n * The two recorder types this module's own signatures name — `Trace` is what\n * {@link Recording.trace} returns and what {@link goldenReplay} accepts bare,\n * `RecorderOptions` is what {@link recordRun} takes. The recorder itself is\n * internal, so this door is the only place either type can be named from\n * outside the package.\n */\nexport type { RecorderOptions, Trace } from \"../internal/persistence/recorder\";\n\n/**\n * A live parity recording attached to a Runtime — the go/no-go gate's golden\n * artifact. Wraps a {@link recorder}: drive the runtime, then snapshot with\n * {@link Recording.trace} (or hand the live `Recording` straight to\n * {@link goldenReplay}, which snapshots for you).\n *\n * @typeParam S - the machine's State type.\n * @typeParam M - the machine's Msg union.\n */\nexport interface Recording<S, M> {\n /**\n * Snapshot the recording so far as a replayable {@link Trace} — `loaded`\n * (boot state), the ordered non-null `msgs`, and `finalState`. Sufficient to\n * reproduce the run via {@link goldenReplay}. Fresh object each call.\n */\n trace(): Trace<S, M>;\n /** Serialize the recording as JSONL (see {@link recorder}'s `toJSONL`). */\n toJSONL(): string;\n /** Detach the underlying observer. Idempotent. */\n stop(): void;\n}\n\n/**\n * Attach a parity recording to a Runtime, reusing the `recorder`/`historyTracker`\n * capture (the boot observe for `loaded` + every non-null msg in order) rather\n * than a fresh observer path.\n *\n * Attach on the synchronous `run()` handle so the boot transition is captured\n * as `loaded`; drive the runtime; then `trace()` (or pass the handle to\n * {@link goldenReplay}). See {@link RecorderOptions} for `captureSteps` (needed\n * only for the per-step / resume-cache-prefix inspection, not for replay).\n */\nexport function recordRun<S, M extends { type: string }>(\n runtime: BootingRuntime<S, M>,\n opts?: RecorderOptions,\n): Recording<S, M> {\n const rec = recorder(runtime, opts);\n return {\n trace: () => rec.dump(),\n toJSONL: () => rec.toJSONL(),\n stop: () => rec.stop(),\n };\n}\n\n/**\n * Reproduce a recording's final state by folding its msgs through the pure\n * `replay` — `init(loaded, ctx)` then `update` per msg, never `interpret`,\n * never a `Store`, never a live subscription. Accepts a live {@link Recording}\n * (snapshotted here) or a bare {@link Trace}.\n *\n * `ctx` is the ENVIRONMENT supplied fresh at replay time (the trace is the\n * INPUT: `loaded` + `msgs`), mirroring `replayTrace`. It is optional so a\n * void-ctx machine can call `goldenReplay(machine, rec)`; a machine that reads\n * ctx passes it explicitly.\n */\nexport function goldenReplay<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n recording: Recording<S, M> | Trace<S, M>,\n ctx?: Ctx,\n): S {\n const trace = asTrace(recording);\n const { state } = replay(machine, {\n msgs: trace.msgs,\n ctx: ctx as Ctx,\n loaded: trace.loaded,\n });\n return state;\n}\n\n/** Narrow a `Recording | Trace` to a `Trace`, snapshotting a live recording. */\nfunction asTrace<S, M>(recording: Recording<S, M> | Trace<S, M>): Trace<S, M> {\n return typeof (recording as Recording<S, M>).trace === \"function\"\n ? (recording as Recording<S, M>).trace()\n : (recording as Trace<S, M>);\n}\n\n/** Configuration for {@link normalizeForParity}. */\nexport interface ParitySchema {\n /**\n * Object keys stripped wherever they appear — the record-time\n * non-determinism `replay` never re-mints. Default: `id`, `runId`,\n * `timestamp`.\n */\n readonly stripKeys?: readonly string[];\n /**\n * Candidate keys used to stable-sort arrays of objects. For each array the\n * first candidate present (as a primitive) on the items decides the order;\n * arrays with no candidate fall back to canonical-JSON order. Default:\n * `ruleId`, `selector`, `url`.\n */\n readonly sortKeys?: readonly string[];\n}\n\n// Defaults proven by the spike: strip the generated id/runId/timestamp,\n// stable-key-sort by ruleId | selector | url. Fan-out ordering is already\n// deterministic on this substrate (array order); the sort only defends against\n// intra-effect concurrency (Promise.all / Set).\nconst DEFAULT_STRIP_KEYS: readonly string[] = [\"id\", \"runId\", \"timestamp\"];\nconst DEFAULT_SORT_KEYS: readonly string[] = [\"ruleId\", \"selector\", \"url\"];\n\n/**\n * Build a finding-normalizer: a pure `(value) => normalized` that strips the\n * schema's `stripKeys`, recursively sorts object keys, and stable-key-sorts\n * arrays by the schema's `sortKeys`. Two runs that differ only in generated\n * ids/timestamps or intra-effect ordering normalize to a byte-identical value,\n * so {@link parityEqual} on the normalized pair reflects behavioural (not incidental)\n * difference.\n */\nexport function normalizeForParity(\n schema?: ParitySchema,\n): (value: unknown) => unknown {\n const stripKeys = new Set(schema?.stripKeys ?? DEFAULT_STRIP_KEYS);\n const sortKeys = schema?.sortKeys ?? DEFAULT_SORT_KEYS;\n\n const normalize = (value: unknown): unknown => {\n if (Array.isArray(value)) {\n const items = value.map(normalize);\n // Decorate-sort-undecorate: primary key = first present sortKey value,\n // secondary = canonical JSON (items are already normalized so keys are\n // sorted), original index last for a stable total order.\n return items\n .map((item, index) => ({\n item,\n index,\n primary: primarySortKey(item, sortKeys),\n json: JSON.stringify(item) ?? \"\",\n }))\n .sort(\n (a, b) =>\n compareStrings(a.primary, b.primary) ||\n compareStrings(a.json, b.json) ||\n a.index - b.index,\n )\n .map((decorated) => decorated.item);\n }\n if (isPlainObject(value)) {\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(value)\n .filter((key) => !stripKeys.has(key))\n .sort()) {\n out[key] = normalize(value[key]);\n }\n return out;\n }\n return value;\n };\n\n return normalize;\n}\n\n/** The first present `sortKeys` candidate on `item` as a string, or `\"\"`. */\nfunction primarySortKey(item: unknown, sortKeys: readonly string[]): string {\n if (!isPlainObject(item)) return \"\";\n for (const key of sortKeys) {\n const candidate = item[key];\n if (\n typeof candidate === \"string\" ||\n typeof candidate === \"number\" ||\n typeof candidate === \"boolean\"\n ) {\n return String(candidate);\n }\n }\n return \"\";\n}\n\nfunction compareStrings(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * The parity verdict: `true` iff `a` and `b` are structurally equal. Reuses\n * `trace-replay`'s order-insensitive deep-equal (the comparator the\n * record/replay lane owns) rather than adding another boolean deep-compare —\n * feed it the pair produced by {@link normalizeForParity} for the\n * normalized-JSON equality the gate rests on.\n */\nexport function parityEqual(a: unknown, b: unknown): boolean {\n return deepEqual(a, b);\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import * as fc from 'fast-check';
2
- import { C as Cmd, S as Sub, M as Machine, d as Reducer, T as Transitions, U as UpdateForm } from '../core-DnzqWy6u.js';
3
- import 'zod';
2
+ import { C as Cmd, S as Sub, M as Machine, i as Reducer, k as Transitions, U as UpdateForm } from '../core-BbgtiXdg.js';
3
+ import '@standard-schema/spec';
4
4
 
5
5
  /**
6
6
  * Build a Ctx value whose every property access throws. Use as the `ctx`
package/dist/pbt/index.js CHANGED
@@ -1,4 +1,5 @@
1
- import { replay, applyCell, msgKeysOf } from '../chunk-66PY5WYF.js';
1
+ import '../chunk-GOX2CYIM.js';
2
+ import { replay, applyCell, msgKeysOf } from '../chunk-CH5KTOEZ.js';
2
3
  import * as fc from 'fast-check';
3
4
 
4
5
  // src/pbt/arbitraries/ctx.ts