@agentshouse/mdruntime 0.1.0-c56daf494ecb4 → 0.1.0-c5d09b1c896fb

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 (126) hide show
  1. package/AGENTS.md +261 -261
  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 +37 -9
  6. package/dist/adapter.d.ts.map +1 -1
  7. package/dist/bootstrap.d.ts +2 -2
  8. package/dist/bootstrap.d.ts.map +1 -1
  9. package/dist/bootstrap.js +26 -4
  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/acts.d.ts +3 -4
  14. package/dist/chat/acts.d.ts.map +1 -1
  15. package/dist/chat/acts.js +10 -10
  16. package/dist/chat/kit.d.ts +2 -1
  17. package/dist/chat/kit.d.ts.map +1 -1
  18. package/dist/chat/kit.js +3 -3
  19. package/dist/chat/list.d.ts +2 -1
  20. package/dist/chat/list.d.ts.map +1 -1
  21. package/dist/chat/list.js +4 -3
  22. package/dist/chat/message.d.ts.map +1 -1
  23. package/dist/chat/message.js +2 -2
  24. package/dist/chat/messages.d.ts +1 -0
  25. package/dist/chat/messages.d.ts.map +1 -1
  26. package/dist/chat/messages.js +4 -0
  27. package/dist/chat/provider.d.ts +1 -1
  28. package/dist/chat/provider.d.ts.map +1 -1
  29. package/dist/chat/provider.js +51 -67
  30. package/dist/chat/ready.d.ts +1 -0
  31. package/dist/chat/ready.d.ts.map +1 -1
  32. package/dist/chat/ready.js +2 -2
  33. package/dist/client.d.ts +12 -8
  34. package/dist/client.d.ts.map +1 -1
  35. package/dist/client.js +3 -1
  36. package/dist/collection.d.ts +17 -3
  37. package/dist/collection.d.ts.map +1 -1
  38. package/dist/collection.js +230 -19
  39. package/dist/connection.d.ts +22 -7
  40. package/dist/connection.d.ts.map +1 -1
  41. package/dist/connection.js +1253 -309
  42. package/dist/conversation.d.ts +14 -7
  43. package/dist/conversation.d.ts.map +1 -1
  44. package/dist/conversation.js +328 -137
  45. package/dist/durability.d.ts +7 -1
  46. package/dist/durability.d.ts.map +1 -1
  47. package/dist/entry.d.ts +2 -2
  48. package/dist/entry.d.ts.map +1 -1
  49. package/dist/entry.js +3 -3
  50. package/dist/fence.d.ts +6 -0
  51. package/dist/fence.d.ts.map +1 -0
  52. package/dist/fence.js +10 -0
  53. package/dist/host.d.ts +13 -2
  54. package/dist/host.d.ts.map +1 -1
  55. package/dist/host.js +49 -7
  56. package/dist/identity.d.ts +2 -0
  57. package/dist/identity.d.ts.map +1 -1
  58. package/dist/identity.js +6 -0
  59. package/dist/index.d.ts +9 -6
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +2 -0
  62. package/dist/memory.d.ts +4 -1
  63. package/dist/memory.d.ts.map +1 -1
  64. package/dist/memory.js +23 -2
  65. package/dist/model.d.ts +4 -2
  66. package/dist/model.d.ts.map +1 -1
  67. package/dist/model.js +10 -1
  68. package/dist/observation.d.ts +5 -2
  69. package/dist/observation.d.ts.map +1 -1
  70. package/dist/observation.js +22 -11
  71. package/dist/outcome.d.ts +4 -0
  72. package/dist/outcome.d.ts.map +1 -1
  73. package/dist/outcome.js +1 -0
  74. package/dist/overlay.d.ts +2 -0
  75. package/dist/overlay.d.ts.map +1 -1
  76. package/dist/overlay.js +58 -55
  77. package/dist/query.d.ts +32 -3
  78. package/dist/query.d.ts.map +1 -1
  79. package/dist/query.js +3 -1
  80. package/dist/reach.d.ts +6 -3
  81. package/dist/reach.d.ts.map +1 -1
  82. package/dist/reach.js +17 -5
  83. package/dist/react.d.ts +7 -4
  84. package/dist/react.d.ts.map +1 -1
  85. package/dist/react.js +6 -4
  86. package/dist/read-side.d.ts +13 -6
  87. package/dist/read-side.d.ts.map +1 -1
  88. package/dist/read-side.js +285 -83
  89. package/dist/readiness.d.ts +53 -0
  90. package/dist/readiness.d.ts.map +1 -0
  91. package/dist/readiness.js +33 -0
  92. package/dist/recovery.d.ts +25 -0
  93. package/dist/recovery.d.ts.map +1 -0
  94. package/dist/recovery.js +197 -0
  95. package/dist/refresh.d.ts.map +1 -1
  96. package/dist/refresh.js +17 -10
  97. package/dist/replica.d.ts +1 -1
  98. package/dist/replica.d.ts.map +1 -1
  99. package/dist/replica.js +57 -20
  100. package/dist/room-draft.d.ts +13 -1
  101. package/dist/room-draft.d.ts.map +1 -1
  102. package/dist/room-draft.js +2 -2
  103. package/dist/rows.d.ts +5 -13
  104. package/dist/rows.d.ts.map +1 -1
  105. package/dist/rows.js +5 -13
  106. package/dist/save.d.ts +5 -0
  107. package/dist/save.d.ts.map +1 -1
  108. package/dist/save.js +36 -13
  109. package/dist/search.d.ts +34 -6
  110. package/dist/search.d.ts.map +1 -1
  111. package/dist/search.js +116 -45
  112. package/dist/shared.d.ts +11 -0
  113. package/dist/shared.d.ts.map +1 -0
  114. package/dist/shared.js +368 -0
  115. package/dist/status.d.ts +4 -0
  116. package/dist/status.d.ts.map +1 -1
  117. package/dist/store.d.ts +42 -11
  118. package/dist/store.d.ts.map +1 -1
  119. package/dist/store.js +276 -83
  120. package/dist/turn.d.ts +7 -0
  121. package/dist/turn.d.ts.map +1 -0
  122. package/dist/turn.js +45 -0
  123. package/dist/wire.d.ts +136 -32
  124. package/dist/wire.d.ts.map +1 -1
  125. package/dist/wire.js +48 -31
  126. package/package.json +2 -2
@@ -6,21 +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
- import { absentBinary, binaryAnswer, calledResult, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
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';
23
- import { roomDraftRevision } from './identity.js';
24
+ import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
25
+ import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
26
+ import { FencedMap } from './fence.js';
27
+ import { tally } from './shared.js';
28
+ import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
29
+ import { identityTime, roomDraftRevision } from './identity.js';
24
30
  import { beneath, normalize } from './paths.js';
25
31
  import { openRuntimeStore, } from './store.js';
26
32
  import { unconfirmed } from './transfer.js';
@@ -38,23 +44,59 @@ function declaredSelection(room) {
38
44
  root !== '' &&
39
45
  selection.roots.every((other, at) => at === index || !within(other, root)));
40
46
  }
47
+ const OPENING_LANE = 'opening';
48
+ const CONVERSATION_LANE = 'conversations';
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
+ }
73
+ function roomLane(locator) {
74
+ return `room ${locator}`;
75
+ }
41
76
  export function composeRuntime(options) {
42
77
  const localSaves = options.adapter.localSaves === true;
43
78
  let authorization = options.authorization;
44
79
  let opening;
45
80
  let store;
46
- const replicas = new Map();
47
- const queuesWithoutReplica = new Map();
81
+ const fences = new Map();
82
+ const isFenced = (locator) => fences.has(locator);
83
+ let fencings = 0;
84
+ const lastFenced = new Map();
85
+ const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
86
+ let discoveries = 0;
87
+ const replicas = new FencedMap(isFenced);
88
+ const queuesWithoutReplica = new FencedMap(isFenced);
48
89
  const pendings = new Map();
49
- const roomDrafts = new Map();
90
+ const roomDrafts = new FencedMap(isFenced);
50
91
  const presentations = new Map();
51
92
  let limits = [];
52
93
  let capabilities = [];
53
- const rooms = new Map();
94
+ const rooms = new FencedMap(isFenced);
54
95
  const counted = new Map();
55
96
  const unreadable = new Set();
56
- const models = new Map();
57
- const liveModels = new Map();
97
+ const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
98
+ const models = new FencedMap(isModelFenced);
99
+ const liveModels = new FencedMap(isModelFenced);
58
100
  const invalidModels = new Set();
59
101
  const discovered = new Map();
60
102
  let roomsListed = false;
@@ -84,7 +126,6 @@ export function composeRuntime(options) {
84
126
  let status = STARTING;
85
127
  let shown = STARTING;
86
128
  let discoveryPaused = false;
87
- let resetTimer;
88
129
  let closed = false;
89
130
  let stopping = false;
90
131
  let withdrawStream;
@@ -92,12 +133,21 @@ export function composeRuntime(options) {
92
133
  let underway = null;
93
134
  let streamed = null;
94
135
  let handed = false;
95
- 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;
96
140
  const answering = new Map();
97
141
  let complete = false;
98
142
  let recorded = false;
99
143
  const stale = new Set();
100
- let chain = Promise.resolve();
144
+ let generation = 0;
145
+ let discovering = null;
146
+ let opened = null;
147
+ const owed = new Set();
148
+ const lanes = new Map();
149
+ let roomReads = { generation, free: ROOM_READS, waiting: [] };
150
+ const working = new Set();
101
151
  let mutations = Promise.resolve();
102
152
  const roomListeners = new Set();
103
153
  const refusalListeners = new Set();
@@ -108,7 +158,7 @@ export function composeRuntime(options) {
108
158
  const presentationListeners = new Set();
109
159
  const readSide = createReadSide();
110
160
  let withdrawConversations;
111
- let currencyQueued = false;
161
+ let currencyQueued = null;
112
162
  let socketOpen = false;
113
163
  let serviceMode = 'open';
114
164
  let haltedRefusal = false;
@@ -116,24 +166,41 @@ export function composeRuntime(options) {
116
166
  const serviceModeListeners = new Set();
117
167
  let member = null;
118
168
  const memberListeners = new Set();
119
- const materialized = new Set();
120
- let everyRoom = false;
121
- const transfers = new Map();
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();
184
+ const transfers = new FencedMap(isFenced);
122
185
  const conversations = createConversationStore({
123
186
  agents: declaredAgents,
124
- documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
187
+ documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
125
188
  call: (name, input) => invokeAction(name, input),
126
189
  durably,
127
190
  stream: () => options.adapter.conversations,
128
191
  publish: publishChangeSet,
192
+ online: () => !status.providerUnreachable && refusal === null,
129
193
  closed: () => closed,
194
+ unanswered: resendLater,
195
+ expired: (operation) => pastHorizon(operation),
130
196
  });
131
197
  const searchIndex = createSearchIndex({
132
- documents: () => readSide.documents(),
198
+ room: (room) => readSide.room(room),
133
199
  document: (room, path) => readSide.document(room, path),
134
- messages: () => conversations.messages(),
135
- message: (conversation, identity) => conversations.message(conversation, identity),
136
200
  });
201
+ const roomsToIndex = new Set();
202
+ const conversationsToIndex = new Set();
203
+ let indexing = false;
137
204
  const journal = createLog();
138
205
  let notifyReady = () => { };
139
206
  const ready = new Promise((resolve) => {
@@ -212,9 +279,16 @@ export function composeRuntime(options) {
212
279
  returned: structuredClone(record.returned),
213
280
  });
214
281
  }
282
+ for (const [room, refusal] of sourceRefusals)
283
+ attentionRequired.push({ room, refusal: structuredClone(refusal) });
215
284
  attentionRequired.sort((left, right) => {
216
285
  if (left.room !== right.room)
217
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
+ }
218
292
  if (left.path !== right.path)
219
293
  return left.path < right.path ? -1 : 1;
220
294
  return 0;
@@ -233,11 +307,9 @@ export function composeRuntime(options) {
233
307
  if (replica === undefined)
234
308
  return 0;
235
309
  const roots = replica.confirmed.record.typedSelection?.roots ?? [];
236
- const paths = [
237
- ...Object.keys(replica.visible.documents).filter(markdownPath),
238
- ...Object.keys(replica.visible.binaries),
239
- ];
240
- 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));
241
313
  }
242
314
  function listed() {
243
315
  return structuredClone([...rooms.values()].map((room) => {
@@ -264,15 +336,19 @@ export function composeRuntime(options) {
264
336
  return { listed: roomsListed, rooms: held };
265
337
  }
266
338
  function emitRooms() {
339
+ readSide.recover();
340
+ queueReadiness();
267
341
  const value = listed();
268
342
  for (const listener of roomListeners)
269
343
  listener(value);
270
344
  }
271
345
  function emitRefusal() {
346
+ queueReadiness();
272
347
  for (const listener of refusalListeners)
273
348
  listener(refusal);
274
349
  }
275
350
  function emitStorageLoss() {
351
+ queueReadiness();
276
352
  for (const listener of storageLossListeners)
277
353
  listener(storageLoss);
278
354
  }
@@ -289,7 +365,8 @@ export function composeRuntime(options) {
289
365
  function show() {
290
366
  if (closed)
291
367
  return;
292
- const next = status.state === 'synced' && !conversations.current()
368
+ queueReadiness();
369
+ const next = status.state === 'synced' && demand.conversations && !conversations.current()
293
370
  ? { ...status, state: 'syncing' }
294
371
  : status;
295
372
  if (same(shown, next))
@@ -318,42 +395,472 @@ export function composeRuntime(options) {
318
395
  }
319
396
  }
320
397
  function applyChangeSet(set) {
321
- const held = everyRoom
322
- ? set
323
- : { ...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
+ };
324
402
  readSide.apply(held);
325
403
  searchIndex.changed(held);
326
404
  }
327
- function materialize(locators) {
328
- 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))
329
421
  return;
330
- if (locators === null)
331
- everyRoom = true;
332
- const sets = [];
333
- for (const locator of locators ?? [...replicas.keys()]) {
334
- if (materialized.has(locator))
335
- continue;
336
- materialized.add(locator);
337
- const replica = replicas.get(locator);
338
- if (replica !== undefined)
339
- 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);
340
440
  }
341
- if (sets.length === 0)
441
+ finally {
442
+ projecting.delete(locator);
443
+ }
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)
342
460
  return;
343
- const set = mergeChangeSets(sets);
344
- readSide.apply(set);
345
- 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
+ })();
346
476
  }
347
477
  function reachedRooms(plan) {
348
- if (plan.rooms !== null)
349
- return plan.rooms;
350
- 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)
351
540
  return null;
352
- 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 search = port.search === 'required';
647
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
648
+ const building = (state) => coverage
649
+ .filter((room) => room.state === state && !unindexable.has(room.room))
650
+ .map(({ room }) => room);
651
+ const preparing = building('preparing');
652
+ if (preparing.length > 0)
653
+ prepare(preparing);
654
+ const indexing = building('indexing');
655
+ if (indexing.length > 0)
656
+ index(indexing, []);
657
+ return readinessOf({
658
+ code: port.code,
659
+ coverage: {
660
+ listed: required.listed,
661
+ rooms: coverage,
662
+ conversations: conversationCoverage(reach, search),
663
+ search: search ? 'required' : 'unused',
664
+ },
665
+ persistence,
666
+ connection: connectionOf(),
667
+ house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
668
+ capacity: capacity !== null &&
669
+ coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
670
+ ? capacity
671
+ : port.capacity,
672
+ refused: refusedIn(required.rooms),
673
+ lost: storageLoss !== null,
674
+ established: targets[port.target]?.established === true,
675
+ });
676
+ }
677
+ function establish(name) {
678
+ targets = { ...targets, [name]: { established: true } };
679
+ queueReadiness();
680
+ void durably((local) => local.keepTargets(targets)).catch(() => {
681
+ journal.record(`target record failed ${name}`);
682
+ });
683
+ }
684
+ function deliverReadiness(watch) {
685
+ if (!readinessWatches.has(watch))
686
+ return;
687
+ const readiness = readinessFor(watch.port, watch.reach);
688
+ if (readiness.ready && targets[watch.port.target]?.established !== true) {
689
+ establish(watch.port.target);
690
+ }
691
+ const shown = JSON.stringify(readiness);
692
+ if (shown === watch.last)
693
+ return;
694
+ watch.last = shown;
695
+ watch.deliver(structuredClone(readiness));
696
+ }
697
+ function queueReadiness() {
698
+ if (readinessQueued || closed || readinessWatches.size === 0)
699
+ return;
700
+ readinessQueued = true;
701
+ queueMicrotask(() => {
702
+ readinessQueued = false;
703
+ if (closed)
704
+ return;
705
+ for (const watch of [...readinessWatches])
706
+ deliverReadiness(watch);
707
+ });
708
+ }
709
+ function watchReadiness(port, reach, deliver) {
710
+ const watch = { port, reach, deliver, last: null };
711
+ readinessWatches.add(watch);
712
+ void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
713
+ return () => {
714
+ readinessWatches.delete(watch);
715
+ };
353
716
  }
354
- function rowsQuery(plan) {
355
- materialize(reachedRooms(plan));
356
- return readSide.query(plan);
717
+ function layerBytes(layer) {
718
+ let bytes = 0;
719
+ for (const [path, text] of Object.entries(layer.documents))
720
+ bytes += (path.length + text.length) * 2;
721
+ return bytes;
722
+ }
723
+ function grownBy(next, previous) {
724
+ if (previous === undefined)
725
+ return layerBytes(next);
726
+ const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
727
+ return layerBytes({ ...next, documents });
728
+ }
729
+ function exceeded(error) {
730
+ return (typeof error === 'object' &&
731
+ error !== null &&
732
+ error.name === 'QuotaExceededError');
733
+ }
734
+ async function estimated() {
735
+ const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
736
+ return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
737
+ }
738
+ function pause(locator, cause) {
739
+ paused.add(locator);
740
+ capacity = cause;
741
+ journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
742
+ queueReadiness();
743
+ if (rechecking !== null)
744
+ return;
745
+ // A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
746
+ rechecking = setInterval(() => {
747
+ void recheck();
748
+ }, 60_000);
749
+ }
750
+ async function recheck() {
751
+ const held = capacity;
752
+ if (held === null || closed)
753
+ return;
754
+ const available = await estimated().catch(() => null);
755
+ if (available === null || available < (held.needed ?? 0))
756
+ return;
757
+ if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
758
+ return;
759
+ resume();
760
+ }
761
+ function resume() {
762
+ if (rechecking !== null)
763
+ clearInterval(rechecking);
764
+ rechecking = null;
765
+ if (paused.size === 0 && capacity === null)
766
+ return;
767
+ paused.clear();
768
+ capacity = null;
769
+ journal.record('capacity resumed');
770
+ queueReadiness();
771
+ if (!closed && authorization !== undefined)
772
+ void sync('unstreamed');
773
+ }
774
+ async function reclaimed(local) {
775
+ const unreferenced = [...transfers.keys()].filter((locator) => {
776
+ const room = discovered.get(locator);
777
+ return room === undefined || !acquires(room);
778
+ });
779
+ for (const locator of unreferenced) {
780
+ await local.abandon(locator);
781
+ transfers.delete(locator);
782
+ }
783
+ if (unreferenced.length > 0)
784
+ journal.record(`capacity reclaimed ${unreferenced.length}`);
785
+ return unreferenced.length > 0;
786
+ }
787
+ async function admitted(local, locator, needed) {
788
+ if (paused.has(locator))
789
+ return false;
790
+ let available = await estimated().catch(() => null);
791
+ if (available === null || available >= needed)
792
+ return true;
793
+ if (await reclaimed(local))
794
+ available = await estimated().catch(() => null);
795
+ if (available === null || available >= needed)
796
+ return true;
797
+ pause(locator, { cause: 'insufficient-space', needed, available });
798
+ return false;
799
+ }
800
+ async function exceededOn(locator, bytes) {
801
+ const available = await estimated().catch(() => null);
802
+ pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
803
+ }
804
+ async function rowsQuery(plan) {
805
+ const before = covered(plan);
806
+ awaitPreparation(before);
807
+ if ('pending' in before)
808
+ return before;
809
+ const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
810
+ awaitPreparation(cover);
811
+ if ('pending' in cover)
812
+ return cover;
813
+ return 'groups' in rows
814
+ ? { groups: rows.groups, coverage: cover.coverage }
815
+ : { documents: rows.documents, coverage: cover.coverage };
816
+ }
817
+ async function indexConversation(conversation) {
818
+ searchIndex.indexMessages(conversation, [], false);
819
+ let after = null;
820
+ for (;;) {
821
+ const batch = await durably((local) => local.messagesAfter(conversation, after));
822
+ if (closed)
823
+ return;
824
+ searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
825
+ if (batch.length === 0)
826
+ return;
827
+ after = batch[batch.length - 1].identity;
828
+ }
829
+ }
830
+ function indexNext() {
831
+ const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
832
+ if (room !== undefined) {
833
+ roomsToIndex.delete(room);
834
+ return () => prepared.has(room)
835
+ ? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
836
+ : undefined;
837
+ }
838
+ const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
839
+ if (conversation === undefined)
840
+ return null;
841
+ conversationsToIndex.delete(conversation);
842
+ return () => indexConversation(conversation);
843
+ }
844
+ function index(rooms, named) {
845
+ for (const room of rooms)
846
+ roomsToIndex.add(room);
847
+ for (const conversation of named)
848
+ conversationsToIndex.add(conversation);
849
+ if (indexing)
850
+ return;
851
+ indexing = true;
852
+ void (async () => {
853
+ try {
854
+ for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
855
+ await yielded();
856
+ await unit();
857
+ queueReadiness();
858
+ }
859
+ }
860
+ finally {
861
+ indexing = false;
862
+ }
863
+ })().catch(() => { });
357
864
  }
358
865
  function publishChangeSet(set) {
359
866
  applyChangeSet(set);
@@ -370,34 +877,7 @@ export function composeRuntime(options) {
370
877
  emitPresentation(app);
371
878
  conversations.changed(set);
372
879
  }
373
- function stopTimer() {
374
- if (resetTimer === undefined)
375
- return;
376
- clearTimeout(resetTimer);
377
- resetTimer = undefined;
378
- }
379
- function armTimer() {
380
- stopTimer();
381
- if (closed || stopping)
382
- return;
383
- let earliest = null;
384
- for (const { submission } of pendings.values()) {
385
- if (submission.state !== 'reset')
386
- continue;
387
- const at = Date.parse(submission.reset);
388
- if (earliest === null || at < earliest)
389
- earliest = at;
390
- }
391
- if (earliest === null)
392
- return;
393
- // A Save House held back goes again only once the reset its answer named has passed.
394
- resetTimer = setTimeout(() => {
395
- resetTimer = undefined;
396
- submitAll();
397
- }, Math.max(0, earliest - Date.now()));
398
- }
399
880
  async function refuse(reason, provider = null) {
400
- stopTimer();
401
881
  discoveryPaused = true;
402
882
  try {
403
883
  if (reason === 'revoked-authorization') {
@@ -476,14 +956,24 @@ export function composeRuntime(options) {
476
956
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
477
957
  });
478
958
  }
479
- async function applyDiscovery(listed, local) {
959
+ async function applyDiscovery(found, started) {
960
+ for (const room of found) {
961
+ const fence = fences.get(room.locator);
962
+ if (fence !== undefined && fence.erased && fence.at < started)
963
+ fences.delete(room.locator);
964
+ }
965
+ const listed = found.filter((room) => !fences.has(room.locator));
480
966
  const listedLocators = new Set(listed.map((room) => room.locator));
481
967
  for (const locator of listedLocators)
482
968
  scopedRefusals.delete(locator);
483
969
  discovered.clear();
484
970
  for (const room of listed)
485
971
  discovered.set(room.locator, room);
486
- await dropUnlisted(listedLocators, local);
972
+ const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
973
+ if (options.adapter.exhaustiveDiscovery === true)
974
+ await loseRooms(unlisted);
975
+ else
976
+ unlistedKept(unlisted);
487
977
  for (const room of listed) {
488
978
  const held = replicas.get(room.locator)?.confirmed.record;
489
979
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -494,6 +984,7 @@ export function composeRuntime(options) {
494
984
  }
495
985
  roomsListed = true;
496
986
  emitRooms();
987
+ return listed;
497
988
  }
498
989
  function unlistedHeld(listed) {
499
990
  return [
@@ -505,41 +996,73 @@ export function composeRuntime(options) {
505
996
  ]),
506
997
  ].filter((locator) => !listed.has(locator));
507
998
  }
508
- async function dropUnlisted(listed, local) {
509
- if (unlistedHeld(listed).length === 0)
510
- return;
511
- await exclusive(async () => {
512
- const unlisted = unlistedHeld(listed);
513
- await local.dropRooms(unlisted);
514
- const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
515
- room: true,
516
- queue: true,
517
- drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
518
- }));
519
- for (const locator of unlisted) {
520
- for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
521
- pendings.delete(batch.id);
522
- replicas.delete(locator);
523
- queuesWithoutReplica.delete(locator);
999
+ function unlistedKept(unlisted) {
1000
+ for (const locator of unlisted) {
1001
+ const held = replicas.get(locator)?.confirmed.record;
1002
+ if (held !== undefined)
1003
+ list(held, false);
1004
+ else
524
1005
  rooms.delete(locator);
525
- roomDrafts.delete(locator);
526
- transfers.delete(locator);
527
- unreadable.delete(locator);
528
- stale.delete(locator);
529
- scopedRefusals.delete(locator);
530
- sourceRefusals.delete(locator);
531
- declarationRefusals.delete(locator);
532
- for (const attached of ['collection', 'typed-selection']) {
533
- const key = modelKey(locator, attached);
534
- models.delete(key);
535
- liveModels.delete(key);
536
- invalidModels.delete(key);
537
- }
538
- journal.record(`room unlisted ${locator}`);
1006
+ }
1007
+ }
1008
+ function fence(locators) {
1009
+ const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1010
+ const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
1011
+ room: true,
1012
+ queue: true,
1013
+ drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
1014
+ }));
1015
+ for (const locator of fresh) {
1016
+ fences.set(locator, { at: discoveries, erased: false });
1017
+ lastFenced.set(locator, ++fencings);
1018
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1019
+ pendings.delete(batch.id);
1020
+ replicas.delete(locator);
1021
+ queuesWithoutReplica.delete(locator);
1022
+ rooms.delete(locator);
1023
+ roomDrafts.delete(locator);
1024
+ transfers.delete(locator);
1025
+ catching.delete(locator);
1026
+ discovered.delete(locator);
1027
+ announced.delete(locator);
1028
+ unreadable.delete(locator);
1029
+ stale.delete(locator);
1030
+ repairable.delete(locator);
1031
+ unindexable.delete(locator);
1032
+ paused.delete(locator);
1033
+ scopedRefusals.delete(locator);
1034
+ sourceRefusals.delete(locator);
1035
+ declarationRefusals.delete(locator);
1036
+ for (const attached of ['collection', 'typed-selection']) {
1037
+ const key = modelKey(locator, attached);
1038
+ models.delete(key);
1039
+ liveModels.delete(key);
1040
+ invalidModels.delete(key);
539
1041
  }
540
- publishChangeSet(mergeChangeSets(removed));
541
- moveTo({ ...status, ...attention() });
542
- });
1042
+ journal.record(`room lost ${locator}`);
1043
+ }
1044
+ if (fresh.length === 0)
1045
+ return fresh;
1046
+ publishChangeSet(mergeChangeSets(removed));
1047
+ for (const locator of fresh)
1048
+ prepared.delete(locator);
1049
+ searchIndex.forget(fresh);
1050
+ readSide.recover();
1051
+ moveTo({ ...status, ...attention() });
1052
+ return fresh;
1053
+ }
1054
+ async function erased(local, locators) {
1055
+ await local.erase(locators);
1056
+ for (const locator of locators) {
1057
+ const held = fences.get(locator);
1058
+ if (held !== undefined)
1059
+ held.erased = true;
1060
+ }
1061
+ }
1062
+ async function loseRooms(locators) {
1063
+ const fresh = fence(locators);
1064
+ if (fresh.length > 0)
1065
+ await durably((local) => erased(local, fresh));
543
1066
  }
544
1067
  function restored(key, persisted) {
545
1068
  const prior = models.get(key);
@@ -560,7 +1083,7 @@ export function composeRuntime(options) {
560
1083
  return model;
561
1084
  }
562
1085
  async function hydrate(opened) {
563
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
1086
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
564
1087
  opened.all(),
565
1088
  opened.queues(),
566
1089
  opened.roomDrafts(),
@@ -572,7 +1095,9 @@ export function composeRuntime(options) {
572
1095
  opened.capabilities(),
573
1096
  opened.member(),
574
1097
  opened.transfers(),
1098
+ opened.targets(),
575
1099
  ]);
1100
+ targets = heldTargets;
576
1101
  transfers.clear();
577
1102
  for (const record of staged)
578
1103
  transfers.set(record.locator, record);
@@ -634,6 +1159,7 @@ export function composeRuntime(options) {
634
1159
  async function readState(opened) {
635
1160
  const { lastCompleted, locators, read } = await hydrate(opened);
636
1161
  applyChangeSet(read);
1162
+ prepare();
637
1163
  if (replicas.size > 0)
638
1164
  emitRooms();
639
1165
  if (member !== null)
@@ -644,8 +1170,15 @@ export function composeRuntime(options) {
644
1170
  return lastCompleted;
645
1171
  }
646
1172
  async function open() {
647
- const opened = await openRuntimeStore();
1173
+ if (closed)
1174
+ throw new Error('runtime closed');
1175
+ const opened = await openRuntimeStore(isFenced);
648
1176
  store = opened;
1177
+ const interrupted = await opened.fences();
1178
+ for (const locator of interrupted) {
1179
+ fences.set(locator, { at: discoveries, erased: false });
1180
+ lastFenced.set(locator, ++fencings);
1181
+ }
649
1182
  const kept = options.shell.generation;
650
1183
  const storedGeneration = await opened.generation();
651
1184
  const loss = detectStorageLoss(storedGeneration, kept);
@@ -659,6 +1192,8 @@ export function composeRuntime(options) {
659
1192
  emitStorageLoss();
660
1193
  }
661
1194
  const lastCompleted = await readState(opened);
1195
+ if (interrupted.length > 0)
1196
+ await erased(opened, interrupted);
662
1197
  moveTo({ ...status, lastCompleted, ...attention() });
663
1198
  return opened;
664
1199
  }
@@ -740,17 +1275,19 @@ export function composeRuntime(options) {
740
1275
  moveTo({ ...status, ...attention() });
741
1276
  throw error;
742
1277
  }
743
- queuesWithoutReplica.delete(locator);
744
- for (const { batch, returned } of unqueued) {
745
- const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
746
- journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
747
- }
748
- replicas.set(locator, replicaOf(confirmed, kept, before));
749
- return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
750
- room: settling.room,
751
- queue: kept.length !== previousQueue.length,
752
- drafts: [...written.keys()],
753
- });
1278
+ return () => {
1279
+ queuesWithoutReplica.delete(locator);
1280
+ for (const { batch, returned } of unqueued) {
1281
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
1282
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1283
+ }
1284
+ replicas.set(locator, replicaOf(confirmed, kept, before));
1285
+ return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
1286
+ room: settling.room,
1287
+ queue: kept.length !== previousQueue.length,
1288
+ drafts: [...written.keys()],
1289
+ });
1290
+ };
754
1291
  }
755
1292
  function adoptModels(locator, record) {
756
1293
  for (const [attached, model] of [
@@ -783,11 +1320,16 @@ export function composeRuntime(options) {
783
1320
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
784
1321
  };
785
1322
  }
786
- function publish(local, room, confirmed, landing = {}) {
1323
+ function publish(local, room, confirmed, landing, live) {
787
1324
  const { base, stored } = landing;
788
1325
  return exclusive(async () => {
789
- if (!continuing())
1326
+ if (stored !== undefined) {
1327
+ await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
1328
+ }
1329
+ if (!live()) {
1330
+ caughtUpTo(room.locator);
790
1331
  return true;
1332
+ }
791
1333
  const prior = replicas.get(room.locator);
792
1334
  if (base !== undefined &&
793
1335
  (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
@@ -801,24 +1343,42 @@ export function composeRuntime(options) {
801
1343
  const accepted = pendings.get(batch.id)?.accepted;
802
1344
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
803
1345
  });
804
- const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
805
- if (stored !== undefined)
1346
+ let settled;
1347
+ try {
1348
+ settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
1349
+ }
1350
+ catch (error) {
1351
+ if (!exceeded(error))
1352
+ throw error;
1353
+ await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
1354
+ caughtUpTo(room.locator);
1355
+ return true;
1356
+ }
1357
+ const set = settled();
1358
+ if (stored !== undefined) {
806
1359
  transfers.delete(room.locator);
1360
+ catching.delete(room.locator);
1361
+ }
807
1362
  for (const batch of accepted)
808
1363
  pendings.delete(batch.id);
809
1364
  stale.delete(room.locator);
1365
+ unindexable.delete(room.locator);
810
1366
  if (unreadable.delete(room.locator))
811
1367
  scopedRefusals.delete(room.locator);
812
1368
  sourceRefusals.delete(room.locator);
813
1369
  declarationRefusals.delete(room.locator);
814
1370
  list(room, true);
1371
+ if (prior === undefined)
1372
+ prepared.add(room.locator);
815
1373
  publishChangeSet(set);
816
1374
  moveTo({ ...status, ...attention() });
1375
+ options.adapter.roomStream?.published(room.locator, roomVersion);
817
1376
  return true;
818
1377
  });
819
1378
  }
820
1379
  async function saveRoomDrafts(locator, named, writtenBy) {
821
1380
  const local = await openLocal();
1381
+ await referenced(locator);
822
1382
  const replica = replicas.get(locator);
823
1383
  const room = rooms.get(locator);
824
1384
  if (replica === undefined || room === undefined) {
@@ -866,6 +1426,11 @@ export function composeRuntime(options) {
866
1426
  changes,
867
1427
  bases: parts.flatMap(({ bases }) => bases),
868
1428
  writtenBy,
1429
+ clean: {
1430
+ text: replica.confirmed.record.documents,
1431
+ binaries: replica.confirmed.record.binaries,
1432
+ composition: compositionOf(replica.confirmed.record),
1433
+ },
869
1434
  };
870
1435
  const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
871
1436
  if (checked?.kind === 'refused')
@@ -936,6 +1501,8 @@ export function composeRuntime(options) {
936
1501
  catch {
937
1502
  return { outcome: 'refused', code: 'durable-write', retryable: true };
938
1503
  }
1504
+ if (fences.has(locator))
1505
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
939
1506
  for (const origin of withdrawn.keys())
940
1507
  pendings.delete(origin);
941
1508
  for (const [id, drafts] of drafted) {
@@ -965,6 +1532,8 @@ export function composeRuntime(options) {
965
1532
  return;
966
1533
  }
967
1534
  await local.submit(locator, identifier, submission, lost);
1535
+ if (fences.has(locator))
1536
+ return;
968
1537
  pendings.set(identifier, { ...state, submission, lost });
969
1538
  const replica = replicas.get(locator);
970
1539
  if (replica !== undefined)
@@ -975,6 +1544,8 @@ export function composeRuntime(options) {
975
1544
  if (!continuing())
976
1545
  throw new Error('connection refused');
977
1546
  await local.keepUpload(locator, identifier, upload);
1547
+ if (fences.has(locator))
1548
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
978
1549
  pendings.set(identifier, { ...pendings.get(identifier), upload });
979
1550
  if (!continuing())
980
1551
  throw new Error('connection refused');
@@ -990,6 +1561,7 @@ export function composeRuntime(options) {
990
1561
  }
991
1562
  if (closed || authorization === undefined || refusal !== null)
992
1563
  return UNCERTAIN;
1564
+ const since = fencings;
993
1565
  const transfer = ['transfer', 'history', locator, path];
994
1566
  if (query.revision !== undefined)
995
1567
  transfer.push(query.revision);
@@ -1004,6 +1576,8 @@ export function composeRuntime(options) {
1004
1576
  ...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
1005
1577
  });
1006
1578
  journal.record(`outcome history ${result.kind}`);
1579
+ if (fencedSince(locator, since))
1580
+ return failure({ kind: 'room-not-readable', locator });
1007
1581
  if (result.kind === 'scoped-refusal') {
1008
1582
  const room = replicas.get(result.locator)?.confirmed.record;
1009
1583
  if (room !== undefined)
@@ -1026,6 +1600,12 @@ export function composeRuntime(options) {
1026
1600
  }
1027
1601
  return null;
1028
1602
  }
1603
+ function binaryRoom(request) {
1604
+ if (request.kind === 'path')
1605
+ return request.locator;
1606
+ const scope = options.adapter.referrerScope(request.referrer);
1607
+ return scope?.kind === 'room' ? scope.locator : null;
1608
+ }
1029
1609
  async function readBinary(request) {
1030
1610
  if (closed || refusal !== null)
1031
1611
  return UNCERTAIN;
@@ -1037,12 +1617,16 @@ export function composeRuntime(options) {
1037
1617
  }
1038
1618
  if (closed || authorization === undefined || refusal !== null)
1039
1619
  return UNCERTAIN;
1620
+ const since = fencings;
1040
1621
  journal.record(request.kind === 'path'
1041
1622
  ? `transfer binary ${request.locator} ${request.path}`
1042
1623
  : `transfer binary ${request.fileRef}`);
1043
1624
  try {
1044
1625
  const result = await options.adapter.binary(authorization, request);
1045
1626
  journal.record(`outcome binary ${result.kind}`);
1627
+ const locator = binaryRoom(request);
1628
+ if (locator !== null && fencedSince(locator, since))
1629
+ return failure({ kind: 'room-not-readable', locator });
1046
1630
  if (result.kind === 'scoped-refusal') {
1047
1631
  const room = replicas.get(result.locator)?.confirmed.record;
1048
1632
  if (room !== undefined)
@@ -1166,11 +1750,11 @@ export function composeRuntime(options) {
1166
1750
  options.adapter.reached(locator, written, roomVersion) &&
1167
1751
  !options.adapter.reached(locator, roomVersion, written));
1168
1752
  }
1169
- async function pullRefresh(authorized, confirmed) {
1170
- if (!continuing())
1753
+ async function pullRefresh(authorized, confirmed, live) {
1754
+ if (!live())
1171
1755
  return unconfirmed;
1172
1756
  try {
1173
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
1757
+ const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1174
1758
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
1175
1759
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
1176
1760
  return result;
@@ -1180,47 +1764,80 @@ export function composeRuntime(options) {
1180
1764
  throw error;
1181
1765
  }
1182
1766
  }
1183
- function staging(local) {
1767
+ function staging(local, live) {
1184
1768
  return {
1185
1769
  held: (locator) => transfers.get(locator) ?? null,
1186
1770
  staged: (record) => local.staged(record),
1187
1771
  stage: (record, page) => exclusive(async () => {
1188
- if (!continuing())
1772
+ if (!live())
1773
+ return false;
1774
+ const bytes = layerBytes(page);
1775
+ if (!(await admitted(local, record.locator, bytes)))
1776
+ return false;
1777
+ try {
1778
+ await local.stage(record, page);
1779
+ }
1780
+ catch (error) {
1781
+ if (!exceeded(error))
1782
+ throw error;
1783
+ await exceededOn(record.locator, bytes);
1189
1784
  return false;
1190
- await local.stage(record, page);
1785
+ }
1191
1786
  transfers.set(record.locator, record);
1787
+ readSide.recover();
1192
1788
  return true;
1193
1789
  }),
1194
1790
  abandon: (locator) => exclusive(async () => {
1195
- if (!continuing())
1791
+ if (!live())
1196
1792
  return;
1197
1793
  await local.abandon(locator);
1198
1794
  transfers.delete(locator);
1795
+ readSide.recover();
1199
1796
  }),
1200
1797
  };
1201
1798
  }
1202
- async function pullBootstrap(authorized, room, local) {
1203
- if (!continuing())
1799
+ function reusable(room) {
1800
+ const record = replicas.get(room.locator)?.confirmed.record;
1801
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1802
+ return null;
1803
+ return { documents: record.documents, revisions: record.revisions };
1804
+ }
1805
+ async function pullBootstrap(authorized, room, local, live) {
1806
+ if (!live())
1204
1807
  return unconfirmed;
1808
+ if (replicas.has(room.locator)) {
1809
+ catching.add(room.locator);
1810
+ readSide.recover();
1811
+ }
1205
1812
  try {
1206
1813
  const result = await bootstrapRoom(options.adapter, authorized, room, {
1207
1814
  collection: modelOf(room.locator),
1208
1815
  selection: modelOf(room.locator, 'typed-selection'),
1209
- }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
1816
+ }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
1210
1817
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
1211
1818
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1819
+ if (result.kind !== 'staged')
1820
+ caughtUpTo(room.locator);
1212
1821
  return result;
1213
1822
  }
1214
1823
  catch (error) {
1215
1824
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
1825
+ caughtUpTo(room.locator);
1216
1826
  throw error;
1217
1827
  }
1218
1828
  }
1219
- async function advance(room, local, authorized) {
1829
+ function caughtUpTo(locator) {
1830
+ if (catching.delete(locator))
1831
+ readSide.recover();
1832
+ }
1833
+ async function advance(room, local, authorized, live) {
1834
+ await referenced(room.locator);
1835
+ if (!live() || paused.has(room.locator))
1836
+ return unconfirmed;
1220
1837
  const held = replicas.get(room.locator);
1221
1838
  if (held !== undefined) {
1222
- const refreshed = await pullRefresh(authorized, held.confirmed);
1223
- if (!continuing())
1839
+ const refreshed = await pullRefresh(authorized, held.confirmed, live);
1840
+ if (!live())
1224
1841
  return unconfirmed;
1225
1842
  if (refreshed.kind === 'still-current') {
1226
1843
  stale.delete(room.locator);
@@ -1234,31 +1851,45 @@ export function composeRuntime(options) {
1234
1851
  return null;
1235
1852
  }
1236
1853
  if (refreshed.kind === 'advanced') {
1237
- if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
1854
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
1238
1855
  const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
1239
1856
  const reached = now !== undefined &&
1240
1857
  options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
1241
- return reached ? null : advance(room, local, authorized);
1858
+ return reached ? null : advance(room, local, authorized, live);
1242
1859
  }
1243
1860
  await retainModel(local, room, 'collection', modelOf(room.locator));
1244
1861
  return null;
1245
1862
  }
1246
1863
  if (refreshed.kind !== 'beyond-horizon')
1247
1864
  return refreshed;
1865
+ if (opened !== null) {
1866
+ owed.add(room.locator);
1867
+ readSide.recover();
1868
+ return unconfirmed;
1869
+ }
1248
1870
  }
1249
- const bootstrapped = await pullBootstrap(authorized, room, local);
1250
- if (!continuing())
1871
+ const bootstrapped = await pullBootstrap(authorized, room, local, live);
1872
+ if (!live())
1251
1873
  return unconfirmed;
1252
1874
  if (bootstrapped.kind !== 'staged')
1253
1875
  return bootstrapped;
1254
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1876
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
1255
1877
  return null;
1256
1878
  }
1257
1879
  async function withhold(withheld, room, local, progress) {
1880
+ if (withheld.kind === 'unconfirmed')
1881
+ return;
1258
1882
  if (withheld.kind === 'refused')
1259
1883
  stale.delete(room.locator);
1260
1884
  else
1261
1885
  stale.add(room.locator);
1886
+ if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
1887
+ const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
1888
+ repairable.set(room.locator, reset);
1889
+ repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
1890
+ }
1891
+ else
1892
+ repairable.delete(room.locator);
1262
1893
  if (withheld.kind === 'scoped-refusal') {
1263
1894
  await scoped(withheld, room, local);
1264
1895
  }
@@ -1294,6 +1925,7 @@ export function composeRuntime(options) {
1294
1925
  unreadable.add(room.locator);
1295
1926
  list(room, false);
1296
1927
  emitRooms();
1928
+ moveTo({ ...status, ...attention() });
1297
1929
  journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
1298
1930
  }
1299
1931
  function leaving(batch, result) {
@@ -1315,6 +1947,7 @@ export function composeRuntime(options) {
1315
1947
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1316
1948
  }
1317
1949
  async function adoptAccepted(room, local, submitted, accepted) {
1950
+ await referenced(room.locator);
1318
1951
  if (!continuing())
1319
1952
  return unconfirmed;
1320
1953
  const current = replicas.get(room.locator);
@@ -1331,16 +1964,17 @@ export function composeRuntime(options) {
1331
1964
  if (applied.kind !== 'staged') {
1332
1965
  const state = pendings.get(submitted.id);
1333
1966
  await local.accept(room.locator, submitted.id, accepted.roomVersion);
1334
- if (state !== undefined)
1967
+ if (state !== undefined && !fences.has(room.locator))
1335
1968
  pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1336
1969
  emitQueue(room.locator);
1337
1970
  return applied;
1338
1971
  }
1339
- const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1972
+ const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1340
1973
  pendings.delete(submitted.id);
1341
1974
  unreadable.delete(room.locator);
1342
1975
  sourceRefusals.delete(room.locator);
1343
1976
  declarationRefusals.delete(room.locator);
1977
+ const set = settled();
1344
1978
  list(room, true);
1345
1979
  publishChangeSet(set);
1346
1980
  moveTo({ ...status, ...attention() });
@@ -1354,14 +1988,14 @@ export function composeRuntime(options) {
1354
1988
  refusedSource(room, failed);
1355
1989
  return false;
1356
1990
  }
1357
- async function submitRoom(room, local, authorized, progress) {
1358
- while (continuing() && authorization !== undefined) {
1991
+ async function submitRoom(room, local, authorized, progress, signal) {
1992
+ while (continuing() && authorization !== undefined && !signal.aborted) {
1359
1993
  const replica = replicas.get(room.locator);
1360
1994
  const head = replica?.queue[0];
1361
1995
  if (replica === undefined || head === undefined)
1362
- return;
1996
+ return 'done';
1363
1997
  if (pendings.get(head.id)?.accepted !== undefined)
1364
- return;
1998
+ return 'done';
1365
1999
  const state = pendings.get(head.id);
1366
2000
  const { submission, lost: earlier } = state ?? {
1367
2001
  submission: { state: 'waiting' },
@@ -1369,24 +2003,30 @@ export function composeRuntime(options) {
1369
2003
  };
1370
2004
  if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
1371
2005
  progress.complete = false;
1372
- return;
2006
+ return { at: Date.parse(submission.reset) };
1373
2007
  }
1374
2008
  if (writesHeld()) {
1375
2009
  progress.complete = false;
1376
2010
  journal.record(`submission ${room.locator} ${head.id} held`);
1377
- return;
2011
+ return 'done';
1378
2012
  }
1379
2013
  const lost = earlier || submission.state === 'submitted';
2014
+ if (lost && pastHorizon(head.id)) {
2015
+ await discardPending(room, local, signal);
2016
+ return 'done';
2017
+ }
1380
2018
  let result;
1381
2019
  journal.record(`submission ${room.locator} ${head.id}`);
1382
2020
  await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1383
- if (discoveryPaused)
1384
- return;
1385
- if (writesHeld()) {
2021
+ if (discoveryPaused || fences.has(room.locator))
2022
+ return 'done';
2023
+ if (signal.aborted || writesHeld()) {
1386
2024
  await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
2025
+ if (signal.aborted)
2026
+ return 'done';
1387
2027
  progress.complete = false;
1388
2028
  journal.record(`submission ${room.locator} ${head.id} held`);
1389
- return;
2029
+ return 'done';
1390
2030
  }
1391
2031
  const opensBefore = openSignals;
1392
2032
  const answered = awaitingAnswer(room.locator);
@@ -1402,32 +2042,36 @@ export function composeRuntime(options) {
1402
2042
  progress.complete = false;
1403
2043
  if (continuing())
1404
2044
  progress.reachable = false;
1405
- return;
2045
+ return 'again';
2046
+ }
2047
+ if (signal.aborted) {
2048
+ answered();
2049
+ return 'done';
1406
2050
  }
1407
2051
  if (result.kind !== 'accepted' || !continuing())
1408
2052
  answered();
1409
2053
  if (!continuing())
1410
- return;
2054
+ return 'done';
1411
2055
  if (result.kind === 'transport-uncertainty') {
1412
2056
  progress.complete = false;
1413
2057
  progress.reachable = false;
1414
- return;
2058
+ return 'again';
1415
2059
  }
1416
2060
  if (result.kind === 'room-not-readable') {
1417
2061
  progress.complete = false;
1418
2062
  unreadable.add(room.locator);
1419
2063
  list(room, false);
1420
2064
  emitRooms();
1421
- return;
2065
+ return 'done';
1422
2066
  }
1423
2067
  if (result.kind === 'revoked-authorization') {
1424
2068
  await refuse('revoked-authorization');
1425
- return;
2069
+ return 'done';
1426
2070
  }
1427
2071
  if (result.kind === 'scoped-refusal') {
1428
2072
  await scoped(result, room, local);
1429
2073
  progress.complete = false;
1430
- return;
2074
+ return 'done';
1431
2075
  }
1432
2076
  if (result.kind === 'write-halted') {
1433
2077
  journal.record(`outcome ${room.locator} ${head.id} write-halted`);
@@ -1436,16 +2080,24 @@ export function composeRuntime(options) {
1436
2080
  continue;
1437
2081
  haltedRefusal = true;
1438
2082
  progress.complete = false;
1439
- return;
2083
+ return 'done';
1440
2084
  }
1441
2085
  if (result.kind === 'conflicted' ||
1442
2086
  result.kind === 'rejected' ||
1443
2087
  result.kind === 'expired' ||
1444
2088
  result.kind === 'retryable-refusal') {
2089
+ if (result.kind === 'expired' && lost && result.restored !== true) {
2090
+ journal.record(`outcome ${room.locator} ${head.id} expired`);
2091
+ await discardPending(room, local, signal);
2092
+ return 'done';
2093
+ }
1445
2094
  if (result.kind === 'expired') {
1446
- const withheld = await advance(room, local, authorized);
1447
- if (withheld !== null)
1448
- return withhold(withheld, room, local, progress);
2095
+ await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
2096
+ const withheld = await advance(room, local, authorized, continuing);
2097
+ if (withheld !== null) {
2098
+ await withhold(withheld, room, local, progress);
2099
+ return 'done';
2100
+ }
1449
2101
  }
1450
2102
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
1451
2103
  (result.kind === 'conflicted' && result.roomVersion !== undefined
@@ -1458,8 +2110,7 @@ export function composeRuntime(options) {
1458
2110
  const reset = result.reset;
1459
2111
  progress.complete = false;
1460
2112
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1461
- armTimer();
1462
- return;
2113
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
1463
2114
  }
1464
2115
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
1465
2116
  const left = leaving(head, result);
@@ -1470,37 +2121,82 @@ export function composeRuntime(options) {
1470
2121
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
1471
2122
  return;
1472
2123
  }
1473
- const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
2124
+ const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1474
2125
  leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1475
2126
  (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1476
2127
  ? { ...left, returned: null }
1477
2128
  : left,
1478
2129
  });
1479
- publishChangeSet(set);
2130
+ publishChangeSet(settled());
1480
2131
  moveTo({ ...status, ...attention() });
1481
2132
  });
1482
2133
  if (result.kind === 'expired')
1483
2134
  continue;
1484
- const withheld = await advance(room, local, authorized);
1485
- if (withheld !== null)
1486
- return withhold(withheld, room, local, progress);
2135
+ const withheld = await advance(room, local, authorized, continuing);
2136
+ if (withheld !== null) {
2137
+ await withhold(withheld, room, local, progress);
2138
+ return 'done';
2139
+ }
1487
2140
  continue;
1488
2141
  }
1489
2142
  const kept = await adopted(room, local, head, result).finally(answered);
1490
2143
  if (!kept) {
1491
2144
  progress.complete = false;
1492
- return;
2145
+ return 'done';
1493
2146
  }
1494
2147
  }
2148
+ return 'done';
2149
+ }
2150
+ function pastHorizon(identity) {
2151
+ return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
2152
+ }
2153
+ async function discardPending(room, local, signal) {
2154
+ const { locator } = room;
2155
+ const discarded = await exclusive(async () => {
2156
+ if (fences.has(locator))
2157
+ return false;
2158
+ const before = replicas.get(locator);
2159
+ const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
2160
+ fences.set(locator, { at: discoveries, erased: false });
2161
+ lastFenced.set(locator, ++fencings);
2162
+ try {
2163
+ await local.discard(locator);
2164
+ }
2165
+ finally {
2166
+ fences.delete(locator);
2167
+ }
2168
+ for (const batch of queue)
2169
+ pendings.delete(batch.id);
2170
+ queuesWithoutReplica.delete(locator);
2171
+ const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
2172
+ if (after !== null)
2173
+ replicas.set(locator, after);
2174
+ journal.record(`room discarded ${locator}`);
2175
+ publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
2176
+ moveTo({ ...status, ...attention() });
2177
+ return true;
2178
+ });
2179
+ if (!discarded)
2180
+ return;
2181
+ const progress = { complete: true, reachable: true };
2182
+ await roomTask(locator, async (live) => {
2183
+ await Promise.race([
2184
+ reload(room, local, progress, () => live() && !signal.aborted),
2185
+ abandonedOn(signal),
2186
+ ]);
2187
+ });
2188
+ if (signal.aborted)
2189
+ return;
2190
+ await rest(progress);
1495
2191
  }
1496
2192
  function awaitingAnswer(locator) {
1497
2193
  let release = () => { };
1498
2194
  const awaited = new Promise((resolve) => {
1499
2195
  release = resolve;
1500
2196
  });
1501
- answering.set(locator, awaited);
2197
+ answering.set(locator, { generation, answered: awaited });
1502
2198
  return () => {
1503
- if (answering.get(locator) === awaited)
2199
+ if (answering.get(locator)?.answered === awaited)
1504
2200
  answering.delete(locator);
1505
2201
  release();
1506
2202
  };
@@ -1616,19 +2312,23 @@ export function composeRuntime(options) {
1616
2312
  ];
1617
2313
  });
1618
2314
  }
1619
- async function synchronize(listed, local, progress, attempt) {
1620
- const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1621
- const ready = listed.filter((_, index) => {
2315
+ async function synchronize(listed, local, progress, attempt, live) {
2316
+ const acquired = listed.filter(acquires);
2317
+ const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
2318
+ if (!live())
2319
+ return;
2320
+ const ready = acquired.filter((_, index) => {
1622
2321
  const outcome = prepared[index];
1623
2322
  return outcome?.status === 'fulfilled' && outcome.value;
1624
2323
  });
1625
2324
  if (attempt === 'opened') {
1626
- options.adapter.roomStream?.hold(heldRooms(ready));
2325
+ options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
1627
2326
  handed = true;
1628
2327
  }
2328
+ discovering?.release();
1629
2329
  const loaded = await Promise.allSettled(ready
1630
2330
  .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1631
- .map((room) => loadRoom(room, local, progress)));
2331
+ .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
1632
2332
  const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1633
2333
  if (failed !== undefined)
1634
2334
  throw failed.reason;
@@ -1657,14 +2357,14 @@ export function composeRuntime(options) {
1657
2357
  }
1658
2358
  return true;
1659
2359
  }
1660
- async function caughtUp(room, local, progress) {
2360
+ async function caughtUp(room, local, progress, live) {
1661
2361
  const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1662
- if (held === undefined || authorization === undefined)
2362
+ if (held === undefined || authorization === undefined || !live())
1663
2363
  return;
1664
2364
  const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1665
2365
  if (!behind)
1666
2366
  return;
1667
- const withheld = await advance(room, local, authorization);
2367
+ const withheld = await advance(room, local, authorization, live);
1668
2368
  if (withheld !== null)
1669
2369
  await withhold(withheld, room, local, progress);
1670
2370
  }
@@ -1674,39 +2374,46 @@ export function composeRuntime(options) {
1674
2374
  return model.status === 'unavailable' && model.reason === 'revoked';
1675
2375
  });
1676
2376
  }
1677
- async function loadRoom(room, local, progress) {
1678
- if (discoveryPaused || authorization === undefined)
2377
+ async function loadRoom(room, local, progress, live) {
2378
+ if (!live() || authorization === undefined)
1679
2379
  return;
1680
2380
  if (replicas.has(room.locator) && revoked(room.locator)) {
1681
- await reload(room, local, progress);
2381
+ await reload(room, local, progress, live);
1682
2382
  return;
1683
2383
  }
1684
- const withheld = await advance(room, local, authorization);
2384
+ const withheld = await advance(room, local, authorization, live);
1685
2385
  if (withheld !== null) {
1686
2386
  await withhold(withheld, room, local, progress);
1687
2387
  return;
1688
2388
  }
1689
- await caughtUp(room, local, progress);
2389
+ await caughtUp(room, local, progress, live);
1690
2390
  }
1691
- async function reload(room, local, progress) {
1692
- if (discoveryPaused || authorization === undefined)
1693
- return;
1694
- const bootstrapped = await pullBootstrap(authorization, room, local);
1695
- if (!continuing())
1696
- return;
1697
- if (bootstrapped.kind !== 'staged') {
1698
- await withhold(bootstrapped, room, local, progress);
1699
- return;
2391
+ async function reload(room, local, progress, live) {
2392
+ try {
2393
+ if (!live() || authorization === undefined)
2394
+ return;
2395
+ const bootstrapped = await pullBootstrap(authorization, room, local, live);
2396
+ if (!live())
2397
+ return;
2398
+ if (bootstrapped.kind !== 'staged') {
2399
+ await withhold(bootstrapped, room, local, progress);
2400
+ return;
2401
+ }
2402
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
2403
+ await caughtUp(room, local, progress, live);
2404
+ }
2405
+ finally {
2406
+ caughtUpTo(room.locator);
1700
2407
  }
1701
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1702
- await caughtUp(room, local, progress);
1703
2408
  }
1704
- async function discoverRooms(local, progress, attempt) {
2409
+ async function discoverRooms(local, progress, attempt, live) {
1705
2410
  if (closed || authorization === undefined) {
1706
2411
  notifyReady();
1707
2412
  return;
1708
2413
  }
1709
2414
  moveTo(transferring(status));
2415
+ discoveries += 1;
2416
+ const started = discoveries;
1710
2417
  let result;
1711
2418
  try {
1712
2419
  result = await options.adapter.discover(authorization);
@@ -1714,7 +2421,8 @@ export function composeRuntime(options) {
1714
2421
  catch {
1715
2422
  result = { kind: 'transport-uncertainty' };
1716
2423
  }
1717
- if (closed) {
2424
+ if (!live()) {
2425
+ progress.complete = false;
1718
2426
  notifyReady();
1719
2427
  return;
1720
2428
  }
@@ -1772,6 +2480,7 @@ export function composeRuntime(options) {
1772
2480
  }
1773
2481
  const previousCapabilities = capabilities;
1774
2482
  capabilities = result.capabilities;
2483
+ outcomeRetention = result.retention.outcome;
1775
2484
  notifyReady();
1776
2485
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1777
2486
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
@@ -1784,8 +2493,7 @@ export function composeRuntime(options) {
1784
2493
  member = signedIn;
1785
2494
  emitMember();
1786
2495
  }
1787
- await applyDiscovery(result.rooms, local);
1788
- await synchronize(result.rooms, local, progress, attempt);
2496
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
1789
2497
  }
1790
2498
  async function ensureAuthorization() {
1791
2499
  if (authorization !== undefined)
@@ -1799,20 +2507,50 @@ export function composeRuntime(options) {
1799
2507
  throw error;
1800
2508
  }
1801
2509
  }
1802
- function task(work) {
2510
+ async function admittedRead(work) {
2511
+ if (roomReads.generation !== generation) {
2512
+ roomReads = { generation, free: ROOM_READS, waiting: [] };
2513
+ }
2514
+ const reads = roomReads;
2515
+ while (reads.free === 0)
2516
+ await new Promise((admit) => reads.waiting.push(admit));
2517
+ reads.free -= 1;
2518
+ try {
2519
+ await work();
2520
+ }
2521
+ finally {
2522
+ reads.free += 1;
2523
+ reads.waiting.shift()?.();
2524
+ }
2525
+ }
2526
+ function roomTask(locator, work) {
2527
+ return task(roomLane(locator), (live) => admittedRead(() => work(live)));
2528
+ }
2529
+ function task(lane, work) {
1803
2530
  if (stopping)
1804
- return chain;
1805
- chain = chain.then(work, work).catch(() => undefined);
1806
- return chain;
2531
+ return Promise.resolve();
2532
+ const held = lanes.get(lane);
2533
+ const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
2534
+ const turn = current.generation;
2535
+ const live = () => turn === generation && continuing();
2536
+ const run = current.chain.then(() => work(live)).catch(() => undefined);
2537
+ current.chain = run;
2538
+ lanes.set(lane, current);
2539
+ working.add(run);
2540
+ void run.finally(() => working.delete(run));
2541
+ return run;
1807
2542
  }
1808
2543
  function sync(attempt) {
1809
- return task(async () => {
1810
- if (closed || stopping || discoveryPaused) {
2544
+ return task(OPENING_LANE, async (live) => {
2545
+ if (!live()) {
1811
2546
  notifyReady();
1812
2547
  return;
1813
2548
  }
1814
2549
  const progress = { complete: true, reachable: true };
1815
- underway = attempt === 'opened' ? progress : null;
2550
+ const gate = discover();
2551
+ if (attempt === 'opened')
2552
+ opened = generation;
2553
+ underway = attempt === 'opened' ? { generation, progress } : null;
1816
2554
  if (attempt === 'opened')
1817
2555
  streamed = progress;
1818
2556
  complete = false;
@@ -1821,12 +2559,12 @@ export function composeRuntime(options) {
1821
2559
  handed = false;
1822
2560
  try {
1823
2561
  const local = await openLocal();
1824
- if (closed)
2562
+ if (!live())
1825
2563
  return;
1826
2564
  await ensureAuthorization();
1827
- if (closed)
2565
+ if (!live())
1828
2566
  return;
1829
- await discoverRooms(local, progress, attempt);
2567
+ await discoverRooms(local, progress, attempt, live);
1830
2568
  }
1831
2569
  catch {
1832
2570
  progress.complete = false;
@@ -1834,17 +2572,21 @@ export function composeRuntime(options) {
1834
2572
  progress.reachable = false;
1835
2573
  }
1836
2574
  finally {
2575
+ gate.release();
1837
2576
  notifyReady();
1838
2577
  }
2578
+ if (!live())
2579
+ return;
1839
2580
  if (attempt !== 'opened') {
1840
- await finishOpening(progress);
2581
+ await finishOpening(progress, live);
1841
2582
  return;
1842
2583
  }
1843
2584
  if (handed || closed || progress.reachable)
1844
2585
  return;
1845
2586
  underway = null;
2587
+ opened = null;
1846
2588
  journal.record('connection reopen');
1847
- await rest(progress);
2589
+ await rest(progress, live);
1848
2590
  options.adapter.roomStream?.reopen();
1849
2591
  });
1850
2592
  }
@@ -1865,24 +2607,41 @@ export function composeRuntime(options) {
1865
2607
  });
1866
2608
  }
1867
2609
  function applyOpening(expired) {
1868
- return task(async () => {
1869
- const progress = underway;
2610
+ const progress = underway?.generation === generation ? underway.progress : null;
2611
+ const cycle = progress ?? { complete: true, reachable: true };
2612
+ const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
2613
+ if (opened === generation)
2614
+ opened = null;
2615
+ return task(OPENING_LANE, async (live) => {
1870
2616
  underway = null;
1871
- if (closed || stopping)
2617
+ if (!live())
1872
2618
  return;
1873
2619
  if (progress === null) {
1874
- await reloadExpired(expired);
2620
+ await replacing;
2621
+ await rest(cycle, live);
1875
2622
  return;
1876
2623
  }
2624
+ await roomsCarried(expired);
2625
+ if (!live())
2626
+ return;
2627
+ const replaced = new Set(owed);
2628
+ owed.clear();
1877
2629
  try {
1878
2630
  const local = await openLocal();
1879
2631
  const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1880
- const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1881
- const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
2632
+ const reloading = held.filter((room) => !expired.includes(room.locator) &&
2633
+ (replaced.has(room.locator) || revoked(room.locator)));
2634
+ for (const room of reloading)
2635
+ catching.add(room.locator);
2636
+ readSide.recover();
2637
+ const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
2638
+ const reloaded = [...(await reloads), ...(await replacing)];
1882
2639
  if (progress.reachable) {
1883
- for (const room of held)
1884
- if (!reloading.includes(room))
2640
+ for (const room of held) {
2641
+ if (!expired.includes(room.locator) && !reloading.includes(room)) {
1885
2642
  await confirmModels(room, local);
2643
+ }
2644
+ }
1886
2645
  }
1887
2646
  const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1888
2647
  if (failed !== undefined)
@@ -1892,21 +2651,21 @@ export function composeRuntime(options) {
1892
2651
  progress.complete = false;
1893
2652
  progress.reachable = false;
1894
2653
  }
1895
- await finishOpening(progress);
2654
+ if (live())
2655
+ await finishOpening(progress, live);
1896
2656
  });
1897
2657
  }
1898
- async function reloadExpired(expired) {
1899
- const progress = { complete: true, reachable: true };
2658
+ async function reloadNow(replaced, progress) {
1900
2659
  try {
1901
2660
  const local = await openLocal();
1902
- await Promise.allSettled([...discovered.values()]
1903
- .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1904
- .map((room) => reload(room, local, progress)));
2661
+ return await Promise.allSettled([...discovered.values()]
2662
+ .filter((room) => replicas.has(room.locator) && replaced(room))
2663
+ .map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
1905
2664
  }
1906
2665
  catch {
1907
2666
  progress.reachable = false;
2667
+ return [];
1908
2668
  }
1909
- await rest(progress);
1910
2669
  }
1911
2670
  async function confirmModels(room, local) {
1912
2671
  for (const attached of ['collection', 'typed-selection']) {
@@ -1919,38 +2678,94 @@ export function composeRuntime(options) {
1919
2678
  function submitLater(locator) {
1920
2679
  if (localSaves)
1921
2680
  return;
1922
- const running = submitting.get(locator);
1923
- if (running !== undefined) {
1924
- running.again = true;
2681
+ void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
2682
+ }
2683
+ async function submitPass(locator, signal) {
2684
+ const room = discovered.get(locator);
2685
+ if (closed || stopping || discoveryPaused || room === undefined)
2686
+ return 'done';
2687
+ const progress = { complete: true, reachable: true };
2688
+ let next;
2689
+ try {
2690
+ await ensureAuthorization();
2691
+ if (authorization === undefined)
2692
+ return 'done';
2693
+ next = await submitRoom(room, await openLocal(), authorization, progress, signal);
2694
+ }
2695
+ catch {
2696
+ progress.reachable = false;
2697
+ next = 'again';
2698
+ }
2699
+ if (signal.aborted)
2700
+ return 'done';
2701
+ await rest(progress);
2702
+ return next === 'again' && !socketOpen ? 'done' : next;
2703
+ }
2704
+ function repairLater(locator, when) {
2705
+ if (!socketOpen || repairing.has(locator))
1925
2706
  return;
2707
+ void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
2708
+ }
2709
+ async function repairPass(locator, signal) {
2710
+ const room = discovered.get(locator);
2711
+ if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
2712
+ return 'done';
2713
+ if (room === undefined || room.replica.admission !== 'admitted')
2714
+ return 'done';
2715
+ const progress = { complete: true, reachable: true };
2716
+ repairing.add(locator);
2717
+ try {
2718
+ const local = await openLocal();
2719
+ await roomTask(locator, async (live) => {
2720
+ const current = () => live() && !signal.aborted;
2721
+ if (!current() || authorization === undefined)
2722
+ return;
2723
+ const withheld = await Promise.race([
2724
+ advance(room, local, authorization, current),
2725
+ abandonedOn(signal),
2726
+ ]);
2727
+ if (withheld !== null && current())
2728
+ await withhold(withheld, room, local, progress);
2729
+ });
1926
2730
  }
1927
- const run = { again: false, done: Promise.resolve() };
1928
- submitting.set(locator, run);
1929
- run.done = (async () => {
1930
- try {
1931
- do {
1932
- run.again = false;
1933
- const room = discovered.get(locator);
1934
- if (closed || stopping || discoveryPaused || room === undefined)
1935
- return;
1936
- const progress = { complete: true, reachable: true };
1937
- try {
1938
- await ensureAuthorization();
1939
- if (authorization === undefined)
1940
- return;
1941
- await submitRoom(room, await openLocal(), authorization, progress);
1942
- }
1943
- catch {
1944
- progress.reachable = false;
1945
- }
1946
- armTimer();
1947
- await rest(progress);
1948
- } while (run.again);
1949
- }
1950
- finally {
1951
- submitting.delete(locator);
1952
- }
1953
- })();
2731
+ catch {
2732
+ stale.add(locator);
2733
+ progress.reachable = false;
2734
+ }
2735
+ finally {
2736
+ repairing.delete(locator);
2737
+ }
2738
+ if (signal.aborted)
2739
+ return 'done';
2740
+ await rest(progress);
2741
+ if (!stale.has(locator)) {
2742
+ if ((replicas.get(locator)?.queue.length ?? 0) > 0)
2743
+ submitLater(locator);
2744
+ return 'done';
2745
+ }
2746
+ if (!repairable.has(locator))
2747
+ return 'done';
2748
+ const reset = repairable.get(locator);
2749
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
2750
+ }
2751
+ function resendLater() {
2752
+ if (!socketOpen)
2753
+ return;
2754
+ void recovery.attempt('resend', resendPass, { again: true });
2755
+ }
2756
+ async function resendPass(signal) {
2757
+ if (!socketOpen || closed || stopping || discoveryPaused)
2758
+ return 'done';
2759
+ const reading = { complete: true, reachable: true };
2760
+ try {
2761
+ await conversations.resend(reading);
2762
+ }
2763
+ catch {
2764
+ reading.reachable = false;
2765
+ }
2766
+ if (signal.aborted)
2767
+ return 'done';
2768
+ return reading.reachable && reading.complete ? 'done' : 'again';
1954
2769
  }
1955
2770
  function submitAll() {
1956
2771
  for (const [locator, replica] of replicas) {
@@ -1958,31 +2773,41 @@ export function composeRuntime(options) {
1958
2773
  submitLater(locator);
1959
2774
  }
1960
2775
  }
1961
- async function finishOpening(progress) {
2776
+ async function finishOpening(progress, live) {
1962
2777
  try {
1963
- if (closed || discoveryPaused) {
2778
+ if (!live()) {
1964
2779
  progress.complete = false;
1965
2780
  return;
1966
2781
  }
1967
2782
  submitAll();
2783
+ for (const locator of stale)
2784
+ if (repairable.has(locator))
2785
+ repairLater(locator, {});
1968
2786
  await conversations.reconcile();
1969
2787
  if (status.providerUnreachable)
1970
2788
  conversations.reconnected();
1971
2789
  const reading = { complete: true, reachable: true };
1972
- await conversations.catchUp(reading);
1973
- if (!reading.reachable)
2790
+ if (demand.conversations)
2791
+ await conversations.catchUp(reading);
2792
+ if (progress.reachable && reading.reachable)
2793
+ await conversations.resend(reading);
2794
+ if (!reading.reachable) {
1974
2795
  progress.reachable = false;
2796
+ resendLater();
2797
+ }
1975
2798
  }
1976
2799
  catch {
1977
2800
  progress.complete = false;
1978
2801
  }
1979
2802
  finally {
1980
- complete = progress.complete;
1981
- await rest(progress);
2803
+ if (live()) {
2804
+ complete = progress.complete;
2805
+ await rest(progress, live);
2806
+ }
1982
2807
  }
1983
2808
  }
1984
- async function rest(progress) {
1985
- if (closed)
2809
+ async function rest(progress, live = () => !closed) {
2810
+ if (!live())
1986
2811
  return;
1987
2812
  let lastCompleted = status.lastCompleted;
1988
2813
  let synced = complete && stale.size === 0 && !pending();
@@ -1990,6 +2815,8 @@ export function composeRuntime(options) {
1990
2815
  const completedAt = new Date().toISOString();
1991
2816
  try {
1992
2817
  await (await openLocal()).complete(completedAt);
2818
+ if (!live())
2819
+ return;
1993
2820
  recorded = true;
1994
2821
  lastCompleted = completedAt;
1995
2822
  }
@@ -2004,40 +2831,68 @@ export function composeRuntime(options) {
2004
2831
  ...attention(),
2005
2832
  });
2006
2833
  }
2834
+ function discover() {
2835
+ let release = () => { };
2836
+ const done = new Promise((resolve) => {
2837
+ release = resolve;
2838
+ });
2839
+ discovering = { generation, done, release };
2840
+ return discovering;
2841
+ }
2007
2842
  function applyRoom(locator, roomVersion) {
2008
2843
  announced.set(locator, roomVersion);
2009
- return task(async () => {
2010
- await answering.get(locator);
2011
- if (closed || stopping || discoveryPaused || authorization === undefined)
2844
+ return task(roomLane(locator), async (live) => {
2845
+ if (discovering?.generation === generation)
2846
+ await discovering.done;
2847
+ const saving = answering.get(locator);
2848
+ if (saving?.generation === generation)
2849
+ await saving.answered;
2850
+ if (!live() || authorization === undefined)
2012
2851
  return;
2013
2852
  const room = discovered.get(locator);
2014
2853
  const held = replicas.get(locator);
2015
- if (room === undefined || room.replica.admission !== 'admitted')
2854
+ if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
2016
2855
  return;
2017
2856
  if (held !== undefined &&
2018
2857
  (held.confirmed.record.replica.selection !== room.replica.selection ||
2019
2858
  options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
2020
2859
  return;
2021
2860
  }
2022
- const during = underway;
2861
+ const during = underway?.generation === generation ? underway.progress : null;
2023
2862
  const progress = during ?? { complete: true, reachable: true };
2024
2863
  if (during === null)
2025
2864
  moveTo(transferring(status));
2026
2865
  try {
2027
2866
  const local = await openLocal();
2028
- const withheld = await advance(room, local, authorization);
2029
- if (withheld !== null)
2030
- await withhold(withheld, room, local, progress);
2867
+ await admittedRead(async () => {
2868
+ if (!live() || authorization === undefined)
2869
+ return;
2870
+ const withheld = await advance(room, local, authorization, live);
2871
+ if (withheld !== null)
2872
+ await withhold(withheld, room, local, progress);
2873
+ });
2031
2874
  }
2032
2875
  catch {
2033
2876
  stale.add(locator);
2034
2877
  }
2035
2878
  finally {
2036
2879
  if (during === null)
2037
- await rest(progress);
2880
+ await rest(progress, live);
2038
2881
  }
2039
2882
  });
2040
2883
  }
2884
+ function roomsCarried(replacing) {
2885
+ const replaced = new Set(replacing.map(roomLane));
2886
+ return Promise.all([...lanes]
2887
+ .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
2888
+ .map(([, held]) => held.chain));
2889
+ }
2890
+ const receipts = {
2891
+ heads: async () => (await openLocal()).receivedHeads(),
2892
+ parts: async (key, count) => (await openLocal()).receivedParts(key, count),
2893
+ keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
2894
+ drop: async (keys) => (await openLocal()).dropReceived(keys),
2895
+ };
2041
2896
  function observeStream() {
2042
2897
  const stream = options.adapter.roomStream;
2043
2898
  if (stream === undefined || withdrawStream !== undefined)
@@ -2047,6 +2902,9 @@ export function composeRuntime(options) {
2047
2902
  return;
2048
2903
  if (event.kind === 'opened') {
2049
2904
  socketOpen = true;
2905
+ generation += 1;
2906
+ conversations.reopened();
2907
+ recovery.resume();
2050
2908
  void sync('opened');
2051
2909
  }
2052
2910
  else if (event.kind === 'closed')
@@ -2055,7 +2913,7 @@ export function composeRuntime(options) {
2055
2913
  void applyOpening(event.expired);
2056
2914
  else
2057
2915
  void applyRoom(event.locator, event.roomVersion);
2058
- });
2916
+ }, receipts);
2059
2917
  }
2060
2918
  function writesHeld() {
2061
2919
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -2070,6 +2928,7 @@ export function composeRuntime(options) {
2070
2928
  haltedRefusal = false;
2071
2929
  if (mode !== serviceMode) {
2072
2930
  serviceMode = mode;
2931
+ queueReadiness();
2073
2932
  journal.record(`service mode ${mode}`);
2074
2933
  for (const listener of serviceModeListeners)
2075
2934
  listener(mode);
@@ -2082,6 +2941,8 @@ export function composeRuntime(options) {
2082
2941
  if (stream === undefined || withdrawConversations !== undefined)
2083
2942
  return;
2084
2943
  withdrawConversations = stream.observe((event) => {
2944
+ if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
2945
+ queueReadiness();
2085
2946
  if (conversations.receive(event)) {
2086
2947
  void ready.then(scheduleCurrency);
2087
2948
  return;
@@ -2093,17 +2954,18 @@ export function composeRuntime(options) {
2093
2954
  });
2094
2955
  }
2095
2956
  function scheduleCurrency() {
2096
- if (currencyQueued || closed || stopping)
2957
+ if (currencyQueued === generation || closed || stopping)
2097
2958
  return;
2098
- currencyQueued = true;
2099
- void task(runCurrency);
2959
+ currencyQueued = generation;
2960
+ void task(CONVERSATION_LANE, runCurrency);
2100
2961
  }
2101
- async function runCurrency() {
2102
- currencyQueued = false;
2103
- if (closed || stopping || discoveryPaused)
2962
+ async function runCurrency(live) {
2963
+ if (currencyQueued === generation)
2964
+ currencyQueued = null;
2965
+ if (!live())
2104
2966
  return;
2105
2967
  await conversations.reconcile();
2106
- if (conversations.current()) {
2968
+ if (conversations.current() || !demand.conversations) {
2107
2969
  show();
2108
2970
  return;
2109
2971
  }
@@ -2115,19 +2977,24 @@ export function composeRuntime(options) {
2115
2977
  catch {
2116
2978
  progress.reachable = false;
2117
2979
  }
2118
- await rest(progress);
2980
+ await rest(progress, live);
2119
2981
  }
2120
2982
  function close() {
2121
2983
  if (closed)
2122
2984
  return;
2123
2985
  closed = true;
2986
+ if (narrowing !== null)
2987
+ clearTimeout(narrowing);
2988
+ if (rechecking !== null)
2989
+ clearInterval(rechecking);
2990
+ readinessWatches.clear();
2124
2991
  withdrawStream?.();
2125
2992
  withdrawStream = undefined;
2126
2993
  withdrawConversations?.();
2127
2994
  withdrawConversations = undefined;
2128
2995
  withdrawServiceMode?.();
2129
2996
  conversations.close();
2130
- stopTimer();
2997
+ void recovery.retire();
2131
2998
  store?.close();
2132
2999
  void opening?.then((opened) => {
2133
3000
  opened.close();
@@ -2145,9 +3012,8 @@ export function composeRuntime(options) {
2145
3012
  }
2146
3013
  async function stop() {
2147
3014
  stopping = true;
2148
- stopTimer();
2149
- await chain.catch(() => undefined);
2150
- await Promise.all([...submitting.values()].map((run) => run.done));
3015
+ await recovery.retire();
3016
+ await Promise.all(working);
2151
3017
  await mutations;
2152
3018
  close();
2153
3019
  }
@@ -2169,17 +3035,69 @@ export function composeRuntime(options) {
2169
3035
  return planOf(input, roomFacts(reach), planReach(reach));
2170
3036
  }
2171
3037
  function watchPlan(plan, deliver) {
2172
- materialize(reachedRooms(plan));
2173
- return readSide.watch(plan, identityOf(plan), deliver);
3038
+ return readSide.watch(plan, identityOf(plan), () => {
3039
+ const cover = covered(plan);
3040
+ awaitPreparation(cover);
3041
+ return cover;
3042
+ }, deliver);
2174
3043
  }
2175
3044
  async function searched(input, reach) {
2176
3045
  const plan = searchPlanOf(input);
2177
- const selected = plan.documents === undefined && reach === null
2178
- ? null
2179
- : (await rowsQuery(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
2180
- if (selected === null)
2181
- materialize(null);
2182
- return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
3046
+ const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
3047
+ for (;;) {
3048
+ const cover = covered(filter);
3049
+ awaitPreparation(cover);
3050
+ const pending = 'pending' in cover ? [...cover.pending] : [];
3051
+ const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3052
+ const held = covered({
3053
+ ...filter,
3054
+ rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
3055
+ });
3056
+ const covering = 'pending' in held ? [] : held.coverage.rooms;
3057
+ const unindexed = covering
3058
+ .filter(({ room }) => !searchIndex.rooms().has(room))
3059
+ .map(({ room }) => room);
3060
+ for (const room of unindexed)
3061
+ pending.push({ room, awaits: 'index' });
3062
+ const listed = conversations.searchable();
3063
+ const named = reach !== null && !reach.conversations
3064
+ ? []
3065
+ : (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
3066
+ const waiting = named.filter((one) => !searchIndex.conversations().has(one));
3067
+ for (const conversation of waiting)
3068
+ pending.push({ conversation, awaits: 'index' });
3069
+ if (unindexed.length > 0 || waiting.length > 0)
3070
+ index(unindexed, waiting);
3071
+ const complete = pending.length === 0 && !('pending' in cover);
3072
+ const answering = complete
3073
+ ? filter
3074
+ : {
3075
+ ...filter,
3076
+ rooms: covering
3077
+ .filter(({ room }) => searchIndex.rooms().has(room))
3078
+ .map(({ room }) => room),
3079
+ };
3080
+ const answered = covered(answering);
3081
+ if ('pending' in answered)
3082
+ continue;
3083
+ const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
3084
+ const seen = searchIndex.changes();
3085
+ await searchIndex.caughtUp();
3086
+ const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(answering);
3087
+ if (selected !== null && 'pending' in selected)
3088
+ continue;
3089
+ if (searchIndex.changes() !== seen ||
3090
+ JSON.stringify(covered(answering)) !== JSON.stringify(answered))
3091
+ continue;
3092
+ const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, complete ? null : new Set(answering.rooms));
3093
+ const coverage = {
3094
+ rooms: answered.coverage.rooms,
3095
+ conversations: searchedConversations,
3096
+ index: searchIndex.generation(),
3097
+ overlay: answered.coverage.overlay,
3098
+ };
3099
+ return complete ? { ...hits, coverage } : { pending, ...hits, coverage };
3100
+ }
2183
3101
  }
2184
3102
  const watch = (topic, input, deliver) => {
2185
3103
  if (topic === 'room-drafts') {
@@ -2225,6 +3143,9 @@ export function composeRuntime(options) {
2225
3143
  presentationListeners.delete(listener);
2226
3144
  };
2227
3145
  }
3146
+ if (topic === 'readiness') {
3147
+ return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
3148
+ }
2228
3149
  if (topic === 'rooms') {
2229
3150
  roomListeners.add(deliver);
2230
3151
  deliver(listed());
@@ -2391,21 +3312,29 @@ export function composeRuntime(options) {
2391
3312
  return journal.export();
2392
3313
  },
2393
3314
  persistence() {
2394
- return options.shell.persistentStorage;
3315
+ return persistence === 'granted';
3316
+ },
3317
+ async retry() {
3318
+ journal.record('retry');
3319
+ unindexable.clear();
3320
+ queueReadiness();
3321
+ resume();
2395
3322
  },
2396
3323
  observe: observing(watch),
2397
3324
  async reauthorize() {
2398
3325
  if (refusal === null || !refusal.reauthorizationOffered)
2399
3326
  return;
2400
3327
  journal.record('connection reauthorize');
2401
- await chain.catch(() => undefined);
3328
+ await Promise.all([...lanes.values()]
3329
+ .filter((held) => held.generation === generation)
3330
+ .map((held) => held.chain));
2402
3331
  await mutations;
2403
3332
  authorization = undefined;
2404
3333
  discoveryPaused = false;
2405
3334
  if (options.adapter.roomStream === undefined)
2406
3335
  await sync('unstreamed');
2407
3336
  else
2408
- await task(ensureAuthorization);
3337
+ await task(OPENING_LANE, ensureAuthorization);
2409
3338
  },
2410
3339
  };
2411
3340
  const reachable = {
@@ -2416,6 +3345,21 @@ export function composeRuntime(options) {
2416
3345
  query: (plan) => rowsQuery(plan),
2417
3346
  watchPlan,
2418
3347
  search: searched,
3348
+ watchReadiness,
3349
+ };
3350
+ return {
3351
+ md,
3352
+ stop,
3353
+ lose: loseRooms,
3354
+ watch,
3355
+ acquire,
3356
+ persisted(answer) {
3357
+ if (answer === persistence)
3358
+ return;
3359
+ persistence = answer;
3360
+ journal.record(`persistence ${answer}`);
3361
+ queueReadiness();
3362
+ },
3363
+ within: (reach, port) => narrowed(reachable, reach, port),
2419
3364
  };
2420
- return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2421
3365
  }