@agentshouse/mdruntime 0.1.0-ca14c49c06d90 → 0.1.0-ca4dc698bd0c4

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 (91) hide show
  1. package/AGENTS.md +260 -259
  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 +6 -1
  6. package/dist/adapter.d.ts.map +1 -1
  7. package/dist/change-set.d.ts +2 -0
  8. package/dist/change-set.d.ts.map +1 -1
  9. package/dist/change-set.js +34 -9
  10. package/dist/client.d.ts +8 -5
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/client.js +1 -0
  13. package/dist/collection.d.ts +17 -3
  14. package/dist/collection.d.ts.map +1 -1
  15. package/dist/collection.js +230 -19
  16. package/dist/connection.d.ts +20 -7
  17. package/dist/connection.d.ts.map +1 -1
  18. package/dist/connection.js +926 -175
  19. package/dist/conversation.d.ts +8 -5
  20. package/dist/conversation.d.ts.map +1 -1
  21. package/dist/conversation.js +128 -83
  22. package/dist/durability.d.ts +7 -1
  23. package/dist/durability.d.ts.map +1 -1
  24. package/dist/entry.d.ts +2 -2
  25. package/dist/entry.d.ts.map +1 -1
  26. package/dist/entry.js +3 -3
  27. package/dist/host.d.ts +11 -2
  28. package/dist/host.d.ts.map +1 -1
  29. package/dist/host.js +33 -3
  30. package/dist/identity.d.ts +2 -0
  31. package/dist/identity.d.ts.map +1 -1
  32. package/dist/identity.js +6 -0
  33. package/dist/index.d.ts +5 -4
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/memory.d.ts +2 -1
  36. package/dist/memory.d.ts.map +1 -1
  37. package/dist/memory.js +7 -2
  38. package/dist/model.d.ts +1 -0
  39. package/dist/model.d.ts.map +1 -1
  40. package/dist/model.js +9 -0
  41. package/dist/observation.d.ts +5 -2
  42. package/dist/observation.d.ts.map +1 -1
  43. package/dist/observation.js +22 -11
  44. package/dist/overlay.d.ts +1 -0
  45. package/dist/overlay.d.ts.map +1 -1
  46. package/dist/overlay.js +25 -54
  47. package/dist/query.d.ts +28 -3
  48. package/dist/query.d.ts.map +1 -1
  49. package/dist/reach.d.ts +6 -2
  50. package/dist/reach.d.ts.map +1 -1
  51. package/dist/reach.js +16 -4
  52. package/dist/react.d.ts +5 -3
  53. package/dist/react.d.ts.map +1 -1
  54. package/dist/read-side.d.ts +13 -6
  55. package/dist/read-side.d.ts.map +1 -1
  56. package/dist/read-side.js +282 -83
  57. package/dist/readiness.d.ts +53 -0
  58. package/dist/readiness.d.ts.map +1 -0
  59. package/dist/readiness.js +33 -0
  60. package/dist/recovery.d.ts +25 -0
  61. package/dist/recovery.d.ts.map +1 -0
  62. package/dist/recovery.js +197 -0
  63. package/dist/refresh.d.ts.map +1 -1
  64. package/dist/refresh.js +11 -10
  65. package/dist/replica.d.ts +1 -1
  66. package/dist/replica.d.ts.map +1 -1
  67. package/dist/replica.js +47 -19
  68. package/dist/rows.d.ts +4 -13
  69. package/dist/rows.d.ts.map +1 -1
  70. package/dist/rows.js +2 -12
  71. package/dist/save.d.ts +5 -0
  72. package/dist/save.d.ts.map +1 -1
  73. package/dist/save.js +36 -13
  74. package/dist/search.d.ts +28 -6
  75. package/dist/search.d.ts.map +1 -1
  76. package/dist/search.js +108 -45
  77. package/dist/shared.d.ts +11 -0
  78. package/dist/shared.d.ts.map +1 -0
  79. package/dist/shared.js +368 -0
  80. package/dist/status.d.ts +4 -0
  81. package/dist/status.d.ts.map +1 -1
  82. package/dist/store.d.ts +18 -3
  83. package/dist/store.d.ts.map +1 -1
  84. package/dist/store.js +147 -49
  85. package/dist/turn.d.ts +7 -0
  86. package/dist/turn.d.ts.map +1 -0
  87. package/dist/turn.js +45 -0
  88. package/dist/wire.d.ts +2 -0
  89. package/dist/wire.d.ts.map +1 -1
  90. package/dist/wire.js +2 -0
  91. package/package.json +2 -2
@@ -6,22 +6,27 @@ import { createReadSide } from './read-side.js';
6
6
  import { markdownPath, roomOfKey } from './rows.js';
7
7
  import { createSearchIndex, searchPlanOf, } from './search.js';
8
8
  import { bootstrapRoom } from './bootstrap.js';
9
- import { changeSet, mergeChangeSets } from './change-set.js';
9
+ import { changeSet, entering, mergeChangeSets } from './change-set.js';
10
10
  import { createConversationStore, } from './conversation.js';
11
11
  import { queuedFile } from './change.js';
12
- import { validRoot } from './collection.js';
12
+ import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
13
13
  import { runtimeError } from './failure.js';
14
14
  import { createLog } from './log.js';
15
15
  import { binaryRecord } from './binary.js';
16
16
  import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
17
+ import { createRecovery } from './recovery.js';
17
18
  import { refreshRoom } from './refresh.js';
18
19
  import { buildBatch, joined, limitedIn, requeued } from './save.js';
19
20
  import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
20
21
  import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
21
22
  import { same, saving, STARTING, transferring, } from './status.js';
22
23
  import { detectStorageLoss, nextGeneration } from './durability.js';
24
+ import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
25
+ import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
23
26
  import { FencedMap } from './fence.js';
24
- import { roomDraftRevision } from './identity.js';
27
+ import { tally } from './shared.js';
28
+ import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
29
+ import { identityTime, roomDraftRevision } from './identity.js';
25
30
  import { beneath, normalize } from './paths.js';
26
31
  import { openRuntimeStore, } from './store.js';
27
32
  import { unconfirmed } from './transfer.js';
@@ -42,6 +47,29 @@ function declaredSelection(room) {
42
47
  const OPENING_LANE = 'opening';
43
48
  const CONVERSATION_LANE = 'conversations';
44
49
  const ROOM_READS = 256;
50
+ const ROOM_ATTEMPTS = 16;
51
+ const outside = new Map();
52
+ function outsideOf(roots) {
53
+ const key = JSON.stringify(roots);
54
+ const held = outside.get(key);
55
+ if (held !== undefined)
56
+ return held;
57
+ const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
58
+ const made = {
59
+ documents: (path) => (markdownPath(path) ? counted(path) : 0),
60
+ binaries: counted,
61
+ };
62
+ outside.set(key, made);
63
+ return made;
64
+ }
65
+ function abandonedOn(signal) {
66
+ return new Promise((resolve) => {
67
+ if (signal.aborted)
68
+ resolve(null);
69
+ else
70
+ signal.addEventListener('abort', () => resolve(null), { once: true });
71
+ });
72
+ }
45
73
  function roomLane(locator) {
46
74
  return `room ${locator}`;
47
75
  }
@@ -98,7 +126,6 @@ export function composeRuntime(options) {
98
126
  let status = STARTING;
99
127
  let shown = STARTING;
100
128
  let discoveryPaused = false;
101
- let resetTimer;
102
129
  let closed = false;
103
130
  let stopping = false;
104
131
  let withdrawStream;
@@ -106,7 +133,10 @@ export function composeRuntime(options) {
106
133
  let underway = null;
107
134
  let streamed = null;
108
135
  let handed = false;
109
- const submitting = new Map();
136
+ const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
137
+ const repairing = new Set();
138
+ const repairable = new Map();
139
+ let outcomeRetention = null;
110
140
  const answering = new Map();
111
141
  let complete = false;
112
142
  let recorded = false;
@@ -136,25 +166,41 @@ export function composeRuntime(options) {
136
166
  const serviceModeListeners = new Set();
137
167
  let member = null;
138
168
  const memberListeners = new Set();
139
- const materialized = new Set();
140
- let everyRoom = false;
169
+ const prepared = new Set();
170
+ const projecting = new Set();
171
+ const unpreparedDemand = new Set();
172
+ const unindexable = new Set();
173
+ let preparing = false;
174
+ let demand = WHOLE_DEMAND;
175
+ let narrowing = null;
176
+ let targets = {};
177
+ let persistence = options.shell.persistence;
178
+ let capacity = null;
179
+ const paused = new Set();
180
+ let rechecking = null;
181
+ const readinessWatches = new Set();
182
+ let readinessQueued = false;
183
+ const catching = new Set();
141
184
  const transfers = new FencedMap(isFenced);
142
185
  const conversations = createConversationStore({
143
186
  agents: declaredAgents,
144
- documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
187
+ documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
145
188
  call: (name, input) => invokeAction(name, input),
146
189
  durably,
147
190
  stream: () => options.adapter.conversations,
148
191
  publish: publishChangeSet,
149
192
  online: () => !status.providerUnreachable && refusal === null,
150
193
  closed: () => closed,
194
+ unanswered: resendLater,
195
+ expired: (operation) => pastHorizon(operation),
151
196
  });
152
197
  const searchIndex = createSearchIndex({
153
- documents: () => readSide.documents(),
198
+ room: (room) => readSide.room(room),
154
199
  document: (room, path) => readSide.document(room, path),
155
- messages: () => conversations.messages(),
156
- message: (conversation, identity) => conversations.message(conversation, identity),
157
200
  });
201
+ const roomsToIndex = new Set();
202
+ const conversationsToIndex = new Set();
203
+ let indexing = false;
158
204
  const journal = createLog();
159
205
  let notifyReady = () => { };
160
206
  const ready = new Promise((resolve) => {
@@ -233,9 +279,16 @@ export function composeRuntime(options) {
233
279
  returned: structuredClone(record.returned),
234
280
  });
235
281
  }
282
+ for (const [room, refusal] of sourceRefusals)
283
+ attentionRequired.push({ room, refusal: structuredClone(refusal) });
236
284
  attentionRequired.sort((left, right) => {
237
285
  if (left.room !== right.room)
238
286
  return left.room < right.room ? -1 : 1;
287
+ if ('refusal' in left || 'refusal' in right) {
288
+ if ('refusal' in left && 'refusal' in right)
289
+ return 0;
290
+ return 'refusal' in left ? -1 : 1;
291
+ }
239
292
  if (left.path !== right.path)
240
293
  return left.path < right.path ? -1 : 1;
241
294
  return 0;
@@ -254,11 +307,9 @@ export function composeRuntime(options) {
254
307
  if (replica === undefined)
255
308
  return 0;
256
309
  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;
310
+ const weighed = outsideOf(roots);
311
+ return (tally(replica.visible.documents, weighed.documents) +
312
+ tally(replica.visible.binaries, weighed.binaries));
262
313
  }
263
314
  function listed() {
264
315
  return structuredClone([...rooms.values()].map((room) => {
@@ -285,15 +336,19 @@ export function composeRuntime(options) {
285
336
  return { listed: roomsListed, rooms: held };
286
337
  }
287
338
  function emitRooms() {
339
+ readSide.recover();
340
+ queueReadiness();
288
341
  const value = listed();
289
342
  for (const listener of roomListeners)
290
343
  listener(value);
291
344
  }
292
345
  function emitRefusal() {
346
+ queueReadiness();
293
347
  for (const listener of refusalListeners)
294
348
  listener(refusal);
295
349
  }
296
350
  function emitStorageLoss() {
351
+ queueReadiness();
297
352
  for (const listener of storageLossListeners)
298
353
  listener(storageLoss);
299
354
  }
@@ -310,7 +365,8 @@ export function composeRuntime(options) {
310
365
  function show() {
311
366
  if (closed)
312
367
  return;
313
- const next = status.state === 'synced' && !conversations.current()
368
+ queueReadiness();
369
+ const next = status.state === 'synced' && demand.conversations && !conversations.current()
314
370
  ? { ...status, state: 'syncing' }
315
371
  : status;
316
372
  if (same(shown, next))
@@ -339,42 +395,481 @@ export function composeRuntime(options) {
339
395
  }
340
396
  }
341
397
  function applyChangeSet(set) {
342
- const held = everyRoom
343
- ? set
344
- : { ...set, documents: set.documents.filter(({ locator }) => materialized.has(locator)) };
398
+ const held = {
399
+ ...set,
400
+ documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
401
+ };
345
402
  readSide.apply(held);
346
403
  searchIndex.changed(held);
347
404
  }
348
- function materialize(locators) {
349
- if (everyRoom)
405
+ async function referenced(locator) {
406
+ for (;;) {
407
+ const record = replicas.get(locator)?.confirmed.record;
408
+ if (record === undefined || closed)
409
+ return;
410
+ const held = { text: record.documents, binaries: record.binaries };
411
+ const composition = compositionOf(record);
412
+ if (referencesIndexed(held, composition))
413
+ return;
414
+ await indexReferrers(held, composition);
415
+ }
416
+ }
417
+ async function prepareReplica(locator) {
418
+ await referenced(locator);
419
+ const replica = replicas.get(locator);
420
+ if (replica === undefined || closed || prepared.has(locator))
350
421
  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));
422
+ const visible = replica.visible;
423
+ const paths = [
424
+ ...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
425
+ ];
426
+ const entered = [];
427
+ projecting.add(locator);
428
+ try {
429
+ await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
430
+ const current = replicas.get(locator);
431
+ if (current === undefined || closed)
432
+ return;
433
+ const set = entering(locator, current, chunk);
434
+ readSide.apply(set);
435
+ entered.push(set);
436
+ });
437
+ if (!replicas.has(locator) || closed)
438
+ return;
439
+ prepared.add(locator);
440
+ }
441
+ finally {
442
+ projecting.delete(locator);
361
443
  }
362
- if (sets.length === 0)
444
+ conversations.changed(mergeChangeSets(entered));
445
+ readSide.recover();
446
+ queueReadiness();
447
+ }
448
+ function unprepared() {
449
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
450
+ }
451
+ function unbuilt(locator, error) {
452
+ unindexable.add(locator);
453
+ journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
454
+ queueReadiness();
455
+ }
456
+ function prepare(locators = []) {
457
+ for (const locator of locators)
458
+ unpreparedDemand.add(locator);
459
+ if (preparing)
363
460
  return;
364
- const set = mergeChangeSets(sets);
365
- readSide.apply(set);
366
- searchIndex.changed(set);
461
+ preparing = true;
462
+ void (async () => {
463
+ try {
464
+ for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
465
+ await yielded();
466
+ if (closed)
467
+ return;
468
+ unpreparedDemand.delete(waiting);
469
+ await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
470
+ }
471
+ }
472
+ finally {
473
+ preparing = false;
474
+ }
475
+ })();
367
476
  }
368
477
  function reachedRooms(plan) {
369
- if (plan.rooms !== null)
370
- return plan.rooms;
371
- if (plan.reach === null)
478
+ const reach = plan.reach;
479
+ const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
480
+ const named = plan.rooms ??
481
+ (reached === null
482
+ ? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
483
+ : [...reached]);
484
+ return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
485
+ }
486
+ function awaited(locator) {
487
+ return rooms.has(locator) || !roomsListed;
488
+ }
489
+ function covered(plan) {
490
+ const locators = reachedRooms(plan);
491
+ if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
492
+ return { pending: [] };
493
+ }
494
+ const pending = [];
495
+ const coverage = [];
496
+ const overlay = [];
497
+ for (const locator of locators) {
498
+ const replica = replicas.get(locator);
499
+ if (replica === undefined) {
500
+ if (awaited(locator))
501
+ pending.push({ room: locator, awaits: 'acquisition' });
502
+ continue;
503
+ }
504
+ if (!prepared.has(locator)) {
505
+ pending.push({ room: locator, awaits: 'preparation' });
506
+ continue;
507
+ }
508
+ coverage.push({
509
+ room: locator,
510
+ generation: replica.confirmed.record.transfer,
511
+ current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
512
+ });
513
+ for (const batch of replica.queue)
514
+ overlay.push(batch.id);
515
+ }
516
+ if (pending.length > 0)
517
+ return { pending };
518
+ return { coverage: { rooms: coverage, indexes: [], overlay } };
519
+ }
520
+ function awaitPreparation(cover) {
521
+ if (!('pending' in cover))
522
+ return;
523
+ const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
524
+ if (waiting.length > 0)
525
+ prepare(waiting.map(({ room }) => room));
526
+ }
527
+ function acquires(room) {
528
+ return (demanded(demand, room) ||
529
+ (replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
530
+ queuesWithoutReplica.has(room.locator));
531
+ }
532
+ function acquiredLocators() {
533
+ return [...discovered.values()]
534
+ .filter(acquires)
535
+ .map((room) => room.locator)
536
+ .sort();
537
+ }
538
+ function heldDemand(acquired) {
539
+ if (demand.whole)
372
540
  return null;
373
- return [...plan.reach.rooms, ...plan.reach.documents.map(roomOfKey)];
541
+ return { rooms: acquired.map((room) => room.locator), conversations: demand.conversations };
542
+ }
543
+ function applyDemand(next) {
544
+ const before = acquiredLocators();
545
+ const conversed = demand.conversations;
546
+ demand = next;
547
+ const after = acquiredLocators();
548
+ queueReadiness();
549
+ if (!conversed && next.conversations)
550
+ scheduleCurrency();
551
+ if (JSON.stringify(before) === JSON.stringify(after) && conversed === next.conversations)
552
+ return;
553
+ journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
554
+ if (options.adapter.roomStream === undefined)
555
+ void sync('unstreamed');
556
+ else if (socketOpen)
557
+ options.adapter.roomStream.reopen();
558
+ }
559
+ function acquire(next) {
560
+ if (closed)
561
+ return;
562
+ if (narrowing !== null)
563
+ clearTimeout(narrowing);
564
+ narrowing = null;
565
+ const widening = wider(next, demand);
566
+ if (!widening && !narrower(next, demand))
567
+ return;
568
+ if (widening || !roomsListed) {
569
+ applyDemand(next);
570
+ return;
571
+ }
572
+ // A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
573
+ narrowing = setTimeout(() => {
574
+ narrowing = null;
575
+ applyDemand(next);
576
+ }, 5_000);
577
+ }
578
+ function requiredRooms(reach) {
579
+ if (reach === null)
580
+ return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
581
+ const named = [...new Set(reach.rooms)].sort();
582
+ if (roomsListed)
583
+ return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
584
+ return { listed: named.every((room) => replicas.has(room)), rooms: named };
585
+ }
586
+ function roomState(locator, search) {
587
+ if (sourceRefusals.has(locator))
588
+ return 'refused';
589
+ const listedRoom = discovered.get(locator);
590
+ if (roomsListed &&
591
+ (listedRoom === undefined ||
592
+ listedRoom.replica.admission !== 'admitted' ||
593
+ unreadable.has(locator)))
594
+ return 'unadmitted';
595
+ if (!replicas.has(locator))
596
+ return 'acquiring';
597
+ if (!prepared.has(locator))
598
+ return 'preparing';
599
+ if (search && !searchIndex.rooms().has(locator))
600
+ return 'indexing';
601
+ return 'complete';
602
+ }
603
+ function conversationCoverage(reach, search) {
604
+ const declared = reach === null || reach.conversations;
605
+ if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
606
+ return 'undeclared';
607
+ if (!conversations.known() || !conversations.current())
608
+ return 'acquiring';
609
+ if (!search)
610
+ return 'complete';
611
+ const unindexed = conversations
612
+ .searchable()
613
+ .filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
614
+ if (unindexed.length === 0)
615
+ return 'complete';
616
+ index([], unindexed);
617
+ return 'indexing';
618
+ }
619
+ function connectionOf() {
620
+ if (socketOpen)
621
+ return 'connected';
622
+ if (status.providerUnreachable)
623
+ return 'unreachable';
624
+ return options.adapter.roomStream === undefined && status.lastCompleted !== null
625
+ ? 'connected'
626
+ : 'connecting';
627
+ }
628
+ function refusedIn(locators) {
629
+ return locators.flatMap((room) => {
630
+ const held = sourceRefusals.get(room);
631
+ if (held === undefined)
632
+ return [];
633
+ const { file, location } = held.fields;
634
+ return [
635
+ {
636
+ room,
637
+ file: typeof file === 'string' ? file : '',
638
+ rule: held.code,
639
+ location: typeof location === 'string' ? location : null,
640
+ },
641
+ ];
642
+ });
643
+ }
644
+ function readinessFor(port, reach) {
645
+ const required = requiredRooms(reach);
646
+ const target = targets[port.target];
647
+ const search = target?.search === true;
648
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
649
+ const building = (state) => coverage
650
+ .filter((room) => room.state === state && !unindexable.has(room.room))
651
+ .map(({ room }) => room);
652
+ const preparing = building('preparing');
653
+ if (preparing.length > 0)
654
+ prepare(preparing);
655
+ const indexing = building('indexing');
656
+ if (indexing.length > 0)
657
+ index(indexing, []);
658
+ return readinessOf({
659
+ code: port.code,
660
+ coverage: {
661
+ listed: required.listed,
662
+ rooms: coverage,
663
+ conversations: conversationCoverage(reach, search),
664
+ search: search ? 'required' : 'unused',
665
+ },
666
+ persistence,
667
+ connection: connectionOf(),
668
+ house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
669
+ capacity: capacity !== null &&
670
+ coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
671
+ ? capacity
672
+ : port.capacity,
673
+ refused: refusedIn(required.rooms),
674
+ lost: storageLoss !== null,
675
+ established: target?.established === true,
676
+ });
374
677
  }
375
- function rowsQuery(plan) {
376
- materialize(reachedRooms(plan));
377
- return readSide.query(plan);
678
+ function keepTarget(name, change) {
679
+ const held = targets[name] ?? { established: false, search: false };
680
+ const next = { ...held, ...change };
681
+ if (next.established === held.established && next.search === held.search)
682
+ return;
683
+ targets = { ...targets, [name]: next };
684
+ queueReadiness();
685
+ void durably((local) => local.keepTargets(targets)).catch(() => {
686
+ journal.record(`target record failed ${name}`);
687
+ });
688
+ }
689
+ function deliverReadiness(watch) {
690
+ if (!readinessWatches.has(watch))
691
+ return;
692
+ const readiness = readinessFor(watch.port, watch.reach);
693
+ if (readiness.ready && targets[watch.port.target]?.established !== true) {
694
+ keepTarget(watch.port.target, { established: true });
695
+ }
696
+ const shown = JSON.stringify(readiness);
697
+ if (shown === watch.last)
698
+ return;
699
+ watch.last = shown;
700
+ watch.deliver(structuredClone(readiness));
701
+ }
702
+ function queueReadiness() {
703
+ if (readinessQueued || closed || readinessWatches.size === 0)
704
+ return;
705
+ readinessQueued = true;
706
+ queueMicrotask(() => {
707
+ readinessQueued = false;
708
+ if (closed)
709
+ return;
710
+ for (const watch of [...readinessWatches])
711
+ deliverReadiness(watch);
712
+ });
713
+ }
714
+ function watchReadiness(port, reach, deliver) {
715
+ const watch = { port, reach, deliver, last: null };
716
+ readinessWatches.add(watch);
717
+ void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
718
+ return () => {
719
+ readinessWatches.delete(watch);
720
+ };
721
+ }
722
+ function searching(target) {
723
+ if (targets[target]?.search !== true)
724
+ keepTarget(target, { search: true });
725
+ }
726
+ function layerBytes(layer) {
727
+ let bytes = 0;
728
+ for (const [path, text] of Object.entries(layer.documents))
729
+ bytes += (path.length + text.length) * 2;
730
+ return bytes;
731
+ }
732
+ function grownBy(next, previous) {
733
+ if (previous === undefined)
734
+ return layerBytes(next);
735
+ const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
736
+ return layerBytes({ ...next, documents });
737
+ }
738
+ function exceeded(error) {
739
+ return (typeof error === 'object' &&
740
+ error !== null &&
741
+ error.name === 'QuotaExceededError');
742
+ }
743
+ async function estimated() {
744
+ const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
745
+ return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
746
+ }
747
+ function pause(locator, cause) {
748
+ paused.add(locator);
749
+ capacity = cause;
750
+ journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
751
+ queueReadiness();
752
+ if (rechecking !== null)
753
+ return;
754
+ // A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
755
+ rechecking = setInterval(() => {
756
+ void recheck();
757
+ }, 60_000);
758
+ }
759
+ async function recheck() {
760
+ const held = capacity;
761
+ if (held === null || closed)
762
+ return;
763
+ const available = await estimated().catch(() => null);
764
+ if (available === null || available < (held.needed ?? 0))
765
+ return;
766
+ if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
767
+ return;
768
+ resume();
769
+ }
770
+ function resume() {
771
+ if (rechecking !== null)
772
+ clearInterval(rechecking);
773
+ rechecking = null;
774
+ if (paused.size === 0 && capacity === null)
775
+ return;
776
+ paused.clear();
777
+ capacity = null;
778
+ journal.record('capacity resumed');
779
+ queueReadiness();
780
+ if (!closed && authorization !== undefined)
781
+ void sync('unstreamed');
782
+ }
783
+ async function reclaimed(local) {
784
+ const unreferenced = [...transfers.keys()].filter((locator) => {
785
+ const room = discovered.get(locator);
786
+ return room === undefined || !acquires(room);
787
+ });
788
+ for (const locator of unreferenced) {
789
+ await local.abandon(locator);
790
+ transfers.delete(locator);
791
+ }
792
+ if (unreferenced.length > 0)
793
+ journal.record(`capacity reclaimed ${unreferenced.length}`);
794
+ return unreferenced.length > 0;
795
+ }
796
+ async function admitted(local, locator, needed) {
797
+ if (paused.has(locator))
798
+ return false;
799
+ let available = await estimated().catch(() => null);
800
+ if (available === null || available >= needed)
801
+ return true;
802
+ if (await reclaimed(local))
803
+ available = await estimated().catch(() => null);
804
+ if (available === null || available >= needed)
805
+ return true;
806
+ pause(locator, { cause: 'insufficient-space', needed, available });
807
+ return false;
808
+ }
809
+ async function exceededOn(locator, bytes) {
810
+ const available = await estimated().catch(() => null);
811
+ pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
812
+ }
813
+ async function rowsQuery(plan) {
814
+ const before = covered(plan);
815
+ awaitPreparation(before);
816
+ if ('pending' in before)
817
+ return before;
818
+ const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
819
+ awaitPreparation(cover);
820
+ if ('pending' in cover)
821
+ return cover;
822
+ return 'groups' in rows
823
+ ? { groups: rows.groups, coverage: cover.coverage }
824
+ : { documents: rows.documents, coverage: cover.coverage };
825
+ }
826
+ async function indexConversation(conversation) {
827
+ searchIndex.indexMessages(conversation, [], false);
828
+ let after = null;
829
+ for (;;) {
830
+ const batch = await durably((local) => local.messagesAfter(conversation, after));
831
+ if (closed)
832
+ return;
833
+ searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
834
+ if (batch.length === 0)
835
+ return;
836
+ after = batch[batch.length - 1].identity;
837
+ }
838
+ }
839
+ function indexNext() {
840
+ const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
841
+ if (room !== undefined) {
842
+ roomsToIndex.delete(room);
843
+ return () => prepared.has(room)
844
+ ? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
845
+ : undefined;
846
+ }
847
+ const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
848
+ if (conversation === undefined)
849
+ return null;
850
+ conversationsToIndex.delete(conversation);
851
+ return () => indexConversation(conversation);
852
+ }
853
+ function index(rooms, named) {
854
+ for (const room of rooms)
855
+ roomsToIndex.add(room);
856
+ for (const conversation of named)
857
+ conversationsToIndex.add(conversation);
858
+ if (indexing)
859
+ return;
860
+ indexing = true;
861
+ void (async () => {
862
+ try {
863
+ for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
864
+ await yielded();
865
+ await unit();
866
+ queueReadiness();
867
+ }
868
+ }
869
+ finally {
870
+ indexing = false;
871
+ }
872
+ })().catch(() => { });
378
873
  }
379
874
  function publishChangeSet(set) {
380
875
  applyChangeSet(set);
@@ -391,34 +886,7 @@ export function composeRuntime(options) {
391
886
  emitPresentation(app);
392
887
  conversations.changed(set);
393
888
  }
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
889
  async function refuse(reason, provider = null) {
421
- stopTimer();
422
890
  discoveryPaused = true;
423
891
  try {
424
892
  if (reason === 'revoked-authorization') {
@@ -563,10 +1031,14 @@ export function composeRuntime(options) {
563
1031
  rooms.delete(locator);
564
1032
  roomDrafts.delete(locator);
565
1033
  transfers.delete(locator);
1034
+ catching.delete(locator);
566
1035
  discovered.delete(locator);
567
1036
  announced.delete(locator);
568
1037
  unreadable.delete(locator);
569
1038
  stale.delete(locator);
1039
+ repairable.delete(locator);
1040
+ unindexable.delete(locator);
1041
+ paused.delete(locator);
570
1042
  scopedRefusals.delete(locator);
571
1043
  sourceRefusals.delete(locator);
572
1044
  declarationRefusals.delete(locator);
@@ -582,7 +1054,9 @@ export function composeRuntime(options) {
582
1054
  return fresh;
583
1055
  publishChangeSet(mergeChangeSets(removed));
584
1056
  for (const locator of fresh)
585
- materialized.delete(locator);
1057
+ prepared.delete(locator);
1058
+ searchIndex.forget(fresh);
1059
+ readSide.recover();
586
1060
  moveTo({ ...status, ...attention() });
587
1061
  return fresh;
588
1062
  }
@@ -618,7 +1092,7 @@ export function composeRuntime(options) {
618
1092
  return model;
619
1093
  }
620
1094
  async function hydrate(opened) {
621
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
1095
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
622
1096
  opened.all(),
623
1097
  opened.queues(),
624
1098
  opened.roomDrafts(),
@@ -630,7 +1104,9 @@ export function composeRuntime(options) {
630
1104
  opened.capabilities(),
631
1105
  opened.member(),
632
1106
  opened.transfers(),
1107
+ opened.targets(),
633
1108
  ]);
1109
+ targets = heldTargets;
634
1110
  transfers.clear();
635
1111
  for (const record of staged)
636
1112
  transfers.set(record.locator, record);
@@ -692,6 +1168,7 @@ export function composeRuntime(options) {
692
1168
  async function readState(opened) {
693
1169
  const { lastCompleted, locators, read } = await hydrate(opened);
694
1170
  applyChangeSet(read);
1171
+ prepare();
695
1172
  if (replicas.size > 0)
696
1173
  emitRooms();
697
1174
  if (member !== null)
@@ -807,17 +1284,19 @@ export function composeRuntime(options) {
807
1284
  moveTo({ ...status, ...attention() });
808
1285
  throw error;
809
1286
  }
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
- });
1287
+ return () => {
1288
+ queuesWithoutReplica.delete(locator);
1289
+ for (const { batch, returned } of unqueued) {
1290
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
1291
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1292
+ }
1293
+ replicas.set(locator, replicaOf(confirmed, kept, before));
1294
+ return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
1295
+ room: settling.room,
1296
+ queue: kept.length !== previousQueue.length,
1297
+ drafts: [...written.keys()],
1298
+ });
1299
+ };
821
1300
  }
822
1301
  function adoptModels(locator, record) {
823
1302
  for (const [attached, model] of [
@@ -853,8 +1332,13 @@ export function composeRuntime(options) {
853
1332
  function publish(local, room, confirmed, landing, live) {
854
1333
  const { base, stored } = landing;
855
1334
  return exclusive(async () => {
856
- if (!live())
1335
+ if (stored !== undefined) {
1336
+ await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
1337
+ }
1338
+ if (!live()) {
1339
+ caughtUpTo(room.locator);
857
1340
  return true;
1341
+ }
858
1342
  const prior = replicas.get(room.locator);
859
1343
  if (base !== undefined &&
860
1344
  (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
@@ -868,17 +1352,33 @@ export function composeRuntime(options) {
868
1352
  const accepted = pendings.get(batch.id)?.accepted;
869
1353
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
870
1354
  });
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)
1355
+ let settled;
1356
+ try {
1357
+ settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
1358
+ }
1359
+ catch (error) {
1360
+ if (!exceeded(error))
1361
+ throw error;
1362
+ await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
1363
+ caughtUpTo(room.locator);
1364
+ return true;
1365
+ }
1366
+ const set = settled();
1367
+ if (stored !== undefined) {
873
1368
  transfers.delete(room.locator);
1369
+ catching.delete(room.locator);
1370
+ }
874
1371
  for (const batch of accepted)
875
1372
  pendings.delete(batch.id);
876
1373
  stale.delete(room.locator);
1374
+ unindexable.delete(room.locator);
877
1375
  if (unreadable.delete(room.locator))
878
1376
  scopedRefusals.delete(room.locator);
879
1377
  sourceRefusals.delete(room.locator);
880
1378
  declarationRefusals.delete(room.locator);
881
1379
  list(room, true);
1380
+ if (prior === undefined)
1381
+ prepared.add(room.locator);
882
1382
  publishChangeSet(set);
883
1383
  moveTo({ ...status, ...attention() });
884
1384
  options.adapter.roomStream?.published(room.locator, roomVersion);
@@ -887,6 +1387,7 @@ export function composeRuntime(options) {
887
1387
  }
888
1388
  async function saveRoomDrafts(locator, named, writtenBy) {
889
1389
  const local = await openLocal();
1390
+ await referenced(locator);
890
1391
  const replica = replicas.get(locator);
891
1392
  const room = rooms.get(locator);
892
1393
  if (replica === undefined || room === undefined) {
@@ -934,6 +1435,11 @@ export function composeRuntime(options) {
934
1435
  changes,
935
1436
  bases: parts.flatMap(({ bases }) => bases),
936
1437
  writtenBy,
1438
+ clean: {
1439
+ text: replica.confirmed.record.documents,
1440
+ binaries: replica.confirmed.record.binaries,
1441
+ composition: compositionOf(replica.confirmed.record),
1442
+ },
937
1443
  };
938
1444
  const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
939
1445
  if (checked?.kind === 'refused')
@@ -1274,8 +1780,20 @@ export function composeRuntime(options) {
1274
1780
  stage: (record, page) => exclusive(async () => {
1275
1781
  if (!live())
1276
1782
  return false;
1277
- await local.stage(record, page);
1783
+ const bytes = layerBytes(page);
1784
+ if (!(await admitted(local, record.locator, bytes)))
1785
+ return false;
1786
+ try {
1787
+ await local.stage(record, page);
1788
+ }
1789
+ catch (error) {
1790
+ if (!exceeded(error))
1791
+ throw error;
1792
+ await exceededOn(record.locator, bytes);
1793
+ return false;
1794
+ }
1278
1795
  transfers.set(record.locator, record);
1796
+ readSide.recover();
1279
1797
  return true;
1280
1798
  }),
1281
1799
  abandon: (locator) => exclusive(async () => {
@@ -1283,6 +1801,7 @@ export function composeRuntime(options) {
1283
1801
  return;
1284
1802
  await local.abandon(locator);
1285
1803
  transfers.delete(locator);
1804
+ readSide.recover();
1286
1805
  }),
1287
1806
  };
1288
1807
  }
@@ -1295,6 +1814,10 @@ export function composeRuntime(options) {
1295
1814
  async function pullBootstrap(authorized, room, local, live) {
1296
1815
  if (!live())
1297
1816
  return unconfirmed;
1817
+ if (replicas.has(room.locator)) {
1818
+ catching.add(room.locator);
1819
+ readSide.recover();
1820
+ }
1298
1821
  try {
1299
1822
  const result = await bootstrapRoom(options.adapter, authorized, room, {
1300
1823
  collection: modelOf(room.locator),
@@ -1302,14 +1825,24 @@ export function composeRuntime(options) {
1302
1825
  }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
1303
1826
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
1304
1827
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1828
+ if (result.kind !== 'staged')
1829
+ caughtUpTo(room.locator);
1305
1830
  return result;
1306
1831
  }
1307
1832
  catch (error) {
1308
1833
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
1834
+ caughtUpTo(room.locator);
1309
1835
  throw error;
1310
1836
  }
1311
1837
  }
1838
+ function caughtUpTo(locator) {
1839
+ if (catching.delete(locator))
1840
+ readSide.recover();
1841
+ }
1312
1842
  async function advance(room, local, authorized, live) {
1843
+ await referenced(room.locator);
1844
+ if (!live() || paused.has(room.locator))
1845
+ return unconfirmed;
1313
1846
  const held = replicas.get(room.locator);
1314
1847
  if (held !== undefined) {
1315
1848
  const refreshed = await pullRefresh(authorized, held.confirmed, live);
@@ -1340,6 +1873,7 @@ export function composeRuntime(options) {
1340
1873
  return refreshed;
1341
1874
  if (opened !== null) {
1342
1875
  owed.add(room.locator);
1876
+ readSide.recover();
1343
1877
  return unconfirmed;
1344
1878
  }
1345
1879
  }
@@ -1352,10 +1886,19 @@ export function composeRuntime(options) {
1352
1886
  return null;
1353
1887
  }
1354
1888
  async function withhold(withheld, room, local, progress) {
1889
+ if (withheld.kind === 'unconfirmed')
1890
+ return;
1355
1891
  if (withheld.kind === 'refused')
1356
1892
  stale.delete(room.locator);
1357
1893
  else
1358
1894
  stale.add(room.locator);
1895
+ if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
1896
+ const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
1897
+ repairable.set(room.locator, reset);
1898
+ repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
1899
+ }
1900
+ else
1901
+ repairable.delete(room.locator);
1359
1902
  if (withheld.kind === 'scoped-refusal') {
1360
1903
  await scoped(withheld, room, local);
1361
1904
  }
@@ -1391,6 +1934,7 @@ export function composeRuntime(options) {
1391
1934
  unreadable.add(room.locator);
1392
1935
  list(room, false);
1393
1936
  emitRooms();
1937
+ moveTo({ ...status, ...attention() });
1394
1938
  journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
1395
1939
  }
1396
1940
  function leaving(batch, result) {
@@ -1412,6 +1956,7 @@ export function composeRuntime(options) {
1412
1956
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1413
1957
  }
1414
1958
  async function adoptAccepted(room, local, submitted, accepted) {
1959
+ await referenced(room.locator);
1415
1960
  if (!continuing())
1416
1961
  return unconfirmed;
1417
1962
  const current = replicas.get(room.locator);
@@ -1433,11 +1978,12 @@ export function composeRuntime(options) {
1433
1978
  emitQueue(room.locator);
1434
1979
  return applied;
1435
1980
  }
1436
- const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1981
+ const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1437
1982
  pendings.delete(submitted.id);
1438
1983
  unreadable.delete(room.locator);
1439
1984
  sourceRefusals.delete(room.locator);
1440
1985
  declarationRefusals.delete(room.locator);
1986
+ const set = settled();
1441
1987
  list(room, true);
1442
1988
  publishChangeSet(set);
1443
1989
  moveTo({ ...status, ...attention() });
@@ -1451,14 +1997,14 @@ export function composeRuntime(options) {
1451
1997
  refusedSource(room, failed);
1452
1998
  return false;
1453
1999
  }
1454
- async function submitRoom(room, local, authorized, progress) {
1455
- while (continuing() && authorization !== undefined) {
2000
+ async function submitRoom(room, local, authorized, progress, signal) {
2001
+ while (continuing() && authorization !== undefined && !signal.aborted) {
1456
2002
  const replica = replicas.get(room.locator);
1457
2003
  const head = replica?.queue[0];
1458
2004
  if (replica === undefined || head === undefined)
1459
- return;
2005
+ return 'done';
1460
2006
  if (pendings.get(head.id)?.accepted !== undefined)
1461
- return;
2007
+ return 'done';
1462
2008
  const state = pendings.get(head.id);
1463
2009
  const { submission, lost: earlier } = state ?? {
1464
2010
  submission: { state: 'waiting' },
@@ -1466,24 +2012,30 @@ export function composeRuntime(options) {
1466
2012
  };
1467
2013
  if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
1468
2014
  progress.complete = false;
1469
- return;
2015
+ return { at: Date.parse(submission.reset) };
1470
2016
  }
1471
2017
  if (writesHeld()) {
1472
2018
  progress.complete = false;
1473
2019
  journal.record(`submission ${room.locator} ${head.id} held`);
1474
- return;
2020
+ return 'done';
1475
2021
  }
1476
2022
  const lost = earlier || submission.state === 'submitted';
2023
+ if (lost && pastHorizon(head.id)) {
2024
+ await discardPending(room, local, signal);
2025
+ return 'done';
2026
+ }
1477
2027
  let result;
1478
2028
  journal.record(`submission ${room.locator} ${head.id}`);
1479
2029
  await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1480
2030
  if (discoveryPaused || fences.has(room.locator))
1481
- return;
1482
- if (writesHeld()) {
2031
+ return 'done';
2032
+ if (signal.aborted || writesHeld()) {
1483
2033
  await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
2034
+ if (signal.aborted)
2035
+ return 'done';
1484
2036
  progress.complete = false;
1485
2037
  journal.record(`submission ${room.locator} ${head.id} held`);
1486
- return;
2038
+ return 'done';
1487
2039
  }
1488
2040
  const opensBefore = openSignals;
1489
2041
  const answered = awaitingAnswer(room.locator);
@@ -1499,32 +2051,36 @@ export function composeRuntime(options) {
1499
2051
  progress.complete = false;
1500
2052
  if (continuing())
1501
2053
  progress.reachable = false;
1502
- return;
2054
+ return 'again';
2055
+ }
2056
+ if (signal.aborted) {
2057
+ answered();
2058
+ return 'done';
1503
2059
  }
1504
2060
  if (result.kind !== 'accepted' || !continuing())
1505
2061
  answered();
1506
2062
  if (!continuing())
1507
- return;
2063
+ return 'done';
1508
2064
  if (result.kind === 'transport-uncertainty') {
1509
2065
  progress.complete = false;
1510
2066
  progress.reachable = false;
1511
- return;
2067
+ return 'again';
1512
2068
  }
1513
2069
  if (result.kind === 'room-not-readable') {
1514
2070
  progress.complete = false;
1515
2071
  unreadable.add(room.locator);
1516
2072
  list(room, false);
1517
2073
  emitRooms();
1518
- return;
2074
+ return 'done';
1519
2075
  }
1520
2076
  if (result.kind === 'revoked-authorization') {
1521
2077
  await refuse('revoked-authorization');
1522
- return;
2078
+ return 'done';
1523
2079
  }
1524
2080
  if (result.kind === 'scoped-refusal') {
1525
2081
  await scoped(result, room, local);
1526
2082
  progress.complete = false;
1527
- return;
2083
+ return 'done';
1528
2084
  }
1529
2085
  if (result.kind === 'write-halted') {
1530
2086
  journal.record(`outcome ${room.locator} ${head.id} write-halted`);
@@ -1533,16 +2089,24 @@ export function composeRuntime(options) {
1533
2089
  continue;
1534
2090
  haltedRefusal = true;
1535
2091
  progress.complete = false;
1536
- return;
2092
+ return 'done';
1537
2093
  }
1538
2094
  if (result.kind === 'conflicted' ||
1539
2095
  result.kind === 'rejected' ||
1540
2096
  result.kind === 'expired' ||
1541
2097
  result.kind === 'retryable-refusal') {
2098
+ if (result.kind === 'expired' && lost && result.restored !== true) {
2099
+ journal.record(`outcome ${room.locator} ${head.id} expired`);
2100
+ await discardPending(room, local, signal);
2101
+ return 'done';
2102
+ }
1542
2103
  if (result.kind === 'expired') {
2104
+ await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
1543
2105
  const withheld = await advance(room, local, authorized, continuing);
1544
- if (withheld !== null)
1545
- return withhold(withheld, room, local, progress);
2106
+ if (withheld !== null) {
2107
+ await withhold(withheld, room, local, progress);
2108
+ return 'done';
2109
+ }
1546
2110
  }
1547
2111
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
1548
2112
  (result.kind === 'conflicted' && result.roomVersion !== undefined
@@ -1555,8 +2119,7 @@ export function composeRuntime(options) {
1555
2119
  const reset = result.reset;
1556
2120
  progress.complete = false;
1557
2121
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1558
- armTimer();
1559
- return;
2122
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
1560
2123
  }
1561
2124
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
1562
2125
  const left = leaving(head, result);
@@ -1567,28 +2130,73 @@ export function composeRuntime(options) {
1567
2130
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
1568
2131
  return;
1569
2132
  }
1570
- const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
2133
+ const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1571
2134
  leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1572
2135
  (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1573
2136
  ? { ...left, returned: null }
1574
2137
  : left,
1575
2138
  });
1576
- publishChangeSet(set);
2139
+ publishChangeSet(settled());
1577
2140
  moveTo({ ...status, ...attention() });
1578
2141
  });
1579
2142
  if (result.kind === 'expired')
1580
2143
  continue;
1581
2144
  const withheld = await advance(room, local, authorized, continuing);
1582
- if (withheld !== null)
1583
- return withhold(withheld, room, local, progress);
2145
+ if (withheld !== null) {
2146
+ await withhold(withheld, room, local, progress);
2147
+ return 'done';
2148
+ }
1584
2149
  continue;
1585
2150
  }
1586
2151
  const kept = await adopted(room, local, head, result).finally(answered);
1587
2152
  if (!kept) {
1588
2153
  progress.complete = false;
1589
- return;
2154
+ return 'done';
1590
2155
  }
1591
2156
  }
2157
+ return 'done';
2158
+ }
2159
+ function pastHorizon(identity) {
2160
+ return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
2161
+ }
2162
+ async function discardPending(room, local, signal) {
2163
+ const { locator } = room;
2164
+ const discarded = await exclusive(async () => {
2165
+ if (fences.has(locator))
2166
+ return false;
2167
+ const before = replicas.get(locator);
2168
+ const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
2169
+ fences.set(locator, { at: discoveries, erased: false });
2170
+ lastFenced.set(locator, ++fencings);
2171
+ try {
2172
+ await local.discard(locator);
2173
+ }
2174
+ finally {
2175
+ fences.delete(locator);
2176
+ }
2177
+ for (const batch of queue)
2178
+ pendings.delete(batch.id);
2179
+ queuesWithoutReplica.delete(locator);
2180
+ const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
2181
+ if (after !== null)
2182
+ replicas.set(locator, after);
2183
+ journal.record(`room discarded ${locator}`);
2184
+ publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
2185
+ moveTo({ ...status, ...attention() });
2186
+ return true;
2187
+ });
2188
+ if (!discarded)
2189
+ return;
2190
+ const progress = { complete: true, reachable: true };
2191
+ await roomTask(locator, async (live) => {
2192
+ await Promise.race([
2193
+ reload(room, local, progress, () => live() && !signal.aborted),
2194
+ abandonedOn(signal),
2195
+ ]);
2196
+ });
2197
+ if (signal.aborted)
2198
+ return;
2199
+ await rest(progress);
1592
2200
  }
1593
2201
  function awaitingAnswer(locator) {
1594
2202
  let release = () => { };
@@ -1714,15 +2322,16 @@ export function composeRuntime(options) {
1714
2322
  });
1715
2323
  }
1716
2324
  async function synchronize(listed, local, progress, attempt, live) {
1717
- const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
2325
+ const acquired = listed.filter(acquires);
2326
+ const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
1718
2327
  if (!live())
1719
2328
  return;
1720
- const ready = listed.filter((_, index) => {
2329
+ const ready = acquired.filter((_, index) => {
1721
2330
  const outcome = prepared[index];
1722
2331
  return outcome?.status === 'fulfilled' && outcome.value;
1723
2332
  });
1724
2333
  if (attempt === 'opened') {
1725
- options.adapter.roomStream?.hold(heldRooms(ready));
2334
+ options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
1726
2335
  handed = true;
1727
2336
  }
1728
2337
  discovering?.release();
@@ -1789,17 +2398,22 @@ export function composeRuntime(options) {
1789
2398
  await caughtUp(room, local, progress, live);
1790
2399
  }
1791
2400
  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;
2401
+ try {
2402
+ if (!live() || authorization === undefined)
2403
+ return;
2404
+ const bootstrapped = await pullBootstrap(authorization, room, local, live);
2405
+ if (!live())
2406
+ return;
2407
+ if (bootstrapped.kind !== 'staged') {
2408
+ await withhold(bootstrapped, room, local, progress);
2409
+ return;
2410
+ }
2411
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
2412
+ await caughtUp(room, local, progress, live);
2413
+ }
2414
+ finally {
2415
+ caughtUpTo(room.locator);
1800
2416
  }
1801
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
1802
- await caughtUp(room, local, progress, live);
1803
2417
  }
1804
2418
  async function discoverRooms(local, progress, attempt, live) {
1805
2419
  if (closed || authorization === undefined) {
@@ -1875,6 +2489,7 @@ export function composeRuntime(options) {
1875
2489
  }
1876
2490
  const previousCapabilities = capabilities;
1877
2491
  capabilities = result.capabilities;
2492
+ outcomeRetention = result.retention.outcome;
1878
2493
  notifyReady();
1879
2494
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1880
2495
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
@@ -2004,6 +2619,8 @@ export function composeRuntime(options) {
2004
2619
  const progress = underway?.generation === generation ? underway.progress : null;
2005
2620
  const cycle = progress ?? { complete: true, reachable: true };
2006
2621
  const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
2622
+ if (opened === generation)
2623
+ opened = null;
2007
2624
  return task(OPENING_LANE, async (live) => {
2008
2625
  underway = null;
2009
2626
  if (!live())
@@ -2016,8 +2633,6 @@ export function composeRuntime(options) {
2016
2633
  await roomsCarried(expired);
2017
2634
  if (!live())
2018
2635
  return;
2019
- if (opened === generation)
2020
- opened = null;
2021
2636
  const replaced = new Set(owed);
2022
2637
  owed.clear();
2023
2638
  try {
@@ -2025,6 +2640,9 @@ export function composeRuntime(options) {
2025
2640
  const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
2026
2641
  const reloading = held.filter((room) => !expired.includes(room.locator) &&
2027
2642
  (replaced.has(room.locator) || revoked(room.locator)));
2643
+ for (const room of reloading)
2644
+ catching.add(room.locator);
2645
+ readSide.recover();
2028
2646
  const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
2029
2647
  const reloaded = [...(await reloads), ...(await replacing)];
2030
2648
  if (progress.reachable) {
@@ -2069,38 +2687,94 @@ export function composeRuntime(options) {
2069
2687
  function submitLater(locator) {
2070
2688
  if (localSaves)
2071
2689
  return;
2072
- const running = submitting.get(locator);
2073
- if (running !== undefined) {
2074
- running.again = true;
2690
+ void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
2691
+ }
2692
+ async function submitPass(locator, signal) {
2693
+ const room = discovered.get(locator);
2694
+ if (closed || stopping || discoveryPaused || room === undefined)
2695
+ return 'done';
2696
+ const progress = { complete: true, reachable: true };
2697
+ let next;
2698
+ try {
2699
+ await ensureAuthorization();
2700
+ if (authorization === undefined)
2701
+ return 'done';
2702
+ next = await submitRoom(room, await openLocal(), authorization, progress, signal);
2703
+ }
2704
+ catch {
2705
+ progress.reachable = false;
2706
+ next = 'again';
2707
+ }
2708
+ if (signal.aborted)
2709
+ return 'done';
2710
+ await rest(progress);
2711
+ return next === 'again' && !socketOpen ? 'done' : next;
2712
+ }
2713
+ function repairLater(locator, when) {
2714
+ if (!socketOpen || repairing.has(locator))
2075
2715
  return;
2716
+ void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
2717
+ }
2718
+ async function repairPass(locator, signal) {
2719
+ const room = discovered.get(locator);
2720
+ if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
2721
+ return 'done';
2722
+ if (room === undefined || room.replica.admission !== 'admitted')
2723
+ return 'done';
2724
+ const progress = { complete: true, reachable: true };
2725
+ repairing.add(locator);
2726
+ try {
2727
+ const local = await openLocal();
2728
+ await roomTask(locator, async (live) => {
2729
+ const current = () => live() && !signal.aborted;
2730
+ if (!current() || authorization === undefined)
2731
+ return;
2732
+ const withheld = await Promise.race([
2733
+ advance(room, local, authorization, current),
2734
+ abandonedOn(signal),
2735
+ ]);
2736
+ if (withheld !== null && current())
2737
+ await withhold(withheld, room, local, progress);
2738
+ });
2076
2739
  }
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
- })();
2740
+ catch {
2741
+ stale.add(locator);
2742
+ progress.reachable = false;
2743
+ }
2744
+ finally {
2745
+ repairing.delete(locator);
2746
+ }
2747
+ if (signal.aborted)
2748
+ return 'done';
2749
+ await rest(progress);
2750
+ if (!stale.has(locator)) {
2751
+ if ((replicas.get(locator)?.queue.length ?? 0) > 0)
2752
+ submitLater(locator);
2753
+ return 'done';
2754
+ }
2755
+ if (!repairable.has(locator))
2756
+ return 'done';
2757
+ const reset = repairable.get(locator);
2758
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
2759
+ }
2760
+ function resendLater() {
2761
+ if (!socketOpen)
2762
+ return;
2763
+ void recovery.attempt('resend', resendPass, { again: true });
2764
+ }
2765
+ async function resendPass(signal) {
2766
+ if (!socketOpen || closed || stopping || discoveryPaused)
2767
+ return 'done';
2768
+ const reading = { complete: true, reachable: true };
2769
+ try {
2770
+ await conversations.resend(reading);
2771
+ }
2772
+ catch {
2773
+ reading.reachable = false;
2774
+ }
2775
+ if (signal.aborted)
2776
+ return 'done';
2777
+ return reading.reachable && reading.complete ? 'done' : 'again';
2104
2778
  }
2105
2779
  function submitAll() {
2106
2780
  for (const [locator, replica] of replicas) {
@@ -2115,15 +2789,21 @@ export function composeRuntime(options) {
2115
2789
  return;
2116
2790
  }
2117
2791
  submitAll();
2792
+ for (const locator of stale)
2793
+ if (repairable.has(locator))
2794
+ repairLater(locator, {});
2118
2795
  await conversations.reconcile();
2119
2796
  if (status.providerUnreachable)
2120
2797
  conversations.reconnected();
2121
2798
  const reading = { complete: true, reachable: true };
2122
- await conversations.catchUp(reading);
2799
+ if (demand.conversations)
2800
+ await conversations.catchUp(reading);
2123
2801
  if (progress.reachable && reading.reachable)
2124
2802
  await conversations.resend(reading);
2125
- if (!reading.reachable)
2803
+ if (!reading.reachable) {
2126
2804
  progress.reachable = false;
2805
+ resendLater();
2806
+ }
2127
2807
  }
2128
2808
  catch {
2129
2809
  progress.complete = false;
@@ -2180,7 +2860,7 @@ export function composeRuntime(options) {
2180
2860
  return;
2181
2861
  const room = discovered.get(locator);
2182
2862
  const held = replicas.get(locator);
2183
- if (room === undefined || room.replica.admission !== 'admitted')
2863
+ if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
2184
2864
  return;
2185
2865
  if (held !== undefined &&
2186
2866
  (held.confirmed.record.replica.selection !== room.replica.selection ||
@@ -2232,6 +2912,8 @@ export function composeRuntime(options) {
2232
2912
  if (event.kind === 'opened') {
2233
2913
  socketOpen = true;
2234
2914
  generation += 1;
2915
+ conversations.reopened();
2916
+ recovery.resume();
2235
2917
  void sync('opened');
2236
2918
  }
2237
2919
  else if (event.kind === 'closed')
@@ -2255,6 +2937,7 @@ export function composeRuntime(options) {
2255
2937
  haltedRefusal = false;
2256
2938
  if (mode !== serviceMode) {
2257
2939
  serviceMode = mode;
2940
+ queueReadiness();
2258
2941
  journal.record(`service mode ${mode}`);
2259
2942
  for (const listener of serviceModeListeners)
2260
2943
  listener(mode);
@@ -2267,6 +2950,8 @@ export function composeRuntime(options) {
2267
2950
  if (stream === undefined || withdrawConversations !== undefined)
2268
2951
  return;
2269
2952
  withdrawConversations = stream.observe((event) => {
2953
+ if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
2954
+ queueReadiness();
2270
2955
  if (conversations.receive(event)) {
2271
2956
  void ready.then(scheduleCurrency);
2272
2957
  return;
@@ -2289,7 +2974,7 @@ export function composeRuntime(options) {
2289
2974
  if (!live())
2290
2975
  return;
2291
2976
  await conversations.reconcile();
2292
- if (conversations.current()) {
2977
+ if (conversations.current() || !demand.conversations) {
2293
2978
  show();
2294
2979
  return;
2295
2980
  }
@@ -2307,13 +2992,18 @@ export function composeRuntime(options) {
2307
2992
  if (closed)
2308
2993
  return;
2309
2994
  closed = true;
2995
+ if (narrowing !== null)
2996
+ clearTimeout(narrowing);
2997
+ if (rechecking !== null)
2998
+ clearInterval(rechecking);
2999
+ readinessWatches.clear();
2310
3000
  withdrawStream?.();
2311
3001
  withdrawStream = undefined;
2312
3002
  withdrawConversations?.();
2313
3003
  withdrawConversations = undefined;
2314
3004
  withdrawServiceMode?.();
2315
3005
  conversations.close();
2316
- stopTimer();
3006
+ void recovery.retire();
2317
3007
  store?.close();
2318
3008
  void opening?.then((opened) => {
2319
3009
  opened.close();
@@ -2331,9 +3021,8 @@ export function composeRuntime(options) {
2331
3021
  }
2332
3022
  async function stop() {
2333
3023
  stopping = true;
2334
- stopTimer();
3024
+ await recovery.retire();
2335
3025
  await Promise.all(working);
2336
- await Promise.all([...submitting.values()].map((run) => run.done));
2337
3026
  await mutations;
2338
3027
  close();
2339
3028
  }
@@ -2355,17 +3044,54 @@ export function composeRuntime(options) {
2355
3044
  return planOf(input, roomFacts(reach), planReach(reach));
2356
3045
  }
2357
3046
  function watchPlan(plan, deliver) {
2358
- materialize(reachedRooms(plan));
2359
- return readSide.watch(plan, identityOf(plan), deliver);
3047
+ return readSide.watch(plan, identityOf(plan), () => {
3048
+ const cover = covered(plan);
3049
+ awaitPreparation(cover);
3050
+ return cover;
3051
+ }, deliver);
2360
3052
  }
2361
3053
  async function searched(input, reach) {
2362
3054
  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);
3055
+ const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
3056
+ for (;;) {
3057
+ const cover = covered(filter);
3058
+ awaitPreparation(cover);
3059
+ const pending = 'pending' in cover ? [...cover.pending] : [];
3060
+ const covering = 'pending' in cover ? [] : cover.coverage.rooms;
3061
+ const unindexed = covering
3062
+ .filter(({ room }) => !searchIndex.rooms().has(room))
3063
+ .map(({ room }) => room);
3064
+ for (const room of unindexed)
3065
+ pending.push({ room, awaits: 'index' });
3066
+ const held = conversations.searchable();
3067
+ const named = reach !== null && !reach.conversations
3068
+ ? []
3069
+ : (plan.conversations ?? held).filter((one) => held.includes(one) && conversations.acquired(one));
3070
+ const waiting = named.filter((one) => !searchIndex.conversations().has(one));
3071
+ for (const conversation of waiting)
3072
+ pending.push({ conversation, awaits: 'index' });
3073
+ if (unindexed.length > 0 || waiting.length > 0)
3074
+ index(unindexed, waiting);
3075
+ if (pending.length > 0 || 'pending' in cover)
3076
+ return { pending };
3077
+ const seen = searchIndex.changes();
3078
+ await searchIndex.caughtUp();
3079
+ const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(filter);
3080
+ if (selected !== null && 'pending' in selected)
3081
+ return selected;
3082
+ if (searchIndex.changes() !== seen ||
3083
+ JSON.stringify(covered(filter)) !== JSON.stringify(cover))
3084
+ continue;
3085
+ return {
3086
+ ...searchIndex.search(plan, selected === null ? null : selected.documents, named),
3087
+ coverage: {
3088
+ rooms: covering,
3089
+ conversations: named,
3090
+ index: searchIndex.generation(),
3091
+ overlay: cover.coverage.overlay,
3092
+ },
3093
+ };
3094
+ }
2369
3095
  }
2370
3096
  const watch = (topic, input, deliver) => {
2371
3097
  if (topic === 'room-drafts') {
@@ -2411,6 +3137,9 @@ export function composeRuntime(options) {
2411
3137
  presentationListeners.delete(listener);
2412
3138
  };
2413
3139
  }
3140
+ if (topic === 'readiness') {
3141
+ return watchReadiness({ target: '', code: 'stored', capacity: null }, null, deliver);
3142
+ }
2414
3143
  if (topic === 'rooms') {
2415
3144
  roomListeners.add(deliver);
2416
3145
  deliver(listed());
@@ -2577,7 +3306,13 @@ export function composeRuntime(options) {
2577
3306
  return journal.export();
2578
3307
  },
2579
3308
  persistence() {
2580
- return options.shell.persistentStorage;
3309
+ return persistence === 'granted';
3310
+ },
3311
+ async retry() {
3312
+ journal.record('retry');
3313
+ unindexable.clear();
3314
+ queueReadiness();
3315
+ resume();
2581
3316
  },
2582
3317
  observe: observing(watch),
2583
3318
  async reauthorize() {
@@ -2604,6 +3339,22 @@ export function composeRuntime(options) {
2604
3339
  query: (plan) => rowsQuery(plan),
2605
3340
  watchPlan,
2606
3341
  search: searched,
3342
+ searching,
3343
+ watchReadiness,
3344
+ };
3345
+ return {
3346
+ md,
3347
+ stop,
3348
+ lose: loseRooms,
3349
+ watch,
3350
+ acquire,
3351
+ persisted(answer) {
3352
+ if (answer === persistence)
3353
+ return;
3354
+ persistence = answer;
3355
+ journal.record(`persistence ${answer}`);
3356
+ queueReadiness();
3357
+ },
3358
+ within: (reach, port) => narrowed(reachable, reach, port),
2607
3359
  };
2608
- return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
2609
3360
  }