@agentshouse/mdruntime 0.1.0-ca9afacd5aebd → 0.1.0-cb0eb0d87ff49

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 (104) hide show
  1. package/AGENTS.md +114 -123
  2. package/dist/adapter.d.ts +26 -3
  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 +858 -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 +2 -2
  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 +49 -32
  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/store.d.ts +35 -10
  99. package/dist/store.d.ts.map +1 -1
  100. package/dist/store.js +275 -74
  101. package/dist/turn.d.ts +7 -0
  102. package/dist/turn.d.ts.map +1 -0
  103. package/dist/turn.js +45 -0
  104. package/package.json +12 -8
@@ -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) => {
@@ -233,12 +288,9 @@ export function composeRuntime(options) {
233
288
  if (replica === undefined)
234
289
  return 0;
235
290
  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;
291
+ const weighed = outsideOf(roots);
292
+ return (tally(replica.visible.documents, weighed.documents) +
293
+ tally(replica.visible.binaries, weighed.binaries));
242
294
  }
243
295
  function listed() {
244
296
  return structuredClone([...rooms.values()].map((room) => {
@@ -265,6 +317,7 @@ export function composeRuntime(options) {
265
317
  return { listed: roomsListed, rooms: held };
266
318
  }
267
319
  function emitRooms() {
320
+ readSide.recover();
268
321
  const value = listed();
269
322
  for (const listener of roomListeners)
270
323
  listener(value);
@@ -319,42 +372,186 @@ export function composeRuntime(options) {
319
372
  }
320
373
  }
321
374
  function applyChangeSet(set) {
322
- const held = everyRoom
323
- ? set
324
- : { ...set, documents: set.documents.filter(({ locator }) => materialized.has(locator)) };
375
+ const held = {
376
+ ...set,
377
+ documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
378
+ };
325
379
  readSide.apply(held);
326
380
  searchIndex.changed(held);
327
381
  }
328
- function materialize(locators) {
329
- if (everyRoom)
382
+ async function referenced(locator) {
383
+ for (;;) {
384
+ const record = replicas.get(locator)?.confirmed.record;
385
+ if (record === undefined || closed)
386
+ return;
387
+ const held = { text: record.documents, binaries: record.binaries };
388
+ const composition = compositionOf(record);
389
+ if (referencesIndexed(held, composition))
390
+ return;
391
+ await indexReferrers(held, composition);
392
+ }
393
+ }
394
+ async function prepareReplica(locator) {
395
+ await referenced(locator);
396
+ const replica = replicas.get(locator);
397
+ if (replica === undefined || closed || prepared.has(locator))
330
398
  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));
399
+ const visible = replica.visible;
400
+ const paths = [
401
+ ...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
402
+ ];
403
+ const entered = [];
404
+ projecting.add(locator);
405
+ try {
406
+ await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
407
+ const current = replicas.get(locator);
408
+ if (current === undefined || closed)
409
+ return;
410
+ const set = entering(locator, current, chunk);
411
+ readSide.apply(set);
412
+ entered.push(set);
413
+ });
414
+ if (!replicas.has(locator) || closed)
415
+ return;
416
+ prepared.add(locator);
341
417
  }
342
- if (sets.length === 0)
418
+ finally {
419
+ projecting.delete(locator);
420
+ }
421
+ conversations.changed(mergeChangeSets(entered));
422
+ readSide.recover();
423
+ }
424
+ function unprepared() {
425
+ return [...demanded, ...replicas.keys()].find((locator) => !prepared.has(locator));
426
+ }
427
+ function prepare(locators = []) {
428
+ for (const locator of locators)
429
+ demanded.add(locator);
430
+ if (preparing)
343
431
  return;
344
- const set = mergeChangeSets(sets);
345
- readSide.apply(set);
346
- searchIndex.changed(set);
432
+ preparing = true;
433
+ void (async () => {
434
+ try {
435
+ for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
436
+ await yielded();
437
+ if (closed)
438
+ return;
439
+ demanded.delete(waiting);
440
+ await prepareReplica(waiting);
441
+ }
442
+ }
443
+ finally {
444
+ preparing = false;
445
+ }
446
+ })();
347
447
  }
348
448
  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)];
449
+ const reach = plan.reach;
450
+ const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
451
+ const named = plan.rooms ??
452
+ (reached === null
453
+ ? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
454
+ : [...reached]);
455
+ return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
456
+ }
457
+ function awaited(locator) {
458
+ return rooms.has(locator) || !roomsListed;
459
+ }
460
+ function covered(plan) {
461
+ const locators = reachedRooms(plan);
462
+ if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
463
+ return { pending: [] };
464
+ }
465
+ const pending = [];
466
+ const coverage = [];
467
+ const overlay = [];
468
+ for (const locator of locators) {
469
+ const replica = replicas.get(locator);
470
+ if (replica === undefined) {
471
+ if (awaited(locator))
472
+ pending.push({ room: locator, awaits: 'acquisition' });
473
+ continue;
474
+ }
475
+ if (!prepared.has(locator)) {
476
+ pending.push({ room: locator, awaits: 'preparation' });
477
+ continue;
478
+ }
479
+ coverage.push({
480
+ room: locator,
481
+ generation: replica.confirmed.record.transfer,
482
+ current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
483
+ });
484
+ for (const batch of replica.queue)
485
+ overlay.push(batch.id);
486
+ }
487
+ if (pending.length > 0)
488
+ return { pending };
489
+ return { coverage: { rooms: coverage, indexes: [], overlay } };
354
490
  }
355
- function rowsQuery(plan) {
356
- materialize(reachedRooms(plan));
357
- return readSide.query(plan);
491
+ function awaitPreparation(cover) {
492
+ if (!('pending' in cover))
493
+ return;
494
+ const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
495
+ if (waiting.length > 0)
496
+ prepare(waiting.map(({ room }) => room));
497
+ }
498
+ async function rowsQuery(plan) {
499
+ const before = covered(plan);
500
+ awaitPreparation(before);
501
+ if ('pending' in before)
502
+ return before;
503
+ const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
504
+ awaitPreparation(cover);
505
+ if ('pending' in cover)
506
+ return cover;
507
+ return 'groups' in rows
508
+ ? { groups: rows.groups, coverage: cover.coverage }
509
+ : { documents: rows.documents, coverage: cover.coverage };
510
+ }
511
+ async function indexConversation(conversation) {
512
+ searchIndex.indexMessages(conversation, [], false);
513
+ let after = null;
514
+ for (;;) {
515
+ const batch = await durably((local) => local.messagesAfter(conversation, after));
516
+ if (closed)
517
+ return;
518
+ searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
519
+ if (batch.length === 0)
520
+ return;
521
+ after = batch[batch.length - 1].identity;
522
+ }
523
+ }
524
+ function indexNext() {
525
+ const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator));
526
+ if (room !== undefined) {
527
+ roomsToIndex.delete(room);
528
+ return () => (prepared.has(room) ? searchIndex.indexRoom(room) : undefined);
529
+ }
530
+ const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
531
+ if (conversation === undefined)
532
+ return null;
533
+ conversationsToIndex.delete(conversation);
534
+ return () => indexConversation(conversation);
535
+ }
536
+ function index(rooms, named) {
537
+ for (const room of rooms)
538
+ roomsToIndex.add(room);
539
+ for (const conversation of named)
540
+ conversationsToIndex.add(conversation);
541
+ if (indexing)
542
+ return;
543
+ indexing = true;
544
+ void (async () => {
545
+ try {
546
+ for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
547
+ await yielded();
548
+ await unit();
549
+ }
550
+ }
551
+ finally {
552
+ indexing = false;
553
+ }
554
+ })().catch(() => { });
358
555
  }
359
556
  function publishChangeSet(set) {
360
557
  applyChangeSet(set);
@@ -371,34 +568,7 @@ export function composeRuntime(options) {
371
568
  emitPresentation(app);
372
569
  conversations.changed(set);
373
570
  }
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
571
  async function refuse(reason, provider = null) {
401
- stopTimer();
402
572
  discoveryPaused = true;
403
573
  try {
404
574
  if (reason === 'revoked-authorization') {
@@ -477,14 +647,24 @@ export function composeRuntime(options) {
477
647
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
478
648
  });
479
649
  }
480
- async function applyDiscovery(listed, local) {
650
+ async function applyDiscovery(found, started) {
651
+ for (const room of found) {
652
+ const fence = fences.get(room.locator);
653
+ if (fence !== undefined && fence.erased && fence.at < started)
654
+ fences.delete(room.locator);
655
+ }
656
+ const listed = found.filter((room) => !fences.has(room.locator));
481
657
  const listedLocators = new Set(listed.map((room) => room.locator));
482
658
  for (const locator of listedLocators)
483
659
  scopedRefusals.delete(locator);
484
660
  discovered.clear();
485
661
  for (const room of listed)
486
662
  discovered.set(room.locator, room);
487
- await dropUnlisted(listedLocators, local);
663
+ const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
664
+ if (options.adapter.exhaustiveDiscovery === true)
665
+ await loseRooms(unlisted);
666
+ else
667
+ unlistedKept(unlisted);
488
668
  for (const room of listed) {
489
669
  const held = replicas.get(room.locator)?.confirmed.record;
490
670
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -495,6 +675,7 @@ export function composeRuntime(options) {
495
675
  }
496
676
  roomsListed = true;
497
677
  emitRooms();
678
+ return listed;
498
679
  }
499
680
  function unlistedHeld(listed) {
500
681
  return [
@@ -506,41 +687,71 @@ export function composeRuntime(options) {
506
687
  ]),
507
688
  ].filter((locator) => !listed.has(locator));
508
689
  }
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);
690
+ function unlistedKept(unlisted) {
691
+ for (const locator of unlisted) {
692
+ const held = replicas.get(locator)?.confirmed.record;
693
+ if (held !== undefined)
694
+ list(held, false);
695
+ else
525
696
  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}`);
697
+ }
698
+ }
699
+ function fence(locators) {
700
+ const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
701
+ const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
702
+ room: true,
703
+ queue: true,
704
+ drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
705
+ }));
706
+ for (const locator of fresh) {
707
+ fences.set(locator, { at: discoveries, erased: false });
708
+ lastFenced.set(locator, ++fencings);
709
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
710
+ pendings.delete(batch.id);
711
+ replicas.delete(locator);
712
+ queuesWithoutReplica.delete(locator);
713
+ rooms.delete(locator);
714
+ roomDrafts.delete(locator);
715
+ transfers.delete(locator);
716
+ catching.delete(locator);
717
+ discovered.delete(locator);
718
+ announced.delete(locator);
719
+ unreadable.delete(locator);
720
+ stale.delete(locator);
721
+ repairable.delete(locator);
722
+ scopedRefusals.delete(locator);
723
+ sourceRefusals.delete(locator);
724
+ declarationRefusals.delete(locator);
725
+ for (const attached of ['collection', 'typed-selection']) {
726
+ const key = modelKey(locator, attached);
727
+ models.delete(key);
728
+ liveModels.delete(key);
729
+ invalidModels.delete(key);
540
730
  }
541
- publishChangeSet(mergeChangeSets(removed));
542
- moveTo({ ...status, ...attention() });
543
- });
731
+ journal.record(`room lost ${locator}`);
732
+ }
733
+ if (fresh.length === 0)
734
+ return fresh;
735
+ publishChangeSet(mergeChangeSets(removed));
736
+ for (const locator of fresh)
737
+ prepared.delete(locator);
738
+ searchIndex.forget(fresh);
739
+ readSide.recover();
740
+ moveTo({ ...status, ...attention() });
741
+ return fresh;
742
+ }
743
+ async function erased(local, locators) {
744
+ await local.erase(locators);
745
+ for (const locator of locators) {
746
+ const held = fences.get(locator);
747
+ if (held !== undefined)
748
+ held.erased = true;
749
+ }
750
+ }
751
+ async function loseRooms(locators) {
752
+ const fresh = fence(locators);
753
+ if (fresh.length > 0)
754
+ await durably((local) => erased(local, fresh));
544
755
  }
545
756
  function restored(key, persisted) {
546
757
  const prior = models.get(key);
@@ -635,6 +846,7 @@ export function composeRuntime(options) {
635
846
  async function readState(opened) {
636
847
  const { lastCompleted, locators, read } = await hydrate(opened);
637
848
  applyChangeSet(read);
849
+ prepare();
638
850
  if (replicas.size > 0)
639
851
  emitRooms();
640
852
  if (member !== null)
@@ -645,8 +857,15 @@ export function composeRuntime(options) {
645
857
  return lastCompleted;
646
858
  }
647
859
  async function open() {
648
- const opened = await openRuntimeStore();
860
+ if (closed)
861
+ throw new Error('runtime closed');
862
+ const opened = await openRuntimeStore(isFenced);
649
863
  store = opened;
864
+ const interrupted = await opened.fences();
865
+ for (const locator of interrupted) {
866
+ fences.set(locator, { at: discoveries, erased: false });
867
+ lastFenced.set(locator, ++fencings);
868
+ }
650
869
  const kept = options.shell.generation;
651
870
  const storedGeneration = await opened.generation();
652
871
  const loss = detectStorageLoss(storedGeneration, kept);
@@ -660,6 +879,8 @@ export function composeRuntime(options) {
660
879
  emitStorageLoss();
661
880
  }
662
881
  const lastCompleted = await readState(opened);
882
+ if (interrupted.length > 0)
883
+ await erased(opened, interrupted);
663
884
  moveTo({ ...status, lastCompleted, ...attention() });
664
885
  return opened;
665
886
  }
@@ -741,17 +962,19 @@ export function composeRuntime(options) {
741
962
  moveTo({ ...status, ...attention() });
742
963
  throw error;
743
964
  }
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
- });
965
+ return () => {
966
+ queuesWithoutReplica.delete(locator);
967
+ for (const { batch, returned } of unqueued) {
968
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
969
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
970
+ }
971
+ replicas.set(locator, replicaOf(confirmed, kept, before));
972
+ return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
973
+ room: settling.room,
974
+ queue: kept.length !== previousQueue.length,
975
+ drafts: [...written.keys()],
976
+ });
977
+ };
755
978
  }
756
979
  function adoptModels(locator, record) {
757
980
  for (const [attached, model] of [
@@ -784,11 +1007,16 @@ export function composeRuntime(options) {
784
1007
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
785
1008
  };
786
1009
  }
787
- function publish(local, room, confirmed, landing = {}) {
1010
+ function publish(local, room, confirmed, landing, live) {
788
1011
  const { base, stored } = landing;
789
1012
  return exclusive(async () => {
790
- if (!continuing())
1013
+ if (stored !== undefined) {
1014
+ await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
1015
+ }
1016
+ if (!live()) {
1017
+ caughtUpTo(room.locator);
791
1018
  return true;
1019
+ }
792
1020
  const prior = replicas.get(room.locator);
793
1021
  if (base !== undefined &&
794
1022
  (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
@@ -802,9 +1030,11 @@ export function composeRuntime(options) {
802
1030
  const accepted = pendings.get(batch.id)?.accepted;
803
1031
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
804
1032
  });
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)
1033
+ const set = (await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) }))();
1034
+ if (stored !== undefined) {
807
1035
  transfers.delete(room.locator);
1036
+ catching.delete(room.locator);
1037
+ }
808
1038
  for (const batch of accepted)
809
1039
  pendings.delete(batch.id);
810
1040
  stale.delete(room.locator);
@@ -813,13 +1043,17 @@ export function composeRuntime(options) {
813
1043
  sourceRefusals.delete(room.locator);
814
1044
  declarationRefusals.delete(room.locator);
815
1045
  list(room, true);
1046
+ if (prior === undefined)
1047
+ prepared.add(room.locator);
816
1048
  publishChangeSet(set);
817
1049
  moveTo({ ...status, ...attention() });
1050
+ options.adapter.roomStream?.published(room.locator, roomVersion);
818
1051
  return true;
819
1052
  });
820
1053
  }
821
1054
  async function saveRoomDrafts(locator, named, writtenBy) {
822
1055
  const local = await openLocal();
1056
+ await referenced(locator);
823
1057
  const replica = replicas.get(locator);
824
1058
  const room = rooms.get(locator);
825
1059
  if (replica === undefined || room === undefined) {
@@ -867,6 +1101,11 @@ export function composeRuntime(options) {
867
1101
  changes,
868
1102
  bases: parts.flatMap(({ bases }) => bases),
869
1103
  writtenBy,
1104
+ clean: {
1105
+ text: replica.confirmed.record.documents,
1106
+ binaries: replica.confirmed.record.binaries,
1107
+ composition: compositionOf(replica.confirmed.record),
1108
+ },
870
1109
  };
871
1110
  const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
872
1111
  if (checked?.kind === 'refused')
@@ -937,6 +1176,8 @@ export function composeRuntime(options) {
937
1176
  catch {
938
1177
  return { outcome: 'refused', code: 'durable-write', retryable: true };
939
1178
  }
1179
+ if (fences.has(locator))
1180
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
940
1181
  for (const origin of withdrawn.keys())
941
1182
  pendings.delete(origin);
942
1183
  for (const [id, drafts] of drafted) {
@@ -966,6 +1207,8 @@ export function composeRuntime(options) {
966
1207
  return;
967
1208
  }
968
1209
  await local.submit(locator, identifier, submission, lost);
1210
+ if (fences.has(locator))
1211
+ return;
969
1212
  pendings.set(identifier, { ...state, submission, lost });
970
1213
  const replica = replicas.get(locator);
971
1214
  if (replica !== undefined)
@@ -976,6 +1219,8 @@ export function composeRuntime(options) {
976
1219
  if (!continuing())
977
1220
  throw new Error('connection refused');
978
1221
  await local.keepUpload(locator, identifier, upload);
1222
+ if (fences.has(locator))
1223
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
979
1224
  pendings.set(identifier, { ...pendings.get(identifier), upload });
980
1225
  if (!continuing())
981
1226
  throw new Error('connection refused');
@@ -991,6 +1236,7 @@ export function composeRuntime(options) {
991
1236
  }
992
1237
  if (closed || authorization === undefined || refusal !== null)
993
1238
  return UNCERTAIN;
1239
+ const since = fencings;
994
1240
  const transfer = ['transfer', 'history', locator, path];
995
1241
  if (query.revision !== undefined)
996
1242
  transfer.push(query.revision);
@@ -1005,6 +1251,8 @@ export function composeRuntime(options) {
1005
1251
  ...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
1006
1252
  });
1007
1253
  journal.record(`outcome history ${result.kind}`);
1254
+ if (fencedSince(locator, since))
1255
+ return failure({ kind: 'room-not-readable', locator });
1008
1256
  if (result.kind === 'scoped-refusal') {
1009
1257
  const room = replicas.get(result.locator)?.confirmed.record;
1010
1258
  if (room !== undefined)
@@ -1027,6 +1275,12 @@ export function composeRuntime(options) {
1027
1275
  }
1028
1276
  return null;
1029
1277
  }
1278
+ function binaryRoom(request) {
1279
+ if (request.kind === 'path')
1280
+ return request.locator;
1281
+ const scope = options.adapter.referrerScope(request.referrer);
1282
+ return scope?.kind === 'room' ? scope.locator : null;
1283
+ }
1030
1284
  async function readBinary(request) {
1031
1285
  if (closed || refusal !== null)
1032
1286
  return UNCERTAIN;
@@ -1038,12 +1292,16 @@ export function composeRuntime(options) {
1038
1292
  }
1039
1293
  if (closed || authorization === undefined || refusal !== null)
1040
1294
  return UNCERTAIN;
1295
+ const since = fencings;
1041
1296
  journal.record(request.kind === 'path'
1042
1297
  ? `transfer binary ${request.locator} ${request.path}`
1043
1298
  : `transfer binary ${request.fileRef}`);
1044
1299
  try {
1045
1300
  const result = await options.adapter.binary(authorization, request);
1046
1301
  journal.record(`outcome binary ${result.kind}`);
1302
+ const locator = binaryRoom(request);
1303
+ if (locator !== null && fencedSince(locator, since))
1304
+ return failure({ kind: 'room-not-readable', locator });
1047
1305
  if (result.kind === 'scoped-refusal') {
1048
1306
  const room = replicas.get(result.locator)?.confirmed.record;
1049
1307
  if (room !== undefined)
@@ -1167,11 +1425,11 @@ export function composeRuntime(options) {
1167
1425
  options.adapter.reached(locator, written, roomVersion) &&
1168
1426
  !options.adapter.reached(locator, roomVersion, written));
1169
1427
  }
1170
- async function pullRefresh(authorized, confirmed) {
1171
- if (!continuing())
1428
+ async function pullRefresh(authorized, confirmed, live) {
1429
+ if (!live())
1172
1430
  return unconfirmed;
1173
1431
  try {
1174
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
1432
+ const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1175
1433
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
1176
1434
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
1177
1435
  return result;
@@ -1181,47 +1439,69 @@ export function composeRuntime(options) {
1181
1439
  throw error;
1182
1440
  }
1183
1441
  }
1184
- function staging(local) {
1442
+ function staging(local, live) {
1185
1443
  return {
1186
1444
  held: (locator) => transfers.get(locator) ?? null,
1187
1445
  staged: (record) => local.staged(record),
1188
1446
  stage: (record, page) => exclusive(async () => {
1189
- if (!continuing())
1447
+ if (!live())
1190
1448
  return false;
1191
1449
  await local.stage(record, page);
1192
1450
  transfers.set(record.locator, record);
1451
+ readSide.recover();
1193
1452
  return true;
1194
1453
  }),
1195
1454
  abandon: (locator) => exclusive(async () => {
1196
- if (!continuing())
1455
+ if (!live())
1197
1456
  return;
1198
1457
  await local.abandon(locator);
1199
1458
  transfers.delete(locator);
1459
+ readSide.recover();
1200
1460
  }),
1201
1461
  };
1202
1462
  }
1203
- async function pullBootstrap(authorized, room, local) {
1204
- if (!continuing())
1463
+ function reusable(room) {
1464
+ const record = replicas.get(room.locator)?.confirmed.record;
1465
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1466
+ return null;
1467
+ return { documents: record.documents, revisions: record.revisions };
1468
+ }
1469
+ async function pullBootstrap(authorized, room, local, live) {
1470
+ if (!live())
1205
1471
  return unconfirmed;
1472
+ if (replicas.has(room.locator)) {
1473
+ catching.add(room.locator);
1474
+ readSide.recover();
1475
+ }
1206
1476
  try {
1207
1477
  const result = await bootstrapRoom(options.adapter, authorized, room, {
1208
1478
  collection: modelOf(room.locator),
1209
1479
  selection: modelOf(room.locator, 'typed-selection'),
1210
- }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
1480
+ }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
1211
1481
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
1212
1482
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1483
+ if (result.kind !== 'staged')
1484
+ caughtUpTo(room.locator);
1213
1485
  return result;
1214
1486
  }
1215
1487
  catch (error) {
1216
1488
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
1489
+ caughtUpTo(room.locator);
1217
1490
  throw error;
1218
1491
  }
1219
1492
  }
1220
- async function advance(room, local, authorized) {
1493
+ function caughtUpTo(locator) {
1494
+ if (catching.delete(locator))
1495
+ readSide.recover();
1496
+ }
1497
+ async function advance(room, local, authorized, live) {
1498
+ await referenced(room.locator);
1499
+ if (!live())
1500
+ return unconfirmed;
1221
1501
  const held = replicas.get(room.locator);
1222
1502
  if (held !== undefined) {
1223
- const refreshed = await pullRefresh(authorized, held.confirmed);
1224
- if (!continuing())
1503
+ const refreshed = await pullRefresh(authorized, held.confirmed, live);
1504
+ if (!live())
1225
1505
  return unconfirmed;
1226
1506
  if (refreshed.kind === 'still-current') {
1227
1507
  stale.delete(room.locator);
@@ -1235,31 +1515,45 @@ export function composeRuntime(options) {
1235
1515
  return null;
1236
1516
  }
1237
1517
  if (refreshed.kind === 'advanced') {
1238
- if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
1518
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
1239
1519
  const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
1240
1520
  const reached = now !== undefined &&
1241
1521
  options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
1242
- return reached ? null : advance(room, local, authorized);
1522
+ return reached ? null : advance(room, local, authorized, live);
1243
1523
  }
1244
1524
  await retainModel(local, room, 'collection', modelOf(room.locator));
1245
1525
  return null;
1246
1526
  }
1247
1527
  if (refreshed.kind !== 'beyond-horizon')
1248
1528
  return refreshed;
1529
+ if (opened !== null) {
1530
+ owed.add(room.locator);
1531
+ readSide.recover();
1532
+ return unconfirmed;
1533
+ }
1249
1534
  }
1250
- const bootstrapped = await pullBootstrap(authorized, room, local);
1251
- if (!continuing())
1535
+ const bootstrapped = await pullBootstrap(authorized, room, local, live);
1536
+ if (!live())
1252
1537
  return unconfirmed;
1253
1538
  if (bootstrapped.kind !== 'staged')
1254
1539
  return bootstrapped;
1255
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1540
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
1256
1541
  return null;
1257
1542
  }
1258
1543
  async function withhold(withheld, room, local, progress) {
1544
+ if (withheld.kind === 'unconfirmed')
1545
+ return;
1259
1546
  if (withheld.kind === 'refused')
1260
1547
  stale.delete(room.locator);
1261
1548
  else
1262
1549
  stale.add(room.locator);
1550
+ if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
1551
+ const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
1552
+ repairable.set(room.locator, reset);
1553
+ repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
1554
+ }
1555
+ else
1556
+ repairable.delete(room.locator);
1263
1557
  if (withheld.kind === 'scoped-refusal') {
1264
1558
  await scoped(withheld, room, local);
1265
1559
  }
@@ -1316,6 +1610,7 @@ export function composeRuntime(options) {
1316
1610
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1317
1611
  }
1318
1612
  async function adoptAccepted(room, local, submitted, accepted) {
1613
+ await referenced(room.locator);
1319
1614
  if (!continuing())
1320
1615
  return unconfirmed;
1321
1616
  const current = replicas.get(room.locator);
@@ -1332,16 +1627,17 @@ export function composeRuntime(options) {
1332
1627
  if (applied.kind !== 'staged') {
1333
1628
  const state = pendings.get(submitted.id);
1334
1629
  await local.accept(room.locator, submitted.id, accepted.roomVersion);
1335
- if (state !== undefined)
1630
+ if (state !== undefined && !fences.has(room.locator))
1336
1631
  pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1337
1632
  emitQueue(room.locator);
1338
1633
  return applied;
1339
1634
  }
1340
- const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1635
+ const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1341
1636
  pendings.delete(submitted.id);
1342
1637
  unreadable.delete(room.locator);
1343
1638
  sourceRefusals.delete(room.locator);
1344
1639
  declarationRefusals.delete(room.locator);
1640
+ const set = settled();
1345
1641
  list(room, true);
1346
1642
  publishChangeSet(set);
1347
1643
  moveTo({ ...status, ...attention() });
@@ -1355,14 +1651,14 @@ export function composeRuntime(options) {
1355
1651
  refusedSource(room, failed);
1356
1652
  return false;
1357
1653
  }
1358
- async function submitRoom(room, local, authorized, progress) {
1359
- while (continuing() && authorization !== undefined) {
1654
+ async function submitRoom(room, local, authorized, progress, signal) {
1655
+ while (continuing() && authorization !== undefined && !signal.aborted) {
1360
1656
  const replica = replicas.get(room.locator);
1361
1657
  const head = replica?.queue[0];
1362
1658
  if (replica === undefined || head === undefined)
1363
- return;
1659
+ return 'done';
1364
1660
  if (pendings.get(head.id)?.accepted !== undefined)
1365
- return;
1661
+ return 'done';
1366
1662
  const state = pendings.get(head.id);
1367
1663
  const { submission, lost: earlier } = state ?? {
1368
1664
  submission: { state: 'waiting' },
@@ -1370,24 +1666,30 @@ export function composeRuntime(options) {
1370
1666
  };
1371
1667
  if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
1372
1668
  progress.complete = false;
1373
- return;
1669
+ return { at: Date.parse(submission.reset) };
1374
1670
  }
1375
1671
  if (writesHeld()) {
1376
1672
  progress.complete = false;
1377
1673
  journal.record(`submission ${room.locator} ${head.id} held`);
1378
- return;
1674
+ return 'done';
1379
1675
  }
1380
1676
  const lost = earlier || submission.state === 'submitted';
1677
+ if (lost && pastHorizon(head.id)) {
1678
+ await discardPending(room, local, signal);
1679
+ return 'done';
1680
+ }
1381
1681
  let result;
1382
1682
  journal.record(`submission ${room.locator} ${head.id}`);
1383
1683
  await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1384
- if (discoveryPaused)
1385
- return;
1386
- if (writesHeld()) {
1684
+ if (discoveryPaused || fences.has(room.locator))
1685
+ return 'done';
1686
+ if (signal.aborted || writesHeld()) {
1387
1687
  await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
1688
+ if (signal.aborted)
1689
+ return 'done';
1388
1690
  progress.complete = false;
1389
1691
  journal.record(`submission ${room.locator} ${head.id} held`);
1390
- return;
1692
+ return 'done';
1391
1693
  }
1392
1694
  const opensBefore = openSignals;
1393
1695
  const answered = awaitingAnswer(room.locator);
@@ -1403,32 +1705,36 @@ export function composeRuntime(options) {
1403
1705
  progress.complete = false;
1404
1706
  if (continuing())
1405
1707
  progress.reachable = false;
1406
- return;
1708
+ return 'again';
1709
+ }
1710
+ if (signal.aborted) {
1711
+ answered();
1712
+ return 'done';
1407
1713
  }
1408
1714
  if (result.kind !== 'accepted' || !continuing())
1409
1715
  answered();
1410
1716
  if (!continuing())
1411
- return;
1717
+ return 'done';
1412
1718
  if (result.kind === 'transport-uncertainty') {
1413
1719
  progress.complete = false;
1414
1720
  progress.reachable = false;
1415
- return;
1721
+ return 'again';
1416
1722
  }
1417
1723
  if (result.kind === 'room-not-readable') {
1418
1724
  progress.complete = false;
1419
1725
  unreadable.add(room.locator);
1420
1726
  list(room, false);
1421
1727
  emitRooms();
1422
- return;
1728
+ return 'done';
1423
1729
  }
1424
1730
  if (result.kind === 'revoked-authorization') {
1425
1731
  await refuse('revoked-authorization');
1426
- return;
1732
+ return 'done';
1427
1733
  }
1428
1734
  if (result.kind === 'scoped-refusal') {
1429
1735
  await scoped(result, room, local);
1430
1736
  progress.complete = false;
1431
- return;
1737
+ return 'done';
1432
1738
  }
1433
1739
  if (result.kind === 'write-halted') {
1434
1740
  journal.record(`outcome ${room.locator} ${head.id} write-halted`);
@@ -1437,16 +1743,24 @@ export function composeRuntime(options) {
1437
1743
  continue;
1438
1744
  haltedRefusal = true;
1439
1745
  progress.complete = false;
1440
- return;
1746
+ return 'done';
1441
1747
  }
1442
1748
  if (result.kind === 'conflicted' ||
1443
1749
  result.kind === 'rejected' ||
1444
1750
  result.kind === 'expired' ||
1445
1751
  result.kind === 'retryable-refusal') {
1752
+ if (result.kind === 'expired' && lost && result.restored !== true) {
1753
+ journal.record(`outcome ${room.locator} ${head.id} expired`);
1754
+ await discardPending(room, local, signal);
1755
+ return 'done';
1756
+ }
1446
1757
  if (result.kind === 'expired') {
1447
- const withheld = await advance(room, local, authorized);
1448
- if (withheld !== null)
1449
- return withhold(withheld, room, local, progress);
1758
+ await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
1759
+ const withheld = await advance(room, local, authorized, continuing);
1760
+ if (withheld !== null) {
1761
+ await withhold(withheld, room, local, progress);
1762
+ return 'done';
1763
+ }
1450
1764
  }
1451
1765
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
1452
1766
  (result.kind === 'conflicted' && result.roomVersion !== undefined
@@ -1459,8 +1773,7 @@ export function composeRuntime(options) {
1459
1773
  const reset = result.reset;
1460
1774
  progress.complete = false;
1461
1775
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1462
- armTimer();
1463
- return;
1776
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
1464
1777
  }
1465
1778
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
1466
1779
  const left = leaving(head, result);
@@ -1471,37 +1784,82 @@ export function composeRuntime(options) {
1471
1784
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
1472
1785
  return;
1473
1786
  }
1474
- const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1787
+ const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1475
1788
  leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1476
1789
  (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1477
1790
  ? { ...left, returned: null }
1478
1791
  : left,
1479
1792
  });
1480
- publishChangeSet(set);
1793
+ publishChangeSet(settled());
1481
1794
  moveTo({ ...status, ...attention() });
1482
1795
  });
1483
1796
  if (result.kind === 'expired')
1484
1797
  continue;
1485
- const withheld = await advance(room, local, authorized);
1486
- if (withheld !== null)
1487
- return withhold(withheld, room, local, progress);
1798
+ const withheld = await advance(room, local, authorized, continuing);
1799
+ if (withheld !== null) {
1800
+ await withhold(withheld, room, local, progress);
1801
+ return 'done';
1802
+ }
1488
1803
  continue;
1489
1804
  }
1490
1805
  const kept = await adopted(room, local, head, result).finally(answered);
1491
1806
  if (!kept) {
1492
1807
  progress.complete = false;
1493
- return;
1808
+ return 'done';
1494
1809
  }
1495
1810
  }
1811
+ return 'done';
1812
+ }
1813
+ function pastHorizon(identity) {
1814
+ return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
1815
+ }
1816
+ async function discardPending(room, local, signal) {
1817
+ const { locator } = room;
1818
+ const discarded = await exclusive(async () => {
1819
+ if (fences.has(locator))
1820
+ return false;
1821
+ const before = replicas.get(locator);
1822
+ const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
1823
+ fences.set(locator, { at: discoveries, erased: false });
1824
+ lastFenced.set(locator, ++fencings);
1825
+ try {
1826
+ await local.discard(locator);
1827
+ }
1828
+ finally {
1829
+ fences.delete(locator);
1830
+ }
1831
+ for (const batch of queue)
1832
+ pendings.delete(batch.id);
1833
+ queuesWithoutReplica.delete(locator);
1834
+ const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
1835
+ if (after !== null)
1836
+ replicas.set(locator, after);
1837
+ journal.record(`room discarded ${locator}`);
1838
+ publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
1839
+ moveTo({ ...status, ...attention() });
1840
+ return true;
1841
+ });
1842
+ if (!discarded)
1843
+ return;
1844
+ const progress = { complete: true, reachable: true };
1845
+ await roomTask(locator, async (live) => {
1846
+ await Promise.race([
1847
+ reload(room, local, progress, () => live() && !signal.aborted),
1848
+ abandonedOn(signal),
1849
+ ]);
1850
+ });
1851
+ if (signal.aborted)
1852
+ return;
1853
+ await rest(progress);
1496
1854
  }
1497
1855
  function awaitingAnswer(locator) {
1498
1856
  let release = () => { };
1499
1857
  const awaited = new Promise((resolve) => {
1500
1858
  release = resolve;
1501
1859
  });
1502
- answering.set(locator, awaited);
1860
+ answering.set(locator, { generation, answered: awaited });
1503
1861
  return () => {
1504
- if (answering.get(locator) === awaited)
1862
+ if (answering.get(locator)?.answered === awaited)
1505
1863
  answering.delete(locator);
1506
1864
  release();
1507
1865
  };
@@ -1617,8 +1975,10 @@ export function composeRuntime(options) {
1617
1975
  ];
1618
1976
  });
1619
1977
  }
1620
- async function synchronize(listed, local, progress, attempt) {
1978
+ async function synchronize(listed, local, progress, attempt, live) {
1621
1979
  const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1980
+ if (!live())
1981
+ return;
1622
1982
  const ready = listed.filter((_, index) => {
1623
1983
  const outcome = prepared[index];
1624
1984
  return outcome?.status === 'fulfilled' && outcome.value;
@@ -1627,9 +1987,10 @@ export function composeRuntime(options) {
1627
1987
  options.adapter.roomStream?.hold(heldRooms(ready));
1628
1988
  handed = true;
1629
1989
  }
1990
+ discovering?.release();
1630
1991
  const loaded = await Promise.allSettled(ready
1631
1992
  .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1632
- .map((room) => loadRoom(room, local, progress)));
1993
+ .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
1633
1994
  const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1634
1995
  if (failed !== undefined)
1635
1996
  throw failed.reason;
@@ -1658,14 +2019,14 @@ export function composeRuntime(options) {
1658
2019
  }
1659
2020
  return true;
1660
2021
  }
1661
- async function caughtUp(room, local, progress) {
2022
+ async function caughtUp(room, local, progress, live) {
1662
2023
  const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1663
- if (held === undefined || authorization === undefined)
2024
+ if (held === undefined || authorization === undefined || !live())
1664
2025
  return;
1665
2026
  const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1666
2027
  if (!behind)
1667
2028
  return;
1668
- const withheld = await advance(room, local, authorization);
2029
+ const withheld = await advance(room, local, authorization, live);
1669
2030
  if (withheld !== null)
1670
2031
  await withhold(withheld, room, local, progress);
1671
2032
  }
@@ -1675,39 +2036,46 @@ export function composeRuntime(options) {
1675
2036
  return model.status === 'unavailable' && model.reason === 'revoked';
1676
2037
  });
1677
2038
  }
1678
- async function loadRoom(room, local, progress) {
1679
- if (discoveryPaused || authorization === undefined)
2039
+ async function loadRoom(room, local, progress, live) {
2040
+ if (!live() || authorization === undefined)
1680
2041
  return;
1681
2042
  if (replicas.has(room.locator) && revoked(room.locator)) {
1682
- await reload(room, local, progress);
2043
+ await reload(room, local, progress, live);
1683
2044
  return;
1684
2045
  }
1685
- const withheld = await advance(room, local, authorization);
2046
+ const withheld = await advance(room, local, authorization, live);
1686
2047
  if (withheld !== null) {
1687
2048
  await withhold(withheld, room, local, progress);
1688
2049
  return;
1689
2050
  }
1690
- await caughtUp(room, local, progress);
2051
+ await caughtUp(room, local, progress, live);
1691
2052
  }
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;
2053
+ async function reload(room, local, progress, live) {
2054
+ try {
2055
+ if (!live() || authorization === undefined)
2056
+ return;
2057
+ const bootstrapped = await pullBootstrap(authorization, room, local, live);
2058
+ if (!live())
2059
+ return;
2060
+ if (bootstrapped.kind !== 'staged') {
2061
+ await withhold(bootstrapped, room, local, progress);
2062
+ return;
2063
+ }
2064
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
2065
+ await caughtUp(room, local, progress, live);
2066
+ }
2067
+ finally {
2068
+ caughtUpTo(room.locator);
1701
2069
  }
1702
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1703
- await caughtUp(room, local, progress);
1704
2070
  }
1705
- async function discoverRooms(local, progress, attempt) {
2071
+ async function discoverRooms(local, progress, attempt, live) {
1706
2072
  if (closed || authorization === undefined) {
1707
2073
  notifyReady();
1708
2074
  return;
1709
2075
  }
1710
2076
  moveTo(transferring(status));
2077
+ discoveries += 1;
2078
+ const started = discoveries;
1711
2079
  let result;
1712
2080
  try {
1713
2081
  result = await options.adapter.discover(authorization);
@@ -1715,7 +2083,8 @@ export function composeRuntime(options) {
1715
2083
  catch {
1716
2084
  result = { kind: 'transport-uncertainty' };
1717
2085
  }
1718
- if (closed) {
2086
+ if (!live()) {
2087
+ progress.complete = false;
1719
2088
  notifyReady();
1720
2089
  return;
1721
2090
  }
@@ -1773,6 +2142,7 @@ export function composeRuntime(options) {
1773
2142
  }
1774
2143
  const previousCapabilities = capabilities;
1775
2144
  capabilities = result.capabilities;
2145
+ outcomeRetention = result.retention.outcome;
1776
2146
  notifyReady();
1777
2147
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1778
2148
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
@@ -1785,8 +2155,7 @@ export function composeRuntime(options) {
1785
2155
  member = signedIn;
1786
2156
  emitMember();
1787
2157
  }
1788
- await applyDiscovery(result.rooms, local);
1789
- await synchronize(result.rooms, local, progress, attempt);
2158
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
1790
2159
  }
1791
2160
  async function ensureAuthorization() {
1792
2161
  if (authorization !== undefined)
@@ -1800,20 +2169,50 @@ export function composeRuntime(options) {
1800
2169
  throw error;
1801
2170
  }
1802
2171
  }
1803
- function task(work) {
2172
+ async function admittedRead(work) {
2173
+ if (roomReads.generation !== generation) {
2174
+ roomReads = { generation, free: ROOM_READS, waiting: [] };
2175
+ }
2176
+ const reads = roomReads;
2177
+ while (reads.free === 0)
2178
+ await new Promise((admit) => reads.waiting.push(admit));
2179
+ reads.free -= 1;
2180
+ try {
2181
+ await work();
2182
+ }
2183
+ finally {
2184
+ reads.free += 1;
2185
+ reads.waiting.shift()?.();
2186
+ }
2187
+ }
2188
+ function roomTask(locator, work) {
2189
+ return task(roomLane(locator), (live) => admittedRead(() => work(live)));
2190
+ }
2191
+ function task(lane, work) {
1804
2192
  if (stopping)
1805
- return chain;
1806
- chain = chain.then(work, work).catch(() => undefined);
1807
- return chain;
2193
+ return Promise.resolve();
2194
+ const held = lanes.get(lane);
2195
+ const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
2196
+ const turn = current.generation;
2197
+ const live = () => turn === generation && continuing();
2198
+ const run = current.chain.then(() => work(live)).catch(() => undefined);
2199
+ current.chain = run;
2200
+ lanes.set(lane, current);
2201
+ working.add(run);
2202
+ void run.finally(() => working.delete(run));
2203
+ return run;
1808
2204
  }
1809
2205
  function sync(attempt) {
1810
- return task(async () => {
1811
- if (closed || stopping || discoveryPaused) {
2206
+ return task(OPENING_LANE, async (live) => {
2207
+ if (!live()) {
1812
2208
  notifyReady();
1813
2209
  return;
1814
2210
  }
1815
2211
  const progress = { complete: true, reachable: true };
1816
- underway = attempt === 'opened' ? progress : null;
2212
+ const gate = discover();
2213
+ if (attempt === 'opened')
2214
+ opened = generation;
2215
+ underway = attempt === 'opened' ? { generation, progress } : null;
1817
2216
  if (attempt === 'opened')
1818
2217
  streamed = progress;
1819
2218
  complete = false;
@@ -1822,12 +2221,12 @@ export function composeRuntime(options) {
1822
2221
  handed = false;
1823
2222
  try {
1824
2223
  const local = await openLocal();
1825
- if (closed)
2224
+ if (!live())
1826
2225
  return;
1827
2226
  await ensureAuthorization();
1828
- if (closed)
2227
+ if (!live())
1829
2228
  return;
1830
- await discoverRooms(local, progress, attempt);
2229
+ await discoverRooms(local, progress, attempt, live);
1831
2230
  }
1832
2231
  catch {
1833
2232
  progress.complete = false;
@@ -1835,17 +2234,21 @@ export function composeRuntime(options) {
1835
2234
  progress.reachable = false;
1836
2235
  }
1837
2236
  finally {
2237
+ gate.release();
1838
2238
  notifyReady();
1839
2239
  }
2240
+ if (!live())
2241
+ return;
1840
2242
  if (attempt !== 'opened') {
1841
- await finishOpening(progress);
2243
+ await finishOpening(progress, live);
1842
2244
  return;
1843
2245
  }
1844
2246
  if (handed || closed || progress.reachable)
1845
2247
  return;
1846
2248
  underway = null;
2249
+ opened = null;
1847
2250
  journal.record('connection reopen');
1848
- await rest(progress);
2251
+ await rest(progress, live);
1849
2252
  options.adapter.roomStream?.reopen();
1850
2253
  });
1851
2254
  }
@@ -1866,24 +2269,41 @@ export function composeRuntime(options) {
1866
2269
  });
1867
2270
  }
1868
2271
  function applyOpening(expired) {
1869
- return task(async () => {
1870
- const progress = underway;
2272
+ const progress = underway?.generation === generation ? underway.progress : null;
2273
+ const cycle = progress ?? { complete: true, reachable: true };
2274
+ const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
2275
+ return task(OPENING_LANE, async (live) => {
1871
2276
  underway = null;
1872
- if (closed || stopping)
2277
+ if (!live())
1873
2278
  return;
1874
2279
  if (progress === null) {
1875
- await reloadExpired(expired);
2280
+ await replacing;
2281
+ await rest(cycle, live);
1876
2282
  return;
1877
2283
  }
2284
+ await roomsCarried(expired);
2285
+ if (!live())
2286
+ return;
2287
+ if (opened === generation)
2288
+ opened = null;
2289
+ const replaced = new Set(owed);
2290
+ owed.clear();
1878
2291
  try {
1879
2292
  const local = await openLocal();
1880
2293
  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)));
2294
+ const reloading = held.filter((room) => !expired.includes(room.locator) &&
2295
+ (replaced.has(room.locator) || revoked(room.locator)));
2296
+ for (const room of reloading)
2297
+ catching.add(room.locator);
2298
+ readSide.recover();
2299
+ const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
2300
+ const reloaded = [...(await reloads), ...(await replacing)];
1883
2301
  if (progress.reachable) {
1884
- for (const room of held)
1885
- if (!reloading.includes(room))
2302
+ for (const room of held) {
2303
+ if (!expired.includes(room.locator) && !reloading.includes(room)) {
1886
2304
  await confirmModels(room, local);
2305
+ }
2306
+ }
1887
2307
  }
1888
2308
  const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1889
2309
  if (failed !== undefined)
@@ -1893,21 +2313,21 @@ export function composeRuntime(options) {
1893
2313
  progress.complete = false;
1894
2314
  progress.reachable = false;
1895
2315
  }
1896
- await finishOpening(progress);
2316
+ if (live())
2317
+ await finishOpening(progress, live);
1897
2318
  });
1898
2319
  }
1899
- async function reloadExpired(expired) {
1900
- const progress = { complete: true, reachable: true };
2320
+ async function reloadNow(replaced, progress) {
1901
2321
  try {
1902
2322
  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)));
2323
+ return await Promise.allSettled([...discovered.values()]
2324
+ .filter((room) => replicas.has(room.locator) && replaced(room))
2325
+ .map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
1906
2326
  }
1907
2327
  catch {
1908
2328
  progress.reachable = false;
2329
+ return [];
1909
2330
  }
1910
- await rest(progress);
1911
2331
  }
1912
2332
  async function confirmModels(room, local) {
1913
2333
  for (const attached of ['collection', 'typed-selection']) {
@@ -1920,38 +2340,94 @@ export function composeRuntime(options) {
1920
2340
  function submitLater(locator) {
1921
2341
  if (localSaves)
1922
2342
  return;
1923
- const running = submitting.get(locator);
1924
- if (running !== undefined) {
1925
- running.again = true;
2343
+ void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
2344
+ }
2345
+ async function submitPass(locator, signal) {
2346
+ const room = discovered.get(locator);
2347
+ if (closed || stopping || discoveryPaused || room === undefined)
2348
+ return 'done';
2349
+ const progress = { complete: true, reachable: true };
2350
+ let next;
2351
+ try {
2352
+ await ensureAuthorization();
2353
+ if (authorization === undefined)
2354
+ return 'done';
2355
+ next = await submitRoom(room, await openLocal(), authorization, progress, signal);
2356
+ }
2357
+ catch {
2358
+ progress.reachable = false;
2359
+ next = 'again';
2360
+ }
2361
+ if (signal.aborted)
2362
+ return 'done';
2363
+ await rest(progress);
2364
+ return next === 'again' && !socketOpen ? 'done' : next;
2365
+ }
2366
+ function repairLater(locator, when) {
2367
+ if (!socketOpen || repairing.has(locator))
1926
2368
  return;
2369
+ void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
2370
+ }
2371
+ async function repairPass(locator, signal) {
2372
+ const room = discovered.get(locator);
2373
+ if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
2374
+ return 'done';
2375
+ if (room === undefined || room.replica.admission !== 'admitted')
2376
+ return 'done';
2377
+ const progress = { complete: true, reachable: true };
2378
+ repairing.add(locator);
2379
+ try {
2380
+ const local = await openLocal();
2381
+ await roomTask(locator, async (live) => {
2382
+ const current = () => live() && !signal.aborted;
2383
+ if (!current() || authorization === undefined)
2384
+ return;
2385
+ const withheld = await Promise.race([
2386
+ advance(room, local, authorization, current),
2387
+ abandonedOn(signal),
2388
+ ]);
2389
+ if (withheld !== null && current())
2390
+ await withhold(withheld, room, local, progress);
2391
+ });
1927
2392
  }
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
- })();
2393
+ catch {
2394
+ stale.add(locator);
2395
+ progress.reachable = false;
2396
+ }
2397
+ finally {
2398
+ repairing.delete(locator);
2399
+ }
2400
+ if (signal.aborted)
2401
+ return 'done';
2402
+ await rest(progress);
2403
+ if (!stale.has(locator)) {
2404
+ if ((replicas.get(locator)?.queue.length ?? 0) > 0)
2405
+ submitLater(locator);
2406
+ return 'done';
2407
+ }
2408
+ if (!repairable.has(locator))
2409
+ return 'done';
2410
+ const reset = repairable.get(locator);
2411
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
2412
+ }
2413
+ function resendLater() {
2414
+ if (!socketOpen)
2415
+ return;
2416
+ void recovery.attempt('resend', resendPass, { again: true });
2417
+ }
2418
+ async function resendPass(signal) {
2419
+ if (!socketOpen || closed || stopping || discoveryPaused)
2420
+ return 'done';
2421
+ const reading = { complete: true, reachable: true };
2422
+ try {
2423
+ await conversations.resend(reading);
2424
+ }
2425
+ catch {
2426
+ reading.reachable = false;
2427
+ }
2428
+ if (signal.aborted)
2429
+ return 'done';
2430
+ return reading.reachable && reading.complete ? 'done' : 'again';
1955
2431
  }
1956
2432
  function submitAll() {
1957
2433
  for (const [locator, replica] of replicas) {
@@ -1959,31 +2435,40 @@ export function composeRuntime(options) {
1959
2435
  submitLater(locator);
1960
2436
  }
1961
2437
  }
1962
- async function finishOpening(progress) {
2438
+ async function finishOpening(progress, live) {
1963
2439
  try {
1964
- if (closed || discoveryPaused) {
2440
+ if (!live()) {
1965
2441
  progress.complete = false;
1966
2442
  return;
1967
2443
  }
1968
2444
  submitAll();
2445
+ for (const locator of stale)
2446
+ if (repairable.has(locator))
2447
+ repairLater(locator, {});
1969
2448
  await conversations.reconcile();
1970
2449
  if (status.providerUnreachable)
1971
2450
  conversations.reconnected();
1972
2451
  const reading = { complete: true, reachable: true };
1973
2452
  await conversations.catchUp(reading);
1974
- if (!reading.reachable)
2453
+ if (progress.reachable && reading.reachable)
2454
+ await conversations.resend(reading);
2455
+ if (!reading.reachable) {
1975
2456
  progress.reachable = false;
2457
+ resendLater();
2458
+ }
1976
2459
  }
1977
2460
  catch {
1978
2461
  progress.complete = false;
1979
2462
  }
1980
2463
  finally {
1981
- complete = progress.complete;
1982
- await rest(progress);
2464
+ if (live()) {
2465
+ complete = progress.complete;
2466
+ await rest(progress, live);
2467
+ }
1983
2468
  }
1984
2469
  }
1985
- async function rest(progress) {
1986
- if (closed)
2470
+ async function rest(progress, live = () => !closed) {
2471
+ if (!live())
1987
2472
  return;
1988
2473
  let lastCompleted = status.lastCompleted;
1989
2474
  let synced = complete && stale.size === 0 && !pending();
@@ -1991,6 +2476,8 @@ export function composeRuntime(options) {
1991
2476
  const completedAt = new Date().toISOString();
1992
2477
  try {
1993
2478
  await (await openLocal()).complete(completedAt);
2479
+ if (!live())
2480
+ return;
1994
2481
  recorded = true;
1995
2482
  lastCompleted = completedAt;
1996
2483
  }
@@ -2005,11 +2492,23 @@ export function composeRuntime(options) {
2005
2492
  ...attention(),
2006
2493
  });
2007
2494
  }
2495
+ function discover() {
2496
+ let release = () => { };
2497
+ const done = new Promise((resolve) => {
2498
+ release = resolve;
2499
+ });
2500
+ discovering = { generation, done, release };
2501
+ return discovering;
2502
+ }
2008
2503
  function applyRoom(locator, roomVersion) {
2009
2504
  announced.set(locator, roomVersion);
2010
- return task(async () => {
2011
- await answering.get(locator);
2012
- if (closed || stopping || discoveryPaused || authorization === undefined)
2505
+ return task(roomLane(locator), async (live) => {
2506
+ if (discovering?.generation === generation)
2507
+ await discovering.done;
2508
+ const saving = answering.get(locator);
2509
+ if (saving?.generation === generation)
2510
+ await saving.answered;
2511
+ if (!live() || authorization === undefined)
2013
2512
  return;
2014
2513
  const room = discovered.get(locator);
2015
2514
  const held = replicas.get(locator);
@@ -2020,25 +2519,41 @@ export function composeRuntime(options) {
2020
2519
  options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
2021
2520
  return;
2022
2521
  }
2023
- const during = underway;
2522
+ const during = underway?.generation === generation ? underway.progress : null;
2024
2523
  const progress = during ?? { complete: true, reachable: true };
2025
2524
  if (during === null)
2026
2525
  moveTo(transferring(status));
2027
2526
  try {
2028
2527
  const local = await openLocal();
2029
- const withheld = await advance(room, local, authorization);
2030
- if (withheld !== null)
2031
- await withhold(withheld, room, local, progress);
2528
+ await admittedRead(async () => {
2529
+ if (!live() || authorization === undefined)
2530
+ return;
2531
+ const withheld = await advance(room, local, authorization, live);
2532
+ if (withheld !== null)
2533
+ await withhold(withheld, room, local, progress);
2534
+ });
2032
2535
  }
2033
2536
  catch {
2034
2537
  stale.add(locator);
2035
2538
  }
2036
2539
  finally {
2037
2540
  if (during === null)
2038
- await rest(progress);
2541
+ await rest(progress, live);
2039
2542
  }
2040
2543
  });
2041
2544
  }
2545
+ function roomsCarried(replacing) {
2546
+ const replaced = new Set(replacing.map(roomLane));
2547
+ return Promise.all([...lanes]
2548
+ .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
2549
+ .map(([, held]) => held.chain));
2550
+ }
2551
+ const receipts = {
2552
+ heads: async () => (await openLocal()).receivedHeads(),
2553
+ parts: async (key, count) => (await openLocal()).receivedParts(key, count),
2554
+ keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
2555
+ drop: async (keys) => (await openLocal()).dropReceived(keys),
2556
+ };
2042
2557
  function observeStream() {
2043
2558
  const stream = options.adapter.roomStream;
2044
2559
  if (stream === undefined || withdrawStream !== undefined)
@@ -2048,6 +2563,9 @@ export function composeRuntime(options) {
2048
2563
  return;
2049
2564
  if (event.kind === 'opened') {
2050
2565
  socketOpen = true;
2566
+ generation += 1;
2567
+ conversations.reopened();
2568
+ recovery.resume();
2051
2569
  void sync('opened');
2052
2570
  }
2053
2571
  else if (event.kind === 'closed')
@@ -2056,7 +2574,7 @@ export function composeRuntime(options) {
2056
2574
  void applyOpening(event.expired);
2057
2575
  else
2058
2576
  void applyRoom(event.locator, event.roomVersion);
2059
- });
2577
+ }, receipts);
2060
2578
  }
2061
2579
  function writesHeld() {
2062
2580
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -2094,14 +2612,15 @@ export function composeRuntime(options) {
2094
2612
  });
2095
2613
  }
2096
2614
  function scheduleCurrency() {
2097
- if (currencyQueued || closed || stopping)
2615
+ if (currencyQueued === generation || closed || stopping)
2098
2616
  return;
2099
- currencyQueued = true;
2100
- void task(runCurrency);
2617
+ currencyQueued = generation;
2618
+ void task(CONVERSATION_LANE, runCurrency);
2101
2619
  }
2102
- async function runCurrency() {
2103
- currencyQueued = false;
2104
- if (closed || stopping || discoveryPaused)
2620
+ async function runCurrency(live) {
2621
+ if (currencyQueued === generation)
2622
+ currencyQueued = null;
2623
+ if (!live())
2105
2624
  return;
2106
2625
  await conversations.reconcile();
2107
2626
  if (conversations.current()) {
@@ -2116,7 +2635,7 @@ export function composeRuntime(options) {
2116
2635
  catch {
2117
2636
  progress.reachable = false;
2118
2637
  }
2119
- await rest(progress);
2638
+ await rest(progress, live);
2120
2639
  }
2121
2640
  function close() {
2122
2641
  if (closed)
@@ -2128,7 +2647,7 @@ export function composeRuntime(options) {
2128
2647
  withdrawConversations = undefined;
2129
2648
  withdrawServiceMode?.();
2130
2649
  conversations.close();
2131
- stopTimer();
2650
+ void recovery.retire();
2132
2651
  store?.close();
2133
2652
  void opening?.then((opened) => {
2134
2653
  opened.close();
@@ -2146,9 +2665,8 @@ export function composeRuntime(options) {
2146
2665
  }
2147
2666
  async function stop() {
2148
2667
  stopping = true;
2149
- stopTimer();
2150
- await chain.catch(() => undefined);
2151
- await Promise.all([...submitting.values()].map((run) => run.done));
2668
+ await recovery.retire();
2669
+ await Promise.all(working);
2152
2670
  await mutations;
2153
2671
  close();
2154
2672
  }
@@ -2170,17 +2688,54 @@ export function composeRuntime(options) {
2170
2688
  return planOf(input, roomFacts(reach), planReach(reach));
2171
2689
  }
2172
2690
  function watchPlan(plan, deliver) {
2173
- materialize(reachedRooms(plan));
2174
- return readSide.watch(plan, identityOf(plan), deliver);
2691
+ return readSide.watch(plan, identityOf(plan), () => {
2692
+ const cover = covered(plan);
2693
+ awaitPreparation(cover);
2694
+ return cover;
2695
+ }, deliver);
2175
2696
  }
2176
2697
  async function searched(input, reach) {
2177
2698
  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);
2699
+ const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
2700
+ for (;;) {
2701
+ const cover = covered(filter);
2702
+ awaitPreparation(cover);
2703
+ const pending = 'pending' in cover ? [...cover.pending] : [];
2704
+ const covering = 'pending' in cover ? [] : cover.coverage.rooms;
2705
+ const unindexed = covering
2706
+ .filter(({ room }) => !searchIndex.rooms().has(room))
2707
+ .map(({ room }) => room);
2708
+ for (const room of unindexed)
2709
+ pending.push({ room, awaits: 'index' });
2710
+ const held = conversations.searchable();
2711
+ const named = reach !== null && !reach.conversations
2712
+ ? []
2713
+ : (plan.conversations ?? held).filter((one) => held.includes(one) && conversations.acquired(one));
2714
+ const waiting = named.filter((one) => !searchIndex.conversations().has(one));
2715
+ for (const conversation of waiting)
2716
+ pending.push({ conversation, awaits: 'index' });
2717
+ if (unindexed.length > 0 || waiting.length > 0)
2718
+ index(unindexed, waiting);
2719
+ if (pending.length > 0 || 'pending' in cover)
2720
+ return { pending };
2721
+ const seen = searchIndex.changes();
2722
+ await searchIndex.caughtUp();
2723
+ const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(filter);
2724
+ if (selected !== null && 'pending' in selected)
2725
+ return selected;
2726
+ if (searchIndex.changes() !== seen ||
2727
+ JSON.stringify(covered(filter)) !== JSON.stringify(cover))
2728
+ continue;
2729
+ return {
2730
+ ...searchIndex.search(plan, selected === null ? null : selected.documents, named),
2731
+ coverage: {
2732
+ rooms: covering,
2733
+ conversations: named,
2734
+ index: searchIndex.generation(),
2735
+ overlay: cover.coverage.overlay,
2736
+ },
2737
+ };
2738
+ }
2184
2739
  }
2185
2740
  const watch = (topic, input, deliver) => {
2186
2741
  if (topic === 'room-drafts') {
@@ -2399,14 +2954,16 @@ export function composeRuntime(options) {
2399
2954
  if (refusal === null || !refusal.reauthorizationOffered)
2400
2955
  return;
2401
2956
  journal.record('connection reauthorize');
2402
- await chain.catch(() => undefined);
2957
+ await Promise.all([...lanes.values()]
2958
+ .filter((held) => held.generation === generation)
2959
+ .map((held) => held.chain));
2403
2960
  await mutations;
2404
2961
  authorization = undefined;
2405
2962
  discoveryPaused = false;
2406
2963
  if (options.adapter.roomStream === undefined)
2407
2964
  await sync('unstreamed');
2408
2965
  else
2409
- await task(ensureAuthorization);
2966
+ await task(OPENING_LANE, ensureAuthorization);
2410
2967
  },
2411
2968
  };
2412
2969
  const reachable = {
@@ -2418,5 +2975,5 @@ export function composeRuntime(options) {
2418
2975
  watchPlan,
2419
2976
  search: searched,
2420
2977
  };
2421
- return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2978
+ return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
2422
2979
  }