@agentshouse/mdruntime 0.1.0-cfb2ae0107054 → 0.1.0-cfcbb238a5fa0

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 (124) 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 +14 -7
  6. package/dist/adapter.d.ts.map +1 -1
  7. package/dist/bootstrap.d.ts +2 -1
  8. package/dist/bootstrap.d.ts.map +1 -1
  9. package/dist/bootstrap.js +4 -0
  10. package/dist/change-set.d.ts +2 -0
  11. package/dist/change-set.d.ts.map +1 -1
  12. package/dist/change-set.js +36 -10
  13. package/dist/chat/kit.d.ts +2 -1
  14. package/dist/chat/kit.d.ts.map +1 -1
  15. package/dist/chat/kit.js +3 -3
  16. package/dist/chat/list.d.ts +2 -1
  17. package/dist/chat/list.d.ts.map +1 -1
  18. package/dist/chat/list.js +4 -3
  19. package/dist/chat/message.d.ts.map +1 -1
  20. package/dist/chat/message.js +2 -2
  21. package/dist/chat/messages.d.ts +1 -0
  22. package/dist/chat/messages.d.ts.map +1 -1
  23. package/dist/chat/messages.js +4 -0
  24. package/dist/chat/provider.d.ts +1 -1
  25. package/dist/chat/provider.d.ts.map +1 -1
  26. package/dist/chat/provider.js +26 -17
  27. package/dist/chat/ready.d.ts +1 -0
  28. package/dist/chat/ready.d.ts.map +1 -1
  29. package/dist/chat/ready.js +2 -2
  30. package/dist/client.d.ts +10 -6
  31. package/dist/client.d.ts.map +1 -1
  32. package/dist/client.js +11 -2
  33. package/dist/collection.d.ts +17 -3
  34. package/dist/collection.d.ts.map +1 -1
  35. package/dist/collection.js +230 -19
  36. package/dist/connection.d.ts +23 -7
  37. package/dist/connection.d.ts.map +1 -1
  38. package/dist/connection.js +1065 -192
  39. package/dist/conversation.d.ts +11 -5
  40. package/dist/conversation.d.ts.map +1 -1
  41. package/dist/conversation.js +241 -100
  42. package/dist/durability.d.ts +7 -1
  43. package/dist/durability.d.ts.map +1 -1
  44. package/dist/entry.d.ts +2 -2
  45. package/dist/entry.d.ts.map +1 -1
  46. package/dist/entry.js +3 -3
  47. package/dist/host.d.ts +16 -2
  48. package/dist/host.d.ts.map +1 -1
  49. package/dist/host.js +82 -8
  50. package/dist/identity.d.ts +2 -0
  51. package/dist/identity.d.ts.map +1 -1
  52. package/dist/identity.js +6 -0
  53. package/dist/index.d.ts +8 -5
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +2 -0
  56. package/dist/memory.d.ts +7 -2
  57. package/dist/memory.d.ts.map +1 -1
  58. package/dist/memory.js +20 -7
  59. package/dist/model.d.ts +4 -2
  60. package/dist/model.d.ts.map +1 -1
  61. package/dist/model.js +10 -1
  62. package/dist/observation.d.ts +5 -2
  63. package/dist/observation.d.ts.map +1 -1
  64. package/dist/observation.js +22 -11
  65. package/dist/overlay.d.ts +2 -0
  66. package/dist/overlay.d.ts.map +1 -1
  67. package/dist/overlay.js +58 -55
  68. package/dist/query.d.ts +35 -3
  69. package/dist/query.d.ts.map +1 -1
  70. package/dist/query.js +8 -2
  71. package/dist/reach.d.ts +6 -3
  72. package/dist/reach.d.ts.map +1 -1
  73. package/dist/reach.js +16 -4
  74. package/dist/react.d.ts +7 -4
  75. package/dist/react.d.ts.map +1 -1
  76. package/dist/react.js +6 -4
  77. package/dist/read-side.d.ts +13 -6
  78. package/dist/read-side.d.ts.map +1 -1
  79. package/dist/read-side.js +285 -83
  80. package/dist/readiness.d.ts +53 -0
  81. package/dist/readiness.d.ts.map +1 -0
  82. package/dist/readiness.js +33 -0
  83. package/dist/reads.d.ts +11 -0
  84. package/dist/reads.d.ts.map +1 -0
  85. package/dist/reads.js +98 -0
  86. package/dist/recovery.d.ts +25 -0
  87. package/dist/recovery.d.ts.map +1 -0
  88. package/dist/recovery.js +197 -0
  89. package/dist/refresh.d.ts +4 -4
  90. package/dist/refresh.d.ts.map +1 -1
  91. package/dist/refresh.js +17 -10
  92. package/dist/replica.d.ts +1 -1
  93. package/dist/replica.d.ts.map +1 -1
  94. package/dist/replica.js +57 -20
  95. package/dist/room-draft.d.ts +13 -1
  96. package/dist/room-draft.d.ts.map +1 -1
  97. package/dist/room-draft.js +2 -2
  98. package/dist/rows.d.ts +5 -13
  99. package/dist/rows.d.ts.map +1 -1
  100. package/dist/rows.js +5 -13
  101. package/dist/save.d.ts +5 -0
  102. package/dist/save.d.ts.map +1 -1
  103. package/dist/save.js +36 -13
  104. package/dist/search.d.ts +36 -6
  105. package/dist/search.d.ts.map +1 -1
  106. package/dist/search.js +132 -46
  107. package/dist/seed.d.ts +16 -0
  108. package/dist/seed.d.ts.map +1 -0
  109. package/dist/seed.js +61 -0
  110. package/dist/shared.d.ts +11 -0
  111. package/dist/shared.d.ts.map +1 -0
  112. package/dist/shared.js +368 -0
  113. package/dist/status.d.ts +4 -0
  114. package/dist/status.d.ts.map +1 -1
  115. package/dist/store.d.ts +21 -5
  116. package/dist/store.d.ts.map +1 -1
  117. package/dist/store.js +157 -53
  118. package/dist/turn.d.ts +7 -0
  119. package/dist/turn.d.ts.map +1 -0
  120. package/dist/turn.js +45 -0
  121. package/dist/wire.d.ts +136 -32
  122. package/dist/wire.d.ts.map +1 -1
  123. package/dist/wire.js +48 -31
  124. package/package.json +2 -2
@@ -1,31 +1,46 @@
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';
19
+ import { doorReads } from './reads.js';
17
20
  import { refreshRoom } from './refresh.js';
18
21
  import { buildBatch, joined, limitedIn, requeued } from './save.js';
19
22
  import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
20
23
  import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
21
24
  import { same, saving, STARTING, transferring, } from './status.js';
22
25
  import { detectStorageLoss, nextGeneration } from './durability.js';
26
+ import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
27
+ import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
23
28
  import { FencedMap } from './fence.js';
24
- import { roomDraftRevision } from './identity.js';
29
+ import { tally } from './shared.js';
30
+ import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
31
+ import { identityTime, roomDraftRevision } from './identity.js';
25
32
  import { beneath, normalize } from './paths.js';
26
33
  import { openRuntimeStore, } from './store.js';
27
34
  import { unconfirmed } from './transfer.js';
28
35
  import { governs, holds, narrowed, planReach, } from './reach.js';
36
+ function roomsAlone(plan) {
37
+ return (plan.reach === null &&
38
+ plan.root.types === null &&
39
+ plan.root.where === null &&
40
+ plan.root.pathPrefix === null &&
41
+ plan.hop === null &&
42
+ plan.backlinks === null);
43
+ }
29
44
  function within(root, path) {
30
45
  return root === '' || beneath(root, path);
31
46
  }
@@ -42,6 +57,29 @@ function declaredSelection(room) {
42
57
  const OPENING_LANE = 'opening';
43
58
  const CONVERSATION_LANE = 'conversations';
44
59
  const ROOM_READS = 256;
60
+ const ROOM_ATTEMPTS = 16;
61
+ const outside = new Map();
62
+ function outsideOf(roots) {
63
+ const key = JSON.stringify(roots);
64
+ const held = outside.get(key);
65
+ if (held !== undefined)
66
+ return held;
67
+ const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
68
+ const made = {
69
+ documents: (path) => (markdownPath(path) ? counted(path) : 0),
70
+ binaries: counted,
71
+ };
72
+ outside.set(key, made);
73
+ return made;
74
+ }
75
+ function abandonedOn(signal) {
76
+ return new Promise((resolve) => {
77
+ if (signal.aborted)
78
+ resolve(null);
79
+ else
80
+ signal.addEventListener('abort', () => resolve(null), { once: true });
81
+ });
82
+ }
45
83
  function roomLane(locator) {
46
84
  return `room ${locator}`;
47
85
  }
@@ -59,6 +97,8 @@ export function composeRuntime(options) {
59
97
  const replicas = new FencedMap(isFenced);
60
98
  const queuesWithoutReplica = new FencedMap(isFenced);
61
99
  const pendings = new Map();
100
+ const seeds = new Map();
101
+ const seedWatchers = new Map();
62
102
  const roomDrafts = new FencedMap(isFenced);
63
103
  const presentations = new Map();
64
104
  let limits = [];
@@ -71,6 +111,7 @@ export function composeRuntime(options) {
71
111
  const liveModels = new FencedMap(isModelFenced);
72
112
  const invalidModels = new Set();
73
113
  const discovered = new Map();
114
+ const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
74
115
  let roomsListed = false;
75
116
  const scopedRefusals = new Map();
76
117
  const sourceRefusals = new Map();
@@ -98,7 +139,6 @@ export function composeRuntime(options) {
98
139
  let status = STARTING;
99
140
  let shown = STARTING;
100
141
  let discoveryPaused = false;
101
- let resetTimer;
102
142
  let closed = false;
103
143
  let stopping = false;
104
144
  let withdrawStream;
@@ -106,7 +146,10 @@ export function composeRuntime(options) {
106
146
  let underway = null;
107
147
  let streamed = null;
108
148
  let handed = false;
109
- const submitting = new Map();
149
+ const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
150
+ const repairing = new Set();
151
+ const repairable = new Map();
152
+ let outcomeRetention = null;
110
153
  const answering = new Map();
111
154
  let complete = false;
112
155
  let recorded = false;
@@ -136,25 +179,41 @@ export function composeRuntime(options) {
136
179
  const serviceModeListeners = new Set();
137
180
  let member = null;
138
181
  const memberListeners = new Set();
139
- const materialized = new Set();
140
- let everyRoom = false;
182
+ const prepared = new Set();
183
+ const projecting = new Set();
184
+ const unpreparedDemand = new Set();
185
+ const unindexable = new Set();
186
+ let preparing = false;
187
+ let demand = WHOLE_DEMAND;
188
+ let narrowing = null;
189
+ let targets = {};
190
+ let persistence = options.shell.persistence;
191
+ let capacity = null;
192
+ const paused = new Set();
193
+ let rechecking = null;
194
+ const readinessWatches = new Set();
195
+ let readinessQueued = false;
196
+ const catching = new Set();
141
197
  const transfers = new FencedMap(isFenced);
142
198
  const conversations = createConversationStore({
143
199
  agents: declaredAgents,
144
- documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
200
+ documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
145
201
  call: (name, input) => invokeAction(name, input),
146
202
  durably,
147
203
  stream: () => options.adapter.conversations,
148
204
  publish: publishChangeSet,
149
205
  online: () => !status.providerUnreachable && refusal === null,
150
206
  closed: () => closed,
207
+ unanswered: resendLater,
208
+ expired: (operation) => pastHorizon(operation),
151
209
  });
152
210
  const searchIndex = createSearchIndex({
153
- documents: () => readSide.documents(),
211
+ room: (room) => readSide.room(room),
154
212
  document: (room, path) => readSide.document(room, path),
155
- messages: () => conversations.messages(),
156
- message: (conversation, identity) => conversations.message(conversation, identity),
157
213
  });
214
+ const roomsToIndex = new Set();
215
+ const conversationsToIndex = new Set();
216
+ let indexing = false;
158
217
  const journal = createLog();
159
218
  let notifyReady = () => { };
160
219
  const ready = new Promise((resolve) => {
@@ -233,9 +292,16 @@ export function composeRuntime(options) {
233
292
  returned: structuredClone(record.returned),
234
293
  });
235
294
  }
295
+ for (const [room, refusal] of sourceRefusals)
296
+ attentionRequired.push({ room, refusal: structuredClone(refusal) });
236
297
  attentionRequired.sort((left, right) => {
237
298
  if (left.room !== right.room)
238
299
  return left.room < right.room ? -1 : 1;
300
+ if ('refusal' in left || 'refusal' in right) {
301
+ if ('refusal' in left && 'refusal' in right)
302
+ return 0;
303
+ return 'refusal' in left ? -1 : 1;
304
+ }
239
305
  if (left.path !== right.path)
240
306
  return left.path < right.path ? -1 : 1;
241
307
  return 0;
@@ -254,11 +320,9 @@ export function composeRuntime(options) {
254
320
  if (replica === undefined)
255
321
  return 0;
256
322
  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;
323
+ const weighed = outsideOf(roots);
324
+ return (tally(replica.visible.documents, weighed.documents) +
325
+ tally(replica.visible.binaries, weighed.binaries));
262
326
  }
263
327
  function listed() {
264
328
  return structuredClone([...rooms.values()].map((room) => {
@@ -285,15 +349,19 @@ export function composeRuntime(options) {
285
349
  return { listed: roomsListed, rooms: held };
286
350
  }
287
351
  function emitRooms() {
352
+ readSide.recover();
353
+ queueReadiness();
288
354
  const value = listed();
289
355
  for (const listener of roomListeners)
290
356
  listener(value);
291
357
  }
292
358
  function emitRefusal() {
359
+ queueReadiness();
293
360
  for (const listener of refusalListeners)
294
361
  listener(refusal);
295
362
  }
296
363
  function emitStorageLoss() {
364
+ queueReadiness();
297
365
  for (const listener of storageLossListeners)
298
366
  listener(storageLoss);
299
367
  }
@@ -310,7 +378,8 @@ export function composeRuntime(options) {
310
378
  function show() {
311
379
  if (closed)
312
380
  return;
313
- const next = status.state === 'synced' && !conversations.current()
381
+ queueReadiness();
382
+ const next = status.state === 'synced' && demand.conversations && !conversations.current()
314
383
  ? { ...status, state: 'syncing' }
315
384
  : status;
316
385
  if (same(shown, next))
@@ -339,42 +408,511 @@ export function composeRuntime(options) {
339
408
  }
340
409
  }
341
410
  function applyChangeSet(set) {
342
- const held = everyRoom
343
- ? set
344
- : { ...set, documents: set.documents.filter(({ locator }) => materialized.has(locator)) };
411
+ const held = {
412
+ ...set,
413
+ documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
414
+ };
345
415
  readSide.apply(held);
346
416
  searchIndex.changed(held);
347
417
  }
348
- function materialize(locators) {
349
- if (everyRoom)
418
+ async function referenced(locator) {
419
+ for (;;) {
420
+ const record = replicas.get(locator)?.confirmed.record;
421
+ if (record === undefined || closed)
422
+ return;
423
+ const held = { text: record.documents, binaries: record.binaries };
424
+ const composition = compositionOf(record);
425
+ if (referencesIndexed(held, composition))
426
+ return;
427
+ await indexReferrers(held, composition);
428
+ }
429
+ }
430
+ async function prepareReplica(locator) {
431
+ await referenced(locator);
432
+ const replica = replicas.get(locator);
433
+ if (replica === undefined || closed || prepared.has(locator))
350
434
  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));
435
+ const visible = replica.visible;
436
+ const paths = [
437
+ ...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
438
+ ];
439
+ const entered = [];
440
+ projecting.add(locator);
441
+ try {
442
+ await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
443
+ const current = replicas.get(locator);
444
+ if (current === undefined || closed)
445
+ return;
446
+ const set = entering(locator, current, chunk);
447
+ readSide.apply(set);
448
+ entered.push(set);
449
+ });
450
+ if (!replicas.has(locator) || closed)
451
+ return;
452
+ prepared.add(locator);
361
453
  }
362
- if (sets.length === 0)
454
+ finally {
455
+ projecting.delete(locator);
456
+ }
457
+ conversations.changed(mergeChangeSets(entered));
458
+ readSide.recover();
459
+ queueReadiness();
460
+ }
461
+ function unprepared() {
462
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
463
+ }
464
+ function unbuilt(locator, error) {
465
+ unindexable.add(locator);
466
+ journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
467
+ queueReadiness();
468
+ }
469
+ function prepare(locators = []) {
470
+ for (const locator of locators)
471
+ unpreparedDemand.add(locator);
472
+ if (preparing)
363
473
  return;
364
- const set = mergeChangeSets(sets);
365
- readSide.apply(set);
366
- searchIndex.changed(set);
474
+ preparing = true;
475
+ void (async () => {
476
+ try {
477
+ for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
478
+ await yielded();
479
+ if (closed)
480
+ return;
481
+ unpreparedDemand.delete(waiting);
482
+ await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
483
+ }
484
+ }
485
+ finally {
486
+ preparing = false;
487
+ }
488
+ })();
367
489
  }
368
490
  function reachedRooms(plan) {
369
- if (plan.rooms !== null)
370
- return plan.rooms;
371
- if (plan.reach === null)
491
+ const reach = plan.reach;
492
+ const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
493
+ const named = plan.rooms ??
494
+ (reached === null
495
+ ? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
496
+ : [...reached]);
497
+ return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
498
+ }
499
+ function awaited(locator) {
500
+ return (!roomsListed ||
501
+ (rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
502
+ }
503
+ function covered(plan) {
504
+ const locators = reachedRooms(plan);
505
+ if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
506
+ return { pending: [] };
507
+ }
508
+ const pending = [];
509
+ const coverage = [];
510
+ const overlay = [];
511
+ for (const locator of locators) {
512
+ const replica = replicas.get(locator);
513
+ if (replica === undefined) {
514
+ if (awaited(locator))
515
+ pending.push({ room: locator, awaits: 'acquisition' });
516
+ continue;
517
+ }
518
+ if (!prepared.has(locator)) {
519
+ pending.push({ room: locator, awaits: 'preparation' });
520
+ continue;
521
+ }
522
+ coverage.push({
523
+ room: locator,
524
+ generation: replica.confirmed.record.transfer,
525
+ current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
526
+ });
527
+ for (const batch of replica.queue)
528
+ overlay.push(batch.id);
529
+ }
530
+ if (pending.length > 0)
531
+ return { pending };
532
+ return { coverage: { rooms: coverage, indexes: [], overlay } };
533
+ }
534
+ function awaitPreparation(cover) {
535
+ if (!('pending' in cover))
536
+ return;
537
+ const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
538
+ if (waiting.length > 0)
539
+ prepare(waiting.map(({ room }) => room));
540
+ }
541
+ function acquires(room) {
542
+ return (demanded(demand, room) ||
543
+ (replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
544
+ queuesWithoutReplica.has(room.locator));
545
+ }
546
+ function acquiredLocators() {
547
+ return [...discovered.values()]
548
+ .filter(acquires)
549
+ .map((room) => room.locator)
550
+ .sort();
551
+ }
552
+ function heldDemand(acquired) {
553
+ if (demand.whole)
554
+ return null;
555
+ return { rooms: acquired.map((room) => room.locator), conversations: demand.conversations };
556
+ }
557
+ function applyDemand(next) {
558
+ const before = acquiredLocators();
559
+ const conversed = demand.conversations;
560
+ demand = next;
561
+ const after = acquiredLocators();
562
+ queueReadiness();
563
+ if (!conversed && next.conversations)
564
+ scheduleCurrency();
565
+ if (JSON.stringify(before) === JSON.stringify(after) && conversed === next.conversations)
566
+ return;
567
+ journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
568
+ if (options.adapter.roomStream === undefined)
569
+ void sync('unstreamed');
570
+ else if (socketOpen)
571
+ options.adapter.roomStream.reopen();
572
+ }
573
+ function acquire(next) {
574
+ if (closed)
575
+ return;
576
+ if (narrowing !== null)
577
+ clearTimeout(narrowing);
578
+ narrowing = null;
579
+ const widening = wider(next, demand);
580
+ if (!widening && !narrower(next, demand))
581
+ return;
582
+ if (widening || !roomsListed) {
583
+ applyDemand(next);
584
+ return;
585
+ }
586
+ // A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
587
+ narrowing = setTimeout(() => {
588
+ narrowing = null;
589
+ applyDemand(next);
590
+ }, 5_000);
591
+ }
592
+ function requiredRooms(reach) {
593
+ if (reach === null)
594
+ return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
595
+ const named = [...new Set(reach.rooms)].sort();
596
+ if (roomsListed)
597
+ return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
598
+ return { listed: named.every((room) => replicas.has(room)), rooms: named };
599
+ }
600
+ function roomState(locator, search) {
601
+ if (sourceRefusals.has(locator))
602
+ return 'refused';
603
+ const listedRoom = discovered.get(locator);
604
+ if (roomsListed &&
605
+ (listedRoom === undefined ||
606
+ listedRoom.replica.admission !== 'admitted' ||
607
+ unreadable.has(locator)))
608
+ return 'unadmitted';
609
+ if (!replicas.has(locator))
610
+ return 'acquiring';
611
+ if (!prepared.has(locator))
612
+ return 'preparing';
613
+ if (search && !searchIndex.rooms().has(locator))
614
+ return 'indexing';
615
+ return 'complete';
616
+ }
617
+ function conversationCoverage(reach, search) {
618
+ const declared = reach === null || reach.conversations;
619
+ if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
620
+ return 'undeclared';
621
+ if (!conversations.known() || !conversations.current())
622
+ return 'acquiring';
623
+ if (!search)
624
+ return 'complete';
625
+ const unindexed = conversations
626
+ .searchable()
627
+ .filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
628
+ if (unindexed.length === 0)
629
+ return 'complete';
630
+ index([], unindexed);
631
+ return 'indexing';
632
+ }
633
+ function connectionOf() {
634
+ if (socketOpen)
635
+ return 'connected';
636
+ if (status.providerUnreachable)
637
+ return 'unreachable';
638
+ return options.adapter.roomStream === undefined && status.lastCompleted !== null
639
+ ? 'connected'
640
+ : 'connecting';
641
+ }
642
+ function refusedIn(locators) {
643
+ return locators.flatMap((room) => {
644
+ const held = sourceRefusals.get(room);
645
+ if (held === undefined)
646
+ return [];
647
+ const { file, location } = held.fields;
648
+ return [
649
+ {
650
+ room,
651
+ file: typeof file === 'string' ? file : '',
652
+ rule: held.code,
653
+ location: typeof location === 'string' ? location : null,
654
+ },
655
+ ];
656
+ });
657
+ }
658
+ function readinessFor(port, reach) {
659
+ const required = requiredRooms(reach);
660
+ const search = port.search === 'required';
661
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
662
+ const building = (state) => coverage
663
+ .filter((room) => room.state === state && !unindexable.has(room.room))
664
+ .map(({ room }) => room);
665
+ const preparing = building('preparing');
666
+ if (preparing.length > 0)
667
+ prepare(preparing);
668
+ const indexing = building('indexing');
669
+ if (indexing.length > 0)
670
+ index(indexing, []);
671
+ return readinessOf({
672
+ code: port.code,
673
+ coverage: {
674
+ listed: required.listed,
675
+ rooms: coverage,
676
+ conversations: conversationCoverage(reach, search),
677
+ search: search ? 'required' : 'unused',
678
+ },
679
+ persistence,
680
+ connection: connectionOf(),
681
+ house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
682
+ capacity: capacity !== null &&
683
+ coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
684
+ ? capacity
685
+ : port.capacity,
686
+ refused: refusedIn(required.rooms),
687
+ lost: storageLoss !== null,
688
+ established: targets[port.target]?.established === true,
689
+ });
690
+ }
691
+ function establish(name) {
692
+ targets = { ...targets, [name]: { established: true } };
693
+ queueReadiness();
694
+ void durably((local) => local.keepTargets(targets)).catch(() => {
695
+ journal.record(`target record failed ${name}`);
696
+ });
697
+ }
698
+ function deliverReadiness(watch) {
699
+ if (!readinessWatches.has(watch))
700
+ return;
701
+ const readiness = readinessFor(watch.port, watch.reach);
702
+ if (readiness.ready && targets[watch.port.target]?.established !== true) {
703
+ establish(watch.port.target);
704
+ }
705
+ const shown = JSON.stringify(readiness);
706
+ if (shown === watch.last)
707
+ return;
708
+ watch.last = shown;
709
+ watch.deliver(structuredClone(readiness));
710
+ }
711
+ function queueReadiness() {
712
+ if (readinessQueued || closed || readinessWatches.size === 0)
713
+ return;
714
+ readinessQueued = true;
715
+ queueMicrotask(() => {
716
+ readinessQueued = false;
717
+ if (closed)
718
+ return;
719
+ for (const watch of [...readinessWatches])
720
+ deliverReadiness(watch);
721
+ });
722
+ }
723
+ function watchReadiness(port, reach, deliver) {
724
+ const watch = { port, reach, deliver, last: null };
725
+ readinessWatches.add(watch);
726
+ void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
727
+ return () => {
728
+ readinessWatches.delete(watch);
729
+ };
730
+ }
731
+ function layerBytes(layer) {
732
+ let bytes = 0;
733
+ for (const [path, text] of Object.entries(layer.documents))
734
+ bytes += (path.length + text.length) * 2;
735
+ return bytes;
736
+ }
737
+ function grownBy(next, previous) {
738
+ if (previous === undefined)
739
+ return layerBytes(next);
740
+ const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
741
+ return layerBytes({ ...next, documents });
742
+ }
743
+ function exceeded(error) {
744
+ return (typeof error === 'object' &&
745
+ error !== null &&
746
+ error.name === 'QuotaExceededError');
747
+ }
748
+ async function estimated() {
749
+ const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
750
+ return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
751
+ }
752
+ function pause(locator, cause) {
753
+ paused.add(locator);
754
+ capacity = cause;
755
+ journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
756
+ queueReadiness();
757
+ if (rechecking !== null)
758
+ return;
759
+ // A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
760
+ rechecking = setInterval(() => {
761
+ void recheck();
762
+ }, 60_000);
763
+ }
764
+ async function recheck() {
765
+ const held = capacity;
766
+ if (held === null || closed)
767
+ return;
768
+ const available = await estimated().catch(() => null);
769
+ if (available === null || available < (held.needed ?? 0))
770
+ return;
771
+ if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
772
+ return;
773
+ resume();
774
+ }
775
+ function resume() {
776
+ if (rechecking !== null)
777
+ clearInterval(rechecking);
778
+ rechecking = null;
779
+ if (paused.size === 0 && capacity === null)
780
+ return;
781
+ paused.clear();
782
+ capacity = null;
783
+ journal.record('capacity resumed');
784
+ queueReadiness();
785
+ if (!closed && authorization !== undefined)
786
+ void sync('unstreamed');
787
+ }
788
+ async function reclaimed(local) {
789
+ const unreferenced = [...transfers.keys()].filter((locator) => {
790
+ const room = discovered.get(locator);
791
+ return room === undefined || !acquires(room);
792
+ });
793
+ for (const locator of unreferenced) {
794
+ await local.abandon(locator);
795
+ transfers.delete(locator);
796
+ }
797
+ if (unreferenced.length > 0)
798
+ journal.record(`capacity reclaimed ${unreferenced.length}`);
799
+ return unreferenced.length > 0;
800
+ }
801
+ async function admitted(local, locator, needed) {
802
+ if (paused.has(locator))
803
+ return false;
804
+ let available = await estimated().catch(() => null);
805
+ if (available === null || available >= needed)
806
+ return true;
807
+ if (await reclaimed(local))
808
+ available = await estimated().catch(() => null);
809
+ if (available === null || available >= needed)
810
+ return true;
811
+ pause(locator, { cause: 'insufficient-space', needed, available });
812
+ return false;
813
+ }
814
+ async function exceededOn(locator, bytes) {
815
+ const available = await estimated().catch(() => null);
816
+ pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
817
+ }
818
+ function seedRoomHeld(room, version) {
819
+ return prepared.has(room) && reachedBy(room, version);
820
+ }
821
+ function seedHeld(at) {
822
+ return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
823
+ }
824
+ function seedApplies(plan) {
825
+ const seed = seeds.get(plan.key);
826
+ if (seed === undefined)
372
827
  return null;
373
- return [...plan.reach.rooms, ...plan.reach.documents.map(roomOfKey)];
828
+ if (!seedHeld(seed.at))
829
+ return seed;
830
+ seeds.delete(plan.key);
831
+ return null;
832
+ }
833
+ function seedBehind(plan, cover) {
834
+ const seed = seedApplies(plan);
835
+ if (seed === null || 'pending' in cover)
836
+ return cover;
837
+ return {
838
+ pending: Object.entries(seed.at)
839
+ .filter(([room, version]) => !seedRoomHeld(room, version))
840
+ .map(([room]) => ({ room, awaits: 'acquisition' })),
841
+ };
374
842
  }
375
- function rowsQuery(plan) {
376
- materialize(reachedRooms(plan));
377
- return readSide.query(plan);
843
+ function seed(entry) {
844
+ if (seedHeld(entry.at))
845
+ return;
846
+ for (const { query, rows } of entry.queries) {
847
+ const key = queryKey(query);
848
+ seeds.set(key, { rows, at: entry.at });
849
+ for (const shown of seedWatchers.get(key) ?? [])
850
+ shown();
851
+ }
852
+ }
853
+ async function rowsQuery(plan) {
854
+ const before = covered(plan);
855
+ awaitPreparation(before);
856
+ const seeded = seedApplies(plan);
857
+ if (seeded !== null)
858
+ return seededResult(seeded).result;
859
+ if ('pending' in before)
860
+ return before;
861
+ const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
862
+ awaitPreparation(cover);
863
+ if ('pending' in cover)
864
+ return cover;
865
+ return 'groups' in rows
866
+ ? { groups: rows.groups, coverage: cover.coverage }
867
+ : { documents: rows.documents, coverage: cover.coverage };
868
+ }
869
+ async function indexConversation(conversation) {
870
+ searchIndex.indexMessages(conversation, [], false);
871
+ let after = null;
872
+ for (;;) {
873
+ const batch = await durably((local) => local.messagesAfter(conversation, after));
874
+ if (closed)
875
+ return;
876
+ searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
877
+ if (batch.length === 0)
878
+ return;
879
+ after = batch[batch.length - 1].identity;
880
+ }
881
+ }
882
+ function indexNext() {
883
+ const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
884
+ if (room !== undefined) {
885
+ roomsToIndex.delete(room);
886
+ return () => prepared.has(room)
887
+ ? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
888
+ : undefined;
889
+ }
890
+ const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
891
+ if (conversation === undefined)
892
+ return null;
893
+ conversationsToIndex.delete(conversation);
894
+ return () => indexConversation(conversation);
895
+ }
896
+ function index(rooms, named) {
897
+ for (const room of rooms)
898
+ roomsToIndex.add(room);
899
+ for (const conversation of named)
900
+ conversationsToIndex.add(conversation);
901
+ if (indexing)
902
+ return;
903
+ indexing = true;
904
+ void (async () => {
905
+ try {
906
+ for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
907
+ await yielded();
908
+ await unit();
909
+ queueReadiness();
910
+ }
911
+ }
912
+ finally {
913
+ indexing = false;
914
+ }
915
+ })().catch(() => { });
378
916
  }
379
917
  function publishChangeSet(set) {
380
918
  applyChangeSet(set);
@@ -391,34 +929,7 @@ export function composeRuntime(options) {
391
929
  emitPresentation(app);
392
930
  conversations.changed(set);
393
931
  }
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
932
  async function refuse(reason, provider = null) {
421
- stopTimer();
422
933
  discoveryPaused = true;
423
934
  try {
424
935
  if (reason === 'revoked-authorization') {
@@ -503,6 +1014,9 @@ export function composeRuntime(options) {
503
1014
  if (fence !== undefined && fence.erased && fence.at < started)
504
1015
  fences.delete(room.locator);
505
1016
  }
1017
+ await loseRooms(found
1018
+ .filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
1019
+ .map((room) => room.locator));
506
1020
  const listed = found.filter((room) => !fences.has(room.locator));
507
1021
  const listedLocators = new Set(listed.map((room) => room.locator));
508
1022
  for (const locator of listedLocators)
@@ -527,6 +1041,12 @@ export function composeRuntime(options) {
527
1041
  emitRooms();
528
1042
  return listed;
529
1043
  }
1044
+ function heldLocally(locator) {
1045
+ return (replicas.has(locator) ||
1046
+ roomDrafts.has(locator) ||
1047
+ transfers.has(locator) ||
1048
+ queuesWithoutReplica.has(locator));
1049
+ }
530
1050
  function unlistedHeld(listed) {
531
1051
  return [
532
1052
  ...new Set([
@@ -563,10 +1083,14 @@ export function composeRuntime(options) {
563
1083
  rooms.delete(locator);
564
1084
  roomDrafts.delete(locator);
565
1085
  transfers.delete(locator);
1086
+ catching.delete(locator);
566
1087
  discovered.delete(locator);
567
1088
  announced.delete(locator);
568
1089
  unreadable.delete(locator);
569
1090
  stale.delete(locator);
1091
+ repairable.delete(locator);
1092
+ unindexable.delete(locator);
1093
+ paused.delete(locator);
570
1094
  scopedRefusals.delete(locator);
571
1095
  sourceRefusals.delete(locator);
572
1096
  declarationRefusals.delete(locator);
@@ -582,7 +1106,9 @@ export function composeRuntime(options) {
582
1106
  return fresh;
583
1107
  publishChangeSet(mergeChangeSets(removed));
584
1108
  for (const locator of fresh)
585
- materialized.delete(locator);
1109
+ prepared.delete(locator);
1110
+ searchIndex.forget(fresh);
1111
+ readSide.recover();
586
1112
  moveTo({ ...status, ...attention() });
587
1113
  return fresh;
588
1114
  }
@@ -618,7 +1144,7 @@ export function composeRuntime(options) {
618
1144
  return model;
619
1145
  }
620
1146
  async function hydrate(opened) {
621
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
1147
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
622
1148
  opened.all(),
623
1149
  opened.queues(),
624
1150
  opened.roomDrafts(),
@@ -630,7 +1156,9 @@ export function composeRuntime(options) {
630
1156
  opened.capabilities(),
631
1157
  opened.member(),
632
1158
  opened.transfers(),
1159
+ opened.targets(),
633
1160
  ]);
1161
+ targets = heldTargets;
634
1162
  transfers.clear();
635
1163
  for (const record of staged)
636
1164
  transfers.set(record.locator, record);
@@ -692,6 +1220,7 @@ export function composeRuntime(options) {
692
1220
  async function readState(opened) {
693
1221
  const { lastCompleted, locators, read } = await hydrate(opened);
694
1222
  applyChangeSet(read);
1223
+ prepare();
695
1224
  if (replicas.size > 0)
696
1225
  emitRooms();
697
1226
  if (member !== null)
@@ -807,17 +1336,19 @@ export function composeRuntime(options) {
807
1336
  moveTo({ ...status, ...attention() });
808
1337
  throw error;
809
1338
  }
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
- });
1339
+ return () => {
1340
+ queuesWithoutReplica.delete(locator);
1341
+ for (const { batch, returned } of unqueued) {
1342
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
1343
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1344
+ }
1345
+ replicas.set(locator, replicaOf(confirmed, kept, before));
1346
+ return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
1347
+ room: settling.room,
1348
+ queue: kept.length !== previousQueue.length,
1349
+ drafts: [...written.keys()],
1350
+ });
1351
+ };
821
1352
  }
822
1353
  function adoptModels(locator, record) {
823
1354
  for (const [attached, model] of [
@@ -853,8 +1384,13 @@ export function composeRuntime(options) {
853
1384
  function publish(local, room, confirmed, landing, live) {
854
1385
  const { base, stored } = landing;
855
1386
  return exclusive(async () => {
856
- if (!live())
1387
+ if (stored !== undefined) {
1388
+ await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
1389
+ }
1390
+ if (!live()) {
1391
+ caughtUpTo(room.locator);
857
1392
  return true;
1393
+ }
858
1394
  const prior = replicas.get(room.locator);
859
1395
  if (base !== undefined &&
860
1396
  (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
@@ -868,17 +1404,33 @@ export function composeRuntime(options) {
868
1404
  const accepted = pendings.get(batch.id)?.accepted;
869
1405
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
870
1406
  });
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)
1407
+ let settled;
1408
+ try {
1409
+ settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
1410
+ }
1411
+ catch (error) {
1412
+ if (!exceeded(error))
1413
+ throw error;
1414
+ await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
1415
+ caughtUpTo(room.locator);
1416
+ return true;
1417
+ }
1418
+ const set = settled();
1419
+ if (stored !== undefined) {
873
1420
  transfers.delete(room.locator);
1421
+ catching.delete(room.locator);
1422
+ }
874
1423
  for (const batch of accepted)
875
1424
  pendings.delete(batch.id);
876
1425
  stale.delete(room.locator);
1426
+ unindexable.delete(room.locator);
877
1427
  if (unreadable.delete(room.locator))
878
1428
  scopedRefusals.delete(room.locator);
879
1429
  sourceRefusals.delete(room.locator);
880
1430
  declarationRefusals.delete(room.locator);
881
1431
  list(room, true);
1432
+ if (prior === undefined)
1433
+ prepared.add(room.locator);
882
1434
  publishChangeSet(set);
883
1435
  moveTo({ ...status, ...attention() });
884
1436
  options.adapter.roomStream?.published(room.locator, roomVersion);
@@ -887,6 +1439,7 @@ export function composeRuntime(options) {
887
1439
  }
888
1440
  async function saveRoomDrafts(locator, named, writtenBy) {
889
1441
  const local = await openLocal();
1442
+ await referenced(locator);
890
1443
  const replica = replicas.get(locator);
891
1444
  const room = rooms.get(locator);
892
1445
  if (replica === undefined || room === undefined) {
@@ -934,6 +1487,11 @@ export function composeRuntime(options) {
934
1487
  changes,
935
1488
  bases: parts.flatMap(({ bases }) => bases),
936
1489
  writtenBy,
1490
+ clean: {
1491
+ text: replica.confirmed.record.documents,
1492
+ binaries: replica.confirmed.record.binaries,
1493
+ composition: compositionOf(replica.confirmed.record),
1494
+ },
937
1495
  };
938
1496
  const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
939
1497
  if (checked?.kind === 'refused')
@@ -1256,8 +1814,9 @@ export function composeRuntime(options) {
1256
1814
  async function pullRefresh(authorized, confirmed, live) {
1257
1815
  if (!live())
1258
1816
  return unconfirmed;
1817
+ const turn = generation;
1259
1818
  try {
1260
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1819
+ const result = await refreshRoom(reads, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1261
1820
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
1262
1821
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
1263
1822
  return result;
@@ -1266,6 +1825,9 @@ export function composeRuntime(options) {
1266
1825
  journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
1267
1826
  throw error;
1268
1827
  }
1828
+ finally {
1829
+ reads.finished(confirmed.record.locator, turn);
1830
+ }
1269
1831
  }
1270
1832
  function staging(local, live) {
1271
1833
  return {
@@ -1274,8 +1836,20 @@ export function composeRuntime(options) {
1274
1836
  stage: (record, page) => exclusive(async () => {
1275
1837
  if (!live())
1276
1838
  return false;
1277
- await local.stage(record, page);
1839
+ const bytes = layerBytes(page);
1840
+ if (!(await admitted(local, record.locator, bytes)))
1841
+ return false;
1842
+ try {
1843
+ await local.stage(record, page);
1844
+ }
1845
+ catch (error) {
1846
+ if (!exceeded(error))
1847
+ throw error;
1848
+ await exceededOn(record.locator, bytes);
1849
+ return false;
1850
+ }
1278
1851
  transfers.set(record.locator, record);
1852
+ readSide.recover();
1279
1853
  return true;
1280
1854
  }),
1281
1855
  abandon: (locator) => exclusive(async () => {
@@ -1283,6 +1857,7 @@ export function composeRuntime(options) {
1283
1857
  return;
1284
1858
  await local.abandon(locator);
1285
1859
  transfers.delete(locator);
1860
+ readSide.recover();
1286
1861
  }),
1287
1862
  };
1288
1863
  }
@@ -1295,21 +1870,52 @@ export function composeRuntime(options) {
1295
1870
  async function pullBootstrap(authorized, room, local, live) {
1296
1871
  if (!live())
1297
1872
  return unconfirmed;
1873
+ const turn = generation;
1874
+ if (replicas.has(room.locator)) {
1875
+ catching.add(room.locator);
1876
+ readSide.recover();
1877
+ }
1298
1878
  try {
1299
- const result = await bootstrapRoom(options.adapter, authorized, room, {
1879
+ const result = await bootstrapRoom(reads, authorized, room, {
1300
1880
  collection: modelOf(room.locator),
1301
1881
  selection: modelOf(room.locator, 'typed-selection'),
1302
1882
  }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
1303
1883
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
1304
1884
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1885
+ if (result.kind !== 'staged')
1886
+ caughtUpTo(room.locator);
1305
1887
  return result;
1306
1888
  }
1307
1889
  catch (error) {
1308
1890
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
1891
+ caughtUpTo(room.locator);
1309
1892
  throw error;
1310
1893
  }
1894
+ finally {
1895
+ reads.finished(room.locator, turn);
1896
+ }
1897
+ }
1898
+ function caughtUpTo(locator) {
1899
+ if (catching.delete(locator))
1900
+ readSide.recover();
1901
+ }
1902
+ function behind(room) {
1903
+ const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1904
+ return (held !== undefined &&
1905
+ [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
1311
1906
  }
1312
1907
  async function advance(room, local, authorized, live) {
1908
+ const carried = reads.carried(room.locator);
1909
+ const withheld = await advanceOnce(room, local, authorized, live);
1910
+ if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
1911
+ return withheld;
1912
+ }
1913
+ return advanceOnce(room, local, authorized, live);
1914
+ }
1915
+ async function advanceOnce(room, local, authorized, live) {
1916
+ await referenced(room.locator);
1917
+ if (!live() || paused.has(room.locator))
1918
+ return unconfirmed;
1313
1919
  const held = replicas.get(room.locator);
1314
1920
  if (held !== undefined) {
1315
1921
  const refreshed = await pullRefresh(authorized, held.confirmed, live);
@@ -1340,6 +1946,7 @@ export function composeRuntime(options) {
1340
1946
  return refreshed;
1341
1947
  if (opened !== null) {
1342
1948
  owed.add(room.locator);
1949
+ readSide.recover();
1343
1950
  return unconfirmed;
1344
1951
  }
1345
1952
  }
@@ -1352,10 +1959,19 @@ export function composeRuntime(options) {
1352
1959
  return null;
1353
1960
  }
1354
1961
  async function withhold(withheld, room, local, progress) {
1962
+ if (withheld.kind === 'unconfirmed')
1963
+ return;
1355
1964
  if (withheld.kind === 'refused')
1356
1965
  stale.delete(room.locator);
1357
1966
  else
1358
1967
  stale.add(room.locator);
1968
+ if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
1969
+ const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
1970
+ repairable.set(room.locator, reset);
1971
+ repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
1972
+ }
1973
+ else
1974
+ repairable.delete(room.locator);
1359
1975
  if (withheld.kind === 'scoped-refusal') {
1360
1976
  await scoped(withheld, room, local);
1361
1977
  }
@@ -1391,6 +2007,7 @@ export function composeRuntime(options) {
1391
2007
  unreadable.add(room.locator);
1392
2008
  list(room, false);
1393
2009
  emitRooms();
2010
+ moveTo({ ...status, ...attention() });
1394
2011
  journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
1395
2012
  }
1396
2013
  function leaving(batch, result) {
@@ -1412,6 +2029,7 @@ export function composeRuntime(options) {
1412
2029
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1413
2030
  }
1414
2031
  async function adoptAccepted(room, local, submitted, accepted) {
2032
+ await referenced(room.locator);
1415
2033
  if (!continuing())
1416
2034
  return unconfirmed;
1417
2035
  const current = replicas.get(room.locator);
@@ -1433,11 +2051,12 @@ export function composeRuntime(options) {
1433
2051
  emitQueue(room.locator);
1434
2052
  return applied;
1435
2053
  }
1436
- const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
2054
+ const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1437
2055
  pendings.delete(submitted.id);
1438
2056
  unreadable.delete(room.locator);
1439
2057
  sourceRefusals.delete(room.locator);
1440
2058
  declarationRefusals.delete(room.locator);
2059
+ const set = settled();
1441
2060
  list(room, true);
1442
2061
  publishChangeSet(set);
1443
2062
  moveTo({ ...status, ...attention() });
@@ -1451,14 +2070,14 @@ export function composeRuntime(options) {
1451
2070
  refusedSource(room, failed);
1452
2071
  return false;
1453
2072
  }
1454
- async function submitRoom(room, local, authorized, progress) {
1455
- while (continuing() && authorization !== undefined) {
2073
+ async function submitRoom(room, local, authorized, progress, signal) {
2074
+ while (continuing() && authorization !== undefined && !signal.aborted) {
1456
2075
  const replica = replicas.get(room.locator);
1457
2076
  const head = replica?.queue[0];
1458
2077
  if (replica === undefined || head === undefined)
1459
- return;
2078
+ return 'done';
1460
2079
  if (pendings.get(head.id)?.accepted !== undefined)
1461
- return;
2080
+ return 'done';
1462
2081
  const state = pendings.get(head.id);
1463
2082
  const { submission, lost: earlier } = state ?? {
1464
2083
  submission: { state: 'waiting' },
@@ -1466,24 +2085,30 @@ export function composeRuntime(options) {
1466
2085
  };
1467
2086
  if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
1468
2087
  progress.complete = false;
1469
- return;
2088
+ return { at: Date.parse(submission.reset) };
1470
2089
  }
1471
2090
  if (writesHeld()) {
1472
2091
  progress.complete = false;
1473
2092
  journal.record(`submission ${room.locator} ${head.id} held`);
1474
- return;
2093
+ return 'done';
1475
2094
  }
1476
2095
  const lost = earlier || submission.state === 'submitted';
2096
+ if (lost && pastHorizon(head.id)) {
2097
+ await discardPending(room, local, signal);
2098
+ return 'done';
2099
+ }
1477
2100
  let result;
1478
2101
  journal.record(`submission ${room.locator} ${head.id}`);
1479
2102
  await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1480
2103
  if (discoveryPaused || fences.has(room.locator))
1481
- return;
1482
- if (writesHeld()) {
2104
+ return 'done';
2105
+ if (signal.aborted || writesHeld()) {
1483
2106
  await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
2107
+ if (signal.aborted)
2108
+ return 'done';
1484
2109
  progress.complete = false;
1485
2110
  journal.record(`submission ${room.locator} ${head.id} held`);
1486
- return;
2111
+ return 'done';
1487
2112
  }
1488
2113
  const opensBefore = openSignals;
1489
2114
  const answered = awaitingAnswer(room.locator);
@@ -1499,32 +2124,36 @@ export function composeRuntime(options) {
1499
2124
  progress.complete = false;
1500
2125
  if (continuing())
1501
2126
  progress.reachable = false;
1502
- return;
2127
+ return 'again';
2128
+ }
2129
+ if (signal.aborted) {
2130
+ answered();
2131
+ return 'done';
1503
2132
  }
1504
2133
  if (result.kind !== 'accepted' || !continuing())
1505
2134
  answered();
1506
2135
  if (!continuing())
1507
- return;
2136
+ return 'done';
1508
2137
  if (result.kind === 'transport-uncertainty') {
1509
2138
  progress.complete = false;
1510
2139
  progress.reachable = false;
1511
- return;
2140
+ return 'again';
1512
2141
  }
1513
2142
  if (result.kind === 'room-not-readable') {
1514
2143
  progress.complete = false;
1515
2144
  unreadable.add(room.locator);
1516
2145
  list(room, false);
1517
2146
  emitRooms();
1518
- return;
2147
+ return 'done';
1519
2148
  }
1520
2149
  if (result.kind === 'revoked-authorization') {
1521
2150
  await refuse('revoked-authorization');
1522
- return;
2151
+ return 'done';
1523
2152
  }
1524
2153
  if (result.kind === 'scoped-refusal') {
1525
2154
  await scoped(result, room, local);
1526
2155
  progress.complete = false;
1527
- return;
2156
+ return 'done';
1528
2157
  }
1529
2158
  if (result.kind === 'write-halted') {
1530
2159
  journal.record(`outcome ${room.locator} ${head.id} write-halted`);
@@ -1533,16 +2162,24 @@ export function composeRuntime(options) {
1533
2162
  continue;
1534
2163
  haltedRefusal = true;
1535
2164
  progress.complete = false;
1536
- return;
2165
+ return 'done';
1537
2166
  }
1538
2167
  if (result.kind === 'conflicted' ||
1539
2168
  result.kind === 'rejected' ||
1540
2169
  result.kind === 'expired' ||
1541
2170
  result.kind === 'retryable-refusal') {
2171
+ if (result.kind === 'expired' && lost && result.restored !== true) {
2172
+ journal.record(`outcome ${room.locator} ${head.id} expired`);
2173
+ await discardPending(room, local, signal);
2174
+ return 'done';
2175
+ }
1542
2176
  if (result.kind === 'expired') {
2177
+ await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
1543
2178
  const withheld = await advance(room, local, authorized, continuing);
1544
- if (withheld !== null)
1545
- return withhold(withheld, room, local, progress);
2179
+ if (withheld !== null) {
2180
+ await withhold(withheld, room, local, progress);
2181
+ return 'done';
2182
+ }
1546
2183
  }
1547
2184
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
1548
2185
  (result.kind === 'conflicted' && result.roomVersion !== undefined
@@ -1555,8 +2192,7 @@ export function composeRuntime(options) {
1555
2192
  const reset = result.reset;
1556
2193
  progress.complete = false;
1557
2194
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1558
- armTimer();
1559
- return;
2195
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
1560
2196
  }
1561
2197
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
1562
2198
  const left = leaving(head, result);
@@ -1567,28 +2203,73 @@ export function composeRuntime(options) {
1567
2203
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
1568
2204
  return;
1569
2205
  }
1570
- const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
2206
+ const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1571
2207
  leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1572
2208
  (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1573
2209
  ? { ...left, returned: null }
1574
2210
  : left,
1575
2211
  });
1576
- publishChangeSet(set);
2212
+ publishChangeSet(settled());
1577
2213
  moveTo({ ...status, ...attention() });
1578
2214
  });
1579
2215
  if (result.kind === 'expired')
1580
2216
  continue;
1581
2217
  const withheld = await advance(room, local, authorized, continuing);
1582
- if (withheld !== null)
1583
- return withhold(withheld, room, local, progress);
2218
+ if (withheld !== null) {
2219
+ await withhold(withheld, room, local, progress);
2220
+ return 'done';
2221
+ }
1584
2222
  continue;
1585
2223
  }
1586
2224
  const kept = await adopted(room, local, head, result).finally(answered);
1587
2225
  if (!kept) {
1588
2226
  progress.complete = false;
1589
- return;
2227
+ return 'done';
1590
2228
  }
1591
2229
  }
2230
+ return 'done';
2231
+ }
2232
+ function pastHorizon(identity) {
2233
+ return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
2234
+ }
2235
+ async function discardPending(room, local, signal) {
2236
+ const { locator } = room;
2237
+ const discarded = await exclusive(async () => {
2238
+ if (fences.has(locator))
2239
+ return false;
2240
+ const before = replicas.get(locator);
2241
+ const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
2242
+ fences.set(locator, { at: discoveries, erased: false });
2243
+ lastFenced.set(locator, ++fencings);
2244
+ try {
2245
+ await local.discard(locator);
2246
+ }
2247
+ finally {
2248
+ fences.delete(locator);
2249
+ }
2250
+ for (const batch of queue)
2251
+ pendings.delete(batch.id);
2252
+ queuesWithoutReplica.delete(locator);
2253
+ const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
2254
+ if (after !== null)
2255
+ replicas.set(locator, after);
2256
+ journal.record(`room discarded ${locator}`);
2257
+ publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
2258
+ moveTo({ ...status, ...attention() });
2259
+ return true;
2260
+ });
2261
+ if (!discarded)
2262
+ return;
2263
+ const progress = { complete: true, reachable: true };
2264
+ await roomTask(locator, async (live) => {
2265
+ await Promise.race([
2266
+ reload(room, local, progress, () => live() && !signal.aborted),
2267
+ abandonedOn(signal),
2268
+ ]);
2269
+ });
2270
+ if (signal.aborted)
2271
+ return;
2272
+ await rest(progress);
1592
2273
  }
1593
2274
  function awaitingAnswer(locator) {
1594
2275
  let release = () => { };
@@ -1668,6 +2349,11 @@ export function composeRuntime(options) {
1668
2349
  });
1669
2350
  }
1670
2351
  async function scoped(result, room, local) {
2352
+ if ((result.scope === 'content' || result.scope === 'sync') && !result.retryable) {
2353
+ journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
2354
+ await loseRooms([room.locator]);
2355
+ return;
2356
+ }
1671
2357
  scopedRefusals.set(room.locator, {
1672
2358
  code: result.code,
1673
2359
  fields: result.fields,
@@ -1714,15 +2400,16 @@ export function composeRuntime(options) {
1714
2400
  });
1715
2401
  }
1716
2402
  async function synchronize(listed, local, progress, attempt, live) {
1717
- const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
2403
+ const acquired = listed.filter(acquires);
2404
+ const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
1718
2405
  if (!live())
1719
2406
  return;
1720
- const ready = listed.filter((_, index) => {
2407
+ const ready = acquired.filter((_, index) => {
1721
2408
  const outcome = prepared[index];
1722
2409
  return outcome?.status === 'fulfilled' && outcome.value;
1723
2410
  });
1724
2411
  if (attempt === 'opened') {
1725
- options.adapter.roomStream?.hold(heldRooms(ready));
2412
+ options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
1726
2413
  handed = true;
1727
2414
  }
1728
2415
  discovering?.release();
@@ -1750,19 +2437,10 @@ export function composeRuntime(options) {
1750
2437
  await retainModel(local, room, 'collection', modelOf(room.locator));
1751
2438
  if (discoveryPaused || authorization === undefined)
1752
2439
  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;
2440
+ return room.replica.admission === 'admitted';
1759
2441
  }
1760
2442
  async function caughtUp(room, local, progress, live) {
1761
- const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1762
- if (held === undefined || authorization === undefined || !live())
1763
- return;
1764
- const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1765
- if (!behind)
2443
+ if (authorization === undefined || !live() || !behind(room))
1766
2444
  return;
1767
2445
  const withheld = await advance(room, local, authorization, live);
1768
2446
  if (withheld !== null)
@@ -1789,17 +2467,22 @@ export function composeRuntime(options) {
1789
2467
  await caughtUp(room, local, progress, live);
1790
2468
  }
1791
2469
  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;
2470
+ try {
2471
+ if (!live() || authorization === undefined)
2472
+ return;
2473
+ const bootstrapped = await pullBootstrap(authorization, room, local, live);
2474
+ if (!live())
2475
+ return;
2476
+ if (bootstrapped.kind !== 'staged') {
2477
+ await withhold(bootstrapped, room, local, progress);
2478
+ return;
2479
+ }
2480
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
2481
+ await caughtUp(room, local, progress, live);
2482
+ }
2483
+ finally {
2484
+ caughtUpTo(room.locator);
1800
2485
  }
1801
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
1802
- await caughtUp(room, local, progress, live);
1803
2486
  }
1804
2487
  async function discoverRooms(local, progress, attempt, live) {
1805
2488
  if (closed || authorization === undefined) {
@@ -1875,6 +2558,7 @@ export function composeRuntime(options) {
1875
2558
  }
1876
2559
  const previousCapabilities = capabilities;
1877
2560
  capabilities = result.capabilities;
2561
+ outcomeRetention = result.retention.outcome;
1878
2562
  notifyReady();
1879
2563
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1880
2564
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
@@ -1984,9 +2668,13 @@ export function composeRuntime(options) {
1984
2668
  options.adapter.roomStream?.reopen();
1985
2669
  });
1986
2670
  }
1987
- function closedSocket(revoked) {
2671
+ function closedSocket(revoked, lost) {
1988
2672
  const opened = socketOpen;
1989
2673
  socketOpen = false;
2674
+ if (lost) {
2675
+ reads.lost();
2676
+ conversations.lost();
2677
+ }
1990
2678
  if (revoked) {
1991
2679
  void refuse('revoked-authorization');
1992
2680
  return;
@@ -2004,6 +2692,8 @@ export function composeRuntime(options) {
2004
2692
  const progress = underway?.generation === generation ? underway.progress : null;
2005
2693
  const cycle = progress ?? { complete: true, reachable: true };
2006
2694
  const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
2695
+ if (opened === generation)
2696
+ opened = null;
2007
2697
  return task(OPENING_LANE, async (live) => {
2008
2698
  underway = null;
2009
2699
  if (!live())
@@ -2016,8 +2706,6 @@ export function composeRuntime(options) {
2016
2706
  await roomsCarried(expired);
2017
2707
  if (!live())
2018
2708
  return;
2019
- if (opened === generation)
2020
- opened = null;
2021
2709
  const replaced = new Set(owed);
2022
2710
  owed.clear();
2023
2711
  try {
@@ -2025,6 +2713,9 @@ export function composeRuntime(options) {
2025
2713
  const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
2026
2714
  const reloading = held.filter((room) => !expired.includes(room.locator) &&
2027
2715
  (replaced.has(room.locator) || revoked(room.locator)));
2716
+ for (const room of reloading)
2717
+ catching.add(room.locator);
2718
+ readSide.recover();
2028
2719
  const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
2029
2720
  const reloaded = [...(await reloads), ...(await replacing)];
2030
2721
  if (progress.reachable) {
@@ -2069,38 +2760,94 @@ export function composeRuntime(options) {
2069
2760
  function submitLater(locator) {
2070
2761
  if (localSaves)
2071
2762
  return;
2072
- const running = submitting.get(locator);
2073
- if (running !== undefined) {
2074
- running.again = true;
2763
+ void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
2764
+ }
2765
+ async function submitPass(locator, signal) {
2766
+ const room = discovered.get(locator);
2767
+ if (closed || stopping || discoveryPaused || room === undefined)
2768
+ return 'done';
2769
+ const progress = { complete: true, reachable: true };
2770
+ let next;
2771
+ try {
2772
+ await ensureAuthorization();
2773
+ if (authorization === undefined)
2774
+ return 'done';
2775
+ next = await submitRoom(room, await openLocal(), authorization, progress, signal);
2776
+ }
2777
+ catch {
2778
+ progress.reachable = false;
2779
+ next = 'again';
2780
+ }
2781
+ if (signal.aborted)
2782
+ return 'done';
2783
+ await rest(progress);
2784
+ return next === 'again' && !socketOpen ? 'done' : next;
2785
+ }
2786
+ function repairLater(locator, when) {
2787
+ if (!socketOpen || repairing.has(locator))
2075
2788
  return;
2789
+ void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
2790
+ }
2791
+ async function repairPass(locator, signal) {
2792
+ const room = discovered.get(locator);
2793
+ if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
2794
+ return 'done';
2795
+ if (room === undefined || room.replica.admission !== 'admitted')
2796
+ return 'done';
2797
+ const progress = { complete: true, reachable: true };
2798
+ repairing.add(locator);
2799
+ try {
2800
+ const local = await openLocal();
2801
+ await roomTask(locator, async (live) => {
2802
+ const current = () => live() && !signal.aborted;
2803
+ if (!current() || authorization === undefined)
2804
+ return;
2805
+ const withheld = await Promise.race([
2806
+ advance(room, local, authorization, current),
2807
+ abandonedOn(signal),
2808
+ ]);
2809
+ if (withheld !== null && current())
2810
+ await withhold(withheld, room, local, progress);
2811
+ });
2076
2812
  }
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
- })();
2813
+ catch {
2814
+ stale.add(locator);
2815
+ progress.reachable = false;
2816
+ }
2817
+ finally {
2818
+ repairing.delete(locator);
2819
+ }
2820
+ if (signal.aborted)
2821
+ return 'done';
2822
+ await rest(progress);
2823
+ if (!stale.has(locator)) {
2824
+ if ((replicas.get(locator)?.queue.length ?? 0) > 0)
2825
+ submitLater(locator);
2826
+ return 'done';
2827
+ }
2828
+ if (!repairable.has(locator))
2829
+ return 'done';
2830
+ const reset = repairable.get(locator);
2831
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
2832
+ }
2833
+ function resendLater() {
2834
+ if (!socketOpen)
2835
+ return;
2836
+ void recovery.attempt('resend', resendPass, { again: true });
2837
+ }
2838
+ async function resendPass(signal) {
2839
+ if (!socketOpen || closed || stopping || discoveryPaused)
2840
+ return 'done';
2841
+ const reading = { complete: true, reachable: true };
2842
+ try {
2843
+ await conversations.resend(reading);
2844
+ }
2845
+ catch {
2846
+ reading.reachable = false;
2847
+ }
2848
+ if (signal.aborted)
2849
+ return 'done';
2850
+ return reading.reachable && reading.complete ? 'done' : 'again';
2104
2851
  }
2105
2852
  function submitAll() {
2106
2853
  for (const [locator, replica] of replicas) {
@@ -2114,16 +2861,23 @@ export function composeRuntime(options) {
2114
2861
  progress.complete = false;
2115
2862
  return;
2116
2863
  }
2864
+ reads.applied();
2117
2865
  submitAll();
2866
+ for (const locator of stale)
2867
+ if (repairable.has(locator))
2868
+ repairLater(locator, {});
2118
2869
  await conversations.reconcile();
2119
2870
  if (status.providerUnreachable)
2120
2871
  conversations.reconnected();
2121
2872
  const reading = { complete: true, reachable: true };
2122
- await conversations.catchUp(reading);
2873
+ if (demand.conversations)
2874
+ await conversations.catchUp(reading);
2123
2875
  if (progress.reachable && reading.reachable)
2124
2876
  await conversations.resend(reading);
2125
- if (!reading.reachable)
2877
+ if (!reading.reachable) {
2126
2878
  progress.reachable = false;
2879
+ resendLater();
2880
+ }
2127
2881
  }
2128
2882
  catch {
2129
2883
  progress.complete = false;
@@ -2180,7 +2934,7 @@ export function composeRuntime(options) {
2180
2934
  return;
2181
2935
  const room = discovered.get(locator);
2182
2936
  const held = replicas.get(locator);
2183
- if (room === undefined || room.replica.admission !== 'admitted')
2937
+ if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
2184
2938
  return;
2185
2939
  if (held !== undefined &&
2186
2940
  (held.confirmed.record.replica.selection !== room.replica.selection ||
@@ -2232,10 +2986,13 @@ export function composeRuntime(options) {
2232
2986
  if (event.kind === 'opened') {
2233
2987
  socketOpen = true;
2234
2988
  generation += 1;
2989
+ reads.opened();
2990
+ conversations.reopened();
2991
+ recovery.resume();
2235
2992
  void sync('opened');
2236
2993
  }
2237
2994
  else if (event.kind === 'closed')
2238
- closedSocket(event.revoked);
2995
+ closedSocket(event.revoked, event.lost);
2239
2996
  else if (event.kind === 'opening')
2240
2997
  void applyOpening(event.expired);
2241
2998
  else
@@ -2255,6 +3012,7 @@ export function composeRuntime(options) {
2255
3012
  haltedRefusal = false;
2256
3013
  if (mode !== serviceMode) {
2257
3014
  serviceMode = mode;
3015
+ queueReadiness();
2258
3016
  journal.record(`service mode ${mode}`);
2259
3017
  for (const listener of serviceModeListeners)
2260
3018
  listener(mode);
@@ -2267,6 +3025,8 @@ export function composeRuntime(options) {
2267
3025
  if (stream === undefined || withdrawConversations !== undefined)
2268
3026
  return;
2269
3027
  withdrawConversations = stream.observe((event) => {
3028
+ if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
3029
+ queueReadiness();
2270
3030
  if (conversations.receive(event)) {
2271
3031
  void ready.then(scheduleCurrency);
2272
3032
  return;
@@ -2289,7 +3049,7 @@ export function composeRuntime(options) {
2289
3049
  if (!live())
2290
3050
  return;
2291
3051
  await conversations.reconcile();
2292
- if (conversations.current()) {
3052
+ if (conversations.current() || !demand.conversations) {
2293
3053
  show();
2294
3054
  return;
2295
3055
  }
@@ -2307,13 +3067,18 @@ export function composeRuntime(options) {
2307
3067
  if (closed)
2308
3068
  return;
2309
3069
  closed = true;
3070
+ if (narrowing !== null)
3071
+ clearTimeout(narrowing);
3072
+ if (rechecking !== null)
3073
+ clearInterval(rechecking);
3074
+ readinessWatches.clear();
2310
3075
  withdrawStream?.();
2311
3076
  withdrawStream = undefined;
2312
3077
  withdrawConversations?.();
2313
3078
  withdrawConversations = undefined;
2314
3079
  withdrawServiceMode?.();
2315
3080
  conversations.close();
2316
- stopTimer();
3081
+ void recovery.retire();
2317
3082
  store?.close();
2318
3083
  void opening?.then((opened) => {
2319
3084
  opened.close();
@@ -2331,9 +3096,8 @@ export function composeRuntime(options) {
2331
3096
  }
2332
3097
  async function stop() {
2333
3098
  stopping = true;
2334
- stopTimer();
3099
+ await recovery.retire();
2335
3100
  await Promise.all(working);
2336
- await Promise.all([...submitting.values()].map((run) => run.done));
2337
3101
  await mutations;
2338
3102
  close();
2339
3103
  }
@@ -2355,17 +3119,101 @@ export function composeRuntime(options) {
2355
3119
  return planOf(input, roomFacts(reach), planReach(reach));
2356
3120
  }
2357
3121
  function watchPlan(plan, deliver) {
2358
- materialize(reachedRooms(plan));
2359
- return readSide.watch(plan, identityOf(plan), deliver);
3122
+ let seeded = false;
3123
+ let pending = true;
3124
+ const shown = () => {
3125
+ const seed = seeds.get(plan.key);
3126
+ if (seed === undefined || seeded || !pending)
3127
+ return;
3128
+ seeded = true;
3129
+ deliver(seededResult(seed));
3130
+ };
3131
+ const watchers = seedWatchers.get(plan.key) ?? new Set();
3132
+ seedWatchers.set(plan.key, watchers);
3133
+ watchers.add(shown);
3134
+ const stop = readSide.watch(plan, identityOf(plan), () => {
3135
+ const cover = covered(plan);
3136
+ awaitPreparation(cover);
3137
+ return seedBehind(plan, cover);
3138
+ }, (message) => {
3139
+ const answered = message;
3140
+ pending = whole(answered) && 'pending' in answered.result;
3141
+ if (pending && seeds.has(plan.key)) {
3142
+ shown();
3143
+ return;
3144
+ }
3145
+ seeded = false;
3146
+ deliver(message);
3147
+ });
3148
+ return () => {
3149
+ watchers.delete(shown);
3150
+ if (watchers.size === 0)
3151
+ seedWatchers.delete(plan.key);
3152
+ stop();
3153
+ };
2360
3154
  }
2361
3155
  async function searched(input, reach) {
2362
3156
  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);
3157
+ const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
3158
+ for (;;) {
3159
+ const cover = covered(filter);
3160
+ awaitPreparation(cover);
3161
+ const pending = 'pending' in cover ? [...cover.pending] : [];
3162
+ const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3163
+ const held = covered({
3164
+ ...filter,
3165
+ rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
3166
+ });
3167
+ const covering = 'pending' in held ? [] : held.coverage.rooms;
3168
+ const unindexed = covering
3169
+ .filter(({ room }) => !searchIndex.rooms().has(room))
3170
+ .map(({ room }) => room);
3171
+ for (const room of unindexed)
3172
+ pending.push({ room, awaits: 'index' });
3173
+ const listed = conversations.searchable();
3174
+ const named = reach !== null && !reach.conversations
3175
+ ? []
3176
+ : (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
3177
+ const waiting = named.filter((one) => !searchIndex.conversations().has(one));
3178
+ for (const conversation of waiting)
3179
+ pending.push({ conversation, awaits: 'index' });
3180
+ if (unindexed.length > 0 || waiting.length > 0)
3181
+ index(unindexed, waiting);
3182
+ const complete = pending.length === 0 && !('pending' in cover);
3183
+ const answering = complete
3184
+ ? filter
3185
+ : {
3186
+ ...filter,
3187
+ rooms: covering
3188
+ .filter(({ room }) => searchIndex.rooms().has(room))
3189
+ .map(({ room }) => room),
3190
+ };
3191
+ const answered = covered(answering);
3192
+ if ('pending' in answered)
3193
+ continue;
3194
+ const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
3195
+ const seen = searchIndex.changes();
3196
+ await searchIndex.caughtUp();
3197
+ const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
3198
+ if (selected !== null && 'pending' in selected)
3199
+ continue;
3200
+ if (searchIndex.changes() !== seen ||
3201
+ JSON.stringify(covered(answering)) !== JSON.stringify(answered))
3202
+ continue;
3203
+ const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms === null ? null : new Set(answering.rooms), plan.collection
3204
+ ? new Map(answered.coverage.rooms.map(({ room }) => [
3205
+ room,
3206
+ replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
3207
+ ]))
3208
+ : null);
3209
+ const coverage = {
3210
+ rooms: answered.coverage.rooms,
3211
+ conversations: searchedConversations,
3212
+ index: searchIndex.generation(),
3213
+ overlay: answered.coverage.overlay,
3214
+ };
3215
+ return complete ? { ...hits, coverage } : { pending, ...hits, coverage };
3216
+ }
2369
3217
  }
2370
3218
  const watch = (topic, input, deliver) => {
2371
3219
  if (topic === 'room-drafts') {
@@ -2411,6 +3259,9 @@ export function composeRuntime(options) {
2411
3259
  presentationListeners.delete(listener);
2412
3260
  };
2413
3261
  }
3262
+ if (topic === 'readiness') {
3263
+ return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
3264
+ }
2414
3265
  if (topic === 'rooms') {
2415
3266
  roomListeners.add(deliver);
2416
3267
  deliver(listed());
@@ -2577,7 +3428,13 @@ export function composeRuntime(options) {
2577
3428
  return journal.export();
2578
3429
  },
2579
3430
  persistence() {
2580
- return options.shell.persistentStorage;
3431
+ return persistence === 'granted';
3432
+ },
3433
+ async retry() {
3434
+ journal.record('retry');
3435
+ unindexable.clear();
3436
+ queueReadiness();
3437
+ resume();
2581
3438
  },
2582
3439
  observe: observing(watch),
2583
3440
  async reauthorize() {
@@ -2604,6 +3461,22 @@ export function composeRuntime(options) {
2604
3461
  query: (plan) => rowsQuery(plan),
2605
3462
  watchPlan,
2606
3463
  search: searched,
3464
+ watchReadiness,
3465
+ };
3466
+ return {
3467
+ md,
3468
+ seed,
3469
+ stop,
3470
+ lose: loseRooms,
3471
+ watch,
3472
+ acquire,
3473
+ persisted(answer) {
3474
+ if (answer === persistence)
3475
+ return;
3476
+ persistence = answer;
3477
+ journal.record(`persistence ${answer}`);
3478
+ queueReadiness();
3479
+ },
3480
+ within: (reach, port) => narrowed(reachable, reach, port),
2607
3481
  };
2608
- return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
2609
3482
  }