@agentshouse/mdruntime 0.1.0-cb0eb0d87ff49 → 0.1.0-cb2e390ee7b89

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 (117) hide show
  1. package/AGENTS.md +275 -285
  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/earlier.d.ts +10 -0
  13. package/dist/chat/earlier.d.ts.map +1 -0
  14. package/dist/chat/earlier.js +54 -0
  15. package/dist/chat/kit.d.ts +2 -1
  16. package/dist/chat/kit.d.ts.map +1 -1
  17. package/dist/chat/kit.js +3 -3
  18. package/dist/chat/list.d.ts +2 -1
  19. package/dist/chat/list.d.ts.map +1 -1
  20. package/dist/chat/list.js +42 -5
  21. package/dist/chat/messages.d.ts +1 -0
  22. package/dist/chat/messages.d.ts.map +1 -1
  23. package/dist/chat/messages.js +4 -0
  24. package/dist/chat/provider.d.ts +1 -1
  25. package/dist/chat/provider.d.ts.map +1 -1
  26. package/dist/chat/provider.js +34 -19
  27. package/dist/chat/question.d.ts +1 -1
  28. package/dist/chat/question.d.ts.map +1 -1
  29. package/dist/chat/question.js +7 -18
  30. package/dist/chat/ready.d.ts +1 -0
  31. package/dist/chat/ready.d.ts.map +1 -1
  32. package/dist/chat/ready.js +2 -2
  33. package/dist/chat/style.js +1 -1
  34. package/dist/client.d.ts +8 -2
  35. package/dist/client.d.ts.map +1 -1
  36. package/dist/client.js +16 -5
  37. package/dist/connection.d.ts +19 -3
  38. package/dist/connection.d.ts.map +1 -1
  39. package/dist/connection.js +741 -100
  40. package/dist/conversation.d.ts +7 -0
  41. package/dist/conversation.d.ts.map +1 -1
  42. package/dist/conversation.js +202 -39
  43. package/dist/durability.d.ts +7 -1
  44. package/dist/durability.d.ts.map +1 -1
  45. package/dist/entry.d.ts.map +1 -1
  46. package/dist/entry.js +1 -1
  47. package/dist/host.d.ts +15 -2
  48. package/dist/host.d.ts.map +1 -1
  49. package/dist/host.js +85 -8
  50. package/dist/index.d.ts +7 -5
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +1 -0
  53. package/dist/memory.d.ts +8 -2
  54. package/dist/memory.d.ts.map +1 -1
  55. package/dist/memory.js +26 -6
  56. package/dist/model.d.ts +3 -2
  57. package/dist/model.d.ts.map +1 -1
  58. package/dist/model.js +1 -1
  59. package/dist/observation.d.ts +2 -0
  60. package/dist/observation.d.ts.map +1 -1
  61. package/dist/observation.js +9 -3
  62. package/dist/outcome.d.ts +8 -2
  63. package/dist/outcome.d.ts.map +1 -1
  64. package/dist/outcome.js +3 -0
  65. package/dist/overlay.d.ts +1 -0
  66. package/dist/overlay.d.ts.map +1 -1
  67. package/dist/overlay.js +27 -3
  68. package/dist/query.d.ts +24 -1
  69. package/dist/query.d.ts.map +1 -1
  70. package/dist/query.js +32 -5
  71. package/dist/reach.d.ts +6 -3
  72. package/dist/reach.d.ts.map +1 -1
  73. package/dist/reach.js +18 -3
  74. package/dist/react.d.ts +4 -1
  75. package/dist/react.d.ts.map +1 -1
  76. package/dist/react.js +6 -4
  77. package/dist/read-side.d.ts.map +1 -1
  78. package/dist/read-side.js +169 -47
  79. package/dist/readiness.d.ts +53 -0
  80. package/dist/readiness.d.ts.map +1 -0
  81. package/dist/readiness.js +33 -0
  82. package/dist/reads.d.ts +11 -0
  83. package/dist/reads.d.ts.map +1 -0
  84. package/dist/reads.js +98 -0
  85. package/dist/refresh.d.ts +4 -4
  86. package/dist/refresh.d.ts.map +1 -1
  87. package/dist/refresh.js +6 -0
  88. package/dist/replica.d.ts +5 -1
  89. package/dist/replica.d.ts.map +1 -1
  90. package/dist/replica.js +30 -5
  91. package/dist/room-draft.d.ts +13 -1
  92. package/dist/room-draft.d.ts.map +1 -1
  93. package/dist/room-draft.js +2 -2
  94. package/dist/rows.d.ts +2 -1
  95. package/dist/rows.d.ts.map +1 -1
  96. package/dist/rows.js +3 -1
  97. package/dist/save.d.ts +1 -0
  98. package/dist/save.d.ts.map +1 -1
  99. package/dist/save.js +17 -1
  100. package/dist/search.d.ts +28 -7
  101. package/dist/search.d.ts.map +1 -1
  102. package/dist/search.js +93 -13
  103. package/dist/seed.d.ts +16 -0
  104. package/dist/seed.d.ts.map +1 -0
  105. package/dist/seed.js +75 -0
  106. package/dist/status.d.ts +4 -0
  107. package/dist/status.d.ts.map +1 -1
  108. package/dist/store.d.ts +26 -4
  109. package/dist/store.d.ts.map +1 -1
  110. package/dist/store.js +92 -25
  111. package/dist/windows.d.ts +18 -0
  112. package/dist/windows.d.ts.map +1 -0
  113. package/dist/windows.js +80 -0
  114. package/dist/wire.d.ts +144 -32
  115. package/dist/wire.d.ts.map +1 -1
  116. package/dist/wire.js +50 -31
  117. 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, plain, 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
- import { buildBatch, joined, limitedIn, requeued } from './save.js';
20
- import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
21
+ import { buildBatch, joined, limitedIn, queuedDocuments, requeued, } from './save.js';
22
+ import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, standing, } 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,15 @@ 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.paths === null &&
42
+ plan.hop === null &&
43
+ plan.backlinks === null);
44
+ }
32
45
  function within(root, path) {
33
46
  return root === '' || beneath(root, path);
34
47
  }
@@ -46,6 +59,10 @@ const OPENING_LANE = 'opening';
46
59
  const CONVERSATION_LANE = 'conversations';
47
60
  const ROOM_READS = 256;
48
61
  const ROOM_ATTEMPTS = 16;
62
+ function operationOf(input) {
63
+ const operation = input?.operation_id;
64
+ return typeof operation === 'string' ? operation : null;
65
+ }
49
66
  const outside = new Map();
50
67
  function outsideOf(roots) {
51
68
  const key = JSON.stringify(roots);
@@ -85,6 +102,8 @@ export function composeRuntime(options) {
85
102
  const replicas = new FencedMap(isFenced);
86
103
  const queuesWithoutReplica = new FencedMap(isFenced);
87
104
  const pendings = new Map();
105
+ const seeds = new Map();
106
+ const seedWatchers = new Map();
88
107
  const roomDrafts = new FencedMap(isFenced);
89
108
  const presentations = new Map();
90
109
  let limits = [];
@@ -97,6 +116,7 @@ export function composeRuntime(options) {
97
116
  const liveModels = new FencedMap(isModelFenced);
98
117
  const invalidModels = new Set();
99
118
  const discovered = new Map();
119
+ const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
100
120
  let roomsListed = false;
101
121
  const scopedRefusals = new Map();
102
122
  const sourceRefusals = new Map();
@@ -133,6 +153,7 @@ export function composeRuntime(options) {
133
153
  let handed = false;
134
154
  const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
135
155
  const repairing = new Set();
156
+ const calling = new Set();
136
157
  const repairable = new Map();
137
158
  let outcomeRetention = null;
138
159
  const answering = new Map();
@@ -166,8 +187,18 @@ export function composeRuntime(options) {
166
187
  const memberListeners = new Set();
167
188
  const prepared = new Set();
168
189
  const projecting = new Set();
169
- const demanded = new Set();
190
+ const unpreparedDemand = new Set();
191
+ const unindexable = new Set();
170
192
  let preparing = false;
193
+ let demand = WHOLE_DEMAND;
194
+ let narrowing = null;
195
+ let targets = {};
196
+ let persistence = options.shell.persistence;
197
+ let capacity = null;
198
+ const paused = new Set();
199
+ let rechecking = null;
200
+ const readinessWatches = new Set();
201
+ let readinessQueued = false;
171
202
  const catching = new Set();
172
203
  const transfers = new FencedMap(isFenced);
173
204
  const conversations = createConversationStore({
@@ -243,6 +274,15 @@ export function composeRuntime(options) {
243
274
  const value = presentations.get(app);
244
275
  return value === undefined ? null : structuredClone(value);
245
276
  }
277
+ function keepPresentation(app, next) {
278
+ return exclusive(async () => {
279
+ const local = await openLocal();
280
+ const stored = next(presentations.get(app));
281
+ await local.present({ app, value: stored });
282
+ presentations.set(app, stored);
283
+ publishChangeSet(changeSet('', null, null, { presentation: app }));
284
+ });
285
+ }
246
286
  function emitPresentation(app) {
247
287
  if (closed)
248
288
  return;
@@ -267,9 +307,16 @@ export function composeRuntime(options) {
267
307
  returned: structuredClone(record.returned),
268
308
  });
269
309
  }
310
+ for (const [room, refusal] of sourceRefusals)
311
+ attentionRequired.push({ room, refusal: structuredClone(refusal) });
270
312
  attentionRequired.sort((left, right) => {
271
313
  if (left.room !== right.room)
272
314
  return left.room < right.room ? -1 : 1;
315
+ if ('refusal' in left || 'refusal' in right) {
316
+ if ('refusal' in left && 'refusal' in right)
317
+ return 0;
318
+ return 'refusal' in left ? -1 : 1;
319
+ }
273
320
  if (left.path !== right.path)
274
321
  return left.path < right.path ? -1 : 1;
275
322
  return 0;
@@ -318,15 +365,18 @@ export function composeRuntime(options) {
318
365
  }
319
366
  function emitRooms() {
320
367
  readSide.recover();
368
+ queueReadiness();
321
369
  const value = listed();
322
370
  for (const listener of roomListeners)
323
371
  listener(value);
324
372
  }
325
373
  function emitRefusal() {
374
+ queueReadiness();
326
375
  for (const listener of refusalListeners)
327
376
  listener(refusal);
328
377
  }
329
378
  function emitStorageLoss() {
379
+ queueReadiness();
330
380
  for (const listener of storageLossListeners)
331
381
  listener(storageLoss);
332
382
  }
@@ -343,7 +393,8 @@ export function composeRuntime(options) {
343
393
  function show() {
344
394
  if (closed)
345
395
  return;
346
- const next = status.state === 'synced' && !conversations.current()
396
+ queueReadiness();
397
+ const next = status.state === 'synced' && demand.conversations && !conversations.current()
347
398
  ? { ...status, state: 'syncing' }
348
399
  : status;
349
400
  if (same(shown, next))
@@ -420,13 +471,19 @@ export function composeRuntime(options) {
420
471
  }
421
472
  conversations.changed(mergeChangeSets(entered));
422
473
  readSide.recover();
474
+ queueReadiness();
423
475
  }
424
476
  function unprepared() {
425
- return [...demanded, ...replicas.keys()].find((locator) => !prepared.has(locator));
477
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
478
+ }
479
+ function unbuilt(locator, error) {
480
+ unindexable.add(locator);
481
+ journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
482
+ queueReadiness();
426
483
  }
427
484
  function prepare(locators = []) {
428
485
  for (const locator of locators)
429
- demanded.add(locator);
486
+ unpreparedDemand.add(locator);
430
487
  if (preparing)
431
488
  return;
432
489
  preparing = true;
@@ -436,8 +493,8 @@ export function composeRuntime(options) {
436
493
  await yielded();
437
494
  if (closed)
438
495
  return;
439
- demanded.delete(waiting);
440
- await prepareReplica(waiting);
496
+ unpreparedDemand.delete(waiting);
497
+ await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
441
498
  }
442
499
  }
443
500
  finally {
@@ -455,7 +512,8 @@ export function composeRuntime(options) {
455
512
  return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
456
513
  }
457
514
  function awaited(locator) {
458
- return rooms.has(locator) || !roomsListed;
515
+ return (!roomsListed ||
516
+ (rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
459
517
  }
460
518
  function covered(plan) {
461
519
  const locators = reachedRooms(plan);
@@ -495,18 +553,339 @@ export function composeRuntime(options) {
495
553
  if (waiting.length > 0)
496
554
  prepare(waiting.map(({ room }) => room));
497
555
  }
556
+ function acquires(room) {
557
+ return (demanded(demand, room) ||
558
+ (replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
559
+ queuesWithoutReplica.has(room.locator));
560
+ }
561
+ function acquiredLocators() {
562
+ return [...discovered.values()]
563
+ .filter(acquires)
564
+ .map((room) => room.locator)
565
+ .sort();
566
+ }
567
+ function heldLocators(acquired) {
568
+ return demand.whole ? acquired : [...new Set([...acquired, ...demand.rooms])].sort();
569
+ }
570
+ function heldDemand(acquired) {
571
+ if (demand.whole)
572
+ return null;
573
+ return {
574
+ rooms: heldLocators(acquired.map((room) => room.locator)),
575
+ conversations: demand.conversations,
576
+ };
577
+ }
578
+ function applyDemand(next) {
579
+ const before = heldLocators(acquiredLocators());
580
+ const { whole, conversations: conversed } = demand;
581
+ demand = next;
582
+ const after = heldLocators(acquiredLocators());
583
+ queueReadiness();
584
+ if (!conversed && next.conversations)
585
+ scheduleCurrency();
586
+ if (JSON.stringify(before) === JSON.stringify(after) &&
587
+ whole === next.whole &&
588
+ conversed === next.conversations)
589
+ return;
590
+ journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
591
+ if (options.adapter.roomStream === undefined)
592
+ void sync('unstreamed');
593
+ else if (socketOpen)
594
+ options.adapter.roomStream.reopen();
595
+ }
596
+ function acquire(next) {
597
+ if (closed)
598
+ return;
599
+ if (narrowing !== null)
600
+ clearTimeout(narrowing);
601
+ narrowing = null;
602
+ const widening = wider(next, demand);
603
+ if (!widening && !narrower(next, demand))
604
+ return;
605
+ if (widening || !roomsListed) {
606
+ applyDemand(next);
607
+ return;
608
+ }
609
+ // A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
610
+ narrowing = setTimeout(() => {
611
+ narrowing = null;
612
+ applyDemand(next);
613
+ }, 5_000);
614
+ }
615
+ function requiredRooms(reach) {
616
+ if (reach === null)
617
+ return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
618
+ const named = [...new Set(reach.rooms)].sort();
619
+ if (roomsListed)
620
+ return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
621
+ return { listed: named.every((room) => replicas.has(room)), rooms: named };
622
+ }
623
+ function roomState(locator, search) {
624
+ if (sourceRefusals.has(locator))
625
+ return 'refused';
626
+ const listedRoom = discovered.get(locator);
627
+ if (roomsListed &&
628
+ (listedRoom === undefined ||
629
+ listedRoom.replica.admission !== 'admitted' ||
630
+ unreadable.has(locator)))
631
+ return 'unadmitted';
632
+ if (!replicas.has(locator))
633
+ return 'acquiring';
634
+ if (!prepared.has(locator))
635
+ return 'preparing';
636
+ if (search && !searchIndex.rooms().has(locator))
637
+ return 'indexing';
638
+ return 'complete';
639
+ }
640
+ function conversationCoverage(reach, search) {
641
+ const declared = reach === null || reach.conversations;
642
+ if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
643
+ return 'undeclared';
644
+ if (!conversations.known() || !conversations.current())
645
+ return 'acquiring';
646
+ if (!search)
647
+ return 'complete';
648
+ const unindexed = conversations
649
+ .searchable()
650
+ .filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
651
+ if (unindexed.length === 0)
652
+ return 'complete';
653
+ index([], unindexed);
654
+ return 'indexing';
655
+ }
656
+ function connectionOf() {
657
+ if (socketOpen)
658
+ return 'connected';
659
+ if (status.providerUnreachable)
660
+ return 'unreachable';
661
+ return options.adapter.roomStream === undefined && status.lastCompleted !== null
662
+ ? 'connected'
663
+ : 'connecting';
664
+ }
665
+ function refusedIn(locators) {
666
+ return locators.flatMap((room) => {
667
+ const held = sourceRefusals.get(room);
668
+ if (held === undefined)
669
+ return [];
670
+ const { file, location } = held.fields;
671
+ return [
672
+ {
673
+ room,
674
+ file: typeof file === 'string' ? file : '',
675
+ rule: held.code,
676
+ location: typeof location === 'string' ? location : null,
677
+ },
678
+ ];
679
+ });
680
+ }
681
+ function readinessFor(port, reach) {
682
+ const required = requiredRooms(reach);
683
+ const search = port.search === 'required';
684
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
685
+ const building = (state) => coverage
686
+ .filter((room) => room.state === state && !unindexable.has(room.room))
687
+ .map(({ room }) => room);
688
+ const preparing = building('preparing');
689
+ if (preparing.length > 0)
690
+ prepare(preparing);
691
+ const indexing = building('indexing');
692
+ if (indexing.length > 0)
693
+ index(indexing, []);
694
+ return readinessOf({
695
+ code: port.code,
696
+ coverage: {
697
+ listed: required.listed,
698
+ rooms: coverage,
699
+ conversations: conversationCoverage(reach, search),
700
+ search: search ? 'required' : 'unused',
701
+ },
702
+ persistence,
703
+ connection: connectionOf(),
704
+ house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
705
+ capacity: capacity !== null &&
706
+ coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
707
+ ? capacity
708
+ : port.capacity,
709
+ refused: refusedIn(required.rooms),
710
+ lost: storageLoss !== null,
711
+ established: targets[port.target]?.established === true,
712
+ });
713
+ }
714
+ function establish(name) {
715
+ targets = { ...targets, [name]: { established: true } };
716
+ queueReadiness();
717
+ void durably((local) => local.keepTargets(targets)).catch(() => {
718
+ journal.record(`target record failed ${name}`);
719
+ });
720
+ }
721
+ function deliverReadiness(watch) {
722
+ if (!readinessWatches.has(watch))
723
+ return;
724
+ const readiness = readinessFor(watch.port, watch.reach);
725
+ if (readiness.ready && targets[watch.port.target]?.established !== true) {
726
+ establish(watch.port.target);
727
+ }
728
+ const shown = JSON.stringify(readiness);
729
+ if (shown === watch.last)
730
+ return;
731
+ watch.last = shown;
732
+ watch.deliver(structuredClone(readiness));
733
+ }
734
+ function queueReadiness() {
735
+ if (readinessQueued || closed || readinessWatches.size === 0)
736
+ return;
737
+ readinessQueued = true;
738
+ queueMicrotask(() => {
739
+ readinessQueued = false;
740
+ if (closed)
741
+ return;
742
+ for (const watch of [...readinessWatches])
743
+ deliverReadiness(watch);
744
+ });
745
+ }
746
+ function watchReadiness(port, reach, deliver) {
747
+ const watch = { port, reach, deliver, last: null };
748
+ readinessWatches.add(watch);
749
+ void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
750
+ return () => {
751
+ readinessWatches.delete(watch);
752
+ };
753
+ }
754
+ function layerBytes(layer) {
755
+ let bytes = 0;
756
+ for (const [path, text] of Object.entries(layer.documents))
757
+ bytes += (path.length + text.length) * 2;
758
+ return bytes;
759
+ }
760
+ function grownBy(next, previous) {
761
+ if (previous === undefined)
762
+ return layerBytes(next);
763
+ const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
764
+ return layerBytes({ ...next, documents });
765
+ }
766
+ function exceeded(error) {
767
+ return (typeof error === 'object' &&
768
+ error !== null &&
769
+ error.name === 'QuotaExceededError');
770
+ }
771
+ async function estimated() {
772
+ const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
773
+ return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
774
+ }
775
+ function pause(locator, cause) {
776
+ paused.add(locator);
777
+ capacity = cause;
778
+ journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
779
+ queueReadiness();
780
+ if (rechecking !== null)
781
+ return;
782
+ // A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
783
+ rechecking = setInterval(() => {
784
+ void recheck();
785
+ }, 60_000);
786
+ }
787
+ async function recheck() {
788
+ const held = capacity;
789
+ if (held === null || closed)
790
+ return;
791
+ const available = await estimated().catch(() => null);
792
+ if (available === null || available < (held.needed ?? 0))
793
+ return;
794
+ if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
795
+ return;
796
+ resume();
797
+ }
798
+ function resume() {
799
+ if (rechecking !== null)
800
+ clearInterval(rechecking);
801
+ rechecking = null;
802
+ if (paused.size === 0 && capacity === null)
803
+ return;
804
+ paused.clear();
805
+ capacity = null;
806
+ journal.record('capacity resumed');
807
+ queueReadiness();
808
+ if (!closed && authorization !== undefined)
809
+ void sync('unstreamed');
810
+ }
811
+ async function reclaimed(local) {
812
+ const unreferenced = [...transfers.keys()].filter((locator) => {
813
+ const room = discovered.get(locator);
814
+ return room === undefined || !acquires(room);
815
+ });
816
+ for (const locator of unreferenced) {
817
+ await local.abandon(locator);
818
+ transfers.delete(locator);
819
+ }
820
+ if (unreferenced.length > 0)
821
+ journal.record(`capacity reclaimed ${unreferenced.length}`);
822
+ return unreferenced.length > 0;
823
+ }
824
+ async function admitted(local, locator, needed) {
825
+ if (paused.has(locator))
826
+ return false;
827
+ let available = await estimated().catch(() => null);
828
+ if (available === null || available >= needed)
829
+ return true;
830
+ if (await reclaimed(local))
831
+ available = await estimated().catch(() => null);
832
+ if (available === null || available >= needed)
833
+ return true;
834
+ pause(locator, { cause: 'insufficient-space', needed, available });
835
+ return false;
836
+ }
837
+ async function exceededOn(locator, bytes) {
838
+ const available = await estimated().catch(() => null);
839
+ pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
840
+ }
841
+ function seedRoomHeld(room, version) {
842
+ return prepared.has(room) && reachedBy(room, version);
843
+ }
844
+ function seedHeld(at) {
845
+ return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
846
+ }
847
+ function seedApplies(plan) {
848
+ const seed = seeds.get(plan.key);
849
+ if (seed === undefined)
850
+ return null;
851
+ if (!seedHeld(seed.at))
852
+ return seed;
853
+ seeds.delete(plan.key);
854
+ return null;
855
+ }
856
+ function seedBehind(plan, cover) {
857
+ const seed = seedApplies(plan);
858
+ if (seed === null || 'pending' in cover)
859
+ return cover;
860
+ return {
861
+ pending: Object.entries(seed.at)
862
+ .filter(([room, version]) => !seedRoomHeld(room, version))
863
+ .map(([room]) => ({ room, awaits: 'acquisition' })),
864
+ };
865
+ }
866
+ function seed(entry) {
867
+ if (seedHeld(entry.at))
868
+ return;
869
+ for (const { query, rows } of entry.queries) {
870
+ const key = queryKey(query);
871
+ seeds.set(key, { rows, at: entry.at });
872
+ for (const shown of seedWatchers.get(key) ?? [])
873
+ shown();
874
+ }
875
+ }
498
876
  async function rowsQuery(plan) {
499
877
  const before = covered(plan);
500
878
  awaitPreparation(before);
879
+ const seeded = seedApplies(plan);
880
+ if (seeded !== null)
881
+ return seededResult(seeded).result;
501
882
  if ('pending' in before)
502
883
  return before;
503
884
  const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
504
885
  awaitPreparation(cover);
505
886
  if ('pending' in cover)
506
887
  return cover;
507
- return 'groups' in rows
508
- ? { groups: rows.groups, coverage: cover.coverage }
509
- : { documents: rows.documents, coverage: cover.coverage };
888
+ return { ...rows, coverage: cover.coverage };
510
889
  }
511
890
  async function indexConversation(conversation) {
512
891
  searchIndex.indexMessages(conversation, [], false);
@@ -522,10 +901,12 @@ export function composeRuntime(options) {
522
901
  }
523
902
  }
524
903
  function indexNext() {
525
- const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator));
904
+ const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
526
905
  if (room !== undefined) {
527
906
  roomsToIndex.delete(room);
528
- return () => (prepared.has(room) ? searchIndex.indexRoom(room) : undefined);
907
+ return () => prepared.has(room)
908
+ ? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
909
+ : undefined;
529
910
  }
530
911
  const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
531
912
  if (conversation === undefined)
@@ -546,6 +927,7 @@ export function composeRuntime(options) {
546
927
  for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
547
928
  await yielded();
548
929
  await unit();
930
+ queueReadiness();
549
931
  }
550
932
  }
551
933
  finally {
@@ -653,6 +1035,9 @@ export function composeRuntime(options) {
653
1035
  if (fence !== undefined && fence.erased && fence.at < started)
654
1036
  fences.delete(room.locator);
655
1037
  }
1038
+ await loseRooms(found
1039
+ .filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
1040
+ .map((room) => room.locator));
656
1041
  const listed = found.filter((room) => !fences.has(room.locator));
657
1042
  const listedLocators = new Set(listed.map((room) => room.locator));
658
1043
  for (const locator of listedLocators)
@@ -677,6 +1062,12 @@ export function composeRuntime(options) {
677
1062
  emitRooms();
678
1063
  return listed;
679
1064
  }
1065
+ function heldLocally(locator) {
1066
+ return (replicas.has(locator) ||
1067
+ roomDrafts.has(locator) ||
1068
+ transfers.has(locator) ||
1069
+ queuesWithoutReplica.has(locator));
1070
+ }
680
1071
  function unlistedHeld(listed) {
681
1072
  return [
682
1073
  ...new Set([
@@ -719,6 +1110,8 @@ export function composeRuntime(options) {
719
1110
  unreadable.delete(locator);
720
1111
  stale.delete(locator);
721
1112
  repairable.delete(locator);
1113
+ unindexable.delete(locator);
1114
+ paused.delete(locator);
722
1115
  scopedRefusals.delete(locator);
723
1116
  sourceRefusals.delete(locator);
724
1117
  declarationRefusals.delete(locator);
@@ -772,7 +1165,7 @@ export function composeRuntime(options) {
772
1165
  return model;
773
1166
  }
774
1167
  async function hydrate(opened) {
775
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
1168
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
776
1169
  opened.all(),
777
1170
  opened.queues(),
778
1171
  opened.roomDrafts(),
@@ -784,7 +1177,9 @@ export function composeRuntime(options) {
784
1177
  opened.capabilities(),
785
1178
  opened.member(),
786
1179
  opened.transfers(),
1180
+ opened.targets(),
787
1181
  ]);
1182
+ targets = heldTargets;
788
1183
  transfers.clear();
789
1184
  for (const record of staged)
790
1185
  transfers.set(record.locator, record);
@@ -908,8 +1303,9 @@ export function composeRuntime(options) {
908
1303
  }
909
1304
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
910
1305
  const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
911
- const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
912
- const kept = queue.filter((batch) => !withdrawn.includes(batch));
1306
+ const { withdrawn, kept } = standing(confirmed, queue, withdrawable, localSaves);
1307
+ const founded = kept.filter((batch) => !queue.includes(batch));
1308
+ const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
913
1309
  const unqueued = withdrawn.map((batch) => ({
914
1310
  batch,
915
1311
  returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
@@ -933,9 +1329,10 @@ export function composeRuntime(options) {
933
1329
  });
934
1330
  }
935
1331
  }
936
- await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
1332
+ await local.keepAuthored({ locator, queue: kept, founded, roomDrafts: [...written.values()] });
1333
+ const staying = new Set(kept.map((batch) => batch.id));
937
1334
  for (const batch of previousQueue) {
938
- if (!kept.includes(batch))
1335
+ if (!staying.has(batch.id))
939
1336
  pendings.delete(batch.id);
940
1337
  }
941
1338
  for (const record of written.values())
@@ -956,7 +1353,7 @@ export function composeRuntime(options) {
956
1353
  else
957
1354
  queuesWithoutReplica.delete(locator);
958
1355
  publishChangeSet(changeSet(locator, before ?? null, held, {
959
- queue: kept.length !== previousQueue.length,
1356
+ queue: queueChanged,
960
1357
  drafts: [...written.keys()],
961
1358
  }));
962
1359
  moveTo({ ...status, ...attention() });
@@ -971,7 +1368,7 @@ export function composeRuntime(options) {
971
1368
  replicas.set(locator, replicaOf(confirmed, kept, before));
972
1369
  return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
973
1370
  room: settling.room,
974
- queue: kept.length !== previousQueue.length,
1371
+ queue: queueChanged,
975
1372
  drafts: [...written.keys()],
976
1373
  });
977
1374
  };
@@ -1030,7 +1427,18 @@ export function composeRuntime(options) {
1030
1427
  const accepted = pendings.get(batch.id)?.accepted;
1031
1428
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
1032
1429
  });
1033
- const set = (await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) }))();
1430
+ let settled;
1431
+ try {
1432
+ settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
1433
+ }
1434
+ catch (error) {
1435
+ if (!exceeded(error))
1436
+ throw error;
1437
+ await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
1438
+ caughtUpTo(room.locator);
1439
+ return true;
1440
+ }
1441
+ const set = settled();
1034
1442
  if (stored !== undefined) {
1035
1443
  transfers.delete(room.locator);
1036
1444
  catching.delete(room.locator);
@@ -1038,6 +1446,7 @@ export function composeRuntime(options) {
1038
1446
  for (const batch of accepted)
1039
1447
  pendings.delete(batch.id);
1040
1448
  stale.delete(room.locator);
1449
+ unindexable.delete(room.locator);
1041
1450
  if (unreadable.delete(room.locator))
1042
1451
  scopedRefusals.delete(room.locator);
1043
1452
  sourceRefusals.delete(room.locator);
@@ -1100,6 +1509,7 @@ export function composeRuntime(options) {
1100
1509
  visible: replica.visible,
1101
1510
  changes,
1102
1511
  bases: parts.flatMap(({ bases }) => bases),
1512
+ queued: queuedDocuments(replica.queue),
1103
1513
  writtenBy,
1104
1514
  clean: {
1105
1515
  text: replica.confirmed.record.documents,
@@ -1107,7 +1517,9 @@ export function composeRuntime(options) {
1107
1517
  composition: compositionOf(replica.confirmed.record),
1108
1518
  },
1109
1519
  };
1110
- const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
1520
+ const checked = withdrawing
1521
+ ? buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
1522
+ : null;
1111
1523
  if (checked?.kind === 'refused')
1112
1524
  return { outcome: 'refused', ...checked.refusal };
1113
1525
  const owners = [...requeue.owners].flatMap((origin) => {
@@ -1130,6 +1542,7 @@ export function composeRuntime(options) {
1130
1542
  ...input,
1131
1543
  visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
1132
1544
  changes: requeue.changes,
1545
+ queued: queuedDocuments(requeue.queue),
1133
1546
  });
1134
1547
  if (built?.kind === 'refused')
1135
1548
  return { outcome: 'refused', ...built.refusal };
@@ -1281,7 +1694,7 @@ export function composeRuntime(options) {
1281
1694
  const scope = options.adapter.referrerScope(request.referrer);
1282
1695
  return scope?.kind === 'room' ? scope.locator : null;
1283
1696
  }
1284
- async function readBinary(request) {
1697
+ async function transferBinary(request, transfer, answer) {
1285
1698
  if (closed || refusal !== null)
1286
1699
  return UNCERTAIN;
1287
1700
  try {
@@ -1297,40 +1710,41 @@ export function composeRuntime(options) {
1297
1710
  ? `transfer binary ${request.locator} ${request.path}`
1298
1711
  : `transfer binary ${request.fileRef}`);
1299
1712
  try {
1300
- const result = await options.adapter.binary(authorization, request);
1301
- journal.record(`outcome binary ${result.kind}`);
1713
+ const result = await transfer(authorization);
1714
+ const settled = result;
1715
+ journal.record(`outcome binary ${settled.kind}`);
1302
1716
  const locator = binaryRoom(request);
1303
1717
  if (locator !== null && fencedSince(locator, since))
1304
1718
  return failure({ kind: 'room-not-readable', locator });
1305
- if (result.kind === 'scoped-refusal') {
1306
- const room = replicas.get(result.locator)?.confirmed.record;
1719
+ if (settled.kind === 'scoped-refusal') {
1720
+ const room = replicas.get(settled.locator)?.confirmed.record;
1307
1721
  if (room !== undefined)
1308
- await scoped(result, room, await openLocal());
1722
+ await scoped(settled, room, await openLocal());
1309
1723
  }
1310
- if (result.kind === 'revoked-authorization')
1724
+ if (settled.kind === 'revoked-authorization')
1311
1725
  await refuse('revoked-authorization');
1312
- return binaryAnswer(result);
1726
+ return answer(result);
1313
1727
  }
1314
1728
  catch {
1315
1729
  journal.record('outcome binary transport-uncertainty');
1316
1730
  return UNCERTAIN;
1317
1731
  }
1318
1732
  }
1319
- async function readBinaryPath(locator, path, revision) {
1320
- await ready;
1733
+ function readBinary(request) {
1734
+ return transferBinary(request, (held) => options.adapter.binary(held, request), binaryAnswer);
1735
+ }
1736
+ function binaryPath(locator, path, revision) {
1321
1737
  if (revision !== undefined)
1322
- return readBinary({ kind: 'path', locator, path, revision });
1738
+ return { kind: 'path', locator, path, revision };
1323
1739
  const visible = replicas.get(locator)?.visible;
1324
1740
  const held = visible?.binaries[path];
1325
1741
  if (visible === undefined || held === undefined)
1326
- return absentBinary(path);
1327
- if (held.state === 'pending') {
1328
- const blob = queuedBlob(locator, path, held.localId);
1329
- return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1330
- }
1742
+ return null;
1743
+ if (held.state === 'pending')
1744
+ return { kind: 'pending', localId: held.localId };
1331
1745
  const record = binaryRecord(path, held, visible.revisions[path]);
1332
1746
  if (record.state !== 'accepted')
1333
- return absentBinary(path);
1747
+ return null;
1334
1748
  const replica = replicas.get(locator);
1335
1749
  const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
1336
1750
  const holds = (at) => {
@@ -1345,13 +1759,31 @@ export function composeRuntime(options) {
1345
1759
  if (change.kind === 'move' && change.to === confirmed)
1346
1760
  confirmed = change.path;
1347
1761
  }
1348
- return readBinary({
1762
+ return {
1349
1763
  kind: 'path',
1350
1764
  locator,
1351
1765
  path: confirmed,
1352
1766
  revision: record.revision,
1353
1767
  fileRef: record.fileRef,
1354
- });
1768
+ };
1769
+ }
1770
+ async function readBinaryPath(locator, path, revision) {
1771
+ await ready;
1772
+ const asked = binaryPath(locator, path, revision);
1773
+ if (asked === null)
1774
+ return absentBinary(path);
1775
+ if (asked.kind === 'pending') {
1776
+ const blob = queuedBlob(locator, path, asked.localId);
1777
+ return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1778
+ }
1779
+ return readBinary(asked);
1780
+ }
1781
+ async function linkBinaryPath(locator, path, revision) {
1782
+ await ready;
1783
+ const asked = binaryPath(locator, path, revision);
1784
+ if (asked === null || asked.kind === 'pending')
1785
+ return absentBinary(path);
1786
+ return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
1355
1787
  }
1356
1788
  async function readBinaryReference(reference) {
1357
1789
  await ready;
@@ -1377,7 +1809,7 @@ export function composeRuntime(options) {
1377
1809
  }
1378
1810
  return undefined;
1379
1811
  }
1380
- async function invokeAction(name, input) {
1812
+ async function invokeAction(name, input, dispatching) {
1381
1813
  await ready;
1382
1814
  if (closed || refusal !== null)
1383
1815
  return UNCERTAIN;
@@ -1391,6 +1823,7 @@ export function composeRuntime(options) {
1391
1823
  }
1392
1824
  if (closed || authorization === undefined || refusal !== null)
1393
1825
  return UNCERTAIN;
1826
+ await dispatching?.();
1394
1827
  journal.record(`transfer action ${name}`);
1395
1828
  try {
1396
1829
  const result = await options.adapter.action(authorization, { name, input });
@@ -1411,6 +1844,60 @@ export function composeRuntime(options) {
1411
1844
  return UNCERTAIN;
1412
1845
  }
1413
1846
  }
1847
+ function unsettled(result) {
1848
+ return result.kind !== 'action' && result.retryable;
1849
+ }
1850
+ async function recoveredAction(name, input) {
1851
+ const operation = operationOf(input);
1852
+ if (operation === null)
1853
+ return invokeAction(name, input);
1854
+ const kept = async () => (await openLocal()).keepCall({ operation, name, input });
1855
+ calling.add(operation);
1856
+ try {
1857
+ let result = await invokeAction(name, input, kept);
1858
+ if (unsettled(result)) {
1859
+ await recovery.attempt(`action ${operation}`, async () => {
1860
+ if (closed || refusal !== null)
1861
+ return 'done';
1862
+ if (!socketOpen)
1863
+ return 'again';
1864
+ result = await invokeAction(name, input, kept);
1865
+ return unsettled(result) ? 'again' : 'done';
1866
+ }, { again: true });
1867
+ }
1868
+ if (!unsettled(result))
1869
+ await (await openLocal()).dropCall(operation);
1870
+ return result;
1871
+ }
1872
+ finally {
1873
+ calling.delete(operation);
1874
+ }
1875
+ }
1876
+ async function resendCalls(signal) {
1877
+ if (!socketOpen || closed || stopping)
1878
+ return 'done';
1879
+ const local = await openLocal();
1880
+ for (const held of await local.calls()) {
1881
+ if (calling.has(held.operation))
1882
+ continue;
1883
+ if (closed || signal.aborted)
1884
+ return 'done';
1885
+ if (pastHorizon(held.operation)) {
1886
+ await local.dropCall(held.operation);
1887
+ continue;
1888
+ }
1889
+ calling.add(held.operation);
1890
+ try {
1891
+ if (unsettled(await invokeAction(held.name, held.input)))
1892
+ return 'again';
1893
+ await local.dropCall(held.operation);
1894
+ }
1895
+ finally {
1896
+ calling.delete(held.operation);
1897
+ }
1898
+ }
1899
+ return 'done';
1900
+ }
1414
1901
  function continuing() {
1415
1902
  return !closed && !discoveryPaused;
1416
1903
  }
@@ -1428,8 +1915,9 @@ export function composeRuntime(options) {
1428
1915
  async function pullRefresh(authorized, confirmed, live) {
1429
1916
  if (!live())
1430
1917
  return unconfirmed;
1918
+ const turn = generation;
1431
1919
  try {
1432
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1920
+ const result = await refreshRoom(reads, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1433
1921
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
1434
1922
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
1435
1923
  return result;
@@ -1438,6 +1926,9 @@ export function composeRuntime(options) {
1438
1926
  journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
1439
1927
  throw error;
1440
1928
  }
1929
+ finally {
1930
+ reads.finished(confirmed.record.locator, turn);
1931
+ }
1441
1932
  }
1442
1933
  function staging(local, live) {
1443
1934
  return {
@@ -1446,7 +1937,18 @@ export function composeRuntime(options) {
1446
1937
  stage: (record, page) => exclusive(async () => {
1447
1938
  if (!live())
1448
1939
  return false;
1449
- await local.stage(record, page);
1940
+ const bytes = layerBytes(page);
1941
+ if (!(await admitted(local, record.locator, bytes)))
1942
+ return false;
1943
+ try {
1944
+ await local.stage(record, page);
1945
+ }
1946
+ catch (error) {
1947
+ if (!exceeded(error))
1948
+ throw error;
1949
+ await exceededOn(record.locator, bytes);
1950
+ return false;
1951
+ }
1450
1952
  transfers.set(record.locator, record);
1451
1953
  readSide.recover();
1452
1954
  return true;
@@ -1469,12 +1971,13 @@ export function composeRuntime(options) {
1469
1971
  async function pullBootstrap(authorized, room, local, live) {
1470
1972
  if (!live())
1471
1973
  return unconfirmed;
1974
+ const turn = generation;
1472
1975
  if (replicas.has(room.locator)) {
1473
1976
  catching.add(room.locator);
1474
1977
  readSide.recover();
1475
1978
  }
1476
1979
  try {
1477
- const result = await bootstrapRoom(options.adapter, authorized, room, {
1980
+ const result = await bootstrapRoom(reads, authorized, room, {
1478
1981
  collection: modelOf(room.locator),
1479
1982
  selection: modelOf(room.locator, 'typed-selection'),
1480
1983
  }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
@@ -1489,14 +1992,30 @@ export function composeRuntime(options) {
1489
1992
  caughtUpTo(room.locator);
1490
1993
  throw error;
1491
1994
  }
1995
+ finally {
1996
+ reads.finished(room.locator, turn);
1997
+ }
1492
1998
  }
1493
1999
  function caughtUpTo(locator) {
1494
2000
  if (catching.delete(locator))
1495
2001
  readSide.recover();
1496
2002
  }
2003
+ function behind(room) {
2004
+ const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
2005
+ return (held !== undefined &&
2006
+ [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
2007
+ }
1497
2008
  async function advance(room, local, authorized, live) {
2009
+ const carried = reads.carried(room.locator);
2010
+ const withheld = await advanceOnce(room, local, authorized, live);
2011
+ if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
2012
+ return withheld;
2013
+ }
2014
+ return advanceOnce(room, local, authorized, live);
2015
+ }
2016
+ async function advanceOnce(room, local, authorized, live) {
1498
2017
  await referenced(room.locator);
1499
- if (!live())
2018
+ if (!live() || paused.has(room.locator))
1500
2019
  return unconfirmed;
1501
2020
  const held = replicas.get(room.locator);
1502
2021
  if (held !== undefined) {
@@ -1589,6 +2108,7 @@ export function composeRuntime(options) {
1589
2108
  unreadable.add(room.locator);
1590
2109
  list(room, false);
1591
2110
  emitRooms();
2111
+ moveTo({ ...status, ...attention() });
1592
2112
  journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
1593
2113
  }
1594
2114
  function leaving(batch, result) {
@@ -1930,6 +2450,11 @@ export function composeRuntime(options) {
1930
2450
  });
1931
2451
  }
1932
2452
  async function scoped(result, room, local) {
2453
+ if ((result.scope === 'content' || result.scope === 'sync') && !result.retryable) {
2454
+ journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
2455
+ await loseRooms([room.locator]);
2456
+ return;
2457
+ }
1933
2458
  scopedRefusals.set(room.locator, {
1934
2459
  code: result.code,
1935
2460
  fields: result.fields,
@@ -1976,15 +2501,16 @@ export function composeRuntime(options) {
1976
2501
  });
1977
2502
  }
1978
2503
  async function synchronize(listed, local, progress, attempt, live) {
1979
- const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
2504
+ const acquired = listed.filter(acquires);
2505
+ const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
1980
2506
  if (!live())
1981
2507
  return;
1982
- const ready = listed.filter((_, index) => {
2508
+ const ready = acquired.filter((_, index) => {
1983
2509
  const outcome = prepared[index];
1984
2510
  return outcome?.status === 'fulfilled' && outcome.value;
1985
2511
  });
1986
2512
  if (attempt === 'opened') {
1987
- options.adapter.roomStream?.hold(heldRooms(ready));
2513
+ options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
1988
2514
  handed = true;
1989
2515
  }
1990
2516
  discovering?.release();
@@ -2012,19 +2538,10 @@ export function composeRuntime(options) {
2012
2538
  await retainModel(local, room, 'collection', modelOf(room.locator));
2013
2539
  if (discoveryPaused || authorization === undefined)
2014
2540
  return false;
2015
- if (room.replica.admission !== 'admitted') {
2016
- if (replicas.has(room.locator))
2017
- progress.complete = false;
2018
- return false;
2019
- }
2020
- return true;
2541
+ return room.replica.admission === 'admitted';
2021
2542
  }
2022
2543
  async function caughtUp(room, local, progress, live) {
2023
- const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
2024
- if (held === undefined || authorization === undefined || !live())
2025
- return;
2026
- const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
2027
- if (!behind)
2544
+ if (authorization === undefined || !live() || !behind(room))
2028
2545
  return;
2029
2546
  const withheld = await advance(room, local, authorization, live);
2030
2547
  if (withheld !== null)
@@ -2252,9 +2769,43 @@ export function composeRuntime(options) {
2252
2769
  options.adapter.roomStream?.reopen();
2253
2770
  });
2254
2771
  }
2255
- function closedSocket(revoked) {
2772
+ function rediscover() {
2773
+ return task(OPENING_LANE, async (live) => {
2774
+ if (!live() || !socketOpen)
2775
+ return;
2776
+ const progress = { complete: true, reachable: true };
2777
+ const gate = discover();
2778
+ moveTo(transferring(status));
2779
+ try {
2780
+ const local = await openLocal();
2781
+ if (!live())
2782
+ return;
2783
+ await ensureAuthorization();
2784
+ if (!live())
2785
+ return;
2786
+ await discoverRooms(local, progress, 'authorized', live);
2787
+ }
2788
+ catch {
2789
+ progress.complete = false;
2790
+ }
2791
+ finally {
2792
+ gate.release();
2793
+ notifyReady();
2794
+ }
2795
+ if (!live())
2796
+ return;
2797
+ if (!progress.complete)
2798
+ complete = false;
2799
+ await rest(progress, live);
2800
+ });
2801
+ }
2802
+ function closedSocket(revoked, lost) {
2256
2803
  const opened = socketOpen;
2257
2804
  socketOpen = false;
2805
+ if (lost) {
2806
+ reads.lost();
2807
+ conversations.lost();
2808
+ }
2258
2809
  if (revoked) {
2259
2810
  void refuse('revoked-authorization');
2260
2811
  return;
@@ -2272,6 +2823,8 @@ export function composeRuntime(options) {
2272
2823
  const progress = underway?.generation === generation ? underway.progress : null;
2273
2824
  const cycle = progress ?? { complete: true, reachable: true };
2274
2825
  const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
2826
+ if (opened === generation)
2827
+ opened = null;
2275
2828
  return task(OPENING_LANE, async (live) => {
2276
2829
  underway = null;
2277
2830
  if (!live())
@@ -2284,8 +2837,6 @@ export function composeRuntime(options) {
2284
2837
  await roomsCarried(expired);
2285
2838
  if (!live())
2286
2839
  return;
2287
- if (opened === generation)
2288
- opened = null;
2289
2840
  const replaced = new Set(owed);
2290
2841
  owed.clear();
2291
2842
  try {
@@ -2441,6 +2992,7 @@ export function composeRuntime(options) {
2441
2992
  progress.complete = false;
2442
2993
  return;
2443
2994
  }
2995
+ reads.applied();
2444
2996
  submitAll();
2445
2997
  for (const locator of stale)
2446
2998
  if (repairable.has(locator))
@@ -2449,9 +3001,12 @@ export function composeRuntime(options) {
2449
3001
  if (status.providerUnreachable)
2450
3002
  conversations.reconnected();
2451
3003
  const reading = { complete: true, reachable: true };
2452
- await conversations.catchUp(reading);
3004
+ if (demand.conversations)
3005
+ await conversations.catchUp(reading);
2453
3006
  if (progress.reachable && reading.reachable)
2454
3007
  await conversations.resend(reading);
3008
+ if (progress.reachable)
3009
+ void recovery.attempt('calls', resendCalls);
2455
3010
  if (!reading.reachable) {
2456
3011
  progress.reachable = false;
2457
3012
  resendLater();
@@ -2512,7 +3067,7 @@ export function composeRuntime(options) {
2512
3067
  return;
2513
3068
  const room = discovered.get(locator);
2514
3069
  const held = replicas.get(locator);
2515
- if (room === undefined || room.replica.admission !== 'admitted')
3070
+ if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
2516
3071
  return;
2517
3072
  if (held !== undefined &&
2518
3073
  (held.confirmed.record.replica.selection !== room.replica.selection ||
@@ -2564,14 +3119,17 @@ export function composeRuntime(options) {
2564
3119
  if (event.kind === 'opened') {
2565
3120
  socketOpen = true;
2566
3121
  generation += 1;
3122
+ reads.opened();
2567
3123
  conversations.reopened();
2568
3124
  recovery.resume();
2569
3125
  void sync('opened');
2570
3126
  }
2571
3127
  else if (event.kind === 'closed')
2572
- closedSocket(event.revoked);
3128
+ closedSocket(event.revoked, event.lost);
2573
3129
  else if (event.kind === 'opening')
2574
3130
  void applyOpening(event.expired);
3131
+ else if (event.kind === 'authority-changed')
3132
+ void rediscover();
2575
3133
  else
2576
3134
  void applyRoom(event.locator, event.roomVersion);
2577
3135
  }, receipts);
@@ -2589,6 +3147,7 @@ export function composeRuntime(options) {
2589
3147
  haltedRefusal = false;
2590
3148
  if (mode !== serviceMode) {
2591
3149
  serviceMode = mode;
3150
+ queueReadiness();
2592
3151
  journal.record(`service mode ${mode}`);
2593
3152
  for (const listener of serviceModeListeners)
2594
3153
  listener(mode);
@@ -2601,6 +3160,8 @@ export function composeRuntime(options) {
2601
3160
  if (stream === undefined || withdrawConversations !== undefined)
2602
3161
  return;
2603
3162
  withdrawConversations = stream.observe((event) => {
3163
+ if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
3164
+ queueReadiness();
2604
3165
  if (conversations.receive(event)) {
2605
3166
  void ready.then(scheduleCurrency);
2606
3167
  return;
@@ -2623,7 +3184,7 @@ export function composeRuntime(options) {
2623
3184
  if (!live())
2624
3185
  return;
2625
3186
  await conversations.reconcile();
2626
- if (conversations.current()) {
3187
+ if (conversations.current() || !demand.conversations) {
2627
3188
  show();
2628
3189
  return;
2629
3190
  }
@@ -2641,6 +3202,11 @@ export function composeRuntime(options) {
2641
3202
  if (closed)
2642
3203
  return;
2643
3204
  closed = true;
3205
+ if (narrowing !== null)
3206
+ clearTimeout(narrowing);
3207
+ if (rechecking !== null)
3208
+ clearInterval(rechecking);
3209
+ readinessWatches.clear();
2644
3210
  withdrawStream?.();
2645
3211
  withdrawStream = undefined;
2646
3212
  withdrawConversations?.();
@@ -2688,11 +3254,38 @@ export function composeRuntime(options) {
2688
3254
  return planOf(input, roomFacts(reach), planReach(reach));
2689
3255
  }
2690
3256
  function watchPlan(plan, deliver) {
2691
- return readSide.watch(plan, identityOf(plan), () => {
3257
+ let seeded = false;
3258
+ let pending = true;
3259
+ const shown = () => {
3260
+ const seed = seeds.get(plan.key);
3261
+ if (seed === undefined || seeded || !pending)
3262
+ return;
3263
+ seeded = true;
3264
+ deliver(seededResult(seed));
3265
+ };
3266
+ const watchers = seedWatchers.get(plan.key) ?? new Set();
3267
+ seedWatchers.set(plan.key, watchers);
3268
+ watchers.add(shown);
3269
+ const stop = readSide.watch(plan, identityOf(plan), () => {
2692
3270
  const cover = covered(plan);
2693
3271
  awaitPreparation(cover);
2694
- return cover;
2695
- }, deliver);
3272
+ return seedBehind(plan, cover);
3273
+ }, (message) => {
3274
+ const answered = message;
3275
+ pending = whole(answered) && 'pending' in answered.result;
3276
+ if (pending && seeds.has(plan.key)) {
3277
+ shown();
3278
+ return;
3279
+ }
3280
+ seeded = false;
3281
+ deliver(message);
3282
+ });
3283
+ return () => {
3284
+ watchers.delete(shown);
3285
+ if (watchers.size === 0)
3286
+ seedWatchers.delete(plan.key);
3287
+ stop();
3288
+ };
2696
3289
  }
2697
3290
  async function searched(input, reach) {
2698
3291
  const plan = searchPlanOf(input);
@@ -2701,40 +3294,62 @@ export function composeRuntime(options) {
2701
3294
  const cover = covered(filter);
2702
3295
  awaitPreparation(cover);
2703
3296
  const pending = 'pending' in cover ? [...cover.pending] : [];
2704
- const covering = 'pending' in cover ? [] : cover.coverage.rooms;
3297
+ const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3298
+ const held = covered({
3299
+ ...filter,
3300
+ rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
3301
+ });
3302
+ const covering = 'pending' in held ? [] : held.coverage.rooms;
2705
3303
  const unindexed = covering
2706
3304
  .filter(({ room }) => !searchIndex.rooms().has(room))
2707
3305
  .map(({ room }) => room);
2708
3306
  for (const room of unindexed)
2709
3307
  pending.push({ room, awaits: 'index' });
2710
- const held = conversations.searchable();
3308
+ const listed = conversations.searchable();
2711
3309
  const named = reach !== null && !reach.conversations
2712
3310
  ? []
2713
- : (plan.conversations ?? held).filter((one) => held.includes(one) && conversations.acquired(one));
3311
+ : (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
2714
3312
  const waiting = named.filter((one) => !searchIndex.conversations().has(one));
2715
3313
  for (const conversation of waiting)
2716
3314
  pending.push({ conversation, awaits: 'index' });
2717
3315
  if (unindexed.length > 0 || waiting.length > 0)
2718
3316
  index(unindexed, waiting);
2719
- if (pending.length > 0 || 'pending' in cover)
2720
- return { pending };
3317
+ const complete = pending.length === 0 && !('pending' in cover);
3318
+ const answering = complete
3319
+ ? filter
3320
+ : {
3321
+ ...filter,
3322
+ rooms: covering
3323
+ .filter(({ room }) => searchIndex.rooms().has(room))
3324
+ .map(({ room }) => room),
3325
+ };
3326
+ const answered = covered(answering);
3327
+ if ('pending' in answered)
3328
+ continue;
3329
+ const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
2721
3330
  const seen = searchIndex.changes();
2722
3331
  await searchIndex.caughtUp();
2723
- const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(filter);
3332
+ const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
2724
3333
  if (selected !== null && 'pending' in selected)
2725
- return selected;
3334
+ continue;
2726
3335
  if (searchIndex.changes() !== seen ||
2727
- JSON.stringify(covered(filter)) !== JSON.stringify(cover))
3336
+ JSON.stringify(covered(answering)) !== JSON.stringify(answered))
2728
3337
  continue;
2729
- return {
2730
- ...searchIndex.search(plan, selected === null ? null : selected.documents, named),
2731
- coverage: {
2732
- rooms: covering,
2733
- conversations: named,
2734
- index: searchIndex.generation(),
2735
- overlay: cover.coverage.overlay,
2736
- },
3338
+ const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms === null ? null : new Set(answering.rooms), plan.collection
3339
+ ? new Map(answered.coverage.rooms.map(({ room }) => [
3340
+ room,
3341
+ replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
3342
+ ]))
3343
+ : null);
3344
+ const coverage = {
3345
+ rooms: answered.coverage.rooms,
3346
+ conversations: searchedConversations,
3347
+ index: searchIndex.generation(),
3348
+ overlay: answered.coverage.overlay,
2737
3349
  };
3350
+ return complete
3351
+ ? { ...hits, coverage }
3352
+ : { pending, documents: hits.documents, messages: hits.messages, coverage };
2738
3353
  }
2739
3354
  }
2740
3355
  const watch = (topic, input, deliver) => {
@@ -2781,6 +3396,9 @@ export function composeRuntime(options) {
2781
3396
  presentationListeners.delete(listener);
2782
3397
  };
2783
3398
  }
3399
+ if (topic === 'readiness') {
3400
+ return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
3401
+ }
2784
3402
  if (topic === 'rooms') {
2785
3403
  roomListeners.add(deliver);
2786
3404
  deliver(listed());
@@ -2916,13 +3534,13 @@ export function composeRuntime(options) {
2916
3534
  });
2917
3535
  },
2918
3536
  write(app, value) {
2919
- return exclusive(async () => {
2920
- const local = await openLocal();
2921
- const stored = structuredClone(value);
2922
- await local.present({ app, value: stored });
2923
- presentations.set(app, stored);
2924
- publishChangeSet(changeSet('', null, null, { presentation: app }));
2925
- });
3537
+ return keepPresentation(app, () => structuredClone(value));
3538
+ },
3539
+ edit(app, members) {
3540
+ return keepPresentation(app, (held) => Object.fromEntries([
3541
+ ...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(members, member)),
3542
+ ...Object.entries(structuredClone(members)).filter(([, value]) => value !== null),
3543
+ ]));
2926
3544
  },
2927
3545
  },
2928
3546
  save(room, named, writtenBy) {
@@ -2933,10 +3551,11 @@ export function composeRuntime(options) {
2933
3551
  },
2934
3552
  binaryFile: {
2935
3553
  read: readBinaryPath,
3554
+ link: linkBinaryPath,
2936
3555
  readReference: readBinaryReference,
2937
3556
  },
2938
3557
  call(name, input) {
2939
- return invokeAction(name, input);
3558
+ return recoveredAction(name, input);
2940
3559
  },
2941
3560
  reached(locator, roomVersion, path) {
2942
3561
  return path === undefined
@@ -2947,7 +3566,13 @@ export function composeRuntime(options) {
2947
3566
  return journal.export();
2948
3567
  },
2949
3568
  persistence() {
2950
- return options.shell.persistentStorage;
3569
+ return persistence === 'granted';
3570
+ },
3571
+ async retry() {
3572
+ journal.record('retry');
3573
+ unindexable.clear();
3574
+ queueReadiness();
3575
+ resume();
2951
3576
  },
2952
3577
  observe: observing(watch),
2953
3578
  async reauthorize() {
@@ -2974,6 +3599,22 @@ export function composeRuntime(options) {
2974
3599
  query: (plan) => rowsQuery(plan),
2975
3600
  watchPlan,
2976
3601
  search: searched,
3602
+ watchReadiness,
3603
+ };
3604
+ return {
3605
+ md,
3606
+ seed,
3607
+ stop,
3608
+ lose: loseRooms,
3609
+ watch,
3610
+ acquire,
3611
+ persisted(answer) {
3612
+ if (answer === persistence)
3613
+ return;
3614
+ persistence = answer;
3615
+ journal.record(`persistence ${answer}`);
3616
+ queueReadiness();
3617
+ },
3618
+ within: (reach, port) => narrowed(reachable, reach, port),
2977
3619
  };
2978
- return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
2979
3620
  }