@lunora/do 1.0.0-alpha.13 → 1.0.0-alpha.15

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.
@@ -1,22 +1,25 @@
1
1
  import { drizzle } from 'drizzle-orm/durable-sqlite';
2
+ import { e as encodeWire, a as awaitWsDrain, t as trySendFrame, d as decodeWire, s as subscriptionListDeltas, b as sendDeltaFrames } from './subscription-delivery-Z6nXHvQN.mjs';
2
3
  import { parseExportShardArgs, parseImportShardArgs } from './exportShardRows-DZEhUeyI.mjs';
3
4
  import { recordAuthEvent, readAuthMetrics } from './AUTH_METRICS_BUCKETS_TABLE-CiHHYeJi.mjs';
4
5
  import { DATA_MIGRATION_STATE_TABLE, readMigrationStatus } from './DATA_MIGRATION_STATE_TABLE-PTtTiQ7U.mjs';
5
6
  import { SCAN_DEP, createDependencyTracker, tableFromDepKey } from './SCAN_DEP-DLJF8dsj.mjs';
6
7
  import { readFunctionMetricsTotals, readFunctionMetricIndexHits, recordFunctionMetric, mergeScanAttribution, readFunctionMetrics, readFunctionMetricBuckets } from './FUNCTION_METRICS_BUCKETS_TABLE-UDNVD7FS.mjs';
7
- import { ADMIN_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, selectMatchingIds, ADMIN_FUNCTIONS, findStorageReferences, listTables, summarizeSubscriptions, readTablePage, facetColumn, FLAGS_FUNCTION_PREFIX, MAX_PAGE_SIZE } from './ADMIN_FUNCTIONS-D_UiYJFk.mjs';
8
+ import { createFanoutCounters, ADMIN_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, selectMatchingIds, ADMIN_FUNCTIONS, findStorageReferences, listTables, summarizeSubscriptions, summarizeFanoutTopics, readTablePage, facetColumn, FLAGS_FUNCTION_PREFIX, recordFanoutPass, MAX_PAGE_SIZE } from './ADMIN_FUNCTIONS-fMl1vyt1.mjs';
8
9
  import { LogBuffer } from './LogBuffer-B_Ezju_N.mjs';
9
10
  import { recordCapturedMail, clearCapturedMail, readCapturedMail, MAIL_TABLE } from './MAIL_RETENTION-CPpgl-dX.mjs';
10
11
  import { readBookmark, armRestore } from './armRestore-BJk53Ro8.mjs';
11
- import { ReactiveCache, reactiveCacheKey } from './ReactiveCache-1hDydFyv.mjs';
12
+ import { ReactiveCache, reactiveCacheKey } from './ReactiveCache-BYlSGY0N.mjs';
13
+ import { stableStringify } from './stableStringify-MydiuScU.mjs';
12
14
  import { redact, standardRules } from '@visulima/redact';
13
15
  import { i as isDevEnvironment, c as buildSettings, b as buildSecurityAudit } from './security-audit-CucgBice.mjs';
14
16
  import { runReadonlySql } from './MAX_SQL_ROWS-dDcFE1YZ.mjs';
15
- import { trySendFrame, subscriptionListDeltas, sendDeltaFrames } from './subscriptionListDeltas-ce84gpwL.mjs';
16
17
  import { ConflictError } from './ConflictError-C0STs6bU.mjs';
17
18
  import { e as deleteGlobalShapeSnapshotsForConnection, g as readIdempotent, h as writeIdempotent, t as trimIdempotent, r as readClientWatermark, m as migrateClientWatermark, c as advanceClientWatermark, d as deleteGlobalShapeSnapshot, f as readGlobalShapeSnapshot, w as writeGlobalShapeSnapshot } from './ctx-db-idempotency-BdcNpvY4.mjs';
18
19
  import { CDC_LOG_TABLE, readCdcChanges, readCdcCursor, readCdcEpoch, minCdcSeq, bumpCdcEpoch } from './CDC_LOG_TABLE-DSycmnDf.mjs';
19
- import { s as selectShapeMemberIds, a as selectShapeRows } from './ctx-db-shapes-DVoeZpo-.mjs';
20
+ import { a as selectShapeMemberIds, s as selectShapeRows } from './ctx-db-shapes-BQapRFjB.mjs';
21
+
22
+ const MAX_BATCH_ENTRIES = 500;
20
23
 
21
24
  const AUDIT_LOG_TABLE = "__lunora_audit__";
22
25
  const AUDIT_LOG_RETENTION = 1e3;
@@ -78,6 +81,38 @@ const readAuditLog = (sql, options = {}) => {
78
81
  });
79
82
  };
80
83
 
84
+ const SHARED_BATCH_HEADERS = [
85
+ "x-lunora-userid",
86
+ "x-lunora-identity",
87
+ "x-d1-bookmark",
88
+ "x-lunora-client-ip",
89
+ "x-lunora-system",
90
+ "x-lunora-shard-binding"
91
+ ];
92
+ const buildBatchEntryRequest = (batchRequest, entry) => {
93
+ const headers = new Headers({ "content-type": "application/json" });
94
+ for (const name of SHARED_BATCH_HEADERS) {
95
+ const value = batchRequest.headers.get(name);
96
+ if (value !== null) {
97
+ headers.set(name, value);
98
+ }
99
+ }
100
+ if (entry.mutationId !== void 0) {
101
+ headers.set("x-lunora-mutation-id", entry.mutationId);
102
+ }
103
+ if (entry.clientId !== void 0) {
104
+ headers.set("x-lunora-client-id", entry.clientId);
105
+ }
106
+ if (entry.clientSeq !== void 0) {
107
+ headers.set("x-lunora-client-seq", String(entry.clientSeq));
108
+ }
109
+ return new Request("https://shard.internal/rpc", {
110
+ body: JSON.stringify({ args: entry.args ?? {}, functionPath: entry.functionPath }),
111
+ headers,
112
+ method: "POST"
113
+ });
114
+ };
115
+
81
116
  const QUERY_METRICS_TABLE = "__lunora_metrics_queries";
82
117
  const QUERY_METRICS_MAX_SQL_LEN = 512;
83
118
  const QUERY_METRICS_MAX_STATEMENTS = 500;
@@ -144,6 +179,714 @@ const readQueryMetrics = (sql) => {
144
179
  });
145
180
  };
146
181
 
182
+ const RELAY_NAME_INFIX = "::relay::";
183
+ const relayName = (ownerKey, index) => `${ownerKey}${RELAY_NAME_INFIX}${String(index)}`;
184
+ const parseRelayName = (name) => {
185
+ const at = name.lastIndexOf(RELAY_NAME_INFIX);
186
+ if (at === -1) {
187
+ return void 0;
188
+ }
189
+ const ownerKey = name.slice(0, at);
190
+ const indexText = name.slice(at + RELAY_NAME_INFIX.length);
191
+ const relayIndex = Number(indexText);
192
+ if (ownerKey.length === 0 || !Number.isInteger(relayIndex) || relayIndex < 0 || String(relayIndex) !== indexText) {
193
+ return void 0;
194
+ }
195
+ return { ownerKey, relayIndex };
196
+ };
197
+
198
+ const shapeRoutingKey = (name, args) => stableStringify({ args: args ?? {}, name });
199
+ const DEFAULT_PROMOTION_THRESHOLDS = { tUp: 8e3 };
200
+
201
+ const projectColumns = (document_, columns) => {
202
+ if (!columns) {
203
+ return document_;
204
+ }
205
+ const projected = /* @__PURE__ */ Object.create(null);
206
+ for (const key of ["_id", "_creationTime", ...columns]) {
207
+ if (Object.hasOwn(document_, key)) {
208
+ projected[key] = document_[key];
209
+ }
210
+ }
211
+ return projected;
212
+ };
213
+ const diffGlobalMembership = (rows, previous, options) => {
214
+ const { columns, table } = options;
215
+ const next = /* @__PURE__ */ new Map();
216
+ const rowsPatch = [];
217
+ for (const { doc, id } of rows) {
218
+ const value = projectColumns(doc, columns);
219
+ const json = JSON.stringify(encodeWire(value));
220
+ next.set(id, json);
221
+ const before = previous.get(id);
222
+ if (before === void 0) {
223
+ rowsPatch.push({ key: id, op: "insert", table, value });
224
+ } else if (before !== json) {
225
+ rowsPatch.push({ key: id, op: "update", table, value });
226
+ }
227
+ }
228
+ for (const id of previous.keys()) {
229
+ if (!next.has(id)) {
230
+ rowsPatch.push({ key: id, op: "delete", table });
231
+ }
232
+ }
233
+ return { next, rowsPatch };
234
+ };
235
+ const buildPokeFrames = (parts, meta) => {
236
+ const { baseCheckpoint, checkpoint, epoch, lastMutationId, pokeId } = meta;
237
+ const frames = [JSON.stringify({ baseCheckpoint, epoch, pokeId, type: "pokeStart" })];
238
+ for (const part of parts) {
239
+ const rowsPatch = part.rowsPatch.map((op) => op.value === void 0 ? op : { ...op, value: encodeWire(op.value) });
240
+ frames.push(
241
+ JSON.stringify({
242
+ pokeId,
243
+ rowsPatch,
244
+ shapeId: part.shapeId,
245
+ type: "pokePart",
246
+ ...lastMutationId === void 0 ? {} : { lastMutationId }
247
+ })
248
+ );
249
+ }
250
+ frames.push(JSON.stringify({ checkpoint, epoch, pokeId, type: "pokeEnd" }));
251
+ return frames;
252
+ };
253
+
254
+ const DEFAULT_RELAY_FAN = 2;
255
+ const DEFAULT_MAX_RELAYS = 8;
256
+ const envPositiveInt = (env, key, fallback) => {
257
+ const raw = env?.[key];
258
+ let parsed = Number.NaN;
259
+ if (typeof raw === "string") {
260
+ parsed = Number.parseInt(raw, 10);
261
+ } else if (typeof raw === "number") {
262
+ parsed = raw;
263
+ }
264
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
265
+ };
266
+ const RELAY_MULTICAST_IDENTITY = {};
267
+ const assertNeverFrame = (frame) => {
268
+ throw new Error(`unhandled relay frame: ${JSON.stringify(frame)}`);
269
+ };
270
+ const asRelayNamespace = (value) => {
271
+ if (value === null || typeof value !== "object") {
272
+ return void 0;
273
+ }
274
+ const candidate = value;
275
+ return typeof candidate.idFromName === "function" && typeof candidate.get === "function" ? candidate : void 0;
276
+ };
277
+ const jsonRelayResponse = (body) => Response.json(body, { headers: { "content-type": "application/json" } });
278
+ const noContent = () => new Response(null, { status: 204 });
279
+ class RelayLink {
280
+ constructor(host, roleId) {
281
+ this.host = host;
282
+ this.roleId = roleId;
283
+ }
284
+ host;
285
+ roleId;
286
+ /**
287
+ * Serve the internal `/_lunora/relay` control channel: parse the frame, then
288
+ * dispatch to the role hooks. One exhaustive switch means a new frame type added
289
+ * to `relay.ts` without a case here is a COMPILE error, not a runtime mis-route.
290
+ */
291
+ async handleControl(request) {
292
+ let message;
293
+ try {
294
+ message = await request.json();
295
+ } catch {
296
+ return new Response("bad request", { status: 400 });
297
+ }
298
+ switch (message.type) {
299
+ case "relay_attach": {
300
+ this.onAttach(message.relayIndex);
301
+ return noContent();
302
+ }
303
+ case "relay_detach": {
304
+ this.onDetach(message.relayIndex);
305
+ return noContent();
306
+ }
307
+ case "relay_frame": {
308
+ this.host.deliverWhisperLocal(message.topic, message.frame, void 0);
309
+ await this.onWhisperFrame(message);
310
+ return noContent();
311
+ }
312
+ case "relay_shape_poke": {
313
+ const iterated = this.host.getWebSockets().length;
314
+ const startMs = Date.now();
315
+ const delivered = this.onShapePoke(message);
316
+ this.host.recordShapePokeFanout(iterated, delivered, Date.now() - startMs);
317
+ return noContent();
318
+ }
319
+ case "relay_shape_subscribe": {
320
+ return jsonRelayResponse(this.onShapeSubscribe(message));
321
+ }
322
+ default: {
323
+ return assertNeverFrame(message);
324
+ }
325
+ }
326
+ }
327
+ /** The hard cap on relays per shard (`LUNORA_MAX_RELAYS`) — a deployment constant the runtime surfaces in Studio as the cost ceiling. */
328
+ maxRelays() {
329
+ return envPositiveInt(this.host.env(), "LUNORA_MAX_RELAYS", DEFAULT_MAX_RELAYS);
330
+ }
331
+ /** Whether this DO can currently address its siblings (a namespace binding has been learned) — the relay tier is inert in single-DO mode. */
332
+ canAddressSiblings() {
333
+ return this.relayNamespace() !== void 0;
334
+ }
335
+ /** Resolve this DO's own namespace binding so it can address sibling owners/relays, or `undefined` when unknown. */
336
+ relayNamespace() {
337
+ const binding = this.host.shardBinding();
338
+ if (binding === void 0) {
339
+ return void 0;
340
+ }
341
+ return asRelayNamespace(this.host.env()?.[binding]);
342
+ }
343
+ /** POST a control frame to a sibling by name, fire-and-forget. Best-effort: a transient cross-DO failure drops the frame rather than throwing into the handler. */
344
+ async postRelayMessage(targetName, message) {
345
+ await this.requestRelayMessage(targetName, message);
346
+ }
347
+ /**
348
+ * POST a control frame to a sibling by name and return its response (the shape-seed
349
+ * path needs the owner's frames back). Best-effort: a transient cross-DO failure
350
+ * returns `undefined` rather than throwing.
351
+ * @returns the sibling's response, or `undefined` when it can't be reached
352
+ */
353
+ async requestRelayMessage(targetName, message) {
354
+ const namespace = this.relayNamespace();
355
+ if (namespace === void 0) {
356
+ return void 0;
357
+ }
358
+ const stub = typeof namespace.getByName === "function" ? namespace.getByName(targetName) : namespace.get(namespace.idFromName(targetName));
359
+ try {
360
+ return await stub.fetch("https://relay.internal/_lunora/relay", {
361
+ body: JSON.stringify(message),
362
+ headers: { "content-type": "application/json", "x-lunora-shard-binding": this.host.shardBinding() ?? "" },
363
+ method: "POST"
364
+ });
365
+ } catch {
366
+ return void 0;
367
+ }
368
+ }
369
+ }
370
+ class OwnerRelay extends RelayLink {
371
+ /** Memoized RLS-uniform verdict per `(name, args)` shape — uniformity is stable, so the gate probe runs at most once per distinct shape. */
372
+ shapeUniformCache = /* @__PURE__ */ new Map();
373
+ /** Active relay indices, hydrated once from `__lunora_relays` and cached for the synchronous forward path. */
374
+ relaySetCache;
375
+ /** Relay-uniform shapes a relay has subscribers for, keyed by `(name, args)`. `cursor` is the cohort frontier the owner has multicast deltas up to. */
376
+ relayShapeRegistry = /* @__PURE__ */ new Map();
377
+ /** NON-uniform (identity-scoped) relay shapes, one entry per relay socket, keyed `relayIndex:connectionId:subId`. Each is served live by a per-socket proxy poke. */
378
+ relayShapeProxies = /* @__PURE__ */ new Map();
379
+ constructor(host, ownerKey) {
380
+ super(host, { ownerKey });
381
+ }
382
+ async forwardWhisper(topic, frame) {
383
+ if (!this.canAddressSiblings()) {
384
+ return;
385
+ }
386
+ const relays = this.ownerRelaySet();
387
+ if (relays.size === 0) {
388
+ return;
389
+ }
390
+ await Promise.all([...relays].map((index) => this.postRelayMessage(relayName(this.roleId.ownerKey, index), { frame, topic, type: "relay_frame" })));
391
+ }
392
+ async onFlush(changed, frameCursor) {
393
+ await Promise.all([this.multicastShapePokes(changed, frameCursor), this.proxyShapePokes(changed, frameCursor)]);
394
+ }
395
+ // eslint-disable-next-line class-methods-use-this -- role hook: an owner serves its own shape subscribers locally, never through a relay
396
+ seedRelayShape() {
397
+ return Promise.resolve(void 0);
398
+ }
399
+ // eslint-disable-next-line class-methods-use-this -- role hook: only a relay announces
400
+ announce() {
401
+ return Promise.resolve();
402
+ }
403
+ // eslint-disable-next-line class-methods-use-this -- role hook: only a relay drains
404
+ announceDrain() {
405
+ return Promise.resolve();
406
+ }
407
+ /**
408
+ * How many relays the runtime should spread new connections across for this shard
409
+ * (plan 075 Phase 2). `0` keeps every connection on the owner. The owner promotes
410
+ * once its live socket count crosses `LUNORA_RELAY_THRESHOLD`, fanning to a fixed
411
+ * `LUNORA_RELAY_FAN` (capped by `LUNORA_MAX_RELAYS`, the cost ceiling).
412
+ */
413
+ relayCount() {
414
+ const threshold = envPositiveInt(this.host.env(), "LUNORA_RELAY_THRESHOLD", DEFAULT_PROMOTION_THRESHOLDS.tUp);
415
+ if (this.host.getWebSockets().length < threshold) {
416
+ return 0;
417
+ }
418
+ const maxRelays = envPositiveInt(this.host.env(), "LUNORA_MAX_RELAYS", DEFAULT_MAX_RELAYS);
419
+ const fan = envPositiveInt(this.host.env(), "LUNORA_RELAY_FAN", DEFAULT_RELAY_FAN);
420
+ return Math.min(maxRelays, Math.max(1, fan));
421
+ }
422
+ /**
423
+ * The RLS-uniform gate (plan 075 Phase 3, review-hardened): whether a reactive
424
+ * shape may be relay-multicast — i.e. one delta is correct for **every**
425
+ * subscriber. Fail-closed on four grounds — a static RLS read-policy guard, the
426
+ * anonymous multicast identity as the probe base, two `Proxy`-backed probes that
427
+ * yield a distinct value for ANY accessed claim (so any claim a `where` reads —
428
+ * even a custom one outside `rls()` — diverges), and a wholesale-copy backstop.
429
+ * Cached per `(name, args)`, whose uniformity is stable.
430
+ */
431
+ isShapeRelayUniform(name, args) {
432
+ const cacheKey = shapeRoutingKey(name, args);
433
+ const cached = this.shapeUniformCache.get(cacheKey);
434
+ if (cached !== void 0) {
435
+ return cached;
436
+ }
437
+ const uniform = this.probeShapeRelayUniform(name, args);
438
+ this.shapeUniformCache.set(cacheKey, uniform);
439
+ return uniform;
440
+ }
441
+ onAttach(index) {
442
+ this.addRelayToSet(index);
443
+ }
444
+ onDetach(index) {
445
+ this.removeRelayFromSet(index);
446
+ }
447
+ async onWhisperFrame(message) {
448
+ await Promise.all(
449
+ [...this.ownerRelaySet()].filter((index) => index !== message.originRelay).map(
450
+ (index) => this.postRelayMessage(relayName(this.roleId.ownerKey, index), { frame: message.frame, topic: message.topic, type: "relay_frame" })
451
+ )
452
+ );
453
+ }
454
+ onShapeSubscribe(message) {
455
+ return this.buildShapeSeedFrames(message);
456
+ }
457
+ // eslint-disable-next-line class-methods-use-this -- role hook: an owner doesn't receive multicast pokes (it sends them)
458
+ onShapePoke() {
459
+ return 0;
460
+ }
461
+ /**
462
+ * Owner side (slice B.2): for every registered relay-uniform shape whose table
463
+ * changed this flush, compute the membership diff ONCE over `(cohort cursor,
464
+ * frameCursor]` and multicast the `rowsPatch` to every relay. Advances the cohort
465
+ * cursor synchronously (before any await) so a seed interleaving during the
466
+ * multicast registers at `frameCursor` and is skipped by this in-flight poke.
467
+ */
468
+ async multicastShapePokes(changed, frameCursor) {
469
+ if (this.relayShapeRegistry.size === 0) {
470
+ return;
471
+ }
472
+ const relays = this.ownerRelaySet();
473
+ if (relays.size === 0) {
474
+ return;
475
+ }
476
+ const epoch = this.host.currentCdcEpoch();
477
+ const sends = [];
478
+ for (const entry of this.relayShapeRegistry.values()) {
479
+ let resolved;
480
+ try {
481
+ resolved = this.host.resolveShape(entry.name, entry.args, RELAY_MULTICAST_IDENTITY);
482
+ } catch {
483
+ continue;
484
+ }
485
+ if (resolved === void 0 || resolved.global === true || !changed.has(resolved.table)) {
486
+ continue;
487
+ }
488
+ const fromCursor = entry.cursor;
489
+ const rowsPatch = this.host.buildShapeDiff(resolved, fromCursor, frameCursor);
490
+ if (rowsPatch.length === 0) {
491
+ continue;
492
+ }
493
+ entry.cursor = frameCursor;
494
+ const poke = {
495
+ args: entry.args,
496
+ checkpoint: frameCursor,
497
+ epoch,
498
+ fromCursor,
499
+ name: entry.name,
500
+ rowsPatch,
501
+ type: "relay_shape_poke"
502
+ };
503
+ for (const index of relays) {
504
+ sends.push(this.postRelayMessage(relayName(this.roleId.ownerKey, index), poke));
505
+ }
506
+ }
507
+ await Promise.all(sends);
508
+ }
509
+ /**
510
+ * Owner side (review MEDIUM-3): for every NON-uniform relay-shape proxy whose
511
+ * table changed this flush, compute that one subscriber's diff over `(entry.cursor,
512
+ * frameCursor]` UNDER ITS OWN forwarded identity (RLS-correct) and deliver a
513
+ * `targetConnectionId`-addressed poke to just that socket's relay. Each entry
514
+ * tracks its own cursor — the diffs are identity-specific, no cohort sharing.
515
+ */
516
+ async proxyShapePokes(changed, frameCursor) {
517
+ if (this.relayShapeProxies.size === 0) {
518
+ return;
519
+ }
520
+ const epoch = this.host.currentCdcEpoch();
521
+ const sends = [];
522
+ for (const entry of this.relayShapeProxies.values()) {
523
+ let resolved;
524
+ try {
525
+ resolved = this.host.resolveShape(entry.name, entry.args, entry.identity);
526
+ } catch {
527
+ continue;
528
+ }
529
+ if (resolved === void 0 || resolved.global === true || !changed.has(resolved.table)) {
530
+ continue;
531
+ }
532
+ const fromCursor = entry.cursor;
533
+ const rowsPatch = this.host.buildShapeDiff(resolved, fromCursor, frameCursor);
534
+ if (rowsPatch.length === 0) {
535
+ continue;
536
+ }
537
+ entry.cursor = frameCursor;
538
+ const poke = {
539
+ args: entry.args,
540
+ checkpoint: frameCursor,
541
+ epoch,
542
+ fromCursor,
543
+ name: entry.name,
544
+ rowsPatch,
545
+ targetConnectionId: entry.connectionId,
546
+ type: "relay_shape_poke"
547
+ };
548
+ sends.push(this.postRelayMessage(relayName(this.roleId.ownerKey, entry.relayIndex), poke));
549
+ }
550
+ await Promise.all(sends);
551
+ }
552
+ /**
553
+ * Serialize a shape's seed poke frames for a relay to deliver verbatim. Resolves
554
+ * under the forwarded socket identity (so RLS applies exactly as for a local
555
+ * subscribe), self-heals the relay set, registers the shape for live updates
556
+ * (cohort multicast when uniform, per-socket proxy when not), and stamps the
557
+ * relay's cohort memo at the registry FRONTIER (not the global cursor) so a late
558
+ * joiner is never stranded. `lastMutationId` is omitted (relayed sockets are
559
+ * owner-served for custom mutators).
560
+ * @returns the serialized frames + the cohort-memo cursor, or an error
561
+ */
562
+ buildShapeSeedFrames(request) {
563
+ const identity = { identity: request.identity, userId: request.userId };
564
+ let resolved;
565
+ try {
566
+ resolved = this.host.resolveShape(request.name, request.args, identity);
567
+ } catch (error) {
568
+ return { error: { code: "SHAPE_RESOLVE_FAILED", message: error instanceof Error ? error.message : "shape resolve failed" } };
569
+ }
570
+ if (resolved === void 0 || resolved.global === true) {
571
+ return { error: { code: "SHAPE_NOT_FOUND", message: `shape not relayable: ${request.name}` } };
572
+ }
573
+ if (request.relayIndex !== void 0) {
574
+ this.addRelayToSet(request.relayIndex);
575
+ }
576
+ const { baseCheckpoint, cursor, epoch, rowsPatch } = this.host.computeOpLogShapeSeed(
577
+ { args: request.args, name: request.name, sinceEpoch: request.sinceEpoch, sinceSeq: request.sinceSeq },
578
+ resolved
579
+ );
580
+ let cohortCursor = cursor;
581
+ if (this.isShapeRelayUniform(request.name, request.args)) {
582
+ const routingKey = shapeRoutingKey(request.name, request.args);
583
+ let entry = this.relayShapeRegistry.get(routingKey);
584
+ if (entry === void 0) {
585
+ entry = { args: request.args, cursor, name: request.name };
586
+ this.relayShapeRegistry.set(routingKey, entry);
587
+ }
588
+ cohortCursor = entry.cursor;
589
+ } else if (request.relayIndex !== void 0 && request.connectionId !== void 0) {
590
+ this.relayShapeProxies.set(`${String(request.relayIndex)}:${request.connectionId}:${request.subId}`, {
591
+ args: request.args,
592
+ connectionId: request.connectionId,
593
+ cursor,
594
+ epoch,
595
+ identity,
596
+ name: request.name,
597
+ relayIndex: request.relayIndex,
598
+ subId: request.subId
599
+ });
600
+ }
601
+ const frames = buildPokeFrames([{ rowsPatch, shapeId: request.subId }], {
602
+ baseCheckpoint,
603
+ checkpoint: cursor,
604
+ epoch,
605
+ lastMutationId: void 0,
606
+ pokeId: this.host.nextPokeId()
607
+ });
608
+ return { cursor: cohortCursor, epoch, frames };
609
+ }
610
+ /** Ensure the reserved owner-side relay-set table exists (auto-hidden from the data browser by the `__lunora` prefix). */
611
+ ensureRelayTable() {
612
+ this.host.sql().exec("CREATE TABLE IF NOT EXISTS __lunora_relays (idx INTEGER PRIMARY KEY)");
613
+ }
614
+ /** The owner's active relay indices, hydrated once from `__lunora_relays` and cached for the synchronous forward path. */
615
+ ownerRelaySet() {
616
+ if (this.relaySetCache === void 0) {
617
+ this.ensureRelayTable();
618
+ const rows = this.host.sql().exec("SELECT idx FROM __lunora_relays").toArray();
619
+ this.relaySetCache = new Set(rows.map((row) => Number(row.idx)));
620
+ }
621
+ return this.relaySetCache;
622
+ }
623
+ /** Record a relay as active (idempotent), persisting it so the set survives the owner's hibernation. */
624
+ addRelayToSet(index) {
625
+ this.ensureRelayTable();
626
+ this.host.sql().exec("INSERT OR IGNORE INTO __lunora_relays (idx) VALUES (?)", index);
627
+ this.ownerRelaySet().add(index);
628
+ }
629
+ /** Drop a drained relay from the set and prune its dead per-socket proxy entries; on full drain, clear the (now dead) registry + uniform cache to bound growth. */
630
+ removeRelayFromSet(index) {
631
+ this.ensureRelayTable();
632
+ this.host.sql().exec("DELETE FROM __lunora_relays WHERE idx = ?", index);
633
+ const set = this.ownerRelaySet();
634
+ set.delete(index);
635
+ for (const [key, entry] of this.relayShapeProxies) {
636
+ if (entry.relayIndex === index) {
637
+ this.relayShapeProxies.delete(key);
638
+ }
639
+ }
640
+ if (set.size === 0) {
641
+ this.relayShapeRegistry.clear();
642
+ this.shapeUniformCache.clear();
643
+ }
644
+ }
645
+ /**
646
+ * The one-shot computation behind {@link OwnerRelay.isShapeRelayUniform}, made
647
+ * sound against the cross-identity row-leak (review). Resolves under the anonymous
648
+ * multicast identity (the base) plus two `Proxy`-backed identities that return a
649
+ * distinct value for ANY accessed claim, requires all to agree on table + where +
650
+ * columns, rejects any table with an RLS read policy or a masked projected column,
651
+ * and fails closed if the claims are enumerated (a wholesale copy the proxy can't
652
+ * differentiate).
653
+ */
654
+ probeShapeRelayUniform(name, args) {
655
+ let base;
656
+ try {
657
+ base = this.host.resolveShape(name, args, RELAY_MULTICAST_IDENTITY);
658
+ } catch {
659
+ return false;
660
+ }
661
+ if (base === void 0 || base.global === true) {
662
+ return false;
663
+ }
664
+ if (this.host.rlsMetadata().policies.some((policy) => policy.on === "read" && policy.table === base.table)) {
665
+ return false;
666
+ }
667
+ if (this.shapeColumnsMasked(base.table, base.columns)) {
668
+ return false;
669
+ }
670
+ const baseWhere = stableStringify(base.effectiveWhere);
671
+ const baseColumns = stableStringify(base.columns);
672
+ let enumerated = false;
673
+ const populate = (side) => {
674
+ const backing = { groups: [`grp_${side}`], roles: [side], sub: `__lunora_probe_${side}__` };
675
+ const claims = /* @__PURE__ */ new Proxy(backing, {
676
+ get: (target, key) => {
677
+ if (typeof key === "symbol" || key in target) {
678
+ return Reflect.get(target, key);
679
+ }
680
+ return `${side}:${key}`;
681
+ },
682
+ getOwnPropertyDescriptor: (target, key) => {
683
+ enumerated = true;
684
+ return Reflect.getOwnPropertyDescriptor(target, key);
685
+ },
686
+ has: (target, key) => typeof key === "symbol" ? Reflect.has(target, key) : true,
687
+ ownKeys: (target) => {
688
+ enumerated = true;
689
+ return Reflect.ownKeys(target);
690
+ }
691
+ });
692
+ return { identity: claims, userId: `__lunora_probe_${side}__` };
693
+ };
694
+ const matches = [RELAY_MULTICAST_IDENTITY, populate("a"), populate("b")].every((probe) => {
695
+ let resolved;
696
+ try {
697
+ resolved = this.host.resolveShape(name, args, probe);
698
+ } catch {
699
+ return false;
700
+ }
701
+ return resolved !== void 0 && resolved.global !== true && resolved.table === base.table && stableStringify(resolved.effectiveWhere) === baseWhere && stableStringify(resolved.columns) === baseColumns;
702
+ });
703
+ return matches && !enumerated;
704
+ }
705
+ /** Whether any column the shape projects from `table` is masked — a masked value is identity-dependent, so the shape can't be relay-uniform. */
706
+ shapeColumnsMasked(table, columns) {
707
+ const masked = this.host.maskMetadata().columns.filter((entry) => entry.table === table);
708
+ if (masked.length === 0) {
709
+ return false;
710
+ }
711
+ if (columns === void 0) {
712
+ return true;
713
+ }
714
+ const projected = new Set(columns);
715
+ return masked.some((entry) => projected.has(entry.column));
716
+ }
717
+ }
718
+ class RelayMember extends RelayLink {
719
+ /** `true` once this relay has announced itself to its owner this wake, so a hot socket churn doesn't re-attach on every subscribe. */
720
+ relayAnnounced = false;
721
+ /** Per-socket cohort memo `ws → subId → { cursor, epoch }`: the relay delivers a poke to a socket only while its memo matches the poke's `fromCursor`+`epoch`. */
722
+ shapeRelayMemos = /* @__PURE__ */ new WeakMap();
723
+ constructor(host, ownerKey, relayIndex) {
724
+ super(host, { ownerKey, relayIndex });
725
+ }
726
+ async forwardWhisper(topic, frame) {
727
+ if (!this.canAddressSiblings()) {
728
+ return;
729
+ }
730
+ await this.postRelayMessage(this.roleId.ownerKey, { frame, originRelay: this.roleId.relayIndex, topic, type: "relay_frame" });
731
+ }
732
+ // eslint-disable-next-line class-methods-use-this -- role hook: a relay receives no writes, so it never flushes its own CDC
733
+ onFlush() {
734
+ return Promise.resolve();
735
+ }
736
+ /**
737
+ * Seed a shape held by a socket on this relay by forwarding the request to the
738
+ * owner: the owner resolves under this socket's verified identity and computes the
739
+ * seed frames, which the relay delivers verbatim. Returns a structured error
740
+ * (surfaced as a `shape_subscribe` error) when the owner can't be reached.
741
+ */
742
+ async seedRelayShape(ws, subId, shape, identity) {
743
+ if (!this.canAddressSiblings()) {
744
+ return { code: "RELAY_MISCONFIGURED", message: "relay cannot address its owner" };
745
+ }
746
+ await this.announce();
747
+ const request = {
748
+ args: shape.args ?? {},
749
+ connectionId: this.host.readAttachment(ws).connectionId,
750
+ identity: identity.identity,
751
+ name: shape.name,
752
+ relayIndex: this.roleId.relayIndex,
753
+ sinceEpoch: shape.sinceEpoch,
754
+ sinceSeq: shape.sinceSeq,
755
+ subId,
756
+ type: "relay_shape_subscribe",
757
+ userId: identity.userId
758
+ };
759
+ const response = await this.requestRelayMessage(this.roleId.ownerKey, request);
760
+ if (response === void 0) {
761
+ return { code: "RELAY_SEED_FAILED", message: "owner did not answer the shape seed" };
762
+ }
763
+ let seed;
764
+ try {
765
+ seed = await response.json();
766
+ } catch {
767
+ return { code: "RELAY_SEED_FAILED", message: "malformed shape seed from owner" };
768
+ }
769
+ if (seed.error !== void 0) {
770
+ return seed.error;
771
+ }
772
+ if (seed.frames === void 0) {
773
+ return { code: "RELAY_SEED_FAILED", message: "owner returned no shape frames" };
774
+ }
775
+ await awaitWsDrain(ws);
776
+ for (const frame of seed.frames) {
777
+ trySendFrame(ws, frame);
778
+ }
779
+ this.recordRelayShapeMemo(ws, subId, seed.cursor ?? 0, seed.epoch);
780
+ return "ok";
781
+ }
782
+ /** A relay announces itself to its owner (once per wake) on its first subscriber, retrying on a failed attach so a dropped frame can't strand its sockets (LOW-4). */
783
+ async announce() {
784
+ if (this.relayAnnounced || !this.canAddressSiblings()) {
785
+ return;
786
+ }
787
+ this.relayAnnounced = true;
788
+ const response = await this.requestRelayMessage(this.roleId.ownerKey, { relayIndex: this.roleId.relayIndex, type: "relay_attach" });
789
+ if (!response?.ok) {
790
+ this.relayAnnounced = false;
791
+ }
792
+ }
793
+ /** Once this relay loses its last socket (the `closing` one excluded), detach from the owner and re-arm the announce latch for a future subscriber. */
794
+ async announceDrain(closing) {
795
+ if (!this.canAddressSiblings()) {
796
+ return;
797
+ }
798
+ if (this.host.getWebSockets().some((ws) => ws !== closing)) {
799
+ return;
800
+ }
801
+ this.relayAnnounced = false;
802
+ await this.postRelayMessage(this.roleId.ownerKey, { relayIndex: this.roleId.relayIndex, type: "relay_detach" });
803
+ }
804
+ // eslint-disable-next-line class-methods-use-this -- role hook: a relay never spreads connections (flat single tier)
805
+ relayCount() {
806
+ return 0;
807
+ }
808
+ // eslint-disable-next-line class-methods-use-this -- role hook: the RLS-uniform gate is an owner concern
809
+ isShapeRelayUniform() {
810
+ return false;
811
+ }
812
+ // eslint-disable-next-line class-methods-use-this -- role hook: only an owner tracks a relay set
813
+ onAttach() {
814
+ }
815
+ // eslint-disable-next-line class-methods-use-this -- role hook: only an owner tracks a relay set
816
+ onDetach() {
817
+ }
818
+ // eslint-disable-next-line class-methods-use-this -- role hook: only an owner re-distributes a forwarded whisper
819
+ onWhisperFrame() {
820
+ return Promise.resolve();
821
+ }
822
+ // eslint-disable-next-line class-methods-use-this -- role hook: a relay can't seed (no op-log) — the owner does
823
+ onShapeSubscribe() {
824
+ return { error: { code: "RELAY_CANNOT_SEED", message: "a relay has no op-log to seed from" } };
825
+ }
826
+ onShapePoke(poke) {
827
+ return this.deliverShapePoke(poke);
828
+ }
829
+ /** Record a relay socket's cohort cursor + epoch for `subId` (creating the per-socket map lazily). */
830
+ recordRelayShapeMemo(ws, subId, cursor, epoch) {
831
+ let memos = this.shapeRelayMemos.get(ws);
832
+ if (memos === void 0) {
833
+ memos = /* @__PURE__ */ new Map();
834
+ this.shapeRelayMemos.set(ws, memos);
835
+ }
836
+ memos.set(subId, { cursor, epoch });
837
+ }
838
+ /**
839
+ * Deliver an owner-multicast shape delta to this relay's cohort sockets. A socket
840
+ * receives it only while its memo matches the poke's `fromCursor` AND `epoch` (so a
841
+ * socket that seeded at a different cursor/epoch never double-applies), then
842
+ * advances to `checkpoint`. A targeted (per-socket proxy) poke goes ONLY to its one
843
+ * connection; a cohort multicast goes to every matching socket.
844
+ * @returns the number of sockets delivered to
845
+ */
846
+ deliverShapePoke(poke) {
847
+ const routingKey = shapeRoutingKey(poke.name, poke.args);
848
+ let delivered = 0;
849
+ for (const ws of this.host.getWebSockets()) {
850
+ const attachment = this.host.readAttachment(ws);
851
+ const { shapes } = attachment;
852
+ const memos = this.shapeRelayMemos.get(ws);
853
+ if (shapes === void 0 || memos === void 0) {
854
+ continue;
855
+ }
856
+ if (poke.targetConnectionId !== void 0 && attachment.connectionId !== poke.targetConnectionId) {
857
+ continue;
858
+ }
859
+ for (const [subId, sub] of Object.entries(shapes)) {
860
+ const memo = memos.get(subId);
861
+ if (memo?.cursor !== poke.fromCursor || memo.epoch !== poke.epoch || shapeRoutingKey(sub.name, sub.args) !== routingKey) {
862
+ continue;
863
+ }
864
+ const frames = buildPokeFrames([{ rowsPatch: poke.rowsPatch, shapeId: subId }], {
865
+ baseCheckpoint: void 0,
866
+ checkpoint: poke.checkpoint,
867
+ epoch: poke.epoch,
868
+ lastMutationId: void 0,
869
+ pokeId: this.host.nextPokeId()
870
+ });
871
+ for (const frame of frames) {
872
+ trySendFrame(ws, frame);
873
+ }
874
+ memos.set(subId, { cursor: poke.checkpoint, epoch: poke.epoch });
875
+ delivered += 1;
876
+ }
877
+ }
878
+ return delivered;
879
+ }
880
+ }
881
+ const createRelayLink = (host) => {
882
+ const name = host.doName();
883
+ if (name === void 0) {
884
+ return void 0;
885
+ }
886
+ const parsed = parseRelayName(name);
887
+ return parsed === void 0 ? new OwnerRelay(host, name) : new RelayMember(host, parsed.ownerKey, parsed.relayIndex);
888
+ };
889
+
147
890
  const REQUEST_LOG_TABLE = "__lunora_reqlog__";
148
891
  const REQUEST_LOG_RETENTION = 1e3;
149
892
  const REQUEST_LOG_EVENT_SOURCE = "lunora";
@@ -351,58 +1094,6 @@ const readRequestLog = (sql, options = {}) => {
351
1094
  });
352
1095
  };
353
1096
 
354
- const projectColumns = (document_, columns) => {
355
- if (!columns) {
356
- return document_;
357
- }
358
- const projected = /* @__PURE__ */ Object.create(null);
359
- for (const key of ["_id", "_creationTime", ...columns]) {
360
- if (Object.hasOwn(document_, key)) {
361
- projected[key] = document_[key];
362
- }
363
- }
364
- return projected;
365
- };
366
- const diffGlobalMembership = (rows, previous, options) => {
367
- const { columns, table } = options;
368
- const next = /* @__PURE__ */ new Map();
369
- const rowsPatch = [];
370
- for (const { doc, id } of rows) {
371
- const value = projectColumns(doc, columns);
372
- const json = JSON.stringify(value);
373
- next.set(id, json);
374
- const before = previous.get(id);
375
- if (before === void 0) {
376
- rowsPatch.push({ key: id, op: "insert", table, value });
377
- } else if (before !== json) {
378
- rowsPatch.push({ key: id, op: "update", table, value });
379
- }
380
- }
381
- for (const id of previous.keys()) {
382
- if (!next.has(id)) {
383
- rowsPatch.push({ key: id, op: "delete", table });
384
- }
385
- }
386
- return { next, rowsPatch };
387
- };
388
- const buildPokeFrames = (parts, meta) => {
389
- const { baseCheckpoint, checkpoint, epoch, lastMutationId, pokeId } = meta;
390
- const frames = [JSON.stringify({ baseCheckpoint, epoch, pokeId, type: "pokeStart" })];
391
- for (const part of parts) {
392
- frames.push(
393
- JSON.stringify({
394
- pokeId,
395
- rowsPatch: part.rowsPatch,
396
- shapeId: part.shapeId,
397
- type: "pokePart",
398
- ...lastMutationId === void 0 ? {} : { lastMutationId }
399
- })
400
- );
401
- }
402
- frames.push(JSON.stringify({ checkpoint, epoch, pokeId, type: "pokeEnd" }));
403
- return frames;
404
- };
405
-
406
1097
  const runSocketPool = async (items, processOne, concurrency = 8) => {
407
1098
  let cursor = 0;
408
1099
  const worker = async () => {
@@ -491,19 +1182,6 @@ const IDEMPOTENCY_RETENTION_MS = 864e5;
491
1182
  const IDEMPOTENCY_GC_INTERVAL_MS = 36e5;
492
1183
  const ROOT_SHARD_NAME = "__root__";
493
1184
  const ADMIN_WILDCARD = "*";
494
- const awaitWsDrain = async (ws) => {
495
- let attempts = 0;
496
- while (attempts < 100) {
497
- attempts += 1;
498
- const buffered = ws.bufferedAmount;
499
- if (typeof buffered !== "number" || buffered < 1048576) {
500
- return;
501
- }
502
- await new Promise((resolve) => {
503
- setTimeout(resolve, 20);
504
- });
505
- }
506
- };
507
1185
  const cdcSuffix = (cursor, epoch) => (cursor === void 0 ? "" : `,"cursor":${String(cursor)}`) + (epoch === void 0 ? "" : `,"epoch":${JSON.stringify(epoch)}`);
508
1186
  const setsIntersect = (a, b) => {
509
1187
  const [small, large] = a.size <= b.size ? [a, b] : [b, a];
@@ -1246,6 +1924,35 @@ class ShardDO {
1246
1924
  * (durable aggregation would be a separate, heavier feature).
1247
1925
  */
1248
1926
  metrics = { errors: 0, requests: 0, sinceMs: Date.now() };
1927
+ /**
1928
+ * Running fan-out cost counters surfaced by the
1929
+ * `__lunora_admin__:getFanoutMetrics` RPC — one tally for the reactive
1930
+ * shape-poke path (`pokeShapeSubscribers`) and one for the whisper broadcast
1931
+ * path (`broadcastWhisper`). Each pass records the sockets it iterated (the
1932
+ * O(subscribers) cost) and delivered to. In-memory and reset on
1933
+ * hibernation/restart, sharing `metrics.sinceMs` as the "since this instance
1934
+ * woke" epoch. This is the observability half of plan 075's auto-elastic
1935
+ * relay tier (Phase 1): measure the per-flush fan-out cost so the promotion
1936
+ * threshold is grounded in real numbers, with no behavior change.
1937
+ */
1938
+ fanout = { shapePoke: createFanoutCounters(), whisper: createFanoutCounters() };
1939
+ /**
1940
+ * The runtime's Durable Object namespace binding name (e.g. `"SHARD"`),
1941
+ * forwarded as `x-lunora-shard-binding` on every request so a DO can address
1942
+ * its siblings (`this.env[binding].getByName(...)`) for the relay hub. Absent
1943
+ * in single-DO mode / the unit harness — when absent, the relay tier is inert
1944
+ * and whispers stay shard-local (no behavior change). In-memory; re-learned per
1945
+ * request.
1946
+ */
1947
+ shardBinding;
1948
+ /**
1949
+ * The auto-elastic fan-out relay collaborator (plan 075) — an {@link OwnerRelay}
1950
+ * or {@link RelayMember} chosen ONCE from this DO's name, or `undefined` for an
1951
+ * unnamed (single-DO) DO where the relay tier is inert. All relay state +
1952
+ * transport lives on it, reached back through the {@link RelayHost} adapter, so
1953
+ * owner-only state can never sit next to relay-only state on this class.
1954
+ */
1955
+ relay;
1249
1956
  /**
1250
1957
  * Declared indexes (`table:index`) a query has exercised since this instance
1251
1958
  * woke, stamped by `getCtxDbIndexUseHook`. In-memory and reset on
@@ -1332,6 +2039,29 @@ class ShardDO {
1332
2039
  if (options.reactiveCache) {
1333
2040
  this.reactiveCache = new ReactiveCache(options.reactiveCache);
1334
2041
  }
2042
+ const host = {
2043
+ buildShapeDiff: (resolved, fromCursor, toCursor) => this.buildShapeDiff(this.sql, resolved, fromCursor, toCursor),
2044
+ computeOpLogShapeSeed: (shape, resolved) => this.computeOpLogShapeSeed(shape, resolved),
2045
+ currentCdcEpoch: () => this.currentCdcEpoch(),
2046
+ deliverWhisperLocal: (topic, frame, exclude) => this.deliverWhisperLocal(topic, frame, exclude),
2047
+ doName: () => this.state.id?.name,
2048
+ env: () => this.env,
2049
+ getWebSockets: () => this.state.getWebSockets(),
2050
+ maskMetadata: () => this.maskMetadata(),
2051
+ nextPokeId: () => {
2052
+ this.pokeSequence += 1;
2053
+ return `poke-${String(this.pokeSequence)}`;
2054
+ },
2055
+ readAttachment: (ws) => this.readAttachment(ws),
2056
+ recordShapePokeFanout: (iterated, delivered, elapsedMs) => {
2057
+ this.fanout.shapePoke = recordFanoutPass(this.fanout.shapePoke, iterated, delivered, elapsedMs);
2058
+ },
2059
+ resolveShape: (name, args, identity) => this.resolveShape(name, args, identity),
2060
+ rlsMetadata: () => this.rlsMetadata(),
2061
+ shardBinding: () => this.shardBinding,
2062
+ sql: () => this.sql
2063
+ };
2064
+ this.relay = createRelayLink(host);
1335
2065
  this.armWebSocketKeepalive();
1336
2066
  }
1337
2067
  /** SQLite handle scoped to this Durable Object. */
@@ -1341,8 +2071,10 @@ class ShardDO {
1341
2071
  */
1342
2072
  async fetch(request) {
1343
2073
  const url = new URL(request.url);
1344
- if (request.headers.get("Upgrade") === "websocket") {
1345
- return this.handleWebSocketUpgrade(request);
2074
+ this.shardBinding = request.headers.get("x-lunora-shard-binding") ?? this.shardBinding;
2075
+ const early = await this.routeNonRpc(url, request);
2076
+ if (early !== void 0) {
2077
+ return early;
1346
2078
  }
1347
2079
  if (url.pathname !== "/rpc" || request.method !== "POST") {
1348
2080
  return new Response("Not found", { status: 404 });
@@ -1389,7 +2121,7 @@ class ShardDO {
1389
2121
  if (cached !== void 0) {
1390
2122
  return this.respondFromIdempotencyCache(payload.functionPath, dispatchStartedAt, mutatorClass, cached.value);
1391
2123
  }
1392
- const result = await this.handleRpc(payload.functionPath, payload.args ?? {});
2124
+ const result = await this.handleRpc(payload.functionPath, decodeWire(payload.args ?? {}));
1393
2125
  this.recordPostDispatchBookkeeping(result, mutatorClass);
1394
2126
  if (mutatorClass?.kind === "next") {
1395
2127
  this.advanceClientMutationWatermark();
@@ -1400,7 +2132,7 @@ class ShardDO {
1400
2132
  const tablesWritten = [...this.pendingChangedTables ?? []];
1401
2133
  this.recordRequestLog(payload.functionPath, payload.args ?? {}, durationMs, "ok", tablesWritten);
1402
2134
  this.maybeWarnRootSize();
1403
- const response = this.buildDispatchResponse(mutatorClass, result);
2135
+ const response = this.buildDispatchResponse(mutatorClass, encodeWire(result));
1404
2136
  await this.flushChangedTables();
1405
2137
  return response;
1406
2138
  } catch (error) {
@@ -1520,19 +2252,23 @@ class ShardDO {
1520
2252
  ws.send(JSON.stringify({ id: envelope.id, message: "streams must be public", type: "error" }));
1521
2253
  return;
1522
2254
  }
1523
- this.handleStream(ws, envelope.id, envelope.query.functionPath, envelope.query.args ?? {}).catch(() => {
2255
+ this.handleStream(ws, envelope.id, envelope.query.functionPath, decodeWire(envelope.query.args ?? {})).catch(() => {
1524
2256
  });
1525
2257
  return;
1526
2258
  }
1527
2259
  if (envelope.type === "whisper_subscribe" || envelope.type === "whisper_unsubscribe") {
1528
2260
  if (typeof envelope.topic === "string" && envelope.topic.length > 0) {
1529
- this.setWhisperMembership(ws, envelope.topic, envelope.type === "whisper_subscribe");
2261
+ const join = envelope.type === "whisper_subscribe";
2262
+ this.setWhisperMembership(ws, envelope.topic, join);
2263
+ if (join) {
2264
+ await this.relay?.announce();
2265
+ }
1530
2266
  }
1531
2267
  return;
1532
2268
  }
1533
2269
  if (envelope.type === "whisper") {
1534
2270
  if (typeof envelope.topic === "string" && envelope.topic.length > 0) {
1535
- this.broadcastWhisper(ws, envelope.topic, envelope.data);
2271
+ await this.broadcastWhisper(ws, envelope.topic, envelope.data);
1536
2272
  }
1537
2273
  return;
1538
2274
  }
@@ -1574,6 +2310,7 @@ class ShardDO {
1574
2310
  }
1575
2311
  }
1576
2312
  ws.serializeAttachment?.(void 0);
2313
+ await this.relay?.announceDrain(ws);
1577
2314
  }
1578
2315
  /** Hibernation API: invoked on socket error. */
1579
2316
  // eslint-disable-next-line class-methods-use-this -- Workers hibernation handler: the platform invokes it on the instance; the signature must stay an instance method
@@ -1596,6 +2333,13 @@ class ShardDO {
1596
2333
  this.recordShapeError("shape:poll", error);
1597
2334
  remaining = 1;
1598
2335
  }
2336
+ try {
2337
+ remaining += await this.pollExternalSources();
2338
+ } catch (error) {
2339
+ this.recordShapeError("source:poll", error);
2340
+ remaining += 1;
2341
+ }
2342
+ await this.flushChangedTables();
1599
2343
  if (remaining > 0) {
1600
2344
  await this.scheduleGlobalPoll();
1601
2345
  }
@@ -2322,7 +3066,7 @@ class ShardDO {
2322
3066
  }
2323
3067
  const now = Date.now();
2324
3068
  try {
2325
- writeIdempotent(this.sql, this.currentRequestUserId ?? "", this.currentRequestMutationId, JSON.stringify(result) ?? "null", now);
3069
+ writeIdempotent(this.sql, this.currentRequestUserId ?? "", this.currentRequestMutationId, JSON.stringify(encodeWire(result)), now);
2326
3070
  if (now - this.lastIdempotencyTrimAt > IDEMPOTENCY_GC_INTERVAL_MS) {
2327
3071
  trimIdempotent(this.sql, now - IDEMPOTENCY_RETENTION_MS);
2328
3072
  this.lastIdempotencyTrimAt = now;
@@ -2705,6 +3449,16 @@ class ShardDO {
2705
3449
  resolveShape(_name, _args, _identity) {
2706
3450
  return void 0;
2707
3451
  }
3452
+ /**
3453
+ * The RLS-uniform gate (plan 075 Phase 3): whether a reactive shape may be
3454
+ * relay-multicast — i.e. one delta is correct for **every** subscriber. The owner
3455
+ * decides it (see {@link OwnerRelay.isShapeRelayUniform} — a static RLS read-policy
3456
+ * guard plus claim-exhaustive `Proxy` probes, fail-closed); this thin delegation
3457
+ * is the seam the gate test exercises. A non-owner DO is never relay-uniform.
3458
+ */
3459
+ isShapeRelayUniform(name, args) {
3460
+ return this.relay?.isShapeRelayUniform(name, args) ?? false;
3461
+ }
2708
3462
  /**
2709
3463
  * Read the FULL current membership of a `.global()`-table shape from its D1
2710
3464
  * (or Hyperdrive) backend — the seed/poll source for the latency-tiered
@@ -2725,6 +3479,41 @@ class ShardDO {
2725
3479
  readGlobalShapeRows(_resolved, _identity) {
2726
3480
  return Promise.resolve([]);
2727
3481
  }
3482
+ /**
3483
+ * Poll external-source (`.source(...)`) tables once (plan 077): materialize
3484
+ * each sourced table's freshly-pulled tenant slice into this DO's SQLite. The
3485
+ * base `ShardDO` has no sourced tables, so it returns `0` and the ingest tier
3486
+ * stays dormant — zero behavior change for every existing DO. The codegen
3487
+ * subclass overrides it to, per sourced table, build a `createShardCtxDb`
3488
+ * writer, read the tenant slice from Hyperdrive under this DO's shard key, and
3489
+ * run `runExternalSourceTick` (read local baseline → diff → apply via the
3490
+ * validated CDC writer). Returns the number of sourced tables still being
3491
+ * polled, so the shared poll alarm ({@link ShardDO.alarm}) re-arms while ingest
3492
+ * is active.
3493
+ */
3494
+ // eslint-disable-next-line class-methods-use-this -- base-class override hook: the codegen subclass implements the real Hyperdrive-backed poll
3495
+ pollExternalSources() {
3496
+ return Promise.resolve(0);
3497
+ }
3498
+ /**
3499
+ * Arm the shared poll alarm for external-source ingest (plan 077). The alarm is
3500
+ * shared with the global-shape poll tier; the codegen subclass calls this once
3501
+ * (on construction / first sourced write) so a sourced DO starts its ingest
3502
+ * loop, after which {@link ShardDO.alarm} re-arms itself while
3503
+ * {@link ShardDO.pollExternalSources} reports remaining work. Idempotent; a
3504
+ * no-op when the runtime exposes no `setAlarm` (unit harness).
3505
+ */
3506
+ scheduleSourcePoll() {
3507
+ return this.scheduleGlobalPoll();
3508
+ }
3509
+ /** This DO's shard key (its DO name), or `__root__` for the single-DO default. The `tenantBy` mapper binds it into the source query. */
3510
+ currentShardKey() {
3511
+ return this.state.id?.name ?? ROOT_SHARD_NAME;
3512
+ }
3513
+ /** Record a contained external-source ingest failure (one sourced table's poll) into the log ring without aborting the others. */
3514
+ recordExternalSourceError(table, error) {
3515
+ this.recordShapeError(`source:${table}`, error);
3516
+ }
2728
3517
  /**
2729
3518
  * Look up a streaming-query function and return a thunk that produces the
2730
3519
  * `AsyncIterable&lt;unknown>` when handed an {@link AbortSignal}. The codegen
@@ -3133,11 +3922,74 @@ class ShardDO {
3133
3922
  if (error && typeof error === "object" && error.name === "LunoraError") {
3134
3923
  const lunoraError = error;
3135
3924
  const status = typeof lunoraError.status === "number" ? lunoraError.status : 500;
3136
- return jsonResponse({ error: { code: lunoraError.code ?? "INTERNAL", message: lunoraError.message ?? "internal error" } }, status);
3925
+ const body = {
3926
+ code: lunoraError.code ?? "INTERNAL",
3927
+ message: lunoraError.message ?? "internal error"
3928
+ };
3929
+ if (lunoraError.data !== void 0) {
3930
+ body.data = encodeWire(lunoraError.data);
3931
+ }
3932
+ return jsonResponse({ error: body }, status);
3137
3933
  }
3138
3934
  console.error("[@lunora/do] unhandled RPC error:", error);
3139
3935
  return jsonResponse({ error: { code: "RPC_FAILED", message: "internal error" } }, 500);
3140
3936
  }
3937
+ /**
3938
+ * Batch dispatch (plan 088). Applies each `calls[]` entry through the SAME
3939
+ * single-call `/rpc` path (via a nested `this.fetch`), **sequentially**, so
3940
+ * the per-`(identity, mutationId)` idempotency dedup and the per-client
3941
+ * `__client_watermark` ordering are enforced entry-by-entry exactly as for an
3942
+ * individual call — no duplication of the dispatch core, no reordering.
3943
+ *
3944
+ * Failures are **per-slot, not fail-fast**: an entry that throws (or a
3945
+ * custom-mutator `OUT_OF_ORDER` gap) is captured in its own result slot and
3946
+ * later entries still run. Ordering is still safe — a later same-client
3947
+ * mutator after a gap re-classifies as a gap too (the watermark never
3948
+ * advanced), so it cannot apply out of order; unrelated entries/queries are
3949
+ * independent. The response is `{ results: [{ id, status, body }] }` in
3950
+ * request order; each `body` is the untouched single-call envelope (its
3951
+ * `result` already wire-encoded), so the client demuxes + decodes each
3952
+ * exactly as one call.
3953
+ */
3954
+ async handleBatchRpc(request) {
3955
+ let payload;
3956
+ try {
3957
+ payload = await request.json();
3958
+ } catch {
3959
+ return jsonResponse({ error: { code: "BAD_REQUEST", message: "invalid JSON body" } }, 400);
3960
+ }
3961
+ if (!Array.isArray(payload.calls)) {
3962
+ return jsonResponse({ error: { code: "BAD_REQUEST", message: "batch `calls` must be an array" } }, 400);
3963
+ }
3964
+ if (payload.calls.length > MAX_BATCH_ENTRIES) {
3965
+ return jsonResponse({ error: { code: "BAD_REQUEST", message: `batch exceeds the ${String(MAX_BATCH_ENTRIES)}-call limit` } }, 400);
3966
+ }
3967
+ const results = [];
3968
+ let latestBookmark;
3969
+ for (const raw of payload.calls) {
3970
+ const outcome = await this.dispatchBatchEntry(request, raw);
3971
+ if (outcome.bookmark !== void 0) {
3972
+ latestBookmark = outcome.bookmark;
3973
+ }
3974
+ results.push({ body: outcome.body, id: outcome.id, status: outcome.status });
3975
+ }
3976
+ return jsonResponse({ results }, 200, latestBookmark);
3977
+ }
3978
+ /** Dispatch one batch entry through the single-call `/rpc` path and capture its envelope (plan 088). */
3979
+ async dispatchBatchEntry(batchRequest, entry) {
3980
+ try {
3981
+ const response = await this.fetch(buildBatchEntryRequest(batchRequest, entry));
3982
+ return { body: await response.json(), bookmark: response.headers.get("x-d1-bookmark") ?? void 0, id: entry.id, status: response.status };
3983
+ } catch (error) {
3984
+ const message = error instanceof Error ? error.message : String(error);
3985
+ return {
3986
+ body: { error: { code: "BATCH_ENTRY_FAILED", message } },
3987
+ bookmark: void 0,
3988
+ id: entry?.id,
3989
+ status: 500
3990
+ };
3991
+ }
3992
+ }
3141
3993
  /**
3142
3994
  * Serve a reserved admin-introspection RPC (`__lunora_admin__:*`) for the
3143
3995
  * data browser. Gated by `env.LUNORA_ADMIN_TOKEN`: introspection is
@@ -3748,6 +4600,9 @@ class ShardDO {
3748
4600
  if (functionPath === ADMIN_FUNCTIONS.listSubscriptions) {
3749
4601
  return this.collectSubscriptions();
3750
4602
  }
4603
+ if (functionPath === ADMIN_FUNCTIONS.getFanoutMetrics) {
4604
+ return this.collectFanoutMetrics();
4605
+ }
3751
4606
  if (functionPath === ADMIN_FUNCTIONS.getLogs) {
3752
4607
  return { entries: this.logs.entries() };
3753
4608
  }
@@ -3790,6 +4645,28 @@ class ShardDO {
3790
4645
  collectSubscriptions() {
3791
4646
  return summarizeSubscriptions(this.state.getWebSockets().map((ws) => this.readAttachment(ws)));
3792
4647
  }
4648
+ /**
4649
+ * Assemble the `__lunora_admin__:getFanoutMetrics` payload for the Studio
4650
+ * fan-out observability panel (plan 075 Phase 1). The point-in-time topic
4651
+ * subscriber counts are folded live from each socket's attachment via
4652
+ * {@link summarizeFanoutTopics}; the running per-path cost counters are the
4653
+ * in-memory {@link ShardDO.fanout} tallies, sharing `metrics.sinceMs` as the
4654
+ * "since this instance woke" epoch. Read-only: touches no SQLite and mutates
4655
+ * no socket state.
4656
+ */
4657
+ collectFanoutMetrics() {
4658
+ const summary = summarizeFanoutTopics(this.state.getWebSockets().map((ws) => this.readAttachment(ws)));
4659
+ const relayCount = this.relay?.relayCount() ?? 0;
4660
+ return {
4661
+ ...summary,
4662
+ maxRelays: this.relay?.maxRelays() ?? DEFAULT_MAX_RELAYS,
4663
+ promoted: relayCount > 0,
4664
+ relayCount,
4665
+ shapePoke: this.fanout.shapePoke,
4666
+ sinceMs: this.metrics.sinceMs,
4667
+ whisper: this.fanout.whisper
4668
+ };
4669
+ }
3793
4670
  /** Resolve a `getAuditLog` admin read, parsing the optional `limit`/`sinceSeq` cursor args and ensuring the reserved table first. */
3794
4671
  // eslint-disable-next-line class-methods-use-this -- kept an instance method for symmetry with the other `readAdmin*` resolvers and future per-instance state
3795
4672
  readAdminAuditLog(sql, args) {
@@ -4062,7 +4939,7 @@ class ShardDO {
4062
4939
  break;
4063
4940
  }
4064
4941
  await awaitWsDrain(ws);
4065
- ws.send(JSON.stringify({ data: chunk, id, type: "chunk" }));
4942
+ ws.send(JSON.stringify({ data: encodeWire(chunk), id, type: "chunk" }));
4066
4943
  }
4067
4944
  if (!controller.signal.aborted) {
4068
4945
  ws.send(JSON.stringify({ id, type: "complete" }));
@@ -4145,7 +5022,11 @@ class ShardDO {
4145
5022
  this.pendingRefreshTables = void 0;
4146
5023
  const frameCursor = this.currentCdcCursor();
4147
5024
  const frameEpoch = this.currentCdcEpoch();
4148
- await Promise.all([this.refreshSubscriptions(batch), this.pokeShapeSubscribers(batch, frameCursor, frameEpoch)]);
5025
+ await Promise.all([
5026
+ this.refreshSubscriptions(batch),
5027
+ this.pokeShapeSubscribers(batch, frameCursor, frameEpoch),
5028
+ this.relay?.onFlush(batch, frameCursor ?? 0)
5029
+ ]);
4149
5030
  batch = this.pendingRefreshTables;
4150
5031
  }
4151
5032
  } finally {
@@ -4346,6 +5227,10 @@ class ShardDO {
4346
5227
  async seedShapeSubscription(ws, subId, shape) {
4347
5228
  const attachment = this.readAttachment(ws);
4348
5229
  const identity = { identity: attachment.identity, userId: attachment.userId };
5230
+ const relayed = await this.relay?.seedRelayShape(ws, subId, shape, identity);
5231
+ if (relayed !== void 0) {
5232
+ return relayed;
5233
+ }
4349
5234
  let resolved;
4350
5235
  try {
4351
5236
  resolved = this.resolveShape(shape.name, shape.args ?? {}, identity);
@@ -4378,17 +5263,32 @@ class ShardDO {
4378
5263
  * it to a structured `shape_subscribe` error.
4379
5264
  */
4380
5265
  async seedOpLogShape(ws, subId, shape, resolved) {
5266
+ const { baseCheckpoint, cursor, epoch, rowsPatch } = this.computeOpLogShapeSeed(shape, resolved);
5267
+ await awaitWsDrain(ws);
5268
+ if (this.sendPoke(ws, [{ rowsPatch, shapeId: subId }], cursor, epoch, baseCheckpoint)) {
5269
+ this.recordShapeMemo(ws, subId, cursor);
5270
+ }
5271
+ return "ok";
5272
+ }
5273
+ /**
5274
+ * Compute an op-log shape seed (cursor, epoch, the resume base, and the
5275
+ * membership `rowsPatch`) WITHOUT sending — the shared core of
5276
+ * {@link ShardDO.seedOpLogShape} (sends to a local socket) and the owner relay's
5277
+ * `buildShapeSeedFrames` (serializes the frames for a relay to deliver, plan 075
5278
+ * Phase 3, via the {@link RelayHost} seam). Resume only when CDC is on, the client is on this
5279
+ * epoch, its checkpoint doesn't run ahead of ours, and the log still covers it;
5280
+ * else a full re-seed. A fully-compacted log only proves "nothing missed" when
5281
+ * the client is already at `cursor`.
5282
+ * @returns the cursor/epoch, the resume base (`baseCheckpoint`), and the membership patch
5283
+ */
5284
+ computeOpLogShapeSeed(shape, resolved) {
4381
5285
  const sql = this.sql;
4382
5286
  const cursor = this.currentCdcCursor() ?? 0;
4383
5287
  const epoch = this.currentCdcEpoch();
4384
5288
  const floor = this.cdcEnabled() ? minCdcSeq(sql) : void 0;
4385
5289
  const canResume = this.cdcEnabled() && shape.sinceSeq !== void 0 && shape.sinceEpoch === epoch && shape.sinceSeq <= cursor && (shape.sinceSeq === cursor || floor !== void 0 && floor <= shape.sinceSeq + 1);
4386
5290
  const rowsPatch = canResume && shape.sinceSeq !== void 0 ? this.buildShapeDiff(sql, resolved, shape.sinceSeq, cursor) : this.buildShapeSeed(sql, resolved);
4387
- await awaitWsDrain(ws);
4388
- if (this.sendPoke(ws, [{ rowsPatch, shapeId: subId }], cursor, epoch, canResume ? shape.sinceSeq : void 0)) {
4389
- this.recordShapeMemo(ws, subId, cursor);
4390
- }
4391
- return "ok";
5291
+ return { baseCheckpoint: canResume ? shape.sinceSeq : void 0, cursor, epoch, rowsPatch };
4392
5292
  }
4393
5293
  /**
4394
5294
  * Fan the membership diff of every shape affected by this flush to its
@@ -4405,6 +5305,7 @@ class ShardDO {
4405
5305
  const checkpoint = frameCursor ?? this.currentCdcCursor() ?? 0;
4406
5306
  const sql = this.sql;
4407
5307
  const opRangeCache = /* @__PURE__ */ new Map();
5308
+ let delivered = 0;
4408
5309
  const pokeOne = async (ws) => {
4409
5310
  if (this.isSocketExpired(ws)) {
4410
5311
  this.dropExpiredSocket(ws);
@@ -4424,6 +5325,7 @@ class ShardDO {
4424
5325
  if (parts.length > 0) {
4425
5326
  await awaitWsDrain(ws);
4426
5327
  if (this.sendPoke(ws, parts, checkpoint, frameEpoch, void 0)) {
5328
+ delivered += 1;
4427
5329
  for (const subId of partAdvanced) {
4428
5330
  this.recordShapeMemo(ws, subId, checkpoint);
4429
5331
  }
@@ -4432,7 +5334,9 @@ class ShardDO {
4432
5334
  } catch {
4433
5335
  }
4434
5336
  };
5337
+ const startMs = Date.now();
4435
5338
  await runSocketPool(sockets, pokeOne);
5339
+ this.fanout.shapePoke = recordFanoutPass(this.fanout.shapePoke, sockets.length, delivered, Date.now() - startMs);
4436
5340
  }
4437
5341
  /**
4438
5342
  * Diff every op-log-backed shape a socket holds against this flush, splitting
@@ -4828,7 +5732,7 @@ class ShardDO {
4828
5732
  memos = /* @__PURE__ */ new Map();
4829
5733
  this.subMemos.set(ws, memos);
4830
5734
  }
4831
- memos.set(subId, { lastJson: JSON.stringify(outcome.result ?? null), tables: outcome.tables });
5735
+ memos.set(subId, { lastJson: JSON.stringify(encodeWire(outcome.result ?? null)), tables: outcome.tables });
4832
5736
  }
4833
5737
  /**
4834
5738
  * Memoise `outcome` for `(ws, subId)` and push it to the socket, unless an
@@ -4855,7 +5759,7 @@ class ShardDO {
4855
5759
  this.subMemos.set(ws, memos);
4856
5760
  }
4857
5761
  const cursorSuffix = cdcSuffix(cursor, epoch);
4858
- const json = JSON.stringify(outcome.result ?? null);
5762
+ const json = JSON.stringify(encodeWire(outcome.result ?? null));
4859
5763
  const existing = memos.get(subId);
4860
5764
  if (existing?.lastJson === json) {
4861
5765
  existing.tables = outcome.tables;
@@ -4957,6 +5861,29 @@ class ShardDO {
4957
5861
  }
4958
5862
  setter.call(this.state, new WebSocketRequestResponsePair(WS_KEEPALIVE_PING, WS_KEEPALIVE_PONG));
4959
5863
  }
5864
+ /**
5865
+ * Route the non-RPC requests `fetch` handles before the shard-local RPC
5866
+ * endpoint: a WebSocket upgrade, and the internal `/_lunora/relay` owner↔relay
5867
+ * control channel (never reachable by a client — the runtime forwards only
5868
+ * worker-internal traffic there). Returns `undefined` for an RPC request, which
5869
+ * `fetch` then dispatches.
5870
+ * @returns the routed response, or `undefined` when this is an RPC request
5871
+ */
5872
+ async routeNonRpc(url, request) {
5873
+ if (url.pathname === "/_lunora/relay" && request.method === "POST") {
5874
+ return this.relay ? await this.relay.handleControl(request) : new Response("relay tier inactive", { status: 404 });
5875
+ }
5876
+ if (url.pathname === "/_lunora/route" && request.method === "GET") {
5877
+ return jsonResponse({ relayCount: this.relay?.relayCount() ?? 0 });
5878
+ }
5879
+ if (url.pathname === "/rpc-batch" && request.method === "POST") {
5880
+ return this.handleBatchRpc(request);
5881
+ }
5882
+ if (request.headers.get("Upgrade") === "websocket") {
5883
+ return this.handleWebSocketUpgrade(request);
5884
+ }
5885
+ return void 0;
5886
+ }
4960
5887
  handleWebSocketUpgrade(request) {
4961
5888
  if (!this.isUpgradeAllowed(request)) {
4962
5889
  return new Response("Forbidden", { status: 403 });
@@ -5074,7 +6001,7 @@ class ShardDO {
5074
6001
  * topic name. That matches the AnyCable model (and `from` is unforgeable),
5075
6002
  * but per-topic auth does not exist here; see `whisperSubscribe` on the client.
5076
6003
  */
5077
- broadcastWhisper(sender, topic, data) {
6004
+ async broadcastWhisper(sender, topic, data) {
5078
6005
  if (!this.allowWhisper(sender)) {
5079
6006
  return;
5080
6007
  }
@@ -5085,12 +6012,29 @@ class ShardDO {
5085
6012
  const from = this.readAttachment(sender).userId;
5086
6013
  const fromSuffix = from === void 0 ? "" : `,"from":${JSON.stringify(from)}`;
5087
6014
  const frame = `{"type":"whisper","topic":${JSON.stringify(topic)},"data":${dataJson}${fromSuffix}}`;
6015
+ this.deliverWhisperLocal(topic, frame, sender);
6016
+ await this.relay?.forwardWhisper(topic, frame);
6017
+ }
6018
+ /**
6019
+ * Deliver an already-serialized whisper `frame` to every local socket joined to
6020
+ * `topic`, excluding `exclude` (the sender, or `undefined` for a frame the relay
6021
+ * hub forwarded in — its sender lives on another DO). Records the fan-out pass
6022
+ * for `getFanoutMetrics` (plan 075 Phase 1). Pure delivery — no SQLite, no CDC.
6023
+ * @returns the number of sockets the frame was sent to
6024
+ */
6025
+ deliverWhisperLocal(topic, frame, exclude) {
6026
+ let scanned = 0;
6027
+ let delivered = 0;
5088
6028
  for (const ws of this.state.getWebSockets()) {
5089
- if (ws === sender || this.readAttachment(ws).whispers?.includes(topic) !== true) {
6029
+ scanned += 1;
6030
+ if (ws === exclude || this.readAttachment(ws).whispers?.includes(topic) !== true) {
5090
6031
  continue;
5091
6032
  }
5092
6033
  trySendFrame(ws, frame);
6034
+ delivered += 1;
5093
6035
  }
6036
+ this.fanout.whisper = recordFanoutPass(this.fanout.whisper, scanned, delivered, 0);
6037
+ return delivered;
5094
6038
  }
5095
6039
  // eslint-disable-next-line class-methods-use-this -- cohesive DO instance method grouped with the hibernation/attachment helpers; reads only the socket
5096
6040
  readAttachment(ws) {