@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.
Files changed (238) hide show
  1. package/LICENSE +21 -0
  2. package/dist/agent/index.d.ts +907 -0
  3. package/dist/agent/index.js +20 -0
  4. package/dist/agent/index.js.map +1 -0
  5. package/dist/authed-call/index.d.ts +119 -0
  6. package/dist/authed-call/index.js +148 -0
  7. package/dist/authed-call/index.js.map +1 -0
  8. package/dist/await-terminal/index.d.ts +99 -0
  9. package/dist/await-terminal/index.js +75 -0
  10. package/dist/await-terminal/index.js.map +1 -0
  11. package/dist/batch-window/index.d.ts +259 -0
  12. package/dist/batch-window/index.js +43 -0
  13. package/dist/batch-window/index.js.map +1 -0
  14. package/dist/cache/index.d.ts +2 -2
  15. package/dist/cache/index.js +2 -50
  16. package/dist/cache/index.js.map +1 -1
  17. package/dist/{chunk-CMG2KMWX.js → chunk-2WZHS4P2.js} +220 -177
  18. package/dist/chunk-2WZHS4P2.js.map +1 -0
  19. package/dist/chunk-2YOE5PIS.js +45 -0
  20. package/dist/chunk-2YOE5PIS.js.map +1 -0
  21. package/dist/chunk-3545BOYL.js +65 -0
  22. package/dist/chunk-3545BOYL.js.map +1 -0
  23. package/dist/chunk-5VNWZADP.js +49 -0
  24. package/dist/chunk-5VNWZADP.js.map +1 -0
  25. package/dist/{chunk-ZRUST6BK.js → chunk-7FYQGDMF.js} +2 -2
  26. package/dist/chunk-7FYQGDMF.js.map +1 -0
  27. package/dist/chunk-7VEG4KHB.js +202 -0
  28. package/dist/chunk-7VEG4KHB.js.map +1 -0
  29. package/dist/{chunk-PYBQREFU.js → chunk-A56GNPI7.js} +20 -6
  30. package/dist/chunk-A56GNPI7.js.map +1 -0
  31. package/dist/chunk-BNJYE4AU.js +157 -0
  32. package/dist/chunk-BNJYE4AU.js.map +1 -0
  33. package/dist/chunk-BYQUGDD3.js +16 -0
  34. package/dist/chunk-BYQUGDD3.js.map +1 -0
  35. package/dist/chunk-D5EQDLDV.js +186 -0
  36. package/dist/chunk-D5EQDLDV.js.map +1 -0
  37. package/dist/chunk-EDGDR5XX.js +60 -0
  38. package/dist/chunk-EDGDR5XX.js.map +1 -0
  39. package/dist/chunk-EIZJTYKR.js +93 -0
  40. package/dist/chunk-EIZJTYKR.js.map +1 -0
  41. package/dist/chunk-EQRUAP2A.js +56 -0
  42. package/dist/chunk-EQRUAP2A.js.map +1 -0
  43. package/dist/chunk-FH5RUIIL.js +61 -0
  44. package/dist/chunk-FH5RUIIL.js.map +1 -0
  45. package/dist/chunk-HJO6VNQI.js +15 -0
  46. package/dist/chunk-HJO6VNQI.js.map +1 -0
  47. package/dist/chunk-IFBQJYGI.js +132 -0
  48. package/dist/chunk-IFBQJYGI.js.map +1 -0
  49. package/dist/chunk-IN33Q4VJ.js +52 -0
  50. package/dist/chunk-IN33Q4VJ.js.map +1 -0
  51. package/dist/chunk-JELSZN25.js +47 -0
  52. package/dist/chunk-JELSZN25.js.map +1 -0
  53. package/dist/chunk-K575PT4S.js +58 -0
  54. package/dist/chunk-K575PT4S.js.map +1 -0
  55. package/dist/chunk-LEC4U6CX.js +91 -0
  56. package/dist/chunk-LEC4U6CX.js.map +1 -0
  57. package/dist/{chunk-Y5JM3UUH.js → chunk-NSDFA6M6.js} +75 -3
  58. package/dist/chunk-NSDFA6M6.js.map +1 -0
  59. package/dist/chunk-QGZW4W3E.js +20 -0
  60. package/dist/chunk-QGZW4W3E.js.map +1 -0
  61. package/dist/{chunk-PYOCUBQJ.js → chunk-RGTPPF4U.js} +22 -15
  62. package/dist/chunk-RGTPPF4U.js.map +1 -0
  63. package/dist/chunk-RVJFGRGK.js +222 -0
  64. package/dist/chunk-RVJFGRGK.js.map +1 -0
  65. package/dist/chunk-RVU3RBE6.js +31 -0
  66. package/dist/chunk-RVU3RBE6.js.map +1 -0
  67. package/dist/chunk-SC3FAFZN.js +53 -0
  68. package/dist/chunk-SC3FAFZN.js.map +1 -0
  69. package/dist/chunk-SINXE47Y.js +55 -0
  70. package/dist/chunk-SINXE47Y.js.map +1 -0
  71. package/dist/chunk-TAO4YOLO.js +87 -0
  72. package/dist/chunk-TAO4YOLO.js.map +1 -0
  73. package/dist/chunk-TC4HWABA.js +506 -0
  74. package/dist/chunk-TC4HWABA.js.map +1 -0
  75. package/dist/chunk-TGG26XMK.js +52 -0
  76. package/dist/chunk-TGG26XMK.js.map +1 -0
  77. package/dist/chunk-UV6UWGEO.js +147 -0
  78. package/dist/chunk-UV6UWGEO.js.map +1 -0
  79. package/dist/chunk-UYWUVEP6.js +158 -0
  80. package/dist/chunk-UYWUVEP6.js.map +1 -0
  81. package/dist/chunk-WUV3PSCZ.js +54 -0
  82. package/dist/chunk-WUV3PSCZ.js.map +1 -0
  83. package/dist/chunk-XRXKH7KA.js +268 -0
  84. package/dist/chunk-XRXKH7KA.js.map +1 -0
  85. package/dist/circuit-breaker/index.js +1 -51
  86. package/dist/circuit-breaker/index.js.map +1 -1
  87. package/dist/core-CymXZCeY.d.ts +178 -0
  88. package/dist/deadline/index.d.ts +24 -5
  89. package/dist/deadline/index.js +4 -18
  90. package/dist/deadline/index.js.map +1 -1
  91. package/dist/debounce/index.js +1 -54
  92. package/dist/debounce/index.js.map +1 -1
  93. package/dist/devtools/index.d.ts +1 -1
  94. package/dist/devtools/index.js +18 -6
  95. package/dist/devtools/index.js.map +1 -1
  96. package/dist/do/index.d.ts +1732 -92
  97. package/dist/do/index.js +677 -31
  98. package/dist/do/index.js.map +1 -1
  99. package/dist/durable-effects-2MDxDs45.d.ts +180 -0
  100. package/dist/extension/index.d.ts +31 -13
  101. package/dist/extension/index.js +5 -3
  102. package/dist/extension/index.js.map +1 -1
  103. package/dist/extension/react.js +8 -3
  104. package/dist/extension/react.js.map +1 -1
  105. package/dist/extension/subs/index.d.ts +2 -2
  106. package/dist/extension/subs/index.js.map +1 -1
  107. package/dist/extension/test-utils.js +11 -3
  108. package/dist/extension/test-utils.js.map +1 -1
  109. package/dist/fan-out/index.d.ts +239 -0
  110. package/dist/fan-out/index.js +5 -0
  111. package/dist/fan-out/index.js.map +1 -0
  112. package/dist/idempotency/adapter.d.ts +62 -0
  113. package/dist/idempotency/adapter.js +4 -0
  114. package/dist/idempotency/adapter.js.map +1 -0
  115. package/dist/idempotency/index.js +1 -56
  116. package/dist/idempotency/index.js.map +1 -1
  117. package/dist/idempotent-intake/index.d.ts +186 -0
  118. package/dist/idempotent-intake/index.js +7 -0
  119. package/dist/idempotent-intake/index.js.map +1 -0
  120. package/dist/index.d.ts +341 -191
  121. package/dist/index.js +2 -1
  122. package/dist/llm-call/index.d.ts +309 -0
  123. package/dist/llm-call/index.js +14 -0
  124. package/dist/llm-call/index.js.map +1 -0
  125. package/dist/machine-viz/index.d.ts +1 -1
  126. package/dist/machine-viz/index.js +1 -1
  127. package/dist/machine-viz/index.js.map +1 -1
  128. package/dist/mem/index.d.ts +1 -0
  129. package/dist/monitored-run/index.d.ts +253 -0
  130. package/dist/monitored-run/index.js +10 -0
  131. package/dist/monitored-run/index.js.map +1 -0
  132. package/dist/node/index.d.ts +5 -5
  133. package/dist/node/index.js.map +1 -1
  134. package/dist/paginated-walk/index.d.ts +234 -0
  135. package/dist/paginated-walk/index.js +15 -0
  136. package/dist/paginated-walk/index.js.map +1 -0
  137. package/dist/paginator/index.d.ts +271 -0
  138. package/dist/paginator/index.js +3 -0
  139. package/dist/paginator/index.js.map +1 -0
  140. package/dist/parity/index.d.ts +86 -0
  141. package/dist/parity/index.js +77 -0
  142. package/dist/parity/index.js.map +1 -0
  143. package/dist/pbt/arbitraries/index.d.ts +1 -1
  144. package/dist/pbt/arbitraries/index.js +1 -1
  145. package/dist/pbt/index.d.ts +1 -1
  146. package/dist/pbt/index.js +5 -4
  147. package/dist/pbt/index.js.map +1 -1
  148. package/dist/pbt/runners/index.d.ts +1 -1
  149. package/dist/pbt/runners/index.js +3 -2
  150. package/dist/poller/index.d.ts +288 -0
  151. package/dist/poller/index.js +79 -0
  152. package/dist/poller/index.js.map +1 -0
  153. package/dist/prediction/index.d.ts +109 -0
  154. package/dist/prediction/index.js +4 -0
  155. package/dist/prediction/index.js.map +1 -0
  156. package/dist/protocol-B5YmJr0d.d.ts +45 -0
  157. package/dist/pure/index.d.ts +2 -0
  158. package/dist/pure/index.js +4 -0
  159. package/dist/pure/index.js.map +1 -0
  160. package/dist/raft/demo.d.ts +105 -0
  161. package/dist/raft/demo.js +172 -0
  162. package/dist/raft/demo.js.map +1 -0
  163. package/dist/raft/index.d.ts +548 -0
  164. package/dist/raft/index.js +7 -0
  165. package/dist/raft/index.js.map +1 -0
  166. package/dist/raft/sim.d.ts +222 -0
  167. package/dist/raft/sim.js +8 -0
  168. package/dist/raft/sim.js.map +1 -0
  169. package/dist/raft/timeline.d.ts +46 -0
  170. package/dist/raft/timeline.js +62 -0
  171. package/dist/raft/timeline.js.map +1 -0
  172. package/dist/rate-limit/index.js +1 -43
  173. package/dist/rate-limit/index.js.map +1 -1
  174. package/dist/react/index.d.ts +2 -1
  175. package/dist/react/index.js +39 -20
  176. package/dist/react/index.js.map +1 -1
  177. package/dist/reconciler/index.d.ts +192 -0
  178. package/dist/reconciler/index.js +166 -0
  179. package/dist/reconciler/index.js.map +1 -0
  180. package/dist/recorder/index.d.ts +7 -3
  181. package/dist/recorder/index.js +1 -155
  182. package/dist/recorder/index.js.map +1 -1
  183. package/dist/resilient-call/index.d.ts +251 -0
  184. package/dist/resilient-call/index.js +13 -0
  185. package/dist/resilient-call/index.js.map +1 -0
  186. package/dist/retry-backoff/index.d.ts +42 -5
  187. package/dist/retry-backoff/index.js +1 -35
  188. package/dist/retry-backoff/index.js.map +1 -1
  189. package/dist/saga/index.d.ts +225 -0
  190. package/dist/saga/index.js +150 -0
  191. package/dist/saga/index.js.map +1 -0
  192. package/dist/snapshot/index.d.ts +334 -0
  193. package/dist/snapshot/index.js +5 -0
  194. package/dist/snapshot/index.js.map +1 -0
  195. package/dist/subs/index.d.ts +48 -10
  196. package/dist/subs/index.js +1 -1
  197. package/dist/testing/index.d.ts +107 -2
  198. package/dist/testing/index.js +204 -6
  199. package/dist/testing/index.js.map +1 -1
  200. package/dist/throttle/index.js +1 -52
  201. package/dist/throttle/index.js.map +1 -1
  202. package/dist/throttled-input/index.d.ts +372 -0
  203. package/dist/throttled-input/index.js +88 -0
  204. package/dist/throttled-input/index.js.map +1 -0
  205. package/dist/token-refresh/index.d.ts +215 -0
  206. package/dist/token-refresh/index.js +6 -0
  207. package/dist/token-refresh/index.js.map +1 -0
  208. package/dist/trace-replay/index.d.ts +11 -2
  209. package/dist/trace-replay/index.js +3 -55
  210. package/dist/trace-replay/index.js.map +1 -1
  211. package/dist/{types-CwBC5wMn.d.ts → types-N9LZbdnr.d.ts} +1 -1
  212. package/dist/with-deadline/index.d.ts +196 -0
  213. package/dist/with-deadline/index.js +130 -0
  214. package/dist/with-deadline/index.js.map +1 -0
  215. package/dist/with-resilience/index.d.ts +205 -0
  216. package/dist/with-resilience/index.js +274 -0
  217. package/dist/with-resilience/index.js.map +1 -0
  218. package/dist/with-telemetry/index.d.ts +149 -0
  219. package/dist/with-telemetry/index.js +95 -0
  220. package/dist/with-telemetry/index.js.map +1 -0
  221. package/dist/work-queue/adapter.d.ts +82 -0
  222. package/dist/work-queue/adapter.js +4 -0
  223. package/dist/work-queue/adapter.js.map +1 -0
  224. package/dist/work-queue/index.d.ts +25 -11
  225. package/dist/work-queue/index.js +1 -46
  226. package/dist/work-queue/index.js.map +1 -1
  227. package/dist/work-queue/ops.d.ts +69 -0
  228. package/dist/work-queue/ops.js +3 -0
  229. package/dist/work-queue/ops.js.map +1 -0
  230. package/dist/workflow/index.d.ts +444 -0
  231. package/dist/workflow/index.js +225 -0
  232. package/dist/workflow/index.js.map +1 -0
  233. package/package.json +162 -25
  234. package/dist/chunk-CMG2KMWX.js.map +0 -1
  235. package/dist/chunk-PYBQREFU.js.map +0 -1
  236. package/dist/chunk-PYOCUBQJ.js.map +0 -1
  237. package/dist/chunk-Y5JM3UUH.js.map +0 -1
  238. 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,3 @@
1
+ export { defaultPaginatorPolicy, drain, initPaginator, isDone, recordPage, resume, start } from '../chunk-SINXE47Y.js';
2
+ //# sourceMappingURL=index.js.map
3
+ //# sourceMappingURL=index.js.map
@@ -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":"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","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,5 +1,5 @@
1
1
  import * as fc from 'fast-check';
2
- import { Cmd, Sub, Machine } from '../../index.js';
2
+ import { C as Cmd, S as Sub, M as Machine } from '../../core-CymXZCeY.js';
3
3
 
4
4
  /**
5
5
  * Build a Ctx value whose every property access throws. Use as the `ctx`
@@ -1,3 +1,3 @@
1
- export { arbConstantMsg, arbGuidedSequence, arbMsg, arbMsgSequence, arbRecordMsg, stubCtxThrowingProxy } from '../../chunk-ZRUST6BK.js';
1
+ export { arbConstantMsg, arbGuidedSequence, arbMsg, arbMsgSequence, arbRecordMsg, stubCtxThrowingProxy } from '../../chunk-7FYQGDMF.js';
2
2
  //# sourceMappingURL=index.js.map
3
3
  //# sourceMappingURL=index.js.map
@@ -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 '../index.js';
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-ZRUST6BK.js';
2
- export { foldEvents, propertyInvariant, propertyTerminates, propertyTrace } from '../chunk-PYBQREFU.js';
3
- import '../chunk-CMG2KMWX.js';
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
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/pbt/util/msg-keys.ts"],"names":[],"mappings":";;;;;AAiCO,SAAS,YACd,MAAA,EACmB;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,CAAE,CAAA;AAC/D,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: Reducer<S, M, C> | ([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
+ {"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,5 +1,5 @@
1
- import { Cmd, Sub, Machine } from '../../index.js';
2
1
  import * as fc from 'fast-check';
2
+ import { C as Cmd, S as Sub, M as Machine } from '../../core-CymXZCeY.js';
3
3
 
4
4
  /**
5
5
  * One unit in a fold trace — the four pieces of data a property
@@ -1,4 +1,5 @@
1
- export { foldEvents, propertyInvariant, propertyTerminates, propertyTrace } from '../../chunk-PYBQREFU.js';
2
- import '../../chunk-CMG2KMWX.js';
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