@demlik/tea 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/agent/index.d.ts +907 -0
- package/dist/agent/index.js +20 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/authed-call/index.d.ts +119 -0
- package/dist/authed-call/index.js +148 -0
- package/dist/authed-call/index.js.map +1 -0
- package/dist/await-terminal/index.d.ts +99 -0
- package/dist/await-terminal/index.js +75 -0
- package/dist/await-terminal/index.js.map +1 -0
- package/dist/batch-window/index.d.ts +259 -0
- package/dist/batch-window/index.js +43 -0
- package/dist/batch-window/index.js.map +1 -0
- package/dist/cache/index.d.ts +2 -2
- package/dist/cache/index.js +2 -50
- package/dist/cache/index.js.map +1 -1
- package/dist/chunk-27N2SPTN.js +222 -0
- package/dist/chunk-27N2SPTN.js.map +1 -0
- package/dist/chunk-2YOE5PIS.js +45 -0
- package/dist/chunk-2YOE5PIS.js.map +1 -0
- package/dist/chunk-3545BOYL.js +65 -0
- package/dist/chunk-3545BOYL.js.map +1 -0
- package/dist/chunk-3XJWC7VW.js +61 -0
- package/dist/chunk-3XJWC7VW.js.map +1 -0
- package/dist/chunk-5DEQ5I3U.js +132 -0
- package/dist/chunk-5DEQ5I3U.js.map +1 -0
- package/dist/chunk-5VNWZADP.js +49 -0
- package/dist/chunk-5VNWZADP.js.map +1 -0
- package/dist/chunk-6RGBKODV.js +268 -0
- package/dist/chunk-6RGBKODV.js.map +1 -0
- package/dist/{chunk-ZRUST6BK.js → chunk-7FYQGDMF.js} +2 -2
- package/dist/chunk-7FYQGDMF.js.map +1 -0
- package/dist/chunk-7W4LMPUW.js +186 -0
- package/dist/chunk-7W4LMPUW.js.map +1 -0
- package/dist/chunk-BYQUGDD3.js +16 -0
- package/dist/chunk-BYQUGDD3.js.map +1 -0
- package/dist/{chunk-PYBQREFU.js → chunk-DILMKOZ7.js} +22 -26
- package/dist/chunk-DILMKOZ7.js.map +1 -0
- package/dist/chunk-EHIX7NYZ.js +93 -0
- package/dist/chunk-EHIX7NYZ.js.map +1 -0
- package/dist/chunk-HCEVHFJ7.js +54 -0
- package/dist/chunk-HCEVHFJ7.js.map +1 -0
- package/dist/chunk-HJO6VNQI.js +15 -0
- package/dist/chunk-HJO6VNQI.js.map +1 -0
- package/dist/chunk-IN33Q4VJ.js +52 -0
- package/dist/chunk-IN33Q4VJ.js.map +1 -0
- package/dist/chunk-J6527VRQ.js +56 -0
- package/dist/chunk-J6527VRQ.js.map +1 -0
- package/dist/chunk-JELSZN25.js +47 -0
- package/dist/chunk-JELSZN25.js.map +1 -0
- package/dist/chunk-K575PT4S.js +58 -0
- package/dist/chunk-K575PT4S.js.map +1 -0
- package/dist/chunk-K7Q23EQ4.js +20 -0
- package/dist/chunk-K7Q23EQ4.js.map +1 -0
- package/dist/chunk-LEC4U6CX.js +91 -0
- package/dist/chunk-LEC4U6CX.js.map +1 -0
- package/dist/{chunk-PYOCUBQJ.js → chunk-LP7VQBRG.js} +22 -15
- package/dist/chunk-LP7VQBRG.js.map +1 -0
- package/dist/{chunk-CMG2KMWX.js → chunk-MMH7CW5K.js} +220 -189
- package/dist/chunk-MMH7CW5K.js.map +1 -0
- package/dist/{chunk-Y5JM3UUH.js → chunk-NSDFA6M6.js} +75 -3
- package/dist/chunk-NSDFA6M6.js.map +1 -0
- package/dist/chunk-ON24RKSE.js +60 -0
- package/dist/chunk-ON24RKSE.js.map +1 -0
- package/dist/chunk-PS23H6KQ.js +87 -0
- package/dist/chunk-PS23H6KQ.js.map +1 -0
- package/dist/chunk-QLA76GIT.js +162 -0
- package/dist/chunk-QLA76GIT.js.map +1 -0
- package/dist/chunk-S3FQN4SL.js +506 -0
- package/dist/chunk-S3FQN4SL.js.map +1 -0
- package/dist/chunk-SC3FAFZN.js +53 -0
- package/dist/chunk-SC3FAFZN.js.map +1 -0
- package/dist/chunk-SINXE47Y.js +55 -0
- package/dist/chunk-SINXE47Y.js.map +1 -0
- package/dist/chunk-SQTCS44W.js +33 -0
- package/dist/chunk-SQTCS44W.js.map +1 -0
- package/dist/chunk-TGG26XMK.js +52 -0
- package/dist/chunk-TGG26XMK.js.map +1 -0
- package/dist/chunk-UV6UWGEO.js +147 -0
- package/dist/chunk-UV6UWGEO.js.map +1 -0
- package/dist/chunk-UYWUVEP6.js +158 -0
- package/dist/chunk-UYWUVEP6.js.map +1 -0
- package/dist/chunk-VLGARN6H.js +202 -0
- package/dist/chunk-VLGARN6H.js.map +1 -0
- package/dist/circuit-breaker/index.js +1 -51
- package/dist/circuit-breaker/index.js.map +1 -1
- package/dist/core-BoB6t7dB.d.ts +188 -0
- package/dist/deadline/index.d.ts +24 -5
- package/dist/deadline/index.js +4 -18
- package/dist/deadline/index.js.map +1 -1
- package/dist/debounce/index.d.ts +6 -0
- package/dist/debounce/index.js +1 -54
- package/dist/debounce/index.js.map +1 -1
- package/dist/devtools/index.d.ts +1 -1
- package/dist/devtools/index.js +18 -6
- package/dist/devtools/index.js.map +1 -1
- package/dist/do/index.d.ts +1723 -92
- package/dist/do/index.js +673 -31
- package/dist/do/index.js.map +1 -1
- package/dist/durable-effects-CO9Uh971.d.ts +180 -0
- package/dist/extension/index.d.ts +31 -13
- package/dist/extension/index.js +5 -3
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/react.js +8 -3
- package/dist/extension/react.js.map +1 -1
- package/dist/extension/subs/index.d.ts +2 -2
- package/dist/extension/subs/index.js.map +1 -1
- package/dist/extension/test-utils.js +11 -3
- package/dist/extension/test-utils.js.map +1 -1
- package/dist/fan-out/index.d.ts +239 -0
- package/dist/fan-out/index.js +5 -0
- package/dist/fan-out/index.js.map +1 -0
- package/dist/idempotency/adapter.d.ts +62 -0
- package/dist/idempotency/adapter.js +4 -0
- package/dist/idempotency/adapter.js.map +1 -0
- package/dist/idempotency/index.js +1 -56
- package/dist/idempotency/index.js.map +1 -1
- package/dist/idempotent-intake/index.d.ts +186 -0
- package/dist/idempotent-intake/index.js +7 -0
- package/dist/idempotent-intake/index.js.map +1 -0
- package/dist/index.d.ts +341 -191
- package/dist/index.js +2 -1
- package/dist/llm-call/index.d.ts +309 -0
- package/dist/llm-call/index.js +14 -0
- package/dist/llm-call/index.js.map +1 -0
- package/dist/machine-viz/index.d.ts +4 -4
- package/dist/machine-viz/index.js +5 -9
- package/dist/machine-viz/index.js.map +1 -1
- package/dist/mem/index.d.ts +1 -0
- package/dist/monitored-run/index.d.ts +253 -0
- package/dist/monitored-run/index.js +10 -0
- package/dist/monitored-run/index.js.map +1 -0
- package/dist/node/index.d.ts +5 -5
- package/dist/node/index.js.map +1 -1
- package/dist/paginated-walk/index.d.ts +234 -0
- package/dist/paginated-walk/index.js +15 -0
- package/dist/paginated-walk/index.js.map +1 -0
- package/dist/paginator/index.d.ts +271 -0
- package/dist/paginator/index.js +3 -0
- package/dist/paginator/index.js.map +1 -0
- package/dist/parity/index.d.ts +92 -0
- package/dist/parity/index.js +78 -0
- package/dist/parity/index.js.map +1 -0
- package/dist/pbt/arbitraries/index.d.ts +1 -1
- package/dist/pbt/arbitraries/index.js +1 -1
- package/dist/pbt/index.d.ts +15 -8
- package/dist/pbt/index.js +6 -11
- package/dist/pbt/index.js.map +1 -1
- package/dist/pbt/runners/index.d.ts +1 -1
- package/dist/pbt/runners/index.js +3 -2
- package/dist/poller/index.d.ts +288 -0
- package/dist/poller/index.js +79 -0
- package/dist/poller/index.js.map +1 -0
- package/dist/prediction/index.d.ts +130 -0
- package/dist/prediction/index.js +4 -0
- package/dist/prediction/index.js.map +1 -0
- package/dist/protocol-B5YmJr0d.d.ts +45 -0
- package/dist/pure/index.d.ts +2 -0
- package/dist/pure/index.js +4 -0
- package/dist/pure/index.js.map +1 -0
- package/dist/raft/demo.d.ts +105 -0
- package/dist/raft/demo.js +172 -0
- package/dist/raft/demo.js.map +1 -0
- package/dist/raft/index.d.ts +548 -0
- package/dist/raft/index.js +7 -0
- package/dist/raft/index.js.map +1 -0
- package/dist/raft/sim.d.ts +222 -0
- package/dist/raft/sim.js +8 -0
- package/dist/raft/sim.js.map +1 -0
- package/dist/raft/timeline.d.ts +46 -0
- package/dist/raft/timeline.js +62 -0
- package/dist/raft/timeline.js.map +1 -0
- package/dist/rate-limit/index.js +1 -43
- package/dist/rate-limit/index.js.map +1 -1
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +43 -20
- package/dist/react/index.js.map +1 -1
- package/dist/reconciler/index.d.ts +192 -0
- package/dist/reconciler/index.js +166 -0
- package/dist/reconciler/index.js.map +1 -0
- package/dist/recorder/index.d.ts +7 -3
- package/dist/recorder/index.js +1 -155
- package/dist/recorder/index.js.map +1 -1
- package/dist/resilient-call/index.d.ts +294 -0
- package/dist/resilient-call/index.js +13 -0
- package/dist/resilient-call/index.js.map +1 -0
- package/dist/retry-backoff/index.d.ts +42 -5
- package/dist/retry-backoff/index.js +1 -35
- package/dist/retry-backoff/index.js.map +1 -1
- package/dist/retry-to-success/index.d.ts +109 -0
- package/dist/retry-to-success/index.js +35 -0
- package/dist/retry-to-success/index.js.map +1 -0
- package/dist/saga/index.d.ts +238 -0
- package/dist/saga/index.js +150 -0
- package/dist/saga/index.js.map +1 -0
- package/dist/snapshot/index.d.ts +334 -0
- package/dist/snapshot/index.js +5 -0
- package/dist/snapshot/index.js.map +1 -0
- package/dist/subs/index.d.ts +48 -10
- package/dist/subs/index.js +1 -1
- package/dist/testing/index.d.ts +107 -2
- package/dist/testing/index.js +204 -6
- package/dist/testing/index.js.map +1 -1
- package/dist/throttle/index.d.ts +9 -0
- package/dist/throttle/index.js +1 -52
- package/dist/throttle/index.js.map +1 -1
- package/dist/throttled-input/index.d.ts +380 -0
- package/dist/throttled-input/index.js +88 -0
- package/dist/throttled-input/index.js.map +1 -0
- package/dist/token-refresh/index.d.ts +215 -0
- package/dist/token-refresh/index.js +6 -0
- package/dist/token-refresh/index.js.map +1 -0
- package/dist/trace-replay/index.d.ts +11 -2
- package/dist/trace-replay/index.js +3 -55
- package/dist/trace-replay/index.js.map +1 -1
- package/dist/{types-CwBC5wMn.d.ts → types-B-8bcAp8.d.ts} +1 -1
- package/dist/with-deadline/index.d.ts +196 -0
- package/dist/with-deadline/index.js +111 -0
- package/dist/with-deadline/index.js.map +1 -0
- package/dist/with-resilience/index.d.ts +205 -0
- package/dist/with-resilience/index.js +251 -0
- package/dist/with-resilience/index.js.map +1 -0
- package/dist/with-telemetry/index.d.ts +149 -0
- package/dist/with-telemetry/index.js +70 -0
- package/dist/with-telemetry/index.js.map +1 -0
- package/dist/work-queue/adapter.d.ts +82 -0
- package/dist/work-queue/adapter.js +4 -0
- package/dist/work-queue/adapter.js.map +1 -0
- package/dist/work-queue/index.d.ts +25 -11
- package/dist/work-queue/index.js +1 -46
- package/dist/work-queue/index.js.map +1 -1
- package/dist/work-queue/ops.d.ts +69 -0
- package/dist/work-queue/ops.js +3 -0
- package/dist/work-queue/ops.js.map +1 -0
- package/dist/workflow/index.d.ts +450 -0
- package/dist/workflow/index.js +225 -0
- package/dist/workflow/index.js.map +1 -0
- package/package.json +168 -25
- package/dist/chunk-CMG2KMWX.js.map +0 -1
- package/dist/chunk-PYBQREFU.js.map +0 -1
- package/dist/chunk-PYOCUBQJ.js.map +0 -1
- package/dist/chunk-Y5JM3UUH.js.map +0 -1
- package/dist/chunk-ZRUST6BK.js.map +0 -1
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { CircuitConfig, RateLimitConfig, DeadlineConfig, ResilientPorts, RunCmd, FailMsg, SucceedMsg } from '../resilient-call/index.js';
|
|
2
|
+
import { TtlCache } from '../cache/index.js';
|
|
3
|
+
import { C as Cmd } from '../core-BoB6t7dB.js';
|
|
4
|
+
import { PaginatedWalkState, FetchPageCmd, PageOkMsg, PageErrMsg, PaginatedWalkTimerMsg } from '../paginated-walk/index.js';
|
|
5
|
+
import { RetryPolicy } from '../retry-backoff/index.js';
|
|
6
|
+
import { DeadlineSub } from '../deadline/index.js';
|
|
7
|
+
export { deadlineSub, subscribeDeadline } from '../deadline/index.js';
|
|
8
|
+
import '../circuit-breaker/index.js';
|
|
9
|
+
import '../protocol-B5YmJr0d.js';
|
|
10
|
+
import '../rate-limit/index.js';
|
|
11
|
+
import '../types-B-8bcAp8.js';
|
|
12
|
+
import '../paginator/index.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The reconciler knob. Generic in:
|
|
16
|
+
* - `Actual` — one item of the ACTUAL world (a fleet node, an audited page, a
|
|
17
|
+
* table row). The scan accumulates these.
|
|
18
|
+
* - `Desired` — the DESIRED spec value the diff converges toward. Opaque to
|
|
19
|
+
* this module; only `diff` reads it.
|
|
20
|
+
* - `Page` — the shape one actual-list page fetch resolves with.
|
|
21
|
+
* - `Change` — one unit of remediation the diff produces and `apply` realizes.
|
|
22
|
+
* - `ApplyCmd`— the Cmd `apply(change)` yields (the effect that mutates the
|
|
23
|
+
* actual world toward desired).
|
|
24
|
+
*
|
|
25
|
+
* `diff` / `apply` / `idOf` / `nextCursor` / `itemsOf` are PURE config functions
|
|
26
|
+
* applied at the verb boundary, never stored in the slice or in a Cmd (Cmds are
|
|
27
|
+
* plain data, not closures). The resilience bricks (`retry` / `circuit` /
|
|
28
|
+
* `rateLimit` / `deadline`) are the SAME optional knobs `paginated-walk` /
|
|
29
|
+
* `resilient-call` expose — reconciler extends them; `rateLimit` keeps the scan
|
|
30
|
+
* from 429-ing the upstream listing API.
|
|
31
|
+
*/
|
|
32
|
+
interface ReconcilerConfig<Actual, Desired, Page, Change, ApplyCmd extends Cmd, Cursor = number> {
|
|
33
|
+
/**
|
|
34
|
+
* The DESIRED spec to converge on. Held verbatim and handed to `diff` once the
|
|
35
|
+
* scan completes. Any JSON-serializable value — a list of specs, a target map,
|
|
36
|
+
* a single target object — `diff` alone interprets it.
|
|
37
|
+
*/
|
|
38
|
+
readonly desired: Desired;
|
|
39
|
+
/**
|
|
40
|
+
* Compute the remediation plan: the ordered `Change[]` that moves `actual` to
|
|
41
|
+
* `desired`. Called once when the scan finishes (the full actual snapshot is in
|
|
42
|
+
* hand). An empty plan means "already in sync — nothing to apply". PURE +
|
|
43
|
+
* consumer-specific (only the consumer knows the domain's reconcile rules).
|
|
44
|
+
*/
|
|
45
|
+
readonly diff: (desired: Desired, actual: readonly Actual[]) => readonly Change[];
|
|
46
|
+
/**
|
|
47
|
+
* Realize one `Change` as the Cmd that mutates the actual world. Returns plain
|
|
48
|
+
* data (a `{ type, ... }` Cmd the consumer's interpret handler performs).
|
|
49
|
+
* PURE — the side effect happens when the runtime performs the emitted Cmd,
|
|
50
|
+
* never here.
|
|
51
|
+
*/
|
|
52
|
+
readonly apply: (change: Change) => ApplyCmd;
|
|
53
|
+
/**
|
|
54
|
+
* Stable identity for a `Change` — the applied-ledger cache key. A re-`applyNext`
|
|
55
|
+
* after eviction (or after a re-plan) skips a change whose id is already in the
|
|
56
|
+
* ledger (idempotent re-apply). MUST be a property of the change itself, NOT its
|
|
57
|
+
* position in the plan: the ledger has to survive re-planning. When omitted the
|
|
58
|
+
* default keys by the change's serialized CONTENT (`JSON.stringify(change)`) —
|
|
59
|
+
* a stable per-change identity that is unchanged by where the change sits in the
|
|
60
|
+
* plan. Supply a domain id (the target node id, the resource key) when one
|
|
61
|
+
* exists; it is cheaper than serializing and dedupes two changes that differ
|
|
62
|
+
* only in fields irrelevant to identity. PURE.
|
|
63
|
+
*
|
|
64
|
+
* Why NOT positional: keying by index silently drops changes on the documented
|
|
65
|
+
* re-plan path. After applying the index-0 change, its `"0"` ledger entry would
|
|
66
|
+
* match WHATEVER different, never-applied change lands at index 0 in the next
|
|
67
|
+
* plan — the apply loop skips it as "already done". Identity must be intrinsic
|
|
68
|
+
* to the change so the ledger means "this change settled", not "slot N settled".
|
|
69
|
+
*/
|
|
70
|
+
readonly idOf?: (change: Change) => string;
|
|
71
|
+
/**
|
|
72
|
+
* The cursor the actual-list scan fetches first. For an offset API typically
|
|
73
|
+
* `0`; for a token API whatever sentinel the API treats as page one.
|
|
74
|
+
*/
|
|
75
|
+
readonly firstPage: Cursor;
|
|
76
|
+
/**
|
|
77
|
+
* Extract the next scan cursor from a fetched page — a `Cursor` to keep
|
|
78
|
+
* scanning, or `null` when the actual listing is exhausted (scan finishes,
|
|
79
|
+
* plan computed). PURE + consumer-specific.
|
|
80
|
+
*/
|
|
81
|
+
readonly nextCursor: (page: Page) => Cursor | null;
|
|
82
|
+
/**
|
|
83
|
+
* Extract the actual items from one fetched page. Each page's items are
|
|
84
|
+
* appended to the running `actual` accumulator. PURE.
|
|
85
|
+
*/
|
|
86
|
+
readonly itemsOf: (page: Page) => readonly Actual[];
|
|
87
|
+
/** Exponential-backoff retry policy for a transient scan-page failure. */
|
|
88
|
+
readonly retry?: RetryPolicy;
|
|
89
|
+
/** Circuit breaker around the scan fetch (one upstream → one breaker). */
|
|
90
|
+
readonly circuit?: CircuitConfig;
|
|
91
|
+
/** Token-bucket rate limit on the scan — the "don't 429 the listing API" knob. */
|
|
92
|
+
readonly rateLimit?: RateLimitConfig;
|
|
93
|
+
/** Overall wall-clock deadline per individual scan-page fetch. */
|
|
94
|
+
readonly deadline?: DeadlineConfig;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Reconcile lifecycle phase. Discriminated on `phase`:
|
|
98
|
+
*
|
|
99
|
+
* - `idle` — created, never scanned. `scan` starts the actual-world walk.
|
|
100
|
+
* - `scanning` — the paginated-walk is traversing the actual listing; pages
|
|
101
|
+
* fold into `actual` as they arrive. The scan owns the `walk`
|
|
102
|
+
* sub-slice; this phase ends when the walk finishes.
|
|
103
|
+
* - `applying` — the scan finished, `diff` produced the `plan`, and changes are
|
|
104
|
+
* being applied one at a time. `appliedCursor` is the index of
|
|
105
|
+
* the next change to apply.
|
|
106
|
+
* - `done` — every planned change applied (or an empty plan: already in
|
|
107
|
+
* sync). Terminal.
|
|
108
|
+
* - `failed` — the scan failed terminally (the page-fetch retries were
|
|
109
|
+
* exhausted / a deadline fired). `error` carries why. Terminal.
|
|
110
|
+
*/
|
|
111
|
+
type ReconcilePhase = "idle" | "scanning" | "applying" | "done" | "failed";
|
|
112
|
+
/**
|
|
113
|
+
* The slice. Plain data end to end:
|
|
114
|
+
* - `phase` — the lifecycle discriminant.
|
|
115
|
+
* - `walk` — the `../paginated-walk` scan slice (cursor + resilience).
|
|
116
|
+
* Drives the actual-world traversal; POSITION survives
|
|
117
|
+
* eviction.
|
|
118
|
+
* - `actual` — the accumulated actual items, appended page by page during
|
|
119
|
+
* the scan. The full snapshot `diff` is computed against.
|
|
120
|
+
* - `plan` — the `Change[]` `diff` produced. Empty until the scan
|
|
121
|
+
* finishes; the apply loop walks it by `appliedCursor`.
|
|
122
|
+
* - `appliedCursor` — index of the NEXT change to apply (0 ≤ cursor ≤ plan.length).
|
|
123
|
+
* `cursor === plan.length` means every change applied.
|
|
124
|
+
* - `applied` — the applied-ledger cache: `idOf(change) → change`, written
|
|
125
|
+
* as each change settles. The idempotency memory the apply
|
|
126
|
+
* loop reads to skip already-applied changes after eviction.
|
|
127
|
+
*/
|
|
128
|
+
interface ReconcilerState<Actual, Change, Page, Cursor = number> {
|
|
129
|
+
readonly phase: ReconcilePhase;
|
|
130
|
+
readonly walk: PaginatedWalkState<Cursor, Page>;
|
|
131
|
+
readonly actual: readonly Actual[];
|
|
132
|
+
readonly plan: readonly Change[];
|
|
133
|
+
readonly appliedCursor: number;
|
|
134
|
+
readonly applied: TtlCache<Change>;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The actual-list page-fetch effect: the inherited `resilient_run` Cmd from
|
|
138
|
+
* paginated-walk, whose `input` is the `Cursor` to fetch and whose `key` is the
|
|
139
|
+
* fixed `PAGE_KEY`. The consumer's `handlers(ports)` interprets it.
|
|
140
|
+
*/
|
|
141
|
+
type ScanPageCmd<Cursor> = FetchPageCmd<Cursor>;
|
|
142
|
+
/** Page-settled Msgs the scan `handlers` port dispatches back (inherited verbatim). */
|
|
143
|
+
type ScanPageOkMsg<Page> = PageOkMsg<Page>;
|
|
144
|
+
type ScanPageErrMsg = PageErrMsg;
|
|
145
|
+
/** The scan retry / deadline timer Msg — inherited from paginated-walk. */
|
|
146
|
+
type ReconcilerTimerMsg = PaginatedWalkTimerMsg;
|
|
147
|
+
/**
|
|
148
|
+
* Ports the consumer supplies to `handlers`. `run(cursor)` fetches ONE page of
|
|
149
|
+
* the ACTUAL listing for the given cursor; throwing routes to `pageErr` (and
|
|
150
|
+
* thus the scan backoff). Same shape as paginated-walk's port.
|
|
151
|
+
*/
|
|
152
|
+
interface ReconcilerPorts<Cursor, Page> extends ResilientPorts<Cursor, Page> {
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Build a reconciler knob from `config`. `rng` is injected for the underlying
|
|
156
|
+
* scan-retry jitter — pass a fixed `() => 0` in tests to pin backoff; defaults
|
|
157
|
+
* to `Math.random` (read at the verb boundary inside resilient-call, never in a
|
|
158
|
+
* reconciler verb). Inherited straight from paginated-walk.
|
|
159
|
+
*
|
|
160
|
+
* Returns the uniform L2 knob contract: `init()`, the verbs `scan` / `pageOk` /
|
|
161
|
+
* `pageErr` / `planned` / `applyNext` / `applied` / `onTimer`, the derived
|
|
162
|
+
* `isComplete`, `subs(state)`, and `handlers(ports)`.
|
|
163
|
+
*/
|
|
164
|
+
declare function createReconciler<Actual, Desired, Page, Change, ApplyCmd extends Cmd = Cmd, Cursor = number>(config: ReconcilerConfig<Actual, Desired, Page, Change, ApplyCmd, Cursor>, rng?: () => number): {
|
|
165
|
+
init: () => ReconcilerState<Actual, Change, Page, Cursor>;
|
|
166
|
+
scan: (s: ReconcilerState<Actual, Change, Page, Cursor>, at: number) => readonly [ReconcilerState<Actual, Change, Page, Cursor>, readonly (ApplyCmd | ScanPageCmd<Cursor>)[]];
|
|
167
|
+
pageOk: (s: ReconcilerState<Actual, Change, Page, Cursor>, page: Page, at: number) => readonly [ReconcilerState<Actual, Change, Page, Cursor>, readonly (ApplyCmd | ScanPageCmd<Cursor>)[]];
|
|
168
|
+
pageErr: (s: ReconcilerState<Actual, Change, Page, Cursor>, error: unknown, at: number) => readonly [ReconcilerState<Actual, Change, Page, Cursor>, readonly (ApplyCmd | ScanPageCmd<Cursor>)[]];
|
|
169
|
+
planned: (s: ReconcilerState<Actual, Change, Page, Cursor>, at: number, changes?: readonly Change[]) => readonly [ReconcilerState<Actual, Change, Page, Cursor>, readonly (ApplyCmd | ScanPageCmd<Cursor>)[]];
|
|
170
|
+
applyNext: (s: ReconcilerState<Actual, Change, Page, Cursor>, at: number) => readonly [ReconcilerState<Actual, Change, Page, Cursor>, readonly (ApplyCmd | ScanPageCmd<Cursor>)[]];
|
|
171
|
+
applied: (s: ReconcilerState<Actual, Change, Page, Cursor>, change: Change, at: number) => readonly [ReconcilerState<Actual, Change, Page, Cursor>, readonly (ApplyCmd | ScanPageCmd<Cursor>)[]];
|
|
172
|
+
onTimer: (s: ReconcilerState<Actual, Change, Page, Cursor>, msg: ReconcilerTimerMsg) => readonly [ReconcilerState<Actual, Change, Page, Cursor>, readonly (ApplyCmd | ScanPageCmd<Cursor>)[]];
|
|
173
|
+
isComplete: (s: ReconcilerState<Actual, Change, Page, Cursor>) => boolean;
|
|
174
|
+
subs: (s: ReconcilerState<Actual, Change, Page, Cursor>) => readonly DeadlineSub[];
|
|
175
|
+
handlers: (ports: ReconcilerPorts<Cursor, Page>) => {
|
|
176
|
+
resilient_run: (cmd: RunCmd<Cursor>, ctx: Readonly<Record<never, never>>) => Promise<FailMsg | SucceedMsg<Page>>;
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* Lift a knob result `[slice, cmds]` into a host `[State, cmds]` where the slice
|
|
181
|
+
* lives at `state.rec`. Convenience for the common single-slice host; hosts with
|
|
182
|
+
* a differently-named field spread by hand. PURE — a thin record rebuild, no
|
|
183
|
+
* clock / RNG.
|
|
184
|
+
*/
|
|
185
|
+
declare function liftReconciler<S extends {
|
|
186
|
+
rec: ReconcilerState<Actual, Change, Page, Cursor>;
|
|
187
|
+
}, Actual, Change, Page, Cursor, C extends Cmd>(state: S, [slice, cmds]: readonly [
|
|
188
|
+
ReconcilerState<Actual, Change, Page, Cursor>,
|
|
189
|
+
readonly C[]
|
|
190
|
+
]): readonly [S, readonly C[]];
|
|
191
|
+
|
|
192
|
+
export { DeadlineSub, type ReconcilePhase, type ReconcilerConfig, type ReconcilerPorts, type ReconcilerState, type ReconcilerTimerMsg, type ScanPageCmd, type ScanPageErrMsg, type ScanPageOkMsg, createReconciler, liftReconciler };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { createPaginatedWalk, PAGE_KEY } from '../chunk-5DEQ5I3U.js';
|
|
2
|
+
import '../chunk-SINXE47Y.js';
|
|
3
|
+
import '../chunk-27N2SPTN.js';
|
|
4
|
+
import '../chunk-JELSZN25.js';
|
|
5
|
+
export { deadlineSub, subscribeDeadline } from '../chunk-K7Q23EQ4.js';
|
|
6
|
+
import '../chunk-5VNWZADP.js';
|
|
7
|
+
import '../chunk-2YOE5PIS.js';
|
|
8
|
+
import '../chunk-SC3FAFZN.js';
|
|
9
|
+
import { initCache, get, set } from '../chunk-IN33Q4VJ.js';
|
|
10
|
+
import '../chunk-NSDFA6M6.js';
|
|
11
|
+
import '../chunk-2YFLOCWX.js';
|
|
12
|
+
import '../chunk-MMH7CW5K.js';
|
|
13
|
+
import '../chunk-QLA76GIT.js';
|
|
14
|
+
|
|
15
|
+
// src/reconciler/index.ts
|
|
16
|
+
function createReconciler(config, rng = Math.random) {
|
|
17
|
+
const walk = createPaginatedWalk(
|
|
18
|
+
{
|
|
19
|
+
firstPage: config.firstPage,
|
|
20
|
+
nextCursor: config.nextCursor,
|
|
21
|
+
// The walk's per-page hook emits no Cmds — the reconciler accumulates the
|
|
22
|
+
// page's items itself in `pageOk` (where it holds its own slice), so the
|
|
23
|
+
// walk's onPage stays empty.
|
|
24
|
+
onPage: () => [],
|
|
25
|
+
retry: config.retry,
|
|
26
|
+
circuit: config.circuit,
|
|
27
|
+
rateLimit: config.rateLimit,
|
|
28
|
+
deadline: config.deadline
|
|
29
|
+
},
|
|
30
|
+
rng
|
|
31
|
+
);
|
|
32
|
+
function changeId(change) {
|
|
33
|
+
return config.idOf ? config.idOf(change) : JSON.stringify(change);
|
|
34
|
+
}
|
|
35
|
+
function withWalk(s, w) {
|
|
36
|
+
return { ...s, walk: w };
|
|
37
|
+
}
|
|
38
|
+
function init() {
|
|
39
|
+
return {
|
|
40
|
+
phase: "idle",
|
|
41
|
+
walk: walk.init(),
|
|
42
|
+
actual: [],
|
|
43
|
+
plan: [],
|
|
44
|
+
appliedCursor: 0,
|
|
45
|
+
applied: initCache()
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function scan(s, at) {
|
|
49
|
+
if (s.phase !== "idle") return [s, []];
|
|
50
|
+
const [w, cmds] = walk.start(s.walk, at);
|
|
51
|
+
const next = { ...s, phase: "scanning", walk: w };
|
|
52
|
+
return [next, cmds];
|
|
53
|
+
}
|
|
54
|
+
function pageOk(s, page, at) {
|
|
55
|
+
if (s.phase !== "scanning") {
|
|
56
|
+
const [w2] = walk.pageOk(s.walk, page, at);
|
|
57
|
+
return [withWalk(s, w2), []];
|
|
58
|
+
}
|
|
59
|
+
const [w, walkCmds] = walk.pageOk(s.walk, page, at);
|
|
60
|
+
const actual = [...s.actual, ...config.itemsOf(page)];
|
|
61
|
+
const scanned = { ...s, walk: w, actual };
|
|
62
|
+
const cmds = walkCmds;
|
|
63
|
+
if (walk.isComplete(scanned.walk)) {
|
|
64
|
+
const [planned2, applyCmds] = beginPlan(scanned, at);
|
|
65
|
+
return [planned2, [...cmds, ...applyCmds]];
|
|
66
|
+
}
|
|
67
|
+
return [scanned, cmds];
|
|
68
|
+
}
|
|
69
|
+
function pageErr(s, error, at) {
|
|
70
|
+
const [w, cmds] = walk.pageErr(s.walk, error, at);
|
|
71
|
+
let next = withWalk(s, w);
|
|
72
|
+
if (s.phase === "scanning" && w.resilience.calls[PAGE_KEY]?.phase === "failed") {
|
|
73
|
+
next = { ...next, phase: "failed" };
|
|
74
|
+
}
|
|
75
|
+
return [next, cmds];
|
|
76
|
+
}
|
|
77
|
+
function planned(s, at, changes) {
|
|
78
|
+
const plan = changes ?? config.diff(config.desired, s.actual);
|
|
79
|
+
const planning = {
|
|
80
|
+
...s,
|
|
81
|
+
phase: "applying",
|
|
82
|
+
plan,
|
|
83
|
+
appliedCursor: 0
|
|
84
|
+
};
|
|
85
|
+
return applyNext(planning, at);
|
|
86
|
+
}
|
|
87
|
+
function beginPlan(s, at) {
|
|
88
|
+
return planned(s, at);
|
|
89
|
+
}
|
|
90
|
+
function applyNext(s, at) {
|
|
91
|
+
if (s.phase !== "applying") return [s, []];
|
|
92
|
+
let cursor = s.appliedCursor;
|
|
93
|
+
while (cursor < s.plan.length) {
|
|
94
|
+
const change = s.plan[cursor];
|
|
95
|
+
if (change === void 0) {
|
|
96
|
+
cursor++;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const id = changeId(change);
|
|
100
|
+
if (get(s.applied, id, at) === void 0) {
|
|
101
|
+
const advanced = { ...s, appliedCursor: cursor };
|
|
102
|
+
return [advanced, [config.apply(change)]];
|
|
103
|
+
}
|
|
104
|
+
cursor++;
|
|
105
|
+
}
|
|
106
|
+
const done = { ...s, phase: "done", appliedCursor: cursor };
|
|
107
|
+
return [done, []];
|
|
108
|
+
}
|
|
109
|
+
function applied(s, change, at) {
|
|
110
|
+
if (s.phase !== "applying") return [s, []];
|
|
111
|
+
const id = changeId(change);
|
|
112
|
+
const applied2 = set(
|
|
113
|
+
s.applied,
|
|
114
|
+
id,
|
|
115
|
+
change,
|
|
116
|
+
at,
|
|
117
|
+
Number.MAX_SAFE_INTEGER
|
|
118
|
+
);
|
|
119
|
+
const advanced = {
|
|
120
|
+
...s,
|
|
121
|
+
applied: applied2,
|
|
122
|
+
appliedCursor: s.appliedCursor + 1
|
|
123
|
+
};
|
|
124
|
+
return applyNext(advanced, at);
|
|
125
|
+
}
|
|
126
|
+
function onTimer(s, msg) {
|
|
127
|
+
const [w, cmds] = walk.onTimer(s.walk, msg);
|
|
128
|
+
if (w === s.walk) {
|
|
129
|
+
return [s, []];
|
|
130
|
+
}
|
|
131
|
+
let next = withWalk(s, w);
|
|
132
|
+
if (s.phase === "scanning" && w.resilience.calls[PAGE_KEY]?.phase === "failed") {
|
|
133
|
+
next = { ...next, phase: "failed" };
|
|
134
|
+
}
|
|
135
|
+
return [next, cmds];
|
|
136
|
+
}
|
|
137
|
+
function isComplete(s) {
|
|
138
|
+
return s.phase === "done";
|
|
139
|
+
}
|
|
140
|
+
function subs(s) {
|
|
141
|
+
return walk.subs(s.walk);
|
|
142
|
+
}
|
|
143
|
+
function handlers(ports) {
|
|
144
|
+
return walk.handlers(ports);
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
init,
|
|
148
|
+
scan,
|
|
149
|
+
pageOk,
|
|
150
|
+
pageErr,
|
|
151
|
+
planned,
|
|
152
|
+
applyNext,
|
|
153
|
+
applied,
|
|
154
|
+
onTimer,
|
|
155
|
+
isComplete,
|
|
156
|
+
subs,
|
|
157
|
+
handlers
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function liftReconciler(state, [slice, cmds]) {
|
|
161
|
+
return [{ ...state, rec: slice }, cmds];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export { createReconciler, liftReconciler };
|
|
165
|
+
//# sourceMappingURL=index.js.map
|
|
166
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/reconciler/index.ts"],"names":["w","planned","applied"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @demlik/tea/reconciler — the desired-vs-actual sync loop (the \"fleet-sync /\n * coverage-gap\" shape): walk the ACTUAL world end to end, diff it against the\n * DESIRED spec, then apply each resulting `Change` one at a time until the two\n * agree. The control-plane reconcile pattern (Kubernetes controllers, fleet\n * config rollout, accessibility coverage-gap remediation) lifted to the uniform\n * L2 knob contract.\n *\n * ## The recipe (which bricks it composes)\n *\n * - **`../paginated-walk`** owns the SCAN. The actual world is rarely a single\n * fetch — it is a paginated list (every node in a fleet, every page of an\n * audit, every row of a table). `reconciler` does not re-implement that walk:\n * it embeds a `paginated-walk` slice and lets it sequence the actual-state\n * fetch (gated, retried, rate-limited, resumable across eviction). The walk's\n * own `onPage` hook stays empty (`() => []`) — `reconciler` accumulates each\n * fetched page's items into the running `actual` snapshot inside its OWN\n * `pageOk` verb, where it holds its slice and so can extend the accumulator\n * the walk cannot see; when the walk reports `isComplete`, the full actual\n * snapshot is in hand.\n * - **`../cache`** owns the APPLIED ledger. Every `Change` that settles is\n * written into a TTL cache keyed by the change's id, so a re-`applyNext`\n * after an eviction skips changes already applied (idempotent re-apply — the\n * reconcile is safe to resume from any point). The cache is the durable\n * \"what have I already done?\" memory the apply loop reads before re-issuing.\n *\n * Because the scan IS a `paginated-walk` slice (which itself IS a\n * `resilient-call` slice), this knob **inherits that contract verbatim**: the\n * slice is flat plain data, every verb returns `readonly [State, Cmd[]]`, time\n * is always an `at` parameter, and the jitter RNG is injected once at\n * `createReconciler(config, rng)` — never read inside a verb (the spec:\n * `reconciler` *extends* `paginated-walk`).\n *\n * ## Why the run lifecycle is INLINE (run-tracker not extracted)\n *\n * The reconcile phase union (`idle` / `scanning` / `applying` / `done` /\n * `failed`) and its bookkeeping (`plan`, `appliedCursor`, the `actual`\n * accumulator) live directly on this slice rather than behind a `run-tracker`\n * sub-knob — exactly as `monitored-run` keeps its lifecycle inline. Extraction\n * earns its keep when two independent axes vary; here the lifecycle has one\n * anchoring axis (this reconcile pass). The genuinely independent axes — the\n * paginated SCAN and the applied LEDGER — ARE extracted (`../paginated-walk`,\n * `../cache`). The lifecycle is the glue that sequences scan → plan → apply.\n *\n * ## The two non-negotiables (canon)\n *\n * - **Durable** — the slice is plain data: the paginated-walk slice, the\n * accumulated `actual` items, the planned `Change[]`, the applied cursor, and\n * the applied-ledger cache (all JSON-serializable). A reconcile survives DO\n * eviction mid-scan or mid-apply and resumes from the exact position.\n * - **Replayable** — every transition is a verb returning new state + Cmds.\n * `diff` / `apply` are pure config functions applied at the verb boundary;\n * the Cmds they yield are plain data. `replay` reconstructs the whole pass\n * exactly. Nothing here closes over time or RNG.\n *\n * ## Where the clock / RNG live\n *\n * Inside the verbs: nowhere. `at` arrives on `scan` / `pageOk` / `pageErr` /\n * `applied` / `onTimer` and threads into the underlying paginated-walk gate (so a\n * retried / rate-limited page measures backoff from that `at`) and into the\n * applied-ledger cache writes. The jitter RNG is injected at construction. The\n * only clock read is `Date.now()` inside the inherited paginated-walk `handlers`\n * port (the effect boundary).\n *\n * ## Typical wiring\n *\n * const rec = createReconciler<FleetNode, NodeSpec, Page, SyncChange, ApplyCmd>({\n * desired: targetSpecs, // the spec to converge on\n * firstPage: null, // actual-list page-one sentinel\n * nextCursor: (page) => page.next ?? null,\n * itemsOf: (page) => page.nodes, // page → actual items\n * diff: (desired, actual) => planChanges(desired, actual),\n * apply: (change) => ({ type: \"apply_change\", change }),\n * idOf: (change) => change.nodeId, // applied-ledger key\n * rateLimit: { capacity: 10, refillPerSec: 5 },\n * retry: defaultRetryPolicy,\n * });\n *\n * // in the machine:\n * init: () => [{ rec: rec.init() }, []],\n * update: {\n * reconcile: (s, m) => lift(s, rec.scan(s.rec, m.at)),\n * page_ok: (s, m) => lift(s, rec.pageOk(s.rec, m.result, m.at)),\n * page_err: (s, m) => lift(s, rec.pageErr(s.rec, m.error, m.at)),\n * change_done: (s, m) => lift(s, rec.applied(s.rec, m.change, m.at)),\n * retry_due: (s, m) => lift(s, rec.onTimer(s.rec, m)),\n * },\n * subscriptions: (s) => rec.subs(s.rec),\n * subscribe: { deadline: subscribeDeadline },\n * interpret: rec.handlers({ run: (cursor) => api.listActual(cursor) }),\n */\n\nimport {\n get as cacheGet,\n set as cacheSet,\n initCache,\n type TtlCache,\n} from \"../cache\";\nimport type { Cmd } from \"../index\";\nimport {\n createPaginatedWalk,\n type DeadlineSub,\n deadlineSub,\n type FetchPageCmd,\n PAGE_KEY,\n type PageErrMsg,\n type PageOkMsg,\n type PaginatedWalkState,\n type PaginatedWalkTimerMsg,\n subscribeDeadline,\n} from \"../paginated-walk\";\nimport type {\n CircuitConfig,\n DeadlineConfig,\n RateLimitConfig,\n ResilientPorts,\n} from \"../resilient-call\";\nimport type { RetryPolicy } from \"../retry-backoff\";\n\n// ===========================================================================\n// Config — the knob. `desired` / `diff` / `apply` / `idOf` describe the\n// reconcile; the SCAN knobs (firstPage / nextCursor / itemsOf) describe how the\n// actual world is walked; the resilience bricks are inherited from\n// paginated-walk (every one optional — omit a brick, omit its gate).\n// ===========================================================================\n\n/**\n * The reconciler knob. Generic in:\n * - `Actual` — one item of the ACTUAL world (a fleet node, an audited page, a\n * table row). The scan accumulates these.\n * - `Desired` — the DESIRED spec value the diff converges toward. Opaque to\n * this module; only `diff` reads it.\n * - `Page` — the shape one actual-list page fetch resolves with.\n * - `Change` — one unit of remediation the diff produces and `apply` realizes.\n * - `ApplyCmd`— the Cmd `apply(change)` yields (the effect that mutates the\n * actual world toward desired).\n *\n * `diff` / `apply` / `idOf` / `nextCursor` / `itemsOf` are PURE config functions\n * applied at the verb boundary, never stored in the slice or in a Cmd (Cmds are\n * plain data, not closures). The resilience bricks (`retry` / `circuit` /\n * `rateLimit` / `deadline`) are the SAME optional knobs `paginated-walk` /\n * `resilient-call` expose — reconciler extends them; `rateLimit` keeps the scan\n * from 429-ing the upstream listing API.\n */\nexport interface ReconcilerConfig<\n Actual,\n Desired,\n Page,\n Change,\n ApplyCmd extends Cmd,\n Cursor = number,\n> {\n /**\n * The DESIRED spec to converge on. Held verbatim and handed to `diff` once the\n * scan completes. Any JSON-serializable value — a list of specs, a target map,\n * a single target object — `diff` alone interprets it.\n */\n readonly desired: Desired;\n /**\n * Compute the remediation plan: the ordered `Change[]` that moves `actual` to\n * `desired`. Called once when the scan finishes (the full actual snapshot is in\n * hand). An empty plan means \"already in sync — nothing to apply\". PURE +\n * consumer-specific (only the consumer knows the domain's reconcile rules).\n */\n readonly diff: (\n desired: Desired,\n actual: readonly Actual[],\n ) => readonly Change[];\n /**\n * Realize one `Change` as the Cmd that mutates the actual world. Returns plain\n * data (a `{ type, ... }` Cmd the consumer's interpret handler performs).\n * PURE — the side effect happens when the runtime performs the emitted Cmd,\n * never here.\n */\n readonly apply: (change: Change) => ApplyCmd;\n /**\n * Stable identity for a `Change` — the applied-ledger cache key. A re-`applyNext`\n * after eviction (or after a re-plan) skips a change whose id is already in the\n * ledger (idempotent re-apply). MUST be a property of the change itself, NOT its\n * position in the plan: the ledger has to survive re-planning. When omitted the\n * default keys by the change's serialized CONTENT (`JSON.stringify(change)`) —\n * a stable per-change identity that is unchanged by where the change sits in the\n * plan. Supply a domain id (the target node id, the resource key) when one\n * exists; it is cheaper than serializing and dedupes two changes that differ\n * only in fields irrelevant to identity. PURE.\n *\n * Why NOT positional: keying by index silently drops changes on the documented\n * re-plan path. After applying the index-0 change, its `\"0\"` ledger entry would\n * match WHATEVER different, never-applied change lands at index 0 in the next\n * plan — the apply loop skips it as \"already done\". Identity must be intrinsic\n * to the change so the ledger means \"this change settled\", not \"slot N settled\".\n */\n readonly idOf?: (change: Change) => string;\n\n // --- scan knobs (the actual-world walk), inherited from paginated-walk ---\n /**\n * The cursor the actual-list scan fetches first. For an offset API typically\n * `0`; for a token API whatever sentinel the API treats as page one.\n */\n readonly firstPage: Cursor;\n /**\n * Extract the next scan cursor from a fetched page — a `Cursor` to keep\n * scanning, or `null` when the actual listing is exhausted (scan finishes,\n * plan computed). PURE + consumer-specific.\n */\n readonly nextCursor: (page: Page) => Cursor | null;\n /**\n * Extract the actual items from one fetched page. Each page's items are\n * appended to the running `actual` accumulator. PURE.\n */\n readonly itemsOf: (page: Page) => readonly Actual[];\n\n // --- resilience bricks, inherited from paginated-walk (all optional) ---\n /** Exponential-backoff retry policy for a transient scan-page failure. */\n readonly retry?: RetryPolicy;\n /** Circuit breaker around the scan fetch (one upstream → one breaker). */\n readonly circuit?: CircuitConfig;\n /** Token-bucket rate limit on the scan — the \"don't 429 the listing API\" knob. */\n readonly rateLimit?: RateLimitConfig;\n /** Overall wall-clock deadline per individual scan-page fetch. */\n readonly deadline?: DeadlineConfig;\n}\n\n// ===========================================================================\n// Slice — the Model field this knob owns. The paginated-walk scan slice + the\n// accumulated actual snapshot + the inline reconcile lifecycle (plan, applied\n// cursor, ledger). All flat plain data.\n// ===========================================================================\n\n/**\n * Reconcile lifecycle phase. Discriminated on `phase`:\n *\n * - `idle` — created, never scanned. `scan` starts the actual-world walk.\n * - `scanning` — the paginated-walk is traversing the actual listing; pages\n * fold into `actual` as they arrive. The scan owns the `walk`\n * sub-slice; this phase ends when the walk finishes.\n * - `applying` — the scan finished, `diff` produced the `plan`, and changes are\n * being applied one at a time. `appliedCursor` is the index of\n * the next change to apply.\n * - `done` — every planned change applied (or an empty plan: already in\n * sync). Terminal.\n * - `failed` — the scan failed terminally (the page-fetch retries were\n * exhausted / a deadline fired). `error` carries why. Terminal.\n */\nexport type ReconcilePhase =\n | \"idle\"\n | \"scanning\"\n | \"applying\"\n | \"done\"\n | \"failed\";\n\n/**\n * The slice. Plain data end to end:\n * - `phase` — the lifecycle discriminant.\n * - `walk` — the `../paginated-walk` scan slice (cursor + resilience).\n * Drives the actual-world traversal; POSITION survives\n * eviction.\n * - `actual` — the accumulated actual items, appended page by page during\n * the scan. The full snapshot `diff` is computed against.\n * - `plan` — the `Change[]` `diff` produced. Empty until the scan\n * finishes; the apply loop walks it by `appliedCursor`.\n * - `appliedCursor` — index of the NEXT change to apply (0 ≤ cursor ≤ plan.length).\n * `cursor === plan.length` means every change applied.\n * - `applied` — the applied-ledger cache: `idOf(change) → change`, written\n * as each change settles. The idempotency memory the apply\n * loop reads to skip already-applied changes after eviction.\n */\nexport interface ReconcilerState<Actual, Change, Page, Cursor = number> {\n readonly phase: ReconcilePhase;\n readonly walk: PaginatedWalkState<Cursor, Page>;\n readonly actual: readonly Actual[];\n readonly plan: readonly Change[];\n readonly appliedCursor: number;\n readonly applied: TtlCache<Change>;\n}\n\n// ===========================================================================\n// Cmds + Msgs the knob speaks. The scan emits the inherited page-fetch effect;\n// the apply loop emits the consumer's `apply(change)` Cmds.\n// ===========================================================================\n\n/**\n * The actual-list page-fetch effect: the inherited `resilient_run` Cmd from\n * paginated-walk, whose `input` is the `Cursor` to fetch and whose `key` is the\n * fixed `PAGE_KEY`. The consumer's `handlers(ports)` interprets it.\n */\nexport type ScanPageCmd<Cursor> = FetchPageCmd<Cursor>;\n\n/** Page-settled Msgs the scan `handlers` port dispatches back (inherited verbatim). */\nexport type ScanPageOkMsg<Page> = PageOkMsg<Page>;\nexport type ScanPageErrMsg = PageErrMsg;\n\n/** The scan retry / deadline timer Msg — inherited from paginated-walk. */\nexport type ReconcilerTimerMsg = PaginatedWalkTimerMsg;\n\n/**\n * Ports the consumer supplies to `handlers`. `run(cursor)` fetches ONE page of\n * the ACTUAL listing for the given cursor; throwing routes to `pageErr` (and\n * thus the scan backoff). Same shape as paginated-walk's port.\n */\nexport interface ReconcilerPorts<Cursor, Page>\n extends ResilientPorts<Cursor, Page> {}\n\n// ===========================================================================\n// The knob factory.\n// ===========================================================================\n\n/**\n * Build a reconciler knob from `config`. `rng` is injected for the underlying\n * scan-retry jitter — pass a fixed `() => 0` in tests to pin backoff; defaults\n * to `Math.random` (read at the verb boundary inside resilient-call, never in a\n * reconciler verb). Inherited straight from paginated-walk.\n *\n * Returns the uniform L2 knob contract: `init()`, the verbs `scan` / `pageOk` /\n * `pageErr` / `planned` / `applyNext` / `applied` / `onTimer`, the derived\n * `isComplete`, `subs(state)`, and `handlers(ports)`.\n */\nexport function createReconciler<\n Actual,\n Desired,\n Page,\n Change,\n ApplyCmd extends Cmd = Cmd,\n Cursor = number,\n>(\n config: ReconcilerConfig<Actual, Desired, Page, Change, ApplyCmd, Cursor>,\n rng: () => number = Math.random,\n) {\n type State = ReconcilerState<Actual, Change, Page, Cursor>;\n // The combined Cmd type the verbs can emit: the inherited scan page-fetch\n // effect plus whatever `apply(change)` yields.\n type OutCmd = ScanPageCmd<Cursor> | ApplyCmd;\n\n // The scan is a paginated-walk over the actual listing. We feed it the scan\n // cursor knobs + the resilience bricks; `onPage` is OURS — it is a pure marker\n // (we accumulate items in `pageOk` instead, where we hold the reconciler\n // slice). The walk's `Page` payload is the consumer's `Page`; the reconciler\n // slice stores the walk as `PaginatedWalkState<Cursor, unknown>` so its own\n // type does not have to thread `Page` (the page never lands in the slice — only\n // the extracted `actual` items do).\n const walk = createPaginatedWalk<Cursor, Page, never>(\n {\n firstPage: config.firstPage,\n nextCursor: config.nextCursor,\n // The walk's per-page hook emits no Cmds — the reconciler accumulates the\n // page's items itself in `pageOk` (where it holds its own slice), so the\n // walk's onPage stays empty.\n onPage: () => [],\n retry: config.retry,\n circuit: config.circuit,\n rateLimit: config.rateLimit,\n deadline: config.deadline,\n },\n rng,\n );\n\n /**\n * The applied-ledger key for a change. Custom `idOf` when supplied; otherwise\n * the change's serialized CONTENT — a stable per-change identity. NEVER the\n * positional index: an index-keyed ledger drops a never-applied change that\n * happens to land at a slot a different, already-applied change once occupied\n * (the re-plan defect). Identity is intrinsic to the change, so the ledger\n * means \"this change settled\", surviving a re-plan that reorders the slots.\n */\n function changeId(change: Change): string {\n return config.idOf ? config.idOf(change) : JSON.stringify(change);\n }\n\n /** Replace the walk sub-slice, leaving the rest of the reconciler untouched. */\n function withWalk(s: State, w: State[\"walk\"]): State {\n return { ...s, walk: w };\n }\n\n /** The starting slice: idle, fresh walk + empty accumulator / plan / ledger. */\n function init(): State {\n return {\n phase: \"idle\",\n walk: walk.init(),\n actual: [],\n plan: [],\n appliedCursor: 0,\n applied: initCache<Change>(),\n };\n }\n\n // === Verb: scan ==========================================================\n\n /**\n * Begin the reconcile: start the paginated walk over the ACTUAL world and\n * enter `scanning`. Issues the first scan-page fetch (gated through the\n * inherited resilience). A no-op on any non-`idle` phase — re-scanning a\n * reconcile already in flight would restart the actual walk and double-count,\n * so a stray `scan` is absorbed. PURE — `at` threads into the fetch gate.\n */\n function scan(s: State, at: number): readonly [State, readonly OutCmd[]] {\n if (s.phase !== \"idle\") return [s, []];\n const [w, cmds] = walk.start(s.walk, at);\n const next: State = { ...s, phase: \"scanning\", walk: w };\n return [next, cmds];\n }\n\n // === Verb: pageOk ========================================================\n\n /**\n * Record a successfully fetched actual-list `page`: append its items to the\n * `actual` accumulator and advance the walk. When the walk finishes (the\n * listing is exhausted), the full actual snapshot is in hand → compute the\n * plan (`planned`) and start applying. PURE — `at` is the scan clock.\n *\n * A stray `pageOk` while not `scanning` (a late duplicate after the scan\n * completed) is absorbed by the walk (no cursor advance) and contributes no\n * items — the reconcile never re-accumulates after the plan is computed.\n */\n function pageOk(\n s: State,\n page: Page,\n at: number,\n ): readonly [State, readonly OutCmd[]] {\n // Once we are past scanning, the actual snapshot is frozen — absorb stray pages.\n if (s.phase !== \"scanning\") {\n const [w] = walk.pageOk(s.walk, page, at);\n return [withWalk(s, w), []];\n }\n\n const [w, walkCmds] = walk.pageOk(s.walk, page, at);\n const actual = [...s.actual, ...config.itemsOf(page)];\n const scanned: State = { ...s, walk: w, actual };\n\n // The walk emits the next scan-page fetch while it stays fetching. When it\n // finishes (done / paused with no fetch), `walkCmds` is empty.\n const cmds: readonly OutCmd[] = walkCmds;\n\n // Scan exhausted → plan + begin applying. `walk.isComplete` is the honest\n // \"the API returned a null next cursor\" signal.\n if (walk.isComplete(scanned.walk)) {\n const [planned, applyCmds] = beginPlan(scanned, at);\n return [planned, [...cmds, ...applyCmds]];\n }\n\n return [scanned, cmds];\n }\n\n // === Verb: pageErr =======================================================\n\n /**\n * Record a failed scan-page fetch and back off via the inherited resilience:\n * schedule a retry (the scan cursor stays parked — no advance) or, once the\n * page-fetch retries are exhausted, the underlying call settles `failed`. When\n * the scan call is terminally failed, the whole reconcile enters `failed`.\n * PURE — `at` stamps the breaker trip + the retry-delay base.\n */\n function pageErr(\n s: State,\n error: unknown,\n at: number,\n ): readonly [State, readonly OutCmd[]] {\n const [w, cmds] = walk.pageErr(s.walk, error, at);\n let next = withWalk(s, w);\n // The page-fetch call settling `failed` (retries exhausted) is a terminal\n // scan failure → the reconcile cannot trust an incomplete actual snapshot.\n if (\n s.phase === \"scanning\" &&\n w.resilience.calls[PAGE_KEY]?.phase === \"failed\"\n ) {\n next = { ...next, phase: \"failed\" };\n }\n return [next, cmds];\n }\n\n // === Verb: planned =======================================================\n\n /**\n * Compute the remediation plan from the accumulated `actual` snapshot and the\n * configured `desired`, store it, and enter `applying`. Normally driven\n * internally by `pageOk` when the scan finishes (`beginPlan`), but exposed as a\n * verb so a consumer can force planning from an externally-supplied actual\n * snapshot (e.g. a single-shot actual fetch outside the paginated scan), or\n * re-plan after a desired change. An empty plan settles straight to `done`\n * (already in sync). PURE — `at` only stamps the (empty) apply-loop clock.\n *\n * `changes` is optional: omit it to diff the slice's own accumulated `actual`\n * against `config.desired`; pass an explicit plan to install it directly\n * (skipping the diff — the consumer computed it elsewhere).\n */\n function planned(\n s: State,\n at: number,\n changes?: readonly Change[],\n ): readonly [State, readonly OutCmd[]] {\n const plan = changes ?? config.diff(config.desired, s.actual);\n const planning: State = {\n ...s,\n phase: \"applying\",\n plan,\n appliedCursor: 0,\n };\n return applyNext(planning, at);\n }\n\n /** Internal: compute + install the plan and kick off the apply loop. */\n function beginPlan(\n s: State,\n at: number,\n ): readonly [State, readonly OutCmd[]] {\n return planned(s, at);\n }\n\n // === Verb: applyNext =====================================================\n\n /**\n * Emit the apply Cmd for the next not-yet-applied change in the plan, skipping\n * any whose id is already in the applied ledger (idempotent re-apply after\n * eviction). Advances `appliedCursor` past skipped + the emitted change. When\n * the cursor reaches the end of the plan, settles `done`. A no-op unless\n * `applying`. PURE — `at` is read only for the ledger lookup clock; `idOf`\n * keys the skip check.\n *\n * Emits at most ONE apply Cmd per call (the loop is sequential: each `applied`\n * drives the next `applyNext`). This keeps the actual-world mutation\n * one-at-a-time, matching a controller's serial reconcile.\n */\n function applyNext(\n s: State,\n at: number,\n ): readonly [State, readonly OutCmd[]] {\n if (s.phase !== \"applying\") return [s, []];\n\n let cursor = s.appliedCursor;\n // Skip changes already in the ledger (a resumed reconcile re-issues from the\n // cursor but must not re-apply what already settled).\n while (cursor < s.plan.length) {\n const change = s.plan[cursor];\n // `cursor < plan.length` guarantees an element under normal indexing; the\n // explicit `undefined` narrow (vs a non-null assertion) keeps the access\n // typed without a suppression and tolerates a sparse hole by skipping it.\n if (change === undefined) {\n cursor++;\n continue;\n }\n const id = changeId(change);\n if (cacheGet(s.applied, id, at) === undefined) {\n // This change is not yet applied — emit its Cmd and park the cursor here\n // (it advances to cursor+1 only once `applied` confirms the change).\n const advanced: State = { ...s, appliedCursor: cursor };\n return [advanced, [config.apply(change)]];\n }\n cursor++;\n }\n\n // Every change applied → reconcile complete.\n const done: State = { ...s, phase: \"done\", appliedCursor: cursor };\n return [done, []];\n }\n\n // === Verb: applied =======================================================\n\n /**\n * Record that `change` finished applying: write it into the applied ledger,\n * advance the cursor past it, and drive the next `applyNext`. PURE — `at` is\n * the ledger-write clock (the entry never expires in practice; a large TTL\n * keeps the idempotency window open for the reconcile's lifetime).\n *\n * `change` is the SAME change the consumer's apply handler settled (echoed back\n * on the settle Msg) — its id is computed from the change's own identity (NOT a\n * cursor position), so the ledger key matches the skip check in `applyNext`\n * regardless of where the change sits in the plan. A no-op unless `applying` (a\n * late settle after `done` is ignored). PURE.\n */\n function applied(\n s: State,\n change: Change,\n at: number,\n ): readonly [State, readonly OutCmd[]] {\n if (s.phase !== \"applying\") return [s, []];\n const id = changeId(change);\n // A very large TTL: the ledger entry must outlive the whole reconcile pass.\n // `get(..., at)` treats it as fresh for `at < expiresAtMs`, which holds for\n // any realistic reconcile (the cache exists to dedupe within one pass).\n const applied = cacheSet(\n s.applied,\n id,\n change,\n at,\n Number.MAX_SAFE_INTEGER,\n );\n const advanced: State = {\n ...s,\n applied,\n appliedCursor: s.appliedCursor + 1,\n };\n return applyNext(advanced, at);\n }\n\n // === Verb: onTimer =======================================================\n\n /**\n * A scan retry / deadline timer fired. Defers to paginated-walk's `onTimer`: a\n * retry timer re-issues the SAME scan-page fetch (re-fetching the parked\n * cursor); a deadline timer settles the scan call failed. A deadline-driven\n * terminal scan failure escalates the whole reconcile to `failed`. A stale fire\n * is a pure no-op (inherited identity). PURE.\n */\n function onTimer(\n s: State,\n msg: ReconcilerTimerMsg,\n ): readonly [State, readonly OutCmd[]] {\n const [w, cmds] = walk.onTimer(s.walk, msg);\n if (w === s.walk) {\n // Inherited no-op identity → keep our slice identity too.\n return [s, []];\n }\n let next = withWalk(s, w);\n if (\n s.phase === \"scanning\" &&\n w.resilience.calls[PAGE_KEY]?.phase === \"failed\"\n ) {\n next = { ...next, phase: \"failed\" };\n }\n return [next, cmds];\n }\n\n // === Derived predicate ===================================================\n\n /**\n * Whether the reconcile has fully settled — every planned change applied (or an\n * empty plan: already in sync). PURE, read-only. Lets a consumer's reducer fire\n * a \"reconcile complete\" Cmd without re-checking the phase by hand.\n */\n function isComplete(s: State): boolean {\n return s.phase === \"done\";\n }\n\n // === Subs ================================================================\n\n /**\n * Pre-wired subscriptions: exactly the scan's paginated-walk subs — a retry\n * timer while a scan page is `waiting_retry`, and (with the `deadline` brick) a\n * per-page deadline timer while a scan fetch is active. The apply loop emits no\n * timers (each apply settles via the consumer's own Msg), so once the scan\n * finishes the desired sub set empties. Reconciled by id; wire `subscribe: {\n * deadline: subscribeDeadline }` (re-exported below).\n */\n function subs(s: State): readonly DeadlineSub[] {\n return walk.subs(s.walk);\n }\n\n // === Handlers ============================================================\n\n /**\n * Pre-wired interpret handler for the scan page-fetch effect. Inherited from\n * paginated-walk (`resilient_run`): wraps the consumer's `run(cursor)` actual-\n * list port via `tryInterpret` (Railway) — success → `resilient_ok` (handled by\n * `pageOk`), failure → `resilient_err` (handled by `pageErr`), each stamped\n * with `Date.now()` at the effect boundary (the ONE permitted clock read).\n *\n * interpret: rec.handlers({ run: (cursor) => api.listActual(cursor) })\n *\n * The apply-Cmd side is the CONSUMER's interpret handler (it knows how to\n * realize `apply(change)`); the consumer routes its settle Msg back through\n * `applied`. This knob only owns the scan's interpret cell.\n */\n function handlers(ports: ReconcilerPorts<Cursor, Page>) {\n return walk.handlers(ports);\n }\n\n return {\n init,\n scan,\n pageOk,\n pageErr,\n planned,\n applyNext,\n applied,\n onTimer,\n isComplete,\n subs,\n handlers,\n };\n}\n\n/**\n * Lift a knob result `[slice, cmds]` into a host `[State, cmds]` where the slice\n * lives at `state.rec`. Convenience for the common single-slice host; hosts with\n * a differently-named field spread by hand. PURE — a thin record rebuild, no\n * clock / RNG.\n */\nexport function liftReconciler<\n S extends { rec: ReconcilerState<Actual, Change, Page, Cursor> },\n Actual,\n Change,\n Page,\n Cursor,\n C extends Cmd,\n>(\n state: S,\n [slice, cmds]: readonly [\n ReconcilerState<Actual, Change, Page, Cursor>,\n readonly C[],\n ],\n): readonly [S, readonly C[]] {\n return [{ ...state, rec: slice }, cmds];\n}\n\n/**\n * Re-export the deadline Sub primitives (inherited from paginated-walk) so\n * consumers wire one import: `subscribeDeadline` is the `subscribe` cell,\n * `deadlineSub` builds the Sub literal this knob's `subs` emits.\n */\nexport { subscribeDeadline, deadlineSub };\nexport type { DeadlineSub };\n"]}
|
package/dist/recorder/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BootingRuntime } from '../index.js';
|
|
2
|
+
import '../core-BoB6t7dB.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* A recorded run, sufficient to reproduce it via `../trace-replay`.
|
|
@@ -156,13 +157,16 @@ interface Recorder<S, M> {
|
|
|
156
157
|
* await runtime.ready; // boot observe has fired
|
|
157
158
|
* const trace = rec.dump(); // { loaded, msgs, finalState }
|
|
158
159
|
*
|
|
159
|
-
* @param runtime - any `
|
|
160
|
+
* @param runtime - any `BootingRuntime<S, M>` (a full `Runtime` satisfies it).
|
|
161
|
+
* The recorder uses only `observe`, which is total before boot
|
|
162
|
+
* — so you attach it to the synchronous `run()` handle to
|
|
163
|
+
* capture the boot transition as `loaded`.
|
|
160
164
|
* @param opts - see {@link RecorderOptions}. Default records everything,
|
|
161
165
|
* no step retention.
|
|
162
166
|
*/
|
|
163
167
|
declare function recorder<S, M extends {
|
|
164
168
|
type: string;
|
|
165
|
-
}>(runtime:
|
|
169
|
+
}>(runtime: BootingRuntime<S, M>, opts?: RecorderOptions): Recorder<S, M>;
|
|
166
170
|
/**
|
|
167
171
|
* Re-hydrate a {@link Trace} from the JSONL produced by
|
|
168
172
|
* {@link Recorder.toJSONL}. The inverse of `toJSONL`, so
|
package/dist/recorder/index.js
CHANGED
|
@@ -1,157 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
function recorder(runtime, opts) {
|
|
3
|
-
const rawRate = opts?.sampleRate;
|
|
4
|
-
const sampleRate = rawRate === void 0 || Number.isNaN(rawRate) || rawRate <= 0 ? 1 : Math.min(rawRate, 1);
|
|
5
|
-
const captureSteps = opts?.captureSteps ?? false;
|
|
6
|
-
let loaded;
|
|
7
|
-
let sawBoot = false;
|
|
8
|
-
const msgs = [];
|
|
9
|
-
const steps = [];
|
|
10
|
-
let finalState;
|
|
11
|
-
let stopped = false;
|
|
12
|
-
const unobserve = runtime.observe((msg, state) => {
|
|
13
|
-
finalState = state;
|
|
14
|
-
if (msg === null) {
|
|
15
|
-
loaded = state;
|
|
16
|
-
sawBoot = true;
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
if (sampleRate < 1 && Math.random() >= sampleRate) return;
|
|
20
|
-
msgs.push(msg);
|
|
21
|
-
if (captureSteps) steps.push({ msg, state });
|
|
22
|
-
});
|
|
23
|
-
return {
|
|
24
|
-
dump() {
|
|
25
|
-
if (finalState === void 0) {
|
|
26
|
-
throw new Error(
|
|
27
|
-
"@demlik/tea/recorder: dump() called before any transition was observed. Await `runtime.ready` (boot fires the first observe) before dumping."
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
return {
|
|
31
|
-
// sawBoot ⇒ `loaded` is the captured boot state; otherwise the
|
|
32
|
-
// recorder attached post-boot and replay must re-run fresh init.
|
|
33
|
-
loaded: sawBoot ? loaded : null,
|
|
34
|
-
msgs: msgs.slice(),
|
|
35
|
-
finalState,
|
|
36
|
-
// Only attach `steps` when captureSteps was requested — keeps the
|
|
37
|
-
// dumped Trace shape honest about what was retained.
|
|
38
|
-
...captureSteps ? { steps: steps.slice() } : {}
|
|
39
|
-
};
|
|
40
|
-
},
|
|
41
|
-
toJSONL() {
|
|
42
|
-
if (finalState === void 0) {
|
|
43
|
-
throw new Error(
|
|
44
|
-
"@demlik/tea/recorder: toJSONL() called before any transition was observed. Await `runtime.ready` (boot fires the first observe) before serializing."
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
return traceToJSONL(this.dump());
|
|
48
|
-
},
|
|
49
|
-
stop() {
|
|
50
|
-
if (stopped) return;
|
|
51
|
-
stopped = true;
|
|
52
|
-
unobserve();
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
function parseJSONL(jsonl) {
|
|
57
|
-
const rawLines = jsonl.split("\n");
|
|
58
|
-
let loaded = null;
|
|
59
|
-
let sawBoot = false;
|
|
60
|
-
const msgs = [];
|
|
61
|
-
const steps = [];
|
|
62
|
-
let finalState = null;
|
|
63
|
-
let finalKnown = false;
|
|
64
|
-
for (const [i, rawLine] of rawLines.entries()) {
|
|
65
|
-
const line = rawLine.trim();
|
|
66
|
-
if (line === "") continue;
|
|
67
|
-
let parsed;
|
|
68
|
-
try {
|
|
69
|
-
parsed = JSON.parse(line);
|
|
70
|
-
} catch (err) {
|
|
71
|
-
throw new Error(
|
|
72
|
-
`@demlik/tea/recorder: parseJSONL failed on line ${i + 1}: not valid JSON. ${String(err)}`
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
if (parsed.kind === "boot") {
|
|
76
|
-
if (sawBoot) {
|
|
77
|
-
throw new Error(
|
|
78
|
-
`@demlik/tea/recorder: parseJSONL saw a second boot header on line ${i + 1}. A trace has exactly one boot line.`
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
sawBoot = true;
|
|
82
|
-
loaded = parsed.state;
|
|
83
|
-
finalState = parsed.state;
|
|
84
|
-
finalKnown = parsed.state !== null;
|
|
85
|
-
} else if (parsed.kind === "step") {
|
|
86
|
-
if (!sawBoot) {
|
|
87
|
-
throw new Error(
|
|
88
|
-
`@demlik/tea/recorder: parseJSONL saw a step line before the boot header on line ${i + 1}.`
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
msgs.push(parsed.msg);
|
|
92
|
-
if (parsed.state !== null) {
|
|
93
|
-
steps.push({ msg: parsed.msg, state: parsed.state });
|
|
94
|
-
finalState = parsed.state;
|
|
95
|
-
finalKnown = true;
|
|
96
|
-
}
|
|
97
|
-
} else {
|
|
98
|
-
throw new Error(
|
|
99
|
-
`@demlik/tea/recorder: parseJSONL saw an unknown line kind on line ${i + 1}: ` + JSON.stringify(parsed.kind)
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
if (!sawBoot) {
|
|
104
|
-
throw new Error(
|
|
105
|
-
'@demlik/tea/recorder: parseJSONL found no boot header. A trace must start with { kind: "boot", state }.'
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
if (!finalKnown || finalState === null) {
|
|
109
|
-
throw new Error(
|
|
110
|
-
"@demlik/tea/recorder: parseJSONL could not determine finalState \u2014 the boot state was null and no step carried a non-null state. The trace is unusable for replay."
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
return { loaded, msgs, finalState, steps };
|
|
114
|
-
}
|
|
115
|
-
function traceAttachment(trace) {
|
|
116
|
-
return {
|
|
117
|
-
filename: "tea-trace.jsonl",
|
|
118
|
-
data: traceToJSONL(trace),
|
|
119
|
-
contentType: "application/x-ndjson"
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
function breadcrumbsFromTrace(trace, opts) {
|
|
123
|
-
const rawLimit = opts?.limit;
|
|
124
|
-
const limit = rawLimit === void 0 || Number.isNaN(rawLimit) ? 100 : Math.max(0, Math.floor(rawLimit));
|
|
125
|
-
const serialize = opts?.serialize;
|
|
126
|
-
const start = Math.max(0, trace.msgs.length - limit);
|
|
127
|
-
const kept = start === 0 ? trace.msgs : trace.msgs.slice(start);
|
|
128
|
-
const crumbs = [];
|
|
129
|
-
for (const msg of kept) {
|
|
130
|
-
crumbs.push({
|
|
131
|
-
category: "tea",
|
|
132
|
-
type: "default",
|
|
133
|
-
level: "info",
|
|
134
|
-
message: serialize ? serialize(msg) : msg.type,
|
|
135
|
-
// Full msg kept verbatim so the structured payload survives even when
|
|
136
|
-
// `message` is a terse one-liner. Cast: a Msg is a `{ type: string }`
|
|
137
|
-
// record, which is a `Record<string, unknown>` for Sentry's `data`.
|
|
138
|
-
data: { msg }
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
return crumbs;
|
|
142
|
-
}
|
|
143
|
-
function traceToJSONL(trace) {
|
|
144
|
-
const lines = [];
|
|
145
|
-
lines.push(JSON.stringify({ kind: "boot", state: trace.loaded }));
|
|
146
|
-
const steps = trace.steps;
|
|
147
|
-
for (let i = 0; i < trace.msgs.length; i++) {
|
|
148
|
-
const msg = trace.msgs[i];
|
|
149
|
-
const stepState = steps ? steps[i]?.state : i === trace.msgs.length - 1 ? trace.finalState : null;
|
|
150
|
-
lines.push(JSON.stringify({ kind: "step", msg, state: stepState }));
|
|
151
|
-
}
|
|
152
|
-
return lines.join("\n");
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export { breadcrumbsFromTrace, parseJSONL, recorder, traceAttachment };
|
|
1
|
+
export { breadcrumbsFromTrace, parseJSONL, recorder, traceAttachment } from '../chunk-UYWUVEP6.js';
|
|
156
2
|
//# sourceMappingURL=index.js.map
|
|
157
3
|
//# sourceMappingURL=index.js.map
|