@lunora/platform 0.0.0 → 1.0.0-alpha.10

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.
@@ -0,0 +1,642 @@
1
+ /**
2
+ * `ShardKvStore` — the provider-neutral contract for durable key-value storage
3
+ * scoped to one shard. On Cloudflare this is backed by `state.storage`'s
4
+ * key-value surface (`get`/`put`/`delete`/`list`); on another provider it may
5
+ * be a DynamoDB item collection, a Redis keyspace, or a table in the shard's
6
+ * local SQL store.
7
+ *
8
+ * This is the surface `ShardHost` deliberately does not cover. `ShardHost`
9
+ * models the reactive engine's needs — single-writer serialization, local SQL,
10
+ * transactions, alarms. A Durable Object that keeps plain records rather than
11
+ * running the engine (`SessionDO` is the canonical one) needs ordered key
12
+ * lookup and prefix scans instead, and forcing it through `ShardHost.sql`
13
+ * would give it a SQL dialect it does not want. Kept a separate contract so a
14
+ * host can implement one, the other, or both.
15
+ *
16
+ * The engine relies on three guarantees:
17
+ * 1. **Durability** — a written value survives host recycling and is readable
18
+ * on the next wake, exactly like a `SocketHost` attachment.
19
+ * 2. **Read-your-writes** — a `get` after a `put` in the same wake observes the
20
+ * written value.
21
+ * 3. **Prefix enumeration** — `list({ prefix })` returns every live key under
22
+ * the prefix and nothing outside it, so a keyspace can be swept (TTL GC) or
23
+ * migrated without a separate index.
24
+ *
25
+ * This is an internal contract. User code never sees it; only DOs that keep
26
+ * durable records and their host adapters consume it.
27
+ */
28
+ /** Options accepted by {@link ShardKvStore.list}. */
29
+ interface ShardKvListOptions {
30
+ /**
31
+ * Restrict the scan to keys beginning with this string. Omitted means every
32
+ * key in the shard's keyspace — hosts should treat a very large keyspace as
33
+ * the caller's responsibility to bound, exactly as `state.storage.list`
34
+ * does.
35
+ */
36
+ prefix?: string;
37
+ }
38
+ /**
39
+ * The durable key-value contract for one shard. One instance per shard key.
40
+ */
41
+ interface ShardKvStore {
42
+ /**
43
+ * Delete `key`. Resolves `true` when a value was removed, `false` when the
44
+ * key was already absent. Idempotent: deleting a missing key is not an
45
+ * error.
46
+ */
47
+ delete: (key: string) => Promise<boolean>;
48
+ /**
49
+ * Read the value stored under `key`, or `undefined` when absent. The type
50
+ * parameter is a caller-side assertion about the stored shape; the host
51
+ * does not validate it.
52
+ */
53
+ get: <T = unknown>(key: string) => Promise<T | undefined>;
54
+ /**
55
+ * Enumerate live keys, optionally restricted to a prefix. The result MUST
56
+ * contain exactly the keys under the prefix — never a superset — so a
57
+ * prefix sweep cannot touch unrelated keys.
58
+ */
59
+ list: <T = unknown>(options?: ShardKvListOptions) => Promise<Map<string, T>>;
60
+ /**
61
+ * Write `value` under `key`, replacing any existing value. The value must
62
+ * be structured-clonable; hosts serialize it durably.
63
+ */
64
+ put: (key: string, value: unknown) => Promise<void>;
65
+ }
66
+ /**
67
+ * `SchedulerHost` — the provider-neutral contract for durable scheduling
68
+ * (delayed jobs, cron triggers, at-least-once dispatch). On Cloudflare this is
69
+ * backed by `SchedulerDO` (a Durable Object with alarms) plus Cron Triggers.
70
+ * On another provider it may be a job scheduler (SQS + EventBridge, Temporal,
71
+ * a database-backed polling loop, or a managed cron service).
72
+ *
73
+ * The contract encodes the guarantees Lunora's `runAfter` / `runAt` / cron
74
+ * features rely on:
75
+ * 1. **At-least-once delivery** — a scheduled job is dispatched at least once;
76
+ * retries and dead-lettering are host-managed.
77
+ * 2. **Durable persistence** — scheduled jobs survive host recycling.
78
+ * 3. **Time-based dispatch** — jobs can be scheduled for an absolute time or
79
+ * after a delay.
80
+ */
81
+ /** Options accepted when scheduling a job. */
82
+ interface ScheduleOptions {
83
+ /** Run the job at this absolute timestamp (ms since epoch). Overrides `delayMs`. */
84
+ at?: number | Date;
85
+ /** Run the job no sooner than this delay (ms) from now. */
86
+ delayMs?: number;
87
+ /** Per-job retry policy. Falls back to the host's defaults when omitted. */
88
+ retry?: {
89
+ /** Backoff multiplier. */
90
+ backoffMultiplier?: number;
91
+ /** Initial backoff delay (ms). */
92
+ initialDelayMs?: number;
93
+ /** Maximum number of delivery attempts. */
94
+ maxAttempts?: number;
95
+ /** Maximum backoff delay (ms). */
96
+ maxDelayMs?: number;
97
+ };
98
+ /** Routing hint forwarded to the worker so the call lands on the right shard. */
99
+ shardKey?: string;
100
+ }
101
+ /** A scheduled job descriptor returned by the host. */
102
+ interface ScheduledJob {
103
+ /** Unique job identifier. */
104
+ id: string;
105
+ /** Timestamp (ms since epoch) the job is scheduled for. */
106
+ scheduledFor: number;
107
+ }
108
+ /**
109
+ * A scheduled job as the host currently sees it — {@link ScheduledJob} plus the
110
+ * delivery state that only the host knows.
111
+ *
112
+ * `attempts` is what makes at-least-once observable rather than merely
113
+ * promised: a job that failed to deliver and is waiting to be retried is still
114
+ * pending, with a higher count. A host reporting `0` forever is either not
115
+ * retrying or not counting, and both are worth knowing.
116
+ */
117
+ interface ScheduledJobStatus extends ScheduledJob {
118
+ /** Delivery attempts made so far. `0` for a job that has not been dispatched yet. */
119
+ attempts: number;
120
+ /** The function path the job dispatches to. */
121
+ functionPath: string;
122
+ }
123
+ /**
124
+ * The scheduler host contract. One instance per scheduler namespace.
125
+ */
126
+ interface SchedulerHost {
127
+ /**
128
+ * Cancel a previously scheduled job. Returns `true` if the job was found
129
+ * and cancelled; `false` if it was already dispatched or never existed.
130
+ */
131
+ cancel: (id: string) => Promise<boolean>;
132
+ /**
133
+ * Register a cron schedule at runtime. The `cron` expression uses standard
134
+ * cron syntax; the `functionPath` is dispatched on each tick.
135
+ *
136
+ * **Optional, and omitted by hosts whose crons are declared rather than
137
+ * registered.** Cloudflare is one: `triggers.crons` lives in
138
+ * `wrangler.jsonc` and is reconciled at build time by the config layer, so
139
+ * there is no runtime call that could add one. Such a host omits this
140
+ * method rather than supplying one that throws or silently no-ops —
141
+ * presence is the host's declaration that dynamic cron works, exactly as
142
+ * with `SocketHost.setTag`. A caller that finds it absent must fall
143
+ * back to the target's declarative configuration.
144
+ */
145
+ cron?: (cron: string, functionPath: string, args?: Record<string, unknown>) => Promise<void>;
146
+ /**
147
+ * List jobs that exhausted their retry budget and were parked instead of
148
+ * dropped, plus return one to the pending set.
149
+ *
150
+ * **Optional, and its absence is a real statement:** a host without it
151
+ * cannot promise at-least-once, only at-most-once. Once retries are
152
+ * exhausted the job either survives somewhere an operator can find it, or
153
+ * it is gone — and "gone" is indistinguishable from "delivered" to every
154
+ * caller. Guarantee 1 in this module's header is exactly what this member
155
+ * makes checkable.
156
+ *
157
+ * `list` MUST be disjoint from {@link SchedulerHost.list}: a parked job is
158
+ * no longer scheduled, and a host reporting it in both shows a permanently
159
+ * failed job as still on its way.
160
+ *
161
+ * `requeue` returns `false` for an id that is not parked, and on `true`
162
+ * returns the job to the pending set with a fresh attempt budget.
163
+ */
164
+ deadLetter?: {
165
+ list: () => Promise<ScheduledJobStatus[]>;
166
+ requeue: (id: string) => Promise<boolean>;
167
+ };
168
+ /**
169
+ * List the jobs currently pending — scheduled and not yet delivered,
170
+ * including those waiting between retries.
171
+ *
172
+ * Optional: a fire-and-forget host with no queryable queue omits it, and
173
+ * the suite reports the gap rather than asserting against a stub.
174
+ */
175
+ list?: () => Promise<ScheduledJobStatus[]>;
176
+ /**
177
+ * Schedule a function call for later execution. The `functionPath` and
178
+ * `args` are serialized and delivered back to the worker at dispatch time.
179
+ */
180
+ schedule: (functionPath: string, args: Record<string, unknown>, options?: ScheduleOptions) => Promise<ScheduledJob>;
181
+ }
182
+ /**
183
+ * Edge geography → placement region, shared by `@lunora/runtime` (which reads
184
+ * `request.cf` to pick where a shard, replica, or region-local socket should
185
+ * live) and `@lunora/do` (which parses a region out of its own DO name). Kept
186
+ * here — inlined into each consumer's bundle — so the two sides can never drift
187
+ * on the region vocabulary without creating a runtime dependency edge between
188
+ * the packages.
189
+ *
190
+ * The values are Cloudflare's Durable Object location hints, which is also the
191
+ * only vocabulary a Lunora deployment needs today: a region is *only* ever used
192
+ * as a placement hint and as a name segment, never as data. Wrong-but-close is
193
+ * fine by construction — a misrouted read is one longer hop, never a wrong
194
+ * answer — so this maps coarsely and returns `undefined` rather than guessing
195
+ * when the request carries no usable geography.
196
+ *
197
+ * Zero-dependency by design (see the repo's `shared/` rules): only relative /
198
+ * builtin imports, named exports, no `.js` extensions.
199
+ */
200
+ /**
201
+ * The placement regions a name may carry and a hint may request — Cloudflare's
202
+ * `DurableObjectLocationHint` values, listed so the set can be validated at a
203
+ * trust boundary (a region parsed out of a DO name is attacker-influenced input
204
+ * on any route that mints names from a client-supplied shard key).
205
+ */
206
+ declare const REGION_HINTS: readonly ["wnam", "enam", "sam", "weur", "eeur", "apac", "apac-ne", "apac-se", "oc", "afr", "me"];
207
+ /** One placement region. Structurally identical to Cloudflare's `DurableObjectLocationHint`. */
208
+ type RegionHint = (typeof REGION_HINTS)[number];
209
+ /**
210
+ * Cloudflare Durable Object jurisdictions restrict where a DO runs and
211
+ * persists data, for data-residency / compliance regimes (GDPR, FedRAMP, US
212
+ * data residency). The set is open — Cloudflare adds values over time — so
213
+ * this is a widening union rather than a closed enum: the three literals are
214
+ * the values we know about (and the ones editors autocomplete), but any other
215
+ * string a provider introduces is accepted without a release of this package.
216
+ *
217
+ * Other providers may map these to their own region/placement concepts or
218
+ * leave them unsupported.
219
+ */
220
+ type ShardJurisdiction = "eu" | "fedramp" | "us" | (Record<never, never> & string);
221
+ /**
222
+ * A geographic placement region — where a shard should be created, when the
223
+ * caller has an opinion.
224
+ *
225
+ * One vocabulary, defined once: `shared/region-hint.ts` owns the region list
226
+ * (it is also what derives a region from edge geography), and this contract
227
+ * re-exports it rather than restating the strings. A second list is how the two
228
+ * ends of a placement request drift apart.
229
+ *
230
+ * Unlike a jurisdiction — a hard constraint the caller must fail closed on — a
231
+ * region is **best effort and advisory**: a provider may ignore it, and on
232
+ * Cloudflare it is honoured only by the call that first creates the object.
233
+ * Everything downstream must work identically whether the hint was honoured,
234
+ * ignored, or never supplied, and no caller may treat a resolved stub's
235
+ * location as known.
236
+ */
237
+ type ShardRegionHint = RegionHint;
238
+ /**
239
+ * A resolved shard stub. The engine calls `fetch` (or an equivalent RPC
240
+ * method) to dispatch work to the shard.
241
+ */
242
+ interface ShardStub {
243
+ /** Dispatch a request to the shard. */
244
+ fetch: (request: Request) => Promise<Response>;
245
+ }
246
+ /**
247
+ * A directory that resolves a shard key straight to a stub in one step. The
248
+ * two-step `idForName` + `get` pair stays available for providers that expose
249
+ * an addressable id (Cloudflare's `DurableObjectNamespace` does both), but a
250
+ * provider whose registry only understands names implements `getByName` alone.
251
+ */
252
+ interface DirectShardDirectory {
253
+ /** Resolve an opaque id (from `idForName`) to a stub, when the provider has ids. */
254
+ get?: (id: unknown, locationHint?: ShardRegionHint) => ShardStub;
255
+ /** Resolve a shard key to a stub. */
256
+ getByName: (name: string, locationHint?: ShardRegionHint) => ShardStub;
257
+ /** Derive a stable, opaque shard id from a shard key, when the provider has ids. */
258
+ idForName?: (name: string) => unknown;
259
+ /** See {@link ShardDirectory}. */
260
+ jurisdiction?: (jurisdiction: ShardJurisdiction) => ShardDirectory;
261
+ }
262
+ /**
263
+ * A directory that resolves a shard key in two steps: derive an opaque id with
264
+ * `idForName`, then materialize a stub for it with `get`. This is the shape of
265
+ * an id-addressed registry that has no name-based lookup of its own.
266
+ */
267
+ interface TwoStepShardDirectory {
268
+ /** Resolve an opaque id (from `idForName`) to a stub. */
269
+ get: (id: unknown, locationHint?: ShardRegionHint) => ShardStub;
270
+ /**
271
+ * Absent — the discriminant that selects the two-step branch.
272
+ */
273
+ getByName?: undefined;
274
+ /** Derive a stable, opaque shard id from a human-readable key. */
275
+ idForName: (name: string) => unknown;
276
+ /** See {@link ShardDirectory}. */
277
+ jurisdiction?: (jurisdiction: ShardJurisdiction) => ShardDirectory;
278
+ }
279
+ /**
280
+ * The shard directory contract. One instance per shard namespace.
281
+ *
282
+ * A provider satisfies this with *either* direct name lookup
283
+ * ({@link DirectShardDirectory}) *or* the two-step id dance
284
+ * ({@link TwoStepShardDirectory}) — it is a union, not one interface with
285
+ * optional halves, so a name-only registry never has to stub out `idForName`
286
+ * and `get` to type-check. Providers that support both (Cloudflare) simply
287
+ * populate all three and land on the direct branch.
288
+ *
289
+ * Prefer {@link resolveShard} over reaching into either branch by hand.
290
+ *
291
+ * `jurisdiction` derives a placement-restricted view of the directory: every
292
+ * stub created from the returned directory is pinned to that jurisdiction. It
293
+ * is optional because some providers have no placement hints; callers must fail
294
+ * closed when a jurisdiction is requested but the method is absent.
295
+ */
296
+ type ShardDirectory = DirectShardDirectory | TwoStepShardDirectory;
297
+ /**
298
+ * Resolve a shard key to a stub against either directory shape. Uses direct
299
+ * name lookup when the provider has it, and falls back to the two-step
300
+ * `idForName` + `get` dance otherwise.
301
+ *
302
+ * `locationHint` is forwarded to whichever branch runs. It is advisory in
303
+ * both: a provider with no placement concept ignores the extra argument, which
304
+ * is exactly what an implementation written against the pre-placement
305
+ * signature does.
306
+ */
307
+ declare const resolveShard: (directory: ShardDirectory, name: string, locationHint?: ShardRegionHint) => ShardStub;
308
+ /**
309
+ * `ShardHost` — the provider-neutral contract for a single-writer, durable
310
+ * shard execution slot. On Cloudflare this is backed by one Durable Object
311
+ * instance (`state.storage` + `state.blockConcurrencyWhile`); on another
312
+ * provider it may be an actor, a container, or a single-node process.
313
+ *
314
+ * The contract encodes the guarantees the Lunora reactive engine relies on:
315
+ * 1. **Single-writer serialization** — mutations for one shard key never
316
+ * interleave; they are serialized through {@link ShardHost.runSerialized}.
317
+ * 2. **Transactional durability** — a mutation either commits fully or rolls
318
+ * back; no partial writes are observable.
319
+ * 3. **Local SQL execution** — the shard can run synchronous-ish SQL against
320
+ * co-located storage (SQLite on Cloudflare; another embedded or local DB
321
+ * elsewhere). Reads must observe the current transaction's writes.
322
+ * 4. **Alarms / scheduled wakeup** — the shard can schedule a future wakeup
323
+ * for background work (timers, retries, TTL cleanup).
324
+ * 5. **Background continuation** — `waitUntil` lets work outlive the request
325
+ * without blocking the response.
326
+ *
327
+ * This is an internal contract. User code never sees it; only the runtime,
328
+ * the shard engine, and host adapters consume it.
329
+ */
330
+ /**
331
+ * Minimal SQL cursor/row shape returned by the local SQL executor. Kept
332
+ * generic so a host can wrap SQLite, libSQL, or another embedded store.
333
+ */
334
+ type SqlRow = Record<string, unknown>;
335
+ /**
336
+ * The result of one statement: a cursor over its rows.
337
+ *
338
+ * Every member here is required, because the engine uses all three and a host
339
+ * that omits one fails at runtime rather than at compile time. That is not
340
+ * hypothetical — an earlier revision of this contract made `toArray` optional
341
+ * and offered a `rowsAffected` nothing reads, which meant a host could satisfy
342
+ * the type and still be unusable.
343
+ *
344
+ * Iteration is part of the contract because read paths stream cursors directly
345
+ * rather than buffering; `toArray` is the buffered form, and `one` is the
346
+ * exactly-one-row form used by lookups and aggregates.
347
+ */
348
+ interface ShardSqlCursor<Row = SqlRow> extends Iterable<Row> {
349
+ /**
350
+ * The single row this statement produced.
351
+ * @throws when the result does not hold exactly one row.
352
+ */
353
+ one: () => Row;
354
+ /** Buffer every row. */
355
+ toArray: () => Row[];
356
+ }
357
+ /**
358
+ * The local, synchronous-ish SQL executor available inside a shard — the
359
+ * engine's hot path. Mirrors the shape the DO's `state.storage.sql` exposes.
360
+ *
361
+ * Implementations may be sync (Cloudflare `SqlStorage`) or async-backed with
362
+ * a sync facade; the engine treats it as fire-and-forget within a
363
+ * {@link ShardHost.transaction} closure.
364
+ */
365
+ interface ShardSqlExec {
366
+ /**
367
+ * Size of the shard's local database in bytes, when the host can report it
368
+ * cheaply. Optional: it is used for storage telemetry and quota warnings,
369
+ * never for correctness, so a host without the number simply omits it.
370
+ *
371
+ * Read as a live getter where the host provides one — do not cache it.
372
+ */
373
+ readonly databaseSize?: number;
374
+ /** Execute a SQL statement with optional bound parameters. */
375
+ exec: <Row = SqlRow>(query: string, ...bindings: ReadonlyArray<unknown>) => ShardSqlCursor<Row>;
376
+ }
377
+ /**
378
+ * Async SQL executor used by the engine's higher-level paths (global tables,
379
+ * metrics, auth). Already defined in `@lunora/sql-store` as `SqlExec`; this
380
+ * alias keeps the platform contract self-contained.
381
+ */
382
+ interface ShardAsyncSqlExec {
383
+ all: (sql: string, params: ReadonlyArray<unknown>) => Promise<SqlRow[]>;
384
+ run: (sql: string, params: ReadonlyArray<unknown>) => Promise<{
385
+ rowsAffected: number;
386
+ }>;
387
+ }
388
+ /**
389
+ * Alarm scheduling for a shard. Alarms are durable: they survive host
390
+ * recycling and fire at the requested timestamp.
391
+ */
392
+ interface ShardAlarms {
393
+ /** Delete any pending alarm. */
394
+ delete: () => Promise<void> | void;
395
+ /** Read the currently scheduled alarm timestamp, if any. */
396
+ get: () => Promise<number | null> | number | null;
397
+ /**
398
+ * Schedule the next alarm. Call {@link ShardAlarms.delete} to clear a
399
+ * pending alarm — `set` always schedules and never clears.
400
+ */
401
+ set: (timestamp: number | Date) => Promise<void> | void;
402
+ }
403
+ /**
404
+ * The core shard host contract. One instance per shard key.
405
+ */
406
+ interface ShardHost {
407
+ /** Durable alarm scheduling for the shard. */
408
+ alarms: ShardAlarms;
409
+ /**
410
+ * Async SQL executor for engine paths that need promise-based row access
411
+ * (global tables, metrics, auth). Hosts may implement this over the same
412
+ * underlying storage as `sql`.
413
+ */
414
+ asyncSql?: ShardAsyncSqlExec;
415
+ /**
416
+ * Run `fn` with exclusive ownership of the shard. Concurrent calls are
417
+ * queued; no two closures run at once for the same shard key. On
418
+ * Cloudflare this maps to `state.blockConcurrencyWhile`.
419
+ *
420
+ * A closure that throws must reject with **the value it threw**, and must
421
+ * leave the host usable for the next call. Engine errors carry a `code` and
422
+ * `status` the RPC edge renders from, so a host that lets its platform
423
+ * substitute a copy silently downgrades every coded error to an internal
424
+ * fault — and a host that tears itself down on a throw makes an ordinary
425
+ * application error cost every other caller on that shard.
426
+ */
427
+ runSerialized: <T>(function_: () => Promise<T>) => Promise<T>;
428
+ /**
429
+ * The shard key this host serves — the name the directory resolved to reach
430
+ * it. Used for telemetry attribution and log correlation ("which shard
431
+ * emitted this?"), never for routing: the host is already the shard.
432
+ *
433
+ * Optional because a host may address a shard by an opaque id with no
434
+ * human-readable name (Cloudflare's `newUniqueId()` objects have none).
435
+ * Callers must tolerate `undefined` rather than assume a key exists.
436
+ */
437
+ readonly shardKey?: string;
438
+ /**
439
+ * The shard's local SQL executor. Reads and writes inside a `transaction`
440
+ * closure observe the transaction's isolation.
441
+ */
442
+ sql: ShardSqlExec;
443
+ /**
444
+ * Run `fn` inside a durable transaction. If `fn` throws, all writes roll
445
+ * back and the call rejects with **the value `fn` threw** — see
446
+ * {@link ShardHost.runSerialized} for why the identity matters. Raw
447
+ * `BEGIN`/`COMMIT`/`ROLLBACK` are forbidden inside the closure; the host
448
+ * manages the transaction boundary.
449
+ */
450
+ transaction: <T>(function_: () => Promise<T>) => Promise<T>;
451
+ /**
452
+ * Extend the lifetime of background work past the response. Optional on
453
+ * hosts that don't distinguish request/background lifetimes.
454
+ */
455
+ waitUntil?: (promise: Promise<unknown>) => void;
456
+ }
457
+ /**
458
+ * `SocketHost` — the provider-neutral contract for hibernated WebSocket
459
+ * subscriptions inside a shard. On Cloudflare this is backed by the Durable
460
+ * Object WebSocket hibernation API (`state.acceptWebSocket`,
461
+ * `ws.serializeAttachment`, `state.getWebSockets`).
462
+ *
463
+ * The engine relies on three guarantees:
464
+ * 1. **Hibernation** — a socket can be evicted from memory and rehydrated
465
+ * later without losing its subscription state.
466
+ * 2. **Attachment round-trip** — arbitrary JSON state serialized with the
467
+ * socket must survive recycling and be readable on wake.
468
+ * 3. **Tagged fan-out** — the host can enumerate live sockets (optionally by
469
+ * tag) to broadcast query invalidations and shape updates.
470
+ *
471
+ * A non-Cloudflare host may keep sockets in memory or in a process-local
472
+ * registry; the contract requires that attachments are durable, that
473
+ * `getSockets()` returns the currently-live set, and that `getSockets(tag)`
474
+ * returns *only* the sockets carrying that tag.
475
+ *
476
+ * Tagging comes in two tiers, because hosts differ on when a tag can be set:
477
+ *
478
+ * - **Accept-time tags** (`accept(socket, attachment, tags)`) — mandatory.
479
+ * Every host must accept them and must honour them in `getSockets(tag)`.
480
+ * They are durable: a tag survives hibernation exactly like an attachment.
481
+ * Cloudflare's `state.acceptWebSocket(ws, tags)` is this tier, and it is the
482
+ * only tier Cloudflare supports — DO tags are immutable once accepted.
483
+ * - **Mutable tags** ({@link SocketHost.setTag} / {@link SocketHost.removeTag})
484
+ * — optional. Presence declares that the host can retag a live socket. Hosts
485
+ * that cannot (Cloudflare) omit both methods, and callers that need to
486
+ * retag must instead close and re-accept the socket with new tags.
487
+ *
488
+ * **Reserved-slot budget.** A host may reserve some of its accept-time tag
489
+ * slots for its own bookkeeping — Cloudflare's adapter prepends one identity
490
+ * tag (so `idFor` survives hibernation) before every `acceptWebSocket` call.
491
+ * Cloudflare's own cap is 10 tags per socket, 256 characters each
492
+ * (developers.cloudflare.com/durable-objects/api/state/), so with one slot
493
+ * reserved a portable caller should assume **at most 9 usable tags, each
494
+ * bounded to at most 256 characters** — a budget-exceeding `accept` call
495
+ * fails loudly on the host that enforces it (see
496
+ * {@link SocketHost.accept}) rather than passing silently on hosts with no
497
+ * cap and only failing, opaquely, on Cloudflare.
498
+ */
499
+ /**
500
+ * The socket the engine sends through.
501
+ *
502
+ * Deliberately **not** a wrapper object. A host is expected to return its own
503
+ * transport socket here, unchanged — which is why identity lives out-of-band on
504
+ * {@link SocketHost.idFor} rather than as an `id` property on this interface.
505
+ *
506
+ * Two reasons, one measured and one structural:
507
+ *
508
+ * 1. **Fan-out is O(subscribers) and this interface is on that loop.** Whisper
509
+ * delivery, shape pokes and delta delivery all walk every socket calling
510
+ * `deserializeAttachment` then `send`. When those forwarded through a wrapper,
511
+ * the two extra call frames per socket cost +1.11 ns/socket — +11% to +13% on
512
+ * whisper fan-out at 128 and 1024 subscribers, against a per-socket body of
513
+ * only ~6-7 ns. An `id` property is the only thing a wrapper was needed for,
514
+ * and it is read a handful of times outside the hot loops.
515
+ * 2. **A wrapper creates two identities for one socket.** Enumeration would
516
+ * yield handles while the runtime's own message/close callbacks yield the
517
+ * transport socket, so every per-socket `WeakMap` memo could key on either and
518
+ * diverge. Returning the transport socket collapses that: there is one object,
519
+ * so {@link SocketHost.handleFor} is an ownership test rather than a
520
+ * translation, and the memos cannot disagree.
521
+ *
522
+ * A host whose transport genuinely cannot satisfy this shape may still wrap —
523
+ * nothing here forbids it — but it pays the per-socket cost itself instead of
524
+ * charging every other host for it.
525
+ */
526
+ interface SocketHandle {
527
+ /**
528
+ * Bytes queued for send but not yet flushed, when the transport reports it.
529
+ *
530
+ * The engine polls this to apply backpressure before pushing another batch
531
+ * at a slow subscriber. Optional because not every transport exposes a
532
+ * queue depth — absent means "assume drained", which degrades to the
533
+ * pre-backpressure behavior rather than stalling.
534
+ */
535
+ readonly bufferedAmount?: number;
536
+ /** Close the socket with an optional code and reason. */
537
+ close: (code?: number, reason?: string) => void;
538
+ /** Read the attachment previously stored with `serializeAttachment`. */
539
+ deserializeAttachment: () => unknown;
540
+ /** Send a text or binary frame. */
541
+ send: (data: string | ArrayBufferLike | Blob | ArrayBufferView) => void;
542
+ /** Persist attachment state for this socket. */
543
+ serializeAttachment: (value: unknown) => void;
544
+ }
545
+ /**
546
+ * The socket host contract. One instance per shard.
547
+ */
548
+ interface SocketHost {
549
+ /**
550
+ * Accept a new WebSocket connection into the shard. `attachment` is
551
+ * serialized immediately and must survive host recycling. `tags` are the
552
+ * socket's durable fan-out labels, fixed for its lifetime — they must
553
+ * survive recycling too and must be honoured by
554
+ * {@link SocketHost.getSockets}. Returns a handle the engine can
555
+ * send/close through.
556
+ *
557
+ * Portable callers should assume a budget of **at most 9 usable tags, each
558
+ * at most 256 characters** — some hosts (Cloudflare) reserve one tag slot
559
+ * of their own 10-tag cap for bookkeeping; see this file's module-header
560
+ * "Reserved-slot budget" note. A host that enforces a cap rejects an
561
+ * over-budget call rather than accepting it and silently dropping or
562
+ * truncating tags, which would break {@link SocketHost.getSockets}'s
563
+ * exactness requirement.
564
+ */
565
+ accept: (socket: unknown, attachment?: unknown, tags?: ReadonlyArray<string>) => SocketHandle;
566
+ /**
567
+ * Enumerate currently-live sockets. When `tag` is supplied the result MUST
568
+ * contain exactly the sockets carrying that tag — never a superset.
569
+ *
570
+ * Exactness is a correctness requirement, not a performance one: returning
571
+ * every socket for a tagged call would fan a shape update out to unrelated
572
+ * subscriptions, which across tenants is a data leak. A host without
573
+ * native tagging must therefore filter in userland over the tags it was
574
+ * handed at {@link SocketHost.accept}.
575
+ */
576
+ getSockets: (tag?: string) => SocketHandle[];
577
+ /**
578
+ * Resolve a raw socket the runtime handed back — to a message or close
579
+ * callback, which carry the transport's own object rather than a handle —
580
+ * to the handle the host issued for it at {@link SocketHost.accept}.
581
+ *
582
+ * Without this the two worlds never meet: enumeration yields handles while
583
+ * event callbacks yield raw sockets, and code that has to compare the two
584
+ * (excluding a sender from its own broadcast, say) is forced back onto the
585
+ * provider type. Returns `undefined` for a socket this host never accepted.
586
+ */
587
+ handleFor: (socket: unknown) => SocketHandle | undefined;
588
+ /**
589
+ * The socket's unique identifier, stable across hibernation.
590
+ *
591
+ * Out-of-band rather than a property on {@link SocketHandle} so a host can
592
+ * return its transport socket unchanged — see the note there for why that
593
+ * matters on the fan-out path. A host supplies identity however it can: a
594
+ * durable tag minted at accept (Cloudflare), a registry key, a `WeakMap`.
595
+ *
596
+ * Must answer consistently for the same socket within a wake AND across a
597
+ * recycle, since the engine uses it to reassociate a rehydrated socket with
598
+ * its subscription state. Callers outside the O(subscribers) loops are the
599
+ * intended consumers; do not reach for this per socket per frame.
600
+ *
601
+ * **A socket this host never {@link SocketHost.accept}ed** (a foreign socket
602
+ * the runtime hands back — a whisper sender in another pool, a relay peer)
603
+ * is outside that "own socket" contract, but a host must still pick ONE
604
+ * consistent answer for it, never a fresh value per call:
605
+ *
606
+ * - Throw, when a `SocketHandle` from this host can *only* ever originate
607
+ * from this host's own `accept`/`recycle` — an unrecognized handle then
608
+ * means caller error (a handle crossed from a different host instance),
609
+ * and failing loud beats returning a plausible-looking wrong id. This is
610
+ * the reference host's choice, since its `SocketHandle` is an opaque
611
+ * object it mints itself.
612
+ * - Mint and cache a read-only fallback id, when `SocketHandle` doubles as
613
+ * the provider's own transport socket (see {@link SocketHandle}'s "not a
614
+ * wrapper" rationale) — a foreign-but-genuine socket then structurally
615
+ * satisfies the type without ever going through `accept`, so throwing
616
+ * would fire on legitimate traffic, not just caller error. This is the
617
+ * Cloudflare host's choice: it caches into a separate map from its
618
+ * accept-time ownership evidence, so an `idFor` lookup can never promote
619
+ * a socket into "ours" for {@link SocketHost.handleFor}.
620
+ *
621
+ * Either is a valid implementation as long as it is consistent: what is not
622
+ * valid is minting a new id on every call for a socket the host does not
623
+ * recognize, which defeats the "same string for the same socket" property
624
+ * every caller of `idFor` — owned or not — depends on.
625
+ */
626
+ idFor: (socket: SocketHandle) => string;
627
+ /**
628
+ * Remove a tag from a live socket — all of them when `tag` is omitted.
629
+ * Optional, and only meaningful alongside {@link SocketHost.setTag}: a
630
+ * host that cannot retag a live socket must omit both.
631
+ */
632
+ removeTag?: (socket: SocketHandle, tag?: string) => void;
633
+ /**
634
+ * Tag a live socket after it was accepted. Presence of this method is the
635
+ * host's declaration that tags are *mutable*; it is not what makes tagged
636
+ * fan-out work, since accept-time tags are mandatory for every host. Omit
637
+ * it (rather than supplying a no-op) when the host's tags are frozen at
638
+ * accept, so callers re-accept instead of silently losing the retag.
639
+ */
640
+ setTag?: (socket: SocketHandle, tag: string) => void;
641
+ }
642
+ export { DirectShardDirectory as D, ScheduleOptions as S, TwoStepShardDirectory as T, ScheduledJob as a, ScheduledJobStatus as b, SchedulerHost as c, ShardAlarms as d, ShardAsyncSqlExec as e, ShardDirectory as f, ShardHost as g, ShardJurisdiction as h, ShardKvListOptions as i, ShardKvStore as j, ShardRegionHint as k, ShardSqlCursor as l, ShardSqlExec as m, ShardStub as n, SocketHandle as o, SocketHost as p, SqlRow as q, resolveShard as r };