@agentshouse/mdruntime 0.1.0-c71223552f22c → 0.1.0-c766beb2a980a

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