@demlik/tea 0.1.0 → 0.1.1
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-CMG2KMWX.js → chunk-2WZHS4P2.js} +220 -177
- package/dist/chunk-2WZHS4P2.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-5VNWZADP.js +49 -0
- package/dist/chunk-5VNWZADP.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-7VEG4KHB.js +202 -0
- package/dist/chunk-7VEG4KHB.js.map +1 -0
- package/dist/{chunk-PYBQREFU.js → chunk-A56GNPI7.js} +20 -6
- package/dist/chunk-A56GNPI7.js.map +1 -0
- package/dist/chunk-BNJYE4AU.js +157 -0
- package/dist/chunk-BNJYE4AU.js.map +1 -0
- package/dist/chunk-BYQUGDD3.js +16 -0
- package/dist/chunk-BYQUGDD3.js.map +1 -0
- package/dist/chunk-D5EQDLDV.js +186 -0
- package/dist/chunk-D5EQDLDV.js.map +1 -0
- package/dist/chunk-EDGDR5XX.js +60 -0
- package/dist/chunk-EDGDR5XX.js.map +1 -0
- package/dist/chunk-EIZJTYKR.js +93 -0
- package/dist/chunk-EIZJTYKR.js.map +1 -0
- package/dist/chunk-EQRUAP2A.js +56 -0
- package/dist/chunk-EQRUAP2A.js.map +1 -0
- package/dist/chunk-FH5RUIIL.js +61 -0
- package/dist/chunk-FH5RUIIL.js.map +1 -0
- package/dist/chunk-HJO6VNQI.js +15 -0
- package/dist/chunk-HJO6VNQI.js.map +1 -0
- package/dist/chunk-IFBQJYGI.js +132 -0
- package/dist/chunk-IFBQJYGI.js.map +1 -0
- package/dist/chunk-IN33Q4VJ.js +52 -0
- package/dist/chunk-IN33Q4VJ.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-LEC4U6CX.js +91 -0
- package/dist/chunk-LEC4U6CX.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-QGZW4W3E.js +20 -0
- package/dist/chunk-QGZW4W3E.js.map +1 -0
- package/dist/{chunk-PYOCUBQJ.js → chunk-RGTPPF4U.js} +22 -15
- package/dist/chunk-RGTPPF4U.js.map +1 -0
- package/dist/chunk-RVJFGRGK.js +222 -0
- package/dist/chunk-RVJFGRGK.js.map +1 -0
- package/dist/chunk-RVU3RBE6.js +31 -0
- package/dist/chunk-RVU3RBE6.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-TAO4YOLO.js +87 -0
- package/dist/chunk-TAO4YOLO.js.map +1 -0
- package/dist/chunk-TC4HWABA.js +506 -0
- package/dist/chunk-TC4HWABA.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-WUV3PSCZ.js +54 -0
- package/dist/chunk-WUV3PSCZ.js.map +1 -0
- package/dist/chunk-XRXKH7KA.js +268 -0
- package/dist/chunk-XRXKH7KA.js.map +1 -0
- package/dist/circuit-breaker/index.js +1 -51
- package/dist/circuit-breaker/index.js.map +1 -1
- package/dist/core-CymXZCeY.d.ts +178 -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.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 +1732 -92
- package/dist/do/index.js +677 -31
- package/dist/do/index.js.map +1 -1
- package/dist/durable-effects-2MDxDs45.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 +1 -1
- package/dist/machine-viz/index.js +1 -1
- 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 +86 -0
- package/dist/parity/index.js +77 -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 +1 -1
- package/dist/pbt/index.js +5 -4
- 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 +109 -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 +39 -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 +251 -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/saga/index.d.ts +225 -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.js +1 -52
- package/dist/throttle/index.js.map +1 -1
- package/dist/throttled-input/index.d.ts +372 -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-N9LZbdnr.d.ts} +1 -1
- package/dist/with-deadline/index.d.ts +196 -0
- package/dist/with-deadline/index.js +130 -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 +274 -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 +95 -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 +444 -0
- package/dist/workflow/index.js +225 -0
- package/dist/workflow/index.js.map +1 -0
- package/package.json +162 -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,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @demlik/tea/paginator — the cursor/offset/page-token walk loop as pure
|
|
3
|
+
* state + ops.
|
|
4
|
+
*
|
|
5
|
+
* Same shape as `@demlik/tea/retry-backoff`, `@demlik/tea/circuit-breaker`,
|
|
6
|
+
* and `@demlik/tea/rate-limit`: a state type plus pure transition functions,
|
|
7
|
+
* host-agnostic by construction. Nothing here reads the clock, the RNG, or any
|
|
8
|
+
* I/O on its own behalf — the consumer fetches a page out-of-band and folds the
|
|
9
|
+
* *result* back in through `recordPage`. That keeps every function inside TEA's
|
|
10
|
+
* invariant 2 (transitions are pure: same inputs → same outputs) so a consumer
|
|
11
|
+
* can thread the walk through `update` and exercise it with `replay` / the pbt
|
|
12
|
+
* fold without a fake network.
|
|
13
|
+
*
|
|
14
|
+
* Every op returns a NEW state value and never mutates its input (invariant 1 —
|
|
15
|
+
* state is a value, not a place). All fields are `readonly` and plain data, so
|
|
16
|
+
* the whole walk is trivially JSON-serializable into a consumer's `Store<S>`
|
|
17
|
+
* and survives a Durable-Object eviction / reload mid-walk: the cursor lives in
|
|
18
|
+
* the Model, not a closure (the durability + replay non-negotiables every TEA
|
|
19
|
+
* knob preserves).
|
|
20
|
+
*
|
|
21
|
+
* ## The cursor is opaque, and the consumer advances it
|
|
22
|
+
*
|
|
23
|
+
* A paginated API hands back a *next cursor* however it likes: an offset
|
|
24
|
+
* (`?offset=40`), a page token (`?pageToken=ey…`), a "next" URL, or `null` at
|
|
25
|
+
* the end. This module is generic in `Cursor` and never inspects it — it only
|
|
26
|
+
* carries it forward. The consumer (who alone knows the API's shape) reads the
|
|
27
|
+
* next cursor off the just-fetched page and passes it to `recordPage`:
|
|
28
|
+
*
|
|
29
|
+
* const next = page.nextPageToken ?? null; // consumer-specific extraction
|
|
30
|
+
* const walk = recordPage(state.walk, page.items.length, next);
|
|
31
|
+
*
|
|
32
|
+
* Passing `next: null` (or a missing `undefined` cursor — the op compares with
|
|
33
|
+
* loose `== null`) means "the API said there is no next page" → the walk
|
|
34
|
+
* finishes (`done`). Passing a `Cursor` means "fetch this one next" → the walk
|
|
35
|
+
* either keeps `fetching` or `pauses` for backpressure (below). Decoupling the
|
|
36
|
+
* extraction from the op is the same discipline `circuit-breaker` uses for the
|
|
37
|
+
* clock: the impure / domain-specific bit (`nextCursor(page)`) lives at the
|
|
38
|
+
* call site; the op stays a pure fold over the extracted value.
|
|
39
|
+
*
|
|
40
|
+
* ## The phase machine
|
|
41
|
+
*
|
|
42
|
+
* Four phases, each a discriminated-union arm carrying ONLY the data that phase
|
|
43
|
+
* needs so impossible states are unrepresentable (pattern 11):
|
|
44
|
+
*
|
|
45
|
+
* - **idle** — created, not started. No cursor is armed yet; `start` arms the
|
|
46
|
+
* first one. Carries the running `seen` / `pages` counters (both 0 here, but
|
|
47
|
+
* present on every phase so a consumer reads them uniformly).
|
|
48
|
+
*
|
|
49
|
+
* - **fetching** — a page request for `cursor` is outstanding. The walk sits
|
|
50
|
+
* here while the consumer's fetch is in flight; `recordPage` resolves it.
|
|
51
|
+
* Carries the `cursor` being fetched so a post-eviction resume re-issues the
|
|
52
|
+
* SAME request (idempotent retry) rather than skipping a page.
|
|
53
|
+
*
|
|
54
|
+
* - **paused** — backpressure tripped. The API still has more pages (a non-null
|
|
55
|
+
* next cursor), but `seen` has reached `highWaterMark` and the consumer has
|
|
56
|
+
* not drained yet. Carries the `cursor` to resume from. No fetch is
|
|
57
|
+
* outstanding — the loop is deliberately idle until `resume`. This is the
|
|
58
|
+
* valve that stops a fast producer (the API) from outrunning a slow consumer
|
|
59
|
+
* (whatever processes each page) and blowing memory.
|
|
60
|
+
*
|
|
61
|
+
* - **done** — the API returned a null next cursor; the walk is exhausted. A
|
|
62
|
+
* terminal phase: `start` / `recordPage` / `resume` are all no-ops on it
|
|
63
|
+
* (idempotent completion — a late duplicate result can't un-finish a walk).
|
|
64
|
+
*
|
|
65
|
+
* ### Transition table
|
|
66
|
+
*
|
|
67
|
+
* Rows are the current phase; columns are the op. `→` denotes the next phase;
|
|
68
|
+
* `self` means unchanged (a no-op for that (phase × op) pair). `next` is the
|
|
69
|
+
* cursor the consumer extracted from the page; `hwm` is `highWaterMark`.
|
|
70
|
+
*
|
|
71
|
+
* | from \ op | start | recordPage(count, next) | resume |
|
|
72
|
+
* | ----------- | -------------- | -------------------------------------------------------------------- | ------------------------------ |
|
|
73
|
+
* | idle | → fetching(c0) | self *(¹)* | self |
|
|
74
|
+
* | fetching | self | seen=max(0,seen+count), pages+=1; next==null → done; seen≥hwm → paused(next); else → fetching(next) | self |
|
|
75
|
+
* | paused | self | self *(¹)* | seen<hwm → fetching(cursor) *(²)* |
|
|
76
|
+
* | done | self | self | self |
|
|
77
|
+
*
|
|
78
|
+
* *(¹)* `recordPage` only makes sense while `fetching` (a page was outstanding).
|
|
79
|
+
* On any other phase it is a no-op: a result that arrives when no fetch was in
|
|
80
|
+
* flight (a late duplicate, a double-dispatch) must not advance the cursor or
|
|
81
|
+
* double-count `seen`. The walk is a coordination aid, not a correctness gate —
|
|
82
|
+
* it absorbs the stray rather than corrupting the count.
|
|
83
|
+
*
|
|
84
|
+
* *(²)* `resume` re-arms the paused cursor ONLY once `seen` has dropped below
|
|
85
|
+
* `highWaterMark` (the consumer drained, lowering `seen` via `drain`). Calling
|
|
86
|
+
* `resume` while still over the mark is a no-op — the valve stays shut until
|
|
87
|
+
* there is real headroom, so a consumer that resumes optimistically can't
|
|
88
|
+
* defeat the backpressure. Resuming a walk that isn't `paused` is likewise a
|
|
89
|
+
* no-op.
|
|
90
|
+
*
|
|
91
|
+
* ## Backpressure
|
|
92
|
+
*
|
|
93
|
+
* `seen` is the count of items the API has produced that the consumer has not
|
|
94
|
+
* yet acknowledged as processed. `recordPage` adds each page's item count to it;
|
|
95
|
+
* `drain(n)` subtracts what the consumer finished. When `seen` reaches
|
|
96
|
+
* `highWaterMark`, `recordPage` parks the walk in `paused` instead of issuing
|
|
97
|
+
* the next fetch — the producer stops until `drain` + `resume` re-open the
|
|
98
|
+
* valve. A `highWaterMark` of `0` (or any non-positive value) disables
|
|
99
|
+
* backpressure: the comparison `seen >= hwm` is satisfied vacuously only when
|
|
100
|
+
* `hwm <= 0` would pause immediately, so we treat `hwm <= 0` as "unbounded" and
|
|
101
|
+
* never pause. The walk then runs flat-out, `fetching → fetching` until `done`.
|
|
102
|
+
*
|
|
103
|
+
* NOT exported from the package root — reached via the `@demlik/tea/paginator`
|
|
104
|
+
* subpath, same one-shape-per-package rule as `work-queue`, `retry-backoff`,
|
|
105
|
+
* `circuit-breaker`, and `rate-limit`. It is the L1 escape hatch the
|
|
106
|
+
* `paginated-walk` composition builds its walk loop on.
|
|
107
|
+
*/
|
|
108
|
+
/**
|
|
109
|
+
* Paginator policy — pure configuration, no mutable state. One policy is shared
|
|
110
|
+
* across the whole walk (often a module-scope const). `PaginatorState` carries
|
|
111
|
+
* the cursor + counters; this carries the knobs they're compared against.
|
|
112
|
+
*
|
|
113
|
+
* Generic in `Cursor` so `firstCursor` matches whatever the API's cursor shape
|
|
114
|
+
* is — an offset `number`, a page-token `string`, a `{ after: string }` record,
|
|
115
|
+
* etc. The module never inspects the cursor; the type just threads through.
|
|
116
|
+
*/
|
|
117
|
+
interface PaginatorPolicy<Cursor> {
|
|
118
|
+
/**
|
|
119
|
+
* The cursor the walk fetches first, armed by `start`. For an offset API this
|
|
120
|
+
* is typically `0`; for a token API, `null`-or-empty-meaning-"first page" is
|
|
121
|
+
* the API's call, so pass whatever sentinel the API treats as page one.
|
|
122
|
+
*/
|
|
123
|
+
readonly firstCursor: Cursor;
|
|
124
|
+
/**
|
|
125
|
+
* Backpressure threshold: the number of un-drained items at which the walk
|
|
126
|
+
* pauses instead of fetching the next page. `recordPage` parks in `paused`
|
|
127
|
+
* once `seen >= highWaterMark`; `drain` + `resume` re-open the valve.
|
|
128
|
+
*
|
|
129
|
+
* A non-positive value (`<= 0`) disables backpressure — the walk never pauses
|
|
130
|
+
* and runs `fetching → fetching` until the API exhausts. Use that when the
|
|
131
|
+
* consumer processes each page synchronously inside `recordPage` and there is
|
|
132
|
+
* nothing to back up.
|
|
133
|
+
*/
|
|
134
|
+
readonly highWaterMark: number;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Sensible defaults: start from a numeric offset of `0`, pause after 1000
|
|
138
|
+
* un-drained items. Tuned for an offset-paginated REST API whose consumer
|
|
139
|
+
* processes pages asynchronously; override per call site (especially
|
|
140
|
+
* `firstCursor`, which is API-specific) rather than mutating this shared const.
|
|
141
|
+
*
|
|
142
|
+
* Typed as `PaginatorPolicy<number>` because the only honest default cursor is
|
|
143
|
+
* the numeric offset `0`; a token-based API supplies its own `firstCursor` and
|
|
144
|
+
* its own `PaginatorPolicy<string>`.
|
|
145
|
+
*/
|
|
146
|
+
declare const defaultPaginatorPolicy: PaginatorPolicy<number>;
|
|
147
|
+
/**
|
|
148
|
+
* The walk's phase. A discriminated union on `phase` so each phase carries ONLY
|
|
149
|
+
* its own data and impossible combinations are unrepresentable (pattern 11):
|
|
150
|
+
* there is no `done` state that still holds a pending cursor, no `idle` state
|
|
151
|
+
* mid-fetch. Every field is `readonly` so the whole value is trivially
|
|
152
|
+
* JSON-serializable into a consumer's `Store<S>` and safe to thread through a
|
|
153
|
+
* pure reducer / a Durable-Object reload.
|
|
154
|
+
*
|
|
155
|
+
* Two counters ride on every phase so a consumer reads them uniformly without
|
|
156
|
+
* narrowing first:
|
|
157
|
+
* - `seen` — items produced by the API but not yet drained (backpressure gauge).
|
|
158
|
+
* - `pages` — total pages recorded so far (progress / observability).
|
|
159
|
+
*
|
|
160
|
+
* - `idle` — not started; `start` arms `firstCursor`.
|
|
161
|
+
* - `fetching` — a page request for `cursor` is outstanding.
|
|
162
|
+
* - `paused` — backpressure tripped; `cursor` is the page to resume from.
|
|
163
|
+
* - `done` — API exhausted (null next cursor); terminal.
|
|
164
|
+
*/
|
|
165
|
+
type PaginatorState<Cursor> = {
|
|
166
|
+
readonly phase: "idle";
|
|
167
|
+
readonly seen: number;
|
|
168
|
+
readonly pages: number;
|
|
169
|
+
} | {
|
|
170
|
+
readonly phase: "fetching";
|
|
171
|
+
readonly cursor: Cursor;
|
|
172
|
+
readonly seen: number;
|
|
173
|
+
readonly pages: number;
|
|
174
|
+
} | {
|
|
175
|
+
readonly phase: "paused";
|
|
176
|
+
readonly cursor: Cursor;
|
|
177
|
+
readonly seen: number;
|
|
178
|
+
readonly pages: number;
|
|
179
|
+
} | {
|
|
180
|
+
readonly phase: "done";
|
|
181
|
+
readonly seen: number;
|
|
182
|
+
readonly pages: number;
|
|
183
|
+
};
|
|
184
|
+
/** The starting state: idle, nothing seen, no pages recorded. */
|
|
185
|
+
declare function initPaginator<Cursor>(): PaginatorState<Cursor>;
|
|
186
|
+
/**
|
|
187
|
+
* Arm the first fetch. PURE — returns a new state; the input is never mutated.
|
|
188
|
+
*
|
|
189
|
+
* - `idle` → `fetching(firstCursor)`. This is the only phase `start` acts on.
|
|
190
|
+
* - any other phase → unchanged. Re-arming a walk that has already begun (or
|
|
191
|
+
* finished) would skip back to page one and double-count; a stray `start`
|
|
192
|
+
* after launch is absorbed, not honored. Use `initPaginator()` to begin a
|
|
193
|
+
* genuinely fresh walk.
|
|
194
|
+
*/
|
|
195
|
+
declare function start<Cursor>(state: PaginatorState<Cursor>, policy: PaginatorPolicy<Cursor>): PaginatorState<Cursor>;
|
|
196
|
+
/**
|
|
197
|
+
* Record the result of the outstanding fetch and decide what comes next. PURE —
|
|
198
|
+
* returns a new state; the input is never mutated.
|
|
199
|
+
*
|
|
200
|
+
* `count` is how many items the just-fetched page yielded (added to `seen` for
|
|
201
|
+
* backpressure accounting). `next` is the cursor the consumer extracted from the
|
|
202
|
+
* page — a `Cursor` to fetch next, or `null`/`undefined` (a missing optional
|
|
203
|
+
* cursor field) if the API signalled the end.
|
|
204
|
+
*
|
|
205
|
+
* - `fetching` → `seen = max(0, seen + count)`, `pages += 1`, then branch on
|
|
206
|
+
* `next`:
|
|
207
|
+
* - `next == null` → `done` (API exhausted; loose-null so a
|
|
208
|
+
* missing/`undefined` cursor also finishes).
|
|
209
|
+
* - `seen >= highWaterMark` → `paused(next)` (backpressure; `hwm <= 0`
|
|
210
|
+
* disables this branch — see policy docs).
|
|
211
|
+
* - otherwise → `fetching(next)` (keep walking).
|
|
212
|
+
* - any other phase → unchanged. A result with no fetch outstanding (a late
|
|
213
|
+
* duplicate, a double-dispatch) must not advance the cursor or double-count;
|
|
214
|
+
* it is absorbed. See the module header's note ¹.
|
|
215
|
+
*
|
|
216
|
+
* `count` is floored into the gauge: a non-finite `count` (`NaN`/`Infinity`) is
|
|
217
|
+
* treated as `0`, and the additive sum is clamped at `0`. A record can only hold
|
|
218
|
+
* or raise `seen`; only `drain` lowers it. This keeps a stray negative or
|
|
219
|
+
* non-finite count from poisoning the `seen >= highWaterMark` comparison and
|
|
220
|
+
* silently defeating backpressure.
|
|
221
|
+
*
|
|
222
|
+
* The new `seen` total — not the pre-page value — is what the backpressure check
|
|
223
|
+
* reads, so a single page large enough to cross the mark pauses immediately
|
|
224
|
+
* rather than fetching one more first.
|
|
225
|
+
*/
|
|
226
|
+
declare function recordPage<Cursor>(state: PaginatorState<Cursor>, count: number, next: Cursor | null | undefined, policy: PaginatorPolicy<Cursor>): PaginatorState<Cursor>;
|
|
227
|
+
/**
|
|
228
|
+
* Acknowledge that the consumer finished processing `n` items, lowering the
|
|
229
|
+
* backpressure gauge. PURE — returns a new state; the input is never mutated.
|
|
230
|
+
*
|
|
231
|
+
* Subtracts `n` from `seen`, floored at 0 (a consumer that over-acks, or acks a
|
|
232
|
+
* page recorded before a reload, can never drive `seen` negative). `n <= 0` is a
|
|
233
|
+
* no-op on the count. `drain` does NOT change the phase — it only frees
|
|
234
|
+
* headroom; `resume` is the separate, explicit decision to re-issue a fetch once
|
|
235
|
+
* the headroom exists. Splitting "I drained" from "I want to continue" lets a
|
|
236
|
+
* consumer drain in several steps and resume once.
|
|
237
|
+
*
|
|
238
|
+
* Works on every phase: a consumer may drain while still `fetching` (the gauge
|
|
239
|
+
* just drops, no pause was reached) or while `paused` (freeing room for a later
|
|
240
|
+
* `resume`). On `done` the gauge can still be drained for accurate final
|
|
241
|
+
* accounting, though it no longer gates anything.
|
|
242
|
+
*/
|
|
243
|
+
declare function drain<Cursor>(state: PaginatorState<Cursor>, n: number): PaginatorState<Cursor>;
|
|
244
|
+
/**
|
|
245
|
+
* Re-open the backpressure valve after a `drain`. PURE — returns a new state;
|
|
246
|
+
* the input is never mutated.
|
|
247
|
+
*
|
|
248
|
+
* - `paused` AND `seen < highWaterMark` → `fetching(cursor)`: the consumer has
|
|
249
|
+
* drained below the mark, so re-issue the parked fetch.
|
|
250
|
+
* - `paused` but `seen >= highWaterMark` → unchanged. There is still no
|
|
251
|
+
* headroom; the valve stays shut until a `drain` lowers `seen`. A consumer
|
|
252
|
+
* that resumes optimistically can't defeat the backpressure. (When `hwm <=
|
|
253
|
+
* 0` backpressure is disabled, so the walk never reaches `paused` and this
|
|
254
|
+
* branch is unreachable — but the guard reads `< hwm` consistently, and a
|
|
255
|
+
* non-positive `hwm` makes `seen < hwm` false for any non-negative `seen`,
|
|
256
|
+
* correctly refusing to resume a state that should never have paused.)
|
|
257
|
+
* - any other phase → unchanged. Resuming a walk that isn't paused (idle,
|
|
258
|
+
* fetching, done) is a no-op.
|
|
259
|
+
*
|
|
260
|
+
* See the module header's note ².
|
|
261
|
+
*/
|
|
262
|
+
declare function resume<Cursor>(state: PaginatorState<Cursor>, policy: PaginatorPolicy<Cursor>): PaginatorState<Cursor>;
|
|
263
|
+
/**
|
|
264
|
+
* Whether the walk has finished (the API returned a null next cursor). PURE,
|
|
265
|
+
* read-only. A derived predicate so a consumer's reducer can branch
|
|
266
|
+
* (`if (isDone(walk)) return [doneState, [joinCmd]]`) without re-checking the
|
|
267
|
+
* `phase` discriminant by hand at every call site.
|
|
268
|
+
*/
|
|
269
|
+
declare function isDone<Cursor>(state: PaginatorState<Cursor>): boolean;
|
|
270
|
+
|
|
271
|
+
export { type PaginatorPolicy, type PaginatorState, defaultPaginatorPolicy, drain, initPaginator, isDone, recordPage, resume, start };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { BootingRuntime } from '../index.js';
|
|
2
|
+
import { Trace, RecorderOptions } from '../recorder/index.js';
|
|
3
|
+
import { C as Cmd, S as Sub, M as Machine } from '../core-CymXZCeY.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A live parity recording attached to a Runtime — the go/no-go gate's golden
|
|
7
|
+
* artifact. Wraps a {@link recorder}: drive the runtime, then snapshot with
|
|
8
|
+
* {@link Recording.trace} (or hand the live `Recording` straight to
|
|
9
|
+
* {@link goldenReplay}, which snapshots for you).
|
|
10
|
+
*
|
|
11
|
+
* @typeParam S - the machine's State type.
|
|
12
|
+
* @typeParam M - the machine's Msg union.
|
|
13
|
+
*/
|
|
14
|
+
interface Recording<S, M> {
|
|
15
|
+
/**
|
|
16
|
+
* Snapshot the recording so far as a replayable {@link Trace} — `loaded`
|
|
17
|
+
* (boot state), the ordered non-null `msgs`, and `finalState`. Sufficient to
|
|
18
|
+
* reproduce the run via {@link goldenReplay}. Fresh object each call.
|
|
19
|
+
*/
|
|
20
|
+
trace(): Trace<S, M>;
|
|
21
|
+
/** Serialize the recording as JSONL (see {@link recorder}'s `toJSONL`). */
|
|
22
|
+
toJSONL(): string;
|
|
23
|
+
/** Detach the underlying observer. Idempotent. */
|
|
24
|
+
stop(): void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Attach a parity recording to a Runtime, reusing the `recorder`/`historyTracker`
|
|
28
|
+
* capture (the boot observe for `loaded` + every non-null msg in order) rather
|
|
29
|
+
* than a fresh observer path.
|
|
30
|
+
*
|
|
31
|
+
* Attach on the synchronous `run()` handle so the boot transition is captured
|
|
32
|
+
* as `loaded`; drive the runtime; then `trace()` (or pass the handle to
|
|
33
|
+
* {@link goldenReplay}). See {@link RecorderOptions} for `captureSteps` (needed
|
|
34
|
+
* only for the per-step / resume-cache-prefix inspection, not for replay).
|
|
35
|
+
*/
|
|
36
|
+
declare function recordRun<S, M extends {
|
|
37
|
+
type: string;
|
|
38
|
+
}>(runtime: BootingRuntime<S, M>, opts?: RecorderOptions): Recording<S, M>;
|
|
39
|
+
/**
|
|
40
|
+
* Reproduce a recording's final state by folding its msgs through the pure
|
|
41
|
+
* `replay` — `init(loaded, ctx)` then `update` per msg, never `interpret`,
|
|
42
|
+
* never a `Store`, never a live subscription. Accepts a live {@link Recording}
|
|
43
|
+
* (snapshotted here) or a bare {@link Trace}.
|
|
44
|
+
*
|
|
45
|
+
* `ctx` is the ENVIRONMENT supplied fresh at replay time (the trace is the
|
|
46
|
+
* INPUT: `loaded` + `msgs`), mirroring `replayTrace`. It is optional so a
|
|
47
|
+
* void-ctx machine can call `goldenReplay(machine, rec)`; a machine that reads
|
|
48
|
+
* ctx passes it explicitly.
|
|
49
|
+
*/
|
|
50
|
+
declare function goldenReplay<S, M extends {
|
|
51
|
+
type: string;
|
|
52
|
+
}, C extends Cmd, U extends Sub, Ctx>(machine: Machine<S, M, C, U, Ctx>, recording: Recording<S, M> | Trace<S, M>, ctx?: Ctx): S;
|
|
53
|
+
/** Configuration for {@link normalizeForParity}. */
|
|
54
|
+
interface ParitySchema {
|
|
55
|
+
/**
|
|
56
|
+
* Object keys stripped wherever they appear — the record-time
|
|
57
|
+
* non-determinism `replay` never re-mints. Default: `id`, `runId`,
|
|
58
|
+
* `timestamp`.
|
|
59
|
+
*/
|
|
60
|
+
readonly stripKeys?: readonly string[];
|
|
61
|
+
/**
|
|
62
|
+
* Candidate keys used to stable-sort arrays of objects. For each array the
|
|
63
|
+
* first candidate present (as a primitive) on the items decides the order;
|
|
64
|
+
* arrays with no candidate fall back to canonical-JSON order. Default:
|
|
65
|
+
* `ruleId`, `selector`, `url`.
|
|
66
|
+
*/
|
|
67
|
+
readonly sortKeys?: readonly string[];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Build a finding-normalizer: a pure `(value) => normalized` that strips the
|
|
71
|
+
* schema's `stripKeys`, recursively sorts object keys, and stable-key-sorts
|
|
72
|
+
* arrays by the schema's `sortKeys`. Two runs that differ only in generated
|
|
73
|
+
* ids/timestamps or intra-effect ordering normalize to a byte-identical value,
|
|
74
|
+
* so {@link diff} on the normalized pair reflects behavioural (not incidental)
|
|
75
|
+
* difference.
|
|
76
|
+
*/
|
|
77
|
+
declare function normalizeForParity(schema?: ParitySchema): (value: unknown) => unknown;
|
|
78
|
+
/**
|
|
79
|
+
* The parity verdict: `true` iff `a` and `b` are structurally equal. Reuses
|
|
80
|
+
* `trace-replay`'s order-insensitive deep-equal (the single comparator this
|
|
81
|
+
* package owns) rather than adding a second one — feed it the pair produced by
|
|
82
|
+
* {@link normalizeForParity} for the normalized-JSON equality the gate rests on.
|
|
83
|
+
*/
|
|
84
|
+
declare function diff(a: unknown, b: unknown): boolean;
|
|
85
|
+
|
|
86
|
+
export { type ParitySchema, type Recording, diff, goldenReplay, normalizeForParity, recordRun };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { recorder } from '../chunk-UYWUVEP6.js';
|
|
2
|
+
import { deepEqual } from '../chunk-EDGDR5XX.js';
|
|
3
|
+
import { replay } from '../chunk-2WZHS4P2.js';
|
|
4
|
+
import '../chunk-BNJYE4AU.js';
|
|
5
|
+
|
|
6
|
+
// src/parity/index.ts
|
|
7
|
+
function recordRun(runtime, opts) {
|
|
8
|
+
const rec = recorder(runtime, opts);
|
|
9
|
+
return {
|
|
10
|
+
trace: () => rec.dump(),
|
|
11
|
+
toJSONL: () => rec.toJSONL(),
|
|
12
|
+
stop: () => rec.stop()
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function goldenReplay(machine, recording, ctx) {
|
|
16
|
+
const trace = asTrace(recording);
|
|
17
|
+
const { state } = replay(machine, {
|
|
18
|
+
msgs: trace.msgs,
|
|
19
|
+
ctx,
|
|
20
|
+
loaded: trace.loaded
|
|
21
|
+
});
|
|
22
|
+
return state;
|
|
23
|
+
}
|
|
24
|
+
function asTrace(recording) {
|
|
25
|
+
return typeof recording.trace === "function" ? recording.trace() : recording;
|
|
26
|
+
}
|
|
27
|
+
var DEFAULT_STRIP_KEYS = ["id", "runId", "timestamp"];
|
|
28
|
+
var DEFAULT_SORT_KEYS = ["ruleId", "selector", "url"];
|
|
29
|
+
function normalizeForParity(schema) {
|
|
30
|
+
const stripKeys = new Set(schema?.stripKeys ?? DEFAULT_STRIP_KEYS);
|
|
31
|
+
const sortKeys = schema?.sortKeys ?? DEFAULT_SORT_KEYS;
|
|
32
|
+
const normalize = (value) => {
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
const items = value.map(normalize);
|
|
35
|
+
return items.map((item, index) => ({
|
|
36
|
+
item,
|
|
37
|
+
index,
|
|
38
|
+
primary: primarySortKey(item, sortKeys),
|
|
39
|
+
json: JSON.stringify(item) ?? ""
|
|
40
|
+
})).sort(
|
|
41
|
+
(a, b) => compareStrings(a.primary, b.primary) || compareStrings(a.json, b.json) || a.index - b.index
|
|
42
|
+
).map((decorated) => decorated.item);
|
|
43
|
+
}
|
|
44
|
+
if (isPlainObject(value)) {
|
|
45
|
+
const out = {};
|
|
46
|
+
for (const key of Object.keys(value).filter((key2) => !stripKeys.has(key2)).sort()) {
|
|
47
|
+
out[key] = normalize(value[key]);
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
};
|
|
53
|
+
return normalize;
|
|
54
|
+
}
|
|
55
|
+
function primarySortKey(item, sortKeys) {
|
|
56
|
+
if (!isPlainObject(item)) return "";
|
|
57
|
+
for (const key of sortKeys) {
|
|
58
|
+
const candidate = item[key];
|
|
59
|
+
if (typeof candidate === "string" || typeof candidate === "number" || typeof candidate === "boolean") {
|
|
60
|
+
return String(candidate);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
65
|
+
function compareStrings(a, b) {
|
|
66
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
67
|
+
}
|
|
68
|
+
function isPlainObject(value) {
|
|
69
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
70
|
+
}
|
|
71
|
+
function diff(a, b) {
|
|
72
|
+
return deepEqual(a, b);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export { diff, goldenReplay, normalizeForParity, recordRun };
|
|
76
|
+
//# sourceMappingURL=index.js.map
|
|
77
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/parity/index.ts"],"names":["key"],"mappings":";;;;;;AA6DO,SAAS,SAAA,CACd,SACA,IAAA,EACiB;AACjB,EAAA,MAAM,GAAA,GAAM,QAAA,CAAS,OAAA,EAAS,IAAI,CAAA;AAClC,EAAA,OAAO;AAAA,IACL,KAAA,EAAO,MAAM,GAAA,CAAI,IAAA,EAAK;AAAA,IACtB,OAAA,EAAS,MAAM,GAAA,CAAI,OAAA,EAAQ;AAAA,IAC3B,IAAA,EAAM,MAAM,GAAA,CAAI,IAAA;AAAK,GACvB;AACF;AAaO,SAAS,YAAA,CAOd,OAAA,EACA,SAAA,EACA,GAAA,EACG;AACH,EAAA,MAAM,KAAA,GAAQ,QAAQ,SAAS,CAAA;AAC/B,EAAA,MAAM,EAAE,KAAA,EAAM,GAAI,MAAA,CAAO,OAAA,EAAS;AAAA,IAChC,MAAM,KAAA,CAAM,IAAA;AAAA,IACZ,GAAA;AAAA,IACA,QAAQ,KAAA,CAAM;AAAA,GACf,CAAA;AACD,EAAA,OAAO,KAAA;AACT;AAGA,SAAS,QAAc,SAAA,EAAuD;AAC5E,EAAA,OAAO,OAAQ,SAAA,CAA8B,KAAA,KAAU,UAAA,GAClD,SAAA,CAA8B,OAAM,GACpC,SAAA;AACP;AAuBA,IAAM,kBAAA,GAAwC,CAAC,IAAA,EAAM,OAAA,EAAS,WAAW,CAAA;AACzE,IAAM,iBAAA,GAAuC,CAAC,QAAA,EAAU,UAAA,EAAY,KAAK,CAAA;AAUlE,SAAS,mBACd,MAAA,EAC6B;AAC7B,EAAA,MAAM,SAAA,GAAY,IAAI,GAAA,CAAI,MAAA,EAAQ,aAAa,kBAAkB,CAAA;AACjE,EAAA,MAAM,QAAA,GAAW,QAAQ,QAAA,IAAY,iBAAA;AAErC,EAAA,MAAM,SAAA,GAAY,CAAC,KAAA,KAA4B;AAC7C,IAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACxB,MAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,GAAA,CAAI,SAAS,CAAA;AAIjC,MAAA,OAAO,KAAA,CACJ,GAAA,CAAI,CAAC,IAAA,EAAM,KAAA,MAAW;AAAA,QACrB,IAAA;AAAA,QACA,KAAA;AAAA,QACA,OAAA,EAAS,cAAA,CAAe,IAAA,EAAM,QAAQ,CAAA;AAAA,QACtC,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,IAAI,CAAA,IAAK;AAAA,QAC9B,CAAA,CACD,IAAA;AAAA,QACC,CAAC,CAAA,EAAG,CAAA,KACF,cAAA,CAAe,CAAA,CAAE,SAAS,CAAA,CAAE,OAAO,CAAA,IACnC,cAAA,CAAe,EAAE,IAAA,EAAM,CAAA,CAAE,IAAI,CAAA,IAC7B,CAAA,CAAE,QAAQ,CAAA,CAAE;AAAA,OAChB,CACC,GAAA,CAAI,CAAC,SAAA,KAAc,UAAU,IAAI,CAAA;AAAA,IACtC;AACA,IAAA,IAAI,aAAA,CAAc,KAAK,CAAA,EAAG;AACxB,MAAA,MAAM,MAA+B,EAAC;AACtC,MAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,KAAK,EAChC,MAAA,CAAO,CAACA,IAAAA,KAAQ,CAAC,UAAU,GAAA,CAAIA,IAAG,CAAC,CAAA,CACnC,MAAK,EAAG;AACT,QAAA,GAAA,CAAI,GAAG,CAAA,GAAI,SAAA,CAAU,KAAA,CAAM,GAAG,CAAC,CAAA;AAAA,MACjC;AACA,MAAA,OAAO,GAAA;AAAA,IACT;AACA,IAAA,OAAO,KAAA;AAAA,EACT,CAAA;AAEA,EAAA,OAAO,SAAA;AACT;AAGA,SAAS,cAAA,CAAe,MAAe,QAAA,EAAqC;AAC1E,EAAA,IAAI,CAAC,aAAA,CAAc,IAAI,CAAA,EAAG,OAAO,EAAA;AACjC,EAAA,KAAA,MAAW,OAAO,QAAA,EAAU;AAC1B,IAAA,MAAM,SAAA,GAAY,KAAK,GAAG,CAAA;AAC1B,IAAA,IACE,OAAO,cAAc,QAAA,IACrB,OAAO,cAAc,QAAA,IACrB,OAAO,cAAc,SAAA,EACrB;AACA,MAAA,OAAO,OAAO,SAAS,CAAA;AAAA,IACzB;AAAA,EACF;AACA,EAAA,OAAO,EAAA;AACT;AAEA,SAAS,cAAA,CAAe,GAAW,CAAA,EAAmB;AACpD,EAAA,OAAO,CAAA,GAAI,CAAA,GAAI,EAAA,GAAK,CAAA,GAAI,IAAI,CAAA,GAAI,CAAA;AAClC;AAEA,SAAS,cAAc,KAAA,EAAkD;AACvE,EAAA,OAAO,OAAO,UAAU,QAAA,IAAY,KAAA,KAAU,QAAQ,CAAC,KAAA,CAAM,QAAQ,KAAK,CAAA;AAC5E;AAQO,SAAS,IAAA,CAAK,GAAY,CAAA,EAAqB;AACpD,EAAA,OAAO,SAAA,CAAU,GAAG,CAAC,CAAA;AACvB","file":"index.js","sourcesContent":["// ---------------------------------------------------------------------------\n// parity — the record → replay → normalized-diff go/no-go gate.\n//\n// The parity spike proved the mechanism already composes from in-tree\n// primitives: `recorder` captures a run's Msg sequence (the recording), `replay`\n// re-folds it purely, and `trace-replay`'s order-insensitive deep-equal diffs\n// the result. Non-determinism is a RECORD-TIME property — ids/timestamps freeze\n// into the recording, `replay` never re-mints them — so the same recording\n// replays byte-identical every time.\n//\n// This module is the thin packaging over that substrate plus the one genuinely\n// new helper, `normalizeForParity`: the finding-normalizer that strips\n// record-time non-determinism (generated ids/timestamps) and stable-key-sorts\n// collections, so an OLD-engine golden run and a NEW-engine run diff as equal\n// exactly when they are behaviourally equivalent. Every domain (audit, auth,\n// ingest, …) gets its old-vs-new parity check without reinventing the capture,\n// the normalization, and the diff.\n//\n// recordRun(runtime) → Recording // wraps the recorder\n// goldenReplay(machine, rec) → State // wraps replay\n// normalizeForParity(schema) → (finding) => norm // strip + stable-sort\n// diff(a, b) → boolean // normalized deep-equal\n// ---------------------------------------------------------------------------\n\nimport type { BootingRuntime, Cmd, Machine, Sub } from \"../index\";\nimport { replay } from \"../index\";\nimport { type RecorderOptions, recorder, type Trace } from \"../recorder\";\nimport { deepEqual } from \"../trace-replay\";\n\n/**\n * A live parity recording attached to a Runtime — the go/no-go gate's golden\n * artifact. Wraps a {@link recorder}: drive the runtime, then snapshot with\n * {@link Recording.trace} (or hand the live `Recording` straight to\n * {@link goldenReplay}, which snapshots for you).\n *\n * @typeParam S - the machine's State type.\n * @typeParam M - the machine's Msg union.\n */\nexport interface Recording<S, M> {\n /**\n * Snapshot the recording so far as a replayable {@link Trace} — `loaded`\n * (boot state), the ordered non-null `msgs`, and `finalState`. Sufficient to\n * reproduce the run via {@link goldenReplay}. Fresh object each call.\n */\n trace(): Trace<S, M>;\n /** Serialize the recording as JSONL (see {@link recorder}'s `toJSONL`). */\n toJSONL(): string;\n /** Detach the underlying observer. Idempotent. */\n stop(): void;\n}\n\n/**\n * Attach a parity recording to a Runtime, reusing the `recorder`/`historyTracker`\n * capture (the boot observe for `loaded` + every non-null msg in order) rather\n * than a fresh observer path.\n *\n * Attach on the synchronous `run()` handle so the boot transition is captured\n * as `loaded`; drive the runtime; then `trace()` (or pass the handle to\n * {@link goldenReplay}). See {@link RecorderOptions} for `captureSteps` (needed\n * only for the per-step / resume-cache-prefix inspection, not for replay).\n */\nexport function recordRun<S, M extends { type: string }>(\n runtime: BootingRuntime<S, M>,\n opts?: RecorderOptions,\n): Recording<S, M> {\n const rec = recorder(runtime, opts);\n return {\n trace: () => rec.dump(),\n toJSONL: () => rec.toJSONL(),\n stop: () => rec.stop(),\n };\n}\n\n/**\n * Reproduce a recording's final state by folding its msgs through the pure\n * `replay` — `init(loaded, ctx)` then `update` per msg, never `interpret`,\n * never a `Store`, never a live subscription. Accepts a live {@link Recording}\n * (snapshotted here) or a bare {@link Trace}.\n *\n * `ctx` is the ENVIRONMENT supplied fresh at replay time (the trace is the\n * INPUT: `loaded` + `msgs`), mirroring `replayTrace`. It is optional so a\n * void-ctx machine can call `goldenReplay(machine, rec)`; a machine that reads\n * ctx passes it explicitly.\n */\nexport function goldenReplay<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n machine: Machine<S, M, C, U, Ctx>,\n recording: Recording<S, M> | Trace<S, M>,\n ctx?: Ctx,\n): S {\n const trace = asTrace(recording);\n const { state } = replay(machine, {\n msgs: trace.msgs,\n ctx: ctx as Ctx,\n loaded: trace.loaded,\n });\n return state;\n}\n\n/** Narrow a `Recording | Trace` to a `Trace`, snapshotting a live recording. */\nfunction asTrace<S, M>(recording: Recording<S, M> | Trace<S, M>): Trace<S, M> {\n return typeof (recording as Recording<S, M>).trace === \"function\"\n ? (recording as Recording<S, M>).trace()\n : (recording as Trace<S, M>);\n}\n\n/** Configuration for {@link normalizeForParity}. */\nexport interface ParitySchema {\n /**\n * Object keys stripped wherever they appear — the record-time\n * non-determinism `replay` never re-mints. Default: `id`, `runId`,\n * `timestamp`.\n */\n readonly stripKeys?: readonly string[];\n /**\n * Candidate keys used to stable-sort arrays of objects. For each array the\n * first candidate present (as a primitive) on the items decides the order;\n * arrays with no candidate fall back to canonical-JSON order. Default:\n * `ruleId`, `selector`, `url`.\n */\n readonly sortKeys?: readonly string[];\n}\n\n// Defaults proven by the spike: strip the generated id/runId/timestamp,\n// stable-key-sort by ruleId | selector | url. Fan-out ordering is already\n// deterministic on this substrate (array order); the sort only defends against\n// intra-effect concurrency (Promise.all / Set).\nconst DEFAULT_STRIP_KEYS: readonly string[] = [\"id\", \"runId\", \"timestamp\"];\nconst DEFAULT_SORT_KEYS: readonly string[] = [\"ruleId\", \"selector\", \"url\"];\n\n/**\n * Build a finding-normalizer: a pure `(value) => normalized` that strips the\n * schema's `stripKeys`, recursively sorts object keys, and stable-key-sorts\n * arrays by the schema's `sortKeys`. Two runs that differ only in generated\n * ids/timestamps or intra-effect ordering normalize to a byte-identical value,\n * so {@link diff} on the normalized pair reflects behavioural (not incidental)\n * difference.\n */\nexport function normalizeForParity(\n schema?: ParitySchema,\n): (value: unknown) => unknown {\n const stripKeys = new Set(schema?.stripKeys ?? DEFAULT_STRIP_KEYS);\n const sortKeys = schema?.sortKeys ?? DEFAULT_SORT_KEYS;\n\n const normalize = (value: unknown): unknown => {\n if (Array.isArray(value)) {\n const items = value.map(normalize);\n // Decorate-sort-undecorate: primary key = first present sortKey value,\n // secondary = canonical JSON (items are already normalized so keys are\n // sorted), original index last for a stable total order.\n return items\n .map((item, index) => ({\n item,\n index,\n primary: primarySortKey(item, sortKeys),\n json: JSON.stringify(item) ?? \"\",\n }))\n .sort(\n (a, b) =>\n compareStrings(a.primary, b.primary) ||\n compareStrings(a.json, b.json) ||\n a.index - b.index,\n )\n .map((decorated) => decorated.item);\n }\n if (isPlainObject(value)) {\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(value)\n .filter((key) => !stripKeys.has(key))\n .sort()) {\n out[key] = normalize(value[key]);\n }\n return out;\n }\n return value;\n };\n\n return normalize;\n}\n\n/** The first present `sortKeys` candidate on `item` as a string, or `\"\"`. */\nfunction primarySortKey(item: unknown, sortKeys: readonly string[]): string {\n if (!isPlainObject(item)) return \"\";\n for (const key of sortKeys) {\n const candidate = item[key];\n if (\n typeof candidate === \"string\" ||\n typeof candidate === \"number\" ||\n typeof candidate === \"boolean\"\n ) {\n return String(candidate);\n }\n }\n return \"\";\n}\n\nfunction compareStrings(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * The parity verdict: `true` iff `a` and `b` are structurally equal. Reuses\n * `trace-replay`'s order-insensitive deep-equal (the single comparator this\n * package owns) rather than adding a second one — feed it the pair produced by\n * {@link normalizeForParity} for the normalized-JSON equality the gate rests on.\n */\nexport function diff(a: unknown, b: unknown): boolean {\n return deepEqual(a, b);\n}\n"]}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { arbConstantMsg, arbGuidedSequence, arbMsg, arbMsgSequence, arbRecordMsg, stubCtxThrowingProxy } from '../../chunk-
|
|
1
|
+
export { arbConstantMsg, arbGuidedSequence, arbMsg, arbMsgSequence, arbRecordMsg, stubCtxThrowingProxy } from '../../chunk-7FYQGDMF.js';
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
package/dist/pbt/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { MsgArbitraryTable, arbConstantMsg, arbGuidedSequence, arbMsg, arbMsgSequence, arbRecordMsg, stubCtxThrowingProxy } from './arbitraries/index.js';
|
|
2
2
|
export { Step, foldEvents, propertyInvariant, propertyTerminates, propertyTrace } from './runners/index.js';
|
|
3
|
-
import { Cmd, Reducer, Transitions } from '../
|
|
3
|
+
import { C as Cmd, R as Reducer, T as Transitions } from '../core-CymXZCeY.js';
|
|
4
4
|
import 'fast-check';
|
|
5
5
|
|
|
6
6
|
/**
|
package/dist/pbt/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
export { arbConstantMsg, arbGuidedSequence, arbMsg, arbMsgSequence, arbRecordMsg, stubCtxThrowingProxy } from '../chunk-
|
|
2
|
-
export { foldEvents, propertyInvariant, propertyTerminates, propertyTrace } from '../chunk-
|
|
3
|
-
import '../chunk-
|
|
1
|
+
export { arbConstantMsg, arbGuidedSequence, arbMsg, arbMsgSequence, arbRecordMsg, stubCtxThrowingProxy } from '../chunk-7FYQGDMF.js';
|
|
2
|
+
export { foldEvents, propertyInvariant, propertyTerminates, propertyTrace } from '../chunk-A56GNPI7.js';
|
|
3
|
+
import '../chunk-2WZHS4P2.js';
|
|
4
|
+
import '../chunk-BNJYE4AU.js';
|
|
4
5
|
|
|
5
6
|
// src/pbt/util/msg-keys.ts
|
|
6
7
|
function msgTypeKeys(update) {
|
|
7
8
|
const keys = Object.keys(update);
|
|
8
9
|
if (keys.length === 0) return [];
|
|
9
|
-
const firstValue = update[keys[0]];
|
|
10
|
+
const firstValue = update[keys[0] ?? ""];
|
|
10
11
|
if (typeof firstValue === "function") {
|
|
11
12
|
return keys;
|
|
12
13
|
}
|
package/dist/pbt/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/pbt/util/msg-keys.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/pbt/util/msg-keys.ts"],"names":[],"mappings":";;;;;;AAiCO,SAAS,YACd,MAAA,EAGmB;AACnB,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,IAAA,CAAK,MAAgB,CAAA;AACzC,EAAA,IAAI,IAAA,CAAK,MAAA,KAAW,CAAA,EAAG,OAAO,EAAC;AAI/B,EAAA,MAAM,UAAA,GAAc,MAAA,CAAmC,IAAA,CAAK,CAAC,KAAK,EAAE,CAAA;AACpE,EAAA,IAAI,OAAO,eAAe,UAAA,EAAY;AACpC,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,OAAO,MAAA,CAAO,KAAK,UAAoB,CAAA;AACzC","file":"index.js","sourcesContent":["// ---------------------------------------------------------------------------\n// msgTypeKeys — read the Msg discriminant set at runtime from a Reducer\n// record or a Transitions table.\n//\n// TS Msg unions only exist at the type level; the discriminant strings are\n// erased at runtime. The reducer record / transitions table is the load-\n// bearing artifact that DOES carry the discriminants at runtime — its keys\n// (for Reducer) or any one cell's inner keys (for Transitions) are exactly\n// the set we need to build an `arbMsg` table without hand-listing them.\n//\n// Used by `arbMsg` to assert (in dev) that the user-supplied table covers\n// every Msg variant the reducer claims to handle. Strict drift detection\n// without runtime introspection of the type system.\n// ---------------------------------------------------------------------------\n\nimport type { Cmd, Reducer, Transitions } from \"../../index\";\n\n/**\n * Extract the Msg discriminant set at runtime from a Reducer record or a\n * Transitions table. Useful for asserting that a user-supplied\n * `MsgArbitraryTable<M>` covers every variant the reducer can handle.\n *\n * - **Reducer form** — keys of the top-level record ARE the Msg variants.\n * - **Transitions form** — keys of any single state's inner record. Every\n * state's inner record must cover every Msg variant (mapped-type\n * discipline), so reading the first state's keys is equivalent to reading\n * the union.\n *\n * @example\n * import { auditBackgroundMachine } from \"./reducer\";\n * msgTypeKeys(auditBackgroundMachine.update);\n * // → [\"start_audit\", \"stop_audit\", \"window_created\", ...]\n */\nexport function msgTypeKeys<S, M extends { type: string }, C extends Cmd>(\n update:\n | Reducer<S, M, C>\n | ([S] extends [{ type: string }] ? Transitions<S, M, C> : never),\n): readonly string[] {\n const keys = Object.keys(update as object);\n if (keys.length === 0) return [];\n // Reducer form: every top-level value is a function (the cell).\n // Transitions form: every top-level value is an object (the inner record).\n // `keys[0]` is present — the length guard above rules out the empty record.\n const firstValue = (update as Record<string, unknown>)[keys[0] ?? \"\"];\n if (typeof firstValue === \"function\") {\n return keys;\n }\n // Transitions form — read inner record.\n return Object.keys(firstValue as object);\n}\n"]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { foldEvents, propertyInvariant, propertyTerminates, propertyTrace } from '../../chunk-
|
|
2
|
-
import '../../chunk-
|
|
1
|
+
export { foldEvents, propertyInvariant, propertyTerminates, propertyTrace } from '../../chunk-A56GNPI7.js';
|
|
2
|
+
import '../../chunk-2WZHS4P2.js';
|
|
3
|
+
import '../../chunk-BNJYE4AU.js';
|
|
3
4
|
//# sourceMappingURL=index.js.map
|
|
4
5
|
//# sourceMappingURL=index.js.map
|