@demlik/tea 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -0
- package/dist/agent/index.d.ts +16 -770
- package/dist/agent/index.js +18 -16
- package/dist/authed-call/index.d.ts +3 -3
- package/dist/authed-call/index.js +22 -36
- package/dist/authed-call/index.js.map +1 -1
- package/dist/await-terminal/index.d.ts +2 -2
- package/dist/await-terminal/index.js +2 -2
- package/dist/batch-window/index.d.ts +3 -3
- package/dist/batch-window/index.js +3 -3
- package/dist/cache/index.d.ts +2 -2
- package/dist/cache/index.js +5 -2
- package/dist/{chunk-3545BOYL.js → chunk-25LBDCBC.js} +6 -5
- package/dist/chunk-25LBDCBC.js.map +1 -0
- package/dist/{chunk-IK4S5DJN.js → chunk-2AHAR7CX.js} +7 -14
- package/dist/chunk-2AHAR7CX.js.map +1 -0
- package/dist/{chunk-IN33Q4VJ.js → chunk-2KNLKQVN.js} +3 -3
- package/dist/{chunk-IN33Q4VJ.js.map → chunk-2KNLKQVN.js.map} +1 -1
- package/dist/{chunk-VFGKTGGC.js → chunk-5W7CNAHB.js} +191 -178
- package/dist/chunk-5W7CNAHB.js.map +1 -0
- package/dist/{chunk-6GIGQT6C.js → chunk-B5RAZVXW.js} +12 -11
- package/dist/chunk-B5RAZVXW.js.map +1 -0
- package/dist/{chunk-6VDCTR7N.js → chunk-D4NJBQAH.js} +258 -151
- package/dist/chunk-D4NJBQAH.js.map +1 -0
- package/dist/{chunk-UV6UWGEO.js → chunk-D736QUGF.js} +6 -5
- package/dist/chunk-D736QUGF.js.map +1 -0
- package/dist/{chunk-EGWTT5XJ.js → chunk-ET6QQNSQ.js} +76 -95
- package/dist/chunk-ET6QQNSQ.js.map +1 -0
- package/dist/chunk-EWJP3YYP.js +21 -0
- package/dist/chunk-EWJP3YYP.js.map +1 -0
- package/dist/{chunk-M6UZT7HS.js → chunk-FCKP6N7Y.js} +6 -5
- package/dist/chunk-FCKP6N7Y.js.map +1 -0
- package/dist/{chunk-TGG26XMK.js → chunk-FV3Z5MBE.js} +10 -5
- package/dist/chunk-FV3Z5MBE.js.map +1 -0
- package/dist/chunk-G5E74IJB.js +8 -0
- package/dist/chunk-G5E74IJB.js.map +1 -0
- package/dist/{chunk-G2U7DJHL.js → chunk-JFFVBEDI.js} +3 -3
- package/dist/{chunk-G2U7DJHL.js.map → chunk-JFFVBEDI.js.map} +1 -1
- package/dist/{chunk-FOGL3TC7.js → chunk-JXYYRKD6.js} +8 -10
- package/dist/chunk-JXYYRKD6.js.map +1 -0
- package/dist/{chunk-ZDRN3YCE.js → chunk-LJJV6ZD5.js} +17 -24
- package/dist/chunk-LJJV6ZD5.js.map +1 -0
- package/dist/chunk-LYLDCIPH.js +365 -0
- package/dist/chunk-LYLDCIPH.js.map +1 -0
- package/dist/{chunk-HJO6VNQI.js → chunk-MU6MCIAC.js} +3 -3
- package/dist/{chunk-HJO6VNQI.js.map → chunk-MU6MCIAC.js.map} +1 -1
- package/dist/{chunk-UYWUVEP6.js → chunk-MUUROGIH.js} +2 -2
- package/dist/chunk-MUUROGIH.js.map +1 -0
- package/dist/{chunk-DYRKDCJI.js → chunk-PBDYW2TU.js} +41 -7
- package/dist/chunk-PBDYW2TU.js.map +1 -0
- package/dist/{chunk-5VNWZADP.js → chunk-Q2UM7BXU.js} +18 -7
- package/dist/chunk-Q2UM7BXU.js.map +1 -0
- package/dist/chunk-RMX4EOQK.js +26 -0
- package/dist/chunk-RMX4EOQK.js.map +1 -0
- package/dist/{chunk-2YOE5PIS.js → chunk-S2EGRPCM.js} +8 -6
- package/dist/chunk-S2EGRPCM.js.map +1 -0
- package/dist/{chunk-5KVHID6A.js → chunk-SPWVPIFQ.js} +4 -4
- package/dist/{chunk-5KVHID6A.js.map → chunk-SPWVPIFQ.js.map} +1 -1
- package/dist/chunk-SSHDBOXF.js +71 -0
- package/dist/chunk-SSHDBOXF.js.map +1 -0
- package/dist/{chunk-6OUDPSSG.js → chunk-TWDHNIY6.js} +3 -3
- package/dist/{chunk-6OUDPSSG.js.map → chunk-TWDHNIY6.js.map} +1 -1
- package/dist/{chunk-SC3FAFZN.js → chunk-UF4MZSLT.js} +2 -2
- package/dist/chunk-UF4MZSLT.js.map +1 -0
- package/dist/chunk-ZJ5AOYBH.js +8 -0
- package/dist/chunk-ZJ5AOYBH.js.map +1 -0
- package/dist/{chunk-LEC4U6CX.js → chunk-ZVJZ72HY.js} +3 -3
- package/dist/{chunk-LEC4U6CX.js.map → chunk-ZVJZ72HY.js.map} +1 -1
- package/dist/circuit-breaker/index.d.ts +1 -0
- package/dist/circuit-breaker/index.js +1 -1
- package/dist/{core-BmlJOvxM.d.ts → core-42kapW0n.d.ts} +96 -6
- package/dist/deadline/index.d.ts +77 -11
- package/dist/deadline/index.js +3 -3
- package/dist/devtools/index.js +8 -28
- package/dist/devtools/index.js.map +1 -1
- package/dist/do/index.d.ts +52 -24
- package/dist/do/index.js +94 -92
- package/dist/do/index.js.map +1 -1
- package/dist/{durable-effects-9LxFpmh3.d.ts → durable-effects-CFRngTNA.d.ts} +3 -3
- package/dist/extension/index.d.ts +25 -7
- package/dist/extension/index.js +4 -3
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/react.js +4 -3
- package/dist/extension/react.js.map +1 -1
- package/dist/extension/subs/index.d.ts +2 -2
- package/dist/extension/subs/index.js +7 -10
- package/dist/extension/subs/index.js.map +1 -1
- package/dist/fan-out/index.d.ts +10 -10
- package/dist/fan-out/index.js +3 -3
- package/dist/idempotent-intake/index.d.ts +2 -2
- package/dist/idempotent-intake/index.js +3 -3
- package/dist/index.d.ts +50 -489
- package/dist/index.js +2 -2
- package/dist/llm-call/index.d.ts +3 -2
- package/dist/llm-call/index.js +12 -10
- package/dist/machine-B_BF4jij.d.ts +804 -0
- package/dist/machine-viz/index.d.ts +1 -1
- package/dist/machine-viz/index.js +2 -2
- package/dist/mem/index.d.ts +3 -2
- package/dist/mem/index.js.map +1 -1
- package/dist/monitored-run/index.d.ts +54 -30
- package/dist/monitored-run/index.js +7 -7
- package/dist/node/index.d.ts +3 -2
- package/dist/node/index.js +2 -3
- package/dist/node/index.js.map +1 -1
- package/dist/paginated-walk/index.d.ts +2 -2
- package/dist/paginated-walk/index.js +11 -10
- package/dist/parity/index.d.ts +6 -5
- package/dist/parity/index.js +5 -4
- package/dist/parity/index.js.map +1 -1
- package/dist/pbt/arbitraries/index.d.ts +1 -1
- package/dist/pbt/index.d.ts +1 -1
- package/dist/pbt/index.js +3 -3
- package/dist/pbt/index.js.map +1 -1
- package/dist/pbt/runners/index.d.ts +1 -1
- package/dist/pbt/runners/index.js +3 -3
- package/dist/poller/index.d.ts +59 -17
- package/dist/poller/index.js +56 -20
- package/dist/poller/index.js.map +1 -1
- package/dist/prediction/index.d.ts +1 -1
- package/dist/prediction/index.js +2 -2
- package/dist/pure/index.d.ts +1 -1
- package/dist/pure/index.js +2 -2
- package/dist/rate-limit/index.d.ts +2 -2
- package/dist/rate-limit/index.js +1 -1
- package/dist/react/index.d.ts +10 -3
- package/dist/react/index.js +2 -2
- package/dist/react/index.js.map +1 -1
- package/dist/reconciler/index.d.ts +2 -2
- package/dist/reconciler/index.js +13 -15
- package/dist/reconciler/index.js.map +1 -1
- package/dist/recorder/index.d.ts +20 -20
- package/dist/recorder/index.js +1 -1
- package/dist/resilient-call/index.d.ts +31 -6
- package/dist/resilient-call/index.js +10 -9
- package/dist/retry-backoff/index.d.ts +191 -14
- package/dist/retry-backoff/index.js +1 -1
- package/dist/retry-to-success/index.js +1 -1
- package/dist/runtime-types-CukV8kMX.d.ts +406 -0
- package/dist/saga/index.d.ts +3 -2
- package/dist/saga/index.js +2 -2
- package/dist/saga/index.js.map +1 -1
- package/dist/snapshot/index.d.ts +1 -1
- package/dist/snapshot/index.js +3 -3
- package/dist/subs/index.d.ts +372 -4
- package/dist/subs/index.js +4 -1
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/throttled-input/index.d.ts +2 -2
- package/dist/throttled-input/index.js +14 -13
- package/dist/throttled-input/index.js.map +1 -1
- package/dist/token-refresh/index.d.ts +1 -1
- package/dist/token-refresh/index.js +3 -3
- package/dist/trace-replay/index.d.ts +12 -7
- package/dist/trace-replay/index.js +4 -3
- package/dist/{types-BoUWgwSm.d.ts → types-KbL3W1mj.d.ts} +1 -1
- package/dist/with-deadline/index.d.ts +1 -1
- package/dist/with-deadline/index.js +2 -2
- package/dist/with-resilience/index.d.ts +21 -2
- package/dist/with-resilience/index.js +12 -23
- package/dist/with-resilience/index.js.map +1 -1
- package/dist/with-telemetry/index.d.ts +1 -1
- package/dist/with-telemetry/index.js +2 -2
- package/dist/work-queue/adapter.d.ts +2 -2
- package/dist/work-queue/adapter.js +2 -2
- package/dist/work-queue/index.d.ts +3 -2
- package/dist/work-queue/index.js +1 -1
- package/dist/work-queue/index.js.map +1 -1
- package/dist/work-queue/ops.d.ts +2 -2
- package/dist/work-queue/ops.js +1 -1
- package/dist/workflow/index.d.ts +51 -19
- package/dist/workflow/index.js +71 -52
- package/dist/workflow/index.js.map +1 -1
- package/package.json +4 -1
- package/dist/chunk-2YOE5PIS.js.map +0 -1
- package/dist/chunk-3545BOYL.js.map +0 -1
- package/dist/chunk-5VNWZADP.js.map +0 -1
- package/dist/chunk-6GIGQT6C.js.map +0 -1
- package/dist/chunk-6VDCTR7N.js.map +0 -1
- package/dist/chunk-DYRKDCJI.js.map +0 -1
- package/dist/chunk-EGWTT5XJ.js.map +0 -1
- package/dist/chunk-EMJTPHI7.js +0 -91
- package/dist/chunk-EMJTPHI7.js.map +0 -1
- package/dist/chunk-FOGL3TC7.js.map +0 -1
- package/dist/chunk-IK4S5DJN.js.map +0 -1
- package/dist/chunk-M6UZT7HS.js.map +0 -1
- package/dist/chunk-NSDFA6M6.js +0 -198
- package/dist/chunk-NSDFA6M6.js.map +0 -1
- package/dist/chunk-SC3FAFZN.js.map +0 -1
- package/dist/chunk-SKAMZKTE.js +0 -20
- package/dist/chunk-SKAMZKTE.js.map +0 -1
- package/dist/chunk-TGG26XMK.js.map +0 -1
- package/dist/chunk-UV6UWGEO.js.map +0 -1
- package/dist/chunk-UYWUVEP6.js.map +0 -1
- package/dist/chunk-VFGKTGGC.js.map +0 -1
- package/dist/chunk-ZDRN3YCE.js.map +0 -1
package/dist/poller/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { DeadlineSub } from '../deadline/index.js';
|
|
2
2
|
import { IdempotencyStore } from '../idempotency/index.js';
|
|
3
|
-
import { C as Cmd, S as Sub } from '../core-
|
|
4
|
-
import {
|
|
5
|
-
import '../types-
|
|
3
|
+
import { C as Cmd, S as Sub } from '../core-42kapW0n.js';
|
|
4
|
+
import { AnyRetryPolicy, RetryState } from '../retry-backoff/index.js';
|
|
5
|
+
import '../types-KbL3W1mj.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @demlik/tea/poller — "poll a source every N ms until a predicate holds, with
|
|
@@ -23,7 +23,10 @@ import '../types-BoUWgwSm.js';
|
|
|
23
23
|
* cadence; it backs off. The next attempt is armed
|
|
24
24
|
* `nextDelayMs(retry, policy)` from now, the retry
|
|
25
25
|
* counter advances, and `shouldRetry` decides whether
|
|
26
|
-
* the poller keeps trying or gives up.
|
|
26
|
+
* the poller keeps trying or gives up. The bound may
|
|
27
|
+
* be a COUNT or a wall-clock OUTAGE DURATION — see
|
|
28
|
+
* `PollerConfig.retry`; `tickErr`'s `at` is the
|
|
29
|
+
* streak clock, so both flow through unchanged.
|
|
27
30
|
* - `../idempotency` — successive ticks against a flaky source can deliver
|
|
28
31
|
* the SAME observation twice (a retried fetch, a
|
|
29
32
|
* duplicate webhook the poll re-reads). `dedupeKey`
|
|
@@ -122,8 +125,15 @@ interface PollerConfig<State, R> {
|
|
|
122
125
|
* Backoff policy for FAILED ticks. Omit to use `defaultRetryPolicy`
|
|
123
126
|
* (full-jitter, 5 attempts). A success resets the retry counter, so the
|
|
124
127
|
* policy only governs consecutive failures.
|
|
128
|
+
*
|
|
129
|
+
* Any bound the `../retry-backoff` union admits: a count (`RetryPolicy`), a
|
|
130
|
+
* wall-clock outage budget (`DurationRetryPolicy`), or explicit
|
|
131
|
+
* `unbounded: true`. The duration bound needs no extra wiring here — every
|
|
132
|
+
* failure already arrives through `tickErr(state, error, at)` carrying the
|
|
133
|
+
* instant it was observed, so the streak clock is fed from the Msg's own
|
|
134
|
+
* data and the poller never reads a clock of its own (invariant 2).
|
|
125
135
|
*/
|
|
126
|
-
readonly retry?:
|
|
136
|
+
readonly retry?: AnyRetryPolicy;
|
|
127
137
|
/**
|
|
128
138
|
* Optional dedupe key derived from a tick result. When provided, a result
|
|
129
139
|
* whose key was already `seen` records the observation but emits NO fresh
|
|
@@ -141,9 +151,9 @@ interface PollerConfig<State, R> {
|
|
|
141
151
|
readonly dedupeTtlMs?: number;
|
|
142
152
|
}
|
|
143
153
|
/**
|
|
144
|
-
* The
|
|
145
|
-
*
|
|
146
|
-
*
|
|
154
|
+
* The bookkeeping every poller phase carries in common, split out so each arm of
|
|
155
|
+
* the `PollerState` discriminated union names only what it adds (`nextAtMs` on
|
|
156
|
+
* `polling`) — the same discipline the sibling `PaginatorState` uses.
|
|
147
157
|
*
|
|
148
158
|
* - `tick` — count of ticks recorded so far (success or failure). Doubles
|
|
149
159
|
* as an observability cursor and a stable per-tick discriminant.
|
|
@@ -151,23 +161,48 @@ interface PollerConfig<State, R> {
|
|
|
151
161
|
* before the first success. `until` typically reads it (via the
|
|
152
162
|
* consumer's Model), but the knob never interprets it.
|
|
153
163
|
* - `retry` — the `../retry-backoff` counter for consecutive failures.
|
|
154
|
-
* Reset to `initRetry()` on every success.
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
164
|
+
* Reset to `initRetry()` on every success. Once a failure has
|
|
165
|
+
* been recorded it is a `TimedRetryState` (it carries the
|
|
166
|
+
* streak's `firstFailureAtMs`, minted from `tickErr`'s `at`);
|
|
167
|
+
* the field is typed at the `RetryState` supertype because a
|
|
168
|
+
* poller that has never failed has no streak, and a slice
|
|
169
|
+
* persisted before the origin existed rehydrates without one.
|
|
160
170
|
* - `dedupe` — the `../idempotency` store backing `dedupeKey`. An empty,
|
|
161
171
|
* unbounded store when `dedupeKey` is unset (never written).
|
|
162
172
|
*/
|
|
163
|
-
interface
|
|
173
|
+
interface PollerCore<R> {
|
|
164
174
|
readonly tick: number;
|
|
165
175
|
readonly lastResult?: R;
|
|
166
176
|
readonly retry: RetryState;
|
|
167
|
-
readonly nextAtMs: number | null;
|
|
168
|
-
readonly phase: "polling" | "done" | "gave_up";
|
|
169
177
|
readonly dedupe: IdempotencyStore<true>;
|
|
170
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* The Model field the poller knob owns — its visible slice (the knob principle:
|
|
181
|
+
* managed state lives in the Model, never a closure, so it is durable and
|
|
182
|
+
* replayable). A discriminated union on `phase` so the armed-timer target lives
|
|
183
|
+
* ONLY where it is meaningful and impossible combinations are unrepresentable
|
|
184
|
+
* (pattern 11), the sibling `PaginatorState` idiom:
|
|
185
|
+
*
|
|
186
|
+
* - `polling` — ticks are scheduled. Carries `nextAtMs`, the ABSOLUTE target
|
|
187
|
+
* the interval/backoff timer is armed against (persisted so a
|
|
188
|
+
* resumed poller re-arms the same moment), or `null` before the
|
|
189
|
+
* first `start` primes it.
|
|
190
|
+
* - `done` — `until` held; the poll is finished. Carries NO `nextAtMs` — a
|
|
191
|
+
* finished poll can never sit on an armed target.
|
|
192
|
+
* - `gave_up` — backoff exhausted `maxAttempts`. Carries NO `nextAtMs` either.
|
|
193
|
+
*
|
|
194
|
+
* Because only `polling` carries `nextAtMs`, `subs` guards on `phase` alone to
|
|
195
|
+
* decide whether a timer is desired — a `done`/`gave_up` state with a live target
|
|
196
|
+
* (the old flat record's hazard) simply cannot be built.
|
|
197
|
+
*/
|
|
198
|
+
type PollerState<R> = (PollerCore<R> & {
|
|
199
|
+
readonly phase: "polling";
|
|
200
|
+
readonly nextAtMs: number | null;
|
|
201
|
+
}) | (PollerCore<R> & {
|
|
202
|
+
readonly phase: "done";
|
|
203
|
+
}) | (PollerCore<R> & {
|
|
204
|
+
readonly phase: "gave_up";
|
|
205
|
+
});
|
|
171
206
|
/**
|
|
172
207
|
* The poller Sub type — a `../deadline` Sub under a fixed id family. One id per
|
|
173
208
|
* knob instance keeps the reconcile pass from churning the timer: the same id
|
|
@@ -253,6 +288,13 @@ interface Poller<State, R> {
|
|
|
253
288
|
* - If backoff is exhausted (`!shouldRetry`), enters `"gave_up"`, disarms
|
|
254
289
|
* (`nextAtMs: null`), and emits no Cmd.
|
|
255
290
|
*
|
|
291
|
+
* `at` is both the backoff anchor AND the streak clock: it is passed to
|
|
292
|
+
* `recordFailure` (starting / preserving `firstFailureAtMs`) and to
|
|
293
|
+
* `shouldRetry`, so a `DurationRetryPolicy` on `config.retry` is honoured
|
|
294
|
+
* with no extra wiring. Under a duration bound the poller keeps retrying
|
|
295
|
+
* however many attempts the outage takes, and stops at the declared
|
|
296
|
+
* wall-clock budget measured from the streak's FIRST failure.
|
|
297
|
+
*
|
|
256
298
|
* The backoff jitter uses the `rng` injected ONCE at `createPoller` (default
|
|
257
299
|
* `Math.random` at the effect boundary, a fixed value in tests); the verb
|
|
258
300
|
* body never names the global RNG, so a poller built with a fixed `rng`
|
package/dist/poller/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { deadlineSub } from '../chunk-
|
|
2
|
-
import { asRng, defaultRetryPolicy, initRetry, recordFailure, shouldRetry, nextDelayMs } from '../chunk-
|
|
1
|
+
import { deadlineSub } from '../chunk-RMX4EOQK.js';
|
|
2
|
+
import { asRng, defaultRetryPolicy, initRetry, recordFailure, shouldRetry, nextDelayMs } from '../chunk-Q2UM7BXU.js';
|
|
3
3
|
import { idempotencyMemory } from '../chunk-BYQUGDD3.js';
|
|
4
4
|
import '../chunk-K575PT4S.js';
|
|
5
5
|
import '../chunk-2YFLOCWX.js';
|
|
6
|
-
import '../chunk-
|
|
7
|
-
import '../chunk-
|
|
6
|
+
import '../chunk-D4NJBQAH.js';
|
|
7
|
+
import '../chunk-PBDYW2TU.js';
|
|
8
8
|
|
|
9
9
|
// src/poller/index.ts
|
|
10
10
|
function pollerSubId(atMs) {
|
|
@@ -16,10 +16,10 @@ function createPoller(config, rng = Math.random) {
|
|
|
16
16
|
const memory = idempotencyMemory();
|
|
17
17
|
function init() {
|
|
18
18
|
return {
|
|
19
|
+
phase: "polling",
|
|
19
20
|
tick: 0,
|
|
20
21
|
retry: initRetry(),
|
|
21
22
|
nextAtMs: null,
|
|
22
|
-
phase: "polling",
|
|
23
23
|
// The dedupe store is created regardless; it is only WRITTEN when
|
|
24
24
|
// `dedupeKey` is set, so an unconfigured poller carries an inert empty
|
|
25
25
|
// store (no per-op cost, JSON-serializable, durable like the rest of
|
|
@@ -28,26 +28,43 @@ function createPoller(config, rng = Math.random) {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
function start(state, at) {
|
|
31
|
-
return [
|
|
31
|
+
return [
|
|
32
|
+
{
|
|
33
|
+
phase: "polling",
|
|
34
|
+
tick: state.tick,
|
|
35
|
+
lastResult: state.lastResult,
|
|
36
|
+
retry: state.retry,
|
|
37
|
+
nextAtMs: at + config.everyMs,
|
|
38
|
+
dedupe: state.dedupe
|
|
39
|
+
},
|
|
40
|
+
[]
|
|
41
|
+
];
|
|
32
42
|
}
|
|
33
43
|
function tick(state) {
|
|
34
44
|
if (state.phase !== "polling") return [state, []];
|
|
35
45
|
return [state, [config.onTick()]];
|
|
36
46
|
}
|
|
37
47
|
function tickResult(state, result, at, untilHeld) {
|
|
38
|
-
const
|
|
39
|
-
...state,
|
|
40
|
-
tick: state.tick + 1,
|
|
41
|
-
lastResult: result,
|
|
42
|
-
retry: initRetry()
|
|
43
|
-
};
|
|
48
|
+
const tick2 = state.tick + 1;
|
|
44
49
|
if (untilHeld) {
|
|
45
|
-
return [
|
|
50
|
+
return [
|
|
51
|
+
{
|
|
52
|
+
phase: "done",
|
|
53
|
+
tick: tick2,
|
|
54
|
+
lastResult: result,
|
|
55
|
+
retry: initRetry(),
|
|
56
|
+
dedupe: state.dedupe
|
|
57
|
+
},
|
|
58
|
+
[]
|
|
59
|
+
];
|
|
46
60
|
}
|
|
47
61
|
const next = {
|
|
48
|
-
...base,
|
|
49
62
|
phase: "polling",
|
|
50
|
-
|
|
63
|
+
tick: tick2,
|
|
64
|
+
lastResult: result,
|
|
65
|
+
retry: initRetry(),
|
|
66
|
+
nextAtMs: at + config.everyMs,
|
|
67
|
+
dedupe: state.dedupe
|
|
51
68
|
};
|
|
52
69
|
if (!config.dedupeKey) return [next, []];
|
|
53
70
|
const key = config.dedupeKey(result);
|
|
@@ -59,13 +76,32 @@ function createPoller(config, rng = Math.random) {
|
|
|
59
76
|
return [deduped, suppress ? [] : [config.onTick()]];
|
|
60
77
|
}
|
|
61
78
|
function tickErr(state, error, at) {
|
|
62
|
-
const retry = recordFailure(state.retry, error);
|
|
63
|
-
const
|
|
64
|
-
if (!shouldRetry(retry, policy)) {
|
|
65
|
-
return [
|
|
79
|
+
const retry = recordFailure(state.retry, error, at);
|
|
80
|
+
const tick2 = state.tick + 1;
|
|
81
|
+
if (!shouldRetry(retry, policy, at)) {
|
|
82
|
+
return [
|
|
83
|
+
{
|
|
84
|
+
phase: "gave_up",
|
|
85
|
+
tick: tick2,
|
|
86
|
+
lastResult: state.lastResult,
|
|
87
|
+
retry,
|
|
88
|
+
dedupe: state.dedupe
|
|
89
|
+
},
|
|
90
|
+
[]
|
|
91
|
+
];
|
|
66
92
|
}
|
|
67
93
|
const delayMs = nextDelayMs(retry, policy, rngBranded);
|
|
68
|
-
return [
|
|
94
|
+
return [
|
|
95
|
+
{
|
|
96
|
+
phase: "polling",
|
|
97
|
+
tick: tick2,
|
|
98
|
+
lastResult: state.lastResult,
|
|
99
|
+
retry,
|
|
100
|
+
nextAtMs: at + delayMs,
|
|
101
|
+
dedupe: state.dedupe
|
|
102
|
+
},
|
|
103
|
+
[]
|
|
104
|
+
];
|
|
69
105
|
}
|
|
70
106
|
function subs(state) {
|
|
71
107
|
if (state.phase !== "polling" || state.nextAtMs === null) return [];
|
package/dist/poller/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/poller/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @demlik/tea/poller — \"poll a source every N ms until a predicate holds, with\n * backoff on failure\" as a TEA knob: one config object + a few pre-wired hooks\n * you spread into your machine.\n *\n * The recipe (which bricks it composes):\n *\n * - `../deadline` — the recurring tick is an ABSOLUTE-deadline Sub.\n * Each successful tick computes the NEXT absolute\n * fire time (`at + everyMs`) and the knob re-arms a\n * deadline against it. Absolute (not relative) so a\n * poller resumed after a DO eviction / page reload\n * fires at the correct wall-clock moment, not\n * `everyMs` after the reload (`../deadline`'s late-\n * subscribe property).\n * - `../retry-backoff` — a failed tick does NOT reschedule on the steady\n * cadence; it backs off. The next attempt is armed\n * `nextDelayMs(retry, policy)` from now, the retry\n * counter advances, and `shouldRetry` decides whether\n * the poller keeps trying or gives up.\n * - `../idempotency` — successive ticks against a flaky source can deliver\n * the SAME observation twice (a retried fetch, a\n * duplicate webhook the poll re-reads). `dedupeKey`\n * lets the consumer key each tick result; a duplicate\n * key is recorded but emits no fresh `onTick` follow-\n * up Cmd, so the side effect runs once per distinct\n * observation.\n *\n * Typical wiring (spread the knob's hooks into your machine):\n *\n * const poll = createPoller<MyState, MyResult>({\n * everyMs: 5_000,\n * until: (s) => s.poll.lastResult?.status === \"ready\",\n * onTick: () => ({ type: \"fetch_status\" }),\n * retry: { ...defaultRetryPolicy, maxAttempts: 4 },\n * });\n *\n * // Model slice:\n * init: (loaded) => [loaded ?? { poll: poll.init(), ... }, []],\n *\n * // The deadline Sub fires `deadline_exceeded` on the cadence/backoff target;\n * // route it to `tick`, the ONLY place the `onTick` fetch is emitted:\n * deadline_exceeded: (s) => {\n * const [slice, cmds] = poll.tick(s.poll);\n * return [{ ...s, poll: slice }, cmds];\n * },\n *\n * // update cells (thread `at` in from the firing Msg / the runtime clock):\n * poll_result: (s, m) => {\n * const untilHeld = config.until({ ...s, ...folded });\n * const [slice, cmds] = poll.tickResult(s.poll, m.result, m.at, untilHeld);\n * return [{ ...s, poll: slice }, cmds];\n * },\n * poll_error: (s, m) => {\n * const [slice, cmds] = poll.tickErr(s.poll, m.error, m.at);\n * return [{ ...s, poll: slice }, cmds];\n * },\n *\n * // Subs — the knob owns the interval/backoff deadline:\n * subscriptions: (s) => poll.subs(s.poll),\n *\n * The cadence has exactly ONE source: the `everyMs` deadline Sub. `start`,\n * `tickResult`, and `tickErr` only RE-ARM the deadline (they emit no cadence\n * Cmd); the fetch fires solely from `tick`, which the consumer routes the\n * deadline's `deadline_exceeded` Msg to. No verb performs an immediate\n * \"fire now\" observation, so the poller never polls faster than `everyMs`.\n *\n * The two non-negotiables every knob preserves hold here:\n * - **Durable** — `tick`, `lastResult`, `retry`, and the next-fire target all\n * live in the Model slice, so a poller survives eviction and resumes its\n * cadence from the persisted target.\n * - **Replayable** — every transition is a pure verb returning new state; the\n * clock is never read inside a verb (the firing Msg carries `at`), and the\n * retry RNG is injected ONCE at `createPoller` and threaded internally (no\n * verb body names the global RNG). `replay` reconstructs a poll run exactly.\n *\n * NOT a substrate primitive: it depends only on sibling subpaths\n * (`../deadline`, `../retry-backoff`, `../idempotency`) and the core `Cmd` /\n * `Sub` types. Consumers reach it via the `@demlik/tea/poller` subpath; the L1\n * escape hatch is those three sibling subpaths threaded by hand.\n */\n\nimport { type DeadlineSub, deadlineSub } from \"../deadline\";\nimport type { IdempotencyStore } from \"../idempotency\";\nimport { idempotencyMemory } from \"../idempotency/adapter\";\nimport type { Cmd, Sub } from \"../index\";\nimport {\n asRng,\n defaultRetryPolicy,\n initRetry,\n nextDelayMs,\n type RetryPolicy,\n type RetryState,\n recordFailure,\n shouldRetry,\n} from \"../retry-backoff\";\n\n/**\n * The poller knob's config — the single object you hand `createPoller`.\n *\n * `State` is the consumer's WHOLE Model (so `until` can read any field), `R` is\n * the shape of one tick observation (`lastResult` carries it forward).\n */\nexport interface PollerConfig<State, R> {\n /**\n * The steady cadence, in milliseconds, between successful ticks. The next\n * tick is armed at the absolute target `at + everyMs` (computed from the\n * `at` the prior tick's Msg carried), NOT `everyMs` from when the Sub\n * happens to be reconciled — so the cadence does not drift across resumes.\n */\n readonly everyMs: number;\n /**\n * The stop predicate, read against the consumer's whole Model. When it\n * returns `true`, the poller is DONE: `subs` returns `[]` (the timer\n * disarms) and no further tick is scheduled. Pure — it must not read the\n * clock or mutate; the substrate calls it inside `subscriptions(state)`.\n */\n readonly until: (state: State) => boolean;\n /**\n * The effect to run on each tick — the actual \"go observe the source\" Cmd\n * (a fetch, a status read, a list call). The knob returns it from\n * `tickResult` when the run continues; the consumer's `interpret` performs\n * it and routes the observation back as the next tick-result Msg.\n *\n * Nullary by contract: a tick carries no per-call argument (the poller\n * re-reads the same source every cadence). Capture any target in the Cmd\n * literal the consumer's `onTick` returns.\n */\n readonly onTick: () => Cmd;\n /**\n * Backoff policy for FAILED ticks. Omit to use `defaultRetryPolicy`\n * (full-jitter, 5 attempts). A success resets the retry counter, so the\n * policy only governs consecutive failures.\n */\n readonly retry?: RetryPolicy;\n /**\n * Optional dedupe key derived from a tick result. When provided, a result\n * whose key was already `seen` records the observation but emits NO fresh\n * `onTick` follow-up — the side effect runs once per distinct observation.\n * Omit to treat every tick result as fresh (no dedupe). The dedupe store is\n * bounded by `dedupeTtlMs` if set; otherwise it grows with distinct keys.\n */\n readonly dedupeKey?: (result: R) => string;\n /**\n * TTL for the dedupe store, in milliseconds. Only meaningful when\n * `dedupeKey` is set. An observation older than `dedupeTtlMs` is forgotten,\n * so a key seen long ago is treated as fresh again. Omit for an unbounded\n * dedupe window (every distinct key remembered for the poller's lifetime).\n */\n readonly dedupeTtlMs?: number;\n}\n\n/**\n * The Model field the poller knob owns — its visible slice (the knob principle:\n * managed state lives in the Model, never a closure, so it is durable and\n * replayable).\n *\n * - `tick` — count of ticks recorded so far (success or failure). Doubles\n * as an observability cursor and a stable per-tick discriminant.\n * - `lastResult` — the most recent successful observation, or `undefined`\n * before the first success. `until` typically reads it (via the\n * consumer's Model), but the knob never interprets it.\n * - `retry` — the `../retry-backoff` counter for consecutive failures.\n * Reset to `initRetry()` on every success.\n * - `nextAtMs` — the ABSOLUTE target the interval/backoff timer is armed\n * against, or `null` when the poller is done / not yet started.\n * Persisted so a resumed poller re-arms the same moment.\n * - `phase` — `\"polling\"` while ticks are scheduled, `\"done\"` once `until`\n * held, `\"gave_up\"` once backoff exhausted `maxAttempts`.\n * - `dedupe` — the `../idempotency` store backing `dedupeKey`. An empty,\n * unbounded store when `dedupeKey` is unset (never written).\n */\nexport interface PollerState<R> {\n readonly tick: number;\n readonly lastResult?: R;\n readonly retry: RetryState;\n readonly nextAtMs: number | null;\n readonly phase: \"polling\" | \"done\" | \"gave_up\";\n readonly dedupe: IdempotencyStore<true>;\n}\n\n/**\n * The poller Sub type — a `../deadline` Sub under a fixed id family. One id per\n * knob instance keeps the reconcile pass from churning the timer: the same id\n * across transitions means \"same armed deadline\", even as `nextAtMs` advances.\n */\nexport type PollerSub = DeadlineSub;\n\n/**\n * The Sub id the poller arms its tick deadline under, keyed on the absolute\n * target. The id ROTATES per target because a one-shot deadline timer is\n * exhausted once it fires and the substrate reconciles subs by `id` alone: a\n * fixed id would leave the fired timer registered and never re-arm, so the\n * cadence would die after exactly one tick. Encoding `atMs` in the id makes each\n * cadence/backoff target a distinct identity — the reconcile pass retires the\n * spent timer and arms the next — while a resumed poller re-arms the SAME id\n * from its persisted `nextAtMs` (eviction is idempotent).\n *\n * A single poller machine arms exactly one tick timer at a time; a consumer\n * running several pollers on one machine namespaces them by composing the knob\n * under distinct slices (a future multi-poller config would thread a prefix).\n */\nfunction pollerSubId(atMs: number): string {\n return `poller:tick:${atMs}`;\n}\n\n/**\n * The knob handle returned by `createPoller`. Spread its hooks into your\n * machine: `init()` seeds the slice, `start(...)` arms the first tick,\n * `tickResult` / `tickErr` are the two update verbs, and `subs(...)` returns the\n * timer Sub while the poller is live.\n *\n * Pure-ops + subs module: there is no `handlers(ports)` cell. The poller's I/O\n * is the consumer's own `onTick` Cmd run by the consumer's `interpret` — the\n * knob never owns a port. (Compare `../deadline`, which ships a `subscribe`\n * cell; the poller reuses `../deadline`'s, so it has nothing of its own to\n * splice into `interpret`.)\n */\nexport interface Poller<\n // State is a phantom-style parameter — present so consumers can spell\n // `Poller<MyState, MyResult>` matching `createPoller<State, R>`. It is\n // consumed by the config's `until` predicate, not by any handle method, so\n // it does not appear in the interface body. Same discipline as\n // `BoundMachine`'s `Ctx`.\n // biome-ignore lint/correctness/noUnusedVariables: phantom type parameter (see above)\n State,\n R,\n> {\n /** Seed the Model slice. Idle until `start(...)` arms the first tick. */\n init(): PollerState<R>;\n /**\n * Arm the first tick. `at` is the current clock (carried in from the Msg /\n * runtime that kicks the poller off — never read inside the verb). Returns\n * the slice with the first deadline target set to `at + everyMs` and emits NO\n * Cmd. The cadence has ONE source — the deadline Sub — so `start` does not\n * perform the first observation itself (an immediate `onTick` here would race\n * the timer and poll as fast as the source responds). The first observation\n * fires when the deadline crosses `at + everyMs` and the consumer routes\n * `deadline_exceeded` → `tick`.\n */\n start(\n state: PollerState<R>,\n at: number,\n ): readonly [PollerState<R>, readonly Cmd[]];\n /**\n * Perform one observation — the cadence verb. The consumer routes the\n * deadline Sub's `deadline_exceeded` Msg here; `tick` emits the single\n * `config.onTick()` Cmd that goes and reads the source. This is the ONLY\n * next-tick mechanism: the timer fires, the consumer calls `tick`, the fetch\n * runs, and the observation comes back through `tickResult` / `tickErr`\n * (which re-arm the next deadline). A no-op on a finished poller — a stale\n * deadline fire racing a terminal transition the reconcile pass has not yet\n * retired emits nothing.\n *\n * Takes no `at`: `tick` does not touch the schedule (the just-fired deadline\n * is exhausted; the next target is set when the result lands), so it needs no\n * clock — keeping it trivially pure.\n */\n tick(state: PollerState<R>): readonly [PollerState<R>, readonly Cmd[]];\n /**\n * Record a successful tick observation at clock `at`.\n *\n * - Resets the retry counter (a success clears consecutive-failure state).\n * - Stores `result` as `lastResult` and bumps `tick`.\n * - Re-arms the next tick at the absolute target `at + everyMs` (the steady\n * cadence). It emits NO cadence `onTick` — the next observation fires from\n * the deadline Sub via `tick`, never immediately from a result.\n * - Dedupe (opt-in, orthogonal to cadence): when `dedupeKey` is set, a FRESH\n * observation emits one `onTick` FOLLOW-UP (the consumer's \"run the\n * downstream side effect once per distinct observation\" hook) and a\n * DUPLICATE key records the sighting but suppresses the follow-up. With no\n * `dedupeKey`, `tickResult` emits nothing — the cadence `tick` is the only\n * `onTick`, so an unconfigured poller never double-fetches.\n * - UNLESS `untilHeld` is `true` (the consumer evaluated `config.until`\n * against the post-result Model) — then the poller enters `\"done\"`,\n * disarms (`nextAtMs: null`), and emits no Cmd, dedupe or otherwise.\n *\n * `untilHeld` is passed in rather than computed here because `until` reads\n * the consumer's WHOLE Model, which this verb does not hold — the consumer\n * evaluates it at the call site after folding `result` into its Model and\n * passes the boolean. (`subs` re-checks `phase` to decide arming, so a\n * mis-passed `untilHeld` cannot leave a \"done\" poller arming timers.)\n */\n tickResult(\n state: PollerState<R>,\n result: R,\n at: number,\n untilHeld: boolean,\n ): readonly [PollerState<R>, readonly Cmd[]];\n /**\n * Record a FAILED tick at clock `at`. Backs off instead of holding the\n * steady cadence:\n *\n * - Advances the retry counter (`recordFailure`).\n * - If `shouldRetry` still permits another attempt, arms the next tick at\n * `at + nextDelayMs(retry, policy)` (the backoff curve) and emits NO Cmd —\n * the retry observation fires when that backoff deadline crosses and the\n * consumer routes `deadline_exceeded` → `tick`. The timer is the single\n * next-tick mechanism, on the backoff curve here instead of `everyMs`.\n * - If backoff is exhausted (`!shouldRetry`), enters `\"gave_up\"`, disarms\n * (`nextAtMs: null`), and emits no Cmd.\n *\n * The backoff jitter uses the `rng` injected ONCE at `createPoller` (default\n * `Math.random` at the effect boundary, a fixed value in tests); the verb\n * body never names the global RNG, so a poller built with a fixed `rng`\n * replays a failure-and-backoff run bit-for-bit.\n */\n tickErr(\n state: PollerState<R>,\n error: unknown,\n at: number,\n ): readonly [PollerState<R>, readonly Cmd[]];\n /**\n * The pre-wired subscriptions cell. Returns the single tick-deadline Sub\n * while the poller is `\"polling\"` with an armed `nextAtMs`; returns `[]`\n * once `\"done\"` / `\"gave_up\"` (the timer disarms) or before `start`. Assign\n * directly: `subscriptions: (s) => poll.subs(s.poll)`.\n *\n * The `DeadlineSub` it returns is consumed by `../deadline`'s\n * `subscribeDeadline` cell, which the consumer wires into `subscribe` —\n * the poller does not redraw the timer lifecycle.\n */\n subs(state: PollerState<R>): readonly Sub[];\n}\n\n/**\n * Build a poller knob from `config`. Returns the hook bag to spread into a\n * machine (see the module doc-comment for the wiring). Pure factory — no clock\n * read, no timer; everything happens when the consumer calls the returned\n * verbs / `subs` with an injected `at`.\n *\n * `rng` is injected ONCE here (default `Math.random` at the effect boundary, a\n * fixed value in tests) and threaded internally into the backoff jitter. No\n * verb body ever names the global RNG, so a poller built with a fixed `rng`\n * replays a failure-and-backoff run bit-for-bit (invariant 2: the verbs are\n * pure given the injected `rng`).\n */\nexport function createPoller<State, R>(\n config: PollerConfig<State, R>,\n rng: () => number = Math.random,\n): Poller<State, R> {\n // Re-affirm the `[0, 1)` contract at the seam where the injected generator\n // enters the backoff math (brand from `../retry-backoff`). The factory's\n // public param stays a plain `() => number` so transitive callers are\n // unchanged; `asRng` is the single point where it's branded for `nextDelayMs`.\n const rngBranded = asRng(rng);\n const policy: RetryPolicy = config.retry ?? defaultRetryPolicy;\n\n // The dedupe store seam, bound once. The poller delegates its dedupe-window\n // reads (`memory.isSeen`) and the per-sighting touch (`memory.remember`) to\n // the verbs, so the `IdempotencyStore` shape stays insulated.\n const memory = idempotencyMemory<true>();\n\n function init(): PollerState<R> {\n return {\n tick: 0,\n retry: initRetry(),\n nextAtMs: null,\n phase: \"polling\",\n // The dedupe store is created regardless; it is only WRITTEN when\n // `dedupeKey` is set, so an unconfigured poller carries an inert empty\n // store (no per-op cost, JSON-serializable, durable like the rest of\n // the slice).\n dedupe: memory.init({ ttlMs: config.dedupeTtlMs }),\n };\n }\n\n function start(\n state: PollerState<R>,\n at: number,\n ): readonly [PollerState<R>, readonly Cmd[]] {\n // Arm the first deadline at `at + everyMs` and emit NOTHING. The cadence has\n // ONE source — the `everyMs` deadline Sub. `start` does not perform the\n // first observation itself: that would be a second, immediate next-tick\n // mechanism racing the timer (it would fire as fast as the source responds,\n // never honoring `everyMs`). The first observation fires when the deadline\n // crosses `at + everyMs` and the consumer routes `deadline_exceeded` → the\n // `tick` verb (which emits `onTick`). No drift, no double-fire.\n return [{ ...state, phase: \"polling\", nextAtMs: at + config.everyMs }, []];\n }\n\n function tick(\n state: PollerState<R>,\n ): readonly [PollerState<R>, readonly Cmd[]] {\n // The deadline fired. Perform the observation — this is the ONLY place the\n // `onTick` Cmd is emitted, so the timer is the single next-tick mechanism.\n // No schedule change here: the just-fired deadline is exhausted, and the\n // NEXT target is re-armed when the observation lands (`tickResult` /\n // `tickErr`). A no-op on a finished poller (a stale fire racing a terminal\n // transition the reconcile pass has not yet retired).\n if (state.phase !== \"polling\") return [state, []];\n return [state, [config.onTick()]];\n }\n\n function tickResult(\n state: PollerState<R>,\n result: R,\n at: number,\n untilHeld: boolean,\n ): readonly [PollerState<R>, readonly Cmd[]] {\n // A success always clears consecutive-failure state and records the datum.\n const base: PollerState<R> = {\n ...state,\n tick: state.tick + 1,\n lastResult: result,\n retry: initRetry(),\n };\n\n // Stop condition first: if the consumer's `until` held against the\n // post-result Model, we are DONE — disarm and emit nothing, regardless of\n // dedupe (the final observation is still recorded as `lastResult`).\n if (untilHeld) {\n return [{ ...base, phase: \"done\", nextAtMs: null }, []];\n }\n\n // Re-arm the steady cadence: the next observation fires when THIS deadline\n // (`at + everyMs`) crosses and the consumer routes `deadline_exceeded` →\n // `tick`. `tickResult` itself never emits the cadence `onTick` — the timer\n // is the single next-tick mechanism (recording a result is not a reason to\n // immediately re-fetch; that would poll as fast as the source responds).\n const next: PollerState<R> = {\n ...base,\n phase: \"polling\",\n nextAtMs: at + config.everyMs,\n };\n\n // Dedupe is an OPT-IN follow-up, orthogonal to the cadence above. When\n // `dedupeKey` is set, a FRESH observation emits one `onTick` follow-up (the\n // consumer's \"run the downstream side effect once per distinct observation\"\n // hook); a DUPLICATE key records the sighting but suppresses that follow-up.\n // With no `dedupeKey` there is no follow-up at all — the cadence `tick` is\n // the only `onTick`, so an unconfigured poller never double-fetches.\n if (!config.dedupeKey) return [next, []];\n\n const key = config.dedupeKey(result);\n const suppress = memory.isSeen(next.dedupe, key, at);\n // `remember` refreshes the key's TTL clock on every sighting (the\n // idempotency \"touch\" semantics), so a steadily-repeating observation never\n // ages out mid-poll. The stored value is the unit `true` — the poller only\n // needs presence, not a payload.\n const deduped: PollerState<R> = {\n ...next,\n dedupe: memory.remember(next.dedupe, key, true, at),\n };\n return [deduped, suppress ? [] : [config.onTick()]];\n }\n\n function tickErr(\n state: PollerState<R>,\n error: unknown,\n at: number,\n ): readonly [PollerState<R>, readonly Cmd[]] {\n const retry = recordFailure(state.retry, error);\n const failed: PollerState<R> = { ...state, tick: state.tick + 1, retry };\n\n // Backoff exhausted → give up: disarm and emit nothing. `shouldRetry`\n // reads the just-advanced `retry`, so the (maxAttempts)-th failure is the\n // one that stops the poller.\n if (!shouldRetry(retry, policy)) {\n return [{ ...failed, phase: \"gave_up\", nextAtMs: null }, []];\n }\n\n // Back off: arm the next attempt at `at + backoff(retry)` rather than the\n // steady cadence, and emit NOTHING. The retry observation fires when this\n // backoff deadline crosses and the consumer routes `deadline_exceeded` →\n // `tick` — the timer is the single next-tick mechanism, on the backoff\n // curve here instead of `everyMs`. `rng` is the value injected once at\n // `createPoller`; the verb body never names the global RNG, so a fixed\n // `rng` makes the backoff target replay bit-for-bit.\n const delayMs = nextDelayMs(retry, policy, rngBranded);\n return [{ ...failed, phase: \"polling\", nextAtMs: at + delayMs }, []];\n }\n\n function subs(state: PollerState<R>): readonly Sub[] {\n // Arm the tick deadline only while live. `phase` is the authority (so a\n // mis-passed `untilHeld` into `tickResult` cannot resurrect a finished\n // poller); `nextAtMs` is the absolute target `../deadline` translates to a\n // remaining delay at subscribe time — a resumed poller re-arms the exact\n // persisted moment.\n if (state.phase !== \"polling\" || state.nextAtMs === null) return [];\n // The Sub id ROTATES with the target. A one-shot deadline timer is\n // exhausted once it fires, and the substrate's reconcile pass keys identity\n // by `id` alone — a fixed id would leave the fired timer in the registry and\n // NEVER re-arm for the next cadence (cadence dies after one tick). Folding\n // `nextAtMs` into the id makes each successive target a distinct identity:\n // the reconcile pass retires the spent timer (old id absent from the desired\n // set) and arms the next one (new id). A resumed poller re-arms the SAME id\n // from its persisted `nextAtMs`, so eviction is idempotent.\n return [deadlineSub(pollerSubId(state.nextAtMs), state.nextAtMs)];\n }\n\n return { init, start, tick, tickResult, tickErr, subs };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/poller/index.ts"],"names":["tick"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @demlik/tea/poller — \"poll a source every N ms until a predicate holds, with\n * backoff on failure\" as a TEA knob: one config object + a few pre-wired hooks\n * you spread into your machine.\n *\n * The recipe (which bricks it composes):\n *\n * - `../deadline` — the recurring tick is an ABSOLUTE-deadline Sub.\n * Each successful tick computes the NEXT absolute\n * fire time (`at + everyMs`) and the knob re-arms a\n * deadline against it. Absolute (not relative) so a\n * poller resumed after a DO eviction / page reload\n * fires at the correct wall-clock moment, not\n * `everyMs` after the reload (`../deadline`'s late-\n * subscribe property).\n * - `../retry-backoff` — a failed tick does NOT reschedule on the steady\n * cadence; it backs off. The next attempt is armed\n * `nextDelayMs(retry, policy)` from now, the retry\n * counter advances, and `shouldRetry` decides whether\n * the poller keeps trying or gives up. The bound may\n * be a COUNT or a wall-clock OUTAGE DURATION — see\n * `PollerConfig.retry`; `tickErr`'s `at` is the\n * streak clock, so both flow through unchanged.\n * - `../idempotency` — successive ticks against a flaky source can deliver\n * the SAME observation twice (a retried fetch, a\n * duplicate webhook the poll re-reads). `dedupeKey`\n * lets the consumer key each tick result; a duplicate\n * key is recorded but emits no fresh `onTick` follow-\n * up Cmd, so the side effect runs once per distinct\n * observation.\n *\n * Typical wiring (spread the knob's hooks into your machine):\n *\n * const poll = createPoller<MyState, MyResult>({\n * everyMs: 5_000,\n * until: (s) => s.poll.lastResult?.status === \"ready\",\n * onTick: () => ({ type: \"fetch_status\" }),\n * retry: { ...defaultRetryPolicy, maxAttempts: 4 },\n * });\n *\n * // Model slice:\n * init: (loaded) => [loaded ?? { poll: poll.init(), ... }, []],\n *\n * // The deadline Sub fires `deadline_exceeded` on the cadence/backoff target;\n * // route it to `tick`, the ONLY place the `onTick` fetch is emitted:\n * deadline_exceeded: (s) => {\n * const [slice, cmds] = poll.tick(s.poll);\n * return [{ ...s, poll: slice }, cmds];\n * },\n *\n * // update cells (thread `at` in from the firing Msg / the runtime clock):\n * poll_result: (s, m) => {\n * const untilHeld = config.until({ ...s, ...folded });\n * const [slice, cmds] = poll.tickResult(s.poll, m.result, m.at, untilHeld);\n * return [{ ...s, poll: slice }, cmds];\n * },\n * poll_error: (s, m) => {\n * const [slice, cmds] = poll.tickErr(s.poll, m.error, m.at);\n * return [{ ...s, poll: slice }, cmds];\n * },\n *\n * // Subs — the knob owns the interval/backoff deadline:\n * subscriptions: (s) => poll.subs(s.poll),\n *\n * The cadence has exactly ONE source: the `everyMs` deadline Sub. `start`,\n * `tickResult`, and `tickErr` only RE-ARM the deadline (they emit no cadence\n * Cmd); the fetch fires solely from `tick`, which the consumer routes the\n * deadline's `deadline_exceeded` Msg to. No verb performs an immediate\n * \"fire now\" observation, so the poller never polls faster than `everyMs`.\n *\n * The two non-negotiables every knob preserves hold here:\n * - **Durable** — `tick`, `lastResult`, `retry`, and the next-fire target all\n * live in the Model slice, so a poller survives eviction and resumes its\n * cadence from the persisted target.\n * - **Replayable** — every transition is a pure verb returning new state; the\n * clock is never read inside a verb (the firing Msg carries `at`), and the\n * retry RNG is injected ONCE at `createPoller` and threaded internally (no\n * verb body names the global RNG). `replay` reconstructs a poll run exactly.\n *\n * NOT a substrate primitive: it depends only on sibling subpaths\n * (`../deadline`, `../retry-backoff`, `../idempotency`) and the core `Cmd` /\n * `Sub` types. Consumers reach it via the `@demlik/tea/poller` subpath; the L1\n * escape hatch is those three sibling subpaths threaded by hand.\n */\n\nimport { type DeadlineSub, deadlineSub } from \"../deadline\";\nimport type { IdempotencyStore } from \"../idempotency\";\nimport { idempotencyMemory } from \"../idempotency/adapter\";\nimport type { Cmd, Sub } from \"../index\";\nimport {\n type AnyRetryPolicy,\n asRng,\n defaultRetryPolicy,\n initRetry,\n nextDelayMs,\n type RetryState,\n recordFailure,\n shouldRetry,\n} from \"../retry-backoff\";\n\n/**\n * The poller knob's config — the single object you hand `createPoller`.\n *\n * `State` is the consumer's WHOLE Model (so `until` can read any field), `R` is\n * the shape of one tick observation (`lastResult` carries it forward).\n */\nexport interface PollerConfig<State, R> {\n /**\n * The steady cadence, in milliseconds, between successful ticks. The next\n * tick is armed at the absolute target `at + everyMs` (computed from the\n * `at` the prior tick's Msg carried), NOT `everyMs` from when the Sub\n * happens to be reconciled — so the cadence does not drift across resumes.\n */\n readonly everyMs: number;\n /**\n * The stop predicate, read against the consumer's whole Model. When it\n * returns `true`, the poller is DONE: `subs` returns `[]` (the timer\n * disarms) and no further tick is scheduled. Pure — it must not read the\n * clock or mutate; the substrate calls it inside `subscriptions(state)`.\n */\n readonly until: (state: State) => boolean;\n /**\n * The effect to run on each tick — the actual \"go observe the source\" Cmd\n * (a fetch, a status read, a list call). The knob returns it from\n * `tickResult` when the run continues; the consumer's `interpret` performs\n * it and routes the observation back as the next tick-result Msg.\n *\n * Nullary by contract: a tick carries no per-call argument (the poller\n * re-reads the same source every cadence). Capture any target in the Cmd\n * literal the consumer's `onTick` returns.\n */\n readonly onTick: () => Cmd;\n /**\n * Backoff policy for FAILED ticks. Omit to use `defaultRetryPolicy`\n * (full-jitter, 5 attempts). A success resets the retry counter, so the\n * policy only governs consecutive failures.\n *\n * Any bound the `../retry-backoff` union admits: a count (`RetryPolicy`), a\n * wall-clock outage budget (`DurationRetryPolicy`), or explicit\n * `unbounded: true`. The duration bound needs no extra wiring here — every\n * failure already arrives through `tickErr(state, error, at)` carrying the\n * instant it was observed, so the streak clock is fed from the Msg's own\n * data and the poller never reads a clock of its own (invariant 2).\n */\n readonly retry?: AnyRetryPolicy;\n /**\n * Optional dedupe key derived from a tick result. When provided, a result\n * whose key was already `seen` records the observation but emits NO fresh\n * `onTick` follow-up — the side effect runs once per distinct observation.\n * Omit to treat every tick result as fresh (no dedupe). The dedupe store is\n * bounded by `dedupeTtlMs` if set; otherwise it grows with distinct keys.\n */\n readonly dedupeKey?: (result: R) => string;\n /**\n * TTL for the dedupe store, in milliseconds. Only meaningful when\n * `dedupeKey` is set. An observation older than `dedupeTtlMs` is forgotten,\n * so a key seen long ago is treated as fresh again. Omit for an unbounded\n * dedupe window (every distinct key remembered for the poller's lifetime).\n */\n readonly dedupeTtlMs?: number;\n}\n\n/**\n * The bookkeeping every poller phase carries in common, split out so each arm of\n * the `PollerState` discriminated union names only what it adds (`nextAtMs` on\n * `polling`) — the same discipline the sibling `PaginatorState` uses.\n *\n * - `tick` — count of ticks recorded so far (success or failure). Doubles\n * as an observability cursor and a stable per-tick discriminant.\n * - `lastResult` — the most recent successful observation, or `undefined`\n * before the first success. `until` typically reads it (via the\n * consumer's Model), but the knob never interprets it.\n * - `retry` — the `../retry-backoff` counter for consecutive failures.\n * Reset to `initRetry()` on every success. Once a failure has\n * been recorded it is a `TimedRetryState` (it carries the\n * streak's `firstFailureAtMs`, minted from `tickErr`'s `at`);\n * the field is typed at the `RetryState` supertype because a\n * poller that has never failed has no streak, and a slice\n * persisted before the origin existed rehydrates without one.\n * - `dedupe` — the `../idempotency` store backing `dedupeKey`. An empty,\n * unbounded store when `dedupeKey` is unset (never written).\n */\ninterface PollerCore<R> {\n readonly tick: number;\n readonly lastResult?: R;\n readonly retry: RetryState;\n readonly dedupe: IdempotencyStore<true>;\n}\n\n/**\n * The Model field the poller knob owns — its visible slice (the knob principle:\n * managed state lives in the Model, never a closure, so it is durable and\n * replayable). A discriminated union on `phase` so the armed-timer target lives\n * ONLY where it is meaningful and impossible combinations are unrepresentable\n * (pattern 11), the sibling `PaginatorState` idiom:\n *\n * - `polling` — ticks are scheduled. Carries `nextAtMs`, the ABSOLUTE target\n * the interval/backoff timer is armed against (persisted so a\n * resumed poller re-arms the same moment), or `null` before the\n * first `start` primes it.\n * - `done` — `until` held; the poll is finished. Carries NO `nextAtMs` — a\n * finished poll can never sit on an armed target.\n * - `gave_up` — backoff exhausted `maxAttempts`. Carries NO `nextAtMs` either.\n *\n * Because only `polling` carries `nextAtMs`, `subs` guards on `phase` alone to\n * decide whether a timer is desired — a `done`/`gave_up` state with a live target\n * (the old flat record's hazard) simply cannot be built.\n */\nexport type PollerState<R> =\n | (PollerCore<R> & {\n readonly phase: \"polling\";\n readonly nextAtMs: number | null;\n })\n | (PollerCore<R> & { readonly phase: \"done\" })\n | (PollerCore<R> & { readonly phase: \"gave_up\" });\n\n/**\n * The poller Sub type — a `../deadline` Sub under a fixed id family. One id per\n * knob instance keeps the reconcile pass from churning the timer: the same id\n * across transitions means \"same armed deadline\", even as `nextAtMs` advances.\n */\nexport type PollerSub = DeadlineSub;\n\n/**\n * The Sub id the poller arms its tick deadline under, keyed on the absolute\n * target. The id ROTATES per target because a one-shot deadline timer is\n * exhausted once it fires and the substrate reconciles subs by `id` alone: a\n * fixed id would leave the fired timer registered and never re-arm, so the\n * cadence would die after exactly one tick. Encoding `atMs` in the id makes each\n * cadence/backoff target a distinct identity — the reconcile pass retires the\n * spent timer and arms the next — while a resumed poller re-arms the SAME id\n * from its persisted `nextAtMs` (eviction is idempotent).\n *\n * A single poller machine arms exactly one tick timer at a time; a consumer\n * running several pollers on one machine namespaces them by composing the knob\n * under distinct slices (a future multi-poller config would thread a prefix).\n */\nfunction pollerSubId(atMs: number): string {\n return `poller:tick:${atMs}`;\n}\n\n/**\n * The knob handle returned by `createPoller`. Spread its hooks into your\n * machine: `init()` seeds the slice, `start(...)` arms the first tick,\n * `tickResult` / `tickErr` are the two update verbs, and `subs(...)` returns the\n * timer Sub while the poller is live.\n *\n * Pure-ops + subs module: there is no `handlers(ports)` cell. The poller's I/O\n * is the consumer's own `onTick` Cmd run by the consumer's `interpret` — the\n * knob never owns a port. (Compare `../deadline`, which ships a `subscribe`\n * cell; the poller reuses `../deadline`'s, so it has nothing of its own to\n * splice into `interpret`.)\n */\nexport interface Poller<\n // State is a phantom-style parameter — present so consumers can spell\n // `Poller<MyState, MyResult>` matching `createPoller<State, R>`. It is\n // consumed by the config's `until` predicate, not by any handle method, so\n // it does not appear in the interface body. Same discipline as\n // `BoundMachine`'s `Ctx`.\n // biome-ignore lint/correctness/noUnusedVariables: phantom type parameter (see above)\n State,\n R,\n> {\n /** Seed the Model slice. Idle until `start(...)` arms the first tick. */\n init(): PollerState<R>;\n /**\n * Arm the first tick. `at` is the current clock (carried in from the Msg /\n * runtime that kicks the poller off — never read inside the verb). Returns\n * the slice with the first deadline target set to `at + everyMs` and emits NO\n * Cmd. The cadence has ONE source — the deadline Sub — so `start` does not\n * perform the first observation itself (an immediate `onTick` here would race\n * the timer and poll as fast as the source responds). The first observation\n * fires when the deadline crosses `at + everyMs` and the consumer routes\n * `deadline_exceeded` → `tick`.\n */\n start(\n state: PollerState<R>,\n at: number,\n ): readonly [PollerState<R>, readonly Cmd[]];\n /**\n * Perform one observation — the cadence verb. The consumer routes the\n * deadline Sub's `deadline_exceeded` Msg here; `tick` emits the single\n * `config.onTick()` Cmd that goes and reads the source. This is the ONLY\n * next-tick mechanism: the timer fires, the consumer calls `tick`, the fetch\n * runs, and the observation comes back through `tickResult` / `tickErr`\n * (which re-arm the next deadline). A no-op on a finished poller — a stale\n * deadline fire racing a terminal transition the reconcile pass has not yet\n * retired emits nothing.\n *\n * Takes no `at`: `tick` does not touch the schedule (the just-fired deadline\n * is exhausted; the next target is set when the result lands), so it needs no\n * clock — keeping it trivially pure.\n */\n tick(state: PollerState<R>): readonly [PollerState<R>, readonly Cmd[]];\n /**\n * Record a successful tick observation at clock `at`.\n *\n * - Resets the retry counter (a success clears consecutive-failure state).\n * - Stores `result` as `lastResult` and bumps `tick`.\n * - Re-arms the next tick at the absolute target `at + everyMs` (the steady\n * cadence). It emits NO cadence `onTick` — the next observation fires from\n * the deadline Sub via `tick`, never immediately from a result.\n * - Dedupe (opt-in, orthogonal to cadence): when `dedupeKey` is set, a FRESH\n * observation emits one `onTick` FOLLOW-UP (the consumer's \"run the\n * downstream side effect once per distinct observation\" hook) and a\n * DUPLICATE key records the sighting but suppresses the follow-up. With no\n * `dedupeKey`, `tickResult` emits nothing — the cadence `tick` is the only\n * `onTick`, so an unconfigured poller never double-fetches.\n * - UNLESS `untilHeld` is `true` (the consumer evaluated `config.until`\n * against the post-result Model) — then the poller enters `\"done\"`,\n * disarms (`nextAtMs: null`), and emits no Cmd, dedupe or otherwise.\n *\n * `untilHeld` is passed in rather than computed here because `until` reads\n * the consumer's WHOLE Model, which this verb does not hold — the consumer\n * evaluates it at the call site after folding `result` into its Model and\n * passes the boolean. (`subs` re-checks `phase` to decide arming, so a\n * mis-passed `untilHeld` cannot leave a \"done\" poller arming timers.)\n */\n tickResult(\n state: PollerState<R>,\n result: R,\n at: number,\n untilHeld: boolean,\n ): readonly [PollerState<R>, readonly Cmd[]];\n /**\n * Record a FAILED tick at clock `at`. Backs off instead of holding the\n * steady cadence:\n *\n * - Advances the retry counter (`recordFailure`).\n * - If `shouldRetry` still permits another attempt, arms the next tick at\n * `at + nextDelayMs(retry, policy)` (the backoff curve) and emits NO Cmd —\n * the retry observation fires when that backoff deadline crosses and the\n * consumer routes `deadline_exceeded` → `tick`. The timer is the single\n * next-tick mechanism, on the backoff curve here instead of `everyMs`.\n * - If backoff is exhausted (`!shouldRetry`), enters `\"gave_up\"`, disarms\n * (`nextAtMs: null`), and emits no Cmd.\n *\n * `at` is both the backoff anchor AND the streak clock: it is passed to\n * `recordFailure` (starting / preserving `firstFailureAtMs`) and to\n * `shouldRetry`, so a `DurationRetryPolicy` on `config.retry` is honoured\n * with no extra wiring. Under a duration bound the poller keeps retrying\n * however many attempts the outage takes, and stops at the declared\n * wall-clock budget measured from the streak's FIRST failure.\n *\n * The backoff jitter uses the `rng` injected ONCE at `createPoller` (default\n * `Math.random` at the effect boundary, a fixed value in tests); the verb\n * body never names the global RNG, so a poller built with a fixed `rng`\n * replays a failure-and-backoff run bit-for-bit.\n */\n tickErr(\n state: PollerState<R>,\n error: unknown,\n at: number,\n ): readonly [PollerState<R>, readonly Cmd[]];\n /**\n * The pre-wired subscriptions cell. Returns the single tick-deadline Sub\n * while the poller is `\"polling\"` with an armed `nextAtMs`; returns `[]`\n * once `\"done\"` / `\"gave_up\"` (the timer disarms) or before `start`. Assign\n * directly: `subscriptions: (s) => poll.subs(s.poll)`.\n *\n * The `DeadlineSub` it returns is consumed by `../deadline`'s\n * `subscribeDeadline` cell, which the consumer wires into `subscribe` —\n * the poller does not redraw the timer lifecycle.\n */\n subs(state: PollerState<R>): readonly Sub[];\n}\n\n/**\n * Build a poller knob from `config`. Returns the hook bag to spread into a\n * machine (see the module doc-comment for the wiring). Pure factory — no clock\n * read, no timer; everything happens when the consumer calls the returned\n * verbs / `subs` with an injected `at`.\n *\n * `rng` is injected ONCE here (default `Math.random` at the effect boundary, a\n * fixed value in tests) and threaded internally into the backoff jitter. No\n * verb body ever names the global RNG, so a poller built with a fixed `rng`\n * replays a failure-and-backoff run bit-for-bit (invariant 2: the verbs are\n * pure given the injected `rng`).\n */\nexport function createPoller<State, R>(\n config: PollerConfig<State, R>,\n rng: () => number = Math.random,\n): Poller<State, R> {\n // Re-affirm the `[0, 1)` contract at the seam where the injected generator\n // enters the backoff math (brand from `../retry-backoff`). The factory's\n // public param stays a plain `() => number` so transitive callers are\n // unchanged; `asRng` is the single point where it's branded for `nextDelayMs`.\n const rngBranded = asRng(rng);\n const policy: AnyRetryPolicy = config.retry ?? defaultRetryPolicy;\n\n // The dedupe store seam, bound once. The poller delegates its dedupe-window\n // reads (`memory.isSeen`) and the per-sighting touch (`memory.remember`) to\n // the verbs, so the `IdempotencyStore` shape stays insulated.\n const memory = idempotencyMemory<true>();\n\n function init(): PollerState<R> {\n return {\n phase: \"polling\",\n tick: 0,\n retry: initRetry(),\n nextAtMs: null,\n // The dedupe store is created regardless; it is only WRITTEN when\n // `dedupeKey` is set, so an unconfigured poller carries an inert empty\n // store (no per-op cost, JSON-serializable, durable like the rest of\n // the slice).\n dedupe: memory.init({ ttlMs: config.dedupeTtlMs }),\n };\n }\n\n function start(\n state: PollerState<R>,\n at: number,\n ): readonly [PollerState<R>, readonly Cmd[]] {\n // Arm the first deadline at `at + everyMs` and emit NOTHING. The cadence has\n // ONE source — the `everyMs` deadline Sub. `start` does not perform the\n // first observation itself: that would be a second, immediate next-tick\n // mechanism racing the timer (it would fire as fast as the source responds,\n // never honoring `everyMs`). The first observation fires when the deadline\n // crosses `at + everyMs` and the consumer routes `deadline_exceeded` → the\n // `tick` verb (which emits `onTick`). No drift, no double-fire. Build the\n // `polling` arm explicitly so a (re)start from a terminal state adopts a live\n // target rather than spreading a terminal arm that never had one.\n return [\n {\n phase: \"polling\",\n tick: state.tick,\n lastResult: state.lastResult,\n retry: state.retry,\n nextAtMs: at + config.everyMs,\n dedupe: state.dedupe,\n },\n [],\n ];\n }\n\n function tick(\n state: PollerState<R>,\n ): readonly [PollerState<R>, readonly Cmd[]] {\n // The deadline fired. Perform the observation — this is the ONLY place the\n // `onTick` Cmd is emitted, so the timer is the single next-tick mechanism.\n // No schedule change here: the just-fired deadline is exhausted, and the\n // NEXT target is re-armed when the observation lands (`tickResult` /\n // `tickErr`). A no-op on a finished poller (a stale fire racing a terminal\n // transition the reconcile pass has not yet retired).\n if (state.phase !== \"polling\") return [state, []];\n return [state, [config.onTick()]];\n }\n\n function tickResult(\n state: PollerState<R>,\n result: R,\n at: number,\n untilHeld: boolean,\n ): readonly [PollerState<R>, readonly Cmd[]] {\n // A success always clears consecutive-failure state and records the datum.\n // The next arm is built explicitly (not spread from `state`) so `done`\n // carries no `nextAtMs` — a finished poll can never sit on an armed target.\n const tick = state.tick + 1;\n\n // Stop condition first: if the consumer's `until` held against the\n // post-result Model, we are DONE — disarm and emit nothing, regardless of\n // dedupe (the final observation is still recorded as `lastResult`).\n if (untilHeld) {\n return [\n {\n phase: \"done\",\n tick,\n lastResult: result,\n retry: initRetry(),\n dedupe: state.dedupe,\n },\n [],\n ];\n }\n\n // Re-arm the steady cadence: the next observation fires when THIS deadline\n // (`at + everyMs`) crosses and the consumer routes `deadline_exceeded` →\n // `tick`. `tickResult` itself never emits the cadence `onTick` — the timer\n // is the single next-tick mechanism (recording a result is not a reason to\n // immediately re-fetch; that would poll as fast as the source responds).\n const next: PollerState<R> = {\n phase: \"polling\",\n tick,\n lastResult: result,\n retry: initRetry(),\n nextAtMs: at + config.everyMs,\n dedupe: state.dedupe,\n };\n\n // Dedupe is an OPT-IN follow-up, orthogonal to the cadence above. When\n // `dedupeKey` is set, a FRESH observation emits one `onTick` follow-up (the\n // consumer's \"run the downstream side effect once per distinct observation\"\n // hook); a DUPLICATE key records the sighting but suppresses that follow-up.\n // With no `dedupeKey` there is no follow-up at all — the cadence `tick` is\n // the only `onTick`, so an unconfigured poller never double-fetches.\n if (!config.dedupeKey) return [next, []];\n\n const key = config.dedupeKey(result);\n const suppress = memory.isSeen(next.dedupe, key, at);\n // `remember` refreshes the key's TTL clock on every sighting (the\n // idempotency \"touch\" semantics), so a steadily-repeating observation never\n // ages out mid-poll. The stored value is the unit `true` — the poller only\n // needs presence, not a payload.\n const deduped: PollerState<R> = {\n ...next,\n dedupe: memory.remember(next.dedupe, key, true, at),\n };\n return [deduped, suppress ? [] : [config.onTick()]];\n }\n\n function tickErr(\n state: PollerState<R>,\n error: unknown,\n at: number,\n ): readonly [PollerState<R>, readonly Cmd[]] {\n // `at` is the failure's OBSERVATION instant, already carried in from the\n // Msg — so it doubles as the streak clock a duration bound is measured\n // against. The first failure of a streak sets `firstFailureAtMs`; every\n // later one preserves it, and a success resets to `initRetry()` (dropping\n // the origin with it). Nothing here reads a clock: `at` is Msg data,\n // exactly as `rng` is factory-injected data (invariant 2).\n const retry = recordFailure(state.retry, error, at);\n const tick = state.tick + 1;\n\n // Backoff exhausted → give up: emit nothing. `shouldRetry` reads the\n // just-advanced `retry`, so under a count bound the (maxAttempts)-th\n // failure is the one that stops the poller, and under a duration bound the\n // first failure observed at or past `firstFailureAtMs + maxElapsedMs` is.\n // The `gave_up` arm carries no `nextAtMs` — there is no next attempt to arm.\n if (!shouldRetry(retry, policy, at)) {\n return [\n {\n phase: \"gave_up\",\n tick,\n lastResult: state.lastResult,\n retry,\n dedupe: state.dedupe,\n },\n [],\n ];\n }\n\n // Back off: arm the next attempt at `at + backoff(retry)` rather than the\n // steady cadence, and emit NOTHING. The retry observation fires when this\n // backoff deadline crosses and the consumer routes `deadline_exceeded` →\n // `tick` — the timer is the single next-tick mechanism, on the backoff\n // curve here instead of `everyMs`. `rng` is the value injected once at\n // `createPoller`; the verb body never names the global RNG, so a fixed\n // `rng` makes the backoff target replay bit-for-bit.\n const delayMs = nextDelayMs(retry, policy, rngBranded);\n return [\n {\n phase: \"polling\",\n tick,\n lastResult: state.lastResult,\n retry,\n nextAtMs: at + delayMs,\n dedupe: state.dedupe,\n },\n [],\n ];\n }\n\n function subs(state: PollerState<R>): readonly Sub[] {\n // Arm the tick deadline only while live. `phase` is the authority (so a\n // mis-passed `untilHeld` into `tickResult` cannot resurrect a finished\n // poller); `nextAtMs` is the absolute target `../deadline` translates to a\n // remaining delay at subscribe time — a resumed poller re-arms the exact\n // persisted moment.\n if (state.phase !== \"polling\" || state.nextAtMs === null) return [];\n // The Sub id ROTATES with the target. A one-shot deadline timer is\n // exhausted once it fires, and the substrate's reconcile pass keys identity\n // by `id` alone — a fixed id would leave the fired timer in the registry and\n // NEVER re-arm for the next cadence (cadence dies after one tick). Folding\n // `nextAtMs` into the id makes each successive target a distinct identity:\n // the reconcile pass retires the spent timer (old id absent from the desired\n // set) and arms the next one (new id). A resumed poller re-arms the SAME id\n // from its persisted `nextAtMs`, so eviction is idempotent.\n return [deadlineSub(pollerSubId(state.nextAtMs), state.nextAtMs)];\n }\n\n return { init, start, tick, tickResult, tickErr, subs };\n}\n"]}
|
package/dist/prediction/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { NO_ACK, ack, initAck, nextSeq, partitionByAck, reconcile, tagSeq } from '../chunk-
|
|
2
|
-
import '../chunk-
|
|
1
|
+
export { NO_ACK, ack, initAck, nextSeq, partitionByAck, reconcile, tagSeq } from '../chunk-JFFVBEDI.js';
|
|
2
|
+
import '../chunk-PBDYW2TU.js';
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
|
4
4
|
//# sourceMappingURL=index.js.map
|
package/dist/pure/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { Ack, AckPartition, NO_ACK, Seq, SeqTagged, ack, initAck, nextSeq, partitionByAck, reconcile, tagSeq } from '../prediction/index.js';
|
|
2
|
-
export { C as Cmd,
|
|
2
|
+
export { C as Cmd, d as ContextFree, D as DepKeyedSub, e as Dispose, f as Identity, c as Interpret, I as InterpretDetached, M as Machine, n as NoCellError, N as NoCtx, P as Port, b as PortEmitter, R as Reducer, S as Sub, a as SubId, g as Subscribe, h as SyncReturn, T as Transitions, U as UpdateForm, j as detectUpdateForm, k as foldMsgs, l as formOf, s as structuralHash, o as subId } from '../core-42kapW0n.js';
|
package/dist/pure/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { NO_ACK, ack, initAck, nextSeq, partitionByAck, reconcile, tagSeq } from '../chunk-
|
|
2
|
-
export { Cmd, NoCellError, detectUpdateForm, foldMsgs, formOf, subId } from '../chunk-
|
|
1
|
+
export { NO_ACK, ack, initAck, nextSeq, partitionByAck, reconcile, tagSeq } from '../chunk-JFFVBEDI.js';
|
|
2
|
+
export { Cmd, NoCellError, detectUpdateForm, foldMsgs, formOf, structuralHash, subId } from '../chunk-PBDYW2TU.js';
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
|
4
4
|
//# sourceMappingURL=index.js.map
|
|
@@ -95,8 +95,8 @@ declare function initWindow(windowMs: number, limit: number): SlidingWindow;
|
|
|
95
95
|
* after pruning, else `[pruned, false]` — the pruned window (capacity recovers
|
|
96
96
|
* as old hits age out) with nothing appended.
|
|
97
97
|
*
|
|
98
|
-
* The cutoff is half-open: a hit exactly `windowMs` old is
|
|
99
|
-
*
|
|
98
|
+
* The cutoff is half-open (see {@link prune}): a hit exactly `windowMs` old is
|
|
99
|
+
* pruned, so a hit recorded at `t` no longer counts at `t + windowMs`. That
|
|
100
100
|
* makes the window's capacity fully recover `windowMs` after the last hit, the
|
|
101
101
|
* behavior tests pin.
|
|
102
102
|
*
|
package/dist/rate-limit/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { initBucket, initWindow, record, refill, remaining, tryConsume } from '../chunk-
|
|
1
|
+
export { initBucket, initWindow, record, refill, remaining, tryConsume } from '../chunk-S2EGRPCM.js';
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { Store, Runtime } from '../
|
|
2
|
-
import { C as Cmd, S as Sub, M as Machine } from '../core-
|
|
1
|
+
import { S as Store, R as Runtime } from '../runtime-types-CukV8kMX.js';
|
|
2
|
+
import { C as Cmd, S as Sub, M as Machine } from '../core-42kapW0n.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
+
* @packageDocumentation
|
|
5
6
|
* @demlik/tea/react — React host adapter for `@demlik/tea`.
|
|
6
7
|
*
|
|
7
8
|
* The thinnest possible adapter that makes a tea machine consumable from a
|
|
@@ -31,6 +32,10 @@ import { C as Cmd, S as Sub, M as Machine } from '../core-BmlJOvxM.js';
|
|
|
31
32
|
* **Identity matters.** The runtime is memoized on `[machine, opts.ctx,
|
|
32
33
|
* opts.store]`. A new `ctx` reference rebuilds the runtime. Always `useMemo`
|
|
33
34
|
* (or otherwise stabilize) the ctx at the call site. See README.
|
|
35
|
+
*
|
|
36
|
+
* Forgetting to is no longer silent: the replaced runtime's `stop()` reports
|
|
37
|
+
* `RuntimeDiscardedError` under `phase: "discard"` when Cmds were still in
|
|
38
|
+
* flight, which the substrate's default sink warns about (issue #365).
|
|
34
39
|
*/
|
|
35
40
|
interface UseMachineOpts<S, Ctx> {
|
|
36
41
|
ctx: Ctx;
|
|
@@ -46,7 +51,9 @@ interface UseMachineOpts<S, Ctx> {
|
|
|
46
51
|
* runtime.getState)` — tearing-free under React 18 concurrent rendering.
|
|
47
52
|
* - On unmount (or any dep change), `useEffect` cleanup calls
|
|
48
53
|
* `runtime.stop()` — drains the queue, runs every active sub cleanup,
|
|
49
|
-
* flushes state
|
|
54
|
+
* flushes state, and — if Cmds were still in flight — reports
|
|
55
|
+
* `RuntimeDiscardedError` (`phase: "discard"`) rather than dropping them
|
|
56
|
+
* silently.
|
|
50
57
|
*
|
|
51
58
|
* Returns `[state, dispatch]` shaped like `useReducer`.
|
|
52
59
|
*/
|
package/dist/react/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { run } from '../chunk-
|
|
2
|
-
import '../chunk-
|
|
1
|
+
import { run } from '../chunk-D4NJBQAH.js';
|
|
2
|
+
import '../chunk-PBDYW2TU.js';
|
|
3
3
|
import { useMemo, useRef, useReducer, useSyncExternalStore, useEffect } from 'react';
|
|
4
4
|
|
|
5
5
|
function useMachine(machine, opts) {
|
package/dist/react/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/index.ts"],"names":[],"mappings":";;;;
|
|
1
|
+
{"version":3,"sources":["../../src/react/index.ts"],"names":[],"mappings":";;;;AA4EO,SAAS,UAAA,CAOd,SACA,IAAA,EACgC;AAMhC,EAAA,MAAM,MAAM,IAAA,CAAK,GAAA;AACjB,EAAA,MAAM,QAAQ,IAAA,CAAK,KAAA;AAMnB,EAAA,MAAM,OAAA,GAAU,OAAA;AAAA,IACd,MAAM,GAAA,CAAI,OAAA,EAAS,EAAE,GAAA,EAAK,OAAO,CAAA;AAAA,IACjC,CAAC,OAAA,EAAS,GAAA,EAAK,KAAK;AAAA,GACtB;AAeA,EAAA,MAAM,QAAA,GAAW,OAA6B,IAAI,CAAA;AAClD,EAAA,MAAM,GAAG,UAAU,CAAA,GAAI,WAAW,CAAC,IAAA,KAAiB,IAAA,GAAO,CAAA,EAAG,CAAC,CAAA;AAa/D,EAAA,MAAM,gBAAA,GAAmB,OAAA;AAAA,IACvB,MAAM,OAAA,CAAQ,IAAA,CAAK,IAAA,EAAM,GAAG,EAAE,CAAC,CAAA;AAAA,IAC/B,CAAC,SAAS,GAAG;AAAA,GACf;AAMA,EAAA,MAAM,WAAA,GAAc,MAClB,QAAA,CAAS,OAAA,KAAY,OAAO,QAAA,CAAS,OAAA,CAAQ,UAAS,GAAI,gBAAA;AAE5D,EAAA,MAAM,KAAA,GAAQ,oBAAA;AAAA,IACZ,OAAA,CAAQ,SAAA;AAAA,IACR,WAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,IAAA,GAAO,IAAA;AAMX,IAAA,OAAA,CAAQ,KAAA,CAAM,IAAA;AAAA,MACZ,CAAC,CAAA,KAAM;AACL,QAAA,IAAI,IAAA,EAAM;AACR,UAAA,QAAA,CAAS,OAAA,GAAU,CAAA;AACnB,UAAA,UAAA,EAAW;AAAA,QACb;AAAA,MACF,CAAA;AAAA,MACA,MAAM;AAAA,MAAC;AAAA,KACT;AACA,IAAA,OAAO,MAAM;AACX,MAAA,IAAA,GAAO,KAAA;AACP,MAAA,QAAA,CAAS,OAAA,GAAU,IAAA;AAInB,MAAA,OAAA,CAAQ,IAAA,EAAK,CAAE,KAAA,CAAM,MAAM;AAAA,MAAC,CAAC,CAAA;AAAA,IAC/B,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AAEZ,EAAA,OAAO,CAAC,KAAA,EAAO,OAAA,CAAQ,QAAQ,CAAA;AACjC;AAeO,SAAS,WACd,OAAA,EACgC;AAChC,EAAA,MAAM,KAAA,GAAQ,oBAAA;AAAA,IACZ,OAAA,CAAQ,SAAA;AAAA,IACR,OAAA,CAAQ,QAAA;AAAA,IACR,OAAA,CAAQ;AAAA,GACV;AACA,EAAA,OAAO,CAAC,KAAA,EAAO,OAAA,CAAQ,QAAQ,CAAA;AACjC","file":"index.js","sourcesContent":["/**\n * @packageDocumentation\n * @demlik/tea/react — React host adapter for `@demlik/tea`.\n *\n * The thinnest possible adapter that makes a tea machine consumable from a\n * React component. Three rules (PRD §\"React adapter\"):\n *\n * 1. The runtime is the source of truth, NOT React state. We subscribe via\n * `useSyncExternalStore(runtime.subscribe, runtime.getState,\n * runtime.getState)`. React renders are driven by runtime state changes;\n * React never owns the state.\n * 2. The runtime is memoized per component-mount, not per render. The deps\n * array is `[machine, opts.ctx, opts.store]` — IDENTITY, not value. See\n * `README.md` for the ctx-identity footgun + fix.\n * 3. Concurrent React is the design target. `useSyncExternalStore` is the\n * React-blessed primitive for tearing-free external state.\n *\n * Client-only in v1 — components that call `useMachine` MUST be `\"use client\"`.\n * Server Components consume data via Relay/RSC as today; tea machines own\n * client-side stateful flows. No RSC streaming, no server-side state\n * hydration. Crossing this boundary is a v2 concern.\n */\n\n\"use client\";\n\nimport {\n useEffect,\n useMemo,\n useReducer,\n useRef,\n useSyncExternalStore,\n} from \"react\";\nimport {\n type BootingRuntime,\n type Cmd,\n type Machine,\n type Runtime,\n run,\n type Store,\n type Sub,\n} from \"../index\";\n\n/**\n * Options passed to `useMachine`. The shape is intentionally minimal — `ctx`\n * is required (every machine has one), `store` is optional (omit it for\n * volatile-state machines).\n *\n * **Identity matters.** The runtime is memoized on `[machine, opts.ctx,\n * opts.store]`. A new `ctx` reference rebuilds the runtime. Always `useMemo`\n * (or otherwise stabilize) the ctx at the call site. See README.\n *\n * Forgetting to is no longer silent: the replaced runtime's `stop()` reports\n * `RuntimeDiscardedError` under `phase: \"discard\"` when Cmds were still in\n * flight, which the substrate's default sink warns about (issue #365).\n */\nexport interface UseMachineOpts<S, Ctx> {\n ctx: Ctx;\n store?: Store<S>;\n}\n\n/**\n * Build and own a `Runtime<S, M>` for the lifetime of the component mount.\n *\n * - Builds via `useMemo(() => run(machine, opts), [machine, opts.ctx,\n * opts.store])` — runtime is recreated when ANY of the three identities\n * change.\n * - Subscribes via `useSyncExternalStore(runtime.subscribe, runtime.getState,\n * runtime.getState)` — tearing-free under React 18 concurrent rendering.\n * - On unmount (or any dep change), `useEffect` cleanup calls\n * `runtime.stop()` — drains the queue, runs every active sub cleanup,\n * flushes state, and — if Cmds were still in flight — reports\n * `RuntimeDiscardedError` (`phase: \"discard\"`) rather than dropping them\n * silently.\n *\n * Returns `[state, dispatch]` shaped like `useReducer`.\n */\nexport function useMachine<\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 opts: UseMachineOpts<S, Ctx>,\n): [S, (msg: M) => Promise<void>] {\n // Deps are literal: machine identity, ctx identity, store identity. NOT\n // `[opts]` (would rebuild every render — callers pass fresh objects).\n // We pass explicit fields to `run()` so the closure captures only the\n // deps in the array — keeps biome's `useExhaustiveDependencies` happy\n // without lying about what the memo actually depends on.\n const ctx = opts.ctx;\n const store = opts.store;\n // `run()` returns a `BootingRuntime<S, M>` SYNCHRONOUSLY (issue #45) — exactly\n // what `useMemo` needs. We never `await` here: awaiting would force an async\n // memo, a null first-commit state, and a resolved-flag dance. Instead we hold\n // the booting handle and capture the booted `Runtime` (the only thing with a\n // total `getState`) once `ready` resolves.\n const booting = useMemo<BootingRuntime<S, M>>(\n () => run(machine, { ctx, store }),\n [machine, ctx, store],\n );\n\n // Captures the booted `Runtime` once `ready` resolves. Until then it is\n // `null` and `getSnapshot` serves `preliminaryState`. Reset whenever the\n // booting handle identity changes (a new machine/ctx/store mount).\n //\n // The boot race the paired `bootedTick` bump exists for: the substrate's\n // boot fanout fires BEFORE `ready` resolves (fireListeners runs inside the\n // boot step; `ready` chains off the boot promise), so at notification time\n // this ref is still null and `getSnapshot` returns the same\n // `preliminaryState` reference — React sees an unchanged snapshot and\n // skips the re-render. Nothing re-notifies after the ref is set, so a\n // store-backed mount would stay stuck on the preliminary state until the\n // next unrelated transition. Bumping a reducer when we capture the booted\n // runtime forces the render that swaps preliminary → real state.\n const readyRef = useRef<Runtime<S, M> | null>(null);\n const [, markBooted] = useReducer((tick: number) => tick + 1, 0);\n\n // Preliminary state computed sync from `machine.init(null, ctx)`. This is\n // the snapshot React sees BEFORE the runtime finishes booting (matters\n // only when `opts.store` is present — without a store, the substrate\n // already sets state synchronously inside `run()`, so the boot fanout that\n // captures `readyRef` lands on the same microtask as the first commit).\n //\n // We deliberately recompute on each render but cache by `[machine,\n // opts.ctx]` — `init` is pure by convention, so cost is negligible. The\n // cached snapshot is also identity-stable across rerenders when the deps\n // don't change, which `useSyncExternalStore` rewards (no spurious\n // re-renders).\n const preliminaryState = useMemo<S>(\n () => machine.init(null, ctx)[0],\n [machine, ctx],\n );\n\n // `getSnapshot` reads the booted runtime if `ready` has resolved (the ref is\n // populated), else falls back to the preliminary state. No try/catch dance\n // anymore — pre-boot there is simply no booted `Runtime` to read from, so\n // the fallback is structural, not exception-driven.\n const getSnapshot = (): S =>\n readyRef.current !== null ? readyRef.current.getState() : preliminaryState;\n\n const state = useSyncExternalStore(\n booting.subscribe,\n getSnapshot,\n getSnapshot,\n );\n\n useEffect(() => {\n let live = true;\n // Capture the booted runtime when `ready` resolves, then force the\n // render that swaps preliminary → real state (see the boot-race note on\n // `readyRef` — the boot fanout fires too early to do it for us). Boot\n // rejections are swallowed: a failed boot surfaces on every `dispatch`,\n // which is where the consumer handles it.\n booting.ready.then(\n (r) => {\n if (live) {\n readyRef.current = r;\n markBooted();\n }\n },\n () => {},\n );\n return () => {\n live = false;\n readyRef.current = null;\n // Fire-and-forget — `stop()` is async but `useEffect` cleanup is sync.\n // The runtime swallows in-flight rejections internally, so this never\n // rejects in practice; we still attach `.catch` for safety.\n booting.stop().catch(() => {});\n };\n }, [booting]);\n\n return [state, booting.dispatch];\n}\n\n/**\n * Lower-level escape hatch: consume an externally-built `Runtime<S, M>`.\n *\n * Use this when the runtime is owned by something OTHER than the component —\n * a parent component, a test harness, a singleton. **The component does NOT\n * call `runtime.stop()` on unmount** — the caller owns the lifecycle.\n *\n * Primary use case in this repo: component tests. Build a runtime with\n * `@demlik/tea/mem` and pre-seed it via `replay`, then mount the component with\n * `useRuntime(testRuntime)` to render in any phase.\n *\n * Returns `[state, dispatch]` — same shape as `useMachine`.\n */\nexport function useRuntime<S, M extends { type: string }>(\n runtime: Runtime<S, M>,\n): [S, (msg: M) => Promise<void>] {\n const state = useSyncExternalStore(\n runtime.subscribe,\n runtime.getState,\n runtime.getState,\n );\n return [state, runtime.dispatch];\n}\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CircuitConfig, RateLimitConfig, DeadlineConfig, ResilientPorts, RunCmd, FailMsg, SucceedMsg } from '../resilient-call/index.js';
|
|
2
2
|
import { TtlCache } from '../cache/index.js';
|
|
3
|
-
import { C as Cmd } from '../core-
|
|
3
|
+
import { C as Cmd } from '../core-42kapW0n.js';
|
|
4
4
|
import { PaginatedWalkState, FetchPageCmd, PageOkMsg, PageErrMsg, PaginatedWalkTimerMsg } from '../paginated-walk/index.js';
|
|
5
5
|
import { RetryPolicy } from '../retry-backoff/index.js';
|
|
6
6
|
import { DeadlineSub } from '../deadline/index.js';
|
|
@@ -8,7 +8,7 @@ export { deadlineSub, subscribeDeadline } from '../deadline/index.js';
|
|
|
8
8
|
import '../circuit-breaker/index.js';
|
|
9
9
|
import '../protocol-B5YmJr0d.js';
|
|
10
10
|
import '../rate-limit/index.js';
|
|
11
|
-
import '../types-
|
|
11
|
+
import '../types-KbL3W1mj.js';
|
|
12
12
|
import '../paginator/index.js';
|
|
13
13
|
|
|
14
14
|
/**
|
package/dist/reconciler/index.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import { createPaginatedWalk, PAGE_KEY } from '../chunk-
|
|
1
|
+
import { createPaginatedWalk, PAGE_KEY } from '../chunk-JXYYRKD6.js';
|
|
2
2
|
import '../chunk-SINXE47Y.js';
|
|
3
|
-
import '../chunk-
|
|
3
|
+
import '../chunk-B5RAZVXW.js';
|
|
4
4
|
import '../chunk-JELSZN25.js';
|
|
5
|
-
export { deadlineSub, subscribeDeadline } from '../chunk-
|
|
6
|
-
import '../chunk-
|
|
7
|
-
import '../chunk-
|
|
8
|
-
import '../chunk-
|
|
9
|
-
import { initCache, get, set } from '../chunk-
|
|
10
|
-
import '../chunk-
|
|
5
|
+
export { deadlineSub, subscribeDeadline } from '../chunk-RMX4EOQK.js';
|
|
6
|
+
import '../chunk-Q2UM7BXU.js';
|
|
7
|
+
import '../chunk-S2EGRPCM.js';
|
|
8
|
+
import '../chunk-UF4MZSLT.js';
|
|
9
|
+
import { initCache, get, set } from '../chunk-2KNLKQVN.js';
|
|
10
|
+
import '../chunk-LYLDCIPH.js';
|
|
11
11
|
import '../chunk-2YFLOCWX.js';
|
|
12
|
-
import '../chunk-
|
|
13
|
-
import '../chunk-
|
|
12
|
+
import '../chunk-ZJ5AOYBH.js';
|
|
13
|
+
import '../chunk-D4NJBQAH.js';
|
|
14
|
+
import '../chunk-PBDYW2TU.js';
|
|
14
15
|
|
|
15
16
|
// src/reconciler/index.ts
|
|
16
17
|
function createReconciler(config, rng = Math.random) {
|
|
@@ -61,8 +62,8 @@ function createReconciler(config, rng = Math.random) {
|
|
|
61
62
|
const scanned = { ...s, walk: w, actual };
|
|
62
63
|
const cmds = walkCmds;
|
|
63
64
|
if (walk.isComplete(scanned.walk)) {
|
|
64
|
-
const [
|
|
65
|
-
return [
|
|
65
|
+
const [plannedState, applyCmds] = planned(scanned, at);
|
|
66
|
+
return [plannedState, [...cmds, ...applyCmds]];
|
|
66
67
|
}
|
|
67
68
|
return [scanned, cmds];
|
|
68
69
|
}
|
|
@@ -84,9 +85,6 @@ function createReconciler(config, rng = Math.random) {
|
|
|
84
85
|
};
|
|
85
86
|
return applyNext(planning, at);
|
|
86
87
|
}
|
|
87
|
-
function beginPlan(s, at) {
|
|
88
|
-
return planned(s, at);
|
|
89
|
-
}
|
|
90
88
|
function applyNext(s, at) {
|
|
91
89
|
if (s.phase !== "applying") return [s, []];
|
|
92
90
|
let cursor = s.appliedCursor;
|