@agentshouse/mdruntime 0.1.0-cd50a4607cb7d → 0.1.0-cd76160689224

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.
Files changed (98) hide show
  1. package/AGENTS.md +118 -115
  2. package/dist/acquisition.d.ts +14 -0
  3. package/dist/acquisition.d.ts.map +1 -0
  4. package/dist/acquisition.js +24 -0
  5. package/dist/adapter.d.ts +25 -7
  6. package/dist/adapter.d.ts.map +1 -1
  7. package/dist/bootstrap.d.ts +2 -1
  8. package/dist/bootstrap.d.ts.map +1 -1
  9. package/dist/bootstrap.js +4 -0
  10. package/dist/change-set.d.ts.map +1 -1
  11. package/dist/change-set.js +2 -1
  12. package/dist/chat/kit.d.ts +2 -1
  13. package/dist/chat/kit.d.ts.map +1 -1
  14. package/dist/chat/kit.js +3 -3
  15. package/dist/chat/list.d.ts +2 -1
  16. package/dist/chat/list.d.ts.map +1 -1
  17. package/dist/chat/list.js +4 -3
  18. package/dist/chat/messages.d.ts +1 -0
  19. package/dist/chat/messages.d.ts.map +1 -1
  20. package/dist/chat/messages.js +4 -0
  21. package/dist/chat/provider.d.ts +1 -1
  22. package/dist/chat/provider.d.ts.map +1 -1
  23. package/dist/chat/provider.js +26 -17
  24. package/dist/chat/ready.d.ts +1 -0
  25. package/dist/chat/ready.d.ts.map +1 -1
  26. package/dist/chat/ready.js +2 -2
  27. package/dist/client.d.ts +7 -2
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +12 -2
  30. package/dist/connection.d.ts +18 -3
  31. package/dist/connection.d.ts.map +1 -1
  32. package/dist/connection.js +696 -79
  33. package/dist/conversation.d.ts +3 -0
  34. package/dist/conversation.d.ts.map +1 -1
  35. package/dist/conversation.js +115 -17
  36. package/dist/durability.d.ts +7 -1
  37. package/dist/durability.d.ts.map +1 -1
  38. package/dist/entry.d.ts.map +1 -1
  39. package/dist/entry.js +1 -1
  40. package/dist/host.d.ts +15 -2
  41. package/dist/host.d.ts.map +1 -1
  42. package/dist/host.js +83 -8
  43. package/dist/index.d.ts +7 -5
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +1 -0
  46. package/dist/memory.d.ts +8 -2
  47. package/dist/memory.d.ts.map +1 -1
  48. package/dist/memory.js +26 -6
  49. package/dist/model.d.ts +3 -2
  50. package/dist/model.d.ts.map +1 -1
  51. package/dist/model.js +1 -1
  52. package/dist/outcome.d.ts +8 -2
  53. package/dist/outcome.d.ts.map +1 -1
  54. package/dist/outcome.js +3 -0
  55. package/dist/overlay.d.ts +1 -0
  56. package/dist/overlay.d.ts.map +1 -1
  57. package/dist/overlay.js +27 -3
  58. package/dist/query.d.ts +8 -1
  59. package/dist/query.d.ts.map +1 -1
  60. package/dist/query.js +8 -2
  61. package/dist/reach.d.ts +6 -3
  62. package/dist/reach.d.ts.map +1 -1
  63. package/dist/reach.js +17 -2
  64. package/dist/react.d.ts +4 -1
  65. package/dist/react.d.ts.map +1 -1
  66. package/dist/react.js +6 -4
  67. package/dist/read-side.d.ts.map +1 -1
  68. package/dist/read-side.js +3 -0
  69. package/dist/readiness.d.ts +53 -0
  70. package/dist/readiness.d.ts.map +1 -0
  71. package/dist/readiness.js +33 -0
  72. package/dist/reads.d.ts +11 -0
  73. package/dist/reads.d.ts.map +1 -0
  74. package/dist/reads.js +98 -0
  75. package/dist/refresh.d.ts +4 -4
  76. package/dist/refresh.d.ts.map +1 -1
  77. package/dist/refresh.js +6 -0
  78. package/dist/replica.d.ts.map +1 -1
  79. package/dist/replica.js +11 -2
  80. package/dist/room-draft.d.ts +13 -1
  81. package/dist/room-draft.d.ts.map +1 -1
  82. package/dist/room-draft.js +2 -2
  83. package/dist/rows.d.ts +2 -1
  84. package/dist/rows.d.ts.map +1 -1
  85. package/dist/rows.js +3 -1
  86. package/dist/search.d.ts +14 -6
  87. package/dist/search.d.ts.map +1 -1
  88. package/dist/search.js +33 -12
  89. package/dist/seed.d.ts +16 -0
  90. package/dist/seed.d.ts.map +1 -0
  91. package/dist/seed.js +61 -0
  92. package/dist/store.d.ts +19 -3
  93. package/dist/store.d.ts.map +1 -1
  94. package/dist/store.js +59 -14
  95. package/dist/wire.d.ts +140 -32
  96. package/dist/wire.d.ts.map +1 -1
  97. package/dist/wire.js +49 -31
  98. package/package.json +2 -2
@@ -1,8 +1,9 @@
1
1
  import { attachedModel } from './attached-model.js';
2
2
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
3
- import { observing } from './observation.js';
4
- import { filterOf, identityOf, planOf, } from './query.js';
3
+ import { observing, whole } from './observation.js';
4
+ import { filterOf, identityOf, planOf, queryKey, } from './query.js';
5
5
  import { createReadSide } from './read-side.js';
6
+ import { seededResult } from './seed.js';
6
7
  import { markdownPath, roomOfKey } from './rows.js';
7
8
  import { createSearchIndex, searchPlanOf, } from './search.js';
8
9
  import { bootstrapRoom } from './bootstrap.js';
@@ -13,14 +14,17 @@ import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
13
14
  import { runtimeError } from './failure.js';
14
15
  import { createLog } from './log.js';
15
16
  import { binaryRecord } from './binary.js';
16
- import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
17
+ import { absentBinary, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
17
18
  import { createRecovery } from './recovery.js';
19
+ import { doorReads } from './reads.js';
18
20
  import { refreshRoom } from './refresh.js';
19
21
  import { buildBatch, joined, limitedIn, requeued } from './save.js';
20
22
  import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
21
23
  import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
22
24
  import { same, saving, STARTING, transferring, } from './status.js';
23
25
  import { detectStorageLoss, nextGeneration } from './durability.js';
26
+ import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
27
+ import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
24
28
  import { FencedMap } from './fence.js';
25
29
  import { tally } from './shared.js';
26
30
  import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
@@ -29,6 +33,14 @@ import { beneath, normalize } from './paths.js';
29
33
  import { openRuntimeStore, } from './store.js';
30
34
  import { unconfirmed } from './transfer.js';
31
35
  import { governs, holds, narrowed, planReach, } from './reach.js';
36
+ function roomsAlone(plan) {
37
+ return (plan.reach === null &&
38
+ plan.root.types === null &&
39
+ plan.root.where === null &&
40
+ plan.root.pathPrefix === null &&
41
+ plan.hop === null &&
42
+ plan.backlinks === null);
43
+ }
32
44
  function within(root, path) {
33
45
  return root === '' || beneath(root, path);
34
46
  }
@@ -46,6 +58,10 @@ const OPENING_LANE = 'opening';
46
58
  const CONVERSATION_LANE = 'conversations';
47
59
  const ROOM_READS = 256;
48
60
  const ROOM_ATTEMPTS = 16;
61
+ function operationOf(input) {
62
+ const operation = input?.operation_id;
63
+ return typeof operation === 'string' ? operation : null;
64
+ }
49
65
  const outside = new Map();
50
66
  function outsideOf(roots) {
51
67
  const key = JSON.stringify(roots);
@@ -85,6 +101,8 @@ export function composeRuntime(options) {
85
101
  const replicas = new FencedMap(isFenced);
86
102
  const queuesWithoutReplica = new FencedMap(isFenced);
87
103
  const pendings = new Map();
104
+ const seeds = new Map();
105
+ const seedWatchers = new Map();
88
106
  const roomDrafts = new FencedMap(isFenced);
89
107
  const presentations = new Map();
90
108
  let limits = [];
@@ -97,6 +115,7 @@ export function composeRuntime(options) {
97
115
  const liveModels = new FencedMap(isModelFenced);
98
116
  const invalidModels = new Set();
99
117
  const discovered = new Map();
118
+ const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
100
119
  let roomsListed = false;
101
120
  const scopedRefusals = new Map();
102
121
  const sourceRefusals = new Map();
@@ -133,6 +152,7 @@ export function composeRuntime(options) {
133
152
  let handed = false;
134
153
  const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
135
154
  const repairing = new Set();
155
+ const calling = new Set();
136
156
  const repairable = new Map();
137
157
  let outcomeRetention = null;
138
158
  const answering = new Map();
@@ -166,8 +186,18 @@ export function composeRuntime(options) {
166
186
  const memberListeners = new Set();
167
187
  const prepared = new Set();
168
188
  const projecting = new Set();
169
- const demanded = new Set();
189
+ const unpreparedDemand = new Set();
190
+ const unindexable = new Set();
170
191
  let preparing = false;
192
+ let demand = WHOLE_DEMAND;
193
+ let narrowing = null;
194
+ let targets = {};
195
+ let persistence = options.shell.persistence;
196
+ let capacity = null;
197
+ const paused = new Set();
198
+ let rechecking = null;
199
+ const readinessWatches = new Set();
200
+ let readinessQueued = false;
171
201
  const catching = new Set();
172
202
  const transfers = new FencedMap(isFenced);
173
203
  const conversations = createConversationStore({
@@ -325,15 +355,18 @@ export function composeRuntime(options) {
325
355
  }
326
356
  function emitRooms() {
327
357
  readSide.recover();
358
+ queueReadiness();
328
359
  const value = listed();
329
360
  for (const listener of roomListeners)
330
361
  listener(value);
331
362
  }
332
363
  function emitRefusal() {
364
+ queueReadiness();
333
365
  for (const listener of refusalListeners)
334
366
  listener(refusal);
335
367
  }
336
368
  function emitStorageLoss() {
369
+ queueReadiness();
337
370
  for (const listener of storageLossListeners)
338
371
  listener(storageLoss);
339
372
  }
@@ -350,7 +383,8 @@ export function composeRuntime(options) {
350
383
  function show() {
351
384
  if (closed)
352
385
  return;
353
- const next = status.state === 'synced' && !conversations.current()
386
+ queueReadiness();
387
+ const next = status.state === 'synced' && demand.conversations && !conversations.current()
354
388
  ? { ...status, state: 'syncing' }
355
389
  : status;
356
390
  if (same(shown, next))
@@ -427,13 +461,19 @@ export function composeRuntime(options) {
427
461
  }
428
462
  conversations.changed(mergeChangeSets(entered));
429
463
  readSide.recover();
464
+ queueReadiness();
430
465
  }
431
466
  function unprepared() {
432
- return [...demanded, ...replicas.keys()].find((locator) => !prepared.has(locator));
467
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
468
+ }
469
+ function unbuilt(locator, error) {
470
+ unindexable.add(locator);
471
+ journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
472
+ queueReadiness();
433
473
  }
434
474
  function prepare(locators = []) {
435
475
  for (const locator of locators)
436
- demanded.add(locator);
476
+ unpreparedDemand.add(locator);
437
477
  if (preparing)
438
478
  return;
439
479
  preparing = true;
@@ -443,8 +483,8 @@ export function composeRuntime(options) {
443
483
  await yielded();
444
484
  if (closed)
445
485
  return;
446
- demanded.delete(waiting);
447
- await prepareReplica(waiting);
486
+ unpreparedDemand.delete(waiting);
487
+ await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
448
488
  }
449
489
  }
450
490
  finally {
@@ -462,7 +502,8 @@ export function composeRuntime(options) {
462
502
  return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
463
503
  }
464
504
  function awaited(locator) {
465
- return rooms.has(locator) || !roomsListed;
505
+ return (!roomsListed ||
506
+ (rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
466
507
  }
467
508
  function covered(plan) {
468
509
  const locators = reachedRooms(plan);
@@ -502,9 +543,332 @@ export function composeRuntime(options) {
502
543
  if (waiting.length > 0)
503
544
  prepare(waiting.map(({ room }) => room));
504
545
  }
546
+ function acquires(room) {
547
+ return (demanded(demand, room) ||
548
+ (replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
549
+ queuesWithoutReplica.has(room.locator));
550
+ }
551
+ function acquiredLocators() {
552
+ return [...discovered.values()]
553
+ .filter(acquires)
554
+ .map((room) => room.locator)
555
+ .sort();
556
+ }
557
+ function heldLocators(acquired) {
558
+ return demand.whole ? acquired : [...new Set([...acquired, ...demand.rooms])].sort();
559
+ }
560
+ function heldDemand(acquired) {
561
+ if (demand.whole)
562
+ return null;
563
+ return {
564
+ rooms: heldLocators(acquired.map((room) => room.locator)),
565
+ conversations: demand.conversations,
566
+ };
567
+ }
568
+ function applyDemand(next) {
569
+ const before = heldLocators(acquiredLocators());
570
+ const { whole, conversations: conversed } = demand;
571
+ demand = next;
572
+ const after = heldLocators(acquiredLocators());
573
+ queueReadiness();
574
+ if (!conversed && next.conversations)
575
+ scheduleCurrency();
576
+ if (JSON.stringify(before) === JSON.stringify(after) &&
577
+ whole === next.whole &&
578
+ conversed === next.conversations)
579
+ return;
580
+ journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
581
+ if (options.adapter.roomStream === undefined)
582
+ void sync('unstreamed');
583
+ else if (socketOpen)
584
+ options.adapter.roomStream.reopen();
585
+ }
586
+ function acquire(next) {
587
+ if (closed)
588
+ return;
589
+ if (narrowing !== null)
590
+ clearTimeout(narrowing);
591
+ narrowing = null;
592
+ const widening = wider(next, demand);
593
+ if (!widening && !narrower(next, demand))
594
+ return;
595
+ if (widening || !roomsListed) {
596
+ applyDemand(next);
597
+ return;
598
+ }
599
+ // A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
600
+ narrowing = setTimeout(() => {
601
+ narrowing = null;
602
+ applyDemand(next);
603
+ }, 5_000);
604
+ }
605
+ function requiredRooms(reach) {
606
+ if (reach === null)
607
+ return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
608
+ const named = [...new Set(reach.rooms)].sort();
609
+ if (roomsListed)
610
+ return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
611
+ return { listed: named.every((room) => replicas.has(room)), rooms: named };
612
+ }
613
+ function roomState(locator, search) {
614
+ if (sourceRefusals.has(locator))
615
+ return 'refused';
616
+ const listedRoom = discovered.get(locator);
617
+ if (roomsListed &&
618
+ (listedRoom === undefined ||
619
+ listedRoom.replica.admission !== 'admitted' ||
620
+ unreadable.has(locator)))
621
+ return 'unadmitted';
622
+ if (!replicas.has(locator))
623
+ return 'acquiring';
624
+ if (!prepared.has(locator))
625
+ return 'preparing';
626
+ if (search && !searchIndex.rooms().has(locator))
627
+ return 'indexing';
628
+ return 'complete';
629
+ }
630
+ function conversationCoverage(reach, search) {
631
+ const declared = reach === null || reach.conversations;
632
+ if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
633
+ return 'undeclared';
634
+ if (!conversations.known() || !conversations.current())
635
+ return 'acquiring';
636
+ if (!search)
637
+ return 'complete';
638
+ const unindexed = conversations
639
+ .searchable()
640
+ .filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
641
+ if (unindexed.length === 0)
642
+ return 'complete';
643
+ index([], unindexed);
644
+ return 'indexing';
645
+ }
646
+ function connectionOf() {
647
+ if (socketOpen)
648
+ return 'connected';
649
+ if (status.providerUnreachable)
650
+ return 'unreachable';
651
+ return options.adapter.roomStream === undefined && status.lastCompleted !== null
652
+ ? 'connected'
653
+ : 'connecting';
654
+ }
655
+ function refusedIn(locators) {
656
+ return locators.flatMap((room) => {
657
+ const held = sourceRefusals.get(room);
658
+ if (held === undefined)
659
+ return [];
660
+ const { file, location } = held.fields;
661
+ return [
662
+ {
663
+ room,
664
+ file: typeof file === 'string' ? file : '',
665
+ rule: held.code,
666
+ location: typeof location === 'string' ? location : null,
667
+ },
668
+ ];
669
+ });
670
+ }
671
+ function readinessFor(port, reach) {
672
+ const required = requiredRooms(reach);
673
+ const search = port.search === 'required';
674
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
675
+ const building = (state) => coverage
676
+ .filter((room) => room.state === state && !unindexable.has(room.room))
677
+ .map(({ room }) => room);
678
+ const preparing = building('preparing');
679
+ if (preparing.length > 0)
680
+ prepare(preparing);
681
+ const indexing = building('indexing');
682
+ if (indexing.length > 0)
683
+ index(indexing, []);
684
+ return readinessOf({
685
+ code: port.code,
686
+ coverage: {
687
+ listed: required.listed,
688
+ rooms: coverage,
689
+ conversations: conversationCoverage(reach, search),
690
+ search: search ? 'required' : 'unused',
691
+ },
692
+ persistence,
693
+ connection: connectionOf(),
694
+ house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
695
+ capacity: capacity !== null &&
696
+ coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
697
+ ? capacity
698
+ : port.capacity,
699
+ refused: refusedIn(required.rooms),
700
+ lost: storageLoss !== null,
701
+ established: targets[port.target]?.established === true,
702
+ });
703
+ }
704
+ function establish(name) {
705
+ targets = { ...targets, [name]: { established: true } };
706
+ queueReadiness();
707
+ void durably((local) => local.keepTargets(targets)).catch(() => {
708
+ journal.record(`target record failed ${name}`);
709
+ });
710
+ }
711
+ function deliverReadiness(watch) {
712
+ if (!readinessWatches.has(watch))
713
+ return;
714
+ const readiness = readinessFor(watch.port, watch.reach);
715
+ if (readiness.ready && targets[watch.port.target]?.established !== true) {
716
+ establish(watch.port.target);
717
+ }
718
+ const shown = JSON.stringify(readiness);
719
+ if (shown === watch.last)
720
+ return;
721
+ watch.last = shown;
722
+ watch.deliver(structuredClone(readiness));
723
+ }
724
+ function queueReadiness() {
725
+ if (readinessQueued || closed || readinessWatches.size === 0)
726
+ return;
727
+ readinessQueued = true;
728
+ queueMicrotask(() => {
729
+ readinessQueued = false;
730
+ if (closed)
731
+ return;
732
+ for (const watch of [...readinessWatches])
733
+ deliverReadiness(watch);
734
+ });
735
+ }
736
+ function watchReadiness(port, reach, deliver) {
737
+ const watch = { port, reach, deliver, last: null };
738
+ readinessWatches.add(watch);
739
+ void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
740
+ return () => {
741
+ readinessWatches.delete(watch);
742
+ };
743
+ }
744
+ function layerBytes(layer) {
745
+ let bytes = 0;
746
+ for (const [path, text] of Object.entries(layer.documents))
747
+ bytes += (path.length + text.length) * 2;
748
+ return bytes;
749
+ }
750
+ function grownBy(next, previous) {
751
+ if (previous === undefined)
752
+ return layerBytes(next);
753
+ const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
754
+ return layerBytes({ ...next, documents });
755
+ }
756
+ function exceeded(error) {
757
+ return (typeof error === 'object' &&
758
+ error !== null &&
759
+ error.name === 'QuotaExceededError');
760
+ }
761
+ async function estimated() {
762
+ const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
763
+ return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
764
+ }
765
+ function pause(locator, cause) {
766
+ paused.add(locator);
767
+ capacity = cause;
768
+ journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
769
+ queueReadiness();
770
+ if (rechecking !== null)
771
+ return;
772
+ // A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
773
+ rechecking = setInterval(() => {
774
+ void recheck();
775
+ }, 60_000);
776
+ }
777
+ async function recheck() {
778
+ const held = capacity;
779
+ if (held === null || closed)
780
+ return;
781
+ const available = await estimated().catch(() => null);
782
+ if (available === null || available < (held.needed ?? 0))
783
+ return;
784
+ if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
785
+ return;
786
+ resume();
787
+ }
788
+ function resume() {
789
+ if (rechecking !== null)
790
+ clearInterval(rechecking);
791
+ rechecking = null;
792
+ if (paused.size === 0 && capacity === null)
793
+ return;
794
+ paused.clear();
795
+ capacity = null;
796
+ journal.record('capacity resumed');
797
+ queueReadiness();
798
+ if (!closed && authorization !== undefined)
799
+ void sync('unstreamed');
800
+ }
801
+ async function reclaimed(local) {
802
+ const unreferenced = [...transfers.keys()].filter((locator) => {
803
+ const room = discovered.get(locator);
804
+ return room === undefined || !acquires(room);
805
+ });
806
+ for (const locator of unreferenced) {
807
+ await local.abandon(locator);
808
+ transfers.delete(locator);
809
+ }
810
+ if (unreferenced.length > 0)
811
+ journal.record(`capacity reclaimed ${unreferenced.length}`);
812
+ return unreferenced.length > 0;
813
+ }
814
+ async function admitted(local, locator, needed) {
815
+ if (paused.has(locator))
816
+ return false;
817
+ let available = await estimated().catch(() => null);
818
+ if (available === null || available >= needed)
819
+ return true;
820
+ if (await reclaimed(local))
821
+ available = await estimated().catch(() => null);
822
+ if (available === null || available >= needed)
823
+ return true;
824
+ pause(locator, { cause: 'insufficient-space', needed, available });
825
+ return false;
826
+ }
827
+ async function exceededOn(locator, bytes) {
828
+ const available = await estimated().catch(() => null);
829
+ pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
830
+ }
831
+ function seedRoomHeld(room, version) {
832
+ return prepared.has(room) && reachedBy(room, version);
833
+ }
834
+ function seedHeld(at) {
835
+ return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
836
+ }
837
+ function seedApplies(plan) {
838
+ const seed = seeds.get(plan.key);
839
+ if (seed === undefined)
840
+ return null;
841
+ if (!seedHeld(seed.at))
842
+ return seed;
843
+ seeds.delete(plan.key);
844
+ return null;
845
+ }
846
+ function seedBehind(plan, cover) {
847
+ const seed = seedApplies(plan);
848
+ if (seed === null || 'pending' in cover)
849
+ return cover;
850
+ return {
851
+ pending: Object.entries(seed.at)
852
+ .filter(([room, version]) => !seedRoomHeld(room, version))
853
+ .map(([room]) => ({ room, awaits: 'acquisition' })),
854
+ };
855
+ }
856
+ function seed(entry) {
857
+ if (seedHeld(entry.at))
858
+ return;
859
+ for (const { query, rows } of entry.queries) {
860
+ const key = queryKey(query);
861
+ seeds.set(key, { rows, at: entry.at });
862
+ for (const shown of seedWatchers.get(key) ?? [])
863
+ shown();
864
+ }
865
+ }
505
866
  async function rowsQuery(plan) {
506
867
  const before = covered(plan);
507
868
  awaitPreparation(before);
869
+ const seeded = seedApplies(plan);
870
+ if (seeded !== null)
871
+ return seededResult(seeded).result;
508
872
  if ('pending' in before)
509
873
  return before;
510
874
  const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
@@ -529,10 +893,12 @@ export function composeRuntime(options) {
529
893
  }
530
894
  }
531
895
  function indexNext() {
532
- const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator));
896
+ const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
533
897
  if (room !== undefined) {
534
898
  roomsToIndex.delete(room);
535
- return () => (prepared.has(room) ? searchIndex.indexRoom(room) : undefined);
899
+ return () => prepared.has(room)
900
+ ? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
901
+ : undefined;
536
902
  }
537
903
  const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
538
904
  if (conversation === undefined)
@@ -553,6 +919,7 @@ export function composeRuntime(options) {
553
919
  for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
554
920
  await yielded();
555
921
  await unit();
922
+ queueReadiness();
556
923
  }
557
924
  }
558
925
  finally {
@@ -660,6 +1027,9 @@ export function composeRuntime(options) {
660
1027
  if (fence !== undefined && fence.erased && fence.at < started)
661
1028
  fences.delete(room.locator);
662
1029
  }
1030
+ await loseRooms(found
1031
+ .filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
1032
+ .map((room) => room.locator));
663
1033
  const listed = found.filter((room) => !fences.has(room.locator));
664
1034
  const listedLocators = new Set(listed.map((room) => room.locator));
665
1035
  for (const locator of listedLocators)
@@ -684,6 +1054,12 @@ export function composeRuntime(options) {
684
1054
  emitRooms();
685
1055
  return listed;
686
1056
  }
1057
+ function heldLocally(locator) {
1058
+ return (replicas.has(locator) ||
1059
+ roomDrafts.has(locator) ||
1060
+ transfers.has(locator) ||
1061
+ queuesWithoutReplica.has(locator));
1062
+ }
687
1063
  function unlistedHeld(listed) {
688
1064
  return [
689
1065
  ...new Set([
@@ -726,6 +1102,8 @@ export function composeRuntime(options) {
726
1102
  unreadable.delete(locator);
727
1103
  stale.delete(locator);
728
1104
  repairable.delete(locator);
1105
+ unindexable.delete(locator);
1106
+ paused.delete(locator);
729
1107
  scopedRefusals.delete(locator);
730
1108
  sourceRefusals.delete(locator);
731
1109
  declarationRefusals.delete(locator);
@@ -779,7 +1157,7 @@ export function composeRuntime(options) {
779
1157
  return model;
780
1158
  }
781
1159
  async function hydrate(opened) {
782
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
1160
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
783
1161
  opened.all(),
784
1162
  opened.queues(),
785
1163
  opened.roomDrafts(),
@@ -791,7 +1169,9 @@ export function composeRuntime(options) {
791
1169
  opened.capabilities(),
792
1170
  opened.member(),
793
1171
  opened.transfers(),
1172
+ opened.targets(),
794
1173
  ]);
1174
+ targets = heldTargets;
795
1175
  transfers.clear();
796
1176
  for (const record of staged)
797
1177
  transfers.set(record.locator, record);
@@ -1037,7 +1417,18 @@ export function composeRuntime(options) {
1037
1417
  const accepted = pendings.get(batch.id)?.accepted;
1038
1418
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
1039
1419
  });
1040
- const set = (await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) }))();
1420
+ let settled;
1421
+ try {
1422
+ settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
1423
+ }
1424
+ catch (error) {
1425
+ if (!exceeded(error))
1426
+ throw error;
1427
+ await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
1428
+ caughtUpTo(room.locator);
1429
+ return true;
1430
+ }
1431
+ const set = settled();
1041
1432
  if (stored !== undefined) {
1042
1433
  transfers.delete(room.locator);
1043
1434
  catching.delete(room.locator);
@@ -1045,6 +1436,7 @@ export function composeRuntime(options) {
1045
1436
  for (const batch of accepted)
1046
1437
  pendings.delete(batch.id);
1047
1438
  stale.delete(room.locator);
1439
+ unindexable.delete(room.locator);
1048
1440
  if (unreadable.delete(room.locator))
1049
1441
  scopedRefusals.delete(room.locator);
1050
1442
  sourceRefusals.delete(room.locator);
@@ -1288,7 +1680,7 @@ export function composeRuntime(options) {
1288
1680
  const scope = options.adapter.referrerScope(request.referrer);
1289
1681
  return scope?.kind === 'room' ? scope.locator : null;
1290
1682
  }
1291
- async function readBinary(request) {
1683
+ async function transferBinary(request, transfer, answer) {
1292
1684
  if (closed || refusal !== null)
1293
1685
  return UNCERTAIN;
1294
1686
  try {
@@ -1304,40 +1696,41 @@ export function composeRuntime(options) {
1304
1696
  ? `transfer binary ${request.locator} ${request.path}`
1305
1697
  : `transfer binary ${request.fileRef}`);
1306
1698
  try {
1307
- const result = await options.adapter.binary(authorization, request);
1308
- journal.record(`outcome binary ${result.kind}`);
1699
+ const result = await transfer(authorization);
1700
+ const settled = result;
1701
+ journal.record(`outcome binary ${settled.kind}`);
1309
1702
  const locator = binaryRoom(request);
1310
1703
  if (locator !== null && fencedSince(locator, since))
1311
1704
  return failure({ kind: 'room-not-readable', locator });
1312
- if (result.kind === 'scoped-refusal') {
1313
- const room = replicas.get(result.locator)?.confirmed.record;
1705
+ if (settled.kind === 'scoped-refusal') {
1706
+ const room = replicas.get(settled.locator)?.confirmed.record;
1314
1707
  if (room !== undefined)
1315
- await scoped(result, room, await openLocal());
1708
+ await scoped(settled, room, await openLocal());
1316
1709
  }
1317
- if (result.kind === 'revoked-authorization')
1710
+ if (settled.kind === 'revoked-authorization')
1318
1711
  await refuse('revoked-authorization');
1319
- return binaryAnswer(result);
1712
+ return answer(result);
1320
1713
  }
1321
1714
  catch {
1322
1715
  journal.record('outcome binary transport-uncertainty');
1323
1716
  return UNCERTAIN;
1324
1717
  }
1325
1718
  }
1326
- async function readBinaryPath(locator, path, revision) {
1327
- await ready;
1719
+ function readBinary(request) {
1720
+ return transferBinary(request, (held) => options.adapter.binary(held, request), binaryAnswer);
1721
+ }
1722
+ function binaryPath(locator, path, revision) {
1328
1723
  if (revision !== undefined)
1329
- return readBinary({ kind: 'path', locator, path, revision });
1724
+ return { kind: 'path', locator, path, revision };
1330
1725
  const visible = replicas.get(locator)?.visible;
1331
1726
  const held = visible?.binaries[path];
1332
1727
  if (visible === undefined || held === undefined)
1333
- return absentBinary(path);
1334
- if (held.state === 'pending') {
1335
- const blob = queuedBlob(locator, path, held.localId);
1336
- return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1337
- }
1728
+ return null;
1729
+ if (held.state === 'pending')
1730
+ return { kind: 'pending', localId: held.localId };
1338
1731
  const record = binaryRecord(path, held, visible.revisions[path]);
1339
1732
  if (record.state !== 'accepted')
1340
- return absentBinary(path);
1733
+ return null;
1341
1734
  const replica = replicas.get(locator);
1342
1735
  const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
1343
1736
  const holds = (at) => {
@@ -1352,13 +1745,31 @@ export function composeRuntime(options) {
1352
1745
  if (change.kind === 'move' && change.to === confirmed)
1353
1746
  confirmed = change.path;
1354
1747
  }
1355
- return readBinary({
1748
+ return {
1356
1749
  kind: 'path',
1357
1750
  locator,
1358
1751
  path: confirmed,
1359
1752
  revision: record.revision,
1360
1753
  fileRef: record.fileRef,
1361
- });
1754
+ };
1755
+ }
1756
+ async function readBinaryPath(locator, path, revision) {
1757
+ await ready;
1758
+ const asked = binaryPath(locator, path, revision);
1759
+ if (asked === null)
1760
+ return absentBinary(path);
1761
+ if (asked.kind === 'pending') {
1762
+ const blob = queuedBlob(locator, path, asked.localId);
1763
+ return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1764
+ }
1765
+ return readBinary(asked);
1766
+ }
1767
+ async function linkBinaryPath(locator, path, revision) {
1768
+ await ready;
1769
+ const asked = binaryPath(locator, path, revision);
1770
+ if (asked === null || asked.kind === 'pending')
1771
+ return absentBinary(path);
1772
+ return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
1362
1773
  }
1363
1774
  async function readBinaryReference(reference) {
1364
1775
  await ready;
@@ -1384,7 +1795,7 @@ export function composeRuntime(options) {
1384
1795
  }
1385
1796
  return undefined;
1386
1797
  }
1387
- async function invokeAction(name, input) {
1798
+ async function invokeAction(name, input, dispatching) {
1388
1799
  await ready;
1389
1800
  if (closed || refusal !== null)
1390
1801
  return UNCERTAIN;
@@ -1398,6 +1809,7 @@ export function composeRuntime(options) {
1398
1809
  }
1399
1810
  if (closed || authorization === undefined || refusal !== null)
1400
1811
  return UNCERTAIN;
1812
+ await dispatching?.();
1401
1813
  journal.record(`transfer action ${name}`);
1402
1814
  try {
1403
1815
  const result = await options.adapter.action(authorization, { name, input });
@@ -1418,6 +1830,60 @@ export function composeRuntime(options) {
1418
1830
  return UNCERTAIN;
1419
1831
  }
1420
1832
  }
1833
+ function unsettled(result) {
1834
+ return result.kind !== 'action' && result.retryable;
1835
+ }
1836
+ async function recoveredAction(name, input) {
1837
+ const operation = operationOf(input);
1838
+ if (operation === null)
1839
+ return invokeAction(name, input);
1840
+ const kept = async () => (await openLocal()).keepCall({ operation, name, input });
1841
+ calling.add(operation);
1842
+ try {
1843
+ let result = await invokeAction(name, input, kept);
1844
+ if (unsettled(result)) {
1845
+ await recovery.attempt(`action ${operation}`, async () => {
1846
+ if (closed || refusal !== null)
1847
+ return 'done';
1848
+ if (!socketOpen)
1849
+ return 'again';
1850
+ result = await invokeAction(name, input, kept);
1851
+ return unsettled(result) ? 'again' : 'done';
1852
+ }, { again: true });
1853
+ }
1854
+ if (!unsettled(result))
1855
+ await (await openLocal()).dropCall(operation);
1856
+ return result;
1857
+ }
1858
+ finally {
1859
+ calling.delete(operation);
1860
+ }
1861
+ }
1862
+ async function resendCalls(signal) {
1863
+ if (!socketOpen || closed || stopping)
1864
+ return 'done';
1865
+ const local = await openLocal();
1866
+ for (const held of await local.calls()) {
1867
+ if (calling.has(held.operation))
1868
+ continue;
1869
+ if (closed || signal.aborted)
1870
+ return 'done';
1871
+ if (pastHorizon(held.operation)) {
1872
+ await local.dropCall(held.operation);
1873
+ continue;
1874
+ }
1875
+ calling.add(held.operation);
1876
+ try {
1877
+ if (unsettled(await invokeAction(held.name, held.input)))
1878
+ return 'again';
1879
+ await local.dropCall(held.operation);
1880
+ }
1881
+ finally {
1882
+ calling.delete(held.operation);
1883
+ }
1884
+ }
1885
+ return 'done';
1886
+ }
1421
1887
  function continuing() {
1422
1888
  return !closed && !discoveryPaused;
1423
1889
  }
@@ -1435,8 +1901,9 @@ export function composeRuntime(options) {
1435
1901
  async function pullRefresh(authorized, confirmed, live) {
1436
1902
  if (!live())
1437
1903
  return unconfirmed;
1904
+ const turn = generation;
1438
1905
  try {
1439
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1906
+ const result = await refreshRoom(reads, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1440
1907
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
1441
1908
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
1442
1909
  return result;
@@ -1445,6 +1912,9 @@ export function composeRuntime(options) {
1445
1912
  journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
1446
1913
  throw error;
1447
1914
  }
1915
+ finally {
1916
+ reads.finished(confirmed.record.locator, turn);
1917
+ }
1448
1918
  }
1449
1919
  function staging(local, live) {
1450
1920
  return {
@@ -1453,7 +1923,18 @@ export function composeRuntime(options) {
1453
1923
  stage: (record, page) => exclusive(async () => {
1454
1924
  if (!live())
1455
1925
  return false;
1456
- await local.stage(record, page);
1926
+ const bytes = layerBytes(page);
1927
+ if (!(await admitted(local, record.locator, bytes)))
1928
+ return false;
1929
+ try {
1930
+ await local.stage(record, page);
1931
+ }
1932
+ catch (error) {
1933
+ if (!exceeded(error))
1934
+ throw error;
1935
+ await exceededOn(record.locator, bytes);
1936
+ return false;
1937
+ }
1457
1938
  transfers.set(record.locator, record);
1458
1939
  readSide.recover();
1459
1940
  return true;
@@ -1476,12 +1957,13 @@ export function composeRuntime(options) {
1476
1957
  async function pullBootstrap(authorized, room, local, live) {
1477
1958
  if (!live())
1478
1959
  return unconfirmed;
1960
+ const turn = generation;
1479
1961
  if (replicas.has(room.locator)) {
1480
1962
  catching.add(room.locator);
1481
1963
  readSide.recover();
1482
1964
  }
1483
1965
  try {
1484
- const result = await bootstrapRoom(options.adapter, authorized, room, {
1966
+ const result = await bootstrapRoom(reads, authorized, room, {
1485
1967
  collection: modelOf(room.locator),
1486
1968
  selection: modelOf(room.locator, 'typed-selection'),
1487
1969
  }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
@@ -1496,14 +1978,30 @@ export function composeRuntime(options) {
1496
1978
  caughtUpTo(room.locator);
1497
1979
  throw error;
1498
1980
  }
1981
+ finally {
1982
+ reads.finished(room.locator, turn);
1983
+ }
1499
1984
  }
1500
1985
  function caughtUpTo(locator) {
1501
1986
  if (catching.delete(locator))
1502
1987
  readSide.recover();
1503
1988
  }
1989
+ function behind(room) {
1990
+ const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1991
+ return (held !== undefined &&
1992
+ [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
1993
+ }
1504
1994
  async function advance(room, local, authorized, live) {
1995
+ const carried = reads.carried(room.locator);
1996
+ const withheld = await advanceOnce(room, local, authorized, live);
1997
+ if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
1998
+ return withheld;
1999
+ }
2000
+ return advanceOnce(room, local, authorized, live);
2001
+ }
2002
+ async function advanceOnce(room, local, authorized, live) {
1505
2003
  await referenced(room.locator);
1506
- if (!live())
2004
+ if (!live() || paused.has(room.locator))
1507
2005
  return unconfirmed;
1508
2006
  const held = replicas.get(room.locator);
1509
2007
  if (held !== undefined) {
@@ -1938,6 +2436,11 @@ export function composeRuntime(options) {
1938
2436
  });
1939
2437
  }
1940
2438
  async function scoped(result, room, local) {
2439
+ if ((result.scope === 'content' || result.scope === 'sync') && !result.retryable) {
2440
+ journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
2441
+ await loseRooms([room.locator]);
2442
+ return;
2443
+ }
1941
2444
  scopedRefusals.set(room.locator, {
1942
2445
  code: result.code,
1943
2446
  fields: result.fields,
@@ -1984,15 +2487,16 @@ export function composeRuntime(options) {
1984
2487
  });
1985
2488
  }
1986
2489
  async function synchronize(listed, local, progress, attempt, live) {
1987
- const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
2490
+ const acquired = listed.filter(acquires);
2491
+ const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
1988
2492
  if (!live())
1989
2493
  return;
1990
- const ready = listed.filter((_, index) => {
2494
+ const ready = acquired.filter((_, index) => {
1991
2495
  const outcome = prepared[index];
1992
2496
  return outcome?.status === 'fulfilled' && outcome.value;
1993
2497
  });
1994
2498
  if (attempt === 'opened') {
1995
- options.adapter.roomStream?.hold(heldRooms(ready));
2499
+ options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
1996
2500
  handed = true;
1997
2501
  }
1998
2502
  discovering?.release();
@@ -2020,19 +2524,10 @@ export function composeRuntime(options) {
2020
2524
  await retainModel(local, room, 'collection', modelOf(room.locator));
2021
2525
  if (discoveryPaused || authorization === undefined)
2022
2526
  return false;
2023
- if (room.replica.admission !== 'admitted') {
2024
- if (replicas.has(room.locator))
2025
- progress.complete = false;
2026
- return false;
2027
- }
2028
- return true;
2527
+ return room.replica.admission === 'admitted';
2029
2528
  }
2030
2529
  async function caughtUp(room, local, progress, live) {
2031
- const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
2032
- if (held === undefined || authorization === undefined || !live())
2033
- return;
2034
- const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
2035
- if (!behind)
2530
+ if (authorization === undefined || !live() || !behind(room))
2036
2531
  return;
2037
2532
  const withheld = await advance(room, local, authorization, live);
2038
2533
  if (withheld !== null)
@@ -2260,9 +2755,43 @@ export function composeRuntime(options) {
2260
2755
  options.adapter.roomStream?.reopen();
2261
2756
  });
2262
2757
  }
2263
- function closedSocket(revoked) {
2758
+ function rediscover() {
2759
+ return task(OPENING_LANE, async (live) => {
2760
+ if (!live() || !socketOpen)
2761
+ return;
2762
+ const progress = { complete: true, reachable: true };
2763
+ const gate = discover();
2764
+ moveTo(transferring(status));
2765
+ try {
2766
+ const local = await openLocal();
2767
+ if (!live())
2768
+ return;
2769
+ await ensureAuthorization();
2770
+ if (!live())
2771
+ return;
2772
+ await discoverRooms(local, progress, 'authorized', live);
2773
+ }
2774
+ catch {
2775
+ progress.complete = false;
2776
+ }
2777
+ finally {
2778
+ gate.release();
2779
+ notifyReady();
2780
+ }
2781
+ if (!live())
2782
+ return;
2783
+ if (!progress.complete)
2784
+ complete = false;
2785
+ await rest(progress, live);
2786
+ });
2787
+ }
2788
+ function closedSocket(revoked, lost) {
2264
2789
  const opened = socketOpen;
2265
2790
  socketOpen = false;
2791
+ if (lost) {
2792
+ reads.lost();
2793
+ conversations.lost();
2794
+ }
2266
2795
  if (revoked) {
2267
2796
  void refuse('revoked-authorization');
2268
2797
  return;
@@ -2449,6 +2978,7 @@ export function composeRuntime(options) {
2449
2978
  progress.complete = false;
2450
2979
  return;
2451
2980
  }
2981
+ reads.applied();
2452
2982
  submitAll();
2453
2983
  for (const locator of stale)
2454
2984
  if (repairable.has(locator))
@@ -2457,9 +2987,12 @@ export function composeRuntime(options) {
2457
2987
  if (status.providerUnreachable)
2458
2988
  conversations.reconnected();
2459
2989
  const reading = { complete: true, reachable: true };
2460
- await conversations.catchUp(reading);
2990
+ if (demand.conversations)
2991
+ await conversations.catchUp(reading);
2461
2992
  if (progress.reachable && reading.reachable)
2462
2993
  await conversations.resend(reading);
2994
+ if (progress.reachable)
2995
+ void recovery.attempt('calls', resendCalls);
2463
2996
  if (!reading.reachable) {
2464
2997
  progress.reachable = false;
2465
2998
  resendLater();
@@ -2520,7 +3053,7 @@ export function composeRuntime(options) {
2520
3053
  return;
2521
3054
  const room = discovered.get(locator);
2522
3055
  const held = replicas.get(locator);
2523
- if (room === undefined || room.replica.admission !== 'admitted')
3056
+ if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
2524
3057
  return;
2525
3058
  if (held !== undefined &&
2526
3059
  (held.confirmed.record.replica.selection !== room.replica.selection ||
@@ -2572,14 +3105,17 @@ export function composeRuntime(options) {
2572
3105
  if (event.kind === 'opened') {
2573
3106
  socketOpen = true;
2574
3107
  generation += 1;
3108
+ reads.opened();
2575
3109
  conversations.reopened();
2576
3110
  recovery.resume();
2577
3111
  void sync('opened');
2578
3112
  }
2579
3113
  else if (event.kind === 'closed')
2580
- closedSocket(event.revoked);
3114
+ closedSocket(event.revoked, event.lost);
2581
3115
  else if (event.kind === 'opening')
2582
3116
  void applyOpening(event.expired);
3117
+ else if (event.kind === 'authority-changed')
3118
+ void rediscover();
2583
3119
  else
2584
3120
  void applyRoom(event.locator, event.roomVersion);
2585
3121
  }, receipts);
@@ -2597,6 +3133,7 @@ export function composeRuntime(options) {
2597
3133
  haltedRefusal = false;
2598
3134
  if (mode !== serviceMode) {
2599
3135
  serviceMode = mode;
3136
+ queueReadiness();
2600
3137
  journal.record(`service mode ${mode}`);
2601
3138
  for (const listener of serviceModeListeners)
2602
3139
  listener(mode);
@@ -2609,6 +3146,8 @@ export function composeRuntime(options) {
2609
3146
  if (stream === undefined || withdrawConversations !== undefined)
2610
3147
  return;
2611
3148
  withdrawConversations = stream.observe((event) => {
3149
+ if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
3150
+ queueReadiness();
2612
3151
  if (conversations.receive(event)) {
2613
3152
  void ready.then(scheduleCurrency);
2614
3153
  return;
@@ -2631,7 +3170,7 @@ export function composeRuntime(options) {
2631
3170
  if (!live())
2632
3171
  return;
2633
3172
  await conversations.reconcile();
2634
- if (conversations.current()) {
3173
+ if (conversations.current() || !demand.conversations) {
2635
3174
  show();
2636
3175
  return;
2637
3176
  }
@@ -2649,6 +3188,11 @@ export function composeRuntime(options) {
2649
3188
  if (closed)
2650
3189
  return;
2651
3190
  closed = true;
3191
+ if (narrowing !== null)
3192
+ clearTimeout(narrowing);
3193
+ if (rechecking !== null)
3194
+ clearInterval(rechecking);
3195
+ readinessWatches.clear();
2652
3196
  withdrawStream?.();
2653
3197
  withdrawStream = undefined;
2654
3198
  withdrawConversations?.();
@@ -2696,11 +3240,38 @@ export function composeRuntime(options) {
2696
3240
  return planOf(input, roomFacts(reach), planReach(reach));
2697
3241
  }
2698
3242
  function watchPlan(plan, deliver) {
2699
- return readSide.watch(plan, identityOf(plan), () => {
3243
+ let seeded = false;
3244
+ let pending = true;
3245
+ const shown = () => {
3246
+ const seed = seeds.get(plan.key);
3247
+ if (seed === undefined || seeded || !pending)
3248
+ return;
3249
+ seeded = true;
3250
+ deliver(seededResult(seed));
3251
+ };
3252
+ const watchers = seedWatchers.get(plan.key) ?? new Set();
3253
+ seedWatchers.set(plan.key, watchers);
3254
+ watchers.add(shown);
3255
+ const stop = readSide.watch(plan, identityOf(plan), () => {
2700
3256
  const cover = covered(plan);
2701
3257
  awaitPreparation(cover);
2702
- return cover;
2703
- }, deliver);
3258
+ return seedBehind(plan, cover);
3259
+ }, (message) => {
3260
+ const answered = message;
3261
+ pending = whole(answered) && 'pending' in answered.result;
3262
+ if (pending && seeds.has(plan.key)) {
3263
+ shown();
3264
+ return;
3265
+ }
3266
+ seeded = false;
3267
+ deliver(message);
3268
+ });
3269
+ return () => {
3270
+ watchers.delete(shown);
3271
+ if (watchers.size === 0)
3272
+ seedWatchers.delete(plan.key);
3273
+ stop();
3274
+ };
2704
3275
  }
2705
3276
  async function searched(input, reach) {
2706
3277
  const plan = searchPlanOf(input);
@@ -2709,40 +3280,60 @@ export function composeRuntime(options) {
2709
3280
  const cover = covered(filter);
2710
3281
  awaitPreparation(cover);
2711
3282
  const pending = 'pending' in cover ? [...cover.pending] : [];
2712
- const covering = 'pending' in cover ? [] : cover.coverage.rooms;
3283
+ const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3284
+ const held = covered({
3285
+ ...filter,
3286
+ rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
3287
+ });
3288
+ const covering = 'pending' in held ? [] : held.coverage.rooms;
2713
3289
  const unindexed = covering
2714
3290
  .filter(({ room }) => !searchIndex.rooms().has(room))
2715
3291
  .map(({ room }) => room);
2716
3292
  for (const room of unindexed)
2717
3293
  pending.push({ room, awaits: 'index' });
2718
- const held = conversations.searchable();
3294
+ const listed = conversations.searchable();
2719
3295
  const named = reach !== null && !reach.conversations
2720
3296
  ? []
2721
- : (plan.conversations ?? held).filter((one) => held.includes(one) && conversations.acquired(one));
3297
+ : (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
2722
3298
  const waiting = named.filter((one) => !searchIndex.conversations().has(one));
2723
3299
  for (const conversation of waiting)
2724
3300
  pending.push({ conversation, awaits: 'index' });
2725
3301
  if (unindexed.length > 0 || waiting.length > 0)
2726
3302
  index(unindexed, waiting);
2727
- if (pending.length > 0 || 'pending' in cover)
2728
- return { pending };
3303
+ const complete = pending.length === 0 && !('pending' in cover);
3304
+ const answering = complete
3305
+ ? filter
3306
+ : {
3307
+ ...filter,
3308
+ rooms: covering
3309
+ .filter(({ room }) => searchIndex.rooms().has(room))
3310
+ .map(({ room }) => room),
3311
+ };
3312
+ const answered = covered(answering);
3313
+ if ('pending' in answered)
3314
+ continue;
3315
+ const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
2729
3316
  const seen = searchIndex.changes();
2730
3317
  await searchIndex.caughtUp();
2731
- const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(filter);
3318
+ const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
2732
3319
  if (selected !== null && 'pending' in selected)
2733
- return selected;
3320
+ continue;
2734
3321
  if (searchIndex.changes() !== seen ||
2735
- JSON.stringify(covered(filter)) !== JSON.stringify(cover))
3322
+ JSON.stringify(covered(answering)) !== JSON.stringify(answered))
2736
3323
  continue;
2737
- return {
2738
- ...searchIndex.search(plan, selected === null ? null : selected.documents, named),
2739
- coverage: {
2740
- rooms: covering,
2741
- conversations: named,
2742
- index: searchIndex.generation(),
2743
- overlay: cover.coverage.overlay,
2744
- },
3324
+ const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms === null ? null : new Set(answering.rooms), plan.collection
3325
+ ? new Map(answered.coverage.rooms.map(({ room }) => [
3326
+ room,
3327
+ replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
3328
+ ]))
3329
+ : null);
3330
+ const coverage = {
3331
+ rooms: answered.coverage.rooms,
3332
+ conversations: searchedConversations,
3333
+ index: searchIndex.generation(),
3334
+ overlay: answered.coverage.overlay,
2745
3335
  };
3336
+ return complete ? { ...hits, coverage } : { pending, ...hits, coverage };
2746
3337
  }
2747
3338
  }
2748
3339
  const watch = (topic, input, deliver) => {
@@ -2789,6 +3380,9 @@ export function composeRuntime(options) {
2789
3380
  presentationListeners.delete(listener);
2790
3381
  };
2791
3382
  }
3383
+ if (topic === 'readiness') {
3384
+ return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
3385
+ }
2792
3386
  if (topic === 'rooms') {
2793
3387
  roomListeners.add(deliver);
2794
3388
  deliver(listed());
@@ -2941,10 +3535,11 @@ export function composeRuntime(options) {
2941
3535
  },
2942
3536
  binaryFile: {
2943
3537
  read: readBinaryPath,
3538
+ link: linkBinaryPath,
2944
3539
  readReference: readBinaryReference,
2945
3540
  },
2946
3541
  call(name, input) {
2947
- return invokeAction(name, input);
3542
+ return recoveredAction(name, input);
2948
3543
  },
2949
3544
  reached(locator, roomVersion, path) {
2950
3545
  return path === undefined
@@ -2955,7 +3550,13 @@ export function composeRuntime(options) {
2955
3550
  return journal.export();
2956
3551
  },
2957
3552
  persistence() {
2958
- return options.shell.persistentStorage;
3553
+ return persistence === 'granted';
3554
+ },
3555
+ async retry() {
3556
+ journal.record('retry');
3557
+ unindexable.clear();
3558
+ queueReadiness();
3559
+ resume();
2959
3560
  },
2960
3561
  observe: observing(watch),
2961
3562
  async reauthorize() {
@@ -2982,6 +3583,22 @@ export function composeRuntime(options) {
2982
3583
  query: (plan) => rowsQuery(plan),
2983
3584
  watchPlan,
2984
3585
  search: searched,
3586
+ watchReadiness,
3587
+ };
3588
+ return {
3589
+ md,
3590
+ seed,
3591
+ stop,
3592
+ lose: loseRooms,
3593
+ watch,
3594
+ acquire,
3595
+ persisted(answer) {
3596
+ if (answer === persistence)
3597
+ return;
3598
+ persistence = answer;
3599
+ journal.record(`persistence ${answer}`);
3600
+ queueReadiness();
3601
+ },
3602
+ within: (reach, port) => narrowed(reachable, reach, port),
2985
3603
  };
2986
- return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
2987
3604
  }