@demlik/tea 0.1.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.
Files changed (96) hide show
  1. package/dist/cache/index.d.ts +151 -0
  2. package/dist/cache/index.js +53 -0
  3. package/dist/cache/index.js.map +1 -0
  4. package/dist/chunk-2YFLOCWX.js +15 -0
  5. package/dist/chunk-2YFLOCWX.js.map +1 -0
  6. package/dist/chunk-CMG2KMWX.js +458 -0
  7. package/dist/chunk-CMG2KMWX.js.map +1 -0
  8. package/dist/chunk-PYBQREFU.js +95 -0
  9. package/dist/chunk-PYBQREFU.js.map +1 -0
  10. package/dist/chunk-PYOCUBQJ.js +196 -0
  11. package/dist/chunk-PYOCUBQJ.js.map +1 -0
  12. package/dist/chunk-Y5JM3UUH.js +126 -0
  13. package/dist/chunk-Y5JM3UUH.js.map +1 -0
  14. package/dist/chunk-ZRUST6BK.js +62 -0
  15. package/dist/chunk-ZRUST6BK.js.map +1 -0
  16. package/dist/circuit-breaker/index.d.ts +196 -0
  17. package/dist/circuit-breaker/index.js +53 -0
  18. package/dist/circuit-breaker/index.js.map +1 -0
  19. package/dist/deadline/index.d.ts +113 -0
  20. package/dist/deadline/index.js +20 -0
  21. package/dist/deadline/index.js.map +1 -0
  22. package/dist/debounce/index.d.ts +114 -0
  23. package/dist/debounce/index.js +56 -0
  24. package/dist/debounce/index.js.map +1 -0
  25. package/dist/devtools/index.d.ts +137 -0
  26. package/dist/devtools/index.js +217 -0
  27. package/dist/devtools/index.js.map +1 -0
  28. package/dist/devtools/styles.css +218 -0
  29. package/dist/do/index.d.ts +135 -0
  30. package/dist/do/index.js +54 -0
  31. package/dist/do/index.js.map +1 -0
  32. package/dist/extension/index.d.ts +207 -0
  33. package/dist/extension/index.js +26 -0
  34. package/dist/extension/index.js.map +1 -0
  35. package/dist/extension/react.d.ts +121 -0
  36. package/dist/extension/react.js +69 -0
  37. package/dist/extension/react.js.map +1 -0
  38. package/dist/extension/subs/index.d.ts +44 -0
  39. package/dist/extension/subs/index.js +83 -0
  40. package/dist/extension/subs/index.js.map +1 -0
  41. package/dist/extension/test-utils.d.ts +128 -0
  42. package/dist/extension/test-utils.js +269 -0
  43. package/dist/extension/test-utils.js.map +1 -0
  44. package/dist/idempotency/index.d.ts +131 -0
  45. package/dist/idempotency/index.js +58 -0
  46. package/dist/idempotency/index.js.map +1 -0
  47. package/dist/index.d.ts +364 -0
  48. package/dist/index.js +3 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/machine-viz/index.d.ts +78 -0
  51. package/dist/machine-viz/index.js +139 -0
  52. package/dist/machine-viz/index.js.map +1 -0
  53. package/dist/mem/index.d.ts +45 -0
  54. package/dist/mem/index.js +19 -0
  55. package/dist/mem/index.js.map +1 -0
  56. package/dist/node/index.d.ts +134 -0
  57. package/dist/node/index.js +89 -0
  58. package/dist/node/index.js.map +1 -0
  59. package/dist/pbt/arbitraries/index.d.ts +135 -0
  60. package/dist/pbt/arbitraries/index.js +3 -0
  61. package/dist/pbt/arbitraries/index.js.map +1 -0
  62. package/dist/pbt/index.d.ts +28 -0
  63. package/dist/pbt/index.js +18 -0
  64. package/dist/pbt/index.js.map +1 -0
  65. package/dist/pbt/runners/index.d.ts +117 -0
  66. package/dist/pbt/runners/index.js +4 -0
  67. package/dist/pbt/runners/index.js.map +1 -0
  68. package/dist/rate-limit/index.d.ts +115 -0
  69. package/dist/rate-limit/index.js +45 -0
  70. package/dist/rate-limit/index.js.map +1 -0
  71. package/dist/react/index.d.ts +72 -0
  72. package/dist/react/index.js +33 -0
  73. package/dist/react/index.js.map +1 -0
  74. package/dist/recorder/index.d.ts +287 -0
  75. package/dist/recorder/index.js +157 -0
  76. package/dist/recorder/index.js.map +1 -0
  77. package/dist/retry-backoff/index.d.ts +131 -0
  78. package/dist/retry-backoff/index.js +37 -0
  79. package/dist/retry-backoff/index.js.map +1 -0
  80. package/dist/subs/index.d.ts +57 -0
  81. package/dist/subs/index.js +4 -0
  82. package/dist/subs/index.js.map +1 -0
  83. package/dist/testing/index.d.ts +220 -0
  84. package/dist/testing/index.js +105 -0
  85. package/dist/testing/index.js.map +1 -0
  86. package/dist/throttle/index.d.ts +125 -0
  87. package/dist/throttle/index.js +54 -0
  88. package/dist/throttle/index.js.map +1 -0
  89. package/dist/trace-replay/index.d.ts +52 -0
  90. package/dist/trace-replay/index.js +57 -0
  91. package/dist/trace-replay/index.js.map +1 -0
  92. package/dist/types-CwBC5wMn.d.ts +5 -0
  93. package/dist/work-queue/index.d.ts +56 -0
  94. package/dist/work-queue/index.js +143 -0
  95. package/dist/work-queue/index.js.map +1 -0
  96. package/package.json +184 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -0,0 +1,28 @@
1
+ export { MsgArbitraryTable, arbConstantMsg, arbGuidedSequence, arbMsg, arbMsgSequence, arbRecordMsg, stubCtxThrowingProxy } from './arbitraries/index.js';
2
+ export { Step, foldEvents, propertyInvariant, propertyTerminates, propertyTrace } from './runners/index.js';
3
+ import { Cmd, Reducer, Transitions } from '../index.js';
4
+ import 'fast-check';
5
+
6
+ /**
7
+ * Extract the Msg discriminant set at runtime from a Reducer record or a
8
+ * Transitions table. Useful for asserting that a user-supplied
9
+ * `MsgArbitraryTable<M>` covers every variant the reducer can handle.
10
+ *
11
+ * - **Reducer form** — keys of the top-level record ARE the Msg variants.
12
+ * - **Transitions form** — keys of any single state's inner record. Every
13
+ * state's inner record must cover every Msg variant (mapped-type
14
+ * discipline), so reading the first state's keys is equivalent to reading
15
+ * the union.
16
+ *
17
+ * @example
18
+ * import { auditBackgroundMachine } from "./reducer";
19
+ * msgTypeKeys(auditBackgroundMachine.update);
20
+ * // → ["start_audit", "stop_audit", "window_created", ...]
21
+ */
22
+ declare function msgTypeKeys<S, M extends {
23
+ type: string;
24
+ }, C extends Cmd>(update: Reducer<S, M, C> | ([S] extends [{
25
+ type: string;
26
+ }] ? Transitions<S, M, C> : never)): readonly string[];
27
+
28
+ export { msgTypeKeys };
@@ -0,0 +1,18 @@
1
+ export { arbConstantMsg, arbGuidedSequence, arbMsg, arbMsgSequence, arbRecordMsg, stubCtxThrowingProxy } from '../chunk-ZRUST6BK.js';
2
+ export { foldEvents, propertyInvariant, propertyTerminates, propertyTrace } from '../chunk-PYBQREFU.js';
3
+ import '../chunk-CMG2KMWX.js';
4
+
5
+ // src/pbt/util/msg-keys.ts
6
+ function msgTypeKeys(update) {
7
+ const keys = Object.keys(update);
8
+ if (keys.length === 0) return [];
9
+ const firstValue = update[keys[0]];
10
+ if (typeof firstValue === "function") {
11
+ return keys;
12
+ }
13
+ return Object.keys(firstValue);
14
+ }
15
+
16
+ export { msgTypeKeys };
17
+ //# sourceMappingURL=index.js.map
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/pbt/util/msg-keys.ts"],"names":[],"mappings":";;;;;AAiCO,SAAS,YACd,MAAA,EACmB;AACnB,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,IAAA,CAAK,MAAgB,CAAA;AACzC,EAAA,IAAI,IAAA,CAAK,MAAA,KAAW,CAAA,EAAG,OAAO,EAAC;AAI/B,EAAA,MAAM,UAAA,GAAc,MAAA,CAAmC,IAAA,CAAK,CAAC,CAAE,CAAA;AAC/D,EAAA,IAAI,OAAO,eAAe,UAAA,EAAY;AACpC,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,OAAO,MAAA,CAAO,KAAK,UAAoB,CAAA;AACzC","file":"index.js","sourcesContent":["// ---------------------------------------------------------------------------\n// msgTypeKeys — read the Msg discriminant set at runtime from a Reducer\n// record or a Transitions table.\n//\n// TS Msg unions only exist at the type level; the discriminant strings are\n// erased at runtime. The reducer record / transitions table is the load-\n// bearing artifact that DOES carry the discriminants at runtime — its keys\n// (for Reducer) or any one cell's inner keys (for Transitions) are exactly\n// the set we need to build an `arbMsg` table without hand-listing them.\n//\n// Used by `arbMsg` to assert (in dev) that the user-supplied table covers\n// every Msg variant the reducer claims to handle. Strict drift detection\n// without runtime introspection of the type system.\n// ---------------------------------------------------------------------------\n\nimport type { Cmd, Reducer, Transitions } from \"../../index\";\n\n/**\n * Extract the Msg discriminant set at runtime from a Reducer record or a\n * Transitions table. Useful for asserting that a user-supplied\n * `MsgArbitraryTable<M>` covers every variant the reducer can handle.\n *\n * - **Reducer form** — keys of the top-level record ARE the Msg variants.\n * - **Transitions form** — keys of any single state's inner record. Every\n * state's inner record must cover every Msg variant (mapped-type\n * discipline), so reading the first state's keys is equivalent to reading\n * the union.\n *\n * @example\n * import { auditBackgroundMachine } from \"./reducer\";\n * msgTypeKeys(auditBackgroundMachine.update);\n * // → [\"start_audit\", \"stop_audit\", \"window_created\", ...]\n */\nexport function msgTypeKeys<S, M extends { type: string }, C extends Cmd>(\n update: Reducer<S, M, C> | ([S] extends [{ type: string }] ? Transitions<S, M, C> : never),\n): readonly string[] {\n const keys = Object.keys(update as object);\n if (keys.length === 0) return [];\n // Reducer form: every top-level value is a function (the cell).\n // Transitions form: every top-level value is an object (the inner record).\n // `keys[0]` is present — the length guard above rules out the empty record.\n const firstValue = (update as Record<string, unknown>)[keys[0]!];\n if (typeof firstValue === \"function\") {\n return keys;\n }\n // Transitions form — read inner record.\n return Object.keys(firstValue as object);\n}\n"]}
@@ -0,0 +1,117 @@
1
+ import { Cmd, Sub, Machine } from '../../index.js';
2
+ import * as fc from 'fast-check';
3
+
4
+ /**
5
+ * One unit in a fold trace — the four pieces of data a property
6
+ * invariant typically asserts on. `prev` is the state before the msg
7
+ * applied; `next` is the state after; `cmds` is the cmds emitted by this
8
+ * single step (NOT the running total).
9
+ */
10
+ interface Step<S, M, C> {
11
+ readonly prev: S;
12
+ readonly msg: M;
13
+ readonly next: S;
14
+ readonly cmds: readonly C[];
15
+ }
16
+ /**
17
+ * Fold a Msg sequence through a machine's pure reducer, returning the
18
+ * per-step trace, every state in order, and the final state. Initial
19
+ * state comes from `replay(machine, { msgs: [], ctx, loaded })` — i.e.
20
+ * `machine.init(loaded ?? null, ctx)` faithfully. Init's emitted cmds
21
+ * are NOT included in any `Step` (they're the boot moment, not a Msg).
22
+ *
23
+ * @param machine The machine under test.
24
+ * @param ctx The Ctx — typically `stubCtxThrowingProxy<Ctx>()` when
25
+ * init is pure. Pass a real ctx if init reads from it.
26
+ * @param loaded Optional pre-init snapshot (the `loaded` arg to
27
+ * `machine.init`). Defaults to `null` (fresh boot).
28
+ * @param msgs The Msg sequence to fold.
29
+ *
30
+ * @example
31
+ * const { states, steps, finalState } = foldEvents(machine, ctx, null, [
32
+ * { type: "start" }, { type: "tick" }, { type: "stop" },
33
+ * ]);
34
+ * // steps[1] = { prev: { type: "counting", n: 0 }, msg: { type: "tick" }, ... }
35
+ */
36
+ declare function foldEvents<S, M extends {
37
+ type: string;
38
+ }, C extends Cmd, U extends Sub, Ctx>(machine: Machine<S, M, C, U, Ctx>, ctx: NoInfer<Ctx>, loaded: NoInfer<S> | null, msgs: readonly NoInfer<M>[]): {
39
+ readonly states: readonly S[];
40
+ readonly steps: readonly Step<S, M, C>[];
41
+ readonly finalState: S;
42
+ };
43
+
44
+ /** Common options shared by every property runner. */
45
+ interface PropertyOpts<S> {
46
+ /** Pre-init snapshot — passed to `machine.init(loaded, ctx)`. Default `null`. */
47
+ readonly loaded?: S | null;
48
+ /** Number of generated sequences to test. Forwarded to `fc.assert`. */
49
+ readonly numRuns?: number;
50
+ /** Optional fast-check seed for reproducibility. */
51
+ readonly seed?: number;
52
+ }
53
+ /**
54
+ * Assert every generated Msg sequence ends in a terminal state. `terminal`
55
+ * is the predicate that names the acceptable final states (often
56
+ * `(s) => s.type === "done" || s.type === "idle"`).
57
+ *
58
+ * The "every sequence" property catches stuck states (machine gets to a
59
+ * non-terminal state and no further msg can escape) AND silent loops
60
+ * (every msg variant is a noop from a given state).
61
+ *
62
+ * @example
63
+ * propertyTerminates(
64
+ * auditBackgroundMachine,
65
+ * stubCtxThrowingProxy<BackgroundCtx>(),
66
+ * arbMsgSequence(msgArb, { minLength: 1, maxLength: 30 }),
67
+ * (s) => s.type === "done" || s.type === "idle",
68
+ * { loaded: auditingState, numRuns: 500 },
69
+ * );
70
+ */
71
+ declare function propertyTerminates<S, M extends {
72
+ type: string;
73
+ }, C extends Cmd, U extends Sub, Ctx>(machine: Machine<S, M, C, U, Ctx>, ctx: NoInfer<Ctx>, seqArb: fc.Arbitrary<readonly NoInfer<M>[]>, terminal: (state: NoInfer<S>) => boolean, opts?: NoInfer<PropertyOpts<S>>): void;
74
+ /**
75
+ * Assert a predicate holds on EVERY transition step in a generated
76
+ * sequence. The predicate sees `{ prev, msg, next, cmds }` per step;
77
+ * return true to accept, false (or throw) to fail.
78
+ *
79
+ * Use to pin a "when X happens in state Y, cmd Z must be emitted" rule.
80
+ * The shrinker minimizes the offending sequence to the smallest Msg list
81
+ * that still violates the predicate.
82
+ *
83
+ * @example
84
+ * // Pin the resume-handshake fix: `ws:client:hello` in auditing must
85
+ * // emit `send_ws { client:ready }`.
86
+ * propertyInvariant(
87
+ * auditBackgroundMachine, ctx,
88
+ * arbMsgSequence(msgArb, { prefix: [{ type: "ws:client:hello" }] }),
89
+ * ({ prev, msg, cmds }) =>
90
+ * !(prev.type === "auditing" && msg.type === "ws:client:hello") ||
91
+ * cmds.some((c) => c.type === "send_ws" && c.msg.type === "client:ready"),
92
+ * { loaded: auditingState, numRuns: 200 },
93
+ * );
94
+ */
95
+ declare function propertyInvariant<S, M extends {
96
+ type: string;
97
+ }, C extends Cmd, U extends Sub, Ctx>(machine: Machine<S, M, C, U, Ctx>, ctx: NoInfer<Ctx>, seqArb: fc.Arbitrary<readonly NoInfer<M>[]>, invariant: (step: Step<NoInfer<S>, NoInfer<M>, NoInfer<C>>) => boolean, opts?: NoInfer<PropertyOpts<S>>): void;
98
+ /**
99
+ * Assert a predicate over the full trace plus the final state. Use when
100
+ * the property is "some Msg appeared in the trace before reaching the
101
+ * final state" or "no two consecutive steps had the same state.type" —
102
+ * predicates that need the full sequence, not a single step.
103
+ *
104
+ * @example
105
+ * propertyTrace(
106
+ * machine, ctx, seqArb,
107
+ * (steps, final) =>
108
+ * final.type !== "done" ||
109
+ * steps.some((step) => step.msg.type === "ws:audit:done"),
110
+ * { loaded: auditingState, numRuns: 500 },
111
+ * );
112
+ */
113
+ declare function propertyTrace<S, M extends {
114
+ type: string;
115
+ }, C extends Cmd, U extends Sub, Ctx>(machine: Machine<S, M, C, U, Ctx>, ctx: NoInfer<Ctx>, seqArb: fc.Arbitrary<readonly NoInfer<M>[]>, predicate: (steps: readonly Step<NoInfer<S>, NoInfer<M>, NoInfer<C>>[], finalState: NoInfer<S>) => boolean, opts?: NoInfer<PropertyOpts<S>>): void;
116
+
117
+ export { type Step, foldEvents, propertyInvariant, propertyTerminates, propertyTrace };
@@ -0,0 +1,4 @@
1
+ export { foldEvents, propertyInvariant, propertyTerminates, propertyTrace } from '../../chunk-PYBQREFU.js';
2
+ import '../../chunk-CMG2KMWX.js';
3
+ //# sourceMappingURL=index.js.map
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @demlik/tea/rate-limit — two rate limiters as pure state + ops.
3
+ *
4
+ * Same shape as `@demlik/tea/work-queue`'s `ops.ts`: a state type plus pure
5
+ * transition functions. Host-agnostic by construction — no `Date.now()`, no
6
+ * timers, no I/O. The caller injects the clock (`nowMs`) and wires the result
7
+ * into its own reducer / interpret. This keeps the limiters inside TEA's
8
+ * invariant 2 (transitions are pure: same inputs → same outputs, no `Date.now`)
9
+ * so a consumer can fold them through `update` and exercise them with `replay`
10
+ * / the pbt fold without a fake clock.
11
+ *
12
+ * Every op returns a NEW state value and never mutates its input (invariant 1 —
13
+ * state is a value, not a place). The `tryConsume` / `record` ops return a
14
+ * `[next, ok]` tuple so the caller branches on the decision while threading the
15
+ * advanced state forward; `refill` / `remaining` are read-shaped helpers.
16
+ *
17
+ * Two complementary algorithms, picked by the cost the caller wants to pay:
18
+ *
19
+ * - **Token bucket** — O(1) state (four numbers), smooths bursts against a
20
+ * steady refill rate. Allows a burst up to `capacity`, then throttles to
21
+ * `refillPerSec`. Cheapest; the default choice.
22
+ * - **Sliding window log** — O(limit) state (one timestamp per in-window
23
+ * hit), exact "no more than `limit` events per `windowMs`". Pay the memory
24
+ * for precise window edges when a token bucket's continuous refill would
25
+ * let through too many at a boundary.
26
+ *
27
+ * NOT exported from the package root — reached via the `@demlik/tea/rate-limit`
28
+ * subpath, same one-shape-per-package rule as `work-queue` and `do`.
29
+ */
30
+ /**
31
+ * A token bucket: `tokens` of `capacity` available now, replenished at
32
+ * `refillPerSec` tokens per second. `lastRefillMs` is the clock reading at
33
+ * which `tokens` was last computed — `refill` advances it. All four fields are
34
+ * plain numbers so the whole bucket is trivially JSON-serializable into a
35
+ * consumer's `Store<S>`.
36
+ */
37
+ interface TokenBucket {
38
+ readonly tokens: number;
39
+ readonly capacity: number;
40
+ readonly refillPerSec: number;
41
+ readonly lastRefillMs: number;
42
+ }
43
+ /**
44
+ * Create a full bucket. Starts at `capacity` tokens (the conventional "a fresh
45
+ * limiter lets the first burst through" behavior) and stamps `lastRefillMs`
46
+ * with the caller's clock so the first `refill` measures elapsed time from
47
+ * boot, not from epoch 0.
48
+ */
49
+ declare function initBucket(capacity: number, refillPerSec: number, nowMs: number): TokenBucket;
50
+ /**
51
+ * Add the tokens accrued since `lastRefillMs`, clamped to `capacity`, and
52
+ * advance `lastRefillMs` to `nowMs`. PURE — returns a new bucket; the input is
53
+ * untouched.
54
+ *
55
+ * A `nowMs` at or before `lastRefillMs` (clock not advanced, or skewed
56
+ * backwards) adds a non-positive amount; `Math.max(0, …)` floors the elapsed
57
+ * term so a backwards clock can never *remove* tokens. `lastRefillMs` still
58
+ * advances to `nowMs` so the bucket tracks the latest reading the caller
59
+ * presented — the next forward step measures from there, not from a stale
60
+ * future timestamp.
61
+ */
62
+ declare function refill(bucket: TokenBucket, nowMs: number): TokenBucket;
63
+ /**
64
+ * Refill against `nowMs`, then attempt to take `n` tokens (default 1).
65
+ *
66
+ * On success returns `[next, true]` with `n` subtracted; on failure returns
67
+ * `[refilled, false]` — the refilled bucket (so the caller keeps the advanced
68
+ * clock + accrued tokens) with no tokens spent. Branch on the boolean, thread
69
+ * the bucket forward regardless:
70
+ *
71
+ * const [b1, ok] = tryConsume(bucket, nowMs);
72
+ * return ok ? [{ ...state, bucket: b1 }, [doWork]] : [{ ...state, bucket: b1 }, []];
73
+ *
74
+ * PURE — input bucket is never mutated.
75
+ */
76
+ declare function tryConsume(bucket: TokenBucket, nowMs: number, n?: number): readonly [TokenBucket, boolean];
77
+ /**
78
+ * A sliding-window log: the timestamps of every hit still inside the trailing
79
+ * `windowMs`, capped at `limit` events per window. `hits` is `readonly` and
80
+ * kept oldest-first (pruning slices the stale prefix off the front, recording
81
+ * appends to the back), so the array is its own sorted index. Exact at the
82
+ * window edge — unlike a token bucket's continuous refill, a hit leaves the
83
+ * count the instant it ages past `windowMs`.
84
+ */
85
+ interface SlidingWindow {
86
+ readonly hits: readonly number[];
87
+ readonly windowMs: number;
88
+ readonly limit: number;
89
+ }
90
+ /** Create an empty window. No hits recorded yet. */
91
+ declare function initWindow(windowMs: number, limit: number): SlidingWindow;
92
+ /**
93
+ * Prune hits older than `nowMs - windowMs`, then record `nowMs` if there is
94
+ * room. Returns `[next, true]` with the hit appended when `hits.length < limit`
95
+ * after pruning, else `[pruned, false]` — the pruned window (capacity recovers
96
+ * as old hits age out) with nothing appended.
97
+ *
98
+ * The cutoff is half-open: a hit exactly `windowMs` old is pruned (`<= cutoff`
99
+ * drops it), so a hit recorded at `t` no longer counts at `t + windowMs`. That
100
+ * makes the window's capacity fully recover `windowMs` after the last hit, the
101
+ * behavior tests pin.
102
+ *
103
+ * PURE — input window is never mutated; `hits` is rebuilt by `filter` +
104
+ * spread.
105
+ */
106
+ declare function record(window: SlidingWindow, nowMs: number): readonly [SlidingWindow, boolean];
107
+ /**
108
+ * How many more hits `record` would accept at `nowMs` without blocking —
109
+ * `limit` minus the live (post-prune) hit count, floored at 0. Read-only: it
110
+ * prunes a local copy to answer but returns no new window. Use it to render a
111
+ * "N requests remaining" affordance without committing a hit.
112
+ */
113
+ declare function remaining(window: SlidingWindow, nowMs: number): number;
114
+
115
+ export { type SlidingWindow, type TokenBucket, initBucket, initWindow, record, refill, remaining, tryConsume };
@@ -0,0 +1,45 @@
1
+ // src/rate-limit/index.ts
2
+ function initBucket(capacity, refillPerSec, nowMs) {
3
+ return {
4
+ tokens: capacity,
5
+ capacity,
6
+ refillPerSec,
7
+ lastRefillMs: nowMs
8
+ };
9
+ }
10
+ function refill(bucket, nowMs) {
11
+ const elapsedSec = Math.max(0, nowMs - bucket.lastRefillMs) / 1e3;
12
+ const replenished = bucket.tokens + elapsedSec * bucket.refillPerSec;
13
+ return {
14
+ ...bucket,
15
+ tokens: Math.min(bucket.capacity, replenished),
16
+ lastRefillMs: nowMs
17
+ };
18
+ }
19
+ function tryConsume(bucket, nowMs, n = 1) {
20
+ const refilled = refill(bucket, nowMs);
21
+ if (refilled.tokens >= n) {
22
+ return [{ ...refilled, tokens: refilled.tokens - n }, true];
23
+ }
24
+ return [refilled, false];
25
+ }
26
+ function initWindow(windowMs, limit) {
27
+ return { hits: [], windowMs, limit };
28
+ }
29
+ function record(window, nowMs) {
30
+ const cutoff = nowMs - window.windowMs;
31
+ const pruned = window.hits.filter((t) => t > cutoff);
32
+ if (pruned.length < window.limit) {
33
+ return [{ ...window, hits: [...pruned, nowMs] }, true];
34
+ }
35
+ return [{ ...window, hits: pruned }, false];
36
+ }
37
+ function remaining(window, nowMs) {
38
+ const cutoff = nowMs - window.windowMs;
39
+ const live = window.hits.filter((t) => t > cutoff).length;
40
+ return Math.max(0, window.limit - live);
41
+ }
42
+
43
+ export { initBucket, initWindow, record, refill, remaining, tryConsume };
44
+ //# sourceMappingURL=index.js.map
45
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/rate-limit/index.ts"],"names":[],"mappings":";AAsDO,SAAS,UAAA,CAAW,QAAA,EAAkB,YAAA,EAAsB,KAAA,EAA4B;AAC7F,EAAA,OAAO;AAAA,IACL,MAAA,EAAQ,QAAA;AAAA,IACR,QAAA;AAAA,IACA,YAAA;AAAA,IACA,YAAA,EAAc;AAAA,GAChB;AACF;AAcO,SAAS,MAAA,CAAO,QAAqB,KAAA,EAA4B;AACtE,EAAA,MAAM,aAAa,IAAA,CAAK,GAAA,CAAI,GAAG,KAAA,GAAQ,MAAA,CAAO,YAAY,CAAA,GAAI,GAAA;AAC9D,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,MAAA,GAAS,UAAA,GAAa,MAAA,CAAO,YAAA;AACxD,EAAA,OAAO;AAAA,IACL,GAAG,MAAA;AAAA,IACH,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,UAAU,WAAW,CAAA;AAAA,IAC7C,YAAA,EAAc;AAAA,GAChB;AACF;AAeO,SAAS,UAAA,CACd,MAAA,EACA,KAAA,EACA,CAAA,GAAI,CAAA,EAC6B;AACjC,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,MAAA,EAAQ,KAAK,CAAA;AACrC,EAAA,IAAI,QAAA,CAAS,UAAU,CAAA,EAAG;AACxB,IAAA,OAAO,CAAC,EAAE,GAAG,QAAA,EAAU,QAAQ,QAAA,CAAS,MAAA,GAAS,CAAA,EAAE,EAAG,IAAI,CAAA;AAAA,EAC5D;AACA,EAAA,OAAO,CAAC,UAAU,KAAK,CAAA;AACzB;AAqBO,SAAS,UAAA,CAAW,UAAkB,KAAA,EAA8B;AACzE,EAAA,OAAO,EAAE,IAAA,EAAM,EAAC,EAAG,UAAU,KAAA,EAAM;AACrC;AAgBO,SAAS,MAAA,CAAO,QAAuB,KAAA,EAAkD;AAC9F,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,CAAO,QAAA;AAC9B,EAAA,MAAM,SAAS,MAAA,CAAO,IAAA,CAAK,OAAO,CAAC,CAAA,KAAM,IAAI,MAAM,CAAA;AACnD,EAAA,IAAI,MAAA,CAAO,MAAA,GAAS,MAAA,CAAO,KAAA,EAAO;AAChC,IAAA,OAAO,CAAC,EAAE,GAAG,MAAA,EAAQ,IAAA,EAAM,CAAC,GAAG,MAAA,EAAQ,KAAK,CAAA,EAAE,EAAG,IAAI,CAAA;AAAA,EACvD;AACA,EAAA,OAAO,CAAC,EAAE,GAAG,QAAQ,IAAA,EAAM,MAAA,IAAU,KAAK,CAAA;AAC5C;AAQO,SAAS,SAAA,CAAU,QAAuB,KAAA,EAAuB;AACtE,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,CAAO,QAAA;AAC9B,EAAA,MAAM,IAAA,GAAO,OAAO,IAAA,CAAK,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,GAAI,MAAM,CAAA,CAAE,MAAA;AACnD,EAAA,OAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,MAAA,CAAO,QAAQ,IAAI,CAAA;AACxC","file":"index.js","sourcesContent":["/**\n * @demlik/tea/rate-limit — two rate limiters as pure state + ops.\n *\n * Same shape as `@demlik/tea/work-queue`'s `ops.ts`: a state type plus pure\n * transition functions. Host-agnostic by construction — no `Date.now()`, no\n * timers, no I/O. The caller injects the clock (`nowMs`) and wires the result\n * into its own reducer / interpret. This keeps the limiters inside TEA's\n * invariant 2 (transitions are pure: same inputs → same outputs, no `Date.now`)\n * so a consumer can fold them through `update` and exercise them with `replay`\n * / the pbt fold without a fake clock.\n *\n * Every op returns a NEW state value and never mutates its input (invariant 1 —\n * state is a value, not a place). The `tryConsume` / `record` ops return a\n * `[next, ok]` tuple so the caller branches on the decision while threading the\n * advanced state forward; `refill` / `remaining` are read-shaped helpers.\n *\n * Two complementary algorithms, picked by the cost the caller wants to pay:\n *\n * - **Token bucket** — O(1) state (four numbers), smooths bursts against a\n * steady refill rate. Allows a burst up to `capacity`, then throttles to\n * `refillPerSec`. Cheapest; the default choice.\n * - **Sliding window log** — O(limit) state (one timestamp per in-window\n * hit), exact \"no more than `limit` events per `windowMs`\". Pay the memory\n * for precise window edges when a token bucket's continuous refill would\n * let through too many at a boundary.\n *\n * NOT exported from the package root — reached via the `@demlik/tea/rate-limit`\n * subpath, same one-shape-per-package rule as `work-queue` and `do`.\n */\n\n// ===========================================================================\n// Token bucket\n// ===========================================================================\n\n/**\n * A token bucket: `tokens` of `capacity` available now, replenished at\n * `refillPerSec` tokens per second. `lastRefillMs` is the clock reading at\n * which `tokens` was last computed — `refill` advances it. All four fields are\n * plain numbers so the whole bucket is trivially JSON-serializable into a\n * consumer's `Store<S>`.\n */\nexport interface TokenBucket {\n readonly tokens: number;\n readonly capacity: number;\n readonly refillPerSec: number;\n readonly lastRefillMs: number;\n}\n\n/**\n * Create a full bucket. Starts at `capacity` tokens (the conventional \"a fresh\n * limiter lets the first burst through\" behavior) and stamps `lastRefillMs`\n * with the caller's clock so the first `refill` measures elapsed time from\n * boot, not from epoch 0.\n */\nexport function initBucket(capacity: number, refillPerSec: number, nowMs: number): TokenBucket {\n return {\n tokens: capacity,\n capacity,\n refillPerSec,\n lastRefillMs: nowMs,\n };\n}\n\n/**\n * Add the tokens accrued since `lastRefillMs`, clamped to `capacity`, and\n * advance `lastRefillMs` to `nowMs`. PURE — returns a new bucket; the input is\n * untouched.\n *\n * A `nowMs` at or before `lastRefillMs` (clock not advanced, or skewed\n * backwards) adds a non-positive amount; `Math.max(0, …)` floors the elapsed\n * term so a backwards clock can never *remove* tokens. `lastRefillMs` still\n * advances to `nowMs` so the bucket tracks the latest reading the caller\n * presented — the next forward step measures from there, not from a stale\n * future timestamp.\n */\nexport function refill(bucket: TokenBucket, nowMs: number): TokenBucket {\n const elapsedSec = Math.max(0, nowMs - bucket.lastRefillMs) / 1000;\n const replenished = bucket.tokens + elapsedSec * bucket.refillPerSec;\n return {\n ...bucket,\n tokens: Math.min(bucket.capacity, replenished),\n lastRefillMs: nowMs,\n };\n}\n\n/**\n * Refill against `nowMs`, then attempt to take `n` tokens (default 1).\n *\n * On success returns `[next, true]` with `n` subtracted; on failure returns\n * `[refilled, false]` — the refilled bucket (so the caller keeps the advanced\n * clock + accrued tokens) with no tokens spent. Branch on the boolean, thread\n * the bucket forward regardless:\n *\n * const [b1, ok] = tryConsume(bucket, nowMs);\n * return ok ? [{ ...state, bucket: b1 }, [doWork]] : [{ ...state, bucket: b1 }, []];\n *\n * PURE — input bucket is never mutated.\n */\nexport function tryConsume(\n bucket: TokenBucket,\n nowMs: number,\n n = 1,\n): readonly [TokenBucket, boolean] {\n const refilled = refill(bucket, nowMs);\n if (refilled.tokens >= n) {\n return [{ ...refilled, tokens: refilled.tokens - n }, true];\n }\n return [refilled, false];\n}\n\n// ===========================================================================\n// Sliding window log\n// ===========================================================================\n\n/**\n * A sliding-window log: the timestamps of every hit still inside the trailing\n * `windowMs`, capped at `limit` events per window. `hits` is `readonly` and\n * kept oldest-first (pruning slices the stale prefix off the front, recording\n * appends to the back), so the array is its own sorted index. Exact at the\n * window edge — unlike a token bucket's continuous refill, a hit leaves the\n * count the instant it ages past `windowMs`.\n */\nexport interface SlidingWindow {\n readonly hits: readonly number[];\n readonly windowMs: number;\n readonly limit: number;\n}\n\n/** Create an empty window. No hits recorded yet. */\nexport function initWindow(windowMs: number, limit: number): SlidingWindow {\n return { hits: [], windowMs, limit };\n}\n\n/**\n * Prune hits older than `nowMs - windowMs`, then record `nowMs` if there is\n * room. Returns `[next, true]` with the hit appended when `hits.length < limit`\n * after pruning, else `[pruned, false]` — the pruned window (capacity recovers\n * as old hits age out) with nothing appended.\n *\n * The cutoff is half-open: a hit exactly `windowMs` old is pruned (`<= cutoff`\n * drops it), so a hit recorded at `t` no longer counts at `t + windowMs`. That\n * makes the window's capacity fully recover `windowMs` after the last hit, the\n * behavior tests pin.\n *\n * PURE — input window is never mutated; `hits` is rebuilt by `filter` +\n * spread.\n */\nexport function record(window: SlidingWindow, nowMs: number): readonly [SlidingWindow, boolean] {\n const cutoff = nowMs - window.windowMs;\n const pruned = window.hits.filter((t) => t > cutoff);\n if (pruned.length < window.limit) {\n return [{ ...window, hits: [...pruned, nowMs] }, true];\n }\n return [{ ...window, hits: pruned }, false];\n}\n\n/**\n * How many more hits `record` would accept at `nowMs` without blocking —\n * `limit` minus the live (post-prune) hit count, floored at 0. Read-only: it\n * prunes a local copy to answer but returns no new window. Use it to render a\n * \"N requests remaining\" affordance without committing a hit.\n */\nexport function remaining(window: SlidingWindow, nowMs: number): number {\n const cutoff = nowMs - window.windowMs;\n const live = window.hits.filter((t) => t > cutoff).length;\n return Math.max(0, window.limit - live);\n}\n"]}
@@ -0,0 +1,72 @@
1
+ import { Store, Cmd, Sub, Machine, Runtime } from '../index.js';
2
+
3
+ /**
4
+ * @demlik/tea/react — React host adapter for `@demlik/tea`.
5
+ *
6
+ * The thinnest possible adapter that makes a tea machine consumable from a
7
+ * React component. Three rules (PRD §"React adapter"):
8
+ *
9
+ * 1. The runtime is the source of truth, NOT React state. We subscribe via
10
+ * `useSyncExternalStore(runtime.subscribe, runtime.getState,
11
+ * runtime.getState)`. React renders are driven by runtime state changes;
12
+ * React never owns the state.
13
+ * 2. The runtime is memoized per component-mount, not per render. The deps
14
+ * array is `[machine, opts.ctx, opts.store]` — IDENTITY, not value. See
15
+ * `README.md` for the ctx-identity footgun + fix.
16
+ * 3. Concurrent React is the design target. `useSyncExternalStore` is the
17
+ * React-blessed primitive for tearing-free external state.
18
+ *
19
+ * Client-only in v1 — components that call `useMachine` MUST be `"use client"`.
20
+ * Server Components consume data via Relay/RSC as today; tea machines own
21
+ * client-side stateful flows. No RSC streaming, no server-side state
22
+ * hydration. Crossing this boundary is a v2 concern.
23
+ */
24
+
25
+ /**
26
+ * Options passed to `useMachine`. The shape is intentionally minimal — `ctx`
27
+ * is required (every machine has one), `store` is optional (omit it for
28
+ * volatile-state machines).
29
+ *
30
+ * **Identity matters.** The runtime is memoized on `[machine, opts.ctx,
31
+ * opts.store]`. A new `ctx` reference rebuilds the runtime. Always `useMemo`
32
+ * (or otherwise stabilize) the ctx at the call site. See README.
33
+ */
34
+ interface UseMachineOpts<S, Ctx> {
35
+ ctx: Ctx;
36
+ store?: Store<S>;
37
+ }
38
+ /**
39
+ * Build and own a `Runtime<S, M>` for the lifetime of the component mount.
40
+ *
41
+ * - Builds via `useMemo(() => run(machine, opts), [machine, opts.ctx,
42
+ * opts.store])` — runtime is recreated when ANY of the three identities
43
+ * change.
44
+ * - Subscribes via `useSyncExternalStore(runtime.subscribe, runtime.getState,
45
+ * runtime.getState)` — tearing-free under React 18 concurrent rendering.
46
+ * - On unmount (or any dep change), `useEffect` cleanup calls
47
+ * `runtime.stop()` — drains the queue, runs every active sub cleanup,
48
+ * flushes state.
49
+ *
50
+ * Returns `[state, dispatch]` shaped like `useReducer`.
51
+ */
52
+ declare function useMachine<S, M extends {
53
+ type: string;
54
+ }, C extends Cmd, U extends Sub, Ctx>(machine: Machine<S, M, C, U, Ctx>, opts: UseMachineOpts<S, Ctx>): [S, (msg: M) => Promise<void>];
55
+ /**
56
+ * Lower-level escape hatch: consume an externally-built `Runtime<S, M>`.
57
+ *
58
+ * Use this when the runtime is owned by something OTHER than the component —
59
+ * a parent component, a test harness, a singleton. **The component does NOT
60
+ * call `runtime.stop()` on unmount** — the caller owns the lifecycle.
61
+ *
62
+ * Primary use case in this repo: component tests. Build a runtime with
63
+ * `@demlik/tea/mem` and pre-seed it via `replay`, then mount the component with
64
+ * `useRuntime(testRuntime)` to render in any phase.
65
+ *
66
+ * Returns `[state, dispatch]` — same shape as `useMachine`.
67
+ */
68
+ declare function useRuntime<S, M extends {
69
+ type: string;
70
+ }>(runtime: Runtime<S, M>): [S, (msg: M) => Promise<void>];
71
+
72
+ export { type UseMachineOpts, useMachine, useRuntime };
@@ -0,0 +1,33 @@
1
+ import { run } from '../chunk-CMG2KMWX.js';
2
+ import { useMemo, useSyncExternalStore, useEffect } from 'react';
3
+
4
+ function useMachine(machine, opts) {
5
+ const ctx = opts.ctx;
6
+ const store = opts.store;
7
+ const runtime = useMemo(() => run(machine, { ctx, store }), [machine, ctx, store]);
8
+ const preliminaryState = useMemo(() => machine.init(null, ctx)[0], [machine, ctx]);
9
+ const getSnapshot = () => {
10
+ try {
11
+ return runtime.getState();
12
+ } catch {
13
+ return preliminaryState;
14
+ }
15
+ };
16
+ const state = useSyncExternalStore(runtime.subscribe, getSnapshot, getSnapshot);
17
+ useEffect(
18
+ () => () => {
19
+ runtime.stop().catch(() => {
20
+ });
21
+ },
22
+ [runtime]
23
+ );
24
+ return [state, runtime.dispatch];
25
+ }
26
+ function useRuntime(runtime) {
27
+ const state = useSyncExternalStore(runtime.subscribe, runtime.getState, runtime.getState);
28
+ return [state, runtime.dispatch];
29
+ }
30
+
31
+ export { useMachine, useRuntime };
32
+ //# sourceMappingURL=index.js.map
33
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/index.ts"],"names":[],"mappings":";;;AAuDO,SAAS,UAAA,CACd,SACA,IAAA,EACgC;AAMhC,EAAA,MAAM,MAAM,IAAA,CAAK,GAAA;AACjB,EAAA,MAAM,QAAQ,IAAA,CAAK,KAAA;AACnB,EAAA,MAAM,OAAA,GAAU,OAAA,CAAuB,MAAM,GAAA,CAAI,SAAS,EAAE,GAAA,EAAK,KAAA,EAAO,CAAA,EAAG,CAAC,OAAA,EAAS,GAAA,EAAK,KAAK,CAAC,CAAA;AAahG,EAAA,MAAM,gBAAA,GAAmB,OAAA,CAAW,MAAM,OAAA,CAAQ,IAAA,CAAK,IAAA,EAAM,GAAG,CAAA,CAAE,CAAC,CAAA,EAAG,CAAC,OAAA,EAAS,GAAG,CAAC,CAAA;AAMpF,EAAA,MAAM,cAAc,MAAS;AAC3B,IAAA,IAAI;AACF,MAAA,OAAO,QAAQ,QAAA,EAAS;AAAA,IAC1B,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,gBAAA;AAAA,IACT;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,KAAA,GAAQ,oBAAA,CAAqB,OAAA,CAAQ,SAAA,EAAW,aAAa,WAAW,CAAA;AAC9E,EAAA,SAAA;AAAA,IACE,MAAM,MAAM;AAIV,MAAA,OAAA,CAAQ,IAAA,EAAK,CAAE,KAAA,CAAM,MAAM;AAAA,MAAC,CAAC,CAAA;AAAA,IAC/B,CAAA;AAAA,IACA,CAAC,OAAO;AAAA,GACV;AACA,EAAA,OAAO,CAAC,KAAA,EAAO,OAAA,CAAQ,QAAQ,CAAA;AACjC;AAeO,SAAS,WACd,OAAA,EACgC;AAChC,EAAA,MAAM,QAAQ,oBAAA,CAAqB,OAAA,CAAQ,WAAW,OAAA,CAAQ,QAAA,EAAU,QAAQ,QAAQ,CAAA;AACxF,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 { type Cmd, type Machine, type Runtime, run, type Store, type Sub } from \"../index\";\nimport { useEffect, useMemo, useSyncExternalStore } from \"react\";\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<S, M extends { type: string }, C extends Cmd, U extends Sub, Ctx>(\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 const runtime = useMemo<Runtime<S, M>>(() => run(machine, { ctx, store }), [machine, ctx, store]);\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()`). Once the runtime\n // boots, `runtime.getState()` returns the real (possibly loaded) state.\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>(() => machine.init(null, ctx)[0], [machine, ctx]);\n\n // `getSnapshot` reads the runtime if it has booted, else falls back to\n // the preliminary state. The runtime's `getState` throws pre-boot when a\n // store is configured — we catch that and substitute the preliminary\n // value.\n const getSnapshot = (): S => {\n try {\n return runtime.getState();\n } catch {\n return preliminaryState;\n }\n };\n\n const state = useSyncExternalStore(runtime.subscribe, getSnapshot, getSnapshot);\n useEffect(\n () => () => {\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 runtime.stop().catch(() => {});\n },\n [runtime],\n );\n return [state, runtime.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(runtime.subscribe, runtime.getState, runtime.getState);\n return [state, runtime.dispatch];\n}\n"]}