@demlik/tea 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/agent/index.d.ts +907 -0
- package/dist/agent/index.js +20 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/authed-call/index.d.ts +119 -0
- package/dist/authed-call/index.js +148 -0
- package/dist/authed-call/index.js.map +1 -0
- package/dist/await-terminal/index.d.ts +99 -0
- package/dist/await-terminal/index.js +75 -0
- package/dist/await-terminal/index.js.map +1 -0
- package/dist/batch-window/index.d.ts +259 -0
- package/dist/batch-window/index.js +43 -0
- package/dist/batch-window/index.js.map +1 -0
- package/dist/cache/index.d.ts +2 -2
- package/dist/cache/index.js +2 -50
- package/dist/cache/index.js.map +1 -1
- package/dist/{chunk-CMG2KMWX.js → chunk-2WZHS4P2.js} +220 -177
- package/dist/chunk-2WZHS4P2.js.map +1 -0
- package/dist/chunk-2YOE5PIS.js +45 -0
- package/dist/chunk-2YOE5PIS.js.map +1 -0
- package/dist/chunk-3545BOYL.js +65 -0
- package/dist/chunk-3545BOYL.js.map +1 -0
- package/dist/chunk-5VNWZADP.js +49 -0
- package/dist/chunk-5VNWZADP.js.map +1 -0
- package/dist/{chunk-ZRUST6BK.js → chunk-7FYQGDMF.js} +2 -2
- package/dist/chunk-7FYQGDMF.js.map +1 -0
- package/dist/chunk-7VEG4KHB.js +202 -0
- package/dist/chunk-7VEG4KHB.js.map +1 -0
- package/dist/{chunk-PYBQREFU.js → chunk-A56GNPI7.js} +20 -6
- package/dist/chunk-A56GNPI7.js.map +1 -0
- package/dist/chunk-BNJYE4AU.js +157 -0
- package/dist/chunk-BNJYE4AU.js.map +1 -0
- package/dist/chunk-BYQUGDD3.js +16 -0
- package/dist/chunk-BYQUGDD3.js.map +1 -0
- package/dist/chunk-D5EQDLDV.js +186 -0
- package/dist/chunk-D5EQDLDV.js.map +1 -0
- package/dist/chunk-EDGDR5XX.js +60 -0
- package/dist/chunk-EDGDR5XX.js.map +1 -0
- package/dist/chunk-EIZJTYKR.js +93 -0
- package/dist/chunk-EIZJTYKR.js.map +1 -0
- package/dist/chunk-EQRUAP2A.js +56 -0
- package/dist/chunk-EQRUAP2A.js.map +1 -0
- package/dist/chunk-FH5RUIIL.js +61 -0
- package/dist/chunk-FH5RUIIL.js.map +1 -0
- package/dist/chunk-HJO6VNQI.js +15 -0
- package/dist/chunk-HJO6VNQI.js.map +1 -0
- package/dist/chunk-IFBQJYGI.js +132 -0
- package/dist/chunk-IFBQJYGI.js.map +1 -0
- package/dist/chunk-IN33Q4VJ.js +52 -0
- package/dist/chunk-IN33Q4VJ.js.map +1 -0
- package/dist/chunk-JELSZN25.js +47 -0
- package/dist/chunk-JELSZN25.js.map +1 -0
- package/dist/chunk-K575PT4S.js +58 -0
- package/dist/chunk-K575PT4S.js.map +1 -0
- package/dist/chunk-LEC4U6CX.js +91 -0
- package/dist/chunk-LEC4U6CX.js.map +1 -0
- package/dist/{chunk-Y5JM3UUH.js → chunk-NSDFA6M6.js} +75 -3
- package/dist/chunk-NSDFA6M6.js.map +1 -0
- package/dist/chunk-QGZW4W3E.js +20 -0
- package/dist/chunk-QGZW4W3E.js.map +1 -0
- package/dist/{chunk-PYOCUBQJ.js → chunk-RGTPPF4U.js} +22 -15
- package/dist/chunk-RGTPPF4U.js.map +1 -0
- package/dist/chunk-RVJFGRGK.js +222 -0
- package/dist/chunk-RVJFGRGK.js.map +1 -0
- package/dist/chunk-RVU3RBE6.js +31 -0
- package/dist/chunk-RVU3RBE6.js.map +1 -0
- package/dist/chunk-SC3FAFZN.js +53 -0
- package/dist/chunk-SC3FAFZN.js.map +1 -0
- package/dist/chunk-SINXE47Y.js +55 -0
- package/dist/chunk-SINXE47Y.js.map +1 -0
- package/dist/chunk-TAO4YOLO.js +87 -0
- package/dist/chunk-TAO4YOLO.js.map +1 -0
- package/dist/chunk-TC4HWABA.js +506 -0
- package/dist/chunk-TC4HWABA.js.map +1 -0
- package/dist/chunk-TGG26XMK.js +52 -0
- package/dist/chunk-TGG26XMK.js.map +1 -0
- package/dist/chunk-UV6UWGEO.js +147 -0
- package/dist/chunk-UV6UWGEO.js.map +1 -0
- package/dist/chunk-UYWUVEP6.js +158 -0
- package/dist/chunk-UYWUVEP6.js.map +1 -0
- package/dist/chunk-WUV3PSCZ.js +54 -0
- package/dist/chunk-WUV3PSCZ.js.map +1 -0
- package/dist/chunk-XRXKH7KA.js +268 -0
- package/dist/chunk-XRXKH7KA.js.map +1 -0
- package/dist/circuit-breaker/index.js +1 -51
- package/dist/circuit-breaker/index.js.map +1 -1
- package/dist/core-CymXZCeY.d.ts +178 -0
- package/dist/deadline/index.d.ts +24 -5
- package/dist/deadline/index.js +4 -18
- package/dist/deadline/index.js.map +1 -1
- package/dist/debounce/index.js +1 -54
- package/dist/debounce/index.js.map +1 -1
- package/dist/devtools/index.d.ts +1 -1
- package/dist/devtools/index.js +18 -6
- package/dist/devtools/index.js.map +1 -1
- package/dist/do/index.d.ts +1732 -92
- package/dist/do/index.js +677 -31
- package/dist/do/index.js.map +1 -1
- package/dist/durable-effects-2MDxDs45.d.ts +180 -0
- package/dist/extension/index.d.ts +31 -13
- package/dist/extension/index.js +5 -3
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/react.js +8 -3
- package/dist/extension/react.js.map +1 -1
- package/dist/extension/subs/index.d.ts +2 -2
- package/dist/extension/subs/index.js.map +1 -1
- package/dist/extension/test-utils.js +11 -3
- package/dist/extension/test-utils.js.map +1 -1
- package/dist/fan-out/index.d.ts +239 -0
- package/dist/fan-out/index.js +5 -0
- package/dist/fan-out/index.js.map +1 -0
- package/dist/idempotency/adapter.d.ts +62 -0
- package/dist/idempotency/adapter.js +4 -0
- package/dist/idempotency/adapter.js.map +1 -0
- package/dist/idempotency/index.js +1 -56
- package/dist/idempotency/index.js.map +1 -1
- package/dist/idempotent-intake/index.d.ts +186 -0
- package/dist/idempotent-intake/index.js +7 -0
- package/dist/idempotent-intake/index.js.map +1 -0
- package/dist/index.d.ts +341 -191
- package/dist/index.js +2 -1
- package/dist/llm-call/index.d.ts +309 -0
- package/dist/llm-call/index.js +14 -0
- package/dist/llm-call/index.js.map +1 -0
- package/dist/machine-viz/index.d.ts +1 -1
- package/dist/machine-viz/index.js +1 -1
- package/dist/machine-viz/index.js.map +1 -1
- package/dist/mem/index.d.ts +1 -0
- package/dist/monitored-run/index.d.ts +253 -0
- package/dist/monitored-run/index.js +10 -0
- package/dist/monitored-run/index.js.map +1 -0
- package/dist/node/index.d.ts +5 -5
- package/dist/node/index.js.map +1 -1
- package/dist/paginated-walk/index.d.ts +234 -0
- package/dist/paginated-walk/index.js +15 -0
- package/dist/paginated-walk/index.js.map +1 -0
- package/dist/paginator/index.d.ts +271 -0
- package/dist/paginator/index.js +3 -0
- package/dist/paginator/index.js.map +1 -0
- package/dist/parity/index.d.ts +86 -0
- package/dist/parity/index.js +77 -0
- package/dist/parity/index.js.map +1 -0
- package/dist/pbt/arbitraries/index.d.ts +1 -1
- package/dist/pbt/arbitraries/index.js +1 -1
- package/dist/pbt/index.d.ts +1 -1
- package/dist/pbt/index.js +5 -4
- package/dist/pbt/index.js.map +1 -1
- package/dist/pbt/runners/index.d.ts +1 -1
- package/dist/pbt/runners/index.js +3 -2
- package/dist/poller/index.d.ts +288 -0
- package/dist/poller/index.js +79 -0
- package/dist/poller/index.js.map +1 -0
- package/dist/prediction/index.d.ts +109 -0
- package/dist/prediction/index.js +4 -0
- package/dist/prediction/index.js.map +1 -0
- package/dist/protocol-B5YmJr0d.d.ts +45 -0
- package/dist/pure/index.d.ts +2 -0
- package/dist/pure/index.js +4 -0
- package/dist/pure/index.js.map +1 -0
- package/dist/raft/demo.d.ts +105 -0
- package/dist/raft/demo.js +172 -0
- package/dist/raft/demo.js.map +1 -0
- package/dist/raft/index.d.ts +548 -0
- package/dist/raft/index.js +7 -0
- package/dist/raft/index.js.map +1 -0
- package/dist/raft/sim.d.ts +222 -0
- package/dist/raft/sim.js +8 -0
- package/dist/raft/sim.js.map +1 -0
- package/dist/raft/timeline.d.ts +46 -0
- package/dist/raft/timeline.js +62 -0
- package/dist/raft/timeline.js.map +1 -0
- package/dist/rate-limit/index.js +1 -43
- package/dist/rate-limit/index.js.map +1 -1
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +39 -20
- package/dist/react/index.js.map +1 -1
- package/dist/reconciler/index.d.ts +192 -0
- package/dist/reconciler/index.js +166 -0
- package/dist/reconciler/index.js.map +1 -0
- package/dist/recorder/index.d.ts +7 -3
- package/dist/recorder/index.js +1 -155
- package/dist/recorder/index.js.map +1 -1
- package/dist/resilient-call/index.d.ts +251 -0
- package/dist/resilient-call/index.js +13 -0
- package/dist/resilient-call/index.js.map +1 -0
- package/dist/retry-backoff/index.d.ts +42 -5
- package/dist/retry-backoff/index.js +1 -35
- package/dist/retry-backoff/index.js.map +1 -1
- package/dist/saga/index.d.ts +225 -0
- package/dist/saga/index.js +150 -0
- package/dist/saga/index.js.map +1 -0
- package/dist/snapshot/index.d.ts +334 -0
- package/dist/snapshot/index.js +5 -0
- package/dist/snapshot/index.js.map +1 -0
- package/dist/subs/index.d.ts +48 -10
- package/dist/subs/index.js +1 -1
- package/dist/testing/index.d.ts +107 -2
- package/dist/testing/index.js +204 -6
- package/dist/testing/index.js.map +1 -1
- package/dist/throttle/index.js +1 -52
- package/dist/throttle/index.js.map +1 -1
- package/dist/throttled-input/index.d.ts +372 -0
- package/dist/throttled-input/index.js +88 -0
- package/dist/throttled-input/index.js.map +1 -0
- package/dist/token-refresh/index.d.ts +215 -0
- package/dist/token-refresh/index.js +6 -0
- package/dist/token-refresh/index.js.map +1 -0
- package/dist/trace-replay/index.d.ts +11 -2
- package/dist/trace-replay/index.js +3 -55
- package/dist/trace-replay/index.js.map +1 -1
- package/dist/{types-CwBC5wMn.d.ts → types-N9LZbdnr.d.ts} +1 -1
- package/dist/with-deadline/index.d.ts +196 -0
- package/dist/with-deadline/index.js +130 -0
- package/dist/with-deadline/index.js.map +1 -0
- package/dist/with-resilience/index.d.ts +205 -0
- package/dist/with-resilience/index.js +274 -0
- package/dist/with-resilience/index.js.map +1 -0
- package/dist/with-telemetry/index.d.ts +149 -0
- package/dist/with-telemetry/index.js +95 -0
- package/dist/with-telemetry/index.js.map +1 -0
- package/dist/work-queue/adapter.d.ts +82 -0
- package/dist/work-queue/adapter.js +4 -0
- package/dist/work-queue/adapter.js.map +1 -0
- package/dist/work-queue/index.d.ts +25 -11
- package/dist/work-queue/index.js +1 -46
- package/dist/work-queue/index.js.map +1 -1
- package/dist/work-queue/ops.d.ts +69 -0
- package/dist/work-queue/ops.js +3 -0
- package/dist/work-queue/ops.js.map +1 -0
- package/dist/workflow/index.d.ts +444 -0
- package/dist/workflow/index.js +225 -0
- package/dist/workflow/index.js.map +1 -0
- package/package.json +162 -25
- package/dist/chunk-CMG2KMWX.js.map +0 -1
- package/dist/chunk-PYBQREFU.js.map +0 -1
- package/dist/chunk-PYOCUBQJ.js.map +0 -1
- package/dist/chunk-Y5JM3UUH.js.map +0 -1
- package/dist/chunk-ZRUST6BK.js.map +0 -1
package/dist/do/index.d.ts
CHANGED
|
@@ -1,9 +1,1685 @@
|
|
|
1
|
-
import { Store,
|
|
1
|
+
import { Store, Runtime, BootingRuntime } from '../index.js';
|
|
2
|
+
import { P as PendingEffectsRecorder, b as EffectOwed, a as EffectConfirmed } from '../durable-effects-2MDxDs45.js';
|
|
3
|
+
export { D as DeliveryId, E as EffectLedgerEvent, O as OwedEffect, d as PendingEffectsLedger, c as applyEffectEvent, e as emptyLedger, f as foldLedger, i as isOwed, p as pendingEffectsLedger, s as survivingEffects } from '../durable-effects-2MDxDs45.js';
|
|
4
|
+
import { RetryPolicy, Rng } from '../retry-backoff/index.js';
|
|
5
|
+
import { C as Cmd, S as Sub, M as Machine } from '../core-CymXZCeY.js';
|
|
6
|
+
import { AgentTurn, AgentState, AgentMachineMsg, AgentEvent, AgentStatus } from '../agent/index.js';
|
|
7
|
+
import '../fan-out/index.js';
|
|
8
|
+
import '../work-queue/index.js';
|
|
9
|
+
import '../llm-call/index.js';
|
|
10
|
+
import '../resilient-call/index.js';
|
|
11
|
+
import '../cache/index.js';
|
|
12
|
+
import '../types-N9LZbdnr.js';
|
|
13
|
+
import '../circuit-breaker/index.js';
|
|
14
|
+
import '../deadline/index.js';
|
|
15
|
+
import '../protocol-B5YmJr0d.js';
|
|
16
|
+
import '../rate-limit/index.js';
|
|
17
|
+
import '../monitored-run/index.js';
|
|
18
|
+
import '../snapshot/index.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @demlik/tea/do — `stepHost`, the HTTP-PULL control carrier (#92).
|
|
22
|
+
*
|
|
23
|
+
* ## Carrier selection — pick by WHO INITIATES the message
|
|
24
|
+
*
|
|
25
|
+
* `stepHost` is a SIBLING to {@link acceptCommandSocket}, never a replacement.
|
|
26
|
+
* Both carriers commonly run together in ONE Durable Object; choose between
|
|
27
|
+
* them per *plane*, by who sends the next message:
|
|
28
|
+
*
|
|
29
|
+
* - **Pull (this file — `stepHost` over `/step`)** — the CONTROL plane. One
|
|
30
|
+
* driver (the hands) initiates every message: it asks for the next tool,
|
|
31
|
+
* runs it, posts the result, asks again. There is exactly one outstanding
|
|
32
|
+
* question at a time and the DO is COLD between steps — nothing is owed
|
|
33
|
+
* while no request is in flight. Hibernation is the resting state, not a
|
|
34
|
+
* failure: each POST wakes the DO, advances the run one step from its
|
|
35
|
+
* durable checkpoint, and lets it hibernate again.
|
|
36
|
+
* - **Push (`acceptCommandSocket` / `sseHub` — WS + SSE)** — the VIEW /
|
|
37
|
+
* fan-out plane. The DO initiates UNSOLICITED messages: broadcast a frame
|
|
38
|
+
* to every connected client, stream a live dashboard, push an event no
|
|
39
|
+
* client asked for. A long-lived socket is the right carrier because the
|
|
40
|
+
* sender is the DO, not a polling client.
|
|
41
|
+
*
|
|
42
|
+
* The rule in one line: **pull when the client drives a cold-between-steps
|
|
43
|
+
* control loop; push when the DO drives unsolicited fan-out.** A hibernating
|
|
44
|
+
* agent whose only traffic is "run the next tool" wants pull — that is the case
|
|
45
|
+
* Binclusive #1812 / #1814 adopted this carrier for.
|
|
46
|
+
*
|
|
47
|
+
* ## Why pull is hibernation-correct
|
|
48
|
+
*
|
|
49
|
+
* The WS push carrier keeps a socket open across steps; if the DO evicts, the
|
|
50
|
+
* socket dies (or, with #91's `acceptDurableCommandSocket`, survives but still
|
|
51
|
+
* holds an in-flight round-trip the heap forgot). The pull carrier holds NO
|
|
52
|
+
* cross-step state in the heap. Every `/step` POST is a FRESH request that:
|
|
53
|
+
*
|
|
54
|
+
* 1. **validates** the per-run capability token (constant-time compare),
|
|
55
|
+
* 2. **re-arms** the eviction-safe give-up alarm via `ctx.storage.setAlarm`
|
|
56
|
+
* (the DO-native alarm API — the `do_alarm` Sub was dropped, see the
|
|
57
|
+
* `host.ts` header; the alarm is a host concern, not a machine Sub),
|
|
58
|
+
* 3. **settles** the posted tool result IDEMPOTENTLY through
|
|
59
|
+
* `idempotent-intake` — a re-POSTed result is absorbed exactly once and
|
|
60
|
+
* the duplicate replays the same next step,
|
|
61
|
+
* 4. **resumes** the run from its durable checkpoint (the #68 event-sourced
|
|
62
|
+
* store / #91 in-flight ledger) by dispatching the settle and running to
|
|
63
|
+
* DISPATCH-QUIESCENCE,
|
|
64
|
+
* 5. **returns** the next step (the next tool call) — or the terminal output
|
|
65
|
+
* read off `runtime.result()` — and lets the DO HIBERNATE.
|
|
66
|
+
*
|
|
67
|
+
* No warm loop is held between responses. The durable checkpoint IS the loop's
|
|
68
|
+
* continuation; the heap is disposable.
|
|
69
|
+
*
|
|
70
|
+
* ## It is ASSEMBLY over bricks tea already ships
|
|
71
|
+
*
|
|
72
|
+
* - `idempotent-intake` (`createIntake`) — the dedupe-by-key receive-once
|
|
73
|
+
* settle. The posted result's `callId` is the idempotency key, so a
|
|
74
|
+
* re-POST lands as a duplicate and replays the cached decision.
|
|
75
|
+
* - `poller` primitives (`retry-backoff`) — the hands-side {@link runStepLoop}
|
|
76
|
+
* pull loop: absolute deadline + backoff + dedupe-by-callId.
|
|
77
|
+
* - the #68/#91 durable checkpoint — read by the consumer's `resume` callback
|
|
78
|
+
* (it dispatches the settle into the runtime, whose `doEventSourcedStore`
|
|
79
|
+
* rebuilt state from the log on this cold wake).
|
|
80
|
+
*
|
|
81
|
+
* This file owns NONE of those primitives — it wires them into the `/step`
|
|
82
|
+
* request/response contract and the token lifecycle.
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The tool result the hands POST back for the step they just executed. `callId`
|
|
87
|
+
* matches the step the previous response handed out (it is BOTH the correlation
|
|
88
|
+
* id and the idempotency key — a re-POST carries the same `callId` and is
|
|
89
|
+
* absorbed once). `result` is the opaque tool output `R`.
|
|
90
|
+
*/
|
|
91
|
+
interface StepResult<R> {
|
|
92
|
+
readonly callId: string;
|
|
93
|
+
readonly result: R;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A `/step` request. `token` is the per-run capability (validated
|
|
97
|
+
* constant-time). `runId` names the run. `posted` is the result of the
|
|
98
|
+
* previously-returned step, or `null` on the FIRST request (no step has been
|
|
99
|
+
* handed out yet, so there is nothing to settle).
|
|
100
|
+
*/
|
|
101
|
+
interface StepRequest<R> {
|
|
102
|
+
readonly token: string;
|
|
103
|
+
readonly runId: string;
|
|
104
|
+
readonly posted: StepResult<R> | null;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The next tool call to execute — the "continue" arm of a `/step` response.
|
|
108
|
+
* `callId` is the correlation/idempotency id the hands echo back in the next
|
|
109
|
+
* request's `posted`. `tool` + `input` describe the call (opaque to the host).
|
|
110
|
+
*/
|
|
111
|
+
interface NextStep<I> {
|
|
112
|
+
readonly callId: string;
|
|
113
|
+
readonly tool: string;
|
|
114
|
+
readonly input: I;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* A `/step` response — a discriminated union on `done`:
|
|
118
|
+
* - `{ done: false, step }` — execute `step`, POST its result, ask again.
|
|
119
|
+
* - `{ done: true, output }` — the run is terminal; `output` is its result.
|
|
120
|
+
*/
|
|
121
|
+
type StepResponse<I, O> = {
|
|
122
|
+
readonly done: false;
|
|
123
|
+
readonly step: NextStep<I>;
|
|
124
|
+
} | {
|
|
125
|
+
readonly done: true;
|
|
126
|
+
readonly output: O;
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Mint a fresh per-run capability token: 32 random bytes, hex-encoded. Uses the
|
|
130
|
+
* Web Crypto `crypto.getRandomValues` available in the Workers runtime (and in
|
|
131
|
+
* Node's test runtime via the global `crypto`). Persist the returned string in
|
|
132
|
+
* DO storage keyed by runId; hand the SAME string to the hands once, at run
|
|
133
|
+
* start.
|
|
134
|
+
*/
|
|
135
|
+
declare function mintRunToken(randomBytes?: (n: number) => Uint8Array): string;
|
|
136
|
+
/**
|
|
137
|
+
* Constant-time string equality. Returns `true` iff `a` and `b` are
|
|
138
|
+
* byte-for-byte equal, in time that depends ONLY on `a.length` — never on WHERE
|
|
139
|
+
* the first mismatch is. A length mismatch still returns `false`, but the
|
|
140
|
+
* comparison loop always runs `a.length` iterations against a same-length view,
|
|
141
|
+
* so the early-exit timing oracle a naive `===` leaks is closed.
|
|
142
|
+
*
|
|
143
|
+
* The XOR-accumulate idiom: fold every position's difference into one
|
|
144
|
+
* accumulator and test it once at the end. No `break`, no short-circuit.
|
|
145
|
+
*/
|
|
146
|
+
declare function constantTimeEqual(a: string, b: string): boolean;
|
|
147
|
+
/**
|
|
148
|
+
* The DO-native alarm slice `stepHost` re-arms. A real `DurableObjectStorage`
|
|
149
|
+
* satisfies this structurally; a test supplies a fake (no live Workers runtime).
|
|
150
|
+
* Only `setAlarm` is needed — the give-up deadline is re-armed on every step so
|
|
151
|
+
* a run abandoned mid-loop (the hands stop POSTing) is eventually evicted by the
|
|
152
|
+
* alarm firing, not left resident forever.
|
|
153
|
+
*/
|
|
154
|
+
interface AlarmStorage {
|
|
155
|
+
setAlarm(scheduledTime: number): void | Promise<void>;
|
|
156
|
+
}
|
|
157
|
+
/** The minimal `ctx` slice `stepHost` reads — just the alarm-bearing storage. */
|
|
158
|
+
interface StepCtx {
|
|
159
|
+
readonly storage: AlarmStorage;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The run-specific operations `stepHost` orchestrates. The consumer implements
|
|
163
|
+
* these against its runtime + storage; `stepHost` never reaches into the agent
|
|
164
|
+
* machine itself (that lives in `../agent`, off-limits to this carrier).
|
|
165
|
+
*/
|
|
166
|
+
interface StepEngine<R, I, O> {
|
|
167
|
+
/**
|
|
168
|
+
* The persisted capability token for `runId`, or `null` if the run is
|
|
169
|
+
* unknown. Read from DO storage. `stepHost` compares the request token
|
|
170
|
+
* against this constant-time.
|
|
171
|
+
*/
|
|
172
|
+
tokenFor(runId: string): Promise<string | null> | string | null;
|
|
173
|
+
/**
|
|
174
|
+
* Settle the posted tool result into the run, then RESUME from the durable
|
|
175
|
+
* checkpoint: dispatch the settle and run to DISPATCH-QUIESCENCE (e.g.
|
|
176
|
+
* `dispatchToIdle(runtime, settleMsg)`). Called at most once per distinct
|
|
177
|
+
* `callId` — `stepHost` dedupes re-POSTs via `idempotent-intake` BEFORE
|
|
178
|
+
* calling this, so the side effect runs exactly once. On the first request
|
|
179
|
+
* (`posted === null`) `stepHost` skips settle and calls {@link nextStep}
|
|
180
|
+
* directly to hand out the first step.
|
|
181
|
+
*/
|
|
182
|
+
resume(runId: string, posted: StepResult<R>): Promise<void> | void;
|
|
183
|
+
/**
|
|
184
|
+
* Read the next step the run is now waiting on — the next tool call — off the
|
|
185
|
+
* resumed runtime's durable state, or `null` if the run has reached its
|
|
186
|
+
* terminal State (then {@link terminalOutput} supplies the output).
|
|
187
|
+
*/
|
|
188
|
+
nextStep(runId: string): Promise<NextStep<I> | null> | NextStep<I> | null;
|
|
189
|
+
/**
|
|
190
|
+
* Read the run's terminal output (off `runtime.result()`). Only called when
|
|
191
|
+
* {@link nextStep} returned `null`.
|
|
192
|
+
*/
|
|
193
|
+
terminalOutput(runId: string): Promise<O> | O;
|
|
194
|
+
}
|
|
195
|
+
/** Tuning for `stepHost`'s give-up alarm. */
|
|
196
|
+
interface StepHostConfig {
|
|
197
|
+
/**
|
|
198
|
+
* How far ahead, in ms, to re-arm the give-up alarm on every step. A run that
|
|
199
|
+
* stops POSTing for longer than this is abandoned and the alarm fires (the
|
|
200
|
+
* consumer's `alarm()` handler tears the run down). Defaults to 5 minutes.
|
|
201
|
+
*/
|
|
202
|
+
readonly giveUpAfterMs?: number;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* A structured `/step` outcome `stepHost` returns — the response body plus the
|
|
206
|
+
* HTTP status the consumer's route should send. Kept transport-agnostic (no
|
|
207
|
+
* `Response`) so the same handler is unit-testable without a live `fetch`.
|
|
208
|
+
*/
|
|
209
|
+
type StepOutcome<I, O> = {
|
|
210
|
+
readonly status: 200;
|
|
211
|
+
readonly body: StepResponse<I, O>;
|
|
212
|
+
} | {
|
|
213
|
+
readonly status: 401 | 404;
|
|
214
|
+
readonly body: {
|
|
215
|
+
readonly error: string;
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
/**
|
|
219
|
+
* Build the `/step` handler for a DO that drives one or more runs over the pull
|
|
220
|
+
* carrier. Returns `handle(request, now)` — call it from the DO's `/step`
|
|
221
|
+
* route, map the `StepOutcome` to a `Response`, and let the DO hibernate.
|
|
222
|
+
*
|
|
223
|
+
* The handler is STATELESS between calls except for its `idempotent-intake`
|
|
224
|
+
* dedupe ledger, which the consumer should itself persist alongside the run's
|
|
225
|
+
* checkpoint if cross-eviction dedupe of an identical re-POST is required; the
|
|
226
|
+
* in-isolate ledger here dedupes re-POSTs within one activation (the common
|
|
227
|
+
* "the hands' POST timed out and got retried" case), and the underlying
|
|
228
|
+
* `resume` is itself idempotent against the durable checkpoint, so a re-POST
|
|
229
|
+
* that crosses an eviction re-settles harmlessly.
|
|
230
|
+
*
|
|
231
|
+
* @param ctx the DO state slice (alarm-bearing storage)
|
|
232
|
+
* @param engine the run-specific operations
|
|
233
|
+
* @param config alarm tuning
|
|
234
|
+
*/
|
|
235
|
+
declare function stepHost<R, I, O>(ctx: StepCtx, engine: StepEngine<R, I, O>, config?: StepHostConfig): {
|
|
236
|
+
handle(request: StepRequest<R>, now: number): Promise<StepOutcome<I, O>>;
|
|
237
|
+
};
|
|
238
|
+
/** The `/step` transport the hands POST through (injected; faked in tests). */
|
|
239
|
+
type StepTransport<R, I, O> = (request: StepRequest<R>) => Promise<StepResponse<I, O>>;
|
|
240
|
+
/** Execute one tool call and produce its result (the consumer's hands). */
|
|
241
|
+
type ExecuteStep<I, R> = (step: NextStep<I>) => Promise<R> | R;
|
|
242
|
+
/** Tuning for {@link runStepLoop}. */
|
|
243
|
+
interface RunStepLoopConfig {
|
|
244
|
+
/**
|
|
245
|
+
* Absolute wall-clock deadline (ms epoch). The loop stops asking once `now()`
|
|
246
|
+
* reaches it, surfacing a `deadline_exceeded` outcome. Mirrors the poller's
|
|
247
|
+
* absolute-deadline contract (a resumed loop honors the SAME moment).
|
|
248
|
+
*/
|
|
249
|
+
readonly deadlineMs: number;
|
|
250
|
+
/** Backoff policy for FAILED `/step` POSTs. Defaults to `defaultRetryPolicy`. */
|
|
251
|
+
readonly retry?: RetryPolicy;
|
|
252
|
+
/** Clock, injected for determinism. Defaults to `Date.now`. */
|
|
253
|
+
readonly now?: () => number;
|
|
254
|
+
/** `(ms) => Promise` sleep, injected for determinism. Defaults to real sleep. */
|
|
255
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
256
|
+
/** Backoff jitter RNG, injected for determinism. Defaults to `Math.random`. */
|
|
257
|
+
readonly rng?: Rng;
|
|
258
|
+
}
|
|
259
|
+
/** The terminal outcome of a full {@link runStepLoop} drive. */
|
|
260
|
+
type StepLoopOutcome<O> = {
|
|
261
|
+
readonly kind: "done";
|
|
262
|
+
readonly output: O;
|
|
263
|
+
} | {
|
|
264
|
+
readonly kind: "deadline_exceeded";
|
|
265
|
+
} | {
|
|
266
|
+
readonly kind: "gave_up";
|
|
267
|
+
readonly error: unknown;
|
|
268
|
+
};
|
|
269
|
+
/**
|
|
270
|
+
* Drive the pull loop to completion. Starting from `{ posted: null }`, POST
|
|
271
|
+
* `/step`, and for each non-terminal response execute the step and POST its
|
|
272
|
+
* result, until the response is terminal (`done: true`) — then return its
|
|
273
|
+
* output.
|
|
274
|
+
*
|
|
275
|
+
* Resilience composed from `poller`'s primitives:
|
|
276
|
+
* - **absolute deadline** — `config.deadlineMs`. Before each POST the loop
|
|
277
|
+
* checks `now() >= deadlineMs` and stops with `deadline_exceeded` rather
|
|
278
|
+
* than asking forever.
|
|
279
|
+
* - **backoff** — a FAILED POST (transport reject) is retried on the
|
|
280
|
+
* `retry-backoff` curve; once `shouldRetry` is exhausted the loop stops
|
|
281
|
+
* with `gave_up`. A success resets the retry counter.
|
|
282
|
+
* - **dedupe** — a step whose `callId` was already executed in THIS drive is
|
|
283
|
+
* not re-executed; its prior result is re-POSTed. Closes the "the host
|
|
284
|
+
* re-handed the same step after a duplicate settle" path (the host side is
|
|
285
|
+
* idempotent; the hands match it).
|
|
286
|
+
*/
|
|
287
|
+
declare function runStepLoop<R, I, O>(token: string, runId: string, transport: StepTransport<R, I, O>, execute: ExecuteStep<I, R>, config: RunStepLoopConfig): Promise<StepLoopOutcome<O>>;
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* `@demlik/tea/do` — the durable-timer / alarm-carrier port (#180).
|
|
291
|
+
*
|
|
292
|
+
* Every native-DO grain that wants a wall-clock tick hand-rolls the SAME
|
|
293
|
+
* eviction-safe pattern: compute the next absolute deadline off the live state,
|
|
294
|
+
* push it to `ctx.storage.setAlarm`, and — when the DO `alarm()` lifecycle hook
|
|
295
|
+
* fires — map that fire back to a Msg, then re-arm off the post-fire state. The
|
|
296
|
+
* `raft/do.ts` election/heartbeat deadline does it (`rearmAlarm` + `onAlarm`),
|
|
297
|
+
* and the vortex arena tick (`room.ts` `armTick` + `onTick`) replicates the
|
|
298
|
+
* exact shape for the 18 Hz game clock. The two old `do_alarm`/`do_ws` Subs were
|
|
299
|
+
* dropped (see `./index`'s historical note) because `toMachine` fixes the agent
|
|
300
|
+
* machine's Sub type to `DeadlineSub`, so they could never union into a grain's
|
|
301
|
+
* Sub set — leaving the alarm re-arm a HOST concern with no first-class home.
|
|
302
|
+
*
|
|
303
|
+
* {@link durableTimer} is that home: a tiny, transport- and Msg-agnostic helper
|
|
304
|
+
* that owns the three repeated responsibilities and nothing else —
|
|
305
|
+
*
|
|
306
|
+
* 1. **Compute-next-deadline → `setAlarm`.** {@link DurableTimer.rearm} calls
|
|
307
|
+
* the injected {@link DurableTimerConfig.nextDeadline}, and arms the alarm
|
|
308
|
+
* at the returned absolute epoch-ms target.
|
|
309
|
+
* 2. **Idle-when-no-work.** `nextDeadline` returns `null` when there is nothing
|
|
310
|
+
* to schedule (an empty room, a node with no pending election/heartbeat),
|
|
311
|
+
* and `rearm` then arms NOTHING — the DO is allowed to go idle / hibernate
|
|
312
|
+
* instead of spinning the alarm forever. Re-arming is a positive act gated
|
|
313
|
+
* on real work, never an unconditional reschedule.
|
|
314
|
+
* 3. **Fire → Msg → re-arm.** {@link DurableTimer.onAlarm} is the body the DO
|
|
315
|
+
* `alarm()` hook calls: it runs the injected {@link DurableTimerConfig.onFire}
|
|
316
|
+
* (the consumer dispatches its timer Msg there — a `Tick`, a re-derived
|
|
317
|
+
* deadline Msg) and THEN re-arms off the now-advanced state. Re-arm always
|
|
318
|
+
* follows the fire, so a still-active grain keeps ticking and an idle one
|
|
319
|
+
* stops by construction.
|
|
320
|
+
*
|
|
321
|
+
* **Eviction-safe across hibernation.** A DO alarm is the one timer that
|
|
322
|
+
* survives eviction (an isolate `setInterval` dies the moment the isolate
|
|
323
|
+
* sleeps), so the same `durableTimer` re-armed on a cold wake resumes the
|
|
324
|
+
* schedule that a never-evicted instance would have held. The recovery contract
|
|
325
|
+
* is just: after the host rebuilds state (`store.load()` → `replay`), call
|
|
326
|
+
* {@link DurableTimer.rearm} once — it recomputes `nextDeadline` off the
|
|
327
|
+
* recovered state and re-arms (or idles) identically to the live path. The timer
|
|
328
|
+
* holds no in-memory schedule of its own; the deadline is always a pure function
|
|
329
|
+
* of the persisted state, which is what makes the cold-wake re-arm correct.
|
|
330
|
+
*
|
|
331
|
+
* **Dependency-injected / testable.** The alarm arrives as a {@link AlarmStorage}
|
|
332
|
+
* port — the SAME minimal `setAlarm` slice `stepHost` already takes (a real
|
|
333
|
+
* `DurableObjectStorage` satisfies it structurally; a test supplies a fake with
|
|
334
|
+
* no live Workers runtime), mirroring how `event-sourced-store.ts` takes its
|
|
335
|
+
* storage. `nextDeadline` and `onFire` are injected thunks, so the whole timer
|
|
336
|
+
* unit-tests with a faked alarm + a faked clock and no `cloudflare:workers`
|
|
337
|
+
* import. See `.patterns/tea-do/recovery.md` for the cold-wake replay contract
|
|
338
|
+
* the re-arm rides on.
|
|
339
|
+
*/
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* The construction inputs for {@link durableTimer} — the impure edges the grain
|
|
343
|
+
* injects, kept out of the pure reducer exactly like raft/do's
|
|
344
|
+
* `RaftGrainCtx`/room's `ArenaPorts`.
|
|
345
|
+
*/
|
|
346
|
+
interface DurableTimerConfig {
|
|
347
|
+
/**
|
|
348
|
+
* The DO alarm carrier. `setAlarm(atMs)` schedules the next fire at an
|
|
349
|
+
* absolute epoch-ms target. A real `DurableObjectStorage` satisfies this
|
|
350
|
+
* structurally; a test supplies a fake. Only `setAlarm` is read — never the
|
|
351
|
+
* whole `DurableObjectStorage`.
|
|
352
|
+
*/
|
|
353
|
+
readonly alarm: AlarmStorage;
|
|
354
|
+
/**
|
|
355
|
+
* Compute the next absolute deadline (epoch ms) off the live state, or `null`
|
|
356
|
+
* when there is no work to schedule. Returning `null` is the idle signal:
|
|
357
|
+
* {@link DurableTimer.rearm} arms nothing and the DO is free to hibernate.
|
|
358
|
+
*
|
|
359
|
+
* Called fresh on every `rearm` (including the post-fire re-arm and the
|
|
360
|
+
* cold-wake re-arm), so it MUST read live state / the injected clock each call
|
|
361
|
+
* — that freshness is what keeps the schedule a pure function of the persisted
|
|
362
|
+
* state and the re-arm eviction-safe. Examples:
|
|
363
|
+
* - raft: `node.subs(state, now())[0]?.atMs ?? null` (election XOR heartbeat).
|
|
364
|
+
* - vortex: `connectionCount() > 0 ? now() + TICK_INTERVAL_MS : null`.
|
|
365
|
+
*/
|
|
366
|
+
nextDeadline(): number | null | Promise<number | null>;
|
|
367
|
+
/**
|
|
368
|
+
* Run the timer's effect when the alarm fires — the consumer dispatches its
|
|
369
|
+
* timer Msg here (a `Tick`, a re-derived deadline Msg) and persists/broadcasts
|
|
370
|
+
* as its grain requires. {@link DurableTimer.onAlarm} awaits this BEFORE it
|
|
371
|
+
* re-arms, so the re-arm sees the post-fire state.
|
|
372
|
+
*/
|
|
373
|
+
onFire(): void | Promise<void>;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* The activated durable timer. Two methods, both idempotent to call from the
|
|
377
|
+
* host edge:
|
|
378
|
+
* - {@link DurableTimer.rearm} — recompute + (re-)arm or idle. Call it once
|
|
379
|
+
* after cold-wake recovery, and after any state transition that can change
|
|
380
|
+
* whether/when the next deadline should fire.
|
|
381
|
+
* - {@link DurableTimer.onAlarm} — the DO `alarm()` body: fire, then re-arm.
|
|
382
|
+
*/
|
|
383
|
+
interface DurableTimer {
|
|
384
|
+
/**
|
|
385
|
+
* Recompute {@link DurableTimerConfig.nextDeadline} off the live state and arm
|
|
386
|
+
* the alarm at it — or, when it returns `null`, arm nothing (idle). Safe to
|
|
387
|
+
* call repeatedly; arming the same absolute target twice is a harmless
|
|
388
|
+
* overwrite on the DO alarm API.
|
|
389
|
+
*/
|
|
390
|
+
rearm(): Promise<void>;
|
|
391
|
+
/**
|
|
392
|
+
* The body the DO `alarm()` lifecycle hook delegates to: run
|
|
393
|
+
* {@link DurableTimerConfig.onFire}, then {@link DurableTimer.rearm} off the
|
|
394
|
+
* post-fire state. A still-active grain keeps ticking; an idle one (its
|
|
395
|
+
* `nextDeadline` now `null`) stops here.
|
|
396
|
+
*/
|
|
397
|
+
onAlarm(): Promise<void>;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Build a {@link DurableTimer} over an injected alarm carrier, a next-deadline
|
|
401
|
+
* computation, and a fire handler. The returned timer owns the
|
|
402
|
+
* compute-next-deadline → `setAlarm` → `alarm()` → fire → re-arm loop and holds
|
|
403
|
+
* no schedule of its own (the deadline is always recomputed from live state),
|
|
404
|
+
* which is what makes it eviction-safe across hibernation.
|
|
405
|
+
*/
|
|
406
|
+
declare function durableTimer(config: DurableTimerConfig): DurableTimer;
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* @demlik/tea/do — event-sourced persistence mode for a Durable Object actor.
|
|
410
|
+
*
|
|
411
|
+
* `doStore` (sibling, `./index`) is the DEFAULT and stays snapshot-only: it
|
|
412
|
+
* writes the whole `S` blob to one cell on every transition and reads it back
|
|
413
|
+
* on boot. That is byte-for-byte unchanged by this file.
|
|
414
|
+
*
|
|
415
|
+
* `doEventSourcedStore` is the OPT-IN alternative. Instead of (only)
|
|
416
|
+
* checkpointing the latest state, it:
|
|
417
|
+
*
|
|
418
|
+
* 1. APPENDS each applied `Msg` to an append-only log in `DurableObjectStorage`
|
|
419
|
+
* (one cell per event, monotonically keyed).
|
|
420
|
+
* 2. Takes a PERIODIC SNAPSHOT every `snapshotEvery` events (count-based
|
|
421
|
+
* retention — mirrors Akka `RetentionCriteria.snapshotEvery`) so the log
|
|
422
|
+
* that must replay on the next activation is bounded. A grain that needs
|
|
423
|
+
* replay bounded by ELAPSED TIME/TICKS rather than raw event count drives
|
|
424
|
+
* `snapshotNow()` instead (or alongside) — see that method.
|
|
425
|
+
* 3. On activation REBUILDS state by FOLDING the log on top of the latest
|
|
426
|
+
* snapshot. The fold is the substrate's own `replay` — `init(snapshot)`
|
|
427
|
+
* then one reducer step per logged Msg. `replay` returns Cmds as data and
|
|
428
|
+
* never interprets them, so **side effects are suppressed on replay**
|
|
429
|
+
* (recovery.md). This is the same primitive `foldEvents` / the recorder
|
|
430
|
+
* use; it is reused here, not reinvented (ADR 0003: "the fold already
|
|
431
|
+
* exists; wire it to Store").
|
|
432
|
+
*
|
|
433
|
+
* The "recovered/ready" signal (recovery.md `RecoveryCompleted`) is delivered
|
|
434
|
+
* exactly once per activation via `onReady`, EVEN for a fresh actor with an
|
|
435
|
+
* empty log, and is documented to be idempotent (it may fire again on a later
|
|
436
|
+
* activation, so the handler must tolerate repeats).
|
|
437
|
+
*
|
|
438
|
+
* Integration with the cooperating DO:
|
|
439
|
+
*
|
|
440
|
+
* const es = doEventSourcedStore(storage, machine, ctx, {
|
|
441
|
+
* snapshotEvery: 100,
|
|
442
|
+
* parse, // optional boundary parse for the snapshot
|
|
443
|
+
* onReady: (state) => { ... }, // optional, idempotent end-of-recovery hook
|
|
444
|
+
* });
|
|
445
|
+
* const runtime = run(machine, { ctx, store: es.store });
|
|
446
|
+
* // Append every applied Msg to the log. `observe` delivers only applied
|
|
447
|
+
* // Msgs now (#47) — boot has no event, so there is no `null` arm to skip.
|
|
448
|
+
* runtime.observe((msg) => { es.append(msg); });
|
|
449
|
+
* await runtime.ready; // store.load() has folded; onReady has fired once.
|
|
450
|
+
*
|
|
451
|
+
* A time/tick-driven grain (e.g. one that derives simulation ticks between
|
|
452
|
+
* sparse intents) bounds its cold-wake replay by ELAPSED TIME rather than event
|
|
453
|
+
* count by calling `es.snapshotNow()` on its own cadence — every N derived ticks
|
|
454
|
+
* or on a wall-clock interval — so a quiet span that never trips `snapshotEvery`
|
|
455
|
+
* is still snapshotted before its replay tail grows unbounded (#190):
|
|
456
|
+
*
|
|
457
|
+
* // in the grain's tick loop, after the turn's appends have settled:
|
|
458
|
+
* if (tick % SNAPSHOT_EVERY_TICKS === 0) await es.snapshotNow();
|
|
459
|
+
*/
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Options for {@link doEventSourcedStore}.
|
|
463
|
+
*/
|
|
464
|
+
interface EventSourcedOptions<S, M> {
|
|
465
|
+
/**
|
|
466
|
+
* Take a snapshot every N appended events (count-based retention, mirrors
|
|
467
|
+
* Akka `RetentionCriteria.snapshotEvery`). A snapshot bounds how many events
|
|
468
|
+
* replay on the next activation — it never changes the fold's result, only
|
|
469
|
+
* its length (recovery.md). Must be >= 1. Default 100.
|
|
470
|
+
*/
|
|
471
|
+
readonly snapshotEvery?: number;
|
|
472
|
+
/**
|
|
473
|
+
* Boundary parse for the persisted SNAPSHOT cell (the `S` blob). Same
|
|
474
|
+
* contract as `doStore`'s `parse`: returns `S` on a recognized shape, `null`
|
|
475
|
+
* on an unrecognized one (boot fresh), and must NOT throw. Default accepts
|
|
476
|
+
* any non-null, non-array object as `S`.
|
|
477
|
+
*/
|
|
478
|
+
readonly parse?: (raw: unknown) => S | null;
|
|
479
|
+
/**
|
|
480
|
+
* Boundary parse for a persisted EVENT (one logged `Msg`). Returns the `M` on
|
|
481
|
+
* a recognized shape; returning `null` DROPS the event from replay (use for a
|
|
482
|
+
* removed Msg variant). Must NOT throw. Default accepts any object carrying a
|
|
483
|
+
* string `type` as `M`.
|
|
484
|
+
*/
|
|
485
|
+
readonly parseEvent?: (raw: unknown) => M | null;
|
|
486
|
+
/**
|
|
487
|
+
* End-of-recovery hook (recovery.md `RecoveryCompleted`). Fires EXACTLY ONCE
|
|
488
|
+
* per activation, after the fold completes, carrying the recovered state —
|
|
489
|
+
* even when the log was empty (fresh actor). It may fire again on a later
|
|
490
|
+
* activation, so it MUST be idempotent. Side effects that belong "once after
|
|
491
|
+
* recovery" go here, never in the reducer (the reducer re-runs on every
|
|
492
|
+
* replay).
|
|
493
|
+
*/
|
|
494
|
+
readonly onReady?: (state: S) => void;
|
|
495
|
+
/** Cell-key overrides (rarely needed; for coexisting actors in one DO). */
|
|
496
|
+
readonly keys?: {
|
|
497
|
+
readonly snapshot?: string;
|
|
498
|
+
readonly meta?: string;
|
|
499
|
+
readonly eventPrefix?: string;
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* One persisted log entry handed to a {@link EventSourcedStore.readEvents}
|
|
504
|
+
* consumer: the monotonic `seq` the event was appended under, paired with the
|
|
505
|
+
* decoded `event`. `seq` is the store's own append counter (the key the event
|
|
506
|
+
* lives under), so it both orders the stream and bounds an {@link EventLogRange}.
|
|
507
|
+
*/
|
|
508
|
+
interface PersistedEvent<M> {
|
|
509
|
+
/** The append sequence number this event was logged under (1-based, gap-free). */
|
|
510
|
+
readonly seq: number;
|
|
511
|
+
/** The decoded `Msg`, parsed through the store's `parseEvent` boundary. */
|
|
512
|
+
readonly event: M;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Optional inclusive bounds for {@link EventSourcedStore.readEvents}. Both ends
|
|
516
|
+
* are inclusive and match against the event's `seq`; omitting an end leaves it
|
|
517
|
+
* unbounded. `{ fromSeq: 5, toSeq: 10 }` yields seqs 5..10; `{}` (or no
|
|
518
|
+
* argument) yields the whole log.
|
|
519
|
+
*/
|
|
520
|
+
interface EventLogRange {
|
|
521
|
+
/** Lowest `seq` to include (inclusive). Default: the first event. */
|
|
522
|
+
readonly fromSeq?: number;
|
|
523
|
+
/** Highest `seq` to include (inclusive). Default: the last event. */
|
|
524
|
+
readonly toSeq?: number;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* The handle returned by {@link doEventSourcedStore}: a `Store<S>` to hand to
|
|
528
|
+
* `run(...)`, plus the append + recovery surface the cooperating DO drives.
|
|
529
|
+
*/
|
|
530
|
+
interface EventSourcedStore<S, M> {
|
|
531
|
+
/**
|
|
532
|
+
* The `Store<S>` to pass as `run(machine, { ctx, store })`. Its `load()`
|
|
533
|
+
* performs the fold (snapshot + log replay); its `save(state)` is the
|
|
534
|
+
* count-based snapshot writer. `migrate` forwards to `parse`.
|
|
535
|
+
*/
|
|
536
|
+
readonly store: Store<S>;
|
|
537
|
+
/**
|
|
538
|
+
* Append one applied `Msg` to the log. Call from `runtime.observe` for every
|
|
539
|
+
* non-null msg. Resolves once the event is durably written. Triggers a
|
|
540
|
+
* snapshot when the event count crosses a `snapshotEvery` boundary.
|
|
541
|
+
*/
|
|
542
|
+
append(msg: M): Promise<void>;
|
|
543
|
+
/**
|
|
544
|
+
* Take a snapshot NOW, independent of the count-based `snapshotEvery` trigger
|
|
545
|
+
* (a time/tick-based retention trigger, #190). The count-based trigger bounds
|
|
546
|
+
* the replay tail by raw EVENT COUNT; a grain that derives many state
|
|
547
|
+
* transitions per logged event (e.g. simulation ticks between sparse intents)
|
|
548
|
+
* needs the tail bounded by ELAPSED TIME instead — so it calls `snapshotNow()`
|
|
549
|
+
* on its own cadence (every N derived ticks, or on a wall-clock interval) to
|
|
550
|
+
* checkpoint the current folded state without injecting filler events purely
|
|
551
|
+
* to advance the counter.
|
|
552
|
+
*
|
|
553
|
+
* It checkpoints the latest state the substrate handed `save()` at the current
|
|
554
|
+
* highest sequence, then advances the count-based baseline (`snapshotEvery`
|
|
555
|
+
* counts afresh from here, so a manual snapshot defers the next automatic one).
|
|
556
|
+
*
|
|
557
|
+
* Returns `true` if a snapshot was written, `false` on a no-op: nothing has
|
|
558
|
+
* been applied yet (fresh actor), or no new event exists since the last
|
|
559
|
+
* snapshot (already current). Like the count-based path, it pairs the held
|
|
560
|
+
* state with the highest sequence, so the caller MUST invoke it between
|
|
561
|
+
* settled transitions — after the turn's `append()`s have resolved — never
|
|
562
|
+
* mid-transition (the same boundary discipline `append`'s own snapshot relies
|
|
563
|
+
* on). Resolves once the snapshot is durably written.
|
|
564
|
+
*/
|
|
565
|
+
snapshotNow(): Promise<boolean>;
|
|
566
|
+
/**
|
|
567
|
+
* Stream the persisted event log in `seq` order, optionally bounded by an
|
|
568
|
+
* inclusive {@link EventLogRange}. This is the PUBLIC read side of the
|
|
569
|
+
* event-sourced write model — the surface replay / projection / audit
|
|
570
|
+
* consumers (CQRS reads, recovery.md / projections.md) read through, instead
|
|
571
|
+
* of mirroring the store's private `@@es/evt/` key convention.
|
|
572
|
+
*
|
|
573
|
+
* The log is APPEND-ONLY and is never truncated by snapshotting (a snapshot
|
|
574
|
+
* only bounds the *replay* tail, never the log), so this yields EVERY event
|
|
575
|
+
* in range — including ones a snapshot already folds. Each entry carries its
|
|
576
|
+
* `seq`, so a consumer can resume from a known offset (`{ fromSeq }`) or read
|
|
577
|
+
* a window (`{ fromSeq, toSeq }`). An empty (or fully out-of-range) log yields
|
|
578
|
+
* nothing. Events whose `parseEvent` returns `null` (a retired Msg variant)
|
|
579
|
+
* are dropped, identical to the recovery fold.
|
|
580
|
+
*
|
|
581
|
+
* Read-only: it never appends, snapshots, or mutates store state, so streaming
|
|
582
|
+
* the log can never perturb the live actor.
|
|
583
|
+
*/
|
|
584
|
+
readEvents(range?: EventLogRange): AsyncIterable<PersistedEvent<M>>;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Build an event-sourced `Store<S>` over `DurableObjectStorage`.
|
|
588
|
+
*
|
|
589
|
+
* The substrate calls `store.load()` once at boot — that is where the fold
|
|
590
|
+
* happens — and `store.save(state)` after every transition — that is where the
|
|
591
|
+
* count-based snapshot is taken. The cooperating DO additionally calls
|
|
592
|
+
* `append(msg)` from `runtime.observe` to grow the log.
|
|
593
|
+
*
|
|
594
|
+
* `machine` and `ctx` are required because the fold is `replay(machine, {
|
|
595
|
+
* loaded: snapshot, msgs: log, ctx })` — the same pure reducer the live runtime
|
|
596
|
+
* uses, so the rebuilt state is identical to the never-evicted state by
|
|
597
|
+
* construction. `ctx` is only used to satisfy `init`'s signature during the
|
|
598
|
+
* fold; an event-sourced machine's `init` rehydrate branch is a pure
|
|
599
|
+
* passthrough (TEA invariant 2), so the fold touches nothing in `ctx`.
|
|
600
|
+
*/
|
|
601
|
+
declare function doEventSourcedStore<S, M extends {
|
|
602
|
+
type: string;
|
|
603
|
+
}, Ctx, C extends Cmd = Cmd, U extends Sub = Sub>(storage: DurableObjectStorage, machine: Machine<S, M, C, U, Ctx>, ctx: Ctx, opts?: EventSourcedOptions<S, M>): EventSourcedStore<S, M>;
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* @demlik/tea/do — CQRS projections as a first-class seam.
|
|
607
|
+
*
|
|
608
|
+
* TODAY (`./host`), "Model → views" is ad-hoc: `sseHub` is a hand-rolled
|
|
609
|
+
* sink-set fanning `runtime.observe` events out. It re-derives a common shape:
|
|
610
|
+
* subscribe to `observe`, fold each update into a private view, push the view
|
|
611
|
+
* at a sink. (The run's terminal output was once another such observe-driven
|
|
612
|
+
* read model — `captureLastTurn` — but that is now modelled first-class on the
|
|
613
|
+
* write model itself as `state.output` / `runtime.result()`, #46.)
|
|
614
|
+
*
|
|
615
|
+
* This file names that shape. A `Projection<Model, View>` is the canon's
|
|
616
|
+
* "one write model, many projections" contract made concrete on the tea
|
|
617
|
+
* substrate (`.patterns/tea-do/projections.md`):
|
|
618
|
+
*
|
|
619
|
+
* 1. ONE WRITE MODEL, MANY PROJECTIONS. The write model is the agent's `run()`
|
|
620
|
+
* State (`Model`) plus its `Msg` stream. Each projection is an INDEPENDENT
|
|
621
|
+
* `(events|Model) → view` derivation into its OWN read model, with its own
|
|
622
|
+
* id-scoped offset. Adding a projection never touches the write model or
|
|
623
|
+
* any other projection — it only registers another consumer of `observe`.
|
|
624
|
+
*
|
|
625
|
+
* 2. OFFSET-RESUME + IDEMPOTENT APPLY ARE ONE UNIT. A durable projection
|
|
626
|
+
* tracks an EXCLUSIVE offset — the last applied position. On restart it
|
|
627
|
+
* resumes STRICTLY AFTER the stored offset (never ±1); at-least-once
|
|
628
|
+
* delivery means the same update may be re-presented, so `apply` must be
|
|
629
|
+
* idempotent (an upsert of the derived view, never a blind increment).
|
|
630
|
+
* WHEN the offset is committed relative to the view write IS the delivery
|
|
631
|
+
* semantic (`.patterns/tea-do/delivery-semantics.md`):
|
|
632
|
+
* - commit AFTER the view write → at-least-once (may reprocess)
|
|
633
|
+
* - commit in the SAME step → exactly-once (the default here)
|
|
634
|
+
* We persist offset-with-view as one unit (a single `OffsetView` cell), so
|
|
635
|
+
* a durable projection is exactly-once by construction; the at-least-once
|
|
636
|
+
* replay window is still exercised because `drive()` may re-present an
|
|
637
|
+
* update at/below the stored offset (boot reconcile, double-observe), and
|
|
638
|
+
* the exclusive-offset guard makes that a no-op.
|
|
639
|
+
*
|
|
640
|
+
* 3. A VIEW IS ALWAYS REBUILDABLE. Clearing the view and resetting the offset
|
|
641
|
+
* to the start (`rebuild`) replays the `Msg`/`Model` stream from zero. The
|
|
642
|
+
* live view and the rebuilt view are equal by construction — both are the
|
|
643
|
+
* same pure `apply` fold over the same ordered updates.
|
|
644
|
+
*
|
|
645
|
+
* Relationship to `./host`: `sseHub` is re-expressed here as ONE projection
|
|
646
|
+
* (`sseProjection`) WITHOUT changing its public API — `host.sseHub()` keeps
|
|
647
|
+
* working for existing callers (it is the volatile, offset-free case: a live
|
|
648
|
+
* SSE stream has no durable read model to resume).
|
|
649
|
+
*
|
|
650
|
+
* Relationship to `./event-sourced-store`: a durable projection resumes over
|
|
651
|
+
* the SAME append-only event log `doEventSourcedStore` (#68) writes. The fold
|
|
652
|
+
* is the projection's `apply`, scoped to the projection's offset — independent
|
|
653
|
+
* of the write model's snapshot/replay. This file does NOT import the store; it
|
|
654
|
+
* is offset-shaped so a consumer can wire either the live `observe` stream or a
|
|
655
|
+
* replay over the log into the same `apply`.
|
|
656
|
+
*/
|
|
657
|
+
/** A projection's identity: `name` (the view) + `key` (the instance). */
|
|
658
|
+
interface ProjectionId {
|
|
659
|
+
readonly name: string;
|
|
660
|
+
readonly key: string;
|
|
661
|
+
}
|
|
662
|
+
/** Render a `ProjectionId` to its canonical `name-key` string. */
|
|
663
|
+
declare function projectionIdString(id: ProjectionId): string;
|
|
664
|
+
/**
|
|
665
|
+
* One unit the projection driver presents to a projection's `apply`. Mirrors
|
|
666
|
+
* the `(msg, model)` pair `runtime.observe` fans out, plus the exclusive
|
|
667
|
+
* `offset` the driver assigns. `msg === null` is the boot update (no event).
|
|
668
|
+
*/
|
|
669
|
+
interface ProjectionUpdate<Model, Msg> {
|
|
670
|
+
/** The applied event, or `null` on the boot update (initial state). */
|
|
671
|
+
readonly msg: Msg | null;
|
|
672
|
+
/** The post-transition write-model state. */
|
|
673
|
+
readonly model: Model;
|
|
674
|
+
/**
|
|
675
|
+
* The exclusive position of this update in the ordered stream — monotonic,
|
|
676
|
+
* 1-based; 0 is the boot update (the `NoOffset` start). The stored offset is
|
|
677
|
+
* EXCLUSIVE: re-presenting an update whose offset is `<=` the stored offset
|
|
678
|
+
* must be a no-op (the at-least-once replay window). Do NOT ±1 it.
|
|
679
|
+
*/
|
|
680
|
+
readonly offset: number;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* A named CQRS projection: an independent fold of the write model's
|
|
684
|
+
* `(Msg|Model)` stream into a private `View`, plus a sink to publish it.
|
|
685
|
+
*
|
|
686
|
+
* @typeParam Model The write-model state (`run()`'s `S`).
|
|
687
|
+
* @typeParam Msg The write-model event (`run()`'s `M`).
|
|
688
|
+
* @typeParam View The read model this projection derives.
|
|
689
|
+
*/
|
|
690
|
+
interface Projection<Model, Msg extends {
|
|
691
|
+
type: string;
|
|
692
|
+
}, View> {
|
|
693
|
+
/** This projection's id (offset-isolation unit). */
|
|
694
|
+
readonly id: ProjectionId;
|
|
695
|
+
/** The seed view for a fresh or rebuilt run (the `NoOffset` start). */
|
|
696
|
+
readonly initial: View;
|
|
697
|
+
/**
|
|
698
|
+
* The per-envelope reducer: derive the next view from the previous view and
|
|
699
|
+
* one update. Pure; MUST be idempotent — re-applying an update at the same
|
|
700
|
+
* offset yields the same view (upsert the derived value, never a blind
|
|
701
|
+
* increment). Returning the SAME view reference (or an equal one) for an
|
|
702
|
+
* update the view does not care about is the canonical "skip".
|
|
703
|
+
*/
|
|
704
|
+
apply(view: View, update: ProjectionUpdate<Model, Msg>): View;
|
|
705
|
+
/**
|
|
706
|
+
* The sink — publish the derived view. SSE frame, durable cell, report row.
|
|
707
|
+
* Fired by the driver after `apply` produces a new view. A throwing `emit` is
|
|
708
|
+
* isolated by the driver (it must not strand sibling projections).
|
|
709
|
+
*/
|
|
710
|
+
emit(view: View): void;
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* A live, running projection instance: the projection's current `View`, its
|
|
714
|
+
* exclusive stored `offset`, and the `present`/`reset` operations the driver
|
|
715
|
+
* (or a rebuild) calls.
|
|
716
|
+
*/
|
|
717
|
+
interface ProjectionRunner<Model, Msg extends {
|
|
718
|
+
type: string;
|
|
719
|
+
}, View> {
|
|
720
|
+
readonly id: ProjectionId;
|
|
721
|
+
/** The current derived view (the read model). */
|
|
722
|
+
view(): View;
|
|
723
|
+
/**
|
|
724
|
+
* The exclusive stored offset — the position of the LAST applied update.
|
|
725
|
+
* Resume reads strictly after this; `0` is the `NoOffset` start.
|
|
726
|
+
*/
|
|
727
|
+
offset(): number;
|
|
728
|
+
/**
|
|
729
|
+
* Present one update to the projection. At-least-once entry point:
|
|
730
|
+
* - if `update.offset <= storedOffset` the update was already applied →
|
|
731
|
+
* NO-OP (idempotent replay window; offset stays, view stays, no emit).
|
|
732
|
+
* - otherwise fold via `apply`, advance the offset TO `update.offset`, and
|
|
733
|
+
* `emit` the new view. Offset-and-view advance together (one unit) →
|
|
734
|
+
* exactly-once: the stored offset always matches the folded view.
|
|
735
|
+
* Returns true iff the update was applied (false on the no-op replay).
|
|
736
|
+
*/
|
|
737
|
+
present(update: ProjectionUpdate<Model, Msg>): boolean;
|
|
738
|
+
/**
|
|
739
|
+
* Clear the view to `initial` and the offset to 0 — the canonical rebuild
|
|
740
|
+
* reset (offset-tracking.md: "resetting it to NoOffset is the view-rebuild
|
|
741
|
+
* operation"). After `reset`, re-presenting the full stream rebuilds the view.
|
|
742
|
+
*/
|
|
743
|
+
reset(): void;
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Build a {@link ProjectionRunner} for a projection, starting from a stored
|
|
747
|
+
* offset (default 0 = `NoOffset`, a fresh run). The view is seeded to
|
|
748
|
+
* `projection.initial`; the caller resumes by presenting updates strictly after
|
|
749
|
+
* `startOffset` (the driver's offset guard enforces exclusivity even if an
|
|
750
|
+
* already-applied update is re-presented).
|
|
751
|
+
*
|
|
752
|
+
* `emitOnStart`/`emitOnReset` default false: a fresh runner holds `initial` but
|
|
753
|
+
* does not push it at the sink until the first real update lands (an SSE client
|
|
754
|
+
* that just connected wants the next event, not a replay of the seed).
|
|
755
|
+
*/
|
|
756
|
+
declare function runProjection<Model, Msg extends {
|
|
757
|
+
type: string;
|
|
758
|
+
}, View>(projection: Projection<Model, Msg, View>, startOffset?: number): ProjectionRunner<Model, Msg, View>;
|
|
759
|
+
/**
|
|
760
|
+
* A driver over a set of projections sharing one write-model stream. Holds the
|
|
761
|
+
* monotonic offset counter; presents each observed update to every registered
|
|
762
|
+
* runner independently.
|
|
763
|
+
*/
|
|
764
|
+
interface ProjectionRegistry<Model, Msg extends {
|
|
765
|
+
type: string;
|
|
766
|
+
}> {
|
|
767
|
+
/** Register a projection; returns its live runner. */
|
|
768
|
+
register<View>(projection: Projection<Model, Msg, View>, startOffset?: number): ProjectionRunner<Model, Msg, View>;
|
|
769
|
+
/**
|
|
770
|
+
* Present one update to EVERY registered runner. Each folds independently; an
|
|
771
|
+
* `apply`/`emit` throw in one runner is caught so it cannot strand the others
|
|
772
|
+
* (errors-are-data: a broken projection is dropped from THIS update, not
|
|
773
|
+
* allowed to corrupt siblings). Returns the offset assigned to this update.
|
|
774
|
+
*/
|
|
775
|
+
dispatch(msg: Msg | null, model: Model): number;
|
|
776
|
+
/** The current monotonic offset (the last dispatched update's position). */
|
|
777
|
+
offset(): number;
|
|
778
|
+
/** The registered runners (introspection / tests). */
|
|
779
|
+
runners(): readonly ProjectionRunner<Model, Msg, unknown>[];
|
|
780
|
+
}
|
|
781
|
+
/** Build an empty {@link ProjectionRegistry}. */
|
|
782
|
+
declare function projectionRegistry<Model, Msg extends {
|
|
783
|
+
type: string;
|
|
784
|
+
}>(): ProjectionRegistry<Model, Msg>;
|
|
785
|
+
/**
|
|
786
|
+
* Wire a {@link ProjectionRegistry} to a runtime's transition stream. The boot
|
|
787
|
+
* update (the initial state, offset 0) is presented via `onBoot`; every applied
|
|
788
|
+
* transition (offset 1, 2, …) via `observe` — together they reproduce the
|
|
789
|
+
* "every transition including boot" stream the single `observe(msg | null)`
|
|
790
|
+
* channel used to carry, now that `observe`'s `msg` is total (#47). The registry
|
|
791
|
+
* still assigns its monotonic exclusive offset (boot → 0, first Msg → 1).
|
|
792
|
+
* Returns a cleanup that detaches both subscriptions.
|
|
793
|
+
*
|
|
794
|
+
* Typed structurally against `{ observe, onBoot }` so it accepts any
|
|
795
|
+
* `Runtime<Model, Msg>` without importing the substrate's `Runtime` (types-only
|
|
796
|
+
* boundary).
|
|
797
|
+
*/
|
|
798
|
+
declare function driveProjections<Model, Msg extends {
|
|
799
|
+
type: string;
|
|
800
|
+
}>(registry: ProjectionRegistry<Model, Msg>, runtime: {
|
|
801
|
+
observe(observer: (msg: Msg, model: Model) => void): () => void;
|
|
802
|
+
onBoot(handler: (model: Model) => void): () => void;
|
|
803
|
+
}): () => void;
|
|
804
|
+
/**
|
|
805
|
+
* Rebuild a projection's view by folding an ordered event stream from the
|
|
806
|
+
* `NoOffset` start. The runner is seeded to `projection.initial` (the implicit
|
|
807
|
+
* boot / `NoOffset` state), then each `updates[i]` is presented at offset
|
|
808
|
+
* `i + 1` — so the FIRST event (`updates[0]`) lands at offset 1 and is folded,
|
|
809
|
+
* exactly mirroring the live driver, which assigns the first applied `Msg`
|
|
810
|
+
* offset 1 (the boot observe is offset 0 and never enters this list). Returns
|
|
811
|
+
* the rebuilt runner whose `view()` equals the live runner's view for the same
|
|
812
|
+
* ordered stream.
|
|
813
|
+
*
|
|
814
|
+
* Pass the ordered events only; do NOT prepend a boot (`msg === null`) entry.
|
|
815
|
+
* With the `i + 1` offsets a leading boot entry would be applied at offset 1
|
|
816
|
+
* like any event (the seed already covers the `NoOffset` start), not skipped —
|
|
817
|
+
* the offset-0 skip that drops the boot is the live driver's, not this fold's.
|
|
818
|
+
*
|
|
819
|
+
* This is the rebuild-equivalence oracle: a view cleared and replayed from the
|
|
820
|
+
* stream equals the live view.
|
|
821
|
+
*/
|
|
822
|
+
declare function rebuildProjection<Model, Msg extends {
|
|
823
|
+
type: string;
|
|
824
|
+
}, View>(projection: Projection<Model, Msg, View>, updates: readonly {
|
|
825
|
+
readonly msg: Msg | null;
|
|
826
|
+
readonly model: Model;
|
|
827
|
+
}[]): ProjectionRunner<Model, Msg, View>;
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* @demlik/tea/do — the minimum-viable Durable-Object HOST for a `@demlik/tea`
|
|
831
|
+
* `createAgent` runtime.
|
|
832
|
+
*
|
|
833
|
+
* `doStore` (in `./index`) is the durability half. This file is the rest of the
|
|
834
|
+
* boilerplate a DO consumer hand-rolls every time it drives a tea agent over a
|
|
835
|
+
* remote transport: the deferred-tool gateway, auto-boot on rehydrate, the
|
|
836
|
+
* command-runner WS accept, the runtime→SSE plumbing, and a clean read of a
|
|
837
|
+
* run's terminal output. Each export below names exactly the hand-rolled piece
|
|
838
|
+
* it deletes from the consumer.
|
|
839
|
+
*
|
|
840
|
+
* ## The transport model: ONE Sub type, gateway-bridged I/O
|
|
841
|
+
*
|
|
842
|
+
* `createAgent().toMachine()` fixes the machine's Sub type to `DeadlineSub` —
|
|
843
|
+
* the agent owns the retry + watchdog timers and nothing else. That is the
|
|
844
|
+
* WHOLE Sub model for a DO-hosted agent, and it is coherent because the
|
|
845
|
+
* transport deliberately does NOT live in the Sub system:
|
|
846
|
+
*
|
|
847
|
+
* - Inbound WebSocket frames are bridged straight into `gateway.settle(...)`
|
|
848
|
+
* from the DO's `webSocketMessage` lifecycle method — a direct dispatch,
|
|
849
|
+
* not a Sub.
|
|
850
|
+
* - The per-tool deadline is a `setTimeout` inside the gateway — a timer, not
|
|
851
|
+
* a DO alarm.
|
|
852
|
+
*
|
|
853
|
+
* The deferred-tool gateway owns each tool round-trip as a Promise the interpret
|
|
854
|
+
* cell awaits; the transport bridges results into `dispatch` from outside the
|
|
855
|
+
* Sub system. The agent's Sub type stays exactly `DeadlineSub` — there is no
|
|
856
|
+
* competing DO-native Sub variant to union in, and the host needs none. This is
|
|
857
|
+
* the single transport model for `@demlik/tea/do` (#52); see
|
|
858
|
+
* `.patterns/tea/durable-actors.md` for the host-layer north star.
|
|
859
|
+
*/
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* The deferred-tool gateway. `R` is the consumer's tool-result type (what
|
|
863
|
+
* `agent_tool_ok` carries).
|
|
864
|
+
*
|
|
865
|
+
* - `await(callId, send, deadlineMs)` — register the round-trip, fire `send`,
|
|
866
|
+
* return the Promise the reply (or the deadline) settles. Idempotent per
|
|
867
|
+
* `callId`: a second call for an in-flight id returns the SAME promise
|
|
868
|
+
* without re-firing `send` (the boot-reconcile re-fire is a no-op at the
|
|
869
|
+
* gateway, mirroring the agent's idempotent boot).
|
|
870
|
+
* - `settle(callId, result)` — the transport calls this on a reply; resolves
|
|
871
|
+
* the awaited promise. A settle for an unknown / already-settled id is a
|
|
872
|
+
* no-op (the callId guard the consumer used to keep in its pending Map).
|
|
873
|
+
* - `fail(callId, reason)` — settle the round-trip as an error on demand
|
|
874
|
+
* (e.g. a transport-level close). Same unknown-id no-op.
|
|
875
|
+
* - `inFlight()` — the callIds with an open round-trip (tests / introspection).
|
|
876
|
+
*
|
|
877
|
+
* The deadline → reject path is what makes "timeout is data": the tool cell's
|
|
878
|
+
* `catch` maps the rejection to `agent_tool_err`, recorded on the conversation,
|
|
879
|
+
* never a hang.
|
|
880
|
+
*/
|
|
881
|
+
interface DeferredGateway<R> {
|
|
882
|
+
await(callId: string, send: () => void, deadlineMs: number): Promise<R>;
|
|
883
|
+
settle(callId: string, result: R): void;
|
|
884
|
+
fail(callId: string, reason: string): void;
|
|
885
|
+
inFlight(): readonly string[];
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Build a deferred-tool gateway. No clock / RNG captured at construction — the
|
|
889
|
+
* deadline is per-call (`await(..., deadlineMs)`), so one gateway serves every
|
|
890
|
+
* tool family with its own timeout.
|
|
891
|
+
*
|
|
892
|
+
* Deletes from the consumer: the `pending` Map, the `Pending` interface, the
|
|
893
|
+
* `awaitClient` method, the `forceTimeout` plumbing, and the `deliverResult`
|
|
894
|
+
* callId-guard body — all of it collapses to this object plus the tool cell's
|
|
895
|
+
* `await` + try/catch.
|
|
896
|
+
*/
|
|
897
|
+
declare function deferredGateway<R>(): DeferredGateway<R>;
|
|
898
|
+
/** A `callId`-keyed durable round-trip: maps the gateway's string id to the
|
|
899
|
+
* ledger's monotonic delivery id so a `settle(callId)` finds the owed entry. */
|
|
900
|
+
interface DurableDeferredGateway<R> extends DeferredGateway<R> {
|
|
901
|
+
/** The ledger recorder this gateway feeds (for `surviving()` on activation). */
|
|
902
|
+
readonly recorder: PendingEffectsRecorder<{
|
|
903
|
+
callId: string;
|
|
904
|
+
}>;
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Wrap a `DeferredGateway<R>` so every round-trip is also recorded in a durable
|
|
908
|
+
* `PendingEffectsRecorder`. The in-memory gateway is the fast path; the recorder
|
|
909
|
+
* is the durability slice.
|
|
910
|
+
*
|
|
911
|
+
* - `recordOwed(callId, event)` — fires from `await`, BEFORE `send`, carrying
|
|
912
|
+
* the `effect_owed` event the consumer must persist (then deliver).
|
|
913
|
+
* - `recordConfirmed(callId, event)` — fires from `settle`/`fail` for a known
|
|
914
|
+
* id, carrying the `effect_confirmed` event to persist.
|
|
915
|
+
* - `reissue(callId, send, deadlineMs)` — re-arm a surviving round-trip on
|
|
916
|
+
* activation. Idempotent: the gateway's own `await` returns the open promise
|
|
917
|
+
* without re-firing `send` for an in-flight `callId`, so re-issuing an id
|
|
918
|
+
* that is somehow still pending is a no-op (mirrors the boot-reconcile
|
|
919
|
+
* re-fire). The effect is already owed, so no second `effect_owed` is
|
|
920
|
+
* recorded.
|
|
921
|
+
*
|
|
922
|
+
* The effect payload the ledger stores is the `callId` (the correlation key the
|
|
923
|
+
* consumer round-trips); the consumer maps it back to its tool-call context.
|
|
924
|
+
*/
|
|
925
|
+
declare function durableDeferredGateway<R>(inner: DeferredGateway<R>, recorder: PendingEffectsRecorder<{
|
|
926
|
+
callId: string;
|
|
927
|
+
}>, hooks: {
|
|
928
|
+
recordOwed(callId: string, event: EffectOwed<{
|
|
929
|
+
callId: string;
|
|
930
|
+
}>): void;
|
|
931
|
+
recordConfirmed(callId: string, event: EffectConfirmed): void;
|
|
932
|
+
}): DurableDeferredGateway<R>;
|
|
933
|
+
/**
|
|
934
|
+
* The typed cold-wake resume port `bootResume` fires through — the agent's
|
|
935
|
+
* `AgentBootPort` (issue #60) generalized to any DO-hosted machine (issue #231).
|
|
936
|
+
*
|
|
937
|
+
* - `isResumable(state)` — true iff the rehydrated State is mid-loop and needs a
|
|
938
|
+
* resume dispatch. False on a fresh boot, which makes `bootResume` a no-op.
|
|
939
|
+
* - `resumeMsg(now)` — the TYPED constructor for the single resume Msg (never a
|
|
940
|
+
* hand-built `{ type: … }` literal); the `agentBootMsg` shape lifted out so a
|
|
941
|
+
* non-agent grain supplies its own boot Msg constructor.
|
|
942
|
+
*/
|
|
943
|
+
interface ResumePort<S, M extends {
|
|
944
|
+
type: string;
|
|
945
|
+
}> {
|
|
946
|
+
readonly isResumable: (state: S) => boolean;
|
|
947
|
+
readonly resumeMsg: (now: number) => M;
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* After `runtime.ready`, derive the single resume Msg from the rehydrated State
|
|
951
|
+
* via `port` and dispatch it exactly once — the generalized AgentBoot. On a
|
|
952
|
+
* fresh (non-rehydrated) machine `port.isResumable` is false and this is a
|
|
953
|
+
* no-op. Call once right after building the runtime; it standardizes the
|
|
954
|
+
* hand-written `if (isResumable(state)) await runtime.dispatch(bootMsg(now()))`
|
|
955
|
+
* every DO-hosted machine otherwise re-derives.
|
|
956
|
+
*
|
|
957
|
+
* `await ready` is the single boot gate (issue #45): `getState()` is total only
|
|
958
|
+
* on the booted `Runtime`, so state cannot be inspected before boot populates
|
|
959
|
+
* it. `now` is injected (the sole boot clock read) so a test can pin it.
|
|
960
|
+
*/
|
|
961
|
+
declare function bootResume<S, M extends {
|
|
962
|
+
type: string;
|
|
963
|
+
}, E extends {
|
|
964
|
+
type: string;
|
|
965
|
+
} = never>(booting: BootingRuntime<S, M, E>, port: ResumePort<S, M>, now?: () => number): Promise<void>;
|
|
966
|
+
/**
|
|
967
|
+
* True iff an agent slice loaded from storage is mid-loop (running + awaiting
|
|
968
|
+
* tools) — the resumable case `agent_boot` re-fires.
|
|
969
|
+
*
|
|
970
|
+
* The resumability question IS "is the run suspended on tools?" — so this
|
|
971
|
+
* delegates to the agent's own typed `status` channel (issue #49) rather than
|
|
972
|
+
* re-deriving the private `run.phase` / `conversation.awaiting` shape by hand.
|
|
973
|
+
* The host no longer leaks the agent's internal slice structure; a change to
|
|
974
|
+
* `Awaiting` surfaces as a compile error in `status`, not a silent break here.
|
|
975
|
+
*/
|
|
976
|
+
declare function agentIsResumable<Stage, P extends string, O extends Record<P, unknown>, R>(state: AgentState<Stage, P, O, R>): boolean;
|
|
977
|
+
/**
|
|
978
|
+
* The AGENT specialization of {@link bootResume} (issue #231): after
|
|
979
|
+
* `runtime.ready`, self-dispatch `agent_boot` iff the rehydrated slice is
|
|
980
|
+
* resumable, a no-op on a fresh DO. Wires the agent slice's `agentIsResumable`
|
|
981
|
+
* predicate + the agent-owned `agentBootMsg` typed port (issue #60) into the
|
|
982
|
+
* generalized helper, so the resume dispatch lives in ONE place — see
|
|
983
|
+
* `bootResume`'s docblock for the boot-gate / init-purity rationale.
|
|
984
|
+
*
|
|
985
|
+
* `now` is injected (the host's only clock read for boot) so a test can pin it.
|
|
986
|
+
*/
|
|
987
|
+
declare function autoBoot<Stage, P extends string, O extends Record<P, unknown>, R, E extends {
|
|
988
|
+
type: string;
|
|
989
|
+
} = never>(booting: BootingRuntime<AgentState<Stage, P, O, R>, AgentMachineMsg<P, O, R>, E>, now?: () => number): Promise<void>;
|
|
990
|
+
/**
|
|
991
|
+
* Accept a command-runner WebSocket on the DO's `fetch`. Adds the server end to
|
|
992
|
+
* `clients`, wires `message` → `onFrame(data)` (string frames only) and
|
|
993
|
+
* `close` → deregister, and returns the 101 upgrade Response.
|
|
994
|
+
*
|
|
995
|
+
* The consumer's `onFrame` parses the frame and calls `gateway.settle(callId,
|
|
996
|
+
* result)` — the inbound bridge. Deletes the consumer's `acceptClient` method.
|
|
997
|
+
*/
|
|
998
|
+
declare function acceptCommandSocket(clients: Set<WebSocket>, onFrame: (data: string) => void): Response;
|
|
999
|
+
/**
|
|
1000
|
+
* Broadcast a JSON frame to every connected command-runner socket. A dead
|
|
1001
|
+
* socket is skipped (dropped on its own `close` event). Lifts the consumer's
|
|
1002
|
+
* fan-out loop in `sendCommand`.
|
|
1003
|
+
*/
|
|
1004
|
+
declare function broadcast(clients: Set<WebSocket>, frame: unknown): void;
|
|
1005
|
+
/**
|
|
1006
|
+
* The slice of `DurableObjectState` the durable carrier needs: the Hibernation
|
|
1007
|
+
* API accept + the registry of hibernatable sockets. A real `DurableObjectState`
|
|
1008
|
+
* satisfies this structurally; a test supplies a fake (no live Workers runtime
|
|
1009
|
+
* needed to exercise the accept/registry seam).
|
|
1010
|
+
*/
|
|
1011
|
+
interface HibernatableCtx {
|
|
1012
|
+
acceptWebSocket(ws: WebSocket): void;
|
|
1013
|
+
getWebSockets(): WebSocket[];
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Accept a command-runner WebSocket using the Cloudflare **Hibernation API** so
|
|
1017
|
+
* the socket survives DO eviction. Unlike {@link acceptCommandSocket}, this does
|
|
1018
|
+
* NOT attach an isolate-resident `message` listener (that listener is what dies
|
|
1019
|
+
* on eviction) — instead `ctx.acceptWebSocket(server)` hands the socket to the
|
|
1020
|
+
* runtime, which re-creates the DO and calls its `webSocketMessage(ws, data)`
|
|
1021
|
+
* lifecycle method on the next inbound frame. The consumer routes that method to
|
|
1022
|
+
* its `onFrame` (which calls `carrier.settle(callId, result)`).
|
|
1023
|
+
*
|
|
1024
|
+
* Returns the 101 upgrade Response. There is no `clients` registry to keep — the
|
|
1025
|
+
* live sockets are `ctx.getWebSockets()` (see {@link broadcastHibernatable}),
|
|
1026
|
+
* which the runtime repopulates after a wake.
|
|
1027
|
+
*/
|
|
1028
|
+
declare function acceptDurableCommandSocket(ctx: HibernatableCtx): Response;
|
|
1029
|
+
/**
|
|
1030
|
+
* Broadcast a JSON frame to every hibernatable command-runner socket. The live
|
|
1031
|
+
* set is read from `ctx.getWebSockets()` (the runtime repopulates it after a
|
|
1032
|
+
* wake), NOT a resident `Set<WebSocket>` — so it is correct immediately after an
|
|
1033
|
+
* eviction, before any other lifecycle method has run. A dead socket is skipped.
|
|
1034
|
+
*/
|
|
1035
|
+
declare function broadcastHibernatable(ctx: HibernatableCtx, frame: unknown): void;
|
|
1036
|
+
/**
|
|
1037
|
+
* The durable command carrier — a {@link DurableDeferredGateway} whose every
|
|
1038
|
+
* tool round-trip is also a durable owed effect.
|
|
1039
|
+
*
|
|
1040
|
+
* A consumer's tool interpret cell still calls `carrier.await(callId, send,
|
|
1041
|
+
* deadlineMs)` and awaits the Promise (mapping Ok/Err to `agent_tool_ok` /
|
|
1042
|
+
* `agent_tool_err`) — the SAME fast path as the volatile gateway. The addition
|
|
1043
|
+
* is durability: `await` hands the consumer an `effect_owed` event (via
|
|
1044
|
+
* `recordOwed`) to PERSIST before the send, and `settle`/`fail` hand it an
|
|
1045
|
+
* `effect_confirmed` (via `recordConfirmed`) to persist. On the next activation
|
|
1046
|
+
* the consumer reads its persisted ledger events back and calls
|
|
1047
|
+
* {@link reissueSurvivingEffects} to re-fire the survivors.
|
|
1048
|
+
*
|
|
1049
|
+
* It extends `DurableDeferredGateway` (so it is a drop-in for the gateway the
|
|
1050
|
+
* interpret cell already awaits) and exposes the underlying `recorder` for a
|
|
1051
|
+
* host that wants to read `surviving()` directly.
|
|
1052
|
+
*/
|
|
1053
|
+
interface DurableCommandCarrier<R> extends DeferredGateway<R> {
|
|
1054
|
+
/** The ledger recorder backing this carrier (for `surviving()` introspection). */
|
|
1055
|
+
readonly recorder: PendingEffectsRecorder<{
|
|
1056
|
+
callId: string;
|
|
1057
|
+
}>;
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Build a durable command carrier over a (volatile) `DeferredGateway<R>` and a
|
|
1061
|
+
* `PendingEffectsRecorder`. Thin assembly over {@link durableDeferredGateway}:
|
|
1062
|
+
* it names the #91 piece — "the command carrier whose round-trips survive
|
|
1063
|
+
* eviction" — and keeps the recorder reachable for the activation-time re-emit.
|
|
1064
|
+
*
|
|
1065
|
+
* - `inner` — the volatile gateway whose in-isolate Promise the interpret cell
|
|
1066
|
+
* awaits (typically `deferredGateway<R>()`).
|
|
1067
|
+
* - `recorder` — the durable-effects recorder (typically
|
|
1068
|
+
* `pendingEffectsLedger({ events, lastId })` rehydrated from the persisted
|
|
1069
|
+
* log on a wake, or a fresh `pendingEffectsLedger()` on a cold boot).
|
|
1070
|
+
* - `hooks.recordOwed` / `hooks.recordConfirmed` — the persist callbacks the
|
|
1071
|
+
* consumer points at its event log (so the ledger survives eviction).
|
|
1072
|
+
*
|
|
1073
|
+
* The carrier never persists anything itself (it does not own the consumer's
|
|
1074
|
+
* log) — it hands the consumer the events, faithful to "persist the intent
|
|
1075
|
+
* before deliver".
|
|
1076
|
+
*/
|
|
1077
|
+
declare function durableCommandCarrier<R>(inner: DeferredGateway<R>, recorder: PendingEffectsRecorder<{
|
|
1078
|
+
callId: string;
|
|
1079
|
+
}>, hooks: {
|
|
1080
|
+
recordOwed(callId: string, event: EffectOwed<{
|
|
1081
|
+
callId: string;
|
|
1082
|
+
}>): void;
|
|
1083
|
+
recordConfirmed(callId: string, event: EffectConfirmed): void;
|
|
1084
|
+
}): DurableCommandCarrier<R>;
|
|
1085
|
+
/**
|
|
1086
|
+
* RE-EMIT ON ACTIVATION (the #91 wake path). Given a durable carrier whose
|
|
1087
|
+
* recorder was rehydrated from the persisted ledger events (e.g.
|
|
1088
|
+
* `durableCommandCarrier(deferredGateway(), pendingEffectsLedger({ events }),
|
|
1089
|
+
* …)` on a wake), re-fire every owed-but-unconfirmed round-trip via
|
|
1090
|
+
* `reissue(callId)` — the consumer's "re-send the command frame for this
|
|
1091
|
+
* `callId`" callback. The survivors are read straight off the carrier's own
|
|
1092
|
+
* recorder (`recorder.surviving()`), which folded the restore log at
|
|
1093
|
+
* construction — the carrier is the single source of the in-flight set, so the
|
|
1094
|
+
* caller does not re-fold the log here.
|
|
1095
|
+
*
|
|
1096
|
+
* Idempotent by construction:
|
|
1097
|
+
* - the ledger's monotonic `deliveryId` is the dedup key the RECEIVER uses, so
|
|
1098
|
+
* a late/duplicate reply for an already-resolved round-trip is absorbed
|
|
1099
|
+
* (at-least-once delivery — see `survivingEffects`);
|
|
1100
|
+
* - re-issuing the SAME survivor twice within one activation re-sends once —
|
|
1101
|
+
* if `reissue` routes through the carrier's `await`, the gateway returns the
|
|
1102
|
+
* open Promise for an in-flight `callId` without re-firing `send`.
|
|
1103
|
+
*
|
|
1104
|
+
* Returns the `callId`s re-emitted (oldest-first by id) — the host can log /
|
|
1105
|
+
* assert the re-emit set. On a fresh or fully-confirmed carrier this is empty
|
|
1106
|
+
* (nothing re-fired).
|
|
1107
|
+
*
|
|
1108
|
+
* Call this once, right after the runtime's boot gate resolves (alongside
|
|
1109
|
+
* {@link autoBoot}): `autoBoot` re-fires the agent's own `agent_boot`;
|
|
1110
|
+
* `reissueSurvivingEffects` re-fires the in-flight TOOL round-trips. Together
|
|
1111
|
+
* they restore a suspended run's full in-flight state after a wake.
|
|
1112
|
+
*/
|
|
1113
|
+
declare function reissueSurvivingEffects<R>(carrier: DurableCommandCarrier<R>, reissue: (callId: string) => void): readonly string[];
|
|
1114
|
+
/**
|
|
1115
|
+
* A set of SSE sinks plus the plumbing to fan an event out to all of them and
|
|
1116
|
+
* to open a `text/event-stream` Response wired to a fresh sink.
|
|
1117
|
+
*
|
|
1118
|
+
* The consumer:
|
|
1119
|
+
* 1. holds one `SseHub<E>` per DO,
|
|
1120
|
+
* 2. calls `hub.emit(event)` from its Msg→event mapping (driven off
|
|
1121
|
+
* `runtime.observe`), and
|
|
1122
|
+
* 3. returns `hub.open()` from the `/sse` route.
|
|
1123
|
+
*
|
|
1124
|
+
* `register(sink)` exists for tests: subscribe a sink directly and observe the
|
|
1125
|
+
* exact event sequence a live client would receive (a live `text/event-stream`
|
|
1126
|
+
* fights vitest-pool-workers teardown). Deletes the consumer's `sseSinks` set,
|
|
1127
|
+
* `openSse`, `emitSse`, and `onSseEvent`.
|
|
1128
|
+
*/
|
|
1129
|
+
interface SseHub<E> {
|
|
1130
|
+
emit(event: E): void;
|
|
1131
|
+
open(): Response;
|
|
1132
|
+
register(sink: (event: E) => void): () => void;
|
|
1133
|
+
}
|
|
1134
|
+
/** Build an SSE hub for event type `E` (the consumer's domain event shape). */
|
|
1135
|
+
declare function sseHub<E>(): SseHub<E>;
|
|
1136
|
+
/**
|
|
1137
|
+
* Express an {@link SseHub} as a {@link Projection}. The projection's view is
|
|
1138
|
+
* the latest emitted SSE event (or `null` before the first); `apply` maps each
|
|
1139
|
+
* write-model update to an event via `toEvent` and emits it through `hub.emit`,
|
|
1140
|
+
* so the SSE stream is driven by the SAME hub `sseHub()` returns — no API
|
|
1141
|
+
* change for the `/sse` route (`hub.open()`) or any existing caller.
|
|
1142
|
+
*
|
|
1143
|
+
* - `toEvent(msg, model)` returns the SSE event for this update, or `null` to
|
|
1144
|
+
* skip it (the view stays, nothing is emitted).
|
|
1145
|
+
* - `name`/`key` default to `"sse"`/`"main"`; override for a second SSE view.
|
|
1146
|
+
*
|
|
1147
|
+
* @example
|
|
1148
|
+
* const hub = sseHub<MyEvent>();
|
|
1149
|
+
* const registry = projectionRegistry<MyState, MyMsg>();
|
|
1150
|
+
* registry.register(sseProjection(hub, (msg) =>
|
|
1151
|
+
* msg?.type === "turn_done" ? { kind: "turn", ... } : null,
|
|
1152
|
+
* ));
|
|
1153
|
+
* driveProjections(registry, runtime); // SSE is now one projection
|
|
1154
|
+
* // /sse route unchanged:
|
|
1155
|
+
* return hub.open();
|
|
1156
|
+
*/
|
|
1157
|
+
declare function sseProjection<Model, Msg extends {
|
|
1158
|
+
type: string;
|
|
1159
|
+
}, E>(hub: SseHub<E>, toEvent: (msg: Msg | null, model: Model) => E | null, id?: {
|
|
1160
|
+
name?: string;
|
|
1161
|
+
key?: string;
|
|
1162
|
+
}): Projection<Model, Msg, E | null>;
|
|
1163
|
+
/**
|
|
1164
|
+
* Wire an {@link SseHub} to a runtime's semantic {@link AgentEvent} stream (#47).
|
|
1165
|
+
* For each event `type` the runtime can emit, subscribes via `runtime.on` and
|
|
1166
|
+
* pushes `toFrame(event)` at the hub (returning `null` skips that event). The
|
|
1167
|
+
* runtime MUST have been built with `run(machine, { events: agentEvents() })`,
|
|
1168
|
+
* which is what makes `on` deliver the `AgentEvent`s. Returns a cleanup that
|
|
1169
|
+
* detaches every subscription.
|
|
1170
|
+
*
|
|
1171
|
+
* This is the runtime→SSE adapter expressed over the typed event channel: no
|
|
1172
|
+
* `observe` firehose, no private-Msg coupling. `hub.open()` and every existing
|
|
1173
|
+
* `sseHub` caller are unchanged — only the SOURCE of `hub.emit` moves from a
|
|
1174
|
+
* hand-rolled Msg switch to `on`.
|
|
1175
|
+
*
|
|
1176
|
+
* @example
|
|
1177
|
+
* const hub = sseHub<MyFrame>();
|
|
1178
|
+
* const runtime = await run(machine, { ctx, events: agentEvents() }).ready;
|
|
1179
|
+
* const off = sseFromAgentEvents(runtime, hub, (e) =>
|
|
1180
|
+
* e.type === "RunDone" ? { kind: "done", output: e.output } : null,
|
|
1181
|
+
* );
|
|
1182
|
+
* // /sse route unchanged:
|
|
1183
|
+
* return hub.open();
|
|
1184
|
+
*/
|
|
1185
|
+
declare function sseFromAgentEvents<R, Frame>(runtime: {
|
|
1186
|
+
on<K extends AgentEvent<R>["type"]>(type: K, handler: (event: Extract<AgentEvent<R>, {
|
|
1187
|
+
type: K;
|
|
1188
|
+
}>) => void): () => void;
|
|
1189
|
+
}, hub: SseHub<Frame>, toFrame: (event: AgentEvent<R>) => Frame | null): () => void;
|
|
1190
|
+
/**
|
|
1191
|
+
* Dispatch `msg` and resolve only once the runtime has drained every follow-up
|
|
1192
|
+
* Msg the transition (transitively) enqueued — run-to-quiescence. Sugar over
|
|
1193
|
+
* `await runtime.dispatch(msg); await runtime.idle();`.
|
|
1194
|
+
*
|
|
1195
|
+
* Replaces the consumer's / test's `until(() => cond)` poll after a `dispatch`
|
|
1196
|
+
* for the common "dispatch, then let the loop settle" case. (A poll is still
|
|
1197
|
+
* the right tool when waiting on an EXTERNAL event — e.g. a WS reply — that the
|
|
1198
|
+
* runtime cannot enqueue itself.)
|
|
1199
|
+
*/
|
|
1200
|
+
declare function dispatchToIdle<M extends {
|
|
1201
|
+
type: string;
|
|
1202
|
+
}>(runtime: Runtime<unknown, M>, msg: M): Promise<void>;
|
|
1203
|
+
/**
|
|
1204
|
+
* What the consumer supplies to {@link createAgentHost} — the domain mappings,
|
|
1205
|
+
* nothing of the wiring. `Stage`/`P`/`O`/`R` are the agent slice's type
|
|
1206
|
+
* parameters (the consumer names them once); `Frame` is its SSE event shape.
|
|
1207
|
+
*/
|
|
1208
|
+
interface AgentHostConfig<Stage, P extends string, O extends Record<P, AgentTurn>, R, Frame> {
|
|
1209
|
+
/**
|
|
1210
|
+
* Build the wired agent machine. Called once per host build (per activation),
|
|
1211
|
+
* AFTER the previous runtime — if any — was torn down. The consumer wires its
|
|
1212
|
+
* per-tool interpret here (`agent.toMachine({ toolInterpret })`).
|
|
1213
|
+
*/
|
|
1214
|
+
readonly buildMachine: () => Machine<AgentState<Stage, P, O, R>, AgentMachineMsg<P, O, R>, any, any, any>;
|
|
1215
|
+
/** The durable `Store` for the agent slice (typically `doStore(storage, parse)`). */
|
|
1216
|
+
readonly store: Store<AgentState<Stage, P, O, R>>;
|
|
1217
|
+
/** The Ctx the machine threads to its interpret cells. */
|
|
1218
|
+
readonly ctx: unknown;
|
|
1219
|
+
/**
|
|
1220
|
+
* The run-terminality predicate (#46) — what makes `result()` first-class.
|
|
1221
|
+
* Defaults to the agent's own terminal phases (`done` / `failed`).
|
|
1222
|
+
*/
|
|
1223
|
+
readonly terminal?: (state: AgentState<Stage, P, O, R>) => boolean;
|
|
1224
|
+
/**
|
|
1225
|
+
* Map one semantic {@link AgentEvent} to the consumer's SSE frame, or `null`
|
|
1226
|
+
* to skip it. This is the ONLY place the consumer touches the SSE seam — the
|
|
1227
|
+
* host owns the subscription wiring (`sseFromAgentEvents`) and the hub.
|
|
1228
|
+
*/
|
|
1229
|
+
readonly toSseFrame: (event: AgentEvent<R>) => Frame | null;
|
|
1230
|
+
/** Clock injected for `autoBoot` (the host's only boot clock read). */
|
|
1231
|
+
readonly now?: () => number;
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* The assembled host: the runtime cell + the SSE hub + the framework test seam,
|
|
1235
|
+
* owned ONCE. A DO holds one `AgentHost` and delegates to it; its own surface
|
|
1236
|
+
* shrinks to the domain (the gateway, the WS bridge, the command-send mapping).
|
|
1237
|
+
*
|
|
1238
|
+
* @typeParam Stage Pipeline stage type.
|
|
1239
|
+
* @typeParam P Brain-call purposes.
|
|
1240
|
+
* @typeParam O Per-purpose outputs (bound to {@link AgentTurn}).
|
|
1241
|
+
* @typeParam R Tool result type.
|
|
1242
|
+
* @typeParam Frame The consumer's SSE event shape.
|
|
1243
|
+
*/
|
|
1244
|
+
interface AgentHost<Stage, P extends string, O extends Record<P, AgentTurn>, R, Frame> {
|
|
1245
|
+
/**
|
|
1246
|
+
* Get (or build) the booted runtime. Build-once-boot-once: the first call
|
|
1247
|
+
* builds the machine, wires SSE off the semantic event stream, awaits the boot
|
|
1248
|
+
* gate, runs the `autoBoot` re-fire for a rehydrated suspended run, and caches
|
|
1249
|
+
* the booted `Runtime`; subsequent calls return the cached handle. The single
|
|
1250
|
+
* assembly every consumer used to hand-roll in `getRuntime()`.
|
|
1251
|
+
*/
|
|
1252
|
+
runtime(): Promise<Runtime<AgentState<Stage, P, O, R>, AgentMachineMsg<P, O, R>, AgentEvent<R>>>;
|
|
1253
|
+
/**
|
|
1254
|
+
* The agent's lifecycle status (#49) — the ONE typed channel a consumer reads
|
|
1255
|
+
* instead of re-deriving `run.phase` / `awaiting` by hand. Ensures the runtime
|
|
1256
|
+
* is built + boot-reconciled first. Replaces the per-consumer `isSuspended` /
|
|
1257
|
+
* `runPhase` test methods (read `status().kind`).
|
|
1258
|
+
*/
|
|
1259
|
+
status(): Promise<AgentStatus<Stage>>;
|
|
1260
|
+
/**
|
|
1261
|
+
* The run's terminal State (#46), or `undefined` while in flight. Read the
|
|
1262
|
+
* run's product off it (e.g. `result()?.output`). Replaces the per-consumer
|
|
1263
|
+
* `verdict` plumbing's source. Ensures the runtime is built first.
|
|
1264
|
+
*/
|
|
1265
|
+
result(): Promise<AgentState<Stage, P, O, R> | undefined>;
|
|
1266
|
+
/**
|
|
1267
|
+
* The SSE hub — the route (`host.sse.open()`) AND the test seam
|
|
1268
|
+
* (`host.sse.register(sink)`). The host drives `hub.emit` off the semantic
|
|
1269
|
+
* event stream; the consumer never touches it except to open / register.
|
|
1270
|
+
*/
|
|
1271
|
+
readonly sse: SseHub<Frame>;
|
|
1272
|
+
/**
|
|
1273
|
+
* Tear down the cached runtime (drain its tail via `stop()`, drop the handle)
|
|
1274
|
+
* so the next `runtime()` rebuilds from storage. The framework `settle` test
|
|
1275
|
+
* seam, owned once.
|
|
1276
|
+
*/
|
|
1277
|
+
reset(): Promise<void>;
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Build an {@link AgentHost} from the consumer's domain mappings. Owns the
|
|
1281
|
+
* runtime+events+autoBoot assembly and the framework test seam — the wiring
|
|
1282
|
+
* issue #53 had every consumer re-write.
|
|
1283
|
+
*
|
|
1284
|
+
* @example
|
|
1285
|
+
* const host = createAgentHost<Stage, Purpose, Outputs, ClientResult, SseEvent>({
|
|
1286
|
+
* buildMachine: () => agent.toMachine<Ctx>({ toolInterpret }),
|
|
1287
|
+
* store: doStore(storage, parse),
|
|
1288
|
+
* ctx,
|
|
1289
|
+
* toSseFrame: (e) =>
|
|
1290
|
+
* e.type === "ToolSettled" ? { kind: "result", ...} :
|
|
1291
|
+
* e.type === "TurnSettled" && e.turn.toolCalls.length === 0
|
|
1292
|
+
* ? { kind: "verdict", verdict: e.turn.content } :
|
|
1293
|
+
* e.type === "RunDone" ? { kind: "phase", phase: "done" } : null,
|
|
1294
|
+
* });
|
|
1295
|
+
* // route: return host.sse.open();
|
|
1296
|
+
* // start: await (await host.runtime()).dispatch({ type: "agent_start", ... });
|
|
1297
|
+
* // test: (await host.status()).kind === "suspended"
|
|
1298
|
+
*/
|
|
1299
|
+
declare function createAgentHost<Stage, P extends string, O extends Record<P, AgentTurn>, R, Frame>(config: AgentHostConfig<Stage, P, O, R, Frame>): AgentHost<Stage, P, O, R, Frame>;
|
|
1300
|
+
|
|
1301
|
+
/**
|
|
1302
|
+
* @demlik/tea/do — the idempotent-effect brick (the receiver/effect-side dedup).
|
|
1303
|
+
*
|
|
1304
|
+
* ## The half this closes (do NOT re-implement the ledger)
|
|
1305
|
+
*
|
|
1306
|
+
* The runtime's `stepDispatch` step order is SAVE-BEFORE-EFFECTS: state is
|
|
1307
|
+
* persisted (`store.save(next)`) *before* its commands are interpreted
|
|
1308
|
+
* (`packages/tea/src/index.ts` — "Save-before-effects is the hard ordering;
|
|
1309
|
+
* tests pin it"). So a DO evicted after `save` but before an effect's result is
|
|
1310
|
+
* folded RE-FIRES that effect on the next activation.
|
|
1311
|
+
*
|
|
1312
|
+
* `durable-effects.ts` (sibling) owns the SENDER half of this: the at-least-once
|
|
1313
|
+
* `pendingEffectsLedger`, whose surviving owed entries `reissueSurvivingEffects`
|
|
1314
|
+
* re-fires on activation. Its own header states the contract this file fulfils:
|
|
1315
|
+
* "At-least-once ⇒ a confirmed-but-not-yet-persisted delivery WILL be re-sent
|
|
1316
|
+
* after a crash, so the receiver must deduplicate by id" and "the receiver owns
|
|
1317
|
+
* its own dedup." This module IS that receiver-side dedup — the piece that
|
|
1318
|
+
* upgrades the ledger's at-least-once re-fire to **effectively-once at the
|
|
1319
|
+
* external write**:
|
|
1320
|
+
*
|
|
1321
|
+
* - ledger (`survivingEffects` / `reissueSurvivingEffects`) = at-least-once re-fire;
|
|
1322
|
+
* - this brick (`idempotentEffect` + `appliedEffects`) = effect-side dedup
|
|
1323
|
+
* ⇒ together, effectively-once.
|
|
1324
|
+
*
|
|
1325
|
+
* It COMPLEMENTS the ledger; it does not re-implement it. The ledger keys owed
|
|
1326
|
+
* effects by a monotonic `DeliveryId` (its private sender bookkeeping); this
|
|
1327
|
+
* brick keys applied effects by a caller-supplied durable `EffectKey` (e.g.
|
|
1328
|
+
* `(runId, violationId)`), because dedup identity must be stable across the
|
|
1329
|
+
* re-fire — the same external write must resolve to the same key on every
|
|
1330
|
+
* activation, which a fresh monotonic id cannot guarantee.
|
|
1331
|
+
*
|
|
1332
|
+
* ## The load-bearing rule (mirrors `durable-effects.ts`)
|
|
1333
|
+
*
|
|
1334
|
+
* The applied-marker set is a PURE FOLD over the same kind of event stream the
|
|
1335
|
+
* actor already replays — it is **NOT** a separately-persisted side table:
|
|
1336
|
+
*
|
|
1337
|
+
* - `effect_applied` — `{ key }` ADDS the key to the applied set.
|
|
1338
|
+
* - `effect_forgotten` — `{ key }` REMOVES it (retention — see below).
|
|
1339
|
+
*
|
|
1340
|
+
* `foldApplied(events)` is `events.reduce(applyAppliedEvent, empty)` — the same
|
|
1341
|
+
* fold shape `replay` / `foldLedger` use. This makes the marker live "alongside
|
|
1342
|
+
* State (single authority)" for BOTH persistence modes:
|
|
1343
|
+
*
|
|
1344
|
+
* - `doEventSourcedStore`: persist the two events into the SAME log it already
|
|
1345
|
+
* appends; folding the log on activation rebuilds the applied set for free.
|
|
1346
|
+
* - default snapshot `doStore`: carry the `AppliedEffects` set as a field of
|
|
1347
|
+
* the Model, round-tripped by the store's `serialize`/`parse` hooks (a `Set`
|
|
1348
|
+
* flattens under `JSON.stringify`, exactly the `Map` sharp edge #182), so it
|
|
1349
|
+
* is checkpointed with the rest of `S`.
|
|
1350
|
+
*
|
|
1351
|
+
* Either way the marker is folded/snapshotted with State, never a side channel.
|
|
1352
|
+
*
|
|
1353
|
+
* ## Ordering — why the marker is recorded AFTER `fn` (at-least-once, not at-most)
|
|
1354
|
+
*
|
|
1355
|
+
* `run` invokes `fn` and only THEN yields the `effect_applied` event to persist.
|
|
1356
|
+
* An eviction in the window between `fn` succeeding and that event being folded
|
|
1357
|
+
* re-fires the effect (at-least-once) — correct, because recording the marker
|
|
1358
|
+
* BEFORE `fn` would let a crash strand the marker with the write never made
|
|
1359
|
+
* (at-most-once ⇒ a silently-dropped effect). Once the marker is folded, every
|
|
1360
|
+
* subsequent re-fire of that key is a proven no-op: `fn` is not invoked again.
|
|
1361
|
+
* The ledger guarantees the re-fire; this brick swallows the duplicate.
|
|
1362
|
+
*
|
|
1363
|
+
* ## Retention — pruning mirrors the ledger's confirm-removes-entry (not a new policy)
|
|
1364
|
+
*
|
|
1365
|
+
* An applied-marker is only needed while its effect can still re-fire. The
|
|
1366
|
+
* ledger stops re-firing an effect once its `effect_confirmed` removes the owed
|
|
1367
|
+
* entry — so at that same point the marker is dead weight and is pruned via
|
|
1368
|
+
* `effect_forgotten` (`guard.forget(key)`), keeping the applied set bounded. This
|
|
1369
|
+
* deliberately reuses the ledger's confirm-removes-entry retention shape rather
|
|
1370
|
+
* than inventing a second policy (the #227 triage's one open question).
|
|
1371
|
+
*/
|
|
1372
|
+
|
|
1373
|
+
/**
|
|
1374
|
+
* The caller-supplied durable dedup identity for an effect — stable across the
|
|
1375
|
+
* re-fire (e.g. `` `${runId}:${violationId}` ``). Unlike the ledger's monotonic
|
|
1376
|
+
* `DeliveryId`, it is chosen by the domain so the SAME external write resolves
|
|
1377
|
+
* to the SAME key on every activation.
|
|
1378
|
+
*/
|
|
1379
|
+
type EffectKey = string;
|
|
1380
|
+
/** An effect keyed `key` has been APPLIED. Adds `key` to the set when folded. */
|
|
1381
|
+
interface EffectApplied extends Cmd<"effect_applied"> {
|
|
1382
|
+
readonly key: EffectKey;
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* An applied-marker is no longer needed (its effect can no longer re-fire, e.g.
|
|
1386
|
+
* its ledger entry was confirmed) and may be pruned. Removes `key` when folded.
|
|
1387
|
+
*/
|
|
1388
|
+
interface EffectForgotten extends Cmd<"effect_forgotten"> {
|
|
1389
|
+
readonly key: EffectKey;
|
|
1390
|
+
}
|
|
1391
|
+
/** The event union the applied-marker set folds over. */
|
|
1392
|
+
type AppliedEffectsEvent = EffectApplied | EffectForgotten;
|
|
1393
|
+
/** The set of keys whose effect has been applied. A pure fold result. */
|
|
1394
|
+
type AppliedEffects = ReadonlySet<EffectKey>;
|
|
1395
|
+
/** The empty applied set — the fold's seed (a fresh actor has applied nothing). */
|
|
1396
|
+
declare function emptyApplied(): AppliedEffects;
|
|
1397
|
+
/**
|
|
1398
|
+
* Apply ONE event — the reducer at the heart of the fold.
|
|
1399
|
+
*
|
|
1400
|
+
* - `effect_applied` → add `key` (a duplicate add is a no-op — a re-fired
|
|
1401
|
+
* already-applied key tolerated by at-least-once).
|
|
1402
|
+
* - `effect_forgotten` → remove `key` (a no-op if already absent).
|
|
1403
|
+
*
|
|
1404
|
+
* Pure: returns a NEW set on change, never mutates the input — so folding the
|
|
1405
|
+
* same events twice (live and post-eviction) yields equal results.
|
|
1406
|
+
*/
|
|
1407
|
+
declare function applyAppliedEvent(applied: AppliedEffects, event: AppliedEffectsEvent): AppliedEffects;
|
|
1408
|
+
/**
|
|
1409
|
+
* THE FOLD. Rebuild the applied set from an event stream — the same fold shape
|
|
1410
|
+
* `replay`/`foldLedger` use. After a simulated eviction this is called over the
|
|
1411
|
+
* persisted events and yields a set equal to the never-evicted one, because a
|
|
1412
|
+
* pure fold over identical input is identical. This is what proves "no side
|
|
1413
|
+
* table": the set is `events` reduced, nothing more.
|
|
1414
|
+
*/
|
|
1415
|
+
declare function foldApplied(events: Iterable<AppliedEffectsEvent>): AppliedEffects;
|
|
1416
|
+
/** Has this key's effect already been applied (folded into the set)? */
|
|
1417
|
+
declare function isApplied(applied: AppliedEffects, key: EffectKey): boolean;
|
|
1418
|
+
/**
|
|
1419
|
+
* A keyed external effect. `key` is the durable dedup identity; `fn` is the
|
|
1420
|
+
* irreversible external write (HTTP write, DB upsert, finding/ticket publish).
|
|
1421
|
+
*/
|
|
1422
|
+
interface IdempotentEffect<A> {
|
|
1423
|
+
readonly key: EffectKey;
|
|
1424
|
+
readonly fn: () => Promise<A> | A;
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Pair a durable `key` with an external effect `fn` — the brick's primary
|
|
1428
|
+
* surface. Run it through {@link appliedEffects}'s guard; a re-fire whose key is
|
|
1429
|
+
* already applied is a proven no-op (`fn` is not invoked a second time).
|
|
1430
|
+
*/
|
|
1431
|
+
declare function idempotentEffect<A>(key: EffectKey, fn: () => Promise<A> | A): IdempotentEffect<A>;
|
|
1432
|
+
/**
|
|
1433
|
+
* The outcome of running a keyed effect through the guard. A boolean-discriminated
|
|
1434
|
+
* union so the skipped arm cannot carry a (never-produced) `result`/`event`:
|
|
1435
|
+
*
|
|
1436
|
+
* - `ran: true` — first application: `fn` ran, `result` is its value, and
|
|
1437
|
+
* `event` is the `effect_applied` marker the caller MUST persist (fold into
|
|
1438
|
+
* State) so future re-fires are swallowed.
|
|
1439
|
+
* - `ran: false` — the key was already applied: `fn` was NOT invoked, the
|
|
1440
|
+
* duplicate is swallowed, and there is no new event to persist.
|
|
1441
|
+
*/
|
|
1442
|
+
type IdempotentOutcome<A> = {
|
|
1443
|
+
readonly ran: true;
|
|
1444
|
+
readonly key: EffectKey;
|
|
1445
|
+
readonly result: A;
|
|
1446
|
+
readonly event: EffectApplied;
|
|
1447
|
+
} | {
|
|
1448
|
+
readonly ran: false;
|
|
1449
|
+
readonly key: EffectKey;
|
|
1450
|
+
};
|
|
1451
|
+
/**
|
|
1452
|
+
* A live guard over an applied-marker set. Mirrors `pendingEffectsLedger`'s
|
|
1453
|
+
* recorder shape: it keeps an in-memory mirror folded from the same events the
|
|
1454
|
+
* caller persists, so the dedup decision is available without a re-read.
|
|
1455
|
+
*
|
|
1456
|
+
* - `run(effect)` — apply-once: skip (no `fn`) if the key is already applied,
|
|
1457
|
+
* else await `fn` and yield the `effect_applied` event to PERSIST.
|
|
1458
|
+
* - `forget(key)` — prune a marker once its effect can no longer re-fire;
|
|
1459
|
+
* yields the `effect_forgotten` event to PERSIST.
|
|
1460
|
+
* - `applied()` — the current folded set.
|
|
1461
|
+
* - `isApplied(key)`— convenience membership test against the mirror.
|
|
1462
|
+
*/
|
|
1463
|
+
interface AppliedEffectsGuard {
|
|
1464
|
+
run<A>(effect: IdempotentEffect<A>): Promise<IdempotentOutcome<A>>;
|
|
1465
|
+
forget(key: EffectKey): {
|
|
1466
|
+
readonly event: EffectForgotten;
|
|
1467
|
+
};
|
|
1468
|
+
applied(): AppliedEffects;
|
|
1469
|
+
isApplied(key: EffectKey): boolean;
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Build a live guard. `restore` rehydrates it after an eviction: pass the events
|
|
1473
|
+
* read back from the log (or the applied set carried in the reloaded snapshot,
|
|
1474
|
+
* as single-element `effect_applied` events) so the mirror reflects what was
|
|
1475
|
+
* already applied. Omit `restore` for a fresh actor.
|
|
1476
|
+
*/
|
|
1477
|
+
declare function appliedEffects(restore?: {
|
|
1478
|
+
readonly events?: Iterable<AppliedEffectsEvent>;
|
|
1479
|
+
}): AppliedEffectsGuard;
|
|
1480
|
+
|
|
1481
|
+
/**
|
|
1482
|
+
* @demlik/tea/do — generic hibernatable PRESENCE + BROADCAST for a NATIVE
|
|
1483
|
+
* (non-agent) Durable-Object grain.
|
|
1484
|
+
*
|
|
1485
|
+
* ## The gap this fills
|
|
1486
|
+
*
|
|
1487
|
+
* `./host` ships `acceptCommandSocket` / `acceptDurableCommandSocket` /
|
|
1488
|
+
* `broadcast` / `broadcastHibernatable`, but those are framed for — and live
|
|
1489
|
+
* alongside — the LLM-agent **deferred-tool gateway** (`createAgentHost`): the
|
|
1490
|
+
* socket is "the command-runner socket", the frame is "a command", and the
|
|
1491
|
+
* fan-out is the agent's `sendCommand` loop. A plain realtime grain — a game
|
|
1492
|
+
* room, a presence list, a collab document — does not have an agent, a gateway,
|
|
1493
|
+
* or commands. It just wants to accept hibernatable client sockets and fan a
|
|
1494
|
+
* frame out to whoever is connected. Today every such grain (vortex's
|
|
1495
|
+
* `ArenaGrain`, #162) hand-rolls the same `ctx.acceptWebSocket(server)` +
|
|
1496
|
+
* `for (const ws of ctx.getWebSockets()) ws.send(...)` boilerplate (#181).
|
|
1497
|
+
*
|
|
1498
|
+
* This module lifts exactly that, decoupled from the agent host:
|
|
1499
|
+
*
|
|
1500
|
+
* - {@link broadcastFrame} — the fan-out, with the **socket set as the input**
|
|
1501
|
+
* (not a `ctx`), so it is a pure function testable with fake sockets and
|
|
1502
|
+
* reusable over any iterable (`ctx.getWebSockets()`, a filtered subset, a
|
|
1503
|
+
* test array). It serializes once, sends to every OPEN socket, and skips a
|
|
1504
|
+
* closed/errored one — returning a {@link BroadcastReport} count rather than
|
|
1505
|
+
* swallowing the skip silently (errors are data).
|
|
1506
|
+
* - {@link registerHibernatableSocket} — register an already-created server
|
|
1507
|
+
* socket on the DO via the Cloudflare **Hibernation API**
|
|
1508
|
+
* (`ctx.acceptWebSocket`), optionally tagging it and persisting a
|
|
1509
|
+
* per-socket attachment that survives eviction. Pure over the `ctx` +
|
|
1510
|
+
* socket seam, so the registration logic is testable with fakes.
|
|
1511
|
+
* - {@link acceptPresenceSocket} — the full upgrade: build the
|
|
1512
|
+
* `WebSocketPair`, register the server end, and return the `server` socket
|
|
1513
|
+
* plus the 101 upgrade `Response`. The one piece that touches the
|
|
1514
|
+
* runtime-only `WebSocketPair` (so it is exercised by type, not by a unit
|
|
1515
|
+
* test without workerd) — the registration it delegates to *is* unit-tested.
|
|
1516
|
+
* - {@link presenceCount} — the live connection count off `ctx.getWebSockets`,
|
|
1517
|
+
* the trivial roster-size lift a presence grain reaches for.
|
|
1518
|
+
*
|
|
1519
|
+
* It is transport/Msg-agnostic: `frame` is `unknown` (JSON-serialized by
|
|
1520
|
+
* default, or via a caller `serialize`), and nothing here references an agent,
|
|
1521
|
+
* a gateway, a Sub, or a Msg. It is ADDITIVE — `./host`'s agent-coupled
|
|
1522
|
+
* helpers keep working untouched for the agent consumer.
|
|
1523
|
+
*/
|
|
1524
|
+
/**
|
|
1525
|
+
* The minimal "send a string frame" surface {@link broadcastFrame} needs. A real
|
|
1526
|
+
* Cloudflare `WebSocket` satisfies it structurally (its `send` accepts a string
|
|
1527
|
+
* and it carries a numeric `readyState`), so `ctx.getWebSockets()` flows in
|
|
1528
|
+
* directly; a test supplies a fake with just `send` (and optionally
|
|
1529
|
+
* `readyState`).
|
|
1530
|
+
*
|
|
1531
|
+
* `readyState` is OPTIONAL: when present and not {@link WS_READY_STATE_OPEN},
|
|
1532
|
+
* the socket is skipped WITHOUT calling `send`; when absent, `send` is attempted
|
|
1533
|
+
* and a throw is caught (a fake that models only liveness via a throwing `send`
|
|
1534
|
+
* needs no `readyState`).
|
|
1535
|
+
*/
|
|
1536
|
+
interface PresenceSocket {
|
|
1537
|
+
send(message: string): void;
|
|
1538
|
+
readonly readyState?: number;
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1541
|
+
* The Hibernation slice of `DurableObjectState` a presence grain needs — the
|
|
1542
|
+
* accept that hands the socket to the runtime (surviving eviction) and the
|
|
1543
|
+
* getter that repopulates the live set after a wake. A real `DurableObjectState`
|
|
1544
|
+
* satisfies this structurally; a test supplies a fake (no live Workers runtime
|
|
1545
|
+
* needed to exercise the accept/registry seam).
|
|
1546
|
+
*/
|
|
1547
|
+
interface PresenceCtx {
|
|
1548
|
+
acceptWebSocket(ws: WebSocket, tags?: string[]): void;
|
|
1549
|
+
getWebSockets(tag?: string): WebSocket[];
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* The OPEN `readyState` value (`WebSocket.READY_STATE_OPEN` in the Cloudflare
|
|
1553
|
+
* runtime). Named here so {@link broadcastFrame} can skip a non-open socket
|
|
1554
|
+
* without referencing the runtime constant (keeping the fold pure + testable).
|
|
1555
|
+
*/
|
|
1556
|
+
declare const WS_READY_STATE_OPEN = 1;
|
|
1557
|
+
/**
|
|
1558
|
+
* What a {@link broadcastFrame} fan-out did, surfaced rather than swallowed:
|
|
1559
|
+
* `sent` is the number of sockets the frame reached; `skipped` is the number
|
|
1560
|
+
* passed over (not OPEN, errored on `send`, or the `except` socket). A caller
|
|
1561
|
+
* can log or assert these — a silent fan-out hides a room going dark.
|
|
1562
|
+
*/
|
|
1563
|
+
interface BroadcastReport {
|
|
1564
|
+
readonly sent: number;
|
|
1565
|
+
readonly skipped: number;
|
|
1566
|
+
}
|
|
1567
|
+
/** Optional knobs for {@link broadcastFrame}. */
|
|
1568
|
+
interface BroadcastOptions<S extends PresenceSocket> {
|
|
1569
|
+
/**
|
|
1570
|
+
* A socket to exclude from the fan-out — typically the sender, so a collab /
|
|
1571
|
+
* chat frame is not echoed back to its originator. Compared by identity.
|
|
1572
|
+
*/
|
|
1573
|
+
readonly except?: S;
|
|
1574
|
+
/**
|
|
1575
|
+
* How to turn `frame` into the wire string. Defaults to `JSON.stringify`.
|
|
1576
|
+
* Override for a non-JSON wire format (e.g. a pre-encoded string passthrough).
|
|
1577
|
+
*/
|
|
1578
|
+
readonly serialize?: (frame: unknown) => string;
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* Serialize `frame` ONCE and send it to every OPEN socket in `sockets`, skipping
|
|
1582
|
+
* any that is closed, errors on `send`, or is the `except` socket. The socket
|
|
1583
|
+
* set is the INPUT (not a `ctx`), so this is a pure function: pass
|
|
1584
|
+
* `ctx.getWebSockets()` in a live grain, a filtered subset for a targeted
|
|
1585
|
+
* fan-out, or a fake array in a test.
|
|
1586
|
+
*
|
|
1587
|
+
* Skip semantics (a dead socket never crashes the broadcast):
|
|
1588
|
+
* - `readyState` present and not {@link WS_READY_STATE_OPEN} → skipped, `send`
|
|
1589
|
+
* not called;
|
|
1590
|
+
* - `send` throws (a socket closing mid-fan-out) → skipped, the throw is
|
|
1591
|
+
* caught (the close lifecycle reaps it);
|
|
1592
|
+
* - the `except` socket → skipped.
|
|
1593
|
+
*
|
|
1594
|
+
* Returns a {@link BroadcastReport} of `{ sent, skipped }` — the skip is data,
|
|
1595
|
+
* not silence.
|
|
1596
|
+
*/
|
|
1597
|
+
declare function broadcastFrame<S extends PresenceSocket>(sockets: Iterable<S>, frame: unknown, options?: BroadcastOptions<S>): BroadcastReport;
|
|
1598
|
+
/** Optional knobs for {@link registerHibernatableSocket} / {@link acceptPresenceSocket}. */
|
|
1599
|
+
interface RegisterOptions<A> {
|
|
1600
|
+
/**
|
|
1601
|
+
* Hibernation tags for the socket — the same strings `ctx.getWebSockets(tag)`
|
|
1602
|
+
* filters on, so a grain can fan out to a subset (e.g. a team, a channel).
|
|
1603
|
+
*/
|
|
1604
|
+
readonly tags?: readonly string[];
|
|
1605
|
+
/**
|
|
1606
|
+
* A per-socket value to persist via `server.serializeAttachment` so it
|
|
1607
|
+
* survives DO eviction (e.g. the player/session id the grain re-reads in
|
|
1608
|
+
* `webSocketMessage`/`webSocketClose`). Omitted ⇒ no attachment written.
|
|
1609
|
+
*/
|
|
1610
|
+
readonly attachment?: A;
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* The subset of `WebSocket` {@link registerHibernatableSocket} writes to — the
|
|
1614
|
+
* attachment serializer. A real server `WebSocket` satisfies it; a test supplies
|
|
1615
|
+
* a fake that records the serialized attachment.
|
|
1616
|
+
*/
|
|
1617
|
+
interface AttachableSocket {
|
|
1618
|
+
serializeAttachment(value: unknown): void;
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Register an already-created `server` socket on the DO via the Cloudflare
|
|
1622
|
+
* **Hibernation API**: `ctx.acceptWebSocket(server, tags)` hands the socket to
|
|
1623
|
+
* the runtime (it survives eviction and re-delivers inbound frames to the DO's
|
|
1624
|
+
* `webSocketMessage` lifecycle method), and — when an `attachment` is supplied —
|
|
1625
|
+
* persists it via `server.serializeAttachment` so it survives the wake.
|
|
1626
|
+
*
|
|
1627
|
+
* Pure over the `ctx` + socket seam (no `WebSocketPair` construction), so the
|
|
1628
|
+
* registration logic — that accept is called with the right tags, that the
|
|
1629
|
+
* attachment is serialized iff present — is unit-testable with fakes.
|
|
1630
|
+
* {@link acceptPresenceSocket} is the thin wrapper that also builds the pair.
|
|
1631
|
+
*/
|
|
1632
|
+
declare function registerHibernatableSocket<S extends AttachableSocket, A>(ctx: PresenceCtx, server: S, options?: RegisterOptions<A>): void;
|
|
1633
|
+
/** What {@link acceptPresenceSocket} hands back: the live server end (to send
|
|
1634
|
+
* initial frames on) and the 101 upgrade `Response` (to return from `fetch`). */
|
|
1635
|
+
interface PresenceUpgrade {
|
|
1636
|
+
/** The server end of the pair — send the welcome / initial snapshot on it. */
|
|
1637
|
+
readonly server: WebSocket;
|
|
1638
|
+
/** The 101 Switching-Protocols upgrade response carrying the client end. */
|
|
1639
|
+
readonly response: Response;
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Accept a client WebSocket upgrade on a native DO and register it for
|
|
1643
|
+
* hibernation. Builds the `WebSocketPair`, registers the server end via
|
|
1644
|
+
* {@link registerHibernatableSocket} (tags + optional attachment), and returns
|
|
1645
|
+
* the `server` socket plus the 101 upgrade {@link PresenceUpgrade.response}.
|
|
1646
|
+
*
|
|
1647
|
+
* The caller's `fetch` collapses to: send any initial frames on `server`
|
|
1648
|
+
* (welcome, current roster/snapshot), then `return upgrade.response`. This lifts
|
|
1649
|
+
* the `new WebSocketPair()` + `ctx.acceptWebSocket` + `serializeAttachment` +
|
|
1650
|
+
* `new Response(null, { status: 101, webSocket })` boilerplate a presence grain
|
|
1651
|
+
* hand-rolls.
|
|
1652
|
+
*
|
|
1653
|
+
* NOTE: `WebSocketPair` is a runtime-only global (no workerd in unit tests), so
|
|
1654
|
+
* this assembly is exercised by TYPE here; its registration seam is unit-tested
|
|
1655
|
+
* via {@link registerHibernatableSocket}.
|
|
1656
|
+
*
|
|
1657
|
+
* @example
|
|
1658
|
+
* async fetch(request: Request): Promise<Response> {
|
|
1659
|
+
* if (request.headers.get("Upgrade") !== "websocket") {
|
|
1660
|
+
* return new Response("expected a websocket upgrade", { status: 426 });
|
|
1661
|
+
* }
|
|
1662
|
+
* const { server, response } = acceptPresenceSocket(this.ctx, {
|
|
1663
|
+
* attachment: { playerId },
|
|
1664
|
+
* });
|
|
1665
|
+
* server.send(JSON.stringify(welcome));
|
|
1666
|
+
* server.send(JSON.stringify(room.snapshot()));
|
|
1667
|
+
* return response;
|
|
1668
|
+
* }
|
|
1669
|
+
*/
|
|
1670
|
+
declare function acceptPresenceSocket<A>(ctx: PresenceCtx, options?: RegisterOptions<A>): PresenceUpgrade;
|
|
1671
|
+
/**
|
|
1672
|
+
* The live connection count for a presence grain — `ctx.getWebSockets(tag)
|
|
1673
|
+
* .length`, repopulated by the runtime after a wake. Pass a `tag` to count only
|
|
1674
|
+
* the sockets registered under it. The trivial roster-size lift a presence /
|
|
1675
|
+
* game grain reaches for (vortex's `connectionCount`).
|
|
1676
|
+
*/
|
|
1677
|
+
declare function presenceCount(ctx: PresenceCtx, tag?: string): number;
|
|
2
1678
|
|
|
3
1679
|
/**
|
|
4
1680
|
* @demlik/tea/do — Durable Object adapter for `@demlik/tea`.
|
|
5
1681
|
*
|
|
6
|
-
*
|
|
1682
|
+
* The integration is built on ONE transport model and one durability primitive:
|
|
7
1683
|
*
|
|
8
1684
|
* 1. `doStore<S>(storage, parse, key?)` — `Store<S>` impl backed by
|
|
9
1685
|
* `DurableObjectStorage`. JSON-stringifies on save, JSON-parses on load
|
|
@@ -15,31 +1691,46 @@ import { Store, SubId, Sub } from '../index.js';
|
|
|
15
1691
|
* synchronously"). Shape mismatch is NOT a throw — `parse` returns
|
|
16
1692
|
* `null` and the substrate boots fresh.
|
|
17
1693
|
*
|
|
18
|
-
* 2. `
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* -
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* dispatching `sub.msg(data)` for matching `socketId`s.
|
|
38
|
-
*
|
|
39
|
-
* `src/test-worker.ts` is the canonical example of this cooperation. See
|
|
40
|
-
* `test/README.md` for the convention.
|
|
1694
|
+
* 2. `deferredGateway` + `createAgentHost` (from `./host`) — THE transport for
|
|
1695
|
+
* a DO-hosted agent. A `createAgent().toMachine()` machine owns exactly one
|
|
1696
|
+
* Sub type, `DeadlineSub` (the agent's own retry + watchdog timers); it has
|
|
1697
|
+
* no DO-native Sub variant to compose, and it needs none. The gateway owns
|
|
1698
|
+
* each deferred tool round-trip as a Promise the interpret cell awaits:
|
|
1699
|
+
* - Inbound WebSocket frames are bridged straight into `gateway.settle(...)`
|
|
1700
|
+
* from the DO's `webSocketMessage` lifecycle method — a direct dispatch,
|
|
1701
|
+
* NOT a Sub.
|
|
1702
|
+
* - The per-tool deadline is a `setTimeout` inside the gateway, NOT a DO
|
|
1703
|
+
* alarm.
|
|
1704
|
+
* One model: the transport lives outside the Sub system, bridged into
|
|
1705
|
+
* `dispatch` by the gateway. See `./host`'s module doc for the full rationale
|
|
1706
|
+
* and `.patterns/tea/durable-actors.md` for the host-layer north star.
|
|
1707
|
+
*
|
|
1708
|
+
* (Historical note: this module previously also shipped `do_ws` / `do_alarm`
|
|
1709
|
+
* Subs + a `doSubscribe` registry. Because `toMachine` fixes the agent machine's
|
|
1710
|
+
* Sub type to `DeadlineSub`, those Subs could never union into an agent host's
|
|
1711
|
+
* Sub set — they were structurally unusable by the flagship consumer and had no
|
|
1712
|
+
* callers. They were dropped in favor of the gateway as the single transport.)
|
|
41
1713
|
*/
|
|
42
1714
|
|
|
1715
|
+
/**
|
|
1716
|
+
* Options for {@link doStore}. Mirrors the optional-bag shape
|
|
1717
|
+
* `doEventSourcedStore` already uses for its `parse`/`keys`, so the two DO
|
|
1718
|
+
* stores read alike.
|
|
1719
|
+
*/
|
|
1720
|
+
interface DoStoreOptions<S> {
|
|
1721
|
+
/** Storage key the snapshot cell lives at. Defaults to `@@state`. */
|
|
1722
|
+
readonly key?: string;
|
|
1723
|
+
/**
|
|
1724
|
+
* The serialize half of the boundary (#182) — the INVERSE of `doStore`'s
|
|
1725
|
+
* `parse`. Maps the live `S` to a JSON-safe carrier before `JSON.stringify`,
|
|
1726
|
+
* so a Model holding a `Map`/`Set`/`Date` (which `JSON.stringify` would
|
|
1727
|
+
* silently flatten) round-trips: `parse` reconstructs the rich `S` from the
|
|
1728
|
+
* carrier on load. Omit for a plain-JSON Model (the identity default — the
|
|
1729
|
+
* `Record`-not-`Map` constraint, unchanged). When present, `parse` MUST
|
|
1730
|
+
* accept whatever `serialize` produces.
|
|
1731
|
+
*/
|
|
1732
|
+
readonly serialize?: (state: S) => unknown;
|
|
1733
|
+
}
|
|
43
1734
|
/**
|
|
44
1735
|
* `Store<S>` over `DurableObjectStorage`.
|
|
45
1736
|
*
|
|
@@ -62,74 +1753,23 @@ import { Store, SubId, Sub } from '../index.js';
|
|
|
62
1753
|
* behavior in `init`/`update`. After this, the boundary parse rejects the
|
|
63
1754
|
* old shape (returns null) and the DO boots fresh, picking up its new
|
|
64
1755
|
* schema from the next dispatch onward.
|
|
65
|
-
*/
|
|
66
|
-
declare function doStore<S>(storage: DurableObjectStorage, parse: (raw: unknown) => S | null, key?: string): Store<S>;
|
|
67
|
-
/**
|
|
68
|
-
* A DO alarm sub: fires `msg` at `firesAt` (epoch ms). The substrate diffs
|
|
69
|
-
* subs by `id`; same id across transitions = same alarm. To reschedule, emit
|
|
70
|
-
* a DIFFERENT id (e.g., `audit-timeout-v2`).
|
|
71
|
-
*/
|
|
72
|
-
type DoAlarmSub<M> = {
|
|
73
|
-
id: SubId;
|
|
74
|
-
type: "do_alarm";
|
|
75
|
-
firesAt: number;
|
|
76
|
-
msg: M;
|
|
77
|
-
};
|
|
78
|
-
/**
|
|
79
|
-
* A DO WebSocket sub: routes incoming messages on `socketId` through
|
|
80
|
-
* `sub.msg(data)` to dispatch.
|
|
81
|
-
*/
|
|
82
|
-
type DoWsSub<M> = {
|
|
83
|
-
id: SubId;
|
|
84
|
-
type: "do_ws";
|
|
85
|
-
socketId: string;
|
|
86
|
-
msg: (data: string) => M;
|
|
87
|
-
};
|
|
88
|
-
type DoSub<M> = DoAlarmSub<M> | DoWsSub<M>;
|
|
89
|
-
type AssertDoSubIsSub<T extends Sub = DoSub<unknown>> = T;
|
|
90
|
-
/**
|
|
91
|
-
* Registry of pending alarm dispatches keyed by sub `id`. The cooperating DO's
|
|
92
|
-
* `alarm()` lifecycle method drains this and dispatches each msg.
|
|
93
|
-
*
|
|
94
|
-
* One alarm-queue cell at a time per DO (DO has exactly one alarm slot). We
|
|
95
|
-
* still key by `id` so multiple alarm subs scheduled in the same reconcile
|
|
96
|
-
* pass don't clobber each other — the earliest `firesAt` wins (DO native
|
|
97
|
-
* behavior) and `alarm()` drains everything due by the fire time.
|
|
98
|
-
*/
|
|
99
|
-
type DoAlarmRegistry<M> = Map<string, {
|
|
100
|
-
firesAt: number;
|
|
101
|
-
msg: M;
|
|
102
|
-
}>;
|
|
103
|
-
/**
|
|
104
|
-
* Registry of WebSocket message subscribers keyed by sub `id`. The cooperating
|
|
105
|
-
* DO's `webSocketMessage(ws, data)` consults this and dispatches
|
|
106
|
-
* `sub.msg(data)` for entries matching the incoming `socketId`.
|
|
107
|
-
*/
|
|
108
|
-
type DoWsRegistry<M> = Map<string, {
|
|
109
|
-
socketId: string;
|
|
110
|
-
msg: (data: string) => M;
|
|
111
|
-
}>;
|
|
112
|
-
/**
|
|
113
|
-
* Minimum shape a cooperating DO must put on its `Ctx` for `doSubscribe`
|
|
114
|
-
* handlers to wire up. Users may extend with anything else they like.
|
|
115
|
-
*/
|
|
116
|
-
interface DoSubscribeCtx<M> {
|
|
117
|
-
state: DurableObjectState;
|
|
118
|
-
alarmRegistry: DoAlarmRegistry<M>;
|
|
119
|
-
wsRegistry: DoWsRegistry<M>;
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Build the `subscribe` handler registry for `do_alarm` and `do_ws` subs.
|
|
123
1756
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
1757
|
+
* Serialization sharp edge (#182): `save` JSON-serializes the Model, and
|
|
1758
|
+
* `JSON.stringify` does NOT survive a `Map`/`Set`/`Date` (a `Map` stringifies
|
|
1759
|
+
* to `{}`, silently losing every entry). A Model that wants a `Map` (the
|
|
1760
|
+
* natural "roster keyed by id") therefore supplies a `serialize` hook — the
|
|
1761
|
+
* INVERSE of `parse` — so the boundary owns both directions: `serialize` turns
|
|
1762
|
+
* the live `S` into a JSON-safe carrier on the way out, `parse` reconstructs
|
|
1763
|
+
* the rich `S` from that carrier on the way back in. They are a matched pair:
|
|
1764
|
+
* `parse(JSON.parse(JSON.stringify(serialize(state))))` must round-trip to a
|
|
1765
|
+
* value equal to `state`. Omit `serialize` and the Model must stay plain-JSON
|
|
1766
|
+
* (the `Record`-not-`Map` constraint the default imposes, unchanged).
|
|
126
1767
|
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
1768
|
+
* @param parse - boundary parse, the deserialize half (`unknown -> S | null`).
|
|
1769
|
+
* @param keyOrOptions - either the storage key (legacy positional form,
|
|
1770
|
+
* defaults to `@@state`) or a {@link DoStoreOptions} bag carrying `key`
|
|
1771
|
+
* and/or the `serialize` hook. A bare string is treated as `{ key }`.
|
|
129
1772
|
*/
|
|
130
|
-
declare function
|
|
131
|
-
do_alarm: (sub: DoAlarmSub<M>, ctx: Ctx, dispatch: (msg: M) => void) => () => void;
|
|
132
|
-
do_ws: (sub: DoWsSub<M>, ctx: Ctx, dispatch: (msg: M) => void) => () => void;
|
|
133
|
-
};
|
|
1773
|
+
declare function doStore<S>(storage: DurableObjectStorage, parse: (raw: unknown) => S | null, keyOrOptions?: string | DoStoreOptions<S>): Store<S>;
|
|
134
1774
|
|
|
135
|
-
export { type
|
|
1775
|
+
export { type AgentHost, type AgentHostConfig, type AlarmStorage, type AppliedEffects, type AppliedEffectsEvent, type AppliedEffectsGuard, type AttachableSocket, type BroadcastOptions, type BroadcastReport, type DeferredGateway, type DoStoreOptions, type DurableCommandCarrier, type DurableTimer, type DurableTimerConfig, type EffectApplied, EffectConfirmed, type EffectForgotten, type EffectKey, EffectOwed, type EventLogRange, type EventSourcedOptions, type EventSourcedStore, type ExecuteStep, type HibernatableCtx, type IdempotentEffect, type IdempotentOutcome, type NextStep, PendingEffectsRecorder, type PersistedEvent, type PresenceCtx, type PresenceSocket, type PresenceUpgrade, type Projection, type ProjectionId, type ProjectionRegistry, type ProjectionRunner, type ProjectionUpdate, type RegisterOptions, type ResumePort, type RunStepLoopConfig, type SseHub, type StepCtx, type StepEngine, type StepHostConfig, type StepLoopOutcome, type StepOutcome, type StepRequest, type StepResponse, type StepResult, type StepTransport, WS_READY_STATE_OPEN, acceptCommandSocket, acceptDurableCommandSocket, acceptPresenceSocket, agentIsResumable, appliedEffects, applyAppliedEvent, autoBoot, bootResume, broadcast, broadcastFrame, broadcastHibernatable, constantTimeEqual, createAgentHost, deferredGateway, dispatchToIdle, doEventSourcedStore, doStore, driveProjections, durableCommandCarrier, durableDeferredGateway, durableTimer, emptyApplied, foldApplied, idempotentEffect, isApplied, mintRunToken, presenceCount, projectionIdString, projectionRegistry, rebuildProjection, registerHibernatableSocket, reissueSurvivingEffects, runProjection, runStepLoop, sseFromAgentEvents, sseHub, sseProjection, stepHost };
|