@demlik/tea 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/agent/index.d.ts +182 -35
- package/dist/agent/index.js +11 -11
- package/dist/{chunk-KWNCM6XM.js → chunk-AHW2CLPT.js} +65 -18
- package/dist/chunk-AHW2CLPT.js.map +1 -0
- package/dist/{chunk-K4ETBNKS.js → chunk-AIIXV4XB.js} +38 -9
- package/dist/chunk-AIIXV4XB.js.map +1 -0
- package/dist/{chunk-LYVOGOBC.js → chunk-BRX3243I.js} +3 -3
- package/dist/{chunk-LYVOGOBC.js.map → chunk-BRX3243I.js.map} +1 -1
- package/dist/{chunk-GWWCCSDH.js → chunk-C3G2QFEZ.js} +4 -4
- package/dist/{chunk-GWWCCSDH.js.map → chunk-C3G2QFEZ.js.map} +1 -1
- package/dist/{chunk-C4U5D5HP.js → chunk-FNGZMVIT.js} +3 -3
- package/dist/chunk-FNGZMVIT.js.map +1 -0
- package/dist/{chunk-QWIPH4YH.js → chunk-H2ELKHI6.js} +16 -6
- package/dist/chunk-H2ELKHI6.js.map +1 -0
- package/dist/chunk-H3MWNH7A.js +119 -0
- package/dist/chunk-H3MWNH7A.js.map +1 -0
- package/dist/chunk-I7E7AD2S.js +213 -0
- package/dist/chunk-I7E7AD2S.js.map +1 -0
- package/dist/{chunk-OYR7ENKU.js → chunk-IHUNKHDX.js} +3 -3
- package/dist/{chunk-OYR7ENKU.js.map → chunk-IHUNKHDX.js.map} +1 -1
- package/dist/chunk-JVE2LBFJ.js +21 -0
- package/dist/chunk-JVE2LBFJ.js.map +1 -0
- package/dist/chunk-JVKU2UQC.js +34 -0
- package/dist/chunk-JVKU2UQC.js.map +1 -0
- package/dist/{chunk-OPH6Y4UT.js → chunk-JYNYG5SF.js} +4 -4
- package/dist/{chunk-OPH6Y4UT.js.map → chunk-JYNYG5SF.js.map} +1 -1
- package/dist/{chunk-XBZQT2TD.js → chunk-PCJA2HTZ.js} +437 -162
- package/dist/chunk-PCJA2HTZ.js.map +1 -0
- package/dist/{chunk-DFUEAA6L.js → chunk-PQSDUX6B.js} +3 -3
- package/dist/{chunk-DFUEAA6L.js.map → chunk-PQSDUX6B.js.map} +1 -1
- package/dist/{chunk-6XBEYZME.js → chunk-WTSOW2MD.js} +5 -5
- package/dist/{chunk-6XBEYZME.js.map → chunk-WTSOW2MD.js.map} +1 -1
- package/dist/{chunk-4PXNZ5UY.js → chunk-XVIKSRCK.js} +4 -4
- package/dist/{chunk-4PXNZ5UY.js.map → chunk-XVIKSRCK.js.map} +1 -1
- package/dist/{chunk-4NTCPTNQ.js → chunk-YIMOFXST.js} +3 -3
- package/dist/{chunk-4NTCPTNQ.js.map → chunk-YIMOFXST.js.map} +1 -1
- package/dist/{core-rDbOaJTN.d.ts → core-Ce9ncpuv.d.ts} +5 -4
- package/dist/do/index.d.ts +140 -64
- package/dist/do/index.js +17 -18
- package/dist/do/index.js.map +1 -1
- package/dist/drive-loop-CsUsW3Wo.d.ts +127 -0
- package/dist/{durable-effects-CNaw97Be.d.ts → durable-effects-BmUrRA-h.d.ts} +1 -1
- package/dist/effect/index.d.ts +6 -150
- package/dist/effect/index.js +4 -96
- package/dist/effect/index.js.map +1 -1
- package/dist/extension/index.d.ts +7 -5
- package/dist/extension/index.js +2 -3
- package/dist/extension/index.js.map +1 -1
- package/dist/flow/index.d.ts +13 -13
- package/dist/flow/index.js +13 -12
- package/dist/flow/index.js.map +1 -1
- package/dist/idempotency/index.d.ts +3 -3
- package/dist/idempotency/index.js +4 -4
- package/dist/{index-q1PoukOZ.d.ts → index-2kNxy6az.d.ts} +2 -2
- package/dist/{index-B_BxplWi.d.ts → index-B1Wzarkl.d.ts} +1 -1
- package/dist/{index-Cberb5Rr.d.ts → index-Bf9_bNql.d.ts} +2 -2
- package/dist/index-BnsAqn0-.d.ts +286 -0
- package/dist/{index-Cx0F2M34.d.ts → index-Ce5GYAuI.d.ts} +2 -2
- package/dist/{index-CWmsXNdX.d.ts → index-D8sYVzCI.d.ts} +1 -1
- package/dist/{index-CcrKLtse.d.ts → index-DWdg7R1l.d.ts} +1 -1
- package/dist/{index-BOrsbdAf.d.ts → index-Dy3379ve.d.ts} +5 -5
- package/dist/{index-DaX_P35Q.d.ts → index-e36uEyl4.d.ts} +3 -3
- package/dist/index.d.ts +109 -36
- package/dist/index.js +2 -2
- package/dist/jev/index.d.ts +8 -8
- package/dist/jev/index.js +6 -7
- package/dist/jev/index.js.map +1 -1
- package/dist/{machine-BcfCFMYF.d.ts → machine-CuojfZup.d.ts} +353 -57
- package/dist/machine-viz/index.d.ts +1 -1
- package/dist/machine-viz/index.js +2 -2
- package/dist/mem/index.d.ts +6 -6
- package/dist/mem/index.js +6 -2
- package/dist/mem/index.js.map +1 -1
- package/dist/node/index.d.ts +16 -10
- package/dist/node/index.js +22 -3
- package/dist/node/index.js.map +1 -1
- package/dist/otel/index.d.ts +161 -0
- package/dist/otel/index.js +403 -0
- package/dist/otel/index.js.map +1 -0
- package/dist/paginate/index.d.ts +4 -4
- package/dist/paginate/index.js +5 -5
- package/dist/parity/index.d.ts +3 -3
- package/dist/parity/index.js +3 -3
- package/dist/pbt/index.d.ts +166 -53
- package/dist/pbt/index.js +2 -2
- package/dist/pbt/index.js.map +1 -1
- package/dist/persistence/index.d.ts +5 -5
- package/dist/persistence/index.js +5 -5
- package/dist/promise/index.d.ts +2 -2
- package/dist/promise/index.js +4 -3
- package/dist/react/index.d.ts +2 -2
- package/dist/resilience/index.d.ts +13 -9
- package/dist/resilience/index.js +5 -5
- package/dist/resilience/index.js.map +1 -1
- package/dist/{runtime-types-B62Ucnxr.d.ts → runtime-types-BKg2CGMx.d.ts} +88 -14
- package/dist/testing/effect/index.d.ts +65 -0
- package/dist/testing/effect/index.js +148 -0
- package/dist/testing/effect/index.js.map +1 -0
- package/dist/testing/index.d.ts +12 -158
- package/dist/testing/index.js +4 -130
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/promise/index.d.ts +41 -0
- package/dist/testing/promise/index.js +37 -0
- package/dist/testing/promise/index.js.map +1 -0
- package/dist/timing/index.d.ts +4 -4
- package/dist/timing/index.js +4 -4
- package/dist/{types-DPm5YCDQ.d.ts → types-SzNSHKlV.d.ts} +1 -1
- package/dist/work-queue/index.d.ts +4 -4
- package/dist/work-queue/index.js +4 -3
- package/dist/work-queue/index.js.map +1 -1
- package/package.json +26 -14
- package/dist/chunk-C4U5D5HP.js.map +0 -1
- package/dist/chunk-G5E74IJB.js +0 -8
- package/dist/chunk-G5E74IJB.js.map +0 -1
- package/dist/chunk-K4ETBNKS.js.map +0 -1
- package/dist/chunk-KWNCM6XM.js.map +0 -1
- package/dist/chunk-QWIPH4YH.js.map +0 -1
- package/dist/chunk-XBZQT2TD.js.map +0 -1
package/dist/mem/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import '../core-
|
|
1
|
+
import { M as Migrated, D as DeletableStore, F as FencedStore } from '../runtime-types-BKg2CGMx.js';
|
|
2
|
+
import '../core-Ce9ncpuv.js';
|
|
3
3
|
import '@standard-schema/spec';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -54,10 +54,10 @@ interface MemoryStoreOptions {
|
|
|
54
54
|
* adapter's degenerate same-process boundary. Pass an explicit parser when
|
|
55
55
|
* the test wants to reject seeded shapes that don't match `S`.
|
|
56
56
|
*/
|
|
57
|
-
declare function memoryStore<S>(initial?: S | null, parse?: (raw: unknown) => S
|
|
58
|
-
declare function memoryStore<S>(initial: S | null | undefined, parse: ((raw: unknown) => S
|
|
57
|
+
declare function memoryStore<S>(initial?: S | null, parse?: (raw: unknown) => Migrated<S>): DeletableStore<S>;
|
|
58
|
+
declare function memoryStore<S>(initial: S | null | undefined, parse: ((raw: unknown) => Migrated<S>) | undefined, options: MemoryStoreOptions & {
|
|
59
59
|
readonly fenced: true;
|
|
60
|
-
}): FencedStore<S>;
|
|
61
|
-
declare function memoryStore<S>(initial?: S | null, parse?: (raw: unknown) => S
|
|
60
|
+
}): FencedStore<S> & DeletableStore<S>;
|
|
61
|
+
declare function memoryStore<S>(initial?: S | null, parse?: (raw: unknown) => Migrated<S>, options?: MemoryStoreOptions): DeletableStore<S> | (FencedStore<S> & DeletableStore<S>);
|
|
62
62
|
|
|
63
63
|
export { type MemoryStoreOptions, memoryStore };
|
package/dist/mem/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import '../chunk-
|
|
2
|
-
import { StoreConflictError } from '../chunk-
|
|
1
|
+
import '../chunk-FNGZMVIT.js';
|
|
2
|
+
import { StoreConflictError } from '../chunk-AIIXV4XB.js';
|
|
3
3
|
|
|
4
4
|
// src/mem/index.ts
|
|
5
5
|
function memoryStore(initial, parse = (raw) => raw, options = {}) {
|
|
@@ -15,6 +15,10 @@ function memoryStore(initial, parse = (raw) => raw, options = {}) {
|
|
|
15
15
|
},
|
|
16
16
|
migrate(raw) {
|
|
17
17
|
return parse(raw);
|
|
18
|
+
},
|
|
19
|
+
async delete() {
|
|
20
|
+
cell = null;
|
|
21
|
+
version = 0;
|
|
18
22
|
}
|
|
19
23
|
};
|
|
20
24
|
if (options.fenced !== true) return base;
|
package/dist/mem/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/mem/index.ts"],"names":[],"mappings":";;;;AAsEO,SAAS,WAAA,CACd,SACA,KAAA,
|
|
1
|
+
{"version":3,"sources":["../../src/mem/index.ts"],"names":[],"mappings":";;;;AAsEO,SAAS,WAAA,CACd,SACA,KAAA,GAAuC,CAAC,QAAQ,GAAA,EAChD,OAAA,GAA8B,EAAC,EAC2B;AAG1D,EAAA,IAAI,OAAiB,OAAA,IAAW,IAAA;AAGhC,EAAA,IAAI,OAAA,GAAU,CAAA;AAEd,EAAA,MAAM,IAAA,GAA0B;AAAA,IAC9B,MAAM,IAAA,GAAyB;AAC7B,MAAA,OAAO,IAAA;AAAA,IACT,CAAA;AAAA,IACA,MAAM,KAAK,KAAA,EAAyB;AAClC,MAAA,IAAA,GAAO,KAAA;AACP,MAAA,OAAA,IAAW,CAAA;AAAA,IACb,CAAA;AAAA,IACA,QAAQ,GAAA,EAA2B;AACjC,MAAA,OAAO,MAAM,GAAG,CAAA;AAAA,IAClB,CAAA;AAAA,IACA,MAAM,MAAA,GAAwB;AAG5B,MAAA,IAAA,GAAO,IAAA;AACP,MAAA,OAAA,GAAU,CAAA;AAAA,IACZ;AAAA,GACF;AACA,EAAA,IAAI,OAAA,CAAQ,MAAA,KAAW,IAAA,EAAM,OAAO,IAAA;AAEpC,EAAA,OAAO;AAAA,IACL,GAAG,IAAA;AAAA,IACH,MAAA,EAAQ,IAAA;AAAA,IACR,MAAM,UAAA,GAAyD;AAC7D,MAAA,OAAO,EAAE,GAAA,EAAK,IAAA,EAAM,OAAA,EAAQ;AAAA,IAC9B,CAAA;AAAA,IACA,MAAM,UAAA,CAAW,KAAA,EAAU,eAAA,EAA0C;AAInE,MAAA,IAAI,YAAY,eAAA,EAAiB;AAC/B,QAAA,MAAM,IAAI,kBAAA,CAAmB,eAAA,EAAiB,OAAO,CAAA;AAAA,MACvD;AACA,MAAA,IAAA,GAAO,KAAA;AACP,MAAA,OAAA,IAAW,CAAA;AACX,MAAA,OAAO,OAAA;AAAA,IACT;AAAA,GACF;AACF","file":"index.js","sourcesContent":["/**\n * @packageDocumentation\n * @demlik/tea/mem — in-memory `Store<S>` adapter for `@demlik/tea`.\n *\n * The standard test adapter — used everywhere a Worker or DO would be\n * overkill. Mirrors the source-only convention from `packages/widget-engine`\n * and `packages/tea` (no build step).\n *\n * Reference semantics, NOT deep-clone semantics:\n * - `save(state)` stores the reference; subsequent mutations to that\n * reference by the caller are observable through later `load()` calls.\n * - `load()` returns the stored reference; mutations made through it are\n * observable on the next `load()`.\n *\n * This matches what a real KV/DO `get()` would NOT do (those serialize +\n * deserialize), but it also matches what an in-memory cache obviously does.\n * Adding a deep clone here would lie about the boundary and impose a JSON\n * round-trip on every test. Tests that need fresh copies should clone at the\n * call site.\n *\n * Boundary parse (invariant 8): `Store<S>` requires `migrate(raw)`. For\n * `memoryStore` we ship a permissive identity-default — `(raw) => raw as\n * S | null` — because this adapter is a degenerate same-process boundary:\n * there is no serialization, the reference we read is structurally the\n * reference we wrote. The `as` cast here is the documented exception to the\n * invariant-8 no-`as` rule, scoped to the parameter default. Callers that\n * want strict parsing (e.g. a test that hand-seeds the cell from outside)\n * may pass an explicit `parse` to override.\n */\n\nimport type { DeletableStore, FencedStore, Migrated } from \"../index\";\nimport { StoreConflictError } from \"../index\";\n\n/** Options for {@link memoryStore}. */\nexport interface MemoryStoreOptions {\n /**\n * Refuse a second live writer (#143). With `{ fenced: true }` the returned\n * store is a `FencedStore<S>` whose version is an in-process counter — honest\n * for this adapter, since the cell it guards is in this process too. Two\n * `run`s handed the SAME `memoryStore` are the case it catches, and it is the\n * cheapest way to exercise the fenced path in a test.\n */\n readonly fenced?: true;\n}\n\n/**\n * Build an in-memory `Store<S>`.\n *\n * @param initial - optional seed value. `undefined` and explicit `null` both\n * mean \"no seed\"; `load()` resolves with `null` until the first `save()`.\n * A concrete `S` seed is returned by `load()` until overwritten.\n * @param parse - optional boundary parse for the `Store<S>.migrate` contract.\n * Defaults to identity (`(raw) => raw as S | null`) — honest for this\n * adapter's degenerate same-process boundary. Pass an explicit parser when\n * the test wants to reject seeded shapes that don't match `S`.\n */\nexport function memoryStore<S>(\n initial?: S | null,\n parse?: (raw: unknown) => Migrated<S>,\n): DeletableStore<S>;\nexport function memoryStore<S>(\n initial: S | null | undefined,\n parse: ((raw: unknown) => Migrated<S>) | undefined,\n options: MemoryStoreOptions & { readonly fenced: true },\n): FencedStore<S> & DeletableStore<S>;\nexport function memoryStore<S>(\n initial?: S | null,\n parse?: (raw: unknown) => Migrated<S>,\n options?: MemoryStoreOptions,\n): DeletableStore<S> | (FencedStore<S> & DeletableStore<S>);\nexport function memoryStore<S>(\n initial?: S | null,\n parse: (raw: unknown) => Migrated<S> = (raw) => raw as S | null,\n options: MemoryStoreOptions = {},\n): DeletableStore<S> | (FencedStore<S> & DeletableStore<S>) {\n // Single internal cell — `undefined` and `null` collapse to one\n // representation so the load site is branchless.\n let cell: S | null = initial ?? null;\n // The version the cell was last written at. A never-written cell reads `0`,\n // matching `fileStore`'s absent stamp.\n let version = 0;\n\n const base: DeletableStore<S> = {\n async load(): Promise<unknown> {\n return cell;\n },\n async save(state: S): Promise<void> {\n cell = state;\n version += 1;\n },\n migrate(raw: unknown): Migrated<S> {\n return parse(raw);\n },\n async delete(): Promise<void> {\n // Back to never-written: an empty cell at version `0`. The seed is gone\n // too — `load()` answers what an unseeded store answers.\n cell = null;\n version = 0;\n },\n };\n if (options.fenced !== true) return base;\n\n return {\n ...base,\n fenced: true,\n async loadFenced(): Promise<{ raw: unknown; version: number }> {\n return { raw: cell, version };\n },\n async saveFenced(state: S, expectedVersion: number): Promise<number> {\n // No lock: the whole store is one process's memory, and this body has no\n // await before the compare, so nothing can interleave between the check\n // and the write.\n if (version !== expectedVersion) {\n throw new StoreConflictError(expectedVersion, version);\n }\n cell = state;\n version += 1;\n return version;\n },\n };\n}\n"]}
|
package/dist/node/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import WebSocket from 'ws';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { M as Migrated, D as DeletableStore, F as FencedStore } from '../runtime-types-BKg2CGMx.js';
|
|
3
|
+
import { c as Sub, D as Dispose } from '../core-Ce9ncpuv.js';
|
|
4
4
|
import '@standard-schema/spec';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -89,7 +89,8 @@ interface Journal<R> {
|
|
|
89
89
|
* Atomic save (temp + rename). Absent file → `load()` resolves `null`
|
|
90
90
|
* (fresh boot). Structurally malformed JSON throws at `load()` (an infra
|
|
91
91
|
* error), matching `doStore`; a shape mismatch is NOT a throw — the
|
|
92
|
-
* substrate calls `migrate`, which returns `
|
|
92
|
+
* substrate calls `migrate`, which returns `refuse(reason)`. Both make
|
|
93
|
+
* `ready` reject with `StoreRefusedError`, and the file is left as is.
|
|
93
94
|
* Use for resumable scripts / local repro of prod machines. Ephemeral
|
|
94
95
|
* CLIs can skip persistence with `memoryStore` from `@demlik/tea/mem`.
|
|
95
96
|
*
|
|
@@ -130,18 +131,23 @@ interface Journal<R> {
|
|
|
130
131
|
* `parse` is REQUIRED because the file is a real serialization boundary — the
|
|
131
132
|
* bytes that come back through `JSON.parse` are structurally `unknown`, and the
|
|
132
133
|
* caller (who owns `S`) is the only party that can validate the shape.
|
|
133
|
-
*
|
|
134
|
-
* substrate boots `init` with `loaded = null
|
|
135
|
-
* `
|
|
134
|
+
* `parse` returns `S` for a shape it recognizes, `null` when nothing was saved
|
|
135
|
+
* (the substrate boots `init` with `loaded = null`), and `refuse(reason)` for
|
|
136
|
+
* saved bytes it cannot read (`ready` rejects with `StoreRefusedError`).
|
|
137
|
+
* `parse` must NOT throw per the `Store<S>.migrate` contract.
|
|
136
138
|
*
|
|
137
139
|
* Pass `{ fenced: true }` to get a `FencedStore<S>` instead — see
|
|
138
140
|
* {@link FileStoreOptions}.
|
|
141
|
+
*
|
|
142
|
+
* Either form is a `DeletableStore<S>`: `delete()` removes the state file and
|
|
143
|
+
* its `<path>.fence` version stamp, so a host that forgets a run needs no
|
|
144
|
+
* knowledge of the layout on disk.
|
|
139
145
|
*/
|
|
140
|
-
declare function fileStore<S>(path: string, parse: (raw: unknown) => S
|
|
141
|
-
declare function fileStore<S>(path: string, parse: (raw: unknown) => S
|
|
146
|
+
declare function fileStore<S>(path: string, parse: (raw: unknown) => Migrated<S>): DeletableStore<S>;
|
|
147
|
+
declare function fileStore<S>(path: string, parse: (raw: unknown) => Migrated<S>, options: FileStoreOptions & {
|
|
142
148
|
readonly fenced: true;
|
|
143
|
-
}): FencedStore<S>;
|
|
144
|
-
declare function fileStore<S>(path: string, parse: (raw: unknown) => S
|
|
149
|
+
}): FencedStore<S> & DeletableStore<S>;
|
|
150
|
+
declare function fileStore<S>(path: string, parse: (raw: unknown) => Migrated<S>, options?: FileStoreOptions): DeletableStore<S> | (FencedStore<S> & DeletableStore<S>);
|
|
145
151
|
/** Options for {@link fileStore}. */
|
|
146
152
|
interface FileStoreOptions {
|
|
147
153
|
/**
|
package/dist/node/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { dispatchIfPresent } from '../chunk-
|
|
2
|
-
import { StoreConflictError } from '../chunk-
|
|
3
|
-
import { mkdir, writeFile, rename,
|
|
1
|
+
import { dispatchIfPresent } from '../chunk-FNGZMVIT.js';
|
|
2
|
+
import { StoreConflictError } from '../chunk-AIIXV4XB.js';
|
|
3
|
+
import { unlink, mkdir, writeFile, rename, open, readFile } from 'fs/promises';
|
|
4
4
|
import { createRequire } from 'module';
|
|
5
5
|
import { dirname, join } from 'path';
|
|
6
6
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
@@ -82,10 +82,20 @@ function fileStore(path, parse, options = {}) {
|
|
|
82
82
|
},
|
|
83
83
|
migrate(raw) {
|
|
84
84
|
return parse(raw);
|
|
85
|
+
},
|
|
86
|
+
async delete() {
|
|
87
|
+
await removeStateFiles(path);
|
|
85
88
|
}
|
|
86
89
|
};
|
|
87
90
|
return options.fenced === true ? fenceFileStore(base, path) : base;
|
|
88
91
|
}
|
|
92
|
+
async function removeStateFiles(path) {
|
|
93
|
+
for (const file of [`${path}.fence`, path]) {
|
|
94
|
+
await unlink(file).catch((err) => {
|
|
95
|
+
if (err.code !== "ENOENT") throw err;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
89
99
|
function fenceFileStore(base, path) {
|
|
90
100
|
const fencePath = `${path}.fence`;
|
|
91
101
|
const lockPath = `${path}.fence.lock`;
|
|
@@ -130,6 +140,15 @@ function fenceFileStore(base, path) {
|
|
|
130
140
|
await unlink(lockPath).catch(() => {
|
|
131
141
|
});
|
|
132
142
|
}
|
|
143
|
+
},
|
|
144
|
+
async delete() {
|
|
145
|
+
await acquireFileLock(dirname(path), lockPath);
|
|
146
|
+
try {
|
|
147
|
+
await base.delete();
|
|
148
|
+
} finally {
|
|
149
|
+
await unlink(lockPath).catch(() => {
|
|
150
|
+
});
|
|
151
|
+
}
|
|
133
152
|
}
|
|
134
153
|
};
|
|
135
154
|
}
|
package/dist/node/index.js.map
CHANGED
|
@@ -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>({ 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
|
+
{"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 `refuse(reason)`. Both make\n * `ready` reject with `StoreRefusedError`, and the file is left as is.\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 {\n DeletableStore,\n Dispose,\n FencedStore,\n Migrated,\n Sub,\n} 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 * `parse` returns `S` for a shape it recognizes, `null` when nothing was saved\n * (the substrate boots `init` with `loaded = null`), and `refuse(reason)` for\n * saved bytes it cannot read (`ready` rejects with `StoreRefusedError`).\n * `parse` must NOT throw per the `Store<S>.migrate` contract.\n *\n * Pass `{ fenced: true }` to get a `FencedStore<S>` instead — see\n * {@link FileStoreOptions}.\n *\n * Either form is a `DeletableStore<S>`: `delete()` removes the state file and\n * its `<path>.fence` version stamp, so a host that forgets a run needs no\n * knowledge of the layout on disk.\n */\nexport function fileStore<S>(\n path: string,\n parse: (raw: unknown) => Migrated<S>,\n): DeletableStore<S>;\nexport function fileStore<S>(\n path: string,\n parse: (raw: unknown) => Migrated<S>,\n options: FileStoreOptions & { readonly fenced: true },\n): FencedStore<S> & DeletableStore<S>;\nexport function fileStore<S>(\n path: string,\n parse: (raw: unknown) => Migrated<S>,\n options?: FileStoreOptions,\n): DeletableStore<S> | (FencedStore<S> & DeletableStore<S>);\nexport function fileStore<S>(\n path: string,\n parse: (raw: unknown) => Migrated<S>,\n options: FileStoreOptions = {},\n): DeletableStore<S> | (FencedStore<S> & DeletableStore<S>) {\n const base: DeletableStore<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): Migrated<S> {\n return parse(raw);\n },\n async delete(): Promise<void> {\n await removeStateFiles(path);\n },\n };\n return options.fenced === true ? fenceFileStore(base, path) : base;\n}\n\n/**\n * Remove a `fileStore`'s state file and its `.fence` stamp; an absent one is\n * already deleted. The unfenced store removes the stamp too, because a file\n * written fenced can be opened unfenced and a stale stamp would outlive it.\n *\n * Stamp FIRST, state second. A crash between the two leaves the bytes at\n * version `0`, which a live fenced run reads as a conflict — the safe\n * direction. The reverse leaves the old version beside no state, and a run\n * still holding that version would swap cleanly onto a deleted run.\n */\nasync function removeStateFiles(path: string): Promise<void> {\n for (const file of [`${path}.fence`, path]) {\n await unlink(file).catch((err: NodeJS.ErrnoException) => {\n if (err.code !== \"ENOENT\") throw err;\n });\n }\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>(\n base: DeletableStore<S>,\n path: string,\n): FencedStore<S> & DeletableStore<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 async delete(): Promise<void> {\n // Under the swap's own lock, so a delete never lands between another\n // writer's state write and its stamp write.\n await acquireFileLock(dirname(path), lockPath);\n try {\n await base.delete();\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"]}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Tracer } from '@opentelemetry/api';
|
|
2
|
+
import { A as AgentEvent } from '../machine-CuojfZup.js';
|
|
3
|
+
import '../core-Ce9ncpuv.js';
|
|
4
|
+
import '@standard-schema/spec';
|
|
5
|
+
import '../index-Dy3379ve.js';
|
|
6
|
+
import '../index-D8sYVzCI.js';
|
|
7
|
+
import '../index-e36uEyl4.js';
|
|
8
|
+
import '../types-SzNSHKlV.js';
|
|
9
|
+
import '../index-DWdg7R1l.js';
|
|
10
|
+
import '../runtime-types-BKg2CGMx.js';
|
|
11
|
+
import '../index-2kNxy6az.js';
|
|
12
|
+
import '../retry-backoff/index.js';
|
|
13
|
+
import '../protocol-ClRZBhHQ.js';
|
|
14
|
+
import '../index-Ce5GYAuI.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
* @demlik/tea/otel — an agent run as OpenTelemetry spans.
|
|
19
|
+
*
|
|
20
|
+
* `traceAgent(runtime, { tracer })` listens to an agent runtime's semantic
|
|
21
|
+
* events and writes one span tree per run: a run span, a brain-call span per
|
|
22
|
+
* turn and a tool span per tool call, the brain and tool spans children of the
|
|
23
|
+
* run. Nothing here touches the reducer: the spans are a projection of the
|
|
24
|
+
* `AgentEvent` stream, the way `transcript` is.
|
|
25
|
+
*
|
|
26
|
+
* The attributes follow the OpenTelemetry GenAI semantic conventions
|
|
27
|
+
* (`gen_ai.*`), plus Langfuse's observation attributes (`langfuse.*`), so
|
|
28
|
+
* Langfuse renders the run as an agent with generations and tool calls without
|
|
29
|
+
* tea depending on any Langfuse package. Any OTel backend reads the same spans.
|
|
30
|
+
*
|
|
31
|
+
* `@opentelemetry/api` is an optional peer. Only this entry imports it.
|
|
32
|
+
*
|
|
33
|
+
* ## One trace across a kill
|
|
34
|
+
*
|
|
35
|
+
* The trace id is derived from the run's `runId` — the SHA-256 of it, cut to
|
|
36
|
+
* 32 hex characters, which is exactly Langfuse's `createTraceId(runId)` — and
|
|
37
|
+
* the run span is parented to a detached span context carrying it. The id
|
|
38
|
+
* lives on the durable Model, so a process that resumes the run from its
|
|
39
|
+
* `Store` writes into the same trace as the one that was killed. Spans the
|
|
40
|
+
* killed process never ended are never exported; OpenTelemetry exports a span
|
|
41
|
+
* when it ends. What it did end is in the trace beside the resumed leg.
|
|
42
|
+
*
|
|
43
|
+
* ## Time
|
|
44
|
+
*
|
|
45
|
+
* Every span starts and ends at the `at` of the event that opened or closed
|
|
46
|
+
* it, and `at` is read as **epoch milliseconds** — the runtime's `clock`
|
|
47
|
+
* contract. It reaches OpenTelemetry as an exact `HrTime`, never a bare
|
|
48
|
+
* number, so a small clock value (a logical or test clock) stays the instant
|
|
49
|
+
* it names instead of being rebased onto process start.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A mask over one exported input or output — the same shape as
|
|
54
|
+
* `LangfuseSpanProcessor`'s `mask`, so one function serves both. `data` is the
|
|
55
|
+
* value already serialized to JSON; return what should be exported instead.
|
|
56
|
+
*/
|
|
57
|
+
type SpanMask = (params: {
|
|
58
|
+
readonly data: unknown;
|
|
59
|
+
}) => unknown;
|
|
60
|
+
/** How {@link traceAgent} writes spans. */
|
|
61
|
+
interface TraceAgentOptions {
|
|
62
|
+
/** The tracer the spans are started on — `provider.getTracer("…")`. */
|
|
63
|
+
readonly tracer: Tracer;
|
|
64
|
+
/**
|
|
65
|
+
* Applied to every exported input and output. Omit → {@link maskBase64DataUris},
|
|
66
|
+
* which keeps a screenshot a tool returned from being exported as megabytes
|
|
67
|
+
* of base64.
|
|
68
|
+
*/
|
|
69
|
+
readonly mask?: SpanMask;
|
|
70
|
+
/**
|
|
71
|
+
* The agent's name, on `gen_ai.agent.name`, the run span's name and
|
|
72
|
+
* Langfuse's trace name. Omit → `"agent"`.
|
|
73
|
+
*/
|
|
74
|
+
readonly name?: string;
|
|
75
|
+
/**
|
|
76
|
+
* The trace id a run writes into, from its `runId`. Omit → {@link runTraceId}.
|
|
77
|
+
* Must return 32 lowercase hex characters, not all zero.
|
|
78
|
+
*/
|
|
79
|
+
readonly traceIdOf?: (runId: string) => string;
|
|
80
|
+
}
|
|
81
|
+
/** Anything that delivers an agent's events by type — a runtime built with `events: agentEvents()`. */
|
|
82
|
+
interface AgentEventSource<R> {
|
|
83
|
+
on<K extends AgentEvent<R>["type"]>(type: K, handler: (event: Extract<AgentEvent<R>, {
|
|
84
|
+
type: K;
|
|
85
|
+
}>) => void): () => void;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Trace an agent runtime into OpenTelemetry. Subscribes to every
|
|
89
|
+
* `AgentEvent` type on `runtime` and returns a cleanup that detaches the
|
|
90
|
+
* subscriptions and ends any span still open, at the `at` of the last event
|
|
91
|
+
* its run folded.
|
|
92
|
+
*
|
|
93
|
+
* The runtime must project events — built with
|
|
94
|
+
* `run(machine, { events: agentEvents() })`. For `defineAgent`, which takes an
|
|
95
|
+
* `onEvent` listener instead of handing out its runtime, wire
|
|
96
|
+
* {@link agentSpans}'s `onEvent` there.
|
|
97
|
+
*
|
|
98
|
+
* Each span is timed by its events' `at`, read as epoch milliseconds.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* import { type AgentEventSource, traceAgent } from "@demlik/tea/otel";
|
|
103
|
+
* import type { TracerProvider } from "@opentelemetry/api";
|
|
104
|
+
*
|
|
105
|
+
* declare const provider: TracerProvider;
|
|
106
|
+
* // Built with `run(machine, { ...wired, ctx, events: agentEvents() }).ready`.
|
|
107
|
+
* declare const runtime: AgentEventSource<string>;
|
|
108
|
+
*
|
|
109
|
+
* const stop = traceAgent(runtime, { tracer: provider.getTracer("audit") });
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
declare function traceAgent<R>(runtime: AgentEventSource<R>, opts: TraceAgentOptions): () => void;
|
|
113
|
+
/** The span writer {@link traceAgent} subscribes, for wiring to an `onEvent` listener. */
|
|
114
|
+
interface AgentSpans<R> {
|
|
115
|
+
/** Fold one event into the span tree. */
|
|
116
|
+
readonly onEvent: (event: AgentEvent<R>) => void;
|
|
117
|
+
/**
|
|
118
|
+
* End every span still open — the leg this process ran, when it stops
|
|
119
|
+
* before its run does. Each carries `tea.run.detached: true`.
|
|
120
|
+
*
|
|
121
|
+
* With `at`, every open span ends at `at`, used as given. Without it, each
|
|
122
|
+
* run's spans end at the `at` of the last event that run folded — its own
|
|
123
|
+
* clock, never wall time.
|
|
124
|
+
*/
|
|
125
|
+
readonly end: (at?: number) => void;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The span writer behind {@link traceAgent}, as a listener. Use it where the
|
|
129
|
+
* events arrive through a callback rather than a runtime:
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```ts
|
|
133
|
+
* import { defineAgent } from "@demlik/tea/agent";
|
|
134
|
+
* import { agentSpans } from "@demlik/tea/otel";
|
|
135
|
+
* import { trace } from "@opentelemetry/api";
|
|
136
|
+
*
|
|
137
|
+
* const agent = defineAgent({
|
|
138
|
+
* model: async () => ({ content: "done", toolCalls: [] }),
|
|
139
|
+
* tools: [],
|
|
140
|
+
* instructions: "Summarise the page.",
|
|
141
|
+
* });
|
|
142
|
+
*
|
|
143
|
+
* const spans = agentSpans({ tracer: trace.getTracer("audit") });
|
|
144
|
+
* await agent.run("https://example.com", { onEvent: spans.onEvent });
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
declare function agentSpans<R>(opts: TraceAgentOptions): AgentSpans<R>;
|
|
148
|
+
/**
|
|
149
|
+
* The default {@link SpanMask}: every base64 `data:` URI in a string becomes
|
|
150
|
+
* `[base64 data URI stripped before export]`. Anything that is not a string
|
|
151
|
+
* passes through untouched.
|
|
152
|
+
*/
|
|
153
|
+
declare const maskBase64DataUris: SpanMask;
|
|
154
|
+
/**
|
|
155
|
+
* The trace id a run's spans are written into: the SHA-256 of `runId` as
|
|
156
|
+
* lowercase hex, cut to 32 characters. It equals Langfuse's
|
|
157
|
+
* `createTraceId(runId)`, so a trace can be looked up by the run it traced.
|
|
158
|
+
*/
|
|
159
|
+
declare function runTraceId(runId: string): string;
|
|
160
|
+
|
|
161
|
+
export { type AgentEventSource, type AgentSpans, type SpanMask, type TraceAgentOptions, agentSpans, maskBase64DataUris, runTraceId, traceAgent };
|