@agentshouse/mdruntime 0.1.0-c8f3163f32f93 → 0.1.0-c93103f5357ff

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 (119) hide show
  1. package/AGENTS.md +262 -262
  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 +13 -7
  6. package/dist/adapter.d.ts.map +1 -1
  7. package/dist/bootstrap.d.ts.map +1 -1
  8. package/dist/bootstrap.js +4 -0
  9. package/dist/change-set.d.ts +2 -0
  10. package/dist/change-set.d.ts.map +1 -1
  11. package/dist/change-set.js +36 -10
  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/message.d.ts.map +1 -1
  19. package/dist/chat/message.js +2 -2
  20. package/dist/chat/messages.d.ts +1 -0
  21. package/dist/chat/messages.d.ts.map +1 -1
  22. package/dist/chat/messages.js +4 -0
  23. package/dist/chat/provider.d.ts +1 -1
  24. package/dist/chat/provider.d.ts.map +1 -1
  25. package/dist/chat/provider.js +26 -17
  26. package/dist/chat/ready.d.ts +1 -0
  27. package/dist/chat/ready.d.ts.map +1 -1
  28. package/dist/chat/ready.js +2 -2
  29. package/dist/client.d.ts +10 -6
  30. package/dist/client.d.ts.map +1 -1
  31. package/dist/client.js +11 -2
  32. package/dist/collection.d.ts +17 -3
  33. package/dist/collection.d.ts.map +1 -1
  34. package/dist/collection.js +230 -19
  35. package/dist/connection.d.ts +23 -7
  36. package/dist/connection.d.ts.map +1 -1
  37. package/dist/connection.js +1031 -183
  38. package/dist/conversation.d.ts +10 -5
  39. package/dist/conversation.d.ts.map +1 -1
  40. package/dist/conversation.js +201 -95
  41. package/dist/durability.d.ts +7 -1
  42. package/dist/durability.d.ts.map +1 -1
  43. package/dist/entry.d.ts +2 -2
  44. package/dist/entry.d.ts.map +1 -1
  45. package/dist/entry.js +3 -3
  46. package/dist/host.d.ts +16 -2
  47. package/dist/host.d.ts.map +1 -1
  48. package/dist/host.js +82 -8
  49. package/dist/identity.d.ts +2 -0
  50. package/dist/identity.d.ts.map +1 -1
  51. package/dist/identity.js +6 -0
  52. package/dist/index.d.ts +8 -5
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +2 -0
  55. package/dist/memory.d.ts +3 -1
  56. package/dist/memory.d.ts.map +1 -1
  57. package/dist/memory.js +14 -2
  58. package/dist/model.d.ts +4 -2
  59. package/dist/model.d.ts.map +1 -1
  60. package/dist/model.js +10 -1
  61. package/dist/observation.d.ts +5 -2
  62. package/dist/observation.d.ts.map +1 -1
  63. package/dist/observation.js +22 -11
  64. package/dist/overlay.d.ts +2 -0
  65. package/dist/overlay.d.ts.map +1 -1
  66. package/dist/overlay.js +50 -55
  67. package/dist/query.d.ts +35 -3
  68. package/dist/query.d.ts.map +1 -1
  69. package/dist/query.js +8 -2
  70. package/dist/reach.d.ts +6 -3
  71. package/dist/reach.d.ts.map +1 -1
  72. package/dist/reach.js +16 -4
  73. package/dist/react.d.ts +7 -4
  74. package/dist/react.d.ts.map +1 -1
  75. package/dist/react.js +6 -4
  76. package/dist/read-side.d.ts +13 -6
  77. package/dist/read-side.d.ts.map +1 -1
  78. package/dist/read-side.js +285 -83
  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/recovery.d.ts +25 -0
  83. package/dist/recovery.d.ts.map +1 -0
  84. package/dist/recovery.js +197 -0
  85. package/dist/refresh.d.ts.map +1 -1
  86. package/dist/refresh.js +17 -10
  87. package/dist/replica.d.ts +1 -1
  88. package/dist/replica.d.ts.map +1 -1
  89. package/dist/replica.js +57 -20
  90. package/dist/room-draft.d.ts +13 -1
  91. package/dist/room-draft.d.ts.map +1 -1
  92. package/dist/room-draft.js +2 -2
  93. package/dist/rows.d.ts +5 -13
  94. package/dist/rows.d.ts.map +1 -1
  95. package/dist/rows.js +5 -13
  96. package/dist/save.d.ts +5 -0
  97. package/dist/save.d.ts.map +1 -1
  98. package/dist/save.js +36 -13
  99. package/dist/search.d.ts +36 -6
  100. package/dist/search.d.ts.map +1 -1
  101. package/dist/search.js +132 -46
  102. package/dist/seed.d.ts +16 -0
  103. package/dist/seed.d.ts.map +1 -0
  104. package/dist/seed.js +61 -0
  105. package/dist/shared.d.ts +11 -0
  106. package/dist/shared.d.ts.map +1 -0
  107. package/dist/shared.js +368 -0
  108. package/dist/status.d.ts +4 -0
  109. package/dist/status.d.ts.map +1 -1
  110. package/dist/store.d.ts +21 -5
  111. package/dist/store.d.ts.map +1 -1
  112. package/dist/store.js +157 -53
  113. package/dist/turn.d.ts +7 -0
  114. package/dist/turn.d.ts.map +1 -0
  115. package/dist/turn.js +45 -0
  116. package/dist/wire.d.ts +136 -32
  117. package/dist/wire.d.ts.map +1 -1
  118. package/dist/wire.js +48 -31
  119. package/package.json +2 -2
@@ -1,31 +1,45 @@
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';
9
- import { changeSet, mergeChangeSets } from './change-set.js';
10
+ import { changeSet, entering, mergeChangeSets } from './change-set.js';
10
11
  import { createConversationStore, } from './conversation.js';
11
12
  import { queuedFile } from './change.js';
12
- import { validRoot } from './collection.js';
13
+ 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
17
  import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
18
+ import { createRecovery } from './recovery.js';
17
19
  import { refreshRoom } from './refresh.js';
18
20
  import { buildBatch, joined, limitedIn, requeued } from './save.js';
19
21
  import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
20
22
  import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
21
23
  import { same, saving, STARTING, transferring, } from './status.js';
22
24
  import { detectStorageLoss, nextGeneration } from './durability.js';
25
+ import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
26
+ import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
23
27
  import { FencedMap } from './fence.js';
24
- import { roomDraftRevision } from './identity.js';
28
+ import { tally } from './shared.js';
29
+ import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
30
+ import { identityTime, roomDraftRevision } from './identity.js';
25
31
  import { beneath, normalize } from './paths.js';
26
32
  import { openRuntimeStore, } from './store.js';
27
33
  import { unconfirmed } from './transfer.js';
28
34
  import { governs, holds, narrowed, planReach, } from './reach.js';
35
+ function roomsAlone(plan) {
36
+ return (plan.reach === null &&
37
+ plan.root.types === null &&
38
+ plan.root.where === null &&
39
+ plan.root.pathPrefix === null &&
40
+ plan.hop === null &&
41
+ plan.backlinks === null);
42
+ }
29
43
  function within(root, path) {
30
44
  return root === '' || beneath(root, path);
31
45
  }
@@ -42,6 +56,29 @@ function declaredSelection(room) {
42
56
  const OPENING_LANE = 'opening';
43
57
  const CONVERSATION_LANE = 'conversations';
44
58
  const ROOM_READS = 256;
59
+ const ROOM_ATTEMPTS = 16;
60
+ const outside = new Map();
61
+ function outsideOf(roots) {
62
+ const key = JSON.stringify(roots);
63
+ const held = outside.get(key);
64
+ if (held !== undefined)
65
+ return held;
66
+ const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
67
+ const made = {
68
+ documents: (path) => (markdownPath(path) ? counted(path) : 0),
69
+ binaries: counted,
70
+ };
71
+ outside.set(key, made);
72
+ return made;
73
+ }
74
+ function abandonedOn(signal) {
75
+ return new Promise((resolve) => {
76
+ if (signal.aborted)
77
+ resolve(null);
78
+ else
79
+ signal.addEventListener('abort', () => resolve(null), { once: true });
80
+ });
81
+ }
45
82
  function roomLane(locator) {
46
83
  return `room ${locator}`;
47
84
  }
@@ -59,6 +96,8 @@ export function composeRuntime(options) {
59
96
  const replicas = new FencedMap(isFenced);
60
97
  const queuesWithoutReplica = new FencedMap(isFenced);
61
98
  const pendings = new Map();
99
+ const seeds = new Map();
100
+ const seedWatchers = new Map();
62
101
  const roomDrafts = new FencedMap(isFenced);
63
102
  const presentations = new Map();
64
103
  let limits = [];
@@ -98,7 +137,6 @@ export function composeRuntime(options) {
98
137
  let status = STARTING;
99
138
  let shown = STARTING;
100
139
  let discoveryPaused = false;
101
- let resetTimer;
102
140
  let closed = false;
103
141
  let stopping = false;
104
142
  let withdrawStream;
@@ -106,7 +144,10 @@ export function composeRuntime(options) {
106
144
  let underway = null;
107
145
  let streamed = null;
108
146
  let handed = false;
109
- const submitting = new Map();
147
+ const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
148
+ const repairing = new Set();
149
+ const repairable = new Map();
150
+ let outcomeRetention = null;
110
151
  const answering = new Map();
111
152
  let complete = false;
112
153
  let recorded = false;
@@ -136,25 +177,41 @@ export function composeRuntime(options) {
136
177
  const serviceModeListeners = new Set();
137
178
  let member = null;
138
179
  const memberListeners = new Set();
139
- const materialized = new Set();
140
- let everyRoom = false;
180
+ const prepared = new Set();
181
+ const projecting = new Set();
182
+ const unpreparedDemand = new Set();
183
+ const unindexable = new Set();
184
+ let preparing = false;
185
+ let demand = WHOLE_DEMAND;
186
+ let narrowing = null;
187
+ let targets = {};
188
+ let persistence = options.shell.persistence;
189
+ let capacity = null;
190
+ const paused = new Set();
191
+ let rechecking = null;
192
+ const readinessWatches = new Set();
193
+ let readinessQueued = false;
194
+ const catching = new Set();
141
195
  const transfers = new FencedMap(isFenced);
142
196
  const conversations = createConversationStore({
143
197
  agents: declaredAgents,
144
- documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
198
+ documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
145
199
  call: (name, input) => invokeAction(name, input),
146
200
  durably,
147
201
  stream: () => options.adapter.conversations,
148
202
  publish: publishChangeSet,
149
203
  online: () => !status.providerUnreachable && refusal === null,
150
204
  closed: () => closed,
205
+ unanswered: resendLater,
206
+ expired: (operation) => pastHorizon(operation),
151
207
  });
152
208
  const searchIndex = createSearchIndex({
153
- documents: () => readSide.documents(),
209
+ room: (room) => readSide.room(room),
154
210
  document: (room, path) => readSide.document(room, path),
155
- messages: () => conversations.messages(),
156
- message: (conversation, identity) => conversations.message(conversation, identity),
157
211
  });
212
+ const roomsToIndex = new Set();
213
+ const conversationsToIndex = new Set();
214
+ let indexing = false;
158
215
  const journal = createLog();
159
216
  let notifyReady = () => { };
160
217
  const ready = new Promise((resolve) => {
@@ -233,9 +290,16 @@ export function composeRuntime(options) {
233
290
  returned: structuredClone(record.returned),
234
291
  });
235
292
  }
293
+ for (const [room, refusal] of sourceRefusals)
294
+ attentionRequired.push({ room, refusal: structuredClone(refusal) });
236
295
  attentionRequired.sort((left, right) => {
237
296
  if (left.room !== right.room)
238
297
  return left.room < right.room ? -1 : 1;
298
+ if ('refusal' in left || 'refusal' in right) {
299
+ if ('refusal' in left && 'refusal' in right)
300
+ return 0;
301
+ return 'refusal' in left ? -1 : 1;
302
+ }
239
303
  if (left.path !== right.path)
240
304
  return left.path < right.path ? -1 : 1;
241
305
  return 0;
@@ -254,11 +318,9 @@ export function composeRuntime(options) {
254
318
  if (replica === undefined)
255
319
  return 0;
256
320
  const roots = replica.confirmed.record.typedSelection?.roots ?? [];
257
- const paths = [
258
- ...Object.keys(replica.visible.documents).filter(markdownPath),
259
- ...Object.keys(replica.visible.binaries),
260
- ];
261
- return paths.filter((path) => !roots.some((root) => beneath(root, path))).length;
321
+ const weighed = outsideOf(roots);
322
+ return (tally(replica.visible.documents, weighed.documents) +
323
+ tally(replica.visible.binaries, weighed.binaries));
262
324
  }
263
325
  function listed() {
264
326
  return structuredClone([...rooms.values()].map((room) => {
@@ -285,15 +347,19 @@ export function composeRuntime(options) {
285
347
  return { listed: roomsListed, rooms: held };
286
348
  }
287
349
  function emitRooms() {
350
+ readSide.recover();
351
+ queueReadiness();
288
352
  const value = listed();
289
353
  for (const listener of roomListeners)
290
354
  listener(value);
291
355
  }
292
356
  function emitRefusal() {
357
+ queueReadiness();
293
358
  for (const listener of refusalListeners)
294
359
  listener(refusal);
295
360
  }
296
361
  function emitStorageLoss() {
362
+ queueReadiness();
297
363
  for (const listener of storageLossListeners)
298
364
  listener(storageLoss);
299
365
  }
@@ -310,7 +376,8 @@ export function composeRuntime(options) {
310
376
  function show() {
311
377
  if (closed)
312
378
  return;
313
- const next = status.state === 'synced' && !conversations.current()
379
+ queueReadiness();
380
+ const next = status.state === 'synced' && demand.conversations && !conversations.current()
314
381
  ? { ...status, state: 'syncing' }
315
382
  : status;
316
383
  if (same(shown, next))
@@ -339,42 +406,511 @@ export function composeRuntime(options) {
339
406
  }
340
407
  }
341
408
  function applyChangeSet(set) {
342
- const held = everyRoom
343
- ? set
344
- : { ...set, documents: set.documents.filter(({ locator }) => materialized.has(locator)) };
409
+ const held = {
410
+ ...set,
411
+ documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
412
+ };
345
413
  readSide.apply(held);
346
414
  searchIndex.changed(held);
347
415
  }
348
- function materialize(locators) {
349
- if (everyRoom)
416
+ async function referenced(locator) {
417
+ for (;;) {
418
+ const record = replicas.get(locator)?.confirmed.record;
419
+ if (record === undefined || closed)
420
+ return;
421
+ const held = { text: record.documents, binaries: record.binaries };
422
+ const composition = compositionOf(record);
423
+ if (referencesIndexed(held, composition))
424
+ return;
425
+ await indexReferrers(held, composition);
426
+ }
427
+ }
428
+ async function prepareReplica(locator) {
429
+ await referenced(locator);
430
+ const replica = replicas.get(locator);
431
+ if (replica === undefined || closed || prepared.has(locator))
350
432
  return;
351
- if (locators === null)
352
- everyRoom = true;
353
- const sets = [];
354
- for (const locator of locators ?? [...replicas.keys()]) {
355
- if (materialized.has(locator))
356
- continue;
357
- materialized.add(locator);
358
- const replica = replicas.get(locator);
359
- if (replica !== undefined)
360
- sets.push(changeSet(locator, null, replica));
433
+ const visible = replica.visible;
434
+ const paths = [
435
+ ...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
436
+ ];
437
+ const entered = [];
438
+ projecting.add(locator);
439
+ try {
440
+ await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
441
+ const current = replicas.get(locator);
442
+ if (current === undefined || closed)
443
+ return;
444
+ const set = entering(locator, current, chunk);
445
+ readSide.apply(set);
446
+ entered.push(set);
447
+ });
448
+ if (!replicas.has(locator) || closed)
449
+ return;
450
+ prepared.add(locator);
361
451
  }
362
- if (sets.length === 0)
452
+ finally {
453
+ projecting.delete(locator);
454
+ }
455
+ conversations.changed(mergeChangeSets(entered));
456
+ readSide.recover();
457
+ queueReadiness();
458
+ }
459
+ function unprepared() {
460
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
461
+ }
462
+ function unbuilt(locator, error) {
463
+ unindexable.add(locator);
464
+ journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
465
+ queueReadiness();
466
+ }
467
+ function prepare(locators = []) {
468
+ for (const locator of locators)
469
+ unpreparedDemand.add(locator);
470
+ if (preparing)
363
471
  return;
364
- const set = mergeChangeSets(sets);
365
- readSide.apply(set);
366
- searchIndex.changed(set);
472
+ preparing = true;
473
+ void (async () => {
474
+ try {
475
+ for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
476
+ await yielded();
477
+ if (closed)
478
+ return;
479
+ unpreparedDemand.delete(waiting);
480
+ await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
481
+ }
482
+ }
483
+ finally {
484
+ preparing = false;
485
+ }
486
+ })();
367
487
  }
368
488
  function reachedRooms(plan) {
369
- if (plan.rooms !== null)
370
- return plan.rooms;
371
- if (plan.reach === null)
489
+ const reach = plan.reach;
490
+ const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
491
+ const named = plan.rooms ??
492
+ (reached === null
493
+ ? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
494
+ : [...reached]);
495
+ return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
496
+ }
497
+ function awaited(locator) {
498
+ return (!roomsListed ||
499
+ (rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
500
+ }
501
+ function covered(plan) {
502
+ const locators = reachedRooms(plan);
503
+ if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
504
+ return { pending: [] };
505
+ }
506
+ const pending = [];
507
+ const coverage = [];
508
+ const overlay = [];
509
+ for (const locator of locators) {
510
+ const replica = replicas.get(locator);
511
+ if (replica === undefined) {
512
+ if (awaited(locator))
513
+ pending.push({ room: locator, awaits: 'acquisition' });
514
+ continue;
515
+ }
516
+ if (!prepared.has(locator)) {
517
+ pending.push({ room: locator, awaits: 'preparation' });
518
+ continue;
519
+ }
520
+ coverage.push({
521
+ room: locator,
522
+ generation: replica.confirmed.record.transfer,
523
+ current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
524
+ });
525
+ for (const batch of replica.queue)
526
+ overlay.push(batch.id);
527
+ }
528
+ if (pending.length > 0)
529
+ return { pending };
530
+ return { coverage: { rooms: coverage, indexes: [], overlay } };
531
+ }
532
+ function awaitPreparation(cover) {
533
+ if (!('pending' in cover))
534
+ return;
535
+ const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
536
+ if (waiting.length > 0)
537
+ prepare(waiting.map(({ room }) => room));
538
+ }
539
+ function acquires(room) {
540
+ return (demanded(demand, room) ||
541
+ (replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
542
+ queuesWithoutReplica.has(room.locator));
543
+ }
544
+ function acquiredLocators() {
545
+ return [...discovered.values()]
546
+ .filter(acquires)
547
+ .map((room) => room.locator)
548
+ .sort();
549
+ }
550
+ function heldDemand(acquired) {
551
+ if (demand.whole)
552
+ return null;
553
+ return { rooms: acquired.map((room) => room.locator), conversations: demand.conversations };
554
+ }
555
+ function applyDemand(next) {
556
+ const before = acquiredLocators();
557
+ const conversed = demand.conversations;
558
+ demand = next;
559
+ const after = acquiredLocators();
560
+ queueReadiness();
561
+ if (!conversed && next.conversations)
562
+ scheduleCurrency();
563
+ if (JSON.stringify(before) === JSON.stringify(after) && conversed === next.conversations)
564
+ return;
565
+ journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
566
+ if (options.adapter.roomStream === undefined)
567
+ void sync('unstreamed');
568
+ else if (socketOpen)
569
+ options.adapter.roomStream.reopen();
570
+ }
571
+ function acquire(next) {
572
+ if (closed)
573
+ return;
574
+ if (narrowing !== null)
575
+ clearTimeout(narrowing);
576
+ narrowing = null;
577
+ const widening = wider(next, demand);
578
+ if (!widening && !narrower(next, demand))
579
+ return;
580
+ if (widening || !roomsListed) {
581
+ applyDemand(next);
582
+ return;
583
+ }
584
+ // A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
585
+ narrowing = setTimeout(() => {
586
+ narrowing = null;
587
+ applyDemand(next);
588
+ }, 5_000);
589
+ }
590
+ function requiredRooms(reach) {
591
+ if (reach === null)
592
+ return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
593
+ const named = [...new Set(reach.rooms)].sort();
594
+ if (roomsListed)
595
+ return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
596
+ return { listed: named.every((room) => replicas.has(room)), rooms: named };
597
+ }
598
+ function roomState(locator, search) {
599
+ if (sourceRefusals.has(locator))
600
+ return 'refused';
601
+ const listedRoom = discovered.get(locator);
602
+ if (roomsListed &&
603
+ (listedRoom === undefined ||
604
+ listedRoom.replica.admission !== 'admitted' ||
605
+ unreadable.has(locator)))
606
+ return 'unadmitted';
607
+ if (!replicas.has(locator))
608
+ return 'acquiring';
609
+ if (!prepared.has(locator))
610
+ return 'preparing';
611
+ if (search && !searchIndex.rooms().has(locator))
612
+ return 'indexing';
613
+ return 'complete';
614
+ }
615
+ function conversationCoverage(reach, search) {
616
+ const declared = reach === null || reach.conversations;
617
+ if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
618
+ return 'undeclared';
619
+ if (!conversations.known() || !conversations.current())
620
+ return 'acquiring';
621
+ if (!search)
622
+ return 'complete';
623
+ const unindexed = conversations
624
+ .searchable()
625
+ .filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
626
+ if (unindexed.length === 0)
627
+ return 'complete';
628
+ index([], unindexed);
629
+ return 'indexing';
630
+ }
631
+ function connectionOf() {
632
+ if (socketOpen)
633
+ return 'connected';
634
+ if (status.providerUnreachable)
635
+ return 'unreachable';
636
+ return options.adapter.roomStream === undefined && status.lastCompleted !== null
637
+ ? 'connected'
638
+ : 'connecting';
639
+ }
640
+ function refusedIn(locators) {
641
+ return locators.flatMap((room) => {
642
+ const held = sourceRefusals.get(room);
643
+ if (held === undefined)
644
+ return [];
645
+ const { file, location } = held.fields;
646
+ return [
647
+ {
648
+ room,
649
+ file: typeof file === 'string' ? file : '',
650
+ rule: held.code,
651
+ location: typeof location === 'string' ? location : null,
652
+ },
653
+ ];
654
+ });
655
+ }
656
+ function readinessFor(port, reach) {
657
+ const required = requiredRooms(reach);
658
+ const search = port.search === 'required';
659
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
660
+ const building = (state) => coverage
661
+ .filter((room) => room.state === state && !unindexable.has(room.room))
662
+ .map(({ room }) => room);
663
+ const preparing = building('preparing');
664
+ if (preparing.length > 0)
665
+ prepare(preparing);
666
+ const indexing = building('indexing');
667
+ if (indexing.length > 0)
668
+ index(indexing, []);
669
+ return readinessOf({
670
+ code: port.code,
671
+ coverage: {
672
+ listed: required.listed,
673
+ rooms: coverage,
674
+ conversations: conversationCoverage(reach, search),
675
+ search: search ? 'required' : 'unused',
676
+ },
677
+ persistence,
678
+ connection: connectionOf(),
679
+ house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
680
+ capacity: capacity !== null &&
681
+ coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
682
+ ? capacity
683
+ : port.capacity,
684
+ refused: refusedIn(required.rooms),
685
+ lost: storageLoss !== null,
686
+ established: targets[port.target]?.established === true,
687
+ });
688
+ }
689
+ function establish(name) {
690
+ targets = { ...targets, [name]: { established: true } };
691
+ queueReadiness();
692
+ void durably((local) => local.keepTargets(targets)).catch(() => {
693
+ journal.record(`target record failed ${name}`);
694
+ });
695
+ }
696
+ function deliverReadiness(watch) {
697
+ if (!readinessWatches.has(watch))
698
+ return;
699
+ const readiness = readinessFor(watch.port, watch.reach);
700
+ if (readiness.ready && targets[watch.port.target]?.established !== true) {
701
+ establish(watch.port.target);
702
+ }
703
+ const shown = JSON.stringify(readiness);
704
+ if (shown === watch.last)
705
+ return;
706
+ watch.last = shown;
707
+ watch.deliver(structuredClone(readiness));
708
+ }
709
+ function queueReadiness() {
710
+ if (readinessQueued || closed || readinessWatches.size === 0)
711
+ return;
712
+ readinessQueued = true;
713
+ queueMicrotask(() => {
714
+ readinessQueued = false;
715
+ if (closed)
716
+ return;
717
+ for (const watch of [...readinessWatches])
718
+ deliverReadiness(watch);
719
+ });
720
+ }
721
+ function watchReadiness(port, reach, deliver) {
722
+ const watch = { port, reach, deliver, last: null };
723
+ readinessWatches.add(watch);
724
+ void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
725
+ return () => {
726
+ readinessWatches.delete(watch);
727
+ };
728
+ }
729
+ function layerBytes(layer) {
730
+ let bytes = 0;
731
+ for (const [path, text] of Object.entries(layer.documents))
732
+ bytes += (path.length + text.length) * 2;
733
+ return bytes;
734
+ }
735
+ function grownBy(next, previous) {
736
+ if (previous === undefined)
737
+ return layerBytes(next);
738
+ const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
739
+ return layerBytes({ ...next, documents });
740
+ }
741
+ function exceeded(error) {
742
+ return (typeof error === 'object' &&
743
+ error !== null &&
744
+ error.name === 'QuotaExceededError');
745
+ }
746
+ async function estimated() {
747
+ const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
748
+ return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
749
+ }
750
+ function pause(locator, cause) {
751
+ paused.add(locator);
752
+ capacity = cause;
753
+ journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
754
+ queueReadiness();
755
+ if (rechecking !== null)
756
+ return;
757
+ // A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
758
+ rechecking = setInterval(() => {
759
+ void recheck();
760
+ }, 60_000);
761
+ }
762
+ async function recheck() {
763
+ const held = capacity;
764
+ if (held === null || closed)
765
+ return;
766
+ const available = await estimated().catch(() => null);
767
+ if (available === null || available < (held.needed ?? 0))
768
+ return;
769
+ if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
770
+ return;
771
+ resume();
772
+ }
773
+ function resume() {
774
+ if (rechecking !== null)
775
+ clearInterval(rechecking);
776
+ rechecking = null;
777
+ if (paused.size === 0 && capacity === null)
778
+ return;
779
+ paused.clear();
780
+ capacity = null;
781
+ journal.record('capacity resumed');
782
+ queueReadiness();
783
+ if (!closed && authorization !== undefined)
784
+ void sync('unstreamed');
785
+ }
786
+ async function reclaimed(local) {
787
+ const unreferenced = [...transfers.keys()].filter((locator) => {
788
+ const room = discovered.get(locator);
789
+ return room === undefined || !acquires(room);
790
+ });
791
+ for (const locator of unreferenced) {
792
+ await local.abandon(locator);
793
+ transfers.delete(locator);
794
+ }
795
+ if (unreferenced.length > 0)
796
+ journal.record(`capacity reclaimed ${unreferenced.length}`);
797
+ return unreferenced.length > 0;
798
+ }
799
+ async function admitted(local, locator, needed) {
800
+ if (paused.has(locator))
801
+ return false;
802
+ let available = await estimated().catch(() => null);
803
+ if (available === null || available >= needed)
804
+ return true;
805
+ if (await reclaimed(local))
806
+ available = await estimated().catch(() => null);
807
+ if (available === null || available >= needed)
808
+ return true;
809
+ pause(locator, { cause: 'insufficient-space', needed, available });
810
+ return false;
811
+ }
812
+ async function exceededOn(locator, bytes) {
813
+ const available = await estimated().catch(() => null);
814
+ pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
815
+ }
816
+ function seedRoomHeld(room, version) {
817
+ return prepared.has(room) && reachedBy(room, version);
818
+ }
819
+ function seedHeld(at) {
820
+ return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
821
+ }
822
+ function seedApplies(plan) {
823
+ const seed = seeds.get(plan.key);
824
+ if (seed === undefined)
372
825
  return null;
373
- return [...plan.reach.rooms, ...plan.reach.documents.map(roomOfKey)];
826
+ if (!seedHeld(seed.at))
827
+ return seed;
828
+ seeds.delete(plan.key);
829
+ return null;
830
+ }
831
+ function seedBehind(plan, cover) {
832
+ const seed = seedApplies(plan);
833
+ if (seed === null || 'pending' in cover)
834
+ return cover;
835
+ return {
836
+ pending: Object.entries(seed.at)
837
+ .filter(([room, version]) => !seedRoomHeld(room, version))
838
+ .map(([room]) => ({ room, awaits: 'acquisition' })),
839
+ };
840
+ }
841
+ function seed(entry) {
842
+ if (seedHeld(entry.at))
843
+ return;
844
+ for (const { query, rows } of entry.queries) {
845
+ const key = queryKey(query);
846
+ seeds.set(key, { rows, at: entry.at });
847
+ for (const shown of seedWatchers.get(key) ?? [])
848
+ shown();
849
+ }
850
+ }
851
+ async function rowsQuery(plan) {
852
+ const before = covered(plan);
853
+ awaitPreparation(before);
854
+ const seeded = seedApplies(plan);
855
+ if (seeded !== null)
856
+ return seededResult(seeded).result;
857
+ if ('pending' in before)
858
+ return before;
859
+ const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
860
+ awaitPreparation(cover);
861
+ if ('pending' in cover)
862
+ return cover;
863
+ return 'groups' in rows
864
+ ? { groups: rows.groups, coverage: cover.coverage }
865
+ : { documents: rows.documents, coverage: cover.coverage };
866
+ }
867
+ async function indexConversation(conversation) {
868
+ searchIndex.indexMessages(conversation, [], false);
869
+ let after = null;
870
+ for (;;) {
871
+ const batch = await durably((local) => local.messagesAfter(conversation, after));
872
+ if (closed)
873
+ return;
874
+ searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
875
+ if (batch.length === 0)
876
+ return;
877
+ after = batch[batch.length - 1].identity;
878
+ }
374
879
  }
375
- function rowsQuery(plan) {
376
- materialize(reachedRooms(plan));
377
- return readSide.query(plan);
880
+ function indexNext() {
881
+ const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
882
+ if (room !== undefined) {
883
+ roomsToIndex.delete(room);
884
+ return () => prepared.has(room)
885
+ ? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
886
+ : undefined;
887
+ }
888
+ const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
889
+ if (conversation === undefined)
890
+ return null;
891
+ conversationsToIndex.delete(conversation);
892
+ return () => indexConversation(conversation);
893
+ }
894
+ function index(rooms, named) {
895
+ for (const room of rooms)
896
+ roomsToIndex.add(room);
897
+ for (const conversation of named)
898
+ conversationsToIndex.add(conversation);
899
+ if (indexing)
900
+ return;
901
+ indexing = true;
902
+ void (async () => {
903
+ try {
904
+ for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
905
+ await yielded();
906
+ await unit();
907
+ queueReadiness();
908
+ }
909
+ }
910
+ finally {
911
+ indexing = false;
912
+ }
913
+ })().catch(() => { });
378
914
  }
379
915
  function publishChangeSet(set) {
380
916
  applyChangeSet(set);
@@ -391,34 +927,7 @@ export function composeRuntime(options) {
391
927
  emitPresentation(app);
392
928
  conversations.changed(set);
393
929
  }
394
- function stopTimer() {
395
- if (resetTimer === undefined)
396
- return;
397
- clearTimeout(resetTimer);
398
- resetTimer = undefined;
399
- }
400
- function armTimer() {
401
- stopTimer();
402
- if (closed || stopping)
403
- return;
404
- let earliest = null;
405
- for (const { submission } of pendings.values()) {
406
- if (submission.state !== 'reset')
407
- continue;
408
- const at = Date.parse(submission.reset);
409
- if (earliest === null || at < earliest)
410
- earliest = at;
411
- }
412
- if (earliest === null)
413
- return;
414
- // A Save House held back goes again only once the reset its answer named has passed.
415
- resetTimer = setTimeout(() => {
416
- resetTimer = undefined;
417
- submitAll();
418
- }, Math.max(0, earliest - Date.now()));
419
- }
420
930
  async function refuse(reason, provider = null) {
421
- stopTimer();
422
931
  discoveryPaused = true;
423
932
  try {
424
933
  if (reason === 'revoked-authorization') {
@@ -503,6 +1012,9 @@ export function composeRuntime(options) {
503
1012
  if (fence !== undefined && fence.erased && fence.at < started)
504
1013
  fences.delete(room.locator);
505
1014
  }
1015
+ await loseRooms(found
1016
+ .filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
1017
+ .map((room) => room.locator));
506
1018
  const listed = found.filter((room) => !fences.has(room.locator));
507
1019
  const listedLocators = new Set(listed.map((room) => room.locator));
508
1020
  for (const locator of listedLocators)
@@ -527,6 +1039,12 @@ export function composeRuntime(options) {
527
1039
  emitRooms();
528
1040
  return listed;
529
1041
  }
1042
+ function heldLocally(locator) {
1043
+ return (replicas.has(locator) ||
1044
+ roomDrafts.has(locator) ||
1045
+ transfers.has(locator) ||
1046
+ queuesWithoutReplica.has(locator));
1047
+ }
530
1048
  function unlistedHeld(listed) {
531
1049
  return [
532
1050
  ...new Set([
@@ -563,10 +1081,14 @@ export function composeRuntime(options) {
563
1081
  rooms.delete(locator);
564
1082
  roomDrafts.delete(locator);
565
1083
  transfers.delete(locator);
1084
+ catching.delete(locator);
566
1085
  discovered.delete(locator);
567
1086
  announced.delete(locator);
568
1087
  unreadable.delete(locator);
569
1088
  stale.delete(locator);
1089
+ repairable.delete(locator);
1090
+ unindexable.delete(locator);
1091
+ paused.delete(locator);
570
1092
  scopedRefusals.delete(locator);
571
1093
  sourceRefusals.delete(locator);
572
1094
  declarationRefusals.delete(locator);
@@ -582,7 +1104,9 @@ export function composeRuntime(options) {
582
1104
  return fresh;
583
1105
  publishChangeSet(mergeChangeSets(removed));
584
1106
  for (const locator of fresh)
585
- materialized.delete(locator);
1107
+ prepared.delete(locator);
1108
+ searchIndex.forget(fresh);
1109
+ readSide.recover();
586
1110
  moveTo({ ...status, ...attention() });
587
1111
  return fresh;
588
1112
  }
@@ -618,7 +1142,7 @@ export function composeRuntime(options) {
618
1142
  return model;
619
1143
  }
620
1144
  async function hydrate(opened) {
621
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
1145
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
622
1146
  opened.all(),
623
1147
  opened.queues(),
624
1148
  opened.roomDrafts(),
@@ -630,7 +1154,9 @@ export function composeRuntime(options) {
630
1154
  opened.capabilities(),
631
1155
  opened.member(),
632
1156
  opened.transfers(),
1157
+ opened.targets(),
633
1158
  ]);
1159
+ targets = heldTargets;
634
1160
  transfers.clear();
635
1161
  for (const record of staged)
636
1162
  transfers.set(record.locator, record);
@@ -692,6 +1218,7 @@ export function composeRuntime(options) {
692
1218
  async function readState(opened) {
693
1219
  const { lastCompleted, locators, read } = await hydrate(opened);
694
1220
  applyChangeSet(read);
1221
+ prepare();
695
1222
  if (replicas.size > 0)
696
1223
  emitRooms();
697
1224
  if (member !== null)
@@ -807,17 +1334,19 @@ export function composeRuntime(options) {
807
1334
  moveTo({ ...status, ...attention() });
808
1335
  throw error;
809
1336
  }
810
- queuesWithoutReplica.delete(locator);
811
- for (const { batch, returned } of unqueued) {
812
- const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
813
- journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
814
- }
815
- replicas.set(locator, replicaOf(confirmed, kept, before));
816
- return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
817
- room: settling.room,
818
- queue: kept.length !== previousQueue.length,
819
- drafts: [...written.keys()],
820
- });
1337
+ return () => {
1338
+ queuesWithoutReplica.delete(locator);
1339
+ for (const { batch, returned } of unqueued) {
1340
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
1341
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1342
+ }
1343
+ replicas.set(locator, replicaOf(confirmed, kept, before));
1344
+ return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
1345
+ room: settling.room,
1346
+ queue: kept.length !== previousQueue.length,
1347
+ drafts: [...written.keys()],
1348
+ });
1349
+ };
821
1350
  }
822
1351
  function adoptModels(locator, record) {
823
1352
  for (const [attached, model] of [
@@ -853,8 +1382,13 @@ export function composeRuntime(options) {
853
1382
  function publish(local, room, confirmed, landing, live) {
854
1383
  const { base, stored } = landing;
855
1384
  return exclusive(async () => {
856
- if (!live())
1385
+ if (stored !== undefined) {
1386
+ await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
1387
+ }
1388
+ if (!live()) {
1389
+ caughtUpTo(room.locator);
857
1390
  return true;
1391
+ }
858
1392
  const prior = replicas.get(room.locator);
859
1393
  if (base !== undefined &&
860
1394
  (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
@@ -868,17 +1402,33 @@ export function composeRuntime(options) {
868
1402
  const accepted = pendings.get(batch.id)?.accepted;
869
1403
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
870
1404
  });
871
- const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
872
- if (stored !== undefined)
1405
+ let settled;
1406
+ try {
1407
+ settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
1408
+ }
1409
+ catch (error) {
1410
+ if (!exceeded(error))
1411
+ throw error;
1412
+ await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
1413
+ caughtUpTo(room.locator);
1414
+ return true;
1415
+ }
1416
+ const set = settled();
1417
+ if (stored !== undefined) {
873
1418
  transfers.delete(room.locator);
1419
+ catching.delete(room.locator);
1420
+ }
874
1421
  for (const batch of accepted)
875
1422
  pendings.delete(batch.id);
876
1423
  stale.delete(room.locator);
1424
+ unindexable.delete(room.locator);
877
1425
  if (unreadable.delete(room.locator))
878
1426
  scopedRefusals.delete(room.locator);
879
1427
  sourceRefusals.delete(room.locator);
880
1428
  declarationRefusals.delete(room.locator);
881
1429
  list(room, true);
1430
+ if (prior === undefined)
1431
+ prepared.add(room.locator);
882
1432
  publishChangeSet(set);
883
1433
  moveTo({ ...status, ...attention() });
884
1434
  options.adapter.roomStream?.published(room.locator, roomVersion);
@@ -887,6 +1437,7 @@ export function composeRuntime(options) {
887
1437
  }
888
1438
  async function saveRoomDrafts(locator, named, writtenBy) {
889
1439
  const local = await openLocal();
1440
+ await referenced(locator);
890
1441
  const replica = replicas.get(locator);
891
1442
  const room = rooms.get(locator);
892
1443
  if (replica === undefined || room === undefined) {
@@ -934,6 +1485,11 @@ export function composeRuntime(options) {
934
1485
  changes,
935
1486
  bases: parts.flatMap(({ bases }) => bases),
936
1487
  writtenBy,
1488
+ clean: {
1489
+ text: replica.confirmed.record.documents,
1490
+ binaries: replica.confirmed.record.binaries,
1491
+ composition: compositionOf(replica.confirmed.record),
1492
+ },
937
1493
  };
938
1494
  const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
939
1495
  if (checked?.kind === 'refused')
@@ -1274,8 +1830,20 @@ export function composeRuntime(options) {
1274
1830
  stage: (record, page) => exclusive(async () => {
1275
1831
  if (!live())
1276
1832
  return false;
1277
- await local.stage(record, page);
1833
+ const bytes = layerBytes(page);
1834
+ if (!(await admitted(local, record.locator, bytes)))
1835
+ return false;
1836
+ try {
1837
+ await local.stage(record, page);
1838
+ }
1839
+ catch (error) {
1840
+ if (!exceeded(error))
1841
+ throw error;
1842
+ await exceededOn(record.locator, bytes);
1843
+ return false;
1844
+ }
1278
1845
  transfers.set(record.locator, record);
1846
+ readSide.recover();
1279
1847
  return true;
1280
1848
  }),
1281
1849
  abandon: (locator) => exclusive(async () => {
@@ -1283,6 +1851,7 @@ export function composeRuntime(options) {
1283
1851
  return;
1284
1852
  await local.abandon(locator);
1285
1853
  transfers.delete(locator);
1854
+ readSide.recover();
1286
1855
  }),
1287
1856
  };
1288
1857
  }
@@ -1295,6 +1864,10 @@ export function composeRuntime(options) {
1295
1864
  async function pullBootstrap(authorized, room, local, live) {
1296
1865
  if (!live())
1297
1866
  return unconfirmed;
1867
+ if (replicas.has(room.locator)) {
1868
+ catching.add(room.locator);
1869
+ readSide.recover();
1870
+ }
1298
1871
  try {
1299
1872
  const result = await bootstrapRoom(options.adapter, authorized, room, {
1300
1873
  collection: modelOf(room.locator),
@@ -1302,14 +1875,24 @@ export function composeRuntime(options) {
1302
1875
  }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
1303
1876
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
1304
1877
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1878
+ if (result.kind !== 'staged')
1879
+ caughtUpTo(room.locator);
1305
1880
  return result;
1306
1881
  }
1307
1882
  catch (error) {
1308
1883
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
1884
+ caughtUpTo(room.locator);
1309
1885
  throw error;
1310
1886
  }
1311
1887
  }
1888
+ function caughtUpTo(locator) {
1889
+ if (catching.delete(locator))
1890
+ readSide.recover();
1891
+ }
1312
1892
  async function advance(room, local, authorized, live) {
1893
+ await referenced(room.locator);
1894
+ if (!live() || paused.has(room.locator))
1895
+ return unconfirmed;
1313
1896
  const held = replicas.get(room.locator);
1314
1897
  if (held !== undefined) {
1315
1898
  const refreshed = await pullRefresh(authorized, held.confirmed, live);
@@ -1340,6 +1923,7 @@ export function composeRuntime(options) {
1340
1923
  return refreshed;
1341
1924
  if (opened !== null) {
1342
1925
  owed.add(room.locator);
1926
+ readSide.recover();
1343
1927
  return unconfirmed;
1344
1928
  }
1345
1929
  }
@@ -1352,10 +1936,19 @@ export function composeRuntime(options) {
1352
1936
  return null;
1353
1937
  }
1354
1938
  async function withhold(withheld, room, local, progress) {
1939
+ if (withheld.kind === 'unconfirmed')
1940
+ return;
1355
1941
  if (withheld.kind === 'refused')
1356
1942
  stale.delete(room.locator);
1357
1943
  else
1358
1944
  stale.add(room.locator);
1945
+ if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
1946
+ const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
1947
+ repairable.set(room.locator, reset);
1948
+ repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
1949
+ }
1950
+ else
1951
+ repairable.delete(room.locator);
1359
1952
  if (withheld.kind === 'scoped-refusal') {
1360
1953
  await scoped(withheld, room, local);
1361
1954
  }
@@ -1391,6 +1984,7 @@ export function composeRuntime(options) {
1391
1984
  unreadable.add(room.locator);
1392
1985
  list(room, false);
1393
1986
  emitRooms();
1987
+ moveTo({ ...status, ...attention() });
1394
1988
  journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
1395
1989
  }
1396
1990
  function leaving(batch, result) {
@@ -1412,6 +2006,7 @@ export function composeRuntime(options) {
1412
2006
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1413
2007
  }
1414
2008
  async function adoptAccepted(room, local, submitted, accepted) {
2009
+ await referenced(room.locator);
1415
2010
  if (!continuing())
1416
2011
  return unconfirmed;
1417
2012
  const current = replicas.get(room.locator);
@@ -1433,11 +2028,12 @@ export function composeRuntime(options) {
1433
2028
  emitQueue(room.locator);
1434
2029
  return applied;
1435
2030
  }
1436
- const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
2031
+ const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1437
2032
  pendings.delete(submitted.id);
1438
2033
  unreadable.delete(room.locator);
1439
2034
  sourceRefusals.delete(room.locator);
1440
2035
  declarationRefusals.delete(room.locator);
2036
+ const set = settled();
1441
2037
  list(room, true);
1442
2038
  publishChangeSet(set);
1443
2039
  moveTo({ ...status, ...attention() });
@@ -1451,14 +2047,14 @@ export function composeRuntime(options) {
1451
2047
  refusedSource(room, failed);
1452
2048
  return false;
1453
2049
  }
1454
- async function submitRoom(room, local, authorized, progress) {
1455
- while (continuing() && authorization !== undefined) {
2050
+ async function submitRoom(room, local, authorized, progress, signal) {
2051
+ while (continuing() && authorization !== undefined && !signal.aborted) {
1456
2052
  const replica = replicas.get(room.locator);
1457
2053
  const head = replica?.queue[0];
1458
2054
  if (replica === undefined || head === undefined)
1459
- return;
2055
+ return 'done';
1460
2056
  if (pendings.get(head.id)?.accepted !== undefined)
1461
- return;
2057
+ return 'done';
1462
2058
  const state = pendings.get(head.id);
1463
2059
  const { submission, lost: earlier } = state ?? {
1464
2060
  submission: { state: 'waiting' },
@@ -1466,24 +2062,30 @@ export function composeRuntime(options) {
1466
2062
  };
1467
2063
  if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
1468
2064
  progress.complete = false;
1469
- return;
2065
+ return { at: Date.parse(submission.reset) };
1470
2066
  }
1471
2067
  if (writesHeld()) {
1472
2068
  progress.complete = false;
1473
2069
  journal.record(`submission ${room.locator} ${head.id} held`);
1474
- return;
2070
+ return 'done';
1475
2071
  }
1476
2072
  const lost = earlier || submission.state === 'submitted';
2073
+ if (lost && pastHorizon(head.id)) {
2074
+ await discardPending(room, local, signal);
2075
+ return 'done';
2076
+ }
1477
2077
  let result;
1478
2078
  journal.record(`submission ${room.locator} ${head.id}`);
1479
2079
  await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1480
2080
  if (discoveryPaused || fences.has(room.locator))
1481
- return;
1482
- if (writesHeld()) {
2081
+ return 'done';
2082
+ if (signal.aborted || writesHeld()) {
1483
2083
  await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
2084
+ if (signal.aborted)
2085
+ return 'done';
1484
2086
  progress.complete = false;
1485
2087
  journal.record(`submission ${room.locator} ${head.id} held`);
1486
- return;
2088
+ return 'done';
1487
2089
  }
1488
2090
  const opensBefore = openSignals;
1489
2091
  const answered = awaitingAnswer(room.locator);
@@ -1499,32 +2101,36 @@ export function composeRuntime(options) {
1499
2101
  progress.complete = false;
1500
2102
  if (continuing())
1501
2103
  progress.reachable = false;
1502
- return;
2104
+ return 'again';
2105
+ }
2106
+ if (signal.aborted) {
2107
+ answered();
2108
+ return 'done';
1503
2109
  }
1504
2110
  if (result.kind !== 'accepted' || !continuing())
1505
2111
  answered();
1506
2112
  if (!continuing())
1507
- return;
2113
+ return 'done';
1508
2114
  if (result.kind === 'transport-uncertainty') {
1509
2115
  progress.complete = false;
1510
2116
  progress.reachable = false;
1511
- return;
2117
+ return 'again';
1512
2118
  }
1513
2119
  if (result.kind === 'room-not-readable') {
1514
2120
  progress.complete = false;
1515
2121
  unreadable.add(room.locator);
1516
2122
  list(room, false);
1517
2123
  emitRooms();
1518
- return;
2124
+ return 'done';
1519
2125
  }
1520
2126
  if (result.kind === 'revoked-authorization') {
1521
2127
  await refuse('revoked-authorization');
1522
- return;
2128
+ return 'done';
1523
2129
  }
1524
2130
  if (result.kind === 'scoped-refusal') {
1525
2131
  await scoped(result, room, local);
1526
2132
  progress.complete = false;
1527
- return;
2133
+ return 'done';
1528
2134
  }
1529
2135
  if (result.kind === 'write-halted') {
1530
2136
  journal.record(`outcome ${room.locator} ${head.id} write-halted`);
@@ -1533,16 +2139,24 @@ export function composeRuntime(options) {
1533
2139
  continue;
1534
2140
  haltedRefusal = true;
1535
2141
  progress.complete = false;
1536
- return;
2142
+ return 'done';
1537
2143
  }
1538
2144
  if (result.kind === 'conflicted' ||
1539
2145
  result.kind === 'rejected' ||
1540
2146
  result.kind === 'expired' ||
1541
2147
  result.kind === 'retryable-refusal') {
2148
+ if (result.kind === 'expired' && lost && result.restored !== true) {
2149
+ journal.record(`outcome ${room.locator} ${head.id} expired`);
2150
+ await discardPending(room, local, signal);
2151
+ return 'done';
2152
+ }
1542
2153
  if (result.kind === 'expired') {
2154
+ await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
1543
2155
  const withheld = await advance(room, local, authorized, continuing);
1544
- if (withheld !== null)
1545
- return withhold(withheld, room, local, progress);
2156
+ if (withheld !== null) {
2157
+ await withhold(withheld, room, local, progress);
2158
+ return 'done';
2159
+ }
1546
2160
  }
1547
2161
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
1548
2162
  (result.kind === 'conflicted' && result.roomVersion !== undefined
@@ -1555,8 +2169,7 @@ export function composeRuntime(options) {
1555
2169
  const reset = result.reset;
1556
2170
  progress.complete = false;
1557
2171
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1558
- armTimer();
1559
- return;
2172
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
1560
2173
  }
1561
2174
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
1562
2175
  const left = leaving(head, result);
@@ -1567,28 +2180,73 @@ export function composeRuntime(options) {
1567
2180
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
1568
2181
  return;
1569
2182
  }
1570
- const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
2183
+ const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1571
2184
  leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1572
2185
  (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1573
2186
  ? { ...left, returned: null }
1574
2187
  : left,
1575
2188
  });
1576
- publishChangeSet(set);
2189
+ publishChangeSet(settled());
1577
2190
  moveTo({ ...status, ...attention() });
1578
2191
  });
1579
2192
  if (result.kind === 'expired')
1580
2193
  continue;
1581
2194
  const withheld = await advance(room, local, authorized, continuing);
1582
- if (withheld !== null)
1583
- return withhold(withheld, room, local, progress);
2195
+ if (withheld !== null) {
2196
+ await withhold(withheld, room, local, progress);
2197
+ return 'done';
2198
+ }
1584
2199
  continue;
1585
2200
  }
1586
2201
  const kept = await adopted(room, local, head, result).finally(answered);
1587
2202
  if (!kept) {
1588
2203
  progress.complete = false;
1589
- return;
2204
+ return 'done';
1590
2205
  }
1591
2206
  }
2207
+ return 'done';
2208
+ }
2209
+ function pastHorizon(identity) {
2210
+ return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
2211
+ }
2212
+ async function discardPending(room, local, signal) {
2213
+ const { locator } = room;
2214
+ const discarded = await exclusive(async () => {
2215
+ if (fences.has(locator))
2216
+ return false;
2217
+ const before = replicas.get(locator);
2218
+ const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
2219
+ fences.set(locator, { at: discoveries, erased: false });
2220
+ lastFenced.set(locator, ++fencings);
2221
+ try {
2222
+ await local.discard(locator);
2223
+ }
2224
+ finally {
2225
+ fences.delete(locator);
2226
+ }
2227
+ for (const batch of queue)
2228
+ pendings.delete(batch.id);
2229
+ queuesWithoutReplica.delete(locator);
2230
+ const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
2231
+ if (after !== null)
2232
+ replicas.set(locator, after);
2233
+ journal.record(`room discarded ${locator}`);
2234
+ publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
2235
+ moveTo({ ...status, ...attention() });
2236
+ return true;
2237
+ });
2238
+ if (!discarded)
2239
+ return;
2240
+ const progress = { complete: true, reachable: true };
2241
+ await roomTask(locator, async (live) => {
2242
+ await Promise.race([
2243
+ reload(room, local, progress, () => live() && !signal.aborted),
2244
+ abandonedOn(signal),
2245
+ ]);
2246
+ });
2247
+ if (signal.aborted)
2248
+ return;
2249
+ await rest(progress);
1592
2250
  }
1593
2251
  function awaitingAnswer(locator) {
1594
2252
  let release = () => { };
@@ -1668,6 +2326,11 @@ export function composeRuntime(options) {
1668
2326
  });
1669
2327
  }
1670
2328
  async function scoped(result, room, local) {
2329
+ if ((result.scope === 'content' || result.scope === 'sync') && !result.retryable) {
2330
+ journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
2331
+ await loseRooms([room.locator]);
2332
+ return;
2333
+ }
1671
2334
  scopedRefusals.set(room.locator, {
1672
2335
  code: result.code,
1673
2336
  fields: result.fields,
@@ -1714,15 +2377,16 @@ export function composeRuntime(options) {
1714
2377
  });
1715
2378
  }
1716
2379
  async function synchronize(listed, local, progress, attempt, live) {
1717
- const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
2380
+ const acquired = listed.filter(acquires);
2381
+ const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
1718
2382
  if (!live())
1719
2383
  return;
1720
- const ready = listed.filter((_, index) => {
2384
+ const ready = acquired.filter((_, index) => {
1721
2385
  const outcome = prepared[index];
1722
2386
  return outcome?.status === 'fulfilled' && outcome.value;
1723
2387
  });
1724
2388
  if (attempt === 'opened') {
1725
- options.adapter.roomStream?.hold(heldRooms(ready));
2389
+ options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
1726
2390
  handed = true;
1727
2391
  }
1728
2392
  discovering?.release();
@@ -1750,12 +2414,7 @@ export function composeRuntime(options) {
1750
2414
  await retainModel(local, room, 'collection', modelOf(room.locator));
1751
2415
  if (discoveryPaused || authorization === undefined)
1752
2416
  return false;
1753
- if (room.replica.admission !== 'admitted') {
1754
- if (replicas.has(room.locator))
1755
- progress.complete = false;
1756
- return false;
1757
- }
1758
- return true;
2417
+ return room.replica.admission === 'admitted';
1759
2418
  }
1760
2419
  async function caughtUp(room, local, progress, live) {
1761
2420
  const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
@@ -1789,17 +2448,22 @@ export function composeRuntime(options) {
1789
2448
  await caughtUp(room, local, progress, live);
1790
2449
  }
1791
2450
  async function reload(room, local, progress, live) {
1792
- if (!live() || authorization === undefined)
1793
- return;
1794
- const bootstrapped = await pullBootstrap(authorization, room, local, live);
1795
- if (!live())
1796
- return;
1797
- if (bootstrapped.kind !== 'staged') {
1798
- await withhold(bootstrapped, room, local, progress);
1799
- return;
2451
+ try {
2452
+ if (!live() || authorization === undefined)
2453
+ return;
2454
+ const bootstrapped = await pullBootstrap(authorization, room, local, live);
2455
+ if (!live())
2456
+ return;
2457
+ if (bootstrapped.kind !== 'staged') {
2458
+ await withhold(bootstrapped, room, local, progress);
2459
+ return;
2460
+ }
2461
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
2462
+ await caughtUp(room, local, progress, live);
2463
+ }
2464
+ finally {
2465
+ caughtUpTo(room.locator);
1800
2466
  }
1801
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
1802
- await caughtUp(room, local, progress, live);
1803
2467
  }
1804
2468
  async function discoverRooms(local, progress, attempt, live) {
1805
2469
  if (closed || authorization === undefined) {
@@ -1875,6 +2539,7 @@ export function composeRuntime(options) {
1875
2539
  }
1876
2540
  const previousCapabilities = capabilities;
1877
2541
  capabilities = result.capabilities;
2542
+ outcomeRetention = result.retention.outcome;
1878
2543
  notifyReady();
1879
2544
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1880
2545
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
@@ -2004,6 +2669,8 @@ export function composeRuntime(options) {
2004
2669
  const progress = underway?.generation === generation ? underway.progress : null;
2005
2670
  const cycle = progress ?? { complete: true, reachable: true };
2006
2671
  const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
2672
+ if (opened === generation)
2673
+ opened = null;
2007
2674
  return task(OPENING_LANE, async (live) => {
2008
2675
  underway = null;
2009
2676
  if (!live())
@@ -2016,8 +2683,6 @@ export function composeRuntime(options) {
2016
2683
  await roomsCarried(expired);
2017
2684
  if (!live())
2018
2685
  return;
2019
- if (opened === generation)
2020
- opened = null;
2021
2686
  const replaced = new Set(owed);
2022
2687
  owed.clear();
2023
2688
  try {
@@ -2025,6 +2690,9 @@ export function composeRuntime(options) {
2025
2690
  const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
2026
2691
  const reloading = held.filter((room) => !expired.includes(room.locator) &&
2027
2692
  (replaced.has(room.locator) || revoked(room.locator)));
2693
+ for (const room of reloading)
2694
+ catching.add(room.locator);
2695
+ readSide.recover();
2028
2696
  const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
2029
2697
  const reloaded = [...(await reloads), ...(await replacing)];
2030
2698
  if (progress.reachable) {
@@ -2069,38 +2737,94 @@ export function composeRuntime(options) {
2069
2737
  function submitLater(locator) {
2070
2738
  if (localSaves)
2071
2739
  return;
2072
- const running = submitting.get(locator);
2073
- if (running !== undefined) {
2074
- running.again = true;
2740
+ void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
2741
+ }
2742
+ async function submitPass(locator, signal) {
2743
+ const room = discovered.get(locator);
2744
+ if (closed || stopping || discoveryPaused || room === undefined)
2745
+ return 'done';
2746
+ const progress = { complete: true, reachable: true };
2747
+ let next;
2748
+ try {
2749
+ await ensureAuthorization();
2750
+ if (authorization === undefined)
2751
+ return 'done';
2752
+ next = await submitRoom(room, await openLocal(), authorization, progress, signal);
2753
+ }
2754
+ catch {
2755
+ progress.reachable = false;
2756
+ next = 'again';
2757
+ }
2758
+ if (signal.aborted)
2759
+ return 'done';
2760
+ await rest(progress);
2761
+ return next === 'again' && !socketOpen ? 'done' : next;
2762
+ }
2763
+ function repairLater(locator, when) {
2764
+ if (!socketOpen || repairing.has(locator))
2075
2765
  return;
2766
+ void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
2767
+ }
2768
+ async function repairPass(locator, signal) {
2769
+ const room = discovered.get(locator);
2770
+ if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
2771
+ return 'done';
2772
+ if (room === undefined || room.replica.admission !== 'admitted')
2773
+ return 'done';
2774
+ const progress = { complete: true, reachable: true };
2775
+ repairing.add(locator);
2776
+ try {
2777
+ const local = await openLocal();
2778
+ await roomTask(locator, async (live) => {
2779
+ const current = () => live() && !signal.aborted;
2780
+ if (!current() || authorization === undefined)
2781
+ return;
2782
+ const withheld = await Promise.race([
2783
+ advance(room, local, authorization, current),
2784
+ abandonedOn(signal),
2785
+ ]);
2786
+ if (withheld !== null && current())
2787
+ await withhold(withheld, room, local, progress);
2788
+ });
2076
2789
  }
2077
- const run = { again: false, done: Promise.resolve() };
2078
- submitting.set(locator, run);
2079
- run.done = (async () => {
2080
- try {
2081
- do {
2082
- run.again = false;
2083
- const room = discovered.get(locator);
2084
- if (closed || stopping || discoveryPaused || room === undefined)
2085
- return;
2086
- const progress = { complete: true, reachable: true };
2087
- try {
2088
- await ensureAuthorization();
2089
- if (authorization === undefined)
2090
- return;
2091
- await submitRoom(room, await openLocal(), authorization, progress);
2092
- }
2093
- catch {
2094
- progress.reachable = false;
2095
- }
2096
- armTimer();
2097
- await rest(progress);
2098
- } while (run.again);
2099
- }
2100
- finally {
2101
- submitting.delete(locator);
2102
- }
2103
- })();
2790
+ catch {
2791
+ stale.add(locator);
2792
+ progress.reachable = false;
2793
+ }
2794
+ finally {
2795
+ repairing.delete(locator);
2796
+ }
2797
+ if (signal.aborted)
2798
+ return 'done';
2799
+ await rest(progress);
2800
+ if (!stale.has(locator)) {
2801
+ if ((replicas.get(locator)?.queue.length ?? 0) > 0)
2802
+ submitLater(locator);
2803
+ return 'done';
2804
+ }
2805
+ if (!repairable.has(locator))
2806
+ return 'done';
2807
+ const reset = repairable.get(locator);
2808
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
2809
+ }
2810
+ function resendLater() {
2811
+ if (!socketOpen)
2812
+ return;
2813
+ void recovery.attempt('resend', resendPass, { again: true });
2814
+ }
2815
+ async function resendPass(signal) {
2816
+ if (!socketOpen || closed || stopping || discoveryPaused)
2817
+ return 'done';
2818
+ const reading = { complete: true, reachable: true };
2819
+ try {
2820
+ await conversations.resend(reading);
2821
+ }
2822
+ catch {
2823
+ reading.reachable = false;
2824
+ }
2825
+ if (signal.aborted)
2826
+ return 'done';
2827
+ return reading.reachable && reading.complete ? 'done' : 'again';
2104
2828
  }
2105
2829
  function submitAll() {
2106
2830
  for (const [locator, replica] of replicas) {
@@ -2115,15 +2839,21 @@ export function composeRuntime(options) {
2115
2839
  return;
2116
2840
  }
2117
2841
  submitAll();
2842
+ for (const locator of stale)
2843
+ if (repairable.has(locator))
2844
+ repairLater(locator, {});
2118
2845
  await conversations.reconcile();
2119
2846
  if (status.providerUnreachable)
2120
2847
  conversations.reconnected();
2121
2848
  const reading = { complete: true, reachable: true };
2122
- await conversations.catchUp(reading);
2849
+ if (demand.conversations)
2850
+ await conversations.catchUp(reading);
2123
2851
  if (progress.reachable && reading.reachable)
2124
2852
  await conversations.resend(reading);
2125
- if (!reading.reachable)
2853
+ if (!reading.reachable) {
2126
2854
  progress.reachable = false;
2855
+ resendLater();
2856
+ }
2127
2857
  }
2128
2858
  catch {
2129
2859
  progress.complete = false;
@@ -2180,7 +2910,7 @@ export function composeRuntime(options) {
2180
2910
  return;
2181
2911
  const room = discovered.get(locator);
2182
2912
  const held = replicas.get(locator);
2183
- if (room === undefined || room.replica.admission !== 'admitted')
2913
+ if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
2184
2914
  return;
2185
2915
  if (held !== undefined &&
2186
2916
  (held.confirmed.record.replica.selection !== room.replica.selection ||
@@ -2232,6 +2962,8 @@ export function composeRuntime(options) {
2232
2962
  if (event.kind === 'opened') {
2233
2963
  socketOpen = true;
2234
2964
  generation += 1;
2965
+ conversations.reopened();
2966
+ recovery.resume();
2235
2967
  void sync('opened');
2236
2968
  }
2237
2969
  else if (event.kind === 'closed')
@@ -2255,6 +2987,7 @@ export function composeRuntime(options) {
2255
2987
  haltedRefusal = false;
2256
2988
  if (mode !== serviceMode) {
2257
2989
  serviceMode = mode;
2990
+ queueReadiness();
2258
2991
  journal.record(`service mode ${mode}`);
2259
2992
  for (const listener of serviceModeListeners)
2260
2993
  listener(mode);
@@ -2267,6 +3000,8 @@ export function composeRuntime(options) {
2267
3000
  if (stream === undefined || withdrawConversations !== undefined)
2268
3001
  return;
2269
3002
  withdrawConversations = stream.observe((event) => {
3003
+ if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
3004
+ queueReadiness();
2270
3005
  if (conversations.receive(event)) {
2271
3006
  void ready.then(scheduleCurrency);
2272
3007
  return;
@@ -2289,7 +3024,7 @@ export function composeRuntime(options) {
2289
3024
  if (!live())
2290
3025
  return;
2291
3026
  await conversations.reconcile();
2292
- if (conversations.current()) {
3027
+ if (conversations.current() || !demand.conversations) {
2293
3028
  show();
2294
3029
  return;
2295
3030
  }
@@ -2307,13 +3042,18 @@ export function composeRuntime(options) {
2307
3042
  if (closed)
2308
3043
  return;
2309
3044
  closed = true;
3045
+ if (narrowing !== null)
3046
+ clearTimeout(narrowing);
3047
+ if (rechecking !== null)
3048
+ clearInterval(rechecking);
3049
+ readinessWatches.clear();
2310
3050
  withdrawStream?.();
2311
3051
  withdrawStream = undefined;
2312
3052
  withdrawConversations?.();
2313
3053
  withdrawConversations = undefined;
2314
3054
  withdrawServiceMode?.();
2315
3055
  conversations.close();
2316
- stopTimer();
3056
+ void recovery.retire();
2317
3057
  store?.close();
2318
3058
  void opening?.then((opened) => {
2319
3059
  opened.close();
@@ -2331,9 +3071,8 @@ export function composeRuntime(options) {
2331
3071
  }
2332
3072
  async function stop() {
2333
3073
  stopping = true;
2334
- stopTimer();
3074
+ await recovery.retire();
2335
3075
  await Promise.all(working);
2336
- await Promise.all([...submitting.values()].map((run) => run.done));
2337
3076
  await mutations;
2338
3077
  close();
2339
3078
  }
@@ -2355,17 +3094,101 @@ export function composeRuntime(options) {
2355
3094
  return planOf(input, roomFacts(reach), planReach(reach));
2356
3095
  }
2357
3096
  function watchPlan(plan, deliver) {
2358
- materialize(reachedRooms(plan));
2359
- return readSide.watch(plan, identityOf(plan), deliver);
3097
+ let seeded = false;
3098
+ let pending = true;
3099
+ const shown = () => {
3100
+ const seed = seeds.get(plan.key);
3101
+ if (seed === undefined || seeded || !pending)
3102
+ return;
3103
+ seeded = true;
3104
+ deliver(seededResult(seed));
3105
+ };
3106
+ const watchers = seedWatchers.get(plan.key) ?? new Set();
3107
+ seedWatchers.set(plan.key, watchers);
3108
+ watchers.add(shown);
3109
+ const stop = readSide.watch(plan, identityOf(plan), () => {
3110
+ const cover = covered(plan);
3111
+ awaitPreparation(cover);
3112
+ return seedBehind(plan, cover);
3113
+ }, (message) => {
3114
+ const answered = message;
3115
+ pending = whole(answered) && 'pending' in answered.result;
3116
+ if (pending && seeds.has(plan.key)) {
3117
+ shown();
3118
+ return;
3119
+ }
3120
+ seeded = false;
3121
+ deliver(message);
3122
+ });
3123
+ return () => {
3124
+ watchers.delete(shown);
3125
+ if (watchers.size === 0)
3126
+ seedWatchers.delete(plan.key);
3127
+ stop();
3128
+ };
2360
3129
  }
2361
3130
  async function searched(input, reach) {
2362
3131
  const plan = searchPlanOf(input);
2363
- const selected = plan.documents === undefined && reach === null
2364
- ? null
2365
- : (await rowsQuery(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
2366
- if (selected === null)
2367
- materialize(null);
2368
- return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
3132
+ const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
3133
+ for (;;) {
3134
+ const cover = covered(filter);
3135
+ awaitPreparation(cover);
3136
+ const pending = 'pending' in cover ? [...cover.pending] : [];
3137
+ const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3138
+ const held = covered({
3139
+ ...filter,
3140
+ rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
3141
+ });
3142
+ const covering = 'pending' in held ? [] : held.coverage.rooms;
3143
+ const unindexed = covering
3144
+ .filter(({ room }) => !searchIndex.rooms().has(room))
3145
+ .map(({ room }) => room);
3146
+ for (const room of unindexed)
3147
+ pending.push({ room, awaits: 'index' });
3148
+ const listed = conversations.searchable();
3149
+ const named = reach !== null && !reach.conversations
3150
+ ? []
3151
+ : (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
3152
+ const waiting = named.filter((one) => !searchIndex.conversations().has(one));
3153
+ for (const conversation of waiting)
3154
+ pending.push({ conversation, awaits: 'index' });
3155
+ if (unindexed.length > 0 || waiting.length > 0)
3156
+ index(unindexed, waiting);
3157
+ const complete = pending.length === 0 && !('pending' in cover);
3158
+ const answering = complete
3159
+ ? filter
3160
+ : {
3161
+ ...filter,
3162
+ rooms: covering
3163
+ .filter(({ room }) => searchIndex.rooms().has(room))
3164
+ .map(({ room }) => room),
3165
+ };
3166
+ const answered = covered(answering);
3167
+ if ('pending' in answered)
3168
+ continue;
3169
+ const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
3170
+ const seen = searchIndex.changes();
3171
+ await searchIndex.caughtUp();
3172
+ const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
3173
+ if (selected !== null && 'pending' in selected)
3174
+ continue;
3175
+ if (searchIndex.changes() !== seen ||
3176
+ JSON.stringify(covered(answering)) !== JSON.stringify(answered))
3177
+ continue;
3178
+ const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms === null ? null : new Set(answering.rooms), plan.collection
3179
+ ? new Map(answered.coverage.rooms.map(({ room }) => [
3180
+ room,
3181
+ replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
3182
+ ]))
3183
+ : null);
3184
+ const coverage = {
3185
+ rooms: answered.coverage.rooms,
3186
+ conversations: searchedConversations,
3187
+ index: searchIndex.generation(),
3188
+ overlay: answered.coverage.overlay,
3189
+ };
3190
+ return complete ? { ...hits, coverage } : { pending, ...hits, coverage };
3191
+ }
2369
3192
  }
2370
3193
  const watch = (topic, input, deliver) => {
2371
3194
  if (topic === 'room-drafts') {
@@ -2411,6 +3234,9 @@ export function composeRuntime(options) {
2411
3234
  presentationListeners.delete(listener);
2412
3235
  };
2413
3236
  }
3237
+ if (topic === 'readiness') {
3238
+ return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
3239
+ }
2414
3240
  if (topic === 'rooms') {
2415
3241
  roomListeners.add(deliver);
2416
3242
  deliver(listed());
@@ -2577,7 +3403,13 @@ export function composeRuntime(options) {
2577
3403
  return journal.export();
2578
3404
  },
2579
3405
  persistence() {
2580
- return options.shell.persistentStorage;
3406
+ return persistence === 'granted';
3407
+ },
3408
+ async retry() {
3409
+ journal.record('retry');
3410
+ unindexable.clear();
3411
+ queueReadiness();
3412
+ resume();
2581
3413
  },
2582
3414
  observe: observing(watch),
2583
3415
  async reauthorize() {
@@ -2604,6 +3436,22 @@ export function composeRuntime(options) {
2604
3436
  query: (plan) => rowsQuery(plan),
2605
3437
  watchPlan,
2606
3438
  search: searched,
3439
+ watchReadiness,
3440
+ };
3441
+ return {
3442
+ md,
3443
+ seed,
3444
+ stop,
3445
+ lose: loseRooms,
3446
+ watch,
3447
+ acquire,
3448
+ persisted(answer) {
3449
+ if (answer === persistence)
3450
+ return;
3451
+ persistence = answer;
3452
+ journal.record(`persistence ${answer}`);
3453
+ queueReadiness();
3454
+ },
3455
+ within: (reach, port) => narrowed(reachable, reach, port),
2607
3456
  };
2608
- return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
2609
3457
  }