@demlik/tea 0.1.1 → 0.2.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/dist/agent/index.d.ts +2 -2
- package/dist/agent/index.js +8 -8
- package/dist/authed-call/index.d.ts +3 -3
- package/dist/authed-call/index.js +5 -5
- package/dist/await-terminal/index.d.ts +1 -1
- package/dist/await-terminal/index.js +2 -2
- package/dist/batch-window/index.d.ts +2 -2
- package/dist/batch-window/index.js +4 -4
- package/dist/cache/index.d.ts +2 -2
- package/dist/{chunk-EIZJTYKR.js → chunk-5KVHID6A.js} +4 -4
- package/dist/{chunk-EIZJTYKR.js.map → chunk-5KVHID6A.js.map} +1 -1
- package/dist/{chunk-RVJFGRGK.js → chunk-6GIGQT6C.js} +4 -4
- package/dist/chunk-6GIGQT6C.js.map +1 -0
- package/dist/{chunk-FH5RUIIL.js → chunk-6OUDPSSG.js} +3 -3
- package/dist/{chunk-FH5RUIIL.js.map → chunk-6OUDPSSG.js.map} +1 -1
- package/dist/{chunk-2WZHS4P2.js → chunk-6VDCTR7N.js} +25 -40
- package/dist/chunk-6VDCTR7N.js.map +1 -0
- package/dist/{chunk-BNJYE4AU.js → chunk-DYRKDCJI.js} +46 -16
- package/dist/chunk-DYRKDCJI.js.map +1 -0
- package/dist/{chunk-RGTPPF4U.js → chunk-EGWTT5XJ.js} +3 -3
- package/dist/{chunk-RGTPPF4U.js.map → chunk-EGWTT5XJ.js.map} +1 -1
- package/dist/{chunk-A56GNPI7.js → chunk-EMJTPHI7.js} +5 -23
- package/dist/chunk-EMJTPHI7.js.map +1 -0
- package/dist/{chunk-IFBQJYGI.js → chunk-FOGL3TC7.js} +3 -3
- package/dist/{chunk-IFBQJYGI.js.map → chunk-FOGL3TC7.js.map} +1 -1
- package/dist/{chunk-RVU3RBE6.js → chunk-G2U7DJHL.js} +6 -4
- package/dist/chunk-G2U7DJHL.js.map +1 -0
- package/dist/{chunk-WUV3PSCZ.js → chunk-HCEVHFJ7.js} +2 -2
- package/dist/chunk-HCEVHFJ7.js.map +1 -0
- package/dist/{chunk-EDGDR5XX.js → chunk-IK4S5DJN.js} +3 -3
- package/dist/{chunk-EDGDR5XX.js.map → chunk-IK4S5DJN.js.map} +1 -1
- package/dist/{chunk-EQRUAP2A.js → chunk-J6527VRQ.js} +2 -2
- package/dist/chunk-J6527VRQ.js.map +1 -0
- package/dist/{chunk-TAO4YOLO.js → chunk-M6UZT7HS.js} +3 -3
- package/dist/{chunk-TAO4YOLO.js.map → chunk-M6UZT7HS.js.map} +1 -1
- package/dist/{chunk-QGZW4W3E.js → chunk-SKAMZKTE.js} +3 -3
- package/dist/{chunk-QGZW4W3E.js.map → chunk-SKAMZKTE.js.map} +1 -1
- package/dist/{chunk-TC4HWABA.js → chunk-VFGKTGGC.js} +7 -7
- package/dist/{chunk-TC4HWABA.js.map → chunk-VFGKTGGC.js.map} +1 -1
- package/dist/{chunk-D5EQDLDV.js → chunk-ZDRN3YCE.js} +4 -4
- package/dist/{chunk-D5EQDLDV.js.map → chunk-ZDRN3YCE.js.map} +1 -1
- package/dist/{core-CymXZCeY.d.ts → core-BmlJOvxM.d.ts} +20 -1
- package/dist/deadline/index.d.ts +2 -2
- package/dist/deadline/index.js +3 -3
- package/dist/debounce/index.d.ts +6 -0
- package/dist/debounce/index.js +1 -1
- package/dist/do/index.d.ts +422 -315
- package/dist/do/index.js +173 -149
- package/dist/do/index.js.map +1 -1
- package/dist/{durable-effects-2MDxDs45.d.ts → durable-effects-9LxFpmh3.d.ts} +1 -1
- package/dist/extension/index.d.ts +1 -1
- package/dist/extension/index.js +3 -3
- package/dist/extension/react.js +3 -3
- package/dist/extension/subs/index.d.ts +2 -2
- package/dist/fan-out/index.d.ts +1 -1
- package/dist/idempotent-intake/index.d.ts +1 -1
- package/dist/index.d.ts +20 -3
- package/dist/index.js +2 -2
- package/dist/llm-call/index.d.ts +2 -2
- package/dist/llm-call/index.js +5 -5
- package/dist/machine-viz/index.d.ts +4 -4
- package/dist/machine-viz/index.js +4 -8
- package/dist/machine-viz/index.js.map +1 -1
- package/dist/mem/index.d.ts +1 -1
- package/dist/monitored-run/index.d.ts +2 -2
- package/dist/monitored-run/index.js +5 -5
- package/dist/node/index.d.ts +1 -1
- package/dist/paginated-walk/index.d.ts +2 -2
- package/dist/paginated-walk/index.js +5 -5
- package/dist/parity/index.d.ts +10 -4
- package/dist/parity/index.js +6 -5
- package/dist/parity/index.js.map +1 -1
- package/dist/pbt/arbitraries/index.d.ts +1 -1
- package/dist/pbt/index.d.ts +15 -8
- package/dist/pbt/index.js +5 -11
- package/dist/pbt/index.js.map +1 -1
- package/dist/pbt/runners/index.d.ts +1 -1
- package/dist/pbt/runners/index.js +3 -3
- package/dist/poller/index.d.ts +2 -2
- package/dist/poller/index.js +3 -3
- package/dist/prediction/index.d.ts +23 -2
- package/dist/prediction/index.js +2 -2
- package/dist/pure/index.d.ts +2 -2
- package/dist/pure/index.js +2 -2
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +8 -4
- package/dist/react/index.js.map +1 -1
- package/dist/reconciler/index.d.ts +2 -2
- package/dist/reconciler/index.js +5 -5
- package/dist/recorder/index.d.ts +1 -1
- package/dist/resilient-call/index.d.ts +45 -2
- package/dist/resilient-call/index.js +4 -4
- package/dist/retry-to-success/index.d.ts +109 -0
- package/dist/retry-to-success/index.js +35 -0
- package/dist/retry-to-success/index.js.map +1 -0
- package/dist/saga/index.d.ts +14 -1
- 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 +2 -2
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -2
- package/dist/throttle/index.d.ts +9 -0
- package/dist/throttle/index.js +1 -1
- package/dist/throttled-input/index.d.ts +10 -2
- package/dist/throttled-input/index.js +6 -6
- 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 +1 -1
- package/dist/trace-replay/index.js +3 -3
- package/dist/{types-N9LZbdnr.d.ts → types-BoUWgwSm.d.ts} +1 -1
- package/dist/with-deadline/index.d.ts +1 -1
- package/dist/with-deadline/index.js +5 -24
- package/dist/with-deadline/index.js.map +1 -1
- package/dist/with-resilience/index.d.ts +2 -2
- package/dist/with-resilience/index.js +13 -36
- package/dist/with-resilience/index.js.map +1 -1
- package/dist/with-telemetry/index.d.ts +1 -1
- package/dist/with-telemetry/index.js +4 -29
- package/dist/with-telemetry/index.js.map +1 -1
- package/dist/work-queue/adapter.d.ts +1 -1
- package/dist/work-queue/index.d.ts +1 -1
- package/dist/work-queue/ops.d.ts +1 -1
- package/dist/workflow/index.d.ts +16 -10
- package/dist/workflow/index.js.map +1 -1
- package/package.json +8 -20
- package/dist/chunk-2WZHS4P2.js.map +0 -1
- package/dist/chunk-7VEG4KHB.js +0 -202
- package/dist/chunk-7VEG4KHB.js.map +0 -1
- package/dist/chunk-A56GNPI7.js.map +0 -1
- package/dist/chunk-BNJYE4AU.js.map +0 -1
- package/dist/chunk-EQRUAP2A.js.map +0 -1
- package/dist/chunk-RVJFGRGK.js.map +0 -1
- package/dist/chunk-RVU3RBE6.js.map +0 -1
- package/dist/chunk-WUV3PSCZ.js.map +0 -1
- package/dist/chunk-XRXKH7KA.js +0 -268
- package/dist/chunk-XRXKH7KA.js.map +0 -1
- package/dist/raft/demo.d.ts +0 -105
- package/dist/raft/demo.js +0 -172
- package/dist/raft/demo.js.map +0 -1
- package/dist/raft/index.d.ts +0 -548
- package/dist/raft/index.js +0 -7
- package/dist/raft/index.js.map +0 -1
- package/dist/raft/sim.d.ts +0 -222
- package/dist/raft/sim.js +0 -8
- package/dist/raft/sim.js.map +0 -1
- package/dist/raft/timeline.d.ts +0 -46
- package/dist/raft/timeline.js +0 -62
- package/dist/raft/timeline.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-
|
|
3
|
+
import { C as Cmd, S as Sub } from '../core-BmlJOvxM.js';
|
|
4
4
|
import { RetryPolicy, RetryState } from '../retry-backoff/index.js';
|
|
5
|
-
import '../types-
|
|
5
|
+
import '../types-BoUWgwSm.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @demlik/tea/poller — "poll a source every N ms until a predicate holds, with
|
package/dist/poller/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { deadlineSub } from '../chunk-
|
|
1
|
+
import { deadlineSub } from '../chunk-SKAMZKTE.js';
|
|
2
2
|
import { asRng, defaultRetryPolicy, initRetry, recordFailure, shouldRetry, nextDelayMs } from '../chunk-5VNWZADP.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-6VDCTR7N.js';
|
|
7
|
+
import '../chunk-DYRKDCJI.js';
|
|
8
8
|
|
|
9
9
|
// src/poller/index.ts
|
|
10
10
|
function pollerSubId(atMs) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as Cmd, S as Sub, M as Machine } from '../core-
|
|
1
|
+
import { C as Cmd, S as Sub, M as Machine } from '../core-BmlJOvxM.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @demlik/tea/prediction — the client-prediction ack primitive (epic #186, facet 2).
|
|
@@ -56,6 +56,27 @@ interface Ack {
|
|
|
56
56
|
}
|
|
57
57
|
/** Construct an `Ack` for a last-applied sequence number. */
|
|
58
58
|
declare const ack: (lastAppliedSeq: Seq) => Ack;
|
|
59
|
+
/**
|
|
60
|
+
* The "nothing applied yet" cursor — the ack value for a server that has applied
|
|
61
|
+
* no client input at all. Because `nextSeq` mints **0-based** seqs and
|
|
62
|
+
* `partitionByAck` acks **inclusively** (`seq <= lastAppliedSeq`), the pre-first
|
|
63
|
+
* cursor must sit strictly *below* seq 0: `lastAppliedSeq = 0` would falsely ack
|
|
64
|
+
* the seq-0 input before the server ever saw it. `-1` is "before any seq", and
|
|
65
|
+
* `partitionByAck(buffer, NO_ACK)` therefore leaves the whole buffer PENDING.
|
|
66
|
+
*
|
|
67
|
+
* This names the boundary value on the seam so no consumer has to re-discover it
|
|
68
|
+
* as a magic number: it is the primitive's own design (0-based mint + inclusive
|
|
69
|
+
* partition) that creates the trap, so it is the primitive's job to name the
|
|
70
|
+
* escape. Pair with {@link initAck} when an {@link Ack} is wanted instead of the
|
|
71
|
+
* bare `Seq`.
|
|
72
|
+
*/
|
|
73
|
+
declare const NO_ACK: Seq;
|
|
74
|
+
/**
|
|
75
|
+
* The `Ack` for a server that has applied nothing yet — `ack(NO_ACK)`. The `Ack`
|
|
76
|
+
* counterpart to the {@link NO_ACK} `Seq` sentinel, for a consumer that carries
|
|
77
|
+
* an `Ack` rather than a bare `lastAppliedSeq`.
|
|
78
|
+
*/
|
|
79
|
+
declare const initAck: () => Ack;
|
|
59
80
|
/** The result of splitting a seq-tagged buffer against an ack. */
|
|
60
81
|
interface AckPartition<T> {
|
|
61
82
|
/** Inputs the authoritative side has applied (`seq <= lastAppliedSeq`). */
|
|
@@ -106,4 +127,4 @@ declare function reconcile<S, M extends {
|
|
|
106
127
|
type: string;
|
|
107
128
|
}, C extends Cmd, U extends Sub, Ctx>(machine: Machine<S, M, C, U, Ctx>, authoritativeState: S, lastAppliedSeq: Ack | Seq, pending: readonly SeqTagged<M>[]): S;
|
|
108
129
|
|
|
109
|
-
export { type Ack, type AckPartition, type Seq, type SeqTagged, ack, nextSeq, partitionByAck, reconcile, tagSeq };
|
|
130
|
+
export { type Ack, type AckPartition, NO_ACK, type Seq, type SeqTagged, ack, initAck, nextSeq, partitionByAck, reconcile, tagSeq };
|
package/dist/prediction/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { ack, nextSeq, partitionByAck, reconcile, tagSeq } from '../chunk-
|
|
2
|
-
import '../chunk-
|
|
1
|
+
export { NO_ACK, ack, initAck, nextSeq, partitionByAck, reconcile, tagSeq } from '../chunk-G2U7DJHL.js';
|
|
2
|
+
import '../chunk-DYRKDCJI.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
|
-
export { Ack, AckPartition, Seq, SeqTagged, ack, nextSeq, partitionByAck, reconcile, tagSeq } from '../prediction/index.js';
|
|
2
|
-
export { C as Cmd, b as ContextFree, I as Interpret, M as Machine, N as NoCtx, P as Port, c as PortEmitter, R as Reducer, S as Sub, a as SubId, d as Subscribe, e as SyncReturn, T as Transitions, U as UpdateForm,
|
|
1
|
+
export { Ack, AckPartition, NO_ACK, Seq, SeqTagged, ack, initAck, nextSeq, partitionByAck, reconcile, tagSeq } from '../prediction/index.js';
|
|
2
|
+
export { C as Cmd, b as ContextFree, I as Interpret, M as Machine, j as NoCellError, N as NoCtx, P as Port, c as PortEmitter, R as Reducer, S as Sub, a as SubId, d as Subscribe, e as SyncReturn, T as Transitions, U as UpdateForm, g as detectUpdateForm, h as foldMsgs, i as formOf, s as subId } from '../core-BmlJOvxM.js';
|
package/dist/pure/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { ack, nextSeq, partitionByAck, reconcile, tagSeq } from '../chunk-
|
|
2
|
-
export { Cmd, detectUpdateForm, foldMsgs, formOf, subId } from '../chunk-
|
|
1
|
+
export { NO_ACK, ack, initAck, nextSeq, partitionByAck, reconcile, tagSeq } from '../chunk-G2U7DJHL.js';
|
|
2
|
+
export { Cmd, NoCellError, detectUpdateForm, foldMsgs, formOf, subId } from '../chunk-DYRKDCJI.js';
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
|
4
4
|
//# sourceMappingURL=index.js.map
|
package/dist/react/index.d.ts
CHANGED
package/dist/react/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { run } from '../chunk-
|
|
2
|
-
import '../chunk-
|
|
3
|
-
import { useMemo, useRef, useSyncExternalStore, useEffect } from 'react';
|
|
1
|
+
import { run } from '../chunk-6VDCTR7N.js';
|
|
2
|
+
import '../chunk-DYRKDCJI.js';
|
|
3
|
+
import { useMemo, useRef, useReducer, useSyncExternalStore, useEffect } from 'react';
|
|
4
4
|
|
|
5
5
|
function useMachine(machine, opts) {
|
|
6
6
|
const ctx = opts.ctx;
|
|
@@ -10,6 +10,7 @@ function useMachine(machine, opts) {
|
|
|
10
10
|
[machine, ctx, store]
|
|
11
11
|
);
|
|
12
12
|
const readyRef = useRef(null);
|
|
13
|
+
const [, markBooted] = useReducer((tick) => tick + 1, 0);
|
|
13
14
|
const preliminaryState = useMemo(
|
|
14
15
|
() => machine.init(null, ctx)[0],
|
|
15
16
|
[machine, ctx]
|
|
@@ -24,7 +25,10 @@ function useMachine(machine, opts) {
|
|
|
24
25
|
let live = true;
|
|
25
26
|
booting.ready.then(
|
|
26
27
|
(r) => {
|
|
27
|
-
if (live)
|
|
28
|
+
if (live) {
|
|
29
|
+
readyRef.current = r;
|
|
30
|
+
markBooted();
|
|
31
|
+
}
|
|
28
32
|
},
|
|
29
33
|
() => {
|
|
30
34
|
}
|
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":";;;;AAqEO,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 * @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 */\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.\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-BmlJOvxM.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-BoUWgwSm.js';
|
|
12
12
|
import '../paginator/index.js';
|
|
13
13
|
|
|
14
14
|
/**
|
package/dist/reconciler/index.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { createPaginatedWalk, PAGE_KEY } from '../chunk-
|
|
1
|
+
import { createPaginatedWalk, PAGE_KEY } from '../chunk-FOGL3TC7.js';
|
|
2
2
|
import '../chunk-SINXE47Y.js';
|
|
3
|
-
import '../chunk-
|
|
3
|
+
import '../chunk-6GIGQT6C.js';
|
|
4
4
|
import '../chunk-JELSZN25.js';
|
|
5
|
-
export { deadlineSub, subscribeDeadline } from '../chunk-
|
|
5
|
+
export { deadlineSub, subscribeDeadline } from '../chunk-SKAMZKTE.js';
|
|
6
6
|
import '../chunk-5VNWZADP.js';
|
|
7
7
|
import '../chunk-2YOE5PIS.js';
|
|
8
8
|
import '../chunk-SC3FAFZN.js';
|
|
9
9
|
import { initCache, get, set } from '../chunk-IN33Q4VJ.js';
|
|
10
10
|
import '../chunk-NSDFA6M6.js';
|
|
11
11
|
import '../chunk-2YFLOCWX.js';
|
|
12
|
-
import '../chunk-
|
|
13
|
-
import '../chunk-
|
|
12
|
+
import '../chunk-6VDCTR7N.js';
|
|
13
|
+
import '../chunk-DYRKDCJI.js';
|
|
14
14
|
|
|
15
15
|
// src/reconciler/index.ts
|
|
16
16
|
function createReconciler(config, rng = Math.random) {
|
package/dist/recorder/index.d.ts
CHANGED
|
@@ -2,12 +2,48 @@ import { TtlCache } from '../cache/index.js';
|
|
|
2
2
|
import { CircuitState } from '../circuit-breaker/index.js';
|
|
3
3
|
import { DeadlineExceeded, DeadlineSub } from '../deadline/index.js';
|
|
4
4
|
export { deadlineSub, subscribeDeadline } from '../deadline/index.js';
|
|
5
|
-
import { C as Cmd } from '../core-
|
|
5
|
+
import { C as Cmd } from '../core-BmlJOvxM.js';
|
|
6
6
|
import { M as MsgType } from '../protocol-B5YmJr0d.js';
|
|
7
7
|
import { TokenBucket } from '../rate-limit/index.js';
|
|
8
8
|
import { RetryState, RetryPolicy } from '../retry-backoff/index.js';
|
|
9
|
-
import '../types-
|
|
9
|
+
import '../types-BoUWgwSm.js';
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* @deprecated `@demlik/tea/resilient-call` is deprecated in favor of
|
|
13
|
+
* `@demlik/tea/with-resilience` — the higher-order `withResilience(base,
|
|
14
|
+
* config)` wrapper (TEA-idiomatic: it wraps any existing machine instead of
|
|
15
|
+
* forcing a bespoke embedded slice; epic consolidation verdict, survivor
|
|
16
|
+
* `with-resilience`). The APIs are NOT drop-in; migration map:
|
|
17
|
+
*
|
|
18
|
+
* - `createResilientCall(config, rng)` + hand-wiring `init` / the verbs /
|
|
19
|
+
* `subs` / `handlers` into your machine → `withResilience(base, { target,
|
|
20
|
+
* ...bricks }, rng)`. Nothing is wired by hand anymore: the wrapper
|
|
21
|
+
* intercepts the `target` base Cmd and routes it through the gate.
|
|
22
|
+
* - `ResilientConfig` bricks (`retry` / `circuit` / `rateLimit` / `cache` /
|
|
23
|
+
* `deadline`) → unchanged; `ResilienceConfig` extends the same knob and
|
|
24
|
+
* adds `target` (required), `keyOf?` (per-call key — was `attempt`'s `key`
|
|
25
|
+
* argument), and `at?` (time-as-data reader — REQUIRED with any
|
|
26
|
+
* time-sensitive brick; `withResilience` throws at construction without it).
|
|
27
|
+
* - The slice at your chosen Model field + `liftResilience` → the wrapper
|
|
28
|
+
* owns `model.$resilience`; there is nothing to lift.
|
|
29
|
+
* - Verbs `attempt` / `succeed` / `fail` / `onTimer` → internal to the
|
|
30
|
+
* wrapper; results route as `$resilience:ok` / `$resilience:err` /
|
|
31
|
+
* `$resilience:timer` Msgs through the merged `update` (the old
|
|
32
|
+
* `resilient_run` / `resilient_ok` / `resilient_err` vocabulary is renamed
|
|
33
|
+
* into the `$resilience:` namespace).
|
|
34
|
+
* - `settleFailed` → NO equivalent. It exists for embedded-slice consumers
|
|
35
|
+
* (`authed-call`, `paginated-walk`) that settle a call without touching the
|
|
36
|
+
* breaker; the wrapper exposes no caller-facing settle verb.
|
|
37
|
+
* - `handlers(ports)` / `subs(state)` / the `subscribeDeadline` re-export →
|
|
38
|
+
* handled by the wrapper; run the returned machine like any other.
|
|
39
|
+
*
|
|
40
|
+
* Removal window ("deprecate, don't delete"): the `./resilient-call` subpath
|
|
41
|
+
* export survives one minor release after this deprecation ships, then is
|
|
42
|
+
* removed. Internal sibling modules (`with-resilience` itself, `agent`,
|
|
43
|
+
* `llm-call`, `authed-call`, `reconciler`, `paginated-walk`) intentionally keep
|
|
44
|
+
* importing `../resilient-call` — the deprecation targets the PUBLISHED export,
|
|
45
|
+
* not the module's internal role.
|
|
46
|
+
*/
|
|
11
47
|
/**
|
|
12
48
|
* @demlik/tea/resilient-call — the ROOT composition: a single resilience knob
|
|
13
49
|
* that bundles cache → circuit-breaker → rate-limit → exponential-backoff retry
|
|
@@ -225,6 +261,9 @@ interface ResilientPorts<I, R> {
|
|
|
225
261
|
*
|
|
226
262
|
* Returns the uniform L2 knob contract. `I` is the port input type, `R` the
|
|
227
263
|
* port result type.
|
|
264
|
+
*
|
|
265
|
+
* @deprecated Use `withResilience` from `@demlik/tea/with-resilience` — not
|
|
266
|
+
* drop-in; see the migration map at the top of this module.
|
|
228
267
|
*/
|
|
229
268
|
declare function createResilientCall<I, R>(config: ResilientConfig, rng?: () => number): {
|
|
230
269
|
init: () => ResilientState<I, R>;
|
|
@@ -243,6 +282,10 @@ declare function createResilientCall<I, R>(config: ResilientConfig, rng?: () =>
|
|
|
243
282
|
* slice lives at `state.resilience`. A convenience for the common single-slice
|
|
244
283
|
* host; consumers with a differently-named field spread by hand. Pure — a thin
|
|
245
284
|
* record rebuild, no clock / RNG.
|
|
285
|
+
*
|
|
286
|
+
* @deprecated Use `withResilience` from `@demlik/tea/with-resilience` — the
|
|
287
|
+
* wrapper owns `model.$resilience`, so there is nothing to lift; see the
|
|
288
|
+
* migration map at the top of this module.
|
|
246
289
|
*/
|
|
247
290
|
declare function liftResilience<S extends {
|
|
248
291
|
resilience: ResilientState<I, R>;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
export { createResilientCall, liftResilience } from '../chunk-
|
|
1
|
+
export { createResilientCall, liftResilience } from '../chunk-6GIGQT6C.js';
|
|
2
2
|
import '../chunk-JELSZN25.js';
|
|
3
|
-
export { deadlineSub, subscribeDeadline } from '../chunk-
|
|
3
|
+
export { deadlineSub, subscribeDeadline } from '../chunk-SKAMZKTE.js';
|
|
4
4
|
import '../chunk-5VNWZADP.js';
|
|
5
5
|
import '../chunk-2YOE5PIS.js';
|
|
6
6
|
import '../chunk-SC3FAFZN.js';
|
|
7
7
|
import '../chunk-IN33Q4VJ.js';
|
|
8
8
|
import '../chunk-NSDFA6M6.js';
|
|
9
9
|
import '../chunk-2YFLOCWX.js';
|
|
10
|
-
import '../chunk-
|
|
11
|
-
import '../chunk-
|
|
10
|
+
import '../chunk-6VDCTR7N.js';
|
|
11
|
+
import '../chunk-DYRKDCJI.js';
|
|
12
12
|
//# sourceMappingURL=index.js.map
|
|
13
13
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Rng, RetryPolicy } from '../retry-backoff/index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @demlik/tea/retry-to-success — run a fallible async port through BOUNDED
|
|
5
|
+
* retry + exponential backoff to success-or-terminal-give-up, and hand the
|
|
6
|
+
* caller a `Promise<R>` that resolves on the first success or REJECTS with a
|
|
7
|
+
* checkable {@link RetryExhaustedError} the moment the retry bound is hit.
|
|
8
|
+
*
|
|
9
|
+
* This is the retry-side parallel to `./await-terminal`. `runToTerminal` gives a
|
|
10
|
+
* caller with a one-shot decision *machine* a fire-and-await `Promise<State>`
|
|
11
|
+
* without wiring `observe`; this gives a caller with a one-shot fallible *port*
|
|
12
|
+
* a fire-and-await `Promise<R>` without either hand-folding the
|
|
13
|
+
* `./retry-backoff` primitives (`recordFailure` / `shouldRetry` / `nextDelayMs`)
|
|
14
|
+
* into their own reducer each time, or standing up the full `./resilient-call`
|
|
15
|
+
* verb contract (`init` + `attempt`/`succeed`/`fail`/`onTimer` + `subs` +
|
|
16
|
+
* `handlers`) when they want retry only and none of cache / circuit / rate-limit
|
|
17
|
+
* / deadline.
|
|
18
|
+
*
|
|
19
|
+
* It adds NO new runtime capability and NO new backoff math — the whole retry
|
|
20
|
+
* decision is `./retry-backoff` exactly as `resilient-call`'s `backoff` verb
|
|
21
|
+
* wires it:
|
|
22
|
+
* - `initRetry()` seeds the per-call attempt counter.
|
|
23
|
+
* - `recordFailure(retry, error)` folds each thrown failure (attempt++).
|
|
24
|
+
* - `shouldRetry(retry, policy)` is the terminal-at-`maxAttempts` bound — the
|
|
25
|
+
* `maxAttempts`-th recorded failure refuses another attempt (attempts
|
|
26
|
+
* `0 .. maxAttempts-1` run; the port is invoked at most `maxAttempts` times).
|
|
27
|
+
* - `nextDelayMs(retry, policy, rng)` is the exp+jitter+cap delay before the
|
|
28
|
+
* next attempt, computed on the incremented state — same call order as
|
|
29
|
+
* `resilient-call`.
|
|
30
|
+
*
|
|
31
|
+
* Where the clock / RNG live: injected, never read on the module's own behalf,
|
|
32
|
+
* mirroring `retry-backoff`'s purity discipline. `rng` is an {@link Rng} (branded
|
|
33
|
+
* `[0, 1)` via `asRng` at the caller boundary) threaded straight into
|
|
34
|
+
* `nextDelayMs`; the wait itself is a `sleep(ms)` port defaulting to a real
|
|
35
|
+
* `setTimeout`, injectable so a test can drive the backoff schedule
|
|
36
|
+
* deterministically without touching wall-clock timers.
|
|
37
|
+
*
|
|
38
|
+
* Exhaustion is NEVER a silent swallow: at the bound the returned promise
|
|
39
|
+
* rejects with {@link RetryExhaustedError} (carrying the attempt count and the
|
|
40
|
+
* final failure), the retry-side dialect of `await-terminal`'s
|
|
41
|
+
* `TerminalTimeoutError` — a distinct `instanceof`- and `_tag`-checkable
|
|
42
|
+
* rejection.
|
|
43
|
+
*
|
|
44
|
+
* NOT a substrate primitive: it depends on nothing from `../index`, only on the
|
|
45
|
+
* leaf `../retry-backoff` utility. Callers reach it via the
|
|
46
|
+
* `@demlik/tea/retry-to-success` subpath.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Raised when `retryToSuccess` exhausts the retry bound without a success — the
|
|
51
|
+
* `maxAttempts`-th recorded failure refuses another attempt. Follows the tea
|
|
52
|
+
* error dialect (`override readonly name`) and carries a `_tag` discriminant, so
|
|
53
|
+
* a caller can branch on it by `instanceof` or on the tag. It carries the number
|
|
54
|
+
* of attempts made and the final failure (`lastError`) so the terminal give-up
|
|
55
|
+
* is a distinct, checkable rejection — never a silent resolve, and never a
|
|
56
|
+
* swallowed error.
|
|
57
|
+
*/
|
|
58
|
+
declare class RetryExhaustedError extends Error {
|
|
59
|
+
/** How many times the port was invoked before giving up (= `policy.maxAttempts`). */
|
|
60
|
+
readonly attempts: number;
|
|
61
|
+
/** The failure the final attempt threw — carried, never interpreted. */
|
|
62
|
+
readonly lastError: unknown;
|
|
63
|
+
readonly name = "RetryExhaustedError";
|
|
64
|
+
readonly _tag: "RetryExhaustedError";
|
|
65
|
+
constructor(
|
|
66
|
+
/** How many times the port was invoked before giving up (= `policy.maxAttempts`). */
|
|
67
|
+
attempts: number,
|
|
68
|
+
/** The failure the final attempt threw — carried, never interpreted. */
|
|
69
|
+
lastError: unknown);
|
|
70
|
+
}
|
|
71
|
+
/** Options for {@link retryToSuccess}. */
|
|
72
|
+
interface RetryToSuccessOptions {
|
|
73
|
+
/**
|
|
74
|
+
* The `[0, 1)` jitter source threaded into `nextDelayMs`, branded via
|
|
75
|
+
* `asRng` at construction (a raw `() => number` is rejected by the type).
|
|
76
|
+
* Injected for deterministic backoff in tests; defaults to `defaultRng`
|
|
77
|
+
* (`Math.random`), read only at the delay boundary — never inside the loop's
|
|
78
|
+
* decision.
|
|
79
|
+
*/
|
|
80
|
+
readonly rng?: Rng;
|
|
81
|
+
/**
|
|
82
|
+
* The wait between attempts, given the backoff delay in ms. Injectable so a
|
|
83
|
+
* test can drive the retry schedule without wall-clock timers (and assert the
|
|
84
|
+
* exact delays); defaults to a real `setTimeout`. The delay value is always
|
|
85
|
+
* `./retry-backoff`'s `nextDelayMs` — this port only realizes the wait.
|
|
86
|
+
*/
|
|
87
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Fire-and-await bounded retry: invoke the fallible `port`, and on each thrown
|
|
91
|
+
* failure fold it through `./retry-backoff` — record it, and if the policy
|
|
92
|
+
* still permits an attempt, back off `nextDelayMs` and retry; otherwise REJECT
|
|
93
|
+
* with {@link RetryExhaustedError}. Resolves with the port's value on the first
|
|
94
|
+
* success.
|
|
95
|
+
*
|
|
96
|
+
* The port is invoked at most `policy.maxAttempts` times (attempts
|
|
97
|
+
* `0 .. maxAttempts-1`); the `maxAttempts`-th failure is terminal. Every failure
|
|
98
|
+
* is either retried or surfaced on the rejection's `lastError` — none is
|
|
99
|
+
* swallowed.
|
|
100
|
+
*
|
|
101
|
+
* @param port the fallible work — throws on failure, resolves with `R` on success.
|
|
102
|
+
* @param policy the bounded-retry curve (`baseMs` / `factor` / `capMs` /
|
|
103
|
+
* `maxAttempts` / `jitter`) from `./retry-backoff`.
|
|
104
|
+
* @param opts optional injected `rng` (jitter) and `sleep` (the wait) — see
|
|
105
|
+
* {@link RetryToSuccessOptions}.
|
|
106
|
+
*/
|
|
107
|
+
declare function retryToSuccess<R>(port: () => Promise<R>, policy: RetryPolicy, opts?: RetryToSuccessOptions): Promise<R>;
|
|
108
|
+
|
|
109
|
+
export { RetryExhaustedError, type RetryToSuccessOptions, retryToSuccess };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defaultRng, initRetry, recordFailure, shouldRetry, nextDelayMs } from '../chunk-5VNWZADP.js';
|
|
2
|
+
|
|
3
|
+
// src/retry-to-success/index.ts
|
|
4
|
+
var RetryExhaustedError = class extends Error {
|
|
5
|
+
constructor(attempts, lastError) {
|
|
6
|
+
super(
|
|
7
|
+
`@demlik/tea: retryToSuccess gave up after ${attempts} attempt(s) \u2014 the bounded retry reached maxAttempts without a success. See .lastError for the final failure.`
|
|
8
|
+
);
|
|
9
|
+
this.attempts = attempts;
|
|
10
|
+
this.lastError = lastError;
|
|
11
|
+
}
|
|
12
|
+
name = "RetryExhaustedError";
|
|
13
|
+
_tag = "RetryExhaustedError";
|
|
14
|
+
};
|
|
15
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
|
+
async function retryToSuccess(port, policy, opts = {}) {
|
|
17
|
+
const rng = opts.rng ?? defaultRng;
|
|
18
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
19
|
+
let retry = initRetry();
|
|
20
|
+
for (; ; ) {
|
|
21
|
+
try {
|
|
22
|
+
return await port();
|
|
23
|
+
} catch (error) {
|
|
24
|
+
retry = recordFailure(retry, error);
|
|
25
|
+
if (!shouldRetry(retry, policy)) {
|
|
26
|
+
throw new RetryExhaustedError(retry.attempt, error);
|
|
27
|
+
}
|
|
28
|
+
await sleep(nextDelayMs(retry, policy, rng));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { RetryExhaustedError, retryToSuccess };
|
|
34
|
+
//# sourceMappingURL=index.js.map
|
|
35
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/retry-to-success/index.ts"],"names":[],"mappings":";;;AAiEO,IAAM,mBAAA,GAAN,cAAkC,KAAA,CAAM;AAAA,EAG7C,WAAA,CAEkB,UAEA,SAAA,EAChB;AACA,IAAA,KAAA;AAAA,MACE,6CAA6C,QAAQ,CAAA,iHAAA;AAAA,KAGvD;AARgB,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AAEA,IAAA,IAAA,CAAA,SAAA,GAAA,SAAA;AAAA,EAOlB;AAAA,EAbkB,IAAA,GAAO,qBAAA;AAAA,EAChB,IAAA,GAAO,qBAAA;AAalB;AAGA,IAAM,YAAA,GAAe,CAAC,EAAA,KACpB,IAAI,OAAA,CAAQ,CAAC,OAAA,KAAY,UAAA,CAAW,OAAA,EAAS,EAAE,CAAC,CAAA;AAuClD,eAAsB,cAAA,CACpB,IAAA,EACA,MAAA,EACA,IAAA,GAA8B,EAAC,EACnB;AACZ,EAAA,MAAM,GAAA,GAAM,KAAK,GAAA,IAAO,UAAA;AACxB,EAAA,MAAM,KAAA,GAAQ,KAAK,KAAA,IAAS,YAAA;AAI5B,EAAA,IAAI,QAAQ,SAAA,EAAU;AACtB,EAAA,WAAS;AACP,IAAA,IAAI;AACF,MAAA,OAAO,MAAM,IAAA,EAAK;AAAA,IACpB,SAAS,KAAA,EAAO;AAId,MAAA,KAAA,GAAQ,aAAA,CAAc,OAAO,KAAK,CAAA;AAClC,MAAA,IAAI,CAAC,WAAA,CAAY,KAAA,EAAO,MAAM,CAAA,EAAG;AAG/B,QAAA,MAAM,IAAI,mBAAA,CAAoB,KAAA,CAAM,OAAA,EAAS,KAAK,CAAA;AAAA,MACpD;AACA,MAAA,MAAM,KAAA,CAAM,WAAA,CAAY,KAAA,EAAO,MAAA,EAAQ,GAAG,CAAC,CAAA;AAAA,IAC7C;AAAA,EACF;AACF","file":"index.js","sourcesContent":["/**\n * @demlik/tea/retry-to-success — run a fallible async port through BOUNDED\n * retry + exponential backoff to success-or-terminal-give-up, and hand the\n * caller a `Promise<R>` that resolves on the first success or REJECTS with a\n * checkable {@link RetryExhaustedError} the moment the retry bound is hit.\n *\n * This is the retry-side parallel to `./await-terminal`. `runToTerminal` gives a\n * caller with a one-shot decision *machine* a fire-and-await `Promise<State>`\n * without wiring `observe`; this gives a caller with a one-shot fallible *port*\n * a fire-and-await `Promise<R>` without either hand-folding the\n * `./retry-backoff` primitives (`recordFailure` / `shouldRetry` / `nextDelayMs`)\n * into their own reducer each time, or standing up the full `./resilient-call`\n * verb contract (`init` + `attempt`/`succeed`/`fail`/`onTimer` + `subs` +\n * `handlers`) when they want retry only and none of cache / circuit / rate-limit\n * / deadline.\n *\n * It adds NO new runtime capability and NO new backoff math — the whole retry\n * decision is `./retry-backoff` exactly as `resilient-call`'s `backoff` verb\n * wires it:\n * - `initRetry()` seeds the per-call attempt counter.\n * - `recordFailure(retry, error)` folds each thrown failure (attempt++).\n * - `shouldRetry(retry, policy)` is the terminal-at-`maxAttempts` bound — the\n * `maxAttempts`-th recorded failure refuses another attempt (attempts\n * `0 .. maxAttempts-1` run; the port is invoked at most `maxAttempts` times).\n * - `nextDelayMs(retry, policy, rng)` is the exp+jitter+cap delay before the\n * next attempt, computed on the incremented state — same call order as\n * `resilient-call`.\n *\n * Where the clock / RNG live: injected, never read on the module's own behalf,\n * mirroring `retry-backoff`'s purity discipline. `rng` is an {@link Rng} (branded\n * `[0, 1)` via `asRng` at the caller boundary) threaded straight into\n * `nextDelayMs`; the wait itself is a `sleep(ms)` port defaulting to a real\n * `setTimeout`, injectable so a test can drive the backoff schedule\n * deterministically without touching wall-clock timers.\n *\n * Exhaustion is NEVER a silent swallow: at the bound the returned promise\n * rejects with {@link RetryExhaustedError} (carrying the attempt count and the\n * final failure), the retry-side dialect of `await-terminal`'s\n * `TerminalTimeoutError` — a distinct `instanceof`- and `_tag`-checkable\n * rejection.\n *\n * NOT a substrate primitive: it depends on nothing from `../index`, only on the\n * leaf `../retry-backoff` utility. Callers reach it via the\n * `@demlik/tea/retry-to-success` subpath.\n */\n\nimport {\n defaultRng,\n initRetry,\n nextDelayMs,\n type RetryPolicy,\n type Rng,\n recordFailure,\n shouldRetry,\n} from \"../retry-backoff\";\n\n/**\n * Raised when `retryToSuccess` exhausts the retry bound without a success — the\n * `maxAttempts`-th recorded failure refuses another attempt. Follows the tea\n * error dialect (`override readonly name`) and carries a `_tag` discriminant, so\n * a caller can branch on it by `instanceof` or on the tag. It carries the number\n * of attempts made and the final failure (`lastError`) so the terminal give-up\n * is a distinct, checkable rejection — never a silent resolve, and never a\n * swallowed error.\n */\nexport class RetryExhaustedError extends Error {\n override readonly name = \"RetryExhaustedError\";\n readonly _tag = \"RetryExhaustedError\" as const;\n constructor(\n /** How many times the port was invoked before giving up (= `policy.maxAttempts`). */\n public readonly attempts: number,\n /** The failure the final attempt threw — carried, never interpreted. */\n public readonly lastError: unknown,\n ) {\n super(\n `@demlik/tea: retryToSuccess gave up after ${attempts} attempt(s) — the ` +\n `bounded retry reached maxAttempts without a success. See .lastError ` +\n `for the final failure.`,\n );\n }\n}\n\n/** Default wait: a real `setTimeout(ms)`, the sole clock read this module makes. */\nconst defaultSleep = (ms: number): Promise<void> =>\n new Promise((resolve) => setTimeout(resolve, ms));\n\n/** Options for {@link retryToSuccess}. */\nexport interface RetryToSuccessOptions {\n /**\n * The `[0, 1)` jitter source threaded into `nextDelayMs`, branded via\n * `asRng` at construction (a raw `() => number` is rejected by the type).\n * Injected for deterministic backoff in tests; defaults to `defaultRng`\n * (`Math.random`), read only at the delay boundary — never inside the loop's\n * decision.\n */\n readonly rng?: Rng;\n /**\n * The wait between attempts, given the backoff delay in ms. Injectable so a\n * test can drive the retry schedule without wall-clock timers (and assert the\n * exact delays); defaults to a real `setTimeout`. The delay value is always\n * `./retry-backoff`'s `nextDelayMs` — this port only realizes the wait.\n */\n readonly sleep?: (ms: number) => Promise<void>;\n}\n\n/**\n * Fire-and-await bounded retry: invoke the fallible `port`, and on each thrown\n * failure fold it through `./retry-backoff` — record it, and if the policy\n * still permits an attempt, back off `nextDelayMs` and retry; otherwise REJECT\n * with {@link RetryExhaustedError}. Resolves with the port's value on the first\n * success.\n *\n * The port is invoked at most `policy.maxAttempts` times (attempts\n * `0 .. maxAttempts-1`); the `maxAttempts`-th failure is terminal. Every failure\n * is either retried or surfaced on the rejection's `lastError` — none is\n * swallowed.\n *\n * @param port the fallible work — throws on failure, resolves with `R` on success.\n * @param policy the bounded-retry curve (`baseMs` / `factor` / `capMs` /\n * `maxAttempts` / `jitter`) from `./retry-backoff`.\n * @param opts optional injected `rng` (jitter) and `sleep` (the wait) — see\n * {@link RetryToSuccessOptions}.\n */\nexport async function retryToSuccess<R>(\n port: () => Promise<R>,\n policy: RetryPolicy,\n opts: RetryToSuccessOptions = {},\n): Promise<R> {\n const rng = opts.rng ?? defaultRng;\n const sleep = opts.sleep ?? defaultSleep;\n\n // The retry counter folds over `./retry-backoff` exactly as resilient-call's\n // `backoff` verb does — no local attempt bookkeeping, no local backoff math.\n let retry = initRetry();\n for (;;) {\n try {\n return await port();\n } catch (error) {\n // Fold the failure first (attempt++), then consult the bound on the\n // incremented state — same order as resilient-call so the semantics of\n // `maxAttempts` are identical across both surfaces.\n retry = recordFailure(retry, error);\n if (!shouldRetry(retry, policy)) {\n // Terminal give-up at the bound: reject, never swallow. `retry.attempt`\n // is the number of failures = the number of port invocations made.\n throw new RetryExhaustedError(retry.attempt, error);\n }\n await sleep(nextDelayMs(retry, policy, rng));\n }\n }\n}\n"]}
|
package/dist/saga/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as Cmd } from '../core-
|
|
1
|
+
import { C as Cmd } from '../core-BmlJOvxM.js';
|
|
2
2
|
import { QueueItem } from '../work-queue/index.js';
|
|
3
3
|
import '../index.js';
|
|
4
4
|
|
|
@@ -12,6 +12,19 @@ import '../index.js';
|
|
|
12
12
|
* the saga drives `do` forward one step at a time, and on the first failure it
|
|
13
13
|
* pivots and drives `undo` backward over exactly the steps that completed.
|
|
14
14
|
*
|
|
15
|
+
* **Boundary with `@demlik/tea/workflow` — the compensation test.** Both drive
|
|
16
|
+
* an ordered multi-step transaction with reverse-order compensation; the line
|
|
17
|
+
* between them is whether every step carries a true inverse. Here
|
|
18
|
+
* `SagaStep<D, U>` REQUIRES both `do` and `undo` — a step without its inverse
|
|
19
|
+
* is unrepresentable, so full reversibility is a type-level guarantee.
|
|
20
|
+
* Workflow's `WorkflowStep.compensation` is OPTIONAL — an irreversible step (a
|
|
21
|
+
* sent email) is skipped during the unwind. Pick by the test: **every step
|
|
22
|
+
* must be reversible → saga; some steps are irreversible but the run must
|
|
23
|
+
* finish → workflow.** Cross to workflow when the transaction includes steps
|
|
24
|
+
* that can't be undone (or when you want each activity recorded as a durable
|
|
25
|
+
* owed effect on the `../do` ledger); stay here when the type system should
|
|
26
|
+
* enforce that every step can be undone.
|
|
27
|
+
*
|
|
15
28
|
* The recipe (which bricks it composes):
|
|
16
29
|
* - `../work-queue/adapter` — the saga's forward progress IS a work-queue
|
|
17
30
|
* lifecycle. Each step's record carries the same `{ id, status, input }`
|