@agentshouse/mdruntime 0.1.0-cd3ee807e46ae → 0.1.0-cd50a4607cb7d

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 (106) hide show
  1. package/AGENTS.md +247 -269
  2. package/dist/adapter.d.ts +25 -2
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/attempt.d.ts.map +1 -1
  5. package/dist/attempt.js +14 -1
  6. package/dist/bootstrap.d.ts +2 -2
  7. package/dist/bootstrap.d.ts.map +1 -1
  8. package/dist/bootstrap.js +28 -6
  9. package/dist/change-set.d.ts +2 -0
  10. package/dist/change-set.d.ts.map +1 -1
  11. package/dist/change-set.js +34 -9
  12. package/dist/chat/acts.d.ts +3 -4
  13. package/dist/chat/acts.d.ts.map +1 -1
  14. package/dist/chat/acts.js +10 -10
  15. package/dist/chat/list.d.ts +6 -0
  16. package/dist/chat/list.d.ts.map +1 -1
  17. package/dist/chat/list.js +6 -3
  18. package/dist/chat/markdown.d.ts +2 -1
  19. package/dist/chat/markdown.d.ts.map +1 -1
  20. package/dist/chat/markdown.js +7 -6
  21. package/dist/chat/message.d.ts.map +1 -1
  22. package/dist/chat/message.js +2 -2
  23. package/dist/chat/provider.d.ts.map +1 -1
  24. package/dist/chat/provider.js +25 -50
  25. package/dist/chat/ready.d.ts.map +1 -1
  26. package/dist/chat/ready.js +2 -2
  27. package/dist/chat/style.js +3 -3
  28. package/dist/client.d.ts +7 -7
  29. package/dist/client.d.ts.map +1 -1
  30. package/dist/client.js +1 -1
  31. package/dist/collection.d.ts +17 -3
  32. package/dist/collection.d.ts.map +1 -1
  33. package/dist/collection.js +230 -19
  34. package/dist/connection.d.ts +8 -5
  35. package/dist/connection.d.ts.map +1 -1
  36. package/dist/connection.js +866 -301
  37. package/dist/conversation.d.ts +12 -7
  38. package/dist/conversation.d.ts.map +1 -1
  39. package/dist/conversation.js +258 -130
  40. package/dist/entry.d.ts +17 -0
  41. package/dist/entry.d.ts.map +1 -0
  42. package/dist/entry.js +156 -0
  43. package/dist/fence.d.ts +6 -0
  44. package/dist/fence.d.ts.map +1 -0
  45. package/dist/fence.js +10 -0
  46. package/dist/host.d.ts +4 -2
  47. package/dist/host.d.ts.map +1 -1
  48. package/dist/host.js +9 -5
  49. package/dist/identity.d.ts +2 -0
  50. package/dist/identity.d.ts.map +1 -1
  51. package/dist/identity.js +6 -0
  52. package/dist/index.d.ts +5 -4
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +1 -0
  55. package/dist/memory.d.ts +1 -0
  56. package/dist/memory.d.ts.map +1 -1
  57. package/dist/memory.js +10 -1
  58. package/dist/model.d.ts +1 -0
  59. package/dist/model.d.ts.map +1 -1
  60. package/dist/model.js +9 -0
  61. package/dist/observation.d.ts +5 -2
  62. package/dist/observation.d.ts.map +1 -1
  63. package/dist/observation.js +22 -11
  64. package/dist/outcome.d.ts +4 -0
  65. package/dist/outcome.d.ts.map +1 -1
  66. package/dist/outcome.js +1 -0
  67. package/dist/overlay.d.ts +1 -0
  68. package/dist/overlay.d.ts.map +1 -1
  69. package/dist/overlay.js +33 -54
  70. package/dist/query.d.ts +28 -3
  71. package/dist/query.d.ts.map +1 -1
  72. package/dist/reach.js +3 -3
  73. package/dist/react.d.ts +3 -3
  74. package/dist/react.d.ts.map +1 -1
  75. package/dist/read-side.d.ts +13 -6
  76. package/dist/read-side.d.ts.map +1 -1
  77. package/dist/read-side.js +282 -83
  78. package/dist/recovery.d.ts +25 -0
  79. package/dist/recovery.d.ts.map +1 -0
  80. package/dist/recovery.js +197 -0
  81. package/dist/refresh.d.ts.map +1 -1
  82. package/dist/refresh.js +11 -10
  83. package/dist/replica.d.ts +1 -1
  84. package/dist/replica.d.ts.map +1 -1
  85. package/dist/replica.js +47 -19
  86. package/dist/rows.d.ts +4 -13
  87. package/dist/rows.d.ts.map +1 -1
  88. package/dist/rows.js +2 -12
  89. package/dist/save.d.ts +6 -2
  90. package/dist/save.d.ts.map +1 -1
  91. package/dist/save.js +42 -18
  92. package/dist/search.d.ts +28 -6
  93. package/dist/search.d.ts.map +1 -1
  94. package/dist/search.js +109 -44
  95. package/dist/shared.d.ts +11 -0
  96. package/dist/shared.d.ts.map +1 -0
  97. package/dist/shared.js +368 -0
  98. package/dist/status.d.ts +4 -0
  99. package/dist/status.d.ts.map +1 -1
  100. package/dist/store.d.ts +35 -10
  101. package/dist/store.d.ts.map +1 -1
  102. package/dist/store.js +275 -74
  103. package/dist/turn.d.ts +7 -0
  104. package/dist/turn.d.ts.map +1 -0
  105. package/dist/turn.js +45 -0
  106. package/package.json +6 -2
@@ -6,21 +6,25 @@ 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 { FencedMap } from './fence.js';
25
+ import { tally } from './shared.js';
26
+ import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
27
+ import { identityTime, roomDraftRevision } from './identity.js';
24
28
  import { beneath, normalize } from './paths.js';
25
29
  import { openRuntimeStore, } from './store.js';
26
30
  import { unconfirmed } from './transfer.js';
@@ -38,23 +42,59 @@ function declaredSelection(room) {
38
42
  root !== '' &&
39
43
  selection.roots.every((other, at) => at === index || !within(other, root)));
40
44
  }
45
+ const OPENING_LANE = 'opening';
46
+ const CONVERSATION_LANE = 'conversations';
47
+ const ROOM_READS = 256;
48
+ const ROOM_ATTEMPTS = 16;
49
+ const outside = new Map();
50
+ function outsideOf(roots) {
51
+ const key = JSON.stringify(roots);
52
+ const held = outside.get(key);
53
+ if (held !== undefined)
54
+ return held;
55
+ const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
56
+ const made = {
57
+ documents: (path) => (markdownPath(path) ? counted(path) : 0),
58
+ binaries: counted,
59
+ };
60
+ outside.set(key, made);
61
+ return made;
62
+ }
63
+ function abandonedOn(signal) {
64
+ return new Promise((resolve) => {
65
+ if (signal.aborted)
66
+ resolve(null);
67
+ else
68
+ signal.addEventListener('abort', () => resolve(null), { once: true });
69
+ });
70
+ }
71
+ function roomLane(locator) {
72
+ return `room ${locator}`;
73
+ }
41
74
  export function composeRuntime(options) {
42
75
  const localSaves = options.adapter.localSaves === true;
43
76
  let authorization = options.authorization;
44
77
  let opening;
45
78
  let store;
46
- const replicas = new Map();
47
- const queuesWithoutReplica = new Map();
79
+ const fences = new Map();
80
+ const isFenced = (locator) => fences.has(locator);
81
+ let fencings = 0;
82
+ const lastFenced = new Map();
83
+ const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
84
+ let discoveries = 0;
85
+ const replicas = new FencedMap(isFenced);
86
+ const queuesWithoutReplica = new FencedMap(isFenced);
48
87
  const pendings = new Map();
49
- const roomDrafts = new Map();
88
+ const roomDrafts = new FencedMap(isFenced);
50
89
  const presentations = new Map();
51
90
  let limits = [];
52
91
  let capabilities = [];
53
- const rooms = new Map();
92
+ const rooms = new FencedMap(isFenced);
54
93
  const counted = new Map();
55
94
  const unreadable = new Set();
56
- const models = new Map();
57
- const liveModels = new Map();
95
+ const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
96
+ const models = new FencedMap(isModelFenced);
97
+ const liveModels = new FencedMap(isModelFenced);
58
98
  const invalidModels = new Set();
59
99
  const discovered = new Map();
60
100
  let roomsListed = false;
@@ -84,7 +124,6 @@ export function composeRuntime(options) {
84
124
  let status = STARTING;
85
125
  let shown = STARTING;
86
126
  let discoveryPaused = false;
87
- let resetTimer;
88
127
  let closed = false;
89
128
  let stopping = false;
90
129
  let withdrawStream;
@@ -92,12 +131,21 @@ export function composeRuntime(options) {
92
131
  let underway = null;
93
132
  let streamed = null;
94
133
  let handed = false;
95
- const submitting = new Map();
134
+ const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
135
+ const repairing = new Set();
136
+ const repairable = new Map();
137
+ let outcomeRetention = null;
96
138
  const answering = new Map();
97
139
  let complete = false;
98
140
  let recorded = false;
99
141
  const stale = new Set();
100
- let chain = Promise.resolve();
142
+ let generation = 0;
143
+ let discovering = null;
144
+ let opened = null;
145
+ const owed = new Set();
146
+ const lanes = new Map();
147
+ let roomReads = { generation, free: ROOM_READS, waiting: [] };
148
+ const working = new Set();
101
149
  let mutations = Promise.resolve();
102
150
  const roomListeners = new Set();
103
151
  const refusalListeners = new Set();
@@ -108,7 +156,7 @@ export function composeRuntime(options) {
108
156
  const presentationListeners = new Set();
109
157
  const readSide = createReadSide();
110
158
  let withdrawConversations;
111
- let currencyQueued = false;
159
+ let currencyQueued = null;
112
160
  let socketOpen = false;
113
161
  let serviceMode = 'open';
114
162
  let haltedRefusal = false;
@@ -116,24 +164,31 @@ export function composeRuntime(options) {
116
164
  const serviceModeListeners = new Set();
117
165
  let member = null;
118
166
  const memberListeners = new Set();
119
- const materialized = new Set();
120
- let everyRoom = false;
121
- const transfers = new Map();
167
+ const prepared = new Set();
168
+ const projecting = new Set();
169
+ const demanded = new Set();
170
+ let preparing = false;
171
+ const catching = new Set();
172
+ const transfers = new FencedMap(isFenced);
122
173
  const conversations = createConversationStore({
123
174
  agents: declaredAgents,
124
- documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
175
+ documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
125
176
  call: (name, input) => invokeAction(name, input),
126
177
  durably,
127
178
  stream: () => options.adapter.conversations,
128
179
  publish: publishChangeSet,
180
+ online: () => !status.providerUnreachable && refusal === null,
129
181
  closed: () => closed,
182
+ unanswered: resendLater,
183
+ expired: (operation) => pastHorizon(operation),
130
184
  });
131
185
  const searchIndex = createSearchIndex({
132
- documents: () => readSide.documents(),
186
+ room: (room) => readSide.room(room),
133
187
  document: (room, path) => readSide.document(room, path),
134
- messages: () => conversations.messages(),
135
- message: (conversation, identity) => conversations.message(conversation, identity),
136
188
  });
189
+ const roomsToIndex = new Set();
190
+ const conversationsToIndex = new Set();
191
+ let indexing = false;
137
192
  const journal = createLog();
138
193
  let notifyReady = () => { };
139
194
  const ready = new Promise((resolve) => {
@@ -212,9 +267,16 @@ export function composeRuntime(options) {
212
267
  returned: structuredClone(record.returned),
213
268
  });
214
269
  }
270
+ for (const [room, refusal] of sourceRefusals)
271
+ attentionRequired.push({ room, refusal: structuredClone(refusal) });
215
272
  attentionRequired.sort((left, right) => {
216
273
  if (left.room !== right.room)
217
274
  return left.room < right.room ? -1 : 1;
275
+ if ('refusal' in left || 'refusal' in right) {
276
+ if ('refusal' in left && 'refusal' in right)
277
+ return 0;
278
+ return 'refusal' in left ? -1 : 1;
279
+ }
218
280
  if (left.path !== right.path)
219
281
  return left.path < right.path ? -1 : 1;
220
282
  return 0;
@@ -233,12 +295,9 @@ export function composeRuntime(options) {
233
295
  if (replica === undefined)
234
296
  return 0;
235
297
  const roots = replica.confirmed.record.typedSelection?.roots ?? [];
236
- let count = 0;
237
- for (const path of Object.keys(replica.visible.documents)) {
238
- if (markdownPath(path) && !roots.some((root) => beneath(root, path)))
239
- count += 1;
240
- }
241
- return count;
298
+ const weighed = outsideOf(roots);
299
+ return (tally(replica.visible.documents, weighed.documents) +
300
+ tally(replica.visible.binaries, weighed.binaries));
242
301
  }
243
302
  function listed() {
244
303
  return structuredClone([...rooms.values()].map((room) => {
@@ -265,6 +324,7 @@ export function composeRuntime(options) {
265
324
  return { listed: roomsListed, rooms: held };
266
325
  }
267
326
  function emitRooms() {
327
+ readSide.recover();
268
328
  const value = listed();
269
329
  for (const listener of roomListeners)
270
330
  listener(value);
@@ -319,42 +379,186 @@ export function composeRuntime(options) {
319
379
  }
320
380
  }
321
381
  function applyChangeSet(set) {
322
- const held = everyRoom
323
- ? set
324
- : { ...set, documents: set.documents.filter(({ locator }) => materialized.has(locator)) };
382
+ const held = {
383
+ ...set,
384
+ documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
385
+ };
325
386
  readSide.apply(held);
326
387
  searchIndex.changed(held);
327
388
  }
328
- function materialize(locators) {
329
- if (everyRoom)
389
+ async function referenced(locator) {
390
+ for (;;) {
391
+ const record = replicas.get(locator)?.confirmed.record;
392
+ if (record === undefined || closed)
393
+ return;
394
+ const held = { text: record.documents, binaries: record.binaries };
395
+ const composition = compositionOf(record);
396
+ if (referencesIndexed(held, composition))
397
+ return;
398
+ await indexReferrers(held, composition);
399
+ }
400
+ }
401
+ async function prepareReplica(locator) {
402
+ await referenced(locator);
403
+ const replica = replicas.get(locator);
404
+ if (replica === undefined || closed || prepared.has(locator))
330
405
  return;
331
- if (locators === null)
332
- everyRoom = true;
333
- const sets = [];
334
- for (const locator of locators ?? [...replicas.keys()]) {
335
- if (materialized.has(locator))
336
- continue;
337
- materialized.add(locator);
338
- const replica = replicas.get(locator);
339
- if (replica !== undefined)
340
- sets.push(changeSet(locator, null, replica));
406
+ const visible = replica.visible;
407
+ const paths = [
408
+ ...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
409
+ ];
410
+ const entered = [];
411
+ projecting.add(locator);
412
+ try {
413
+ await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
414
+ const current = replicas.get(locator);
415
+ if (current === undefined || closed)
416
+ return;
417
+ const set = entering(locator, current, chunk);
418
+ readSide.apply(set);
419
+ entered.push(set);
420
+ });
421
+ if (!replicas.has(locator) || closed)
422
+ return;
423
+ prepared.add(locator);
424
+ }
425
+ finally {
426
+ projecting.delete(locator);
341
427
  }
342
- if (sets.length === 0)
428
+ conversations.changed(mergeChangeSets(entered));
429
+ readSide.recover();
430
+ }
431
+ function unprepared() {
432
+ return [...demanded, ...replicas.keys()].find((locator) => !prepared.has(locator));
433
+ }
434
+ function prepare(locators = []) {
435
+ for (const locator of locators)
436
+ demanded.add(locator);
437
+ if (preparing)
343
438
  return;
344
- const set = mergeChangeSets(sets);
345
- readSide.apply(set);
346
- searchIndex.changed(set);
439
+ preparing = true;
440
+ void (async () => {
441
+ try {
442
+ for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
443
+ await yielded();
444
+ if (closed)
445
+ return;
446
+ demanded.delete(waiting);
447
+ await prepareReplica(waiting);
448
+ }
449
+ }
450
+ finally {
451
+ preparing = false;
452
+ }
453
+ })();
347
454
  }
348
455
  function reachedRooms(plan) {
349
- if (plan.rooms !== null)
350
- return plan.rooms;
351
- if (plan.reach === null)
352
- return null;
353
- return [...plan.reach.rooms, ...plan.reach.documents.map(roomOfKey)];
456
+ const reach = plan.reach;
457
+ const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
458
+ const named = plan.rooms ??
459
+ (reached === null
460
+ ? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
461
+ : [...reached]);
462
+ return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
463
+ }
464
+ function awaited(locator) {
465
+ return rooms.has(locator) || !roomsListed;
466
+ }
467
+ function covered(plan) {
468
+ const locators = reachedRooms(plan);
469
+ if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
470
+ return { pending: [] };
471
+ }
472
+ const pending = [];
473
+ const coverage = [];
474
+ const overlay = [];
475
+ for (const locator of locators) {
476
+ const replica = replicas.get(locator);
477
+ if (replica === undefined) {
478
+ if (awaited(locator))
479
+ pending.push({ room: locator, awaits: 'acquisition' });
480
+ continue;
481
+ }
482
+ if (!prepared.has(locator)) {
483
+ pending.push({ room: locator, awaits: 'preparation' });
484
+ continue;
485
+ }
486
+ coverage.push({
487
+ room: locator,
488
+ generation: replica.confirmed.record.transfer,
489
+ current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
490
+ });
491
+ for (const batch of replica.queue)
492
+ overlay.push(batch.id);
493
+ }
494
+ if (pending.length > 0)
495
+ return { pending };
496
+ return { coverage: { rooms: coverage, indexes: [], overlay } };
497
+ }
498
+ function awaitPreparation(cover) {
499
+ if (!('pending' in cover))
500
+ return;
501
+ const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
502
+ if (waiting.length > 0)
503
+ prepare(waiting.map(({ room }) => room));
504
+ }
505
+ async function rowsQuery(plan) {
506
+ const before = covered(plan);
507
+ awaitPreparation(before);
508
+ if ('pending' in before)
509
+ return before;
510
+ const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
511
+ awaitPreparation(cover);
512
+ if ('pending' in cover)
513
+ return cover;
514
+ return 'groups' in rows
515
+ ? { groups: rows.groups, coverage: cover.coverage }
516
+ : { documents: rows.documents, coverage: cover.coverage };
517
+ }
518
+ async function indexConversation(conversation) {
519
+ searchIndex.indexMessages(conversation, [], false);
520
+ let after = null;
521
+ for (;;) {
522
+ const batch = await durably((local) => local.messagesAfter(conversation, after));
523
+ if (closed)
524
+ return;
525
+ searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
526
+ if (batch.length === 0)
527
+ return;
528
+ after = batch[batch.length - 1].identity;
529
+ }
354
530
  }
355
- function rowsQuery(plan) {
356
- materialize(reachedRooms(plan));
357
- return readSide.query(plan);
531
+ function indexNext() {
532
+ const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator));
533
+ if (room !== undefined) {
534
+ roomsToIndex.delete(room);
535
+ return () => (prepared.has(room) ? searchIndex.indexRoom(room) : undefined);
536
+ }
537
+ const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
538
+ if (conversation === undefined)
539
+ return null;
540
+ conversationsToIndex.delete(conversation);
541
+ return () => indexConversation(conversation);
542
+ }
543
+ function index(rooms, named) {
544
+ for (const room of rooms)
545
+ roomsToIndex.add(room);
546
+ for (const conversation of named)
547
+ conversationsToIndex.add(conversation);
548
+ if (indexing)
549
+ return;
550
+ indexing = true;
551
+ void (async () => {
552
+ try {
553
+ for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
554
+ await yielded();
555
+ await unit();
556
+ }
557
+ }
558
+ finally {
559
+ indexing = false;
560
+ }
561
+ })().catch(() => { });
358
562
  }
359
563
  function publishChangeSet(set) {
360
564
  applyChangeSet(set);
@@ -371,34 +575,7 @@ export function composeRuntime(options) {
371
575
  emitPresentation(app);
372
576
  conversations.changed(set);
373
577
  }
374
- function stopTimer() {
375
- if (resetTimer === undefined)
376
- return;
377
- clearTimeout(resetTimer);
378
- resetTimer = undefined;
379
- }
380
- function armTimer() {
381
- stopTimer();
382
- if (closed || stopping)
383
- return;
384
- let earliest = null;
385
- for (const { submission } of pendings.values()) {
386
- if (submission.state !== 'reset')
387
- continue;
388
- const at = Date.parse(submission.reset);
389
- if (earliest === null || at < earliest)
390
- earliest = at;
391
- }
392
- if (earliest === null)
393
- return;
394
- // A Save House held back goes again only once the reset its answer named has passed.
395
- resetTimer = setTimeout(() => {
396
- resetTimer = undefined;
397
- submitAll();
398
- }, Math.max(0, earliest - Date.now()));
399
- }
400
578
  async function refuse(reason, provider = null) {
401
- stopTimer();
402
579
  discoveryPaused = true;
403
580
  try {
404
581
  if (reason === 'revoked-authorization') {
@@ -477,14 +654,24 @@ export function composeRuntime(options) {
477
654
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
478
655
  });
479
656
  }
480
- async function applyDiscovery(listed, local) {
657
+ async function applyDiscovery(found, started) {
658
+ for (const room of found) {
659
+ const fence = fences.get(room.locator);
660
+ if (fence !== undefined && fence.erased && fence.at < started)
661
+ fences.delete(room.locator);
662
+ }
663
+ const listed = found.filter((room) => !fences.has(room.locator));
481
664
  const listedLocators = new Set(listed.map((room) => room.locator));
482
665
  for (const locator of listedLocators)
483
666
  scopedRefusals.delete(locator);
484
667
  discovered.clear();
485
668
  for (const room of listed)
486
669
  discovered.set(room.locator, room);
487
- await dropUnlisted(listedLocators, local);
670
+ const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
671
+ if (options.adapter.exhaustiveDiscovery === true)
672
+ await loseRooms(unlisted);
673
+ else
674
+ unlistedKept(unlisted);
488
675
  for (const room of listed) {
489
676
  const held = replicas.get(room.locator)?.confirmed.record;
490
677
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -495,6 +682,7 @@ export function composeRuntime(options) {
495
682
  }
496
683
  roomsListed = true;
497
684
  emitRooms();
685
+ return listed;
498
686
  }
499
687
  function unlistedHeld(listed) {
500
688
  return [
@@ -506,41 +694,71 @@ export function composeRuntime(options) {
506
694
  ]),
507
695
  ].filter((locator) => !listed.has(locator));
508
696
  }
509
- async function dropUnlisted(listed, local) {
510
- if (unlistedHeld(listed).length === 0)
511
- return;
512
- await exclusive(async () => {
513
- const unlisted = unlistedHeld(listed);
514
- await local.dropRooms(unlisted);
515
- const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
516
- room: true,
517
- queue: true,
518
- drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
519
- }));
520
- for (const locator of unlisted) {
521
- for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
522
- pendings.delete(batch.id);
523
- replicas.delete(locator);
524
- queuesWithoutReplica.delete(locator);
697
+ function unlistedKept(unlisted) {
698
+ for (const locator of unlisted) {
699
+ const held = replicas.get(locator)?.confirmed.record;
700
+ if (held !== undefined)
701
+ list(held, false);
702
+ else
525
703
  rooms.delete(locator);
526
- roomDrafts.delete(locator);
527
- transfers.delete(locator);
528
- unreadable.delete(locator);
529
- stale.delete(locator);
530
- scopedRefusals.delete(locator);
531
- sourceRefusals.delete(locator);
532
- declarationRefusals.delete(locator);
533
- for (const attached of ['collection', 'typed-selection']) {
534
- const key = modelKey(locator, attached);
535
- models.delete(key);
536
- liveModels.delete(key);
537
- invalidModels.delete(key);
538
- }
539
- journal.record(`room unlisted ${locator}`);
704
+ }
705
+ }
706
+ function fence(locators) {
707
+ const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
708
+ const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
709
+ room: true,
710
+ queue: true,
711
+ drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
712
+ }));
713
+ for (const locator of fresh) {
714
+ fences.set(locator, { at: discoveries, erased: false });
715
+ lastFenced.set(locator, ++fencings);
716
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
717
+ pendings.delete(batch.id);
718
+ replicas.delete(locator);
719
+ queuesWithoutReplica.delete(locator);
720
+ rooms.delete(locator);
721
+ roomDrafts.delete(locator);
722
+ transfers.delete(locator);
723
+ catching.delete(locator);
724
+ discovered.delete(locator);
725
+ announced.delete(locator);
726
+ unreadable.delete(locator);
727
+ stale.delete(locator);
728
+ repairable.delete(locator);
729
+ scopedRefusals.delete(locator);
730
+ sourceRefusals.delete(locator);
731
+ declarationRefusals.delete(locator);
732
+ for (const attached of ['collection', 'typed-selection']) {
733
+ const key = modelKey(locator, attached);
734
+ models.delete(key);
735
+ liveModels.delete(key);
736
+ invalidModels.delete(key);
540
737
  }
541
- publishChangeSet(mergeChangeSets(removed));
542
- moveTo({ ...status, ...attention() });
543
- });
738
+ journal.record(`room lost ${locator}`);
739
+ }
740
+ if (fresh.length === 0)
741
+ return fresh;
742
+ publishChangeSet(mergeChangeSets(removed));
743
+ for (const locator of fresh)
744
+ prepared.delete(locator);
745
+ searchIndex.forget(fresh);
746
+ readSide.recover();
747
+ moveTo({ ...status, ...attention() });
748
+ return fresh;
749
+ }
750
+ async function erased(local, locators) {
751
+ await local.erase(locators);
752
+ for (const locator of locators) {
753
+ const held = fences.get(locator);
754
+ if (held !== undefined)
755
+ held.erased = true;
756
+ }
757
+ }
758
+ async function loseRooms(locators) {
759
+ const fresh = fence(locators);
760
+ if (fresh.length > 0)
761
+ await durably((local) => erased(local, fresh));
544
762
  }
545
763
  function restored(key, persisted) {
546
764
  const prior = models.get(key);
@@ -635,6 +853,7 @@ export function composeRuntime(options) {
635
853
  async function readState(opened) {
636
854
  const { lastCompleted, locators, read } = await hydrate(opened);
637
855
  applyChangeSet(read);
856
+ prepare();
638
857
  if (replicas.size > 0)
639
858
  emitRooms();
640
859
  if (member !== null)
@@ -645,8 +864,15 @@ export function composeRuntime(options) {
645
864
  return lastCompleted;
646
865
  }
647
866
  async function open() {
648
- const opened = await openRuntimeStore();
867
+ if (closed)
868
+ throw new Error('runtime closed');
869
+ const opened = await openRuntimeStore(isFenced);
649
870
  store = opened;
871
+ const interrupted = await opened.fences();
872
+ for (const locator of interrupted) {
873
+ fences.set(locator, { at: discoveries, erased: false });
874
+ lastFenced.set(locator, ++fencings);
875
+ }
650
876
  const kept = options.shell.generation;
651
877
  const storedGeneration = await opened.generation();
652
878
  const loss = detectStorageLoss(storedGeneration, kept);
@@ -660,6 +886,8 @@ export function composeRuntime(options) {
660
886
  emitStorageLoss();
661
887
  }
662
888
  const lastCompleted = await readState(opened);
889
+ if (interrupted.length > 0)
890
+ await erased(opened, interrupted);
663
891
  moveTo({ ...status, lastCompleted, ...attention() });
664
892
  return opened;
665
893
  }
@@ -741,17 +969,19 @@ export function composeRuntime(options) {
741
969
  moveTo({ ...status, ...attention() });
742
970
  throw error;
743
971
  }
744
- queuesWithoutReplica.delete(locator);
745
- for (const { batch, returned } of unqueued) {
746
- const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
747
- journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
748
- }
749
- replicas.set(locator, replicaOf(confirmed, kept, before));
750
- return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
751
- room: settling.room,
752
- queue: kept.length !== previousQueue.length,
753
- drafts: [...written.keys()],
754
- });
972
+ return () => {
973
+ queuesWithoutReplica.delete(locator);
974
+ for (const { batch, returned } of unqueued) {
975
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
976
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
977
+ }
978
+ replicas.set(locator, replicaOf(confirmed, kept, before));
979
+ return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
980
+ room: settling.room,
981
+ queue: kept.length !== previousQueue.length,
982
+ drafts: [...written.keys()],
983
+ });
984
+ };
755
985
  }
756
986
  function adoptModels(locator, record) {
757
987
  for (const [attached, model] of [
@@ -784,11 +1014,16 @@ export function composeRuntime(options) {
784
1014
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
785
1015
  };
786
1016
  }
787
- function publish(local, room, confirmed, landing = {}) {
1017
+ function publish(local, room, confirmed, landing, live) {
788
1018
  const { base, stored } = landing;
789
1019
  return exclusive(async () => {
790
- if (!continuing())
1020
+ if (stored !== undefined) {
1021
+ await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
1022
+ }
1023
+ if (!live()) {
1024
+ caughtUpTo(room.locator);
791
1025
  return true;
1026
+ }
792
1027
  const prior = replicas.get(room.locator);
793
1028
  if (base !== undefined &&
794
1029
  (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
@@ -802,9 +1037,11 @@ export function composeRuntime(options) {
802
1037
  const accepted = pendings.get(batch.id)?.accepted;
803
1038
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
804
1039
  });
805
- const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
806
- if (stored !== undefined)
1040
+ const set = (await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) }))();
1041
+ if (stored !== undefined) {
807
1042
  transfers.delete(room.locator);
1043
+ catching.delete(room.locator);
1044
+ }
808
1045
  for (const batch of accepted)
809
1046
  pendings.delete(batch.id);
810
1047
  stale.delete(room.locator);
@@ -813,13 +1050,17 @@ export function composeRuntime(options) {
813
1050
  sourceRefusals.delete(room.locator);
814
1051
  declarationRefusals.delete(room.locator);
815
1052
  list(room, true);
1053
+ if (prior === undefined)
1054
+ prepared.add(room.locator);
816
1055
  publishChangeSet(set);
817
1056
  moveTo({ ...status, ...attention() });
1057
+ options.adapter.roomStream?.published(room.locator, roomVersion);
818
1058
  return true;
819
1059
  });
820
1060
  }
821
1061
  async function saveRoomDrafts(locator, named, writtenBy) {
822
1062
  const local = await openLocal();
1063
+ await referenced(locator);
823
1064
  const replica = replicas.get(locator);
824
1065
  const room = rooms.get(locator);
825
1066
  if (replica === undefined || room === undefined) {
@@ -867,6 +1108,11 @@ export function composeRuntime(options) {
867
1108
  changes,
868
1109
  bases: parts.flatMap(({ bases }) => bases),
869
1110
  writtenBy,
1111
+ clean: {
1112
+ text: replica.confirmed.record.documents,
1113
+ binaries: replica.confirmed.record.binaries,
1114
+ composition: compositionOf(replica.confirmed.record),
1115
+ },
870
1116
  };
871
1117
  const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
872
1118
  if (checked?.kind === 'refused')
@@ -937,6 +1183,8 @@ export function composeRuntime(options) {
937
1183
  catch {
938
1184
  return { outcome: 'refused', code: 'durable-write', retryable: true };
939
1185
  }
1186
+ if (fences.has(locator))
1187
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
940
1188
  for (const origin of withdrawn.keys())
941
1189
  pendings.delete(origin);
942
1190
  for (const [id, drafts] of drafted) {
@@ -966,6 +1214,8 @@ export function composeRuntime(options) {
966
1214
  return;
967
1215
  }
968
1216
  await local.submit(locator, identifier, submission, lost);
1217
+ if (fences.has(locator))
1218
+ return;
969
1219
  pendings.set(identifier, { ...state, submission, lost });
970
1220
  const replica = replicas.get(locator);
971
1221
  if (replica !== undefined)
@@ -976,6 +1226,8 @@ export function composeRuntime(options) {
976
1226
  if (!continuing())
977
1227
  throw new Error('connection refused');
978
1228
  await local.keepUpload(locator, identifier, upload);
1229
+ if (fences.has(locator))
1230
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
979
1231
  pendings.set(identifier, { ...pendings.get(identifier), upload });
980
1232
  if (!continuing())
981
1233
  throw new Error('connection refused');
@@ -991,6 +1243,7 @@ export function composeRuntime(options) {
991
1243
  }
992
1244
  if (closed || authorization === undefined || refusal !== null)
993
1245
  return UNCERTAIN;
1246
+ const since = fencings;
994
1247
  const transfer = ['transfer', 'history', locator, path];
995
1248
  if (query.revision !== undefined)
996
1249
  transfer.push(query.revision);
@@ -1005,6 +1258,8 @@ export function composeRuntime(options) {
1005
1258
  ...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
1006
1259
  });
1007
1260
  journal.record(`outcome history ${result.kind}`);
1261
+ if (fencedSince(locator, since))
1262
+ return failure({ kind: 'room-not-readable', locator });
1008
1263
  if (result.kind === 'scoped-refusal') {
1009
1264
  const room = replicas.get(result.locator)?.confirmed.record;
1010
1265
  if (room !== undefined)
@@ -1027,6 +1282,12 @@ export function composeRuntime(options) {
1027
1282
  }
1028
1283
  return null;
1029
1284
  }
1285
+ function binaryRoom(request) {
1286
+ if (request.kind === 'path')
1287
+ return request.locator;
1288
+ const scope = options.adapter.referrerScope(request.referrer);
1289
+ return scope?.kind === 'room' ? scope.locator : null;
1290
+ }
1030
1291
  async function readBinary(request) {
1031
1292
  if (closed || refusal !== null)
1032
1293
  return UNCERTAIN;
@@ -1038,12 +1299,16 @@ export function composeRuntime(options) {
1038
1299
  }
1039
1300
  if (closed || authorization === undefined || refusal !== null)
1040
1301
  return UNCERTAIN;
1302
+ const since = fencings;
1041
1303
  journal.record(request.kind === 'path'
1042
1304
  ? `transfer binary ${request.locator} ${request.path}`
1043
1305
  : `transfer binary ${request.fileRef}`);
1044
1306
  try {
1045
1307
  const result = await options.adapter.binary(authorization, request);
1046
1308
  journal.record(`outcome binary ${result.kind}`);
1309
+ const locator = binaryRoom(request);
1310
+ if (locator !== null && fencedSince(locator, since))
1311
+ return failure({ kind: 'room-not-readable', locator });
1047
1312
  if (result.kind === 'scoped-refusal') {
1048
1313
  const room = replicas.get(result.locator)?.confirmed.record;
1049
1314
  if (room !== undefined)
@@ -1167,11 +1432,11 @@ export function composeRuntime(options) {
1167
1432
  options.adapter.reached(locator, written, roomVersion) &&
1168
1433
  !options.adapter.reached(locator, roomVersion, written));
1169
1434
  }
1170
- async function pullRefresh(authorized, confirmed) {
1171
- if (!continuing())
1435
+ async function pullRefresh(authorized, confirmed, live) {
1436
+ if (!live())
1172
1437
  return unconfirmed;
1173
1438
  try {
1174
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
1439
+ const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1175
1440
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
1176
1441
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
1177
1442
  return result;
@@ -1181,47 +1446,69 @@ export function composeRuntime(options) {
1181
1446
  throw error;
1182
1447
  }
1183
1448
  }
1184
- function staging(local) {
1449
+ function staging(local, live) {
1185
1450
  return {
1186
1451
  held: (locator) => transfers.get(locator) ?? null,
1187
1452
  staged: (record) => local.staged(record),
1188
1453
  stage: (record, page) => exclusive(async () => {
1189
- if (!continuing())
1454
+ if (!live())
1190
1455
  return false;
1191
1456
  await local.stage(record, page);
1192
1457
  transfers.set(record.locator, record);
1458
+ readSide.recover();
1193
1459
  return true;
1194
1460
  }),
1195
1461
  abandon: (locator) => exclusive(async () => {
1196
- if (!continuing())
1462
+ if (!live())
1197
1463
  return;
1198
1464
  await local.abandon(locator);
1199
1465
  transfers.delete(locator);
1466
+ readSide.recover();
1200
1467
  }),
1201
1468
  };
1202
1469
  }
1203
- async function pullBootstrap(authorized, room, local) {
1204
- if (!continuing())
1470
+ function reusable(room) {
1471
+ const record = replicas.get(room.locator)?.confirmed.record;
1472
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1473
+ return null;
1474
+ return { documents: record.documents, revisions: record.revisions };
1475
+ }
1476
+ async function pullBootstrap(authorized, room, local, live) {
1477
+ if (!live())
1205
1478
  return unconfirmed;
1479
+ if (replicas.has(room.locator)) {
1480
+ catching.add(room.locator);
1481
+ readSide.recover();
1482
+ }
1206
1483
  try {
1207
1484
  const result = await bootstrapRoom(options.adapter, authorized, room, {
1208
1485
  collection: modelOf(room.locator),
1209
1486
  selection: modelOf(room.locator, 'typed-selection'),
1210
- }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
1487
+ }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
1211
1488
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
1212
1489
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1490
+ if (result.kind !== 'staged')
1491
+ caughtUpTo(room.locator);
1213
1492
  return result;
1214
1493
  }
1215
1494
  catch (error) {
1216
1495
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
1496
+ caughtUpTo(room.locator);
1217
1497
  throw error;
1218
1498
  }
1219
1499
  }
1220
- async function advance(room, local, authorized) {
1500
+ function caughtUpTo(locator) {
1501
+ if (catching.delete(locator))
1502
+ readSide.recover();
1503
+ }
1504
+ async function advance(room, local, authorized, live) {
1505
+ await referenced(room.locator);
1506
+ if (!live())
1507
+ return unconfirmed;
1221
1508
  const held = replicas.get(room.locator);
1222
1509
  if (held !== undefined) {
1223
- const refreshed = await pullRefresh(authorized, held.confirmed);
1224
- if (!continuing())
1510
+ const refreshed = await pullRefresh(authorized, held.confirmed, live);
1511
+ if (!live())
1225
1512
  return unconfirmed;
1226
1513
  if (refreshed.kind === 'still-current') {
1227
1514
  stale.delete(room.locator);
@@ -1235,31 +1522,45 @@ export function composeRuntime(options) {
1235
1522
  return null;
1236
1523
  }
1237
1524
  if (refreshed.kind === 'advanced') {
1238
- if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
1525
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
1239
1526
  const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
1240
1527
  const reached = now !== undefined &&
1241
1528
  options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
1242
- return reached ? null : advance(room, local, authorized);
1529
+ return reached ? null : advance(room, local, authorized, live);
1243
1530
  }
1244
1531
  await retainModel(local, room, 'collection', modelOf(room.locator));
1245
1532
  return null;
1246
1533
  }
1247
1534
  if (refreshed.kind !== 'beyond-horizon')
1248
1535
  return refreshed;
1536
+ if (opened !== null) {
1537
+ owed.add(room.locator);
1538
+ readSide.recover();
1539
+ return unconfirmed;
1540
+ }
1249
1541
  }
1250
- const bootstrapped = await pullBootstrap(authorized, room, local);
1251
- if (!continuing())
1542
+ const bootstrapped = await pullBootstrap(authorized, room, local, live);
1543
+ if (!live())
1252
1544
  return unconfirmed;
1253
1545
  if (bootstrapped.kind !== 'staged')
1254
1546
  return bootstrapped;
1255
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1547
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
1256
1548
  return null;
1257
1549
  }
1258
1550
  async function withhold(withheld, room, local, progress) {
1551
+ if (withheld.kind === 'unconfirmed')
1552
+ return;
1259
1553
  if (withheld.kind === 'refused')
1260
1554
  stale.delete(room.locator);
1261
1555
  else
1262
1556
  stale.add(room.locator);
1557
+ if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
1558
+ const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
1559
+ repairable.set(room.locator, reset);
1560
+ repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
1561
+ }
1562
+ else
1563
+ repairable.delete(room.locator);
1263
1564
  if (withheld.kind === 'scoped-refusal') {
1264
1565
  await scoped(withheld, room, local);
1265
1566
  }
@@ -1295,6 +1596,7 @@ export function composeRuntime(options) {
1295
1596
  unreadable.add(room.locator);
1296
1597
  list(room, false);
1297
1598
  emitRooms();
1599
+ moveTo({ ...status, ...attention() });
1298
1600
  journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
1299
1601
  }
1300
1602
  function leaving(batch, result) {
@@ -1316,6 +1618,7 @@ export function composeRuntime(options) {
1316
1618
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1317
1619
  }
1318
1620
  async function adoptAccepted(room, local, submitted, accepted) {
1621
+ await referenced(room.locator);
1319
1622
  if (!continuing())
1320
1623
  return unconfirmed;
1321
1624
  const current = replicas.get(room.locator);
@@ -1332,16 +1635,17 @@ export function composeRuntime(options) {
1332
1635
  if (applied.kind !== 'staged') {
1333
1636
  const state = pendings.get(submitted.id);
1334
1637
  await local.accept(room.locator, submitted.id, accepted.roomVersion);
1335
- if (state !== undefined)
1638
+ if (state !== undefined && !fences.has(room.locator))
1336
1639
  pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1337
1640
  emitQueue(room.locator);
1338
1641
  return applied;
1339
1642
  }
1340
- const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1643
+ const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1341
1644
  pendings.delete(submitted.id);
1342
1645
  unreadable.delete(room.locator);
1343
1646
  sourceRefusals.delete(room.locator);
1344
1647
  declarationRefusals.delete(room.locator);
1648
+ const set = settled();
1345
1649
  list(room, true);
1346
1650
  publishChangeSet(set);
1347
1651
  moveTo({ ...status, ...attention() });
@@ -1355,14 +1659,14 @@ export function composeRuntime(options) {
1355
1659
  refusedSource(room, failed);
1356
1660
  return false;
1357
1661
  }
1358
- async function submitRoom(room, local, authorized, progress) {
1359
- while (continuing() && authorization !== undefined) {
1662
+ async function submitRoom(room, local, authorized, progress, signal) {
1663
+ while (continuing() && authorization !== undefined && !signal.aborted) {
1360
1664
  const replica = replicas.get(room.locator);
1361
1665
  const head = replica?.queue[0];
1362
1666
  if (replica === undefined || head === undefined)
1363
- return;
1667
+ return 'done';
1364
1668
  if (pendings.get(head.id)?.accepted !== undefined)
1365
- return;
1669
+ return 'done';
1366
1670
  const state = pendings.get(head.id);
1367
1671
  const { submission, lost: earlier } = state ?? {
1368
1672
  submission: { state: 'waiting' },
@@ -1370,24 +1674,30 @@ export function composeRuntime(options) {
1370
1674
  };
1371
1675
  if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
1372
1676
  progress.complete = false;
1373
- return;
1677
+ return { at: Date.parse(submission.reset) };
1374
1678
  }
1375
1679
  if (writesHeld()) {
1376
1680
  progress.complete = false;
1377
1681
  journal.record(`submission ${room.locator} ${head.id} held`);
1378
- return;
1682
+ return 'done';
1379
1683
  }
1380
1684
  const lost = earlier || submission.state === 'submitted';
1685
+ if (lost && pastHorizon(head.id)) {
1686
+ await discardPending(room, local, signal);
1687
+ return 'done';
1688
+ }
1381
1689
  let result;
1382
1690
  journal.record(`submission ${room.locator} ${head.id}`);
1383
1691
  await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1384
- if (discoveryPaused)
1385
- return;
1386
- if (writesHeld()) {
1692
+ if (discoveryPaused || fences.has(room.locator))
1693
+ return 'done';
1694
+ if (signal.aborted || writesHeld()) {
1387
1695
  await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
1696
+ if (signal.aborted)
1697
+ return 'done';
1388
1698
  progress.complete = false;
1389
1699
  journal.record(`submission ${room.locator} ${head.id} held`);
1390
- return;
1700
+ return 'done';
1391
1701
  }
1392
1702
  const opensBefore = openSignals;
1393
1703
  const answered = awaitingAnswer(room.locator);
@@ -1403,32 +1713,36 @@ export function composeRuntime(options) {
1403
1713
  progress.complete = false;
1404
1714
  if (continuing())
1405
1715
  progress.reachable = false;
1406
- return;
1716
+ return 'again';
1717
+ }
1718
+ if (signal.aborted) {
1719
+ answered();
1720
+ return 'done';
1407
1721
  }
1408
1722
  if (result.kind !== 'accepted' || !continuing())
1409
1723
  answered();
1410
1724
  if (!continuing())
1411
- return;
1725
+ return 'done';
1412
1726
  if (result.kind === 'transport-uncertainty') {
1413
1727
  progress.complete = false;
1414
1728
  progress.reachable = false;
1415
- return;
1729
+ return 'again';
1416
1730
  }
1417
1731
  if (result.kind === 'room-not-readable') {
1418
1732
  progress.complete = false;
1419
1733
  unreadable.add(room.locator);
1420
1734
  list(room, false);
1421
1735
  emitRooms();
1422
- return;
1736
+ return 'done';
1423
1737
  }
1424
1738
  if (result.kind === 'revoked-authorization') {
1425
1739
  await refuse('revoked-authorization');
1426
- return;
1740
+ return 'done';
1427
1741
  }
1428
1742
  if (result.kind === 'scoped-refusal') {
1429
1743
  await scoped(result, room, local);
1430
1744
  progress.complete = false;
1431
- return;
1745
+ return 'done';
1432
1746
  }
1433
1747
  if (result.kind === 'write-halted') {
1434
1748
  journal.record(`outcome ${room.locator} ${head.id} write-halted`);
@@ -1437,16 +1751,24 @@ export function composeRuntime(options) {
1437
1751
  continue;
1438
1752
  haltedRefusal = true;
1439
1753
  progress.complete = false;
1440
- return;
1754
+ return 'done';
1441
1755
  }
1442
1756
  if (result.kind === 'conflicted' ||
1443
1757
  result.kind === 'rejected' ||
1444
1758
  result.kind === 'expired' ||
1445
1759
  result.kind === 'retryable-refusal') {
1760
+ if (result.kind === 'expired' && lost && result.restored !== true) {
1761
+ journal.record(`outcome ${room.locator} ${head.id} expired`);
1762
+ await discardPending(room, local, signal);
1763
+ return 'done';
1764
+ }
1446
1765
  if (result.kind === 'expired') {
1447
- const withheld = await advance(room, local, authorized);
1448
- if (withheld !== null)
1449
- return withhold(withheld, room, local, progress);
1766
+ await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
1767
+ const withheld = await advance(room, local, authorized, continuing);
1768
+ if (withheld !== null) {
1769
+ await withhold(withheld, room, local, progress);
1770
+ return 'done';
1771
+ }
1450
1772
  }
1451
1773
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
1452
1774
  (result.kind === 'conflicted' && result.roomVersion !== undefined
@@ -1459,8 +1781,7 @@ export function composeRuntime(options) {
1459
1781
  const reset = result.reset;
1460
1782
  progress.complete = false;
1461
1783
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1462
- armTimer();
1463
- return;
1784
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
1464
1785
  }
1465
1786
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
1466
1787
  const left = leaving(head, result);
@@ -1471,37 +1792,82 @@ export function composeRuntime(options) {
1471
1792
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
1472
1793
  return;
1473
1794
  }
1474
- const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1795
+ const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1475
1796
  leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1476
1797
  (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1477
1798
  ? { ...left, returned: null }
1478
1799
  : left,
1479
1800
  });
1480
- publishChangeSet(set);
1801
+ publishChangeSet(settled());
1481
1802
  moveTo({ ...status, ...attention() });
1482
1803
  });
1483
1804
  if (result.kind === 'expired')
1484
1805
  continue;
1485
- const withheld = await advance(room, local, authorized);
1486
- if (withheld !== null)
1487
- return withhold(withheld, room, local, progress);
1806
+ const withheld = await advance(room, local, authorized, continuing);
1807
+ if (withheld !== null) {
1808
+ await withhold(withheld, room, local, progress);
1809
+ return 'done';
1810
+ }
1488
1811
  continue;
1489
1812
  }
1490
1813
  const kept = await adopted(room, local, head, result).finally(answered);
1491
1814
  if (!kept) {
1492
1815
  progress.complete = false;
1493
- return;
1816
+ return 'done';
1494
1817
  }
1495
1818
  }
1819
+ return 'done';
1820
+ }
1821
+ function pastHorizon(identity) {
1822
+ return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
1823
+ }
1824
+ async function discardPending(room, local, signal) {
1825
+ const { locator } = room;
1826
+ const discarded = await exclusive(async () => {
1827
+ if (fences.has(locator))
1828
+ return false;
1829
+ const before = replicas.get(locator);
1830
+ const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
1831
+ fences.set(locator, { at: discoveries, erased: false });
1832
+ lastFenced.set(locator, ++fencings);
1833
+ try {
1834
+ await local.discard(locator);
1835
+ }
1836
+ finally {
1837
+ fences.delete(locator);
1838
+ }
1839
+ for (const batch of queue)
1840
+ pendings.delete(batch.id);
1841
+ queuesWithoutReplica.delete(locator);
1842
+ const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
1843
+ if (after !== null)
1844
+ replicas.set(locator, after);
1845
+ journal.record(`room discarded ${locator}`);
1846
+ publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
1847
+ moveTo({ ...status, ...attention() });
1848
+ return true;
1849
+ });
1850
+ if (!discarded)
1851
+ return;
1852
+ const progress = { complete: true, reachable: true };
1853
+ await roomTask(locator, async (live) => {
1854
+ await Promise.race([
1855
+ reload(room, local, progress, () => live() && !signal.aborted),
1856
+ abandonedOn(signal),
1857
+ ]);
1858
+ });
1859
+ if (signal.aborted)
1860
+ return;
1861
+ await rest(progress);
1496
1862
  }
1497
1863
  function awaitingAnswer(locator) {
1498
1864
  let release = () => { };
1499
1865
  const awaited = new Promise((resolve) => {
1500
1866
  release = resolve;
1501
1867
  });
1502
- answering.set(locator, awaited);
1868
+ answering.set(locator, { generation, answered: awaited });
1503
1869
  return () => {
1504
- if (answering.get(locator) === awaited)
1870
+ if (answering.get(locator)?.answered === awaited)
1505
1871
  answering.delete(locator);
1506
1872
  release();
1507
1873
  };
@@ -1617,8 +1983,10 @@ export function composeRuntime(options) {
1617
1983
  ];
1618
1984
  });
1619
1985
  }
1620
- async function synchronize(listed, local, progress, attempt) {
1986
+ async function synchronize(listed, local, progress, attempt, live) {
1621
1987
  const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1988
+ if (!live())
1989
+ return;
1622
1990
  const ready = listed.filter((_, index) => {
1623
1991
  const outcome = prepared[index];
1624
1992
  return outcome?.status === 'fulfilled' && outcome.value;
@@ -1627,9 +1995,10 @@ export function composeRuntime(options) {
1627
1995
  options.adapter.roomStream?.hold(heldRooms(ready));
1628
1996
  handed = true;
1629
1997
  }
1998
+ discovering?.release();
1630
1999
  const loaded = await Promise.allSettled(ready
1631
2000
  .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1632
- .map((room) => loadRoom(room, local, progress)));
2001
+ .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
1633
2002
  const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1634
2003
  if (failed !== undefined)
1635
2004
  throw failed.reason;
@@ -1658,14 +2027,14 @@ export function composeRuntime(options) {
1658
2027
  }
1659
2028
  return true;
1660
2029
  }
1661
- async function caughtUp(room, local, progress) {
2030
+ async function caughtUp(room, local, progress, live) {
1662
2031
  const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1663
- if (held === undefined || authorization === undefined)
2032
+ if (held === undefined || authorization === undefined || !live())
1664
2033
  return;
1665
2034
  const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1666
2035
  if (!behind)
1667
2036
  return;
1668
- const withheld = await advance(room, local, authorization);
2037
+ const withheld = await advance(room, local, authorization, live);
1669
2038
  if (withheld !== null)
1670
2039
  await withhold(withheld, room, local, progress);
1671
2040
  }
@@ -1675,39 +2044,46 @@ export function composeRuntime(options) {
1675
2044
  return model.status === 'unavailable' && model.reason === 'revoked';
1676
2045
  });
1677
2046
  }
1678
- async function loadRoom(room, local, progress) {
1679
- if (discoveryPaused || authorization === undefined)
2047
+ async function loadRoom(room, local, progress, live) {
2048
+ if (!live() || authorization === undefined)
1680
2049
  return;
1681
2050
  if (replicas.has(room.locator) && revoked(room.locator)) {
1682
- await reload(room, local, progress);
2051
+ await reload(room, local, progress, live);
1683
2052
  return;
1684
2053
  }
1685
- const withheld = await advance(room, local, authorization);
2054
+ const withheld = await advance(room, local, authorization, live);
1686
2055
  if (withheld !== null) {
1687
2056
  await withhold(withheld, room, local, progress);
1688
2057
  return;
1689
2058
  }
1690
- await caughtUp(room, local, progress);
2059
+ await caughtUp(room, local, progress, live);
1691
2060
  }
1692
- async function reload(room, local, progress) {
1693
- if (discoveryPaused || authorization === undefined)
1694
- return;
1695
- const bootstrapped = await pullBootstrap(authorization, room, local);
1696
- if (!continuing())
1697
- return;
1698
- if (bootstrapped.kind !== 'staged') {
1699
- await withhold(bootstrapped, room, local, progress);
1700
- return;
2061
+ async function reload(room, local, progress, live) {
2062
+ try {
2063
+ if (!live() || authorization === undefined)
2064
+ return;
2065
+ const bootstrapped = await pullBootstrap(authorization, room, local, live);
2066
+ if (!live())
2067
+ return;
2068
+ if (bootstrapped.kind !== 'staged') {
2069
+ await withhold(bootstrapped, room, local, progress);
2070
+ return;
2071
+ }
2072
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
2073
+ await caughtUp(room, local, progress, live);
2074
+ }
2075
+ finally {
2076
+ caughtUpTo(room.locator);
1701
2077
  }
1702
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1703
- await caughtUp(room, local, progress);
1704
2078
  }
1705
- async function discoverRooms(local, progress, attempt) {
2079
+ async function discoverRooms(local, progress, attempt, live) {
1706
2080
  if (closed || authorization === undefined) {
1707
2081
  notifyReady();
1708
2082
  return;
1709
2083
  }
1710
2084
  moveTo(transferring(status));
2085
+ discoveries += 1;
2086
+ const started = discoveries;
1711
2087
  let result;
1712
2088
  try {
1713
2089
  result = await options.adapter.discover(authorization);
@@ -1715,7 +2091,8 @@ export function composeRuntime(options) {
1715
2091
  catch {
1716
2092
  result = { kind: 'transport-uncertainty' };
1717
2093
  }
1718
- if (closed) {
2094
+ if (!live()) {
2095
+ progress.complete = false;
1719
2096
  notifyReady();
1720
2097
  return;
1721
2098
  }
@@ -1773,6 +2150,7 @@ export function composeRuntime(options) {
1773
2150
  }
1774
2151
  const previousCapabilities = capabilities;
1775
2152
  capabilities = result.capabilities;
2153
+ outcomeRetention = result.retention.outcome;
1776
2154
  notifyReady();
1777
2155
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1778
2156
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
@@ -1785,8 +2163,7 @@ export function composeRuntime(options) {
1785
2163
  member = signedIn;
1786
2164
  emitMember();
1787
2165
  }
1788
- await applyDiscovery(result.rooms, local);
1789
- await synchronize(result.rooms, local, progress, attempt);
2166
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
1790
2167
  }
1791
2168
  async function ensureAuthorization() {
1792
2169
  if (authorization !== undefined)
@@ -1800,20 +2177,50 @@ export function composeRuntime(options) {
1800
2177
  throw error;
1801
2178
  }
1802
2179
  }
1803
- function task(work) {
2180
+ async function admittedRead(work) {
2181
+ if (roomReads.generation !== generation) {
2182
+ roomReads = { generation, free: ROOM_READS, waiting: [] };
2183
+ }
2184
+ const reads = roomReads;
2185
+ while (reads.free === 0)
2186
+ await new Promise((admit) => reads.waiting.push(admit));
2187
+ reads.free -= 1;
2188
+ try {
2189
+ await work();
2190
+ }
2191
+ finally {
2192
+ reads.free += 1;
2193
+ reads.waiting.shift()?.();
2194
+ }
2195
+ }
2196
+ function roomTask(locator, work) {
2197
+ return task(roomLane(locator), (live) => admittedRead(() => work(live)));
2198
+ }
2199
+ function task(lane, work) {
1804
2200
  if (stopping)
1805
- return chain;
1806
- chain = chain.then(work, work).catch(() => undefined);
1807
- return chain;
2201
+ return Promise.resolve();
2202
+ const held = lanes.get(lane);
2203
+ const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
2204
+ const turn = current.generation;
2205
+ const live = () => turn === generation && continuing();
2206
+ const run = current.chain.then(() => work(live)).catch(() => undefined);
2207
+ current.chain = run;
2208
+ lanes.set(lane, current);
2209
+ working.add(run);
2210
+ void run.finally(() => working.delete(run));
2211
+ return run;
1808
2212
  }
1809
2213
  function sync(attempt) {
1810
- return task(async () => {
1811
- if (closed || stopping || discoveryPaused) {
2214
+ return task(OPENING_LANE, async (live) => {
2215
+ if (!live()) {
1812
2216
  notifyReady();
1813
2217
  return;
1814
2218
  }
1815
2219
  const progress = { complete: true, reachable: true };
1816
- underway = attempt === 'opened' ? progress : null;
2220
+ const gate = discover();
2221
+ if (attempt === 'opened')
2222
+ opened = generation;
2223
+ underway = attempt === 'opened' ? { generation, progress } : null;
1817
2224
  if (attempt === 'opened')
1818
2225
  streamed = progress;
1819
2226
  complete = false;
@@ -1822,12 +2229,12 @@ export function composeRuntime(options) {
1822
2229
  handed = false;
1823
2230
  try {
1824
2231
  const local = await openLocal();
1825
- if (closed)
2232
+ if (!live())
1826
2233
  return;
1827
2234
  await ensureAuthorization();
1828
- if (closed)
2235
+ if (!live())
1829
2236
  return;
1830
- await discoverRooms(local, progress, attempt);
2237
+ await discoverRooms(local, progress, attempt, live);
1831
2238
  }
1832
2239
  catch {
1833
2240
  progress.complete = false;
@@ -1835,17 +2242,21 @@ export function composeRuntime(options) {
1835
2242
  progress.reachable = false;
1836
2243
  }
1837
2244
  finally {
2245
+ gate.release();
1838
2246
  notifyReady();
1839
2247
  }
2248
+ if (!live())
2249
+ return;
1840
2250
  if (attempt !== 'opened') {
1841
- await finishOpening(progress);
2251
+ await finishOpening(progress, live);
1842
2252
  return;
1843
2253
  }
1844
2254
  if (handed || closed || progress.reachable)
1845
2255
  return;
1846
2256
  underway = null;
2257
+ opened = null;
1847
2258
  journal.record('connection reopen');
1848
- await rest(progress);
2259
+ await rest(progress, live);
1849
2260
  options.adapter.roomStream?.reopen();
1850
2261
  });
1851
2262
  }
@@ -1866,24 +2277,41 @@ export function composeRuntime(options) {
1866
2277
  });
1867
2278
  }
1868
2279
  function applyOpening(expired) {
1869
- return task(async () => {
1870
- const progress = underway;
2280
+ const progress = underway?.generation === generation ? underway.progress : null;
2281
+ const cycle = progress ?? { complete: true, reachable: true };
2282
+ const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
2283
+ if (opened === generation)
2284
+ opened = null;
2285
+ return task(OPENING_LANE, async (live) => {
1871
2286
  underway = null;
1872
- if (closed || stopping)
2287
+ if (!live())
1873
2288
  return;
1874
2289
  if (progress === null) {
1875
- await reloadExpired(expired);
2290
+ await replacing;
2291
+ await rest(cycle, live);
1876
2292
  return;
1877
2293
  }
2294
+ await roomsCarried(expired);
2295
+ if (!live())
2296
+ return;
2297
+ const replaced = new Set(owed);
2298
+ owed.clear();
1878
2299
  try {
1879
2300
  const local = await openLocal();
1880
2301
  const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1881
- const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1882
- const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
2302
+ const reloading = held.filter((room) => !expired.includes(room.locator) &&
2303
+ (replaced.has(room.locator) || revoked(room.locator)));
2304
+ for (const room of reloading)
2305
+ catching.add(room.locator);
2306
+ readSide.recover();
2307
+ const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
2308
+ const reloaded = [...(await reloads), ...(await replacing)];
1883
2309
  if (progress.reachable) {
1884
- for (const room of held)
1885
- if (!reloading.includes(room))
2310
+ for (const room of held) {
2311
+ if (!expired.includes(room.locator) && !reloading.includes(room)) {
1886
2312
  await confirmModels(room, local);
2313
+ }
2314
+ }
1887
2315
  }
1888
2316
  const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1889
2317
  if (failed !== undefined)
@@ -1893,21 +2321,21 @@ export function composeRuntime(options) {
1893
2321
  progress.complete = false;
1894
2322
  progress.reachable = false;
1895
2323
  }
1896
- await finishOpening(progress);
2324
+ if (live())
2325
+ await finishOpening(progress, live);
1897
2326
  });
1898
2327
  }
1899
- async function reloadExpired(expired) {
1900
- const progress = { complete: true, reachable: true };
2328
+ async function reloadNow(replaced, progress) {
1901
2329
  try {
1902
2330
  const local = await openLocal();
1903
- await Promise.allSettled([...discovered.values()]
1904
- .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1905
- .map((room) => reload(room, local, progress)));
2331
+ return await Promise.allSettled([...discovered.values()]
2332
+ .filter((room) => replicas.has(room.locator) && replaced(room))
2333
+ .map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
1906
2334
  }
1907
2335
  catch {
1908
2336
  progress.reachable = false;
2337
+ return [];
1909
2338
  }
1910
- await rest(progress);
1911
2339
  }
1912
2340
  async function confirmModels(room, local) {
1913
2341
  for (const attached of ['collection', 'typed-selection']) {
@@ -1920,38 +2348,94 @@ export function composeRuntime(options) {
1920
2348
  function submitLater(locator) {
1921
2349
  if (localSaves)
1922
2350
  return;
1923
- const running = submitting.get(locator);
1924
- if (running !== undefined) {
1925
- running.again = true;
2351
+ void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
2352
+ }
2353
+ async function submitPass(locator, signal) {
2354
+ const room = discovered.get(locator);
2355
+ if (closed || stopping || discoveryPaused || room === undefined)
2356
+ return 'done';
2357
+ const progress = { complete: true, reachable: true };
2358
+ let next;
2359
+ try {
2360
+ await ensureAuthorization();
2361
+ if (authorization === undefined)
2362
+ return 'done';
2363
+ next = await submitRoom(room, await openLocal(), authorization, progress, signal);
2364
+ }
2365
+ catch {
2366
+ progress.reachable = false;
2367
+ next = 'again';
2368
+ }
2369
+ if (signal.aborted)
2370
+ return 'done';
2371
+ await rest(progress);
2372
+ return next === 'again' && !socketOpen ? 'done' : next;
2373
+ }
2374
+ function repairLater(locator, when) {
2375
+ if (!socketOpen || repairing.has(locator))
1926
2376
  return;
2377
+ void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
2378
+ }
2379
+ async function repairPass(locator, signal) {
2380
+ const room = discovered.get(locator);
2381
+ if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
2382
+ return 'done';
2383
+ if (room === undefined || room.replica.admission !== 'admitted')
2384
+ return 'done';
2385
+ const progress = { complete: true, reachable: true };
2386
+ repairing.add(locator);
2387
+ try {
2388
+ const local = await openLocal();
2389
+ await roomTask(locator, async (live) => {
2390
+ const current = () => live() && !signal.aborted;
2391
+ if (!current() || authorization === undefined)
2392
+ return;
2393
+ const withheld = await Promise.race([
2394
+ advance(room, local, authorization, current),
2395
+ abandonedOn(signal),
2396
+ ]);
2397
+ if (withheld !== null && current())
2398
+ await withhold(withheld, room, local, progress);
2399
+ });
1927
2400
  }
1928
- const run = { again: false, done: Promise.resolve() };
1929
- submitting.set(locator, run);
1930
- run.done = (async () => {
1931
- try {
1932
- do {
1933
- run.again = false;
1934
- const room = discovered.get(locator);
1935
- if (closed || stopping || discoveryPaused || room === undefined)
1936
- return;
1937
- const progress = { complete: true, reachable: true };
1938
- try {
1939
- await ensureAuthorization();
1940
- if (authorization === undefined)
1941
- return;
1942
- await submitRoom(room, await openLocal(), authorization, progress);
1943
- }
1944
- catch {
1945
- progress.reachable = false;
1946
- }
1947
- armTimer();
1948
- await rest(progress);
1949
- } while (run.again);
1950
- }
1951
- finally {
1952
- submitting.delete(locator);
1953
- }
1954
- })();
2401
+ catch {
2402
+ stale.add(locator);
2403
+ progress.reachable = false;
2404
+ }
2405
+ finally {
2406
+ repairing.delete(locator);
2407
+ }
2408
+ if (signal.aborted)
2409
+ return 'done';
2410
+ await rest(progress);
2411
+ if (!stale.has(locator)) {
2412
+ if ((replicas.get(locator)?.queue.length ?? 0) > 0)
2413
+ submitLater(locator);
2414
+ return 'done';
2415
+ }
2416
+ if (!repairable.has(locator))
2417
+ return 'done';
2418
+ const reset = repairable.get(locator);
2419
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
2420
+ }
2421
+ function resendLater() {
2422
+ if (!socketOpen)
2423
+ return;
2424
+ void recovery.attempt('resend', resendPass, { again: true });
2425
+ }
2426
+ async function resendPass(signal) {
2427
+ if (!socketOpen || closed || stopping || discoveryPaused)
2428
+ return 'done';
2429
+ const reading = { complete: true, reachable: true };
2430
+ try {
2431
+ await conversations.resend(reading);
2432
+ }
2433
+ catch {
2434
+ reading.reachable = false;
2435
+ }
2436
+ if (signal.aborted)
2437
+ return 'done';
2438
+ return reading.reachable && reading.complete ? 'done' : 'again';
1955
2439
  }
1956
2440
  function submitAll() {
1957
2441
  for (const [locator, replica] of replicas) {
@@ -1959,31 +2443,40 @@ export function composeRuntime(options) {
1959
2443
  submitLater(locator);
1960
2444
  }
1961
2445
  }
1962
- async function finishOpening(progress) {
2446
+ async function finishOpening(progress, live) {
1963
2447
  try {
1964
- if (closed || discoveryPaused) {
2448
+ if (!live()) {
1965
2449
  progress.complete = false;
1966
2450
  return;
1967
2451
  }
1968
2452
  submitAll();
2453
+ for (const locator of stale)
2454
+ if (repairable.has(locator))
2455
+ repairLater(locator, {});
1969
2456
  await conversations.reconcile();
1970
2457
  if (status.providerUnreachable)
1971
2458
  conversations.reconnected();
1972
2459
  const reading = { complete: true, reachable: true };
1973
2460
  await conversations.catchUp(reading);
1974
- if (!reading.reachable)
2461
+ if (progress.reachable && reading.reachable)
2462
+ await conversations.resend(reading);
2463
+ if (!reading.reachable) {
1975
2464
  progress.reachable = false;
2465
+ resendLater();
2466
+ }
1976
2467
  }
1977
2468
  catch {
1978
2469
  progress.complete = false;
1979
2470
  }
1980
2471
  finally {
1981
- complete = progress.complete;
1982
- await rest(progress);
2472
+ if (live()) {
2473
+ complete = progress.complete;
2474
+ await rest(progress, live);
2475
+ }
1983
2476
  }
1984
2477
  }
1985
- async function rest(progress) {
1986
- if (closed)
2478
+ async function rest(progress, live = () => !closed) {
2479
+ if (!live())
1987
2480
  return;
1988
2481
  let lastCompleted = status.lastCompleted;
1989
2482
  let synced = complete && stale.size === 0 && !pending();
@@ -1991,6 +2484,8 @@ export function composeRuntime(options) {
1991
2484
  const completedAt = new Date().toISOString();
1992
2485
  try {
1993
2486
  await (await openLocal()).complete(completedAt);
2487
+ if (!live())
2488
+ return;
1994
2489
  recorded = true;
1995
2490
  lastCompleted = completedAt;
1996
2491
  }
@@ -2005,11 +2500,23 @@ export function composeRuntime(options) {
2005
2500
  ...attention(),
2006
2501
  });
2007
2502
  }
2503
+ function discover() {
2504
+ let release = () => { };
2505
+ const done = new Promise((resolve) => {
2506
+ release = resolve;
2507
+ });
2508
+ discovering = { generation, done, release };
2509
+ return discovering;
2510
+ }
2008
2511
  function applyRoom(locator, roomVersion) {
2009
2512
  announced.set(locator, roomVersion);
2010
- return task(async () => {
2011
- await answering.get(locator);
2012
- if (closed || stopping || discoveryPaused || authorization === undefined)
2513
+ return task(roomLane(locator), async (live) => {
2514
+ if (discovering?.generation === generation)
2515
+ await discovering.done;
2516
+ const saving = answering.get(locator);
2517
+ if (saving?.generation === generation)
2518
+ await saving.answered;
2519
+ if (!live() || authorization === undefined)
2013
2520
  return;
2014
2521
  const room = discovered.get(locator);
2015
2522
  const held = replicas.get(locator);
@@ -2020,25 +2527,41 @@ export function composeRuntime(options) {
2020
2527
  options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
2021
2528
  return;
2022
2529
  }
2023
- const during = underway;
2530
+ const during = underway?.generation === generation ? underway.progress : null;
2024
2531
  const progress = during ?? { complete: true, reachable: true };
2025
2532
  if (during === null)
2026
2533
  moveTo(transferring(status));
2027
2534
  try {
2028
2535
  const local = await openLocal();
2029
- const withheld = await advance(room, local, authorization);
2030
- if (withheld !== null)
2031
- await withhold(withheld, room, local, progress);
2536
+ await admittedRead(async () => {
2537
+ if (!live() || authorization === undefined)
2538
+ return;
2539
+ const withheld = await advance(room, local, authorization, live);
2540
+ if (withheld !== null)
2541
+ await withhold(withheld, room, local, progress);
2542
+ });
2032
2543
  }
2033
2544
  catch {
2034
2545
  stale.add(locator);
2035
2546
  }
2036
2547
  finally {
2037
2548
  if (during === null)
2038
- await rest(progress);
2549
+ await rest(progress, live);
2039
2550
  }
2040
2551
  });
2041
2552
  }
2553
+ function roomsCarried(replacing) {
2554
+ const replaced = new Set(replacing.map(roomLane));
2555
+ return Promise.all([...lanes]
2556
+ .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
2557
+ .map(([, held]) => held.chain));
2558
+ }
2559
+ const receipts = {
2560
+ heads: async () => (await openLocal()).receivedHeads(),
2561
+ parts: async (key, count) => (await openLocal()).receivedParts(key, count),
2562
+ keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
2563
+ drop: async (keys) => (await openLocal()).dropReceived(keys),
2564
+ };
2042
2565
  function observeStream() {
2043
2566
  const stream = options.adapter.roomStream;
2044
2567
  if (stream === undefined || withdrawStream !== undefined)
@@ -2048,6 +2571,9 @@ export function composeRuntime(options) {
2048
2571
  return;
2049
2572
  if (event.kind === 'opened') {
2050
2573
  socketOpen = true;
2574
+ generation += 1;
2575
+ conversations.reopened();
2576
+ recovery.resume();
2051
2577
  void sync('opened');
2052
2578
  }
2053
2579
  else if (event.kind === 'closed')
@@ -2056,7 +2582,7 @@ export function composeRuntime(options) {
2056
2582
  void applyOpening(event.expired);
2057
2583
  else
2058
2584
  void applyRoom(event.locator, event.roomVersion);
2059
- });
2585
+ }, receipts);
2060
2586
  }
2061
2587
  function writesHeld() {
2062
2588
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -2094,14 +2620,15 @@ export function composeRuntime(options) {
2094
2620
  });
2095
2621
  }
2096
2622
  function scheduleCurrency() {
2097
- if (currencyQueued || closed || stopping)
2623
+ if (currencyQueued === generation || closed || stopping)
2098
2624
  return;
2099
- currencyQueued = true;
2100
- void task(runCurrency);
2625
+ currencyQueued = generation;
2626
+ void task(CONVERSATION_LANE, runCurrency);
2101
2627
  }
2102
- async function runCurrency() {
2103
- currencyQueued = false;
2104
- if (closed || stopping || discoveryPaused)
2628
+ async function runCurrency(live) {
2629
+ if (currencyQueued === generation)
2630
+ currencyQueued = null;
2631
+ if (!live())
2105
2632
  return;
2106
2633
  await conversations.reconcile();
2107
2634
  if (conversations.current()) {
@@ -2116,7 +2643,7 @@ export function composeRuntime(options) {
2116
2643
  catch {
2117
2644
  progress.reachable = false;
2118
2645
  }
2119
- await rest(progress);
2646
+ await rest(progress, live);
2120
2647
  }
2121
2648
  function close() {
2122
2649
  if (closed)
@@ -2128,7 +2655,7 @@ export function composeRuntime(options) {
2128
2655
  withdrawConversations = undefined;
2129
2656
  withdrawServiceMode?.();
2130
2657
  conversations.close();
2131
- stopTimer();
2658
+ void recovery.retire();
2132
2659
  store?.close();
2133
2660
  void opening?.then((opened) => {
2134
2661
  opened.close();
@@ -2146,9 +2673,8 @@ export function composeRuntime(options) {
2146
2673
  }
2147
2674
  async function stop() {
2148
2675
  stopping = true;
2149
- stopTimer();
2150
- await chain.catch(() => undefined);
2151
- await Promise.all([...submitting.values()].map((run) => run.done));
2676
+ await recovery.retire();
2677
+ await Promise.all(working);
2152
2678
  await mutations;
2153
2679
  close();
2154
2680
  }
@@ -2170,17 +2696,54 @@ export function composeRuntime(options) {
2170
2696
  return planOf(input, roomFacts(reach), planReach(reach));
2171
2697
  }
2172
2698
  function watchPlan(plan, deliver) {
2173
- materialize(reachedRooms(plan));
2174
- return readSide.watch(plan, identityOf(plan), deliver);
2699
+ return readSide.watch(plan, identityOf(plan), () => {
2700
+ const cover = covered(plan);
2701
+ awaitPreparation(cover);
2702
+ return cover;
2703
+ }, deliver);
2175
2704
  }
2176
2705
  async function searched(input, reach) {
2177
2706
  const plan = searchPlanOf(input);
2178
- const selected = plan.documents === undefined && reach === null
2179
- ? null
2180
- : (await rowsQuery(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
2181
- if (selected === null)
2182
- materialize(null);
2183
- return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
2707
+ const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
2708
+ for (;;) {
2709
+ const cover = covered(filter);
2710
+ awaitPreparation(cover);
2711
+ const pending = 'pending' in cover ? [...cover.pending] : [];
2712
+ const covering = 'pending' in cover ? [] : cover.coverage.rooms;
2713
+ const unindexed = covering
2714
+ .filter(({ room }) => !searchIndex.rooms().has(room))
2715
+ .map(({ room }) => room);
2716
+ for (const room of unindexed)
2717
+ pending.push({ room, awaits: 'index' });
2718
+ const held = conversations.searchable();
2719
+ const named = reach !== null && !reach.conversations
2720
+ ? []
2721
+ : (plan.conversations ?? held).filter((one) => held.includes(one) && conversations.acquired(one));
2722
+ const waiting = named.filter((one) => !searchIndex.conversations().has(one));
2723
+ for (const conversation of waiting)
2724
+ pending.push({ conversation, awaits: 'index' });
2725
+ if (unindexed.length > 0 || waiting.length > 0)
2726
+ index(unindexed, waiting);
2727
+ if (pending.length > 0 || 'pending' in cover)
2728
+ return { pending };
2729
+ const seen = searchIndex.changes();
2730
+ await searchIndex.caughtUp();
2731
+ const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(filter);
2732
+ if (selected !== null && 'pending' in selected)
2733
+ return selected;
2734
+ if (searchIndex.changes() !== seen ||
2735
+ JSON.stringify(covered(filter)) !== JSON.stringify(cover))
2736
+ continue;
2737
+ return {
2738
+ ...searchIndex.search(plan, selected === null ? null : selected.documents, named),
2739
+ coverage: {
2740
+ rooms: covering,
2741
+ conversations: named,
2742
+ index: searchIndex.generation(),
2743
+ overlay: cover.coverage.overlay,
2744
+ },
2745
+ };
2746
+ }
2184
2747
  }
2185
2748
  const watch = (topic, input, deliver) => {
2186
2749
  if (topic === 'room-drafts') {
@@ -2399,14 +2962,16 @@ export function composeRuntime(options) {
2399
2962
  if (refusal === null || !refusal.reauthorizationOffered)
2400
2963
  return;
2401
2964
  journal.record('connection reauthorize');
2402
- await chain.catch(() => undefined);
2965
+ await Promise.all([...lanes.values()]
2966
+ .filter((held) => held.generation === generation)
2967
+ .map((held) => held.chain));
2403
2968
  await mutations;
2404
2969
  authorization = undefined;
2405
2970
  discoveryPaused = false;
2406
2971
  if (options.adapter.roomStream === undefined)
2407
2972
  await sync('unstreamed');
2408
2973
  else
2409
- await task(ensureAuthorization);
2974
+ await task(OPENING_LANE, ensureAuthorization);
2410
2975
  },
2411
2976
  };
2412
2977
  const reachable = {
@@ -2418,5 +2983,5 @@ export function composeRuntime(options) {
2418
2983
  watchPlan,
2419
2984
  search: searched,
2420
2985
  };
2421
- return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2986
+ return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
2422
2987
  }