@helipod/runtime-embedded 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +722 -0
- package/dist/index.js +1143 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
import * as _helipod_docstore_d1 from '@helipod/docstore-d1';
|
|
2
|
+
import { WakeHost, Driver } from '@helipod/component';
|
|
3
|
+
import { ShardId } from '@helipod/id-codec';
|
|
4
|
+
import { SerializedKeyRange } from '@helipod/index-key-codec';
|
|
5
|
+
import { JSONValue } from '@helipod/values';
|
|
6
|
+
import { CommitGuardUnit, DocStore } from '@helipod/docstore';
|
|
7
|
+
import { InlineUdfExecutor, IndexCatalog, RegisteredFunction, LogSink, ContextProvider, TablePolicy, PolicyContextProvider, RelationRegistry, GuestDatabaseWriter, WriteRouter, UdfResult } from '@helipod/executor';
|
|
8
|
+
export { ClientReplay, WriteRouter } from '@helipod/executor';
|
|
9
|
+
import { ClientMessage, ServerMessage, SyncWebSocket, SyncProtocolHandler } from '@helipod/sync';
|
|
10
|
+
import { WrittenDoc, WriteFanout, OplogDelta } from '@helipod/transactor';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The transactor→sync fan-out seam (scale-seam #4). A committed `OplogDelta` is published as
|
|
14
|
+
* a fully-serializable payload to a swappable `EmbeddedWriteFanoutAdapter`. At Tier 0 the
|
|
15
|
+
* adapter is in-memory; swap it for BroadcastChannel / Redis / Queues and the SAME fan-out
|
|
16
|
+
* spans many processes — with no change to app code. Each fan-out tags its `originId` so a
|
|
17
|
+
* subscriber can ignore its own writes (avoiding a self-loop across processes).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
interface EmbeddedWriteFanoutPayload {
|
|
21
|
+
commitTs: number;
|
|
22
|
+
tables: string[];
|
|
23
|
+
ranges: SerializedKeyRange[];
|
|
24
|
+
originId: string;
|
|
25
|
+
/**
|
|
26
|
+
* G4 origin-frontier tag (client-sync verdict §(d) item 2) — the originating sync SESSION id,
|
|
27
|
+
* sourced verbatim from `OplogDelta.origin`. Distinct from `originId` (the fleet-node/process
|
|
28
|
+
* origin used for the cross-process self-loop guard): this is a per-commit ephemeral session tag
|
|
29
|
+
* the drain hands to `handler.notifyWrites(inv, origin)` so the origin session's `version.ts` is
|
|
30
|
+
* advanced past its own commit. Undefined for commits with no originating session.
|
|
31
|
+
*/
|
|
32
|
+
origin?: string;
|
|
33
|
+
/** The shard this commit landed on (Fenced Frontier B1, D6) — sourced verbatim from
|
|
34
|
+
* `OplogDelta.shardId`. Additive: single-shard (Tier 0/B1) deployments always see `"default"`
|
|
35
|
+
* (`DEFAULT_SHARD`); a multi-shard fan-out consumer (B2+) can use it to route/filter, but every
|
|
36
|
+
* existing consumer today ignores it. */
|
|
37
|
+
shardId: string;
|
|
38
|
+
/** Written documents for local row-diffing (§DLR 2a) — sourced verbatim from
|
|
39
|
+
* `OplogDelta.writtenDocs`. Present only on the local in-process fan-out (this adapter always
|
|
40
|
+
* runs in-process at Tier 0/1); absent if a future cross-process adapter swap doesn't carry it. */
|
|
41
|
+
writtenDocs?: WrittenDoc[];
|
|
42
|
+
}
|
|
43
|
+
type FanoutListener = (payload: EmbeddedWriteFanoutPayload) => void;
|
|
44
|
+
interface EmbeddedWriteFanoutAdapter {
|
|
45
|
+
publish(payload: EmbeddedWriteFanoutPayload): void;
|
|
46
|
+
subscribe(listener: FanoutListener): () => void;
|
|
47
|
+
}
|
|
48
|
+
/** The default Tier 0 adapter: an in-process channel (also records what it published). */
|
|
49
|
+
declare class InMemoryWriteFanoutAdapter implements EmbeddedWriteFanoutAdapter {
|
|
50
|
+
private readonly listeners;
|
|
51
|
+
readonly published: EmbeddedWriteFanoutPayload[];
|
|
52
|
+
publish(payload: EmbeddedWriteFanoutPayload): void;
|
|
53
|
+
subscribe(listener: FanoutListener): () => void;
|
|
54
|
+
}
|
|
55
|
+
/** Implements the transactor's `WriteFanout` over a swappable adapter. */
|
|
56
|
+
declare class EmbeddedWriteFanout implements WriteFanout {
|
|
57
|
+
private readonly adapter;
|
|
58
|
+
private readonly originId;
|
|
59
|
+
constructor(adapter: EmbeddedWriteFanoutAdapter, originId: string);
|
|
60
|
+
publish(delta: OplogDelta): void;
|
|
61
|
+
/** Subscribe to deltas from OTHER origins (ignores our own — the Tier 2 self-loop guard). */
|
|
62
|
+
subscribe(listener: FanoutListener): () => void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* In-memory loopback transport. A `LoopbackConnection` is the client end of an in-process
|
|
67
|
+
* "WebSocket": `send` delivers a client message straight to the sync handler, and the
|
|
68
|
+
* handler's server-side socket delivers messages straight back to the connection's
|
|
69
|
+
* listeners. No sockets, no serialization-over-the-wire — but the EXACT same `SyncWebSocket`
|
|
70
|
+
* the handler talks to, so a real WebSocket transport drops in unchanged at Tier 2.
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
type ServerMessageListener = (msg: ServerMessage) => void;
|
|
74
|
+
interface LoopbackConnection {
|
|
75
|
+
readonly sessionId: string;
|
|
76
|
+
/** Client → server. Resolves once the handler has finished processing (loopback is synchronous). */
|
|
77
|
+
send(message: ClientMessage | string): Promise<void>;
|
|
78
|
+
/** Register a server → client listener; returns an unsubscribe. */
|
|
79
|
+
onMessage(listener: ServerMessageListener): () => void;
|
|
80
|
+
close(): void;
|
|
81
|
+
}
|
|
82
|
+
interface LoopbackHandler {
|
|
83
|
+
connect(sessionId: string, socket: SyncWebSocket): void;
|
|
84
|
+
disconnect(sessionId: string): void;
|
|
85
|
+
handleMessage(sessionId: string, raw: string): Promise<void>;
|
|
86
|
+
}
|
|
87
|
+
declare function createLoopbackConnection(handler: LoopbackHandler, sessionId: string): LoopbackConnection;
|
|
88
|
+
|
|
89
|
+
/** The durable per-tab dedup key threaded from the wire (`Mutation.clientId`/`seq`). */
|
|
90
|
+
interface DedupKey {
|
|
91
|
+
clientId: string;
|
|
92
|
+
seq: number;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The `applied`-receipt commit guard (registered ONCE at runtime construction, BEFORE fleet's epoch
|
|
96
|
+
* fence — verdict §(c) Risk R6). For every unit whose `meta` carries a dedup key, INSERTs the
|
|
97
|
+
* `client_mutations` receipt at that unit's own `ts`, inside the mutation's commit transaction. The
|
|
98
|
+
* INSERT is PLAIN (never `ON CONFLICT DO NOTHING`): a PK collision IS the dedup signal — it means a
|
|
99
|
+
* concurrent duplicate already committed this seq, so we throw a typed {@link CommitGuardRejection}
|
|
100
|
+
* (`CLIENT_MUTATION_DUP`) carrying this unit's index. On the single-commit path it propagates as this
|
|
101
|
+
* mutation's own rejection (the caller replay-reads the winner); under group commit the transactor's
|
|
102
|
+
* split-retry rejects ONLY this unit and re-flushes the innocent remainder. A unit with no dedup key
|
|
103
|
+
* (every ordinary mutation) is skipped — the whole guard costs a deployment nothing until the outbox
|
|
104
|
+
* is used.
|
|
105
|
+
*
|
|
106
|
+
* Store-agnostic by querier shape (verdict Risk R9 — the guard is ONE closure, not two): a Postgres
|
|
107
|
+
* `PgQuerier` exposes async `query()`; a `SqliteGuardQuerier` exposes synchronous `run()`. The SQLite
|
|
108
|
+
* branch runs fully synchronously and returns `undefined` (void) — it MUST, because SQLite's commit
|
|
109
|
+
* is one synchronous transaction that cannot await a guard (a returned thenable there is a dev-time
|
|
110
|
+
* error). The Postgres branch returns a Promise the async `commitWriteBatch` awaits.
|
|
111
|
+
*/
|
|
112
|
+
declare function clientReceiptsGuard(): (q: unknown, units: readonly CommitGuardUnit[], shardId: ShardId) => void | Promise<void>;
|
|
113
|
+
|
|
114
|
+
interface EmbeddedRuntimeOptions {
|
|
115
|
+
store: DocStore;
|
|
116
|
+
catalog: IndexCatalog;
|
|
117
|
+
modules: Record<string, RegisteredFunction>;
|
|
118
|
+
/** Privileged built-in functions (`_system:*`). Kept off the public run/sync surface. */
|
|
119
|
+
systemModules?: Record<string, RegisteredFunction>;
|
|
120
|
+
/** Privileged admin functions (`_admin:*`). Served over the admin sync channel. */
|
|
121
|
+
adminModules?: Record<string, RegisteredFunction>;
|
|
122
|
+
/** Validate an admin key presented via `SetAdminAuth`. Defaults to `() => false`. */
|
|
123
|
+
verifyAdmin?: (key: string) => boolean;
|
|
124
|
+
/** Set of component names; used to resolve the namespace for each function path. */
|
|
125
|
+
componentNames?: ReadonlySet<string>;
|
|
126
|
+
/** Swap the Tier 0 in-memory fan-out for a cross-process adapter (no app-code change). */
|
|
127
|
+
fanoutAdapter?: EmbeddedWriteFanoutAdapter;
|
|
128
|
+
originId?: string;
|
|
129
|
+
logSink?: LogSink;
|
|
130
|
+
/** Context providers contributed by composed components; attached as ctx[name] on every function call. */
|
|
131
|
+
contextProviders?: ReadonlyArray<ContextProvider>;
|
|
132
|
+
/** Row-policy registry contributed by components; enforced on every non-privileged run. */
|
|
133
|
+
policyRegistry?: ReadonlyMap<string, TablePolicy>;
|
|
134
|
+
/** Policy context providers contributed by components; used to build rule context for row policies. */
|
|
135
|
+
policyProviders?: ReadonlyArray<PolicyContextProvider>;
|
|
136
|
+
/** Declared relations, consulted by the kernel when resolving relation predicates. */
|
|
137
|
+
relationRegistry?: RelationRegistry;
|
|
138
|
+
/** Wall-clock source; defaults to `Date.now`. Injected for deterministic testing. */
|
|
139
|
+
now?: () => number;
|
|
140
|
+
/**
|
|
141
|
+
* The host's single alarm, for a host that STOPS THE PROCESS between requests (Cloudflare
|
|
142
|
+
* Containers: stopped ~5s after the last incoming request; internal timer activity does not keep
|
|
143
|
+
* it alive, so `setTimeout` silently never fires and every driver goes dead). Set → this runtime
|
|
144
|
+
* multiplexes every live driver timer down to ONE pending wake and hands the minimum to
|
|
145
|
+
* `armWake`; the host is then the SOLE thing that fires them, via `fireDueTimers()` (the engine
|
|
146
|
+
* arms no `setTimeout` of its own — two schedulers would double-fire the same callback).
|
|
147
|
+
*
|
|
148
|
+
* UNSET (the default, and every existing deployment — Docker/VPS/Fly/Railway/single-binary): the
|
|
149
|
+
* plain `setTimeout` path below, byte-for-byte unchanged.
|
|
150
|
+
*/
|
|
151
|
+
wakeHost?: WakeHost;
|
|
152
|
+
/**
|
|
153
|
+
* Answers `DriverContext.backstopMs` — a driver's declared cadence for a PURE BACKSTOP poll
|
|
154
|
+
* (scheduler lease-reclaim sweep, triggers beat, storage reaper). Defaults to IDENTITY (the
|
|
155
|
+
* driver's own 30s/60s constants, unchanged). A host where every wake costs a cold start stretches
|
|
156
|
+
* it: on Cloudflare a 30s backstop is a container boot every 30s forever — a ~15% duty cycle for a
|
|
157
|
+
* completely idle app, which would destroy scale-to-zero.
|
|
158
|
+
*/
|
|
159
|
+
backstopMs?: (defaultMs: number) => number;
|
|
160
|
+
/**
|
|
161
|
+
* Disarm the sync handler's process-shaped background timers (its `setInterval` flush/resume sweep
|
|
162
|
+
* and every per-session ping heartbeat). Defaults to `false` — every long-lived process host is
|
|
163
|
+
* byte-for-byte unchanged. Set ONLY by the Cloudflare Durable Object host (Slice 3), where those
|
|
164
|
+
* timers are actively harmful (a `setInterval` is lost on hibernation; an app-level ping would wake
|
|
165
|
+
* the hibernated object every beat, defeating scale-to-zero). Just a boolean — no host type crosses
|
|
166
|
+
* this seam. See `SyncProtocolHandlerOptions.disableBackgroundTimers`.
|
|
167
|
+
*/
|
|
168
|
+
disableSyncBackgroundTimers?: boolean;
|
|
169
|
+
/** Component boot steps to run once at create, namespaced + non-user (before serving traffic). */
|
|
170
|
+
bootSteps?: {
|
|
171
|
+
name: string;
|
|
172
|
+
run: (ctx: {
|
|
173
|
+
db: GuestDatabaseWriter;
|
|
174
|
+
now: number;
|
|
175
|
+
}) => Promise<void>;
|
|
176
|
+
}[];
|
|
177
|
+
/** Component drivers to start once at create, after boot steps + the commit fan-out are wired. */
|
|
178
|
+
drivers?: Driver[];
|
|
179
|
+
/**
|
|
180
|
+
* `fullTableName → tableNumber` (the same map `composeComponents`/`loadProject` produce).
|
|
181
|
+
* Used ONLY to translate the encoded storage-table ids on `adapter.subscribe`'s commit payload
|
|
182
|
+
* (e.g. `"3"`) back into full names (e.g. `"scheduler/jobs"`) before handing them to driver
|
|
183
|
+
* `onCommit` callbacks — drivers filter `inv.tables` by name (see `components/scheduler/src/
|
|
184
|
+
* driver.ts`), and a raw storage id never matches. Optional: a runtime with no components (or
|
|
185
|
+
* whose drivers don't inspect `inv.tables`) can omit it and driver callbacks just see raw ids.
|
|
186
|
+
*/
|
|
187
|
+
tableNumbers?: Record<string, number>;
|
|
188
|
+
/**
|
|
189
|
+
* Route mutations/actions to another node instead of executing them locally, when
|
|
190
|
+
* `writeRouter.isLocalWriter()` is false. Applies to EVERY mutation/action entry point (the
|
|
191
|
+
* WS `syncExecutor.runMutation`/`runAction` and the public `run`/`runAction` methods) —
|
|
192
|
+
* queries are never routed. See `WriteRouter`.
|
|
193
|
+
*/
|
|
194
|
+
writeRouter?: WriteRouter;
|
|
195
|
+
/**
|
|
196
|
+
* Skip starting component drivers at `create()` time; call the returned instance's
|
|
197
|
+
* `startDrivers()` later instead. For a fleet node that boots as a non-writer and only
|
|
198
|
+
* wants drivers running once/if it becomes the writer.
|
|
199
|
+
*/
|
|
200
|
+
deferDrivers?: boolean;
|
|
201
|
+
/**
|
|
202
|
+
* Receipted Outbox — receipts-guard ownership handoff (the promotion-barrier hole). Normally
|
|
203
|
+
* `create()` registers the `clientReceiptsGuard()` exactly-once barrier on `options.store` here at
|
|
204
|
+
* construction, because for every non-fleet deployment `options.store` IS the concrete store its
|
|
205
|
+
* commits land on. A fleet SYNC node is the exception: `options.store` is a `SwitchableDocStore`
|
|
206
|
+
* over the read-only replica, and on writer promotion the runtime store is swapped to the concrete
|
|
207
|
+
* Postgres store WITHOUT re-forwarding registered commit guards (by `SwitchableDocStore`'s
|
|
208
|
+
* documented design) — so a guard registered here would silently vanish on promotion, and a
|
|
209
|
+
* promoted single-writer node's client mutations would commit with NO receipt (dedup miss → a
|
|
210
|
+
* resent (clientId, seq) re-executes). When this is set, `create()` SKIPS its own registration and
|
|
211
|
+
* the caller (fleet's `armWriter`) owns installing the receipts guard on the CONCRETE write store —
|
|
212
|
+
* BEFORE the epoch fence, with the same release-on-re-arm discipline the fence uses. Ownership rule:
|
|
213
|
+
* whoever owns the concrete store owns the receipts guard. Unset → the runtime owns it (today's
|
|
214
|
+
* behavior, byte-identical for every non-fleet deployment). See `ee/fleet`'s `armWriter`.
|
|
215
|
+
*/
|
|
216
|
+
externalReceiptsGuard?: boolean;
|
|
217
|
+
/**
|
|
218
|
+
* Number of shards to run (Shards B2a). When `> 1`, the runtime builds ONE `ShardedTransactor`
|
|
219
|
+
* (N independent per-shard mutexes + OCC rings + oracles) over the store instead of the
|
|
220
|
+
* single-shard `SingleWriterTransactor`, so mutations routed to different shards commit in
|
|
221
|
+
* parallel. Unset / `1` → the single-shard transactor, byte-identical to before (the existing
|
|
222
|
+
* suites are the proof). The executor resolves each mutation's shard (`shardBy`) and passes it
|
|
223
|
+
* through `runInTransaction({ shardId })`; a fleet writer pairs this with the per-shard commit
|
|
224
|
+
* pool so different shards' commits are genuinely concurrent Postgres transactions.
|
|
225
|
+
*/
|
|
226
|
+
numShards?: number;
|
|
227
|
+
/**
|
|
228
|
+
* Fleet B4 (group commit): route commits through the two-buffer stage-then-flush committer loop
|
|
229
|
+
* instead of the byte-identical single-commit path. Threaded straight into the transactor
|
|
230
|
+
* (`ShardedTransactor`/`SingleWriterTransactor`) constructed below — every shard batches when this
|
|
231
|
+
* is set, none do when it's unset/false. The CLI resolves this from `HELIPOD_GROUP_COMMIT`
|
|
232
|
+
* (default OFF at Fleet B4/T4 — T5 owns flipping the production default); leaving it unset keeps a
|
|
233
|
+
* non-fleet / flag-off deployment structurally on today's path, byte-identical to before this
|
|
234
|
+
* option existed. See `ShardedTransactorOptions.groupCommit`'s doc comment for the mechanism.
|
|
235
|
+
*/
|
|
236
|
+
groupCommit?: boolean;
|
|
237
|
+
/**
|
|
238
|
+
* Hybrid-node split-read seam (Fleet B3, D1). When set, `create()` builds a SECOND, separate
|
|
239
|
+
* query-path transactor (`SingleWriterTransactor` — queries never commit, so no sharding is
|
|
240
|
+
* needed here regardless of the write side) + `QueryRuntime` over this store, seeded from ITS
|
|
241
|
+
* OWN `maxTimestamp()` (not the write store's) and wired onto `ExecutorDeps.queryPath`, so
|
|
242
|
+
* `fn.type === "query"` runs against `queryStore` instead of `store`. Its oracle is advanced
|
|
243
|
+
* ONLY by `observeTimestamp` (see below) — never by this runtime's own local commits, which
|
|
244
|
+
* only ever land on the WRITE store. Unset → `queryPath` is never built; every query runs
|
|
245
|
+
* against `store` through the primary pair, byte-identical to before this option existed.
|
|
246
|
+
*/
|
|
247
|
+
queryStore?: DocStore;
|
|
248
|
+
/**
|
|
249
|
+
* M2b: the D1 store for `.global()` tables, pre-built by the boot layer (which holds
|
|
250
|
+
* `schemaJson`). Mirrors the `queryStore` precedent above — handed in already-constructed,
|
|
251
|
+
* not built inside `create()`. Threaded straight into `ExecutorDeps.globalStore`. Absent →
|
|
252
|
+
* `.global()` ops fail fast in the kernel (`requireGlobalTxn`), byte-identical to before this
|
|
253
|
+
* option existed. NOT `DocStore`-shaped; it never enters a transactor/`QueryRuntime`.
|
|
254
|
+
*/
|
|
255
|
+
globalStore?: _helipod_docstore_d1.D1DocStore;
|
|
256
|
+
/**
|
|
257
|
+
* The AUTHORITATIVE receipts store for the `Connect` resume handshake — where the outbox verdict
|
|
258
|
+
* classification (`classifyClientMutation`) and ack-prune (`pruneClientMutations`) reads/writes
|
|
259
|
+
* land (verdict §(c): "the classification read runs where the commit runs... never against a
|
|
260
|
+
* follower's embedded replica"). The `client_mutations`/`client_floors` receipts tables are NOT
|
|
261
|
+
* part of the replicated MVCC document log — they live ONLY on the authoritative primary. On a
|
|
262
|
+
* fleet SYNC node, `options.store` is a `SwitchableDocStore` over the read-only replica, which
|
|
263
|
+
* carries NONE of those receipts: classifying a committed seq there returns `unknown` for every
|
|
264
|
+
* lookup, so the handshake spuriously reports `known:false` (a reset that re-mints the clientId)
|
|
265
|
+
* and — worse — a kill-after-commit reload via a sync node reports a FALSE failure for a mutation
|
|
266
|
+
* that actually committed. Set this to the primary Postgres store so the handshake reads/prunes the
|
|
267
|
+
* real receipts. Safe from a read-only-until-promoted primary: `getClientVerdict` is a pure read
|
|
268
|
+
* and `pruneClientMutations` is a monotonic (`GREATEST`-floored) direct query on standalone,
|
|
269
|
+
* unsharded receipt tables — architecturally identical to what the WRITER's own handshake does
|
|
270
|
+
* outside the OCC transactor, needing no advisory lock or write-forward hop. Unset → the handshake
|
|
271
|
+
* uses `options.store` (byte-identical for every non-fleet/writer deployment, whose `store` IS the
|
|
272
|
+
* primary; the mutation-path dedup already places its own classification on the owner via
|
|
273
|
+
* `writeRouter`, so only the Connect handshake needed this seam). See `ee/fleet`'s sync boot.
|
|
274
|
+
*/
|
|
275
|
+
receiptsStore?: DocStore;
|
|
276
|
+
/**
|
|
277
|
+
* Hybrid-node RYOW gate (Fleet B3, D2). Awaited in the runtime's serial fan-out `drain()`
|
|
278
|
+
* BEFORE each queued invalidation's `handler.notifyWrites` — e.g. a hybrid node's
|
|
279
|
+
* `tailer.waitFor(commitTs)`, so a locally-committed mutation's subscription re-runs don't fire
|
|
280
|
+
* against a replica that hasn't applied that commit yet. A rejection is caught and logged; the
|
|
281
|
+
* drain loop continues to the next queued invalidation rather than wedging the whole queue on
|
|
282
|
+
* one bad wait. Unset → `drain()` is byte-identical to before this hook existed.
|
|
283
|
+
*/
|
|
284
|
+
beforeNotify?: (commitTs: bigint) => Promise<void>;
|
|
285
|
+
/**
|
|
286
|
+
* The stable-prefix accessor for `DriverContext.readLog` (triggers D1). Returns the highest log
|
|
287
|
+
* timestamp below which the log is GAP-FREE — the upper bound `readLog` scans to. Non-null ONLY in
|
|
288
|
+
* a fleet, where N per-shard commit connections land timestamps out of order (a scan can see ts 10
|
|
289
|
+
* while ts 9 is still in flight), so the bound must be `min(shard_leases.frontier_ts)` — the fleet
|
|
290
|
+
* node wires it to exactly that. A `null` return (or an unset accessor) means "no fleet gap": every
|
|
291
|
+
* non-fleet topology commits over one serialized session (SQLite's single connection; the single
|
|
292
|
+
* PINNED Postgres connection — the commit pool is fleet-only), so `readLog` falls back to
|
|
293
|
+
* `store.maxTimestamp()`. Consulted per `readLog` call; only ever invoked on the default-shard
|
|
294
|
+
* holder by the driver lifecycle.
|
|
295
|
+
*/
|
|
296
|
+
stablePrefix?: () => Promise<bigint | null>;
|
|
297
|
+
}
|
|
298
|
+
declare class EmbeddedRuntime {
|
|
299
|
+
readonly store: DocStore;
|
|
300
|
+
readonly executor: InlineUdfExecutor;
|
|
301
|
+
readonly handler: SyncProtocolHandler;
|
|
302
|
+
readonly writeFanoutAdapter: EmbeddedWriteFanoutAdapter;
|
|
303
|
+
private readonly modules;
|
|
304
|
+
private readonly systemModules;
|
|
305
|
+
private readonly adminModules;
|
|
306
|
+
private readonly componentNames;
|
|
307
|
+
private readonly contextProviders;
|
|
308
|
+
private readonly policyRegistry;
|
|
309
|
+
private readonly policyProviders;
|
|
310
|
+
private readonly relationRegistry;
|
|
311
|
+
private readonly drivers;
|
|
312
|
+
private readonly timers;
|
|
313
|
+
/** Threaded from `create()` (like `driverCtx`) so the public `fireDueTimers()` below runs against
|
|
314
|
+
* the SAME timer map + wake-arming closures the `DriverContext` writes to. */
|
|
315
|
+
private readonly fireDue;
|
|
316
|
+
/**
|
|
317
|
+
* Inverse of `tableNumbers` (tableNumber → fullTableName). Mutable (not `readonly`) so
|
|
318
|
+
* `setTableNumbers` can rebuild it in place after an additive deploy — the very same `Map`
|
|
319
|
+
* object `create()`'s `namesForCommit` closure captured, so mutating it here (rather than
|
|
320
|
+
* reassigning the field) keeps that closure correct without any circular-reference dance.
|
|
321
|
+
*/
|
|
322
|
+
private tableNumberToName;
|
|
323
|
+
/** The WRITE transactor's timestamp observer (the single-shard oracle, or the
|
|
324
|
+
* `ShardedTransactor` itself). `observeTimestamp` delegates to this UNLESS `queryOracle` is
|
|
325
|
+
* set (see it below) — this oracle's own local commits always advance it directly via
|
|
326
|
+
* `ShardWriter.commit`'s `oracle.publishCommitted`; `observeTimestamp` is the SEPARATE
|
|
327
|
+
* fleet-follower/tailer-observation channel, which a hybrid node must NOT point back at the
|
|
328
|
+
* write oracle (D1 — own local commits must never advance the query oracle, and observed
|
|
329
|
+
* foreign timestamps must never advance the write oracle either, once a hybrid has a real
|
|
330
|
+
* query-path oracle of its own). */
|
|
331
|
+
private readonly oracle;
|
|
332
|
+
/** Fleet B3 (D1): the QUERY-path oracle, set only when `EmbeddedRuntimeOptions.queryStore` was
|
|
333
|
+
* configured. When set, `observeTimestamp` routes to THIS oracle instead of `oracle` — its
|
|
334
|
+
* purpose is tailer post-apply feeding (a hybrid node's replica-backed query snapshot rises
|
|
335
|
+
* only as the tailer confirms applied writes), never this runtime's own local commits (those
|
|
336
|
+
* land on the write store and advance `oracle` directly, as shipped). Undefined when
|
|
337
|
+
* `queryStore` is unset → `observeTimestamp` keeps the exact shipped write-oracle routing. */
|
|
338
|
+
private readonly queryOracle;
|
|
339
|
+
/** The write path — retained so a fleet writer can take a shard's commit mutex non-blockingly
|
|
340
|
+
* (`tryRunExclusiveOnShard`) to close idle frontiers without racing that shard's own commits. */
|
|
341
|
+
private readonly transactor;
|
|
342
|
+
/** Threaded from `create()` so `startDrivers()` can start deferred drivers later. */
|
|
343
|
+
private readonly driverCtx;
|
|
344
|
+
/** Mutable: false when `deferDrivers` was set and `startDrivers()` hasn't run yet. */
|
|
345
|
+
private driversStarted;
|
|
346
|
+
/** When set and `isLocalWriter()` is false, every mutation/action entry point forwards
|
|
347
|
+
* through it instead of executing locally. Queries never consult this. */
|
|
348
|
+
private readonly writeRouter;
|
|
349
|
+
/** Shards B2a (T5): the SAME resolved count `create()` used to build the transactor and every
|
|
350
|
+
* closure's `RunOptions.numShards` — instance methods (`run`/`runAction`/`runHttpAction`/
|
|
351
|
+
* `runSystem`/`runAdmin`) thread it through here so a call made AFTER construction routes
|
|
352
|
+
* identically to one made during `create()`. Defaults to 1 (byte-identical to before) when
|
|
353
|
+
* `EmbeddedRuntimeOptions.numShards` is unset. */
|
|
354
|
+
private readonly numShards;
|
|
355
|
+
/** The SAME catalog the executor/kernel enforce ownership against — so `runSystem`'s privileged
|
|
356
|
+
* doc-mutation routing (`resolveDocMutationShard`) reads a table's `shardKey` from the exact
|
|
357
|
+
* source of truth the guards use, and can never disagree with them. */
|
|
358
|
+
private readonly catalog;
|
|
359
|
+
/** The SAME `commitSubs` set `create()`'s `adapter.subscribe` callback fires on every LOCAL
|
|
360
|
+
* commit — also fired by `notifyExternalCommit` (below) for a FOREIGN (fleet) commit, so a
|
|
361
|
+
* driver's `onCommit` wakes on both without the two paths ever drifting on which set they
|
|
362
|
+
* target. */
|
|
363
|
+
private readonly commitSubs;
|
|
364
|
+
private sessionCounter;
|
|
365
|
+
private constructor();
|
|
366
|
+
static create(options: EmbeddedRuntimeOptions): Promise<EmbeddedRuntime>;
|
|
367
|
+
/**
|
|
368
|
+
* Resolves a target path's REAL registered kind against the live `this.modules` map — same
|
|
369
|
+
* resolver shape as `create()`'s local `functionKind` closure, but bound to the instance so it
|
|
370
|
+
* stays correct across `setModules` hot-swaps. See `ComponentContext.functionKind`'s doc
|
|
371
|
+
* comment (packages/executor/src/executor.ts).
|
|
372
|
+
*/
|
|
373
|
+
private functionKind;
|
|
374
|
+
/** Hot-swap the function map (dev reload) without disturbing the store/transactor. */
|
|
375
|
+
setModules(modules: Record<string, RegisteredFunction>): void;
|
|
376
|
+
/**
|
|
377
|
+
* Rebuild the tableNumber→name map after an additive deploy so driver commit fan-out
|
|
378
|
+
* (`namesForCommit` in `create()`, which closed over this same `Map` instance) keeps
|
|
379
|
+
* translating newly-added tables' encoded storage ids to their full names correctly.
|
|
380
|
+
* Additive deploys keep existing numbers, so this only ever adds entries in practice.
|
|
381
|
+
*/
|
|
382
|
+
setTableNumbers(tableNumbers: Record<string, number>): void;
|
|
383
|
+
/**
|
|
384
|
+
* The FOREIGN-COMMIT driver wake for a multi-writer fleet hybrid (Fleet B3, trigger-wake gap
|
|
385
|
+
* fix). `commitSubs` (driver `onCommit` wakes) normally fires only from `create()`'s
|
|
386
|
+
* `adapter.subscribe` callback — the LOCAL commit fan-out. In an opt-in multi-writer fleet, a
|
|
387
|
+
* co-writer's commit reaches THIS node only through the hybrid-tailer `invalidationSink`
|
|
388
|
+
* (`ee/packages/fleet/src/node.ts`), which calls `handler.notifyWrites` directly — bypassing
|
|
389
|
+
* `adapter.subscribe` entirely, so a driver here (e.g. `@helipod/triggers`) never woke on a
|
|
390
|
+
* foreign writer's commit and instead slept up to its own wall-clock beat. Delivery was always
|
|
391
|
+
* guaranteed (the durable cursor over the log) — this is a LATENCY fix, not a correctness one.
|
|
392
|
+
*
|
|
393
|
+
* The caller (the fleet's `invalidationSink`) invokes this after `notifyWrites`, passing the
|
|
394
|
+
* SAME derived invalidation. Translates `inv.tables` with the identical `translateTableIds`
|
|
395
|
+
* helper the local path uses, so a driver's `t.startsWith("scheduler/")`-style filter matches
|
|
396
|
+
* either source the same way. A local commit and a foreign commit that happen to touch the same
|
|
397
|
+
* table may both wake a driver for what is conceptually "the same" change window — harmless,
|
|
398
|
+
* since driver wakes are level-triggered (a driver re-checks its own state, it doesn't trust the
|
|
399
|
+
* wake payload as the sole source of truth).
|
|
400
|
+
*
|
|
401
|
+
* A no-op on any node with no registered drivers (`commitSubs` empty) — cheap to call
|
|
402
|
+
* unconditionally from every fleet node, writer-ish or not.
|
|
403
|
+
*/
|
|
404
|
+
notifyExternalCommit(inv: {
|
|
405
|
+
tables: string[];
|
|
406
|
+
ranges: readonly SerializedKeyRange[];
|
|
407
|
+
commitTs: number;
|
|
408
|
+
}): void;
|
|
409
|
+
/**
|
|
410
|
+
* Live view of the registered app+component function paths. Reads the same mutable `modules`
|
|
411
|
+
* map `setModules` hot-swaps in place, so counts stay correct across a dev reload or deploy
|
|
412
|
+
* (the boot-time snapshot the server used to cache went stale after the first hot-swap).
|
|
413
|
+
*/
|
|
414
|
+
functionPaths(): string[];
|
|
415
|
+
/** Live view of the registered table names (mirrors `functionPaths`, reads the live map). */
|
|
416
|
+
tableNames(): string[];
|
|
417
|
+
/** Open an in-process connection an unmodified client can talk to. */
|
|
418
|
+
connect(sessionId?: string): LoopbackConnection;
|
|
419
|
+
/**
|
|
420
|
+
* Synthesizes a `UdfResult` for a write forwarded to the writer node: `forward` only returns
|
|
421
|
+
* the function's JSON result, not read ranges / an oplog, so those come back empty/zero here.
|
|
422
|
+
* Shared by `run()` and `runAction()` — both route through the same `WriteRouter`.
|
|
423
|
+
*/
|
|
424
|
+
private forwardedResult;
|
|
425
|
+
/** Directly invoke a function (for HTTP routes / the CLI `run` command). A MUTATION routes
|
|
426
|
+
* per-shard inside the executor (`executor.run` forwards a shard this node doesn't own); an
|
|
427
|
+
* ACTION forwards wholesale here to the default-shard holder; queries always run locally. */
|
|
428
|
+
run<T = unknown>(path: string, args: JSONValue, opts?: {
|
|
429
|
+
identity?: string | null;
|
|
430
|
+
commitMeta?: Record<string, string>;
|
|
431
|
+
dedup?: DedupKey;
|
|
432
|
+
}): Promise<UdfResult<T>>;
|
|
433
|
+
/**
|
|
434
|
+
* The OWNER-side dedup classification for a mutation reaching `run()` with a dedup key (the fleet
|
|
435
|
+
* `/_fleet/run` forward path). Shares the exact classify → run-with-guard → replay-on-collision
|
|
436
|
+
* logic the sync `runMutation` uses; a replay is surfaced as a `UdfResult` with `clientReplay` set
|
|
437
|
+
* (the `/_fleet/run` handler serializes it to a replay body). `base` is any pre-existing commit
|
|
438
|
+
* meta (e.g. fleet's `idempotencyKey`) — the dedup keys merge onto it (disjoint keys).
|
|
439
|
+
*/
|
|
440
|
+
private runMutationClassified;
|
|
441
|
+
/** Wrap a {@link ClientReplay} as a `UdfResult` carrying `clientReplay` — no commit happened. */
|
|
442
|
+
private replayResult;
|
|
443
|
+
/** Directly invoke an action (for HTTP routes / the CLI `run` command). Public gate: blocks
|
|
444
|
+
* `_`-prefixed paths. Routes through `writeRouter` when set and this node isn't the writer. */
|
|
445
|
+
runAction<T = unknown>(path: string, args: JSONValue, opts?: {
|
|
446
|
+
identity?: string | null;
|
|
447
|
+
}): Promise<UdfResult<T>>;
|
|
448
|
+
/** Directly invoke an httpAction (for the public HTTP router). Passes the raw `Request` through
|
|
449
|
+
* untouched and returns the handler's `Response`. Public gate: blocks `_`-prefixed paths. */
|
|
450
|
+
runHttpAction(path: string, request: Request, opts?: {
|
|
451
|
+
identity?: string | null;
|
|
452
|
+
}): Promise<Response>;
|
|
453
|
+
/**
|
|
454
|
+
* The privileged built-in doc mutations whose target is a USER table (and thus may be sharded).
|
|
455
|
+
* These are the ONLY `runSystem` paths that need shard routing — every other `_system:*`/
|
|
456
|
+
* `_storage:*`/`_test:*` built-in writes UNSHARDED component-internal tables, which are owned by
|
|
457
|
+
* the default ring (INSERT from any ring; RMW on default), so they need no override.
|
|
458
|
+
*/
|
|
459
|
+
private static readonly DOC_MUTATION_PATHS;
|
|
460
|
+
/** Run a privileged built-in (`_system:*`) function. Trusted callers only (the admin API).
|
|
461
|
+
* For a doc mutation on a user table, the target document's OWNING shard is resolved and passed
|
|
462
|
+
* through so the privileged write lands on the same ring a user's sharded mutation of that doc
|
|
463
|
+
* would — one-doc-one-ring. `opts.shardId` lets a trusted caller override the resolution. */
|
|
464
|
+
runSystem<T = unknown>(path: string, args: JSONValue, opts?: {
|
|
465
|
+
shardId?: ShardId;
|
|
466
|
+
commitMeta?: Record<string, string>;
|
|
467
|
+
}): Promise<UdfResult<T>>;
|
|
468
|
+
/**
|
|
469
|
+
* Resolve the owning shard for a privileged admin doc mutation so the write commits on the SAME
|
|
470
|
+
* ring a user's sharded mutation of that document would use (the one-doc-one-ring invariant).
|
|
471
|
+
* Without this, a dashboard edit of a sharded doc runs on the default ring and forks the doc's
|
|
472
|
+
* prev_ts chain against its home-shard writer — a permanent tailer halt + a silently-lost update.
|
|
473
|
+
*
|
|
474
|
+
* The shard-key field is IMMUTABLE after insert, so peeking the current doc's key value BEFORE the
|
|
475
|
+
* transaction is race-free (its shard can't have changed by the time the txn opens). An unsharded
|
|
476
|
+
* target (component tables, or app tables with no `.shardKey`) resolves to `"default"` — its RMW
|
|
477
|
+
* ring per the same invariant. Called only for `DOC_MUTATION_PATHS` when `numShards > 1`.
|
|
478
|
+
*/
|
|
479
|
+
private resolveDocMutationShard;
|
|
480
|
+
/** Run a privileged admin built-in (`_admin:*`) once (e.g. for the HTTP fallback). Trusted callers only. */
|
|
481
|
+
runAdmin<T = unknown>(path: string, args: JSONValue): Promise<UdfResult<T>>;
|
|
482
|
+
/**
|
|
483
|
+
* Run every driver timer that is due (`atMs <= now()`), drop it, and re-arm the host to the new
|
|
484
|
+
* minimum — the firing half of the `EmbeddedRuntimeOptions.wakeHost` seam, called when the host's
|
|
485
|
+
* alarm goes off (`serve`'s `POST /_admin/wake`). It cannot be a direct call: the alarm lives in
|
|
486
|
+
* the host (a Durable Object), across a network boundary from this process.
|
|
487
|
+
*
|
|
488
|
+
* Safe and cheap on both paths through it, so a host never branches:
|
|
489
|
+
* - the process was STOPPED (the common case) — the wake request BOOTS it, the drivers' own
|
|
490
|
+
* `start()` re-derives everything from committed state, and this finds nothing due: a no-op.
|
|
491
|
+
* - the process was RUNNING — this runs the actual pending callback.
|
|
492
|
+
*
|
|
493
|
+
* Also a no-op without a `wakeHost` (nothing arms a wake, and `setTimeout` already fired
|
|
494
|
+
* everything due), so the route being registered on every deployment costs nothing.
|
|
495
|
+
*/
|
|
496
|
+
fireDueTimers(): void;
|
|
497
|
+
/**
|
|
498
|
+
* Stop the component drivers and clear their pending timers, resetting `driversStarted` so a later
|
|
499
|
+
* `startDrivers()` can bring them back up. Shared by the driver-only `stopDriversOnly()` (B2b, D5)
|
|
500
|
+
* and the full-shutdown `stopDrivers()` — the ONLY difference is whether the sync handler is also
|
|
501
|
+
* disposed, so the two can never drift on how a driver is torn down.
|
|
502
|
+
*/
|
|
503
|
+
private stopDriversInternal;
|
|
504
|
+
/**
|
|
505
|
+
* Stop all component drivers and clear all pending driver timers. Call on runtime shutdown.
|
|
506
|
+
* ALSO disposes the sync handler's background flush sweep — this is the full-teardown path.
|
|
507
|
+
*/
|
|
508
|
+
stopDrivers(): Promise<void>;
|
|
509
|
+
/**
|
|
510
|
+
* Driver-only stop (B2b, D5 — "drivers follow the default shard"): stop the scheduler/workflow/cron/
|
|
511
|
+
* reaper drivers and clear their timers, WITHOUT disposing the sync handler. A fleet node that
|
|
512
|
+
* relinquishes (or gracefully releases) the default shard keeps serving reads, subscriptions, and
|
|
513
|
+
* mutations for every OTHER shard — only its drivers go quiet, because a different node now owns the
|
|
514
|
+
* default ring the scheduler tables live on. Symmetric with `startDrivers()` and idempotent both
|
|
515
|
+
* ways (the reset flag makes a later `startDrivers()` a real restart, and a second stop a no-op),
|
|
516
|
+
* so callers never need to track whether drivers are currently running. Deliberately NEVER touches
|
|
517
|
+
* `handler.dispose()` (the shipped `stopDrivers()` did — fatal on a default-relinquish, since the
|
|
518
|
+
* node stays live).
|
|
519
|
+
*/
|
|
520
|
+
stopDriversOnly(): Promise<void>;
|
|
521
|
+
/**
|
|
522
|
+
* Start component drivers deferred via `EmbeddedRuntimeOptions.deferDrivers`, OR restart them after
|
|
523
|
+
* a `stopDriversOnly()` (B2b, D5). Idempotent — a second (or later) call is a no-op once drivers are
|
|
524
|
+
* running, so callers don't need to track whether they've already called it (e.g. a fleet node
|
|
525
|
+
* calling this on every default-shard acquisition attempt).
|
|
526
|
+
*/
|
|
527
|
+
startDrivers(): Promise<void>;
|
|
528
|
+
/**
|
|
529
|
+
* Lets a non-writer fleet node advance its local timestamp oracle past timestamps it learns
|
|
530
|
+
* from the writer's change stream, so its own next allocated timestamp (if/when it becomes
|
|
531
|
+
* the writer) never collides with or precedes one it already observed.
|
|
532
|
+
*
|
|
533
|
+
* Fleet B3 (D1) routing: WITHOUT a `queryStore` (`queryOracle` undefined), this is the shipped
|
|
534
|
+
* behavior — delegates straight to the WRITE oracle (`this.oracle`; a `ShardedTransactor` fans
|
|
535
|
+
* it to every shard). WITH a `queryStore` configured, a hybrid node has a real query-path oracle
|
|
536
|
+
* whose sole purpose IS tailer post-apply feeding — so this routes to `queryOracle` INSTEAD, and
|
|
537
|
+
* the write oracle is left untouched by tailer observations (it advances only via this runtime's
|
|
538
|
+
* own local commits, through `ShardWriter.commit`'s `oracle.publishCommitted`). Sharing one
|
|
539
|
+
* oracle between the two would let a query snapshot ABOVE the replica's actual watermark and
|
|
540
|
+
* read holes (D1's spec-review requirement) — this branch is what keeps them separate.
|
|
541
|
+
*
|
|
542
|
+
* This is the READ-PATH observer (tailer/follower freshness → query snapshot). Its write-path
|
|
543
|
+
* counterpart, `observeWriteTimestamp` (below), always targets the WRITE oracle and exists
|
|
544
|
+
* precisely because this method stopped feeding the write side once a hybrid has a query oracle.
|
|
545
|
+
*/
|
|
546
|
+
observeTimestamp(ts: bigint): void;
|
|
547
|
+
/**
|
|
548
|
+
* Advance the WRITE transactor's timestamp oracle(s) past `ts` — the write-path counterpart to
|
|
549
|
+
* `observeTimestamp` above, and the pre-T1 semantics of what `observeTimestamp` used to do before
|
|
550
|
+
* hybrid routing split the two. ALWAYS targets `this.oracle` (the write side), independent of
|
|
551
|
+
* `queryStore`/`queryOracle` routing: a `ShardedTransactor` fans `ts` to every existing shard
|
|
552
|
+
* oracle AND raises its `observedHighWater` floor (so a shard writer CREATED later seeds at-or-past
|
|
553
|
+
* `ts`); the single-shard `MonotonicTimestampOracle` takes it directly.
|
|
554
|
+
*
|
|
555
|
+
* Purpose (distinct from `observeTimestamp`): re-floor the WRITE snapshot on a shard OWNERSHIP
|
|
556
|
+
* CHANGE. On a hybrid node `observeTimestamp` feeds the QUERY oracle, so nothing feeds the write
|
|
557
|
+
* oracle from foreign observations anymore. A shard this node held, RELEASED (its `ShardWriter`
|
|
558
|
+
* stays in the transactor's Map — only the fleet epoch is dropped), and later RE-ACQUIRES would
|
|
559
|
+
* keep that `ShardWriter`'s oracle frozen at this node's own last commit; the next mutation would
|
|
560
|
+
* snapshot BELOW an interim owner's commits and an RMW handler would compute on stale state (the
|
|
561
|
+
* durable chain stays intact via latest-`prev_ts`, but the update is semantically lost). The fleet
|
|
562
|
+
* calls this on EVERY shard acquisition with the lease row's `frontier_ts` — which is >= every
|
|
563
|
+
* prior commit on that shard by the fence invariant (the commit guard writes `frontier_ts =
|
|
564
|
+
* GREATEST(frontier_ts, commitTs)` inside each commit txn) — so it is the exact correct floor.
|
|
565
|
+
* Idempotent/monotone: a `ts` at or below the oracle's position is a no-op (harmless on a
|
|
566
|
+
* fresh/never-released shard, and on every non-hybrid node where it is called uniformly too).
|
|
567
|
+
*/
|
|
568
|
+
observeWriteTimestamp(ts: bigint): void;
|
|
569
|
+
/**
|
|
570
|
+
* Run `fn` under shard `shardId`'s commit mutex IFF that shard is idle right now — the seam a fleet
|
|
571
|
+
* writer's idle-frontier closer uses to publish a shard's frontier atomically with respect to that
|
|
572
|
+
* shard's own commits (see `ShardedTransactor.tryRunExclusiveOnShard`). A shard is idle only when
|
|
573
|
+
* the commit mutex is free AND no group-commit batch is staged/flushing (Fleet B4: the flush runs
|
|
574
|
+
* OFF the mutex, so mutex-freedom alone is not enough — a mid-flush batch has ts's drawn but rows
|
|
575
|
+
* not yet landed, and must read as busy to keep the closer from publishing a frontier above them).
|
|
576
|
+
* Returns `true` if `fn` ran, `false` if the shard is busy (skip; retry next beat). Total across
|
|
577
|
+
* sharded/single-shard runtimes — the single-shard transactor ignores `shardId` and uses its one
|
|
578
|
+
* writer.
|
|
579
|
+
*/
|
|
580
|
+
tryRunExclusiveOnShard(shardId: ShardId, fn: () => Promise<void>): Promise<boolean>;
|
|
581
|
+
/**
|
|
582
|
+
* Group-commit counters (Fleet B4, T4 health) — total across the transactor, whichever shape it
|
|
583
|
+
* is: `ShardedTransactor.groupCommitStats()` aggregates over every live shard,
|
|
584
|
+
* `SingleWriterTransactor.groupCommitStats()` mirrors it for the one writer. Both are
|
|
585
|
+
* structurally all-zero when `EmbeddedRuntimeOptions.groupCommit` is unset/false (the underlying
|
|
586
|
+
* `ShardWriter` never touches these fields on the single-commit path) — callers need no separate
|
|
587
|
+
* on/off branch. The fleet health seam (`@helipod/fleet`'s `node.ts`) reads this to derive
|
|
588
|
+
* `flushesPerSec` between successive `/api/health` reads.
|
|
589
|
+
*/
|
|
590
|
+
groupCommitStats(): {
|
|
591
|
+
lastBatchSize: number;
|
|
592
|
+
maxBatchSize: number;
|
|
593
|
+
flushCount: number;
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
declare function createEmbeddedRuntime(options: EmbeddedRuntimeOptions): Promise<EmbeddedRuntime>;
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* `RuntimeHost` — the neutral seam between the engine and whatever binds it to a transport
|
|
600
|
+
* (HTTP + WebSocket). It exists so the SAME `EmbeddedRuntime` can run on a long-lived process
|
|
601
|
+
* (`Bun.serve` / `node:http` + `ws`) AND on a Cloudflare Durable Object (Worker `fetch` +
|
|
602
|
+
* `WebSocketPair`/hibernation), with each backend implementing this one method.
|
|
603
|
+
*
|
|
604
|
+
* NEUTRALITY RULE (a Slice-1 gate — asserted mechanically in
|
|
605
|
+
* `packages/runtime-embedded/test/host-neutral.test.ts`): this file imports ONLY `@helipod/*`
|
|
606
|
+
* symbols and TS type-only imports. It contains NO host I/O primitive — no `bun`, `node:*`, `ws`,
|
|
607
|
+
* no cloudflare type, no `DurableObjectNamespace`. Where a concept cannot be expressed neutrally it
|
|
608
|
+
* is documented, not papered over (see `ServerHandle.close` and `ServeOptions` below).
|
|
609
|
+
*
|
|
610
|
+
* WHY GENERIC: `ServeOptions`/`ServerHandle` are lifted verbatim (no field changes) from the
|
|
611
|
+
* process host's `DevServerOptions`/`DevServer`. Several of those fields reference types that live
|
|
612
|
+
* in `@helipod/cli`, `@helipod/admin`, and `@helipod/storage` — all of which depend ON
|
|
613
|
+
* `@helipod/runtime-embedded`, so importing them here would be a dependency cycle AND would drag
|
|
614
|
+
* a host I/O concern into the neutral seam. The route/admin/storage/deploy/fleet shapes are
|
|
615
|
+
* therefore carried as type parameters (defaulting to `unknown`): the neutral seam stays parametric
|
|
616
|
+
* and each concrete host pins the parameters (`@helipod/cli` re-aliases them as `DevServer`/
|
|
617
|
+
* `DevServerOptions`). The FIELDS are unchanged; only their concrete types are supplied by the host.
|
|
618
|
+
*/
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* The handle a host returns from {@link RuntimeHost.serve}. Its `close()`/`setRoutes()` are the ONLY
|
|
622
|
+
* lifecycle a caller drives post-serve. Lifted verbatim from the process host's `DevServer`.
|
|
623
|
+
*/
|
|
624
|
+
interface ServerHandle<Route = unknown> {
|
|
625
|
+
url: string;
|
|
626
|
+
/**
|
|
627
|
+
* The bound TCP port. A portless host (a Durable Object — the Worker owns ingress, there is no
|
|
628
|
+
* socket to bind) returns `0` as a sentinel. Do not treat `0` as "unbound/failed".
|
|
629
|
+
*/
|
|
630
|
+
port: number;
|
|
631
|
+
/**
|
|
632
|
+
* Stop serving and release resources. **MAY never be called.** A host with no shutdown moment —
|
|
633
|
+
* a Durable Object hibernates and is evicted silently, with no `SIGTERM` — is a valid host, and
|
|
634
|
+
* such a host's `close()` may legitimately be a no-op. Do NOT rely on `close()` (and therefore
|
|
635
|
+
* on `runtime.stopDrivers()` / `store.close()`) running: durable work must survive without it.
|
|
636
|
+
* The process host DOES call `close()` on `SIGTERM`/`SIGINT`, exactly as before this seam existed.
|
|
637
|
+
*/
|
|
638
|
+
close(): Promise<void>;
|
|
639
|
+
/** Replace the httpAction route table, e.g. after a hot reload re-resolves `http.ts`. */
|
|
640
|
+
setRoutes(routes: Route[]): void;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* The options a caller passes to {@link RuntimeHost.serve}. Lifted verbatim from the process host's
|
|
644
|
+
* `DevServerOptions` — no field changes.
|
|
645
|
+
*
|
|
646
|
+
* IMPEDANCE CONTRACT (do not design against it): the sync handler's per-session state (each
|
|
647
|
+
* session's read-set, held behind `runtime.handler.connect`/`handleMessage`) is NOT guaranteed to
|
|
648
|
+
* survive a single `serve()` lifetime. On a Durable Object, WebSocket hibernation discards
|
|
649
|
+
* in-memory state; a host may reconstruct a session from its durable attachment and call
|
|
650
|
+
* `handler.connect` again on revival. A host must therefore treat
|
|
651
|
+
* `handler.connect(sessionId, socket)` / `handler.disconnect(sessionId)` as the transport boundary
|
|
652
|
+
* and never assume the handler's in-memory session map persists for the server's lifetime.
|
|
653
|
+
*/
|
|
654
|
+
interface ServeOptions<Route = unknown, Admin = unknown, StorageRt = unknown, Deploy = unknown, Fleet = unknown> {
|
|
655
|
+
port: number;
|
|
656
|
+
ip: string;
|
|
657
|
+
webDir?: string;
|
|
658
|
+
admin?: {
|
|
659
|
+
api: Admin;
|
|
660
|
+
key: string;
|
|
661
|
+
};
|
|
662
|
+
/**
|
|
663
|
+
* The dashboard SPA. Two variants:
|
|
664
|
+
* - `dev`/`serve`: `{ distDir, html }` — dist dir (hashed Vite assets) + key-injected index.html.
|
|
665
|
+
* - a compiled `helipod build` binary: `{ assets, html }` — a urlPath→embedded-path map.
|
|
666
|
+
*/
|
|
667
|
+
dashboard?: {
|
|
668
|
+
distDir: string;
|
|
669
|
+
html: string;
|
|
670
|
+
} | {
|
|
671
|
+
assets: Record<string, string>;
|
|
672
|
+
html: string;
|
|
673
|
+
};
|
|
674
|
+
/** The app's `http.ts` routes, resolved to `path:name` function paths for dispatch. */
|
|
675
|
+
routes?: Route[];
|
|
676
|
+
/** Engine-owned `/api/storage/*` handlers (always-on file storage). Reserved — matched before
|
|
677
|
+
* user routes; stable across reload/deploy. */
|
|
678
|
+
storageRoutes?: StorageRt[];
|
|
679
|
+
/** Reserved routes contributed by composed components (e.g. auth's OAuth callbacks). Matched
|
|
680
|
+
* after storage routes, before user routes. Engine-owned `{method,pathPrefix,handler}` closures.
|
|
681
|
+
* Shares the `StorageRt` shape (`{method,pathPrefix,handler}`) — the CLI pins both to `StorageRoute`. */
|
|
682
|
+
componentRoutes?: StorageRt[];
|
|
683
|
+
/** `POST /_admin/deploy` + `GET /_admin/deploy/modules` handlers — present only when deploy is
|
|
684
|
+
* enabled. `apply` accepts a legacy `{files}` OR a delta `{changed, unchanged}` payload; `modules`
|
|
685
|
+
* returns the current per-path hashes for the client's delta partition. */
|
|
686
|
+
deploy?: {
|
|
687
|
+
apply: (payload: {
|
|
688
|
+
files: Array<{
|
|
689
|
+
path: string;
|
|
690
|
+
code: string;
|
|
691
|
+
}>;
|
|
692
|
+
} | {
|
|
693
|
+
changed: Array<{
|
|
694
|
+
path: string;
|
|
695
|
+
code: string;
|
|
696
|
+
}>;
|
|
697
|
+
unchanged: Array<{
|
|
698
|
+
path: string;
|
|
699
|
+
sha256: string;
|
|
700
|
+
}>;
|
|
701
|
+
}) => Promise<Deploy>;
|
|
702
|
+
modules: () => Record<string, string>;
|
|
703
|
+
};
|
|
704
|
+
/** Fleet node handle — present only under `serve --fleet`. Absent → byte-for-byte non-fleet. */
|
|
705
|
+
fleet?: Fleet;
|
|
706
|
+
/** Present only when THIS node is a `--replica` configured with `--writer-url` — arms `/api/run`'s
|
|
707
|
+
* single-hop defensive guard. Absent → byte-for-byte unchanged behavior. */
|
|
708
|
+
replicaWriterUrl?: string;
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* The seam. A host binds an `EmbeddedRuntime` to a transport and starts serving; the returned
|
|
712
|
+
* {@link ServerHandle} is the whole post-serve lifecycle surface. Slice 1 ships one implementation
|
|
713
|
+
* (`@helipod/cli`'s `ProcessRuntimeHost`); a Durable Object host (Slice 3) implements the same
|
|
714
|
+
* method, wiring Worker `fetch` → the engine's HTTP dispatch and `WebSocketPair`/hibernation →
|
|
715
|
+
* `runtime.handler.connect`. `serve()` is called once per host instance (one call per process, or
|
|
716
|
+
* one per DO incarnation) — it must not assume it is the only call in a process's lifetime.
|
|
717
|
+
*/
|
|
718
|
+
interface RuntimeHost<Route = unknown, Admin = unknown, StorageRt = unknown, Deploy = unknown, Fleet = unknown> {
|
|
719
|
+
serve(runtime: EmbeddedRuntime, options: ServeOptions<Route, Admin, StorageRt, Deploy, Fleet>): Promise<ServerHandle<Route>>;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
export { EmbeddedRuntime, type EmbeddedRuntimeOptions, EmbeddedWriteFanout, type EmbeddedWriteFanoutAdapter, type EmbeddedWriteFanoutPayload, type FanoutListener, InMemoryWriteFanoutAdapter, type LoopbackConnection, type LoopbackHandler, type RuntimeHost, type ServeOptions, type ServerHandle, type ServerMessageListener, clientReceiptsGuard, createEmbeddedRuntime, createLoopbackConnection };
|