@lunora/platform 0.0.0 → 1.0.0-alpha.2

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,580 @@
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
+ * `ShardDirectory` — the provider-neutral contract for resolving shard keys to
184
+ * callable stubs. On Cloudflare this is backed by `DurableObjectNamespace`
185
+ * (`idFromName` + `get` + `jurisdiction`). On another provider it may be an
186
+ * actor registry, a consistent-hash router, or a local in-process map.
187
+ *
188
+ * The engine relies on two capabilities:
189
+ * 1. **Deterministic placement** — a shard key always resolves to the same
190
+ * logical shard (`idForName`).
191
+ * 2. **RPC dispatch** — a resolved stub can receive a `fetch` request (or
192
+ * equivalent RPC call) that the shard handles.
193
+ *
194
+ * Placement hints (jurisdiction, region) are provider-mapped and may be
195
+ * unsupported per the capability matrix.
196
+ */
197
+ /**
198
+ * Cloudflare Durable Object jurisdictions restrict where a DO runs and
199
+ * persists data, for data-residency / compliance regimes (GDPR, FedRAMP, US
200
+ * data residency). The set is open — Cloudflare adds values over time — so
201
+ * this is a widening union rather than a closed enum: the three literals are
202
+ * the values we know about (and the ones editors autocomplete), but any other
203
+ * string a provider introduces is accepted without a release of this package.
204
+ *
205
+ * Other providers may map these to their own region/placement concepts or
206
+ * leave them unsupported.
207
+ */
208
+ type ShardJurisdiction = "eu" | "fedramp" | "us" | (Record<never, never> & string);
209
+ /**
210
+ * A resolved shard stub. The engine calls `fetch` (or an equivalent RPC
211
+ * method) to dispatch work to the shard.
212
+ */
213
+ interface ShardStub {
214
+ /** Dispatch a request to the shard. */
215
+ fetch: (request: Request) => Promise<Response>;
216
+ }
217
+ /**
218
+ * A directory that resolves a shard key straight to a stub in one step. The
219
+ * two-step `idForName` + `get` pair stays available for providers that expose
220
+ * an addressable id (Cloudflare's `DurableObjectNamespace` does both), but a
221
+ * provider whose registry only understands names implements `getByName` alone.
222
+ */
223
+ interface DirectShardDirectory {
224
+ /** Resolve an opaque id (from `idForName`) to a stub, when the provider has ids. */
225
+ get?: (id: unknown) => ShardStub;
226
+ /** Resolve a shard key to a stub. */
227
+ getByName: (name: string) => ShardStub;
228
+ /** Derive a stable, opaque shard id from a shard key, when the provider has ids. */
229
+ idForName?: (name: string) => unknown;
230
+ /** See {@link ShardDirectory}. */
231
+ jurisdiction?: (jurisdiction: ShardJurisdiction) => ShardDirectory;
232
+ }
233
+ /**
234
+ * A directory that resolves a shard key in two steps: derive an opaque id with
235
+ * `idForName`, then materialize a stub for it with `get`. This is the shape of
236
+ * an id-addressed registry that has no name-based lookup of its own.
237
+ */
238
+ interface TwoStepShardDirectory {
239
+ /** Resolve an opaque id (from `idForName`) to a stub. */
240
+ get: (id: unknown) => ShardStub;
241
+ /**
242
+ * Absent — the discriminant that selects the two-step branch.
243
+ */
244
+ getByName?: undefined;
245
+ /** Derive a stable, opaque shard id from a human-readable key. */
246
+ idForName: (name: string) => unknown;
247
+ /** See {@link ShardDirectory}. */
248
+ jurisdiction?: (jurisdiction: ShardJurisdiction) => ShardDirectory;
249
+ }
250
+ /**
251
+ * The shard directory contract. One instance per shard namespace.
252
+ *
253
+ * A provider satisfies this with *either* direct name lookup
254
+ * ({@link DirectShardDirectory}) *or* the two-step id dance
255
+ * ({@link TwoStepShardDirectory}) — it is a union, not one interface with
256
+ * optional halves, so a name-only registry never has to stub out `idForName`
257
+ * and `get` to type-check. Providers that support both (Cloudflare) simply
258
+ * populate all three and land on the direct branch.
259
+ *
260
+ * Prefer {@link resolveShard} over reaching into either branch by hand.
261
+ *
262
+ * `jurisdiction` derives a placement-restricted view of the directory: every
263
+ * stub created from the returned directory is pinned to that jurisdiction. It
264
+ * is optional because some providers have no placement hints; callers must fail
265
+ * closed when a jurisdiction is requested but the method is absent.
266
+ */
267
+ type ShardDirectory = DirectShardDirectory | TwoStepShardDirectory;
268
+ /**
269
+ * Resolve a shard key to a stub against either directory shape. Uses direct
270
+ * name lookup when the provider has it, and falls back to the two-step
271
+ * `idForName` + `get` dance otherwise.
272
+ */
273
+ declare const resolveShard: (directory: ShardDirectory, name: string) => ShardStub;
274
+ /**
275
+ * `ShardHost` — the provider-neutral contract for a single-writer, durable
276
+ * shard execution slot. On Cloudflare this is backed by one Durable Object
277
+ * instance (`state.storage` + `state.blockConcurrencyWhile`); on another
278
+ * provider it may be an actor, a container, or a single-node process.
279
+ *
280
+ * The contract encodes the guarantees the Lunora reactive engine relies on:
281
+ * 1. **Single-writer serialization** — mutations for one shard key never
282
+ * interleave; they are serialized through {@link ShardHost.runSerialized}.
283
+ * 2. **Transactional durability** — a mutation either commits fully or rolls
284
+ * back; no partial writes are observable.
285
+ * 3. **Local SQL execution** — the shard can run synchronous-ish SQL against
286
+ * co-located storage (SQLite on Cloudflare; another embedded or local DB
287
+ * elsewhere). Reads must observe the current transaction's writes.
288
+ * 4. **Alarms / scheduled wakeup** — the shard can schedule a future wakeup
289
+ * for background work (timers, retries, TTL cleanup).
290
+ * 5. **Background continuation** — `waitUntil` lets work outlive the request
291
+ * without blocking the response.
292
+ *
293
+ * This is an internal contract. User code never sees it; only the runtime,
294
+ * the shard engine, and host adapters consume it.
295
+ */
296
+ /**
297
+ * Minimal SQL cursor/row shape returned by the local SQL executor. Kept
298
+ * generic so a host can wrap SQLite, libSQL, or another embedded store.
299
+ */
300
+ type SqlRow = Record<string, unknown>;
301
+ /**
302
+ * The result of one statement: a cursor over its rows.
303
+ *
304
+ * Every member here is required, because the engine uses all three and a host
305
+ * that omits one fails at runtime rather than at compile time. That is not
306
+ * hypothetical — an earlier revision of this contract made `toArray` optional
307
+ * and offered a `rowsAffected` nothing reads, which meant a host could satisfy
308
+ * the type and still be unusable.
309
+ *
310
+ * Iteration is part of the contract because read paths stream cursors directly
311
+ * rather than buffering; `toArray` is the buffered form, and `one` is the
312
+ * exactly-one-row form used by lookups and aggregates.
313
+ */
314
+ interface ShardSqlCursor<Row = SqlRow> extends Iterable<Row> {
315
+ /**
316
+ * The single row this statement produced.
317
+ * @throws when the result does not hold exactly one row.
318
+ */
319
+ one: () => Row;
320
+ /** Buffer every row. */
321
+ toArray: () => Row[];
322
+ }
323
+ /**
324
+ * The local, synchronous-ish SQL executor available inside a shard — the
325
+ * engine's hot path. Mirrors the shape the DO's `state.storage.sql` exposes.
326
+ *
327
+ * Implementations may be sync (Cloudflare `SqlStorage`) or async-backed with
328
+ * a sync facade; the engine treats it as fire-and-forget within a
329
+ * {@link ShardHost.transaction} closure.
330
+ */
331
+ interface ShardSqlExec {
332
+ /**
333
+ * Size of the shard's local database in bytes, when the host can report it
334
+ * cheaply. Optional: it is used for storage telemetry and quota warnings,
335
+ * never for correctness, so a host without the number simply omits it.
336
+ *
337
+ * Read as a live getter where the host provides one — do not cache it.
338
+ */
339
+ readonly databaseSize?: number;
340
+ /** Execute a SQL statement with optional bound parameters. */
341
+ exec: <Row = SqlRow>(query: string, ...bindings: ReadonlyArray<unknown>) => ShardSqlCursor<Row>;
342
+ }
343
+ /**
344
+ * Async SQL executor used by the engine's higher-level paths (global tables,
345
+ * metrics, auth). Already defined in `@lunora/sql-store` as `SqlExec`; this
346
+ * alias keeps the platform contract self-contained.
347
+ */
348
+ interface ShardAsyncSqlExec {
349
+ all: (sql: string, params: ReadonlyArray<unknown>) => Promise<SqlRow[]>;
350
+ run: (sql: string, params: ReadonlyArray<unknown>) => Promise<{
351
+ rowsAffected: number;
352
+ }>;
353
+ }
354
+ /**
355
+ * Alarm scheduling for a shard. Alarms are durable: they survive host
356
+ * recycling and fire at the requested timestamp.
357
+ */
358
+ interface ShardAlarms {
359
+ /** Delete any pending alarm. */
360
+ delete: () => Promise<void> | void;
361
+ /** Read the currently scheduled alarm timestamp, if any. */
362
+ get: () => Promise<number | null> | number | null;
363
+ /**
364
+ * Schedule the next alarm. Call {@link ShardAlarms.delete} to clear a
365
+ * pending alarm — `set` always schedules and never clears.
366
+ */
367
+ set: (timestamp: number | Date) => Promise<void> | void;
368
+ }
369
+ /**
370
+ * The core shard host contract. One instance per shard key.
371
+ */
372
+ interface ShardHost {
373
+ /** Durable alarm scheduling for the shard. */
374
+ alarms: ShardAlarms;
375
+ /**
376
+ * Async SQL executor for engine paths that need promise-based row access
377
+ * (global tables, metrics, auth). Hosts may implement this over the same
378
+ * underlying storage as `sql`.
379
+ */
380
+ asyncSql?: ShardAsyncSqlExec;
381
+ /**
382
+ * Run `fn` with exclusive ownership of the shard. Concurrent calls are
383
+ * queued; no two closures run at once for the same shard key. On
384
+ * Cloudflare this maps to `state.blockConcurrencyWhile`.
385
+ */
386
+ runSerialized: <T>(function_: () => Promise<T>) => Promise<T>;
387
+ /**
388
+ * The shard key this host serves — the name the directory resolved to reach
389
+ * it. Used for telemetry attribution and log correlation ("which shard
390
+ * emitted this?"), never for routing: the host is already the shard.
391
+ *
392
+ * Optional because a host may address a shard by an opaque id with no
393
+ * human-readable name (Cloudflare's `newUniqueId()` objects have none).
394
+ * Callers must tolerate `undefined` rather than assume a key exists.
395
+ */
396
+ readonly shardKey?: string;
397
+ /**
398
+ * The shard's local SQL executor. Reads and writes inside a `transaction`
399
+ * closure observe the transaction's isolation.
400
+ */
401
+ sql: ShardSqlExec;
402
+ /**
403
+ * Run `fn` inside a durable transaction. If `fn` throws, all writes roll
404
+ * back. Raw `BEGIN`/`COMMIT`/`ROLLBACK` are forbidden inside the closure;
405
+ * the host manages the transaction boundary.
406
+ */
407
+ transaction: <T>(function_: () => Promise<T>) => Promise<T>;
408
+ /**
409
+ * Extend the lifetime of background work past the response. Optional on
410
+ * hosts that don't distinguish request/background lifetimes.
411
+ */
412
+ waitUntil?: (promise: Promise<unknown>) => void;
413
+ }
414
+ /**
415
+ * `SocketHost` — the provider-neutral contract for hibernated WebSocket
416
+ * subscriptions inside a shard. On Cloudflare this is backed by the Durable
417
+ * Object WebSocket hibernation API (`state.acceptWebSocket`,
418
+ * `ws.serializeAttachment`, `state.getWebSockets`).
419
+ *
420
+ * The engine relies on three guarantees:
421
+ * 1. **Hibernation** — a socket can be evicted from memory and rehydrated
422
+ * later without losing its subscription state.
423
+ * 2. **Attachment round-trip** — arbitrary JSON state serialized with the
424
+ * socket must survive recycling and be readable on wake.
425
+ * 3. **Tagged fan-out** — the host can enumerate live sockets (optionally by
426
+ * tag) to broadcast query invalidations and shape updates.
427
+ *
428
+ * A non-Cloudflare host may keep sockets in memory or in a process-local
429
+ * registry; the contract requires that attachments are durable, that
430
+ * `getSockets()` returns the currently-live set, and that `getSockets(tag)`
431
+ * returns *only* the sockets carrying that tag.
432
+ *
433
+ * Tagging comes in two tiers, because hosts differ on when a tag can be set:
434
+ *
435
+ * - **Accept-time tags** (`accept(socket, attachment, tags)`) — mandatory.
436
+ * Every host must accept them and must honour them in `getSockets(tag)`.
437
+ * They are durable: a tag survives hibernation exactly like an attachment.
438
+ * Cloudflare's `state.acceptWebSocket(ws, tags)` is this tier, and it is the
439
+ * only tier Cloudflare supports — DO tags are immutable once accepted.
440
+ * - **Mutable tags** ({@link SocketHost.setTag} / {@link SocketHost.removeTag})
441
+ * — optional. Presence declares that the host can retag a live socket. Hosts
442
+ * that cannot (Cloudflare) omit both methods, and callers that need to
443
+ * retag must instead close and re-accept the socket with new tags.
444
+ */
445
+ /**
446
+ * The socket the engine sends through.
447
+ *
448
+ * Deliberately **not** a wrapper object. A host is expected to return its own
449
+ * transport socket here, unchanged — which is why identity lives out-of-band on
450
+ * {@link SocketHost.idFor} rather than as an `id` property on this interface.
451
+ *
452
+ * Two reasons, one measured and one structural:
453
+ *
454
+ * 1. **Fan-out is O(subscribers) and this interface is on that loop.** Whisper
455
+ * delivery, shape pokes and delta delivery all walk every socket calling
456
+ * `deserializeAttachment` then `send`. When those forwarded through a wrapper,
457
+ * the two extra call frames per socket cost +1.11 ns/socket — +11% to +13% on
458
+ * whisper fan-out at 128 and 1024 subscribers, against a per-socket body of
459
+ * only ~6-7 ns. An `id` property is the only thing a wrapper was needed for,
460
+ * and it is read a handful of times outside the hot loops.
461
+ * 2. **A wrapper creates two identities for one socket.** Enumeration would
462
+ * yield handles while the runtime's own message/close callbacks yield the
463
+ * transport socket, so every per-socket `WeakMap` memo could key on either and
464
+ * diverge. Returning the transport socket collapses that: there is one object,
465
+ * so {@link SocketHost.handleFor} is an ownership test rather than a
466
+ * translation, and the memos cannot disagree.
467
+ *
468
+ * A host whose transport genuinely cannot satisfy this shape may still wrap —
469
+ * nothing here forbids it — but it pays the per-socket cost itself instead of
470
+ * charging every other host for it.
471
+ */
472
+ interface SocketHandle {
473
+ /**
474
+ * Bytes queued for send but not yet flushed, when the transport reports it.
475
+ *
476
+ * The engine polls this to apply backpressure before pushing another batch
477
+ * at a slow subscriber. Optional because not every transport exposes a
478
+ * queue depth — absent means "assume drained", which degrades to the
479
+ * pre-backpressure behavior rather than stalling.
480
+ */
481
+ readonly bufferedAmount?: number;
482
+ /** Close the socket with an optional code and reason. */
483
+ close: (code?: number, reason?: string) => void;
484
+ /** Read the attachment previously stored with `serializeAttachment`. */
485
+ deserializeAttachment: () => unknown;
486
+ /** Send a text or binary frame. */
487
+ send: (data: string | ArrayBufferLike | Blob | ArrayBufferView) => void;
488
+ /** Persist attachment state for this socket. */
489
+ serializeAttachment: (value: unknown) => void;
490
+ }
491
+ /**
492
+ * The socket host contract. One instance per shard.
493
+ */
494
+ interface SocketHost {
495
+ /**
496
+ * Accept a new WebSocket connection into the shard. `attachment` is
497
+ * serialized immediately and must survive host recycling. `tags` are the
498
+ * socket's durable fan-out labels, fixed for its lifetime — they must
499
+ * survive recycling too and must be honoured by
500
+ * {@link SocketHost.getSockets}. Returns a handle the engine can
501
+ * send/close through.
502
+ */
503
+ accept: (socket: unknown, attachment?: unknown, tags?: ReadonlyArray<string>) => SocketHandle;
504
+ /**
505
+ * Enumerate currently-live sockets. When `tag` is supplied the result MUST
506
+ * contain exactly the sockets carrying that tag — never a superset.
507
+ *
508
+ * Exactness is a correctness requirement, not a performance one: returning
509
+ * every socket for a tagged call would fan a shape update out to unrelated
510
+ * subscriptions, which across tenants is a data leak. A host without
511
+ * native tagging must therefore filter in userland over the tags it was
512
+ * handed at {@link SocketHost.accept}.
513
+ */
514
+ getSockets: (tag?: string) => SocketHandle[];
515
+ /**
516
+ * Resolve a raw socket the runtime handed back — to a message or close
517
+ * callback, which carry the transport's own object rather than a handle —
518
+ * to the handle the host issued for it at {@link SocketHost.accept}.
519
+ *
520
+ * Without this the two worlds never meet: enumeration yields handles while
521
+ * event callbacks yield raw sockets, and code that has to compare the two
522
+ * (excluding a sender from its own broadcast, say) is forced back onto the
523
+ * provider type. Returns `undefined` for a socket this host never accepted.
524
+ */
525
+ handleFor: (socket: unknown) => SocketHandle | undefined;
526
+ /**
527
+ * The socket's unique identifier, stable across hibernation.
528
+ *
529
+ * Out-of-band rather than a property on {@link SocketHandle} so a host can
530
+ * return its transport socket unchanged — see the note there for why that
531
+ * matters on the fan-out path. A host supplies identity however it can: a
532
+ * durable tag minted at accept (Cloudflare), a registry key, a `WeakMap`.
533
+ *
534
+ * Must answer consistently for the same socket within a wake AND across a
535
+ * recycle, since the engine uses it to reassociate a rehydrated socket with
536
+ * its subscription state. Callers outside the O(subscribers) loops are the
537
+ * intended consumers; do not reach for this per socket per frame.
538
+ *
539
+ * **A socket this host never {@link SocketHost.accept}ed** (a foreign socket
540
+ * the runtime hands back — a whisper sender in another pool, a relay peer)
541
+ * is outside that "own socket" contract, but a host must still pick ONE
542
+ * consistent answer for it, never a fresh value per call:
543
+ *
544
+ * - Throw, when a `SocketHandle` from this host can *only* ever originate
545
+ * from this host's own `accept`/`recycle` — an unrecognized handle then
546
+ * means caller error (a handle crossed from a different host instance),
547
+ * and failing loud beats returning a plausible-looking wrong id. This is
548
+ * the reference host's choice, since its `SocketHandle` is an opaque
549
+ * object it mints itself.
550
+ * - Mint and cache a read-only fallback id, when `SocketHandle` doubles as
551
+ * the provider's own transport socket (see {@link SocketHandle}'s "not a
552
+ * wrapper" rationale) — a foreign-but-genuine socket then structurally
553
+ * satisfies the type without ever going through `accept`, so throwing
554
+ * would fire on legitimate traffic, not just caller error. This is the
555
+ * Cloudflare host's choice: it caches into a separate map from its
556
+ * accept-time ownership evidence, so an `idFor` lookup can never promote
557
+ * a socket into "ours" for {@link SocketHost.handleFor}.
558
+ *
559
+ * Either is a valid implementation as long as it is consistent: what is not
560
+ * valid is minting a new id on every call for a socket the host does not
561
+ * recognize, which defeats the "same string for the same socket" property
562
+ * every caller of `idFor` — owned or not — depends on.
563
+ */
564
+ idFor: (socket: SocketHandle) => string;
565
+ /**
566
+ * Remove a tag from a live socket — all of them when `tag` is omitted.
567
+ * Optional, and only meaningful alongside {@link SocketHost.setTag}: a
568
+ * host that cannot retag a live socket must omit both.
569
+ */
570
+ removeTag?: (socket: SocketHandle, tag?: string) => void;
571
+ /**
572
+ * Tag a live socket after it was accepted. Presence of this method is the
573
+ * host's declaration that tags are *mutable*; it is not what makes tagged
574
+ * fan-out work, since accept-time tags are mandatory for every host. Omit
575
+ * it (rather than supplying a no-op) when the host's tags are frozen at
576
+ * accept, so callers re-accept instead of silently losing the retag.
577
+ */
578
+ setTag?: (socket: SocketHandle, tag: string) => void;
579
+ }
580
+ 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, ShardSqlCursor as k, ShardSqlExec as l, ShardStub as m, SocketHandle as n, SocketHost as o, SqlRow as p, resolveShard as r };