@agentshouse/mdruntime 0.1.0-c8e110144a30a → 0.1.0-c93103f5357ff

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 (135) hide show
  1. package/AGENTS.md +262 -262
  2. package/dist/acquisition.d.ts +14 -0
  3. package/dist/acquisition.d.ts.map +1 -0
  4. package/dist/acquisition.js +24 -0
  5. package/dist/adapter.d.ts +37 -9
  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 +32 -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 +36 -10
  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 +3 -3
  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 +3 -3
  39. package/dist/client.d.ts +12 -8
  40. package/dist/client.d.ts.map +1 -1
  41. package/dist/client.js +12 -3
  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 +24 -7
  46. package/dist/connection.d.ts.map +1 -1
  47. package/dist/connection.js +1353 -318
  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 +17 -2
  60. package/dist/host.d.ts.map +1 -1
  61. package/dist/host.js +86 -9
  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 +9 -6
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +2 -0
  68. package/dist/memory.d.ts +4 -1
  69. package/dist/memory.d.ts.map +1 -1
  70. package/dist/memory.js +23 -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 +2 -0
  81. package/dist/overlay.d.ts.map +1 -1
  82. package/dist/overlay.js +58 -55
  83. package/dist/query.d.ts +35 -3
  84. package/dist/query.d.ts.map +1 -1
  85. package/dist/query.js +8 -2
  86. package/dist/reach.d.ts +6 -3
  87. package/dist/reach.d.ts.map +1 -1
  88. package/dist/reach.js +17 -5
  89. package/dist/react.d.ts +7 -4
  90. package/dist/react.d.ts.map +1 -1
  91. package/dist/react.js +6 -4
  92. package/dist/read-side.d.ts +13 -6
  93. package/dist/read-side.d.ts.map +1 -1
  94. package/dist/read-side.js +285 -83
  95. package/dist/readiness.d.ts +53 -0
  96. package/dist/readiness.d.ts.map +1 -0
  97. package/dist/readiness.js +33 -0
  98. package/dist/recovery.d.ts +25 -0
  99. package/dist/recovery.d.ts.map +1 -0
  100. package/dist/recovery.js +197 -0
  101. package/dist/refresh.d.ts.map +1 -1
  102. package/dist/refresh.js +17 -10
  103. package/dist/replica.d.ts +1 -1
  104. package/dist/replica.d.ts.map +1 -1
  105. package/dist/replica.js +57 -20
  106. package/dist/room-draft.d.ts +13 -1
  107. package/dist/room-draft.d.ts.map +1 -1
  108. package/dist/room-draft.js +2 -2
  109. package/dist/rows.d.ts +5 -13
  110. package/dist/rows.d.ts.map +1 -1
  111. package/dist/rows.js +5 -13
  112. package/dist/save.d.ts +6 -2
  113. package/dist/save.d.ts.map +1 -1
  114. package/dist/save.js +42 -18
  115. package/dist/search.d.ts +36 -6
  116. package/dist/search.d.ts.map +1 -1
  117. package/dist/search.js +132 -46
  118. package/dist/seed.d.ts +16 -0
  119. package/dist/seed.d.ts.map +1 -0
  120. package/dist/seed.js +61 -0
  121. package/dist/shared.d.ts +11 -0
  122. package/dist/shared.d.ts.map +1 -0
  123. package/dist/shared.js +368 -0
  124. package/dist/status.d.ts +4 -0
  125. package/dist/status.d.ts.map +1 -1
  126. package/dist/store.d.ts +42 -11
  127. package/dist/store.d.ts.map +1 -1
  128. package/dist/store.js +276 -83
  129. package/dist/turn.d.ts +7 -0
  130. package/dist/turn.d.ts.map +1 -0
  131. package/dist/turn.js +45 -0
  132. package/dist/wire.d.ts +136 -32
  133. package/dist/wire.d.ts.map +1 -1
  134. package/dist/wire.js +48 -31
  135. package/package.json +6 -2
@@ -1,30 +1,45 @@
1
1
  import { attachedModel } from './attached-model.js';
2
2
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
3
- import { observing } from './observation.js';
4
- import { filterOf, identityOf, planOf, } from './query.js';
3
+ import { observing, whole } from './observation.js';
4
+ import { filterOf, identityOf, planOf, queryKey, } from './query.js';
5
5
  import { createReadSide } from './read-side.js';
6
+ import { seededResult } from './seed.js';
6
7
  import { markdownPath, roomOfKey } from './rows.js';
7
8
  import { createSearchIndex, searchPlanOf, } from './search.js';
8
9
  import { bootstrapRoom } from './bootstrap.js';
9
- import { changeSet, mergeChangeSets } from './change-set.js';
10
+ import { changeSet, entering, mergeChangeSets } from './change-set.js';
10
11
  import { createConversationStore, } from './conversation.js';
11
12
  import { queuedFile } from './change.js';
12
- import { validRoot } from './collection.js';
13
+ import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
13
14
  import { runtimeError } from './failure.js';
14
15
  import { createLog } from './log.js';
15
16
  import { binaryRecord } from './binary.js';
16
- import { absentBinary, binaryAnswer, calledResult, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
17
+ import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
18
+ import { createRecovery } from './recovery.js';
17
19
  import { refreshRoom } from './refresh.js';
18
20
  import { buildBatch, joined, limitedIn, requeued } from './save.js';
19
21
  import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
20
22
  import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
21
23
  import { same, saving, STARTING, transferring, } from './status.js';
22
24
  import { detectStorageLoss, nextGeneration } from './durability.js';
23
- import { roomDraftRevision } from './identity.js';
25
+ import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
26
+ import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
27
+ import { FencedMap } from './fence.js';
28
+ import { tally } from './shared.js';
29
+ import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
30
+ import { identityTime, roomDraftRevision } from './identity.js';
24
31
  import { beneath, normalize } from './paths.js';
25
32
  import { openRuntimeStore, } from './store.js';
26
33
  import { unconfirmed } from './transfer.js';
27
34
  import { governs, holds, narrowed, planReach, } from './reach.js';
35
+ function roomsAlone(plan) {
36
+ return (plan.reach === null &&
37
+ plan.root.types === null &&
38
+ plan.root.where === null &&
39
+ plan.root.pathPrefix === null &&
40
+ plan.hop === null &&
41
+ plan.backlinks === null);
42
+ }
28
43
  function within(root, path) {
29
44
  return root === '' || beneath(root, path);
30
45
  }
@@ -38,23 +53,61 @@ function declaredSelection(room) {
38
53
  root !== '' &&
39
54
  selection.roots.every((other, at) => at === index || !within(other, root)));
40
55
  }
56
+ const OPENING_LANE = 'opening';
57
+ const CONVERSATION_LANE = 'conversations';
58
+ const ROOM_READS = 256;
59
+ const ROOM_ATTEMPTS = 16;
60
+ const outside = new Map();
61
+ function outsideOf(roots) {
62
+ const key = JSON.stringify(roots);
63
+ const held = outside.get(key);
64
+ if (held !== undefined)
65
+ return held;
66
+ const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
67
+ const made = {
68
+ documents: (path) => (markdownPath(path) ? counted(path) : 0),
69
+ binaries: counted,
70
+ };
71
+ outside.set(key, made);
72
+ return made;
73
+ }
74
+ function abandonedOn(signal) {
75
+ return new Promise((resolve) => {
76
+ if (signal.aborted)
77
+ resolve(null);
78
+ else
79
+ signal.addEventListener('abort', () => resolve(null), { once: true });
80
+ });
81
+ }
82
+ function roomLane(locator) {
83
+ return `room ${locator}`;
84
+ }
41
85
  export function composeRuntime(options) {
42
86
  const localSaves = options.adapter.localSaves === true;
43
87
  let authorization = options.authorization;
44
88
  let opening;
45
89
  let store;
46
- const replicas = new Map();
47
- const queuesWithoutReplica = new Map();
90
+ const fences = new Map();
91
+ const isFenced = (locator) => fences.has(locator);
92
+ let fencings = 0;
93
+ const lastFenced = new Map();
94
+ const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
95
+ let discoveries = 0;
96
+ const replicas = new FencedMap(isFenced);
97
+ const queuesWithoutReplica = new FencedMap(isFenced);
48
98
  const pendings = new Map();
49
- const roomDrafts = new Map();
99
+ const seeds = new Map();
100
+ const seedWatchers = new Map();
101
+ const roomDrafts = new FencedMap(isFenced);
50
102
  const presentations = new Map();
51
103
  let limits = [];
52
104
  let capabilities = [];
53
- const rooms = new Map();
105
+ const rooms = new FencedMap(isFenced);
54
106
  const counted = new Map();
55
107
  const unreadable = new Set();
56
- const models = new Map();
57
- const liveModels = new Map();
108
+ const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
109
+ const models = new FencedMap(isModelFenced);
110
+ const liveModels = new FencedMap(isModelFenced);
58
111
  const invalidModels = new Set();
59
112
  const discovered = new Map();
60
113
  let roomsListed = false;
@@ -84,7 +137,6 @@ export function composeRuntime(options) {
84
137
  let status = STARTING;
85
138
  let shown = STARTING;
86
139
  let discoveryPaused = false;
87
- let resetTimer;
88
140
  let closed = false;
89
141
  let stopping = false;
90
142
  let withdrawStream;
@@ -92,12 +144,21 @@ export function composeRuntime(options) {
92
144
  let underway = null;
93
145
  let streamed = null;
94
146
  let handed = false;
95
- const submitting = new Map();
147
+ const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
148
+ const repairing = new Set();
149
+ const repairable = new Map();
150
+ let outcomeRetention = null;
96
151
  const answering = new Map();
97
152
  let complete = false;
98
153
  let recorded = false;
99
154
  const stale = new Set();
100
- let chain = Promise.resolve();
155
+ let generation = 0;
156
+ let discovering = null;
157
+ let opened = null;
158
+ const owed = new Set();
159
+ const lanes = new Map();
160
+ let roomReads = { generation, free: ROOM_READS, waiting: [] };
161
+ const working = new Set();
101
162
  let mutations = Promise.resolve();
102
163
  const roomListeners = new Set();
103
164
  const refusalListeners = new Set();
@@ -108,7 +169,7 @@ export function composeRuntime(options) {
108
169
  const presentationListeners = new Set();
109
170
  const readSide = createReadSide();
110
171
  let withdrawConversations;
111
- let currencyQueued = false;
172
+ let currencyQueued = null;
112
173
  let socketOpen = false;
113
174
  let serviceMode = 'open';
114
175
  let haltedRefusal = false;
@@ -116,24 +177,41 @@ export function composeRuntime(options) {
116
177
  const serviceModeListeners = new Set();
117
178
  let member = null;
118
179
  const memberListeners = new Set();
119
- const materialized = new Set();
120
- let everyRoom = false;
121
- const transfers = new Map();
180
+ const prepared = new Set();
181
+ const projecting = new Set();
182
+ const unpreparedDemand = new Set();
183
+ const unindexable = new Set();
184
+ let preparing = false;
185
+ let demand = WHOLE_DEMAND;
186
+ let narrowing = null;
187
+ let targets = {};
188
+ let persistence = options.shell.persistence;
189
+ let capacity = null;
190
+ const paused = new Set();
191
+ let rechecking = null;
192
+ const readinessWatches = new Set();
193
+ let readinessQueued = false;
194
+ const catching = new Set();
195
+ const transfers = new FencedMap(isFenced);
122
196
  const conversations = createConversationStore({
123
197
  agents: declaredAgents,
124
- documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
198
+ documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
125
199
  call: (name, input) => invokeAction(name, input),
126
200
  durably,
127
201
  stream: () => options.adapter.conversations,
128
202
  publish: publishChangeSet,
203
+ online: () => !status.providerUnreachable && refusal === null,
129
204
  closed: () => closed,
205
+ unanswered: resendLater,
206
+ expired: (operation) => pastHorizon(operation),
130
207
  });
131
208
  const searchIndex = createSearchIndex({
132
- documents: () => readSide.documents(),
209
+ room: (room) => readSide.room(room),
133
210
  document: (room, path) => readSide.document(room, path),
134
- messages: () => conversations.messages(),
135
- message: (conversation, identity) => conversations.message(conversation, identity),
136
211
  });
212
+ const roomsToIndex = new Set();
213
+ const conversationsToIndex = new Set();
214
+ let indexing = false;
137
215
  const journal = createLog();
138
216
  let notifyReady = () => { };
139
217
  const ready = new Promise((resolve) => {
@@ -212,9 +290,16 @@ export function composeRuntime(options) {
212
290
  returned: structuredClone(record.returned),
213
291
  });
214
292
  }
293
+ for (const [room, refusal] of sourceRefusals)
294
+ attentionRequired.push({ room, refusal: structuredClone(refusal) });
215
295
  attentionRequired.sort((left, right) => {
216
296
  if (left.room !== right.room)
217
297
  return left.room < right.room ? -1 : 1;
298
+ if ('refusal' in left || 'refusal' in right) {
299
+ if ('refusal' in left && 'refusal' in right)
300
+ return 0;
301
+ return 'refusal' in left ? -1 : 1;
302
+ }
218
303
  if (left.path !== right.path)
219
304
  return left.path < right.path ? -1 : 1;
220
305
  return 0;
@@ -233,12 +318,9 @@ export function composeRuntime(options) {
233
318
  if (replica === undefined)
234
319
  return 0;
235
320
  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;
321
+ const weighed = outsideOf(roots);
322
+ return (tally(replica.visible.documents, weighed.documents) +
323
+ tally(replica.visible.binaries, weighed.binaries));
242
324
  }
243
325
  function listed() {
244
326
  return structuredClone([...rooms.values()].map((room) => {
@@ -265,15 +347,19 @@ export function composeRuntime(options) {
265
347
  return { listed: roomsListed, rooms: held };
266
348
  }
267
349
  function emitRooms() {
350
+ readSide.recover();
351
+ queueReadiness();
268
352
  const value = listed();
269
353
  for (const listener of roomListeners)
270
354
  listener(value);
271
355
  }
272
356
  function emitRefusal() {
357
+ queueReadiness();
273
358
  for (const listener of refusalListeners)
274
359
  listener(refusal);
275
360
  }
276
361
  function emitStorageLoss() {
362
+ queueReadiness();
277
363
  for (const listener of storageLossListeners)
278
364
  listener(storageLoss);
279
365
  }
@@ -290,7 +376,8 @@ export function composeRuntime(options) {
290
376
  function show() {
291
377
  if (closed)
292
378
  return;
293
- const next = status.state === 'synced' && !conversations.current()
379
+ queueReadiness();
380
+ const next = status.state === 'synced' && demand.conversations && !conversations.current()
294
381
  ? { ...status, state: 'syncing' }
295
382
  : status;
296
383
  if (same(shown, next))
@@ -319,42 +406,511 @@ export function composeRuntime(options) {
319
406
  }
320
407
  }
321
408
  function applyChangeSet(set) {
322
- const held = everyRoom
323
- ? set
324
- : { ...set, documents: set.documents.filter(({ locator }) => materialized.has(locator)) };
409
+ const held = {
410
+ ...set,
411
+ documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
412
+ };
325
413
  readSide.apply(held);
326
414
  searchIndex.changed(held);
327
415
  }
328
- function materialize(locators) {
329
- if (everyRoom)
416
+ async function referenced(locator) {
417
+ for (;;) {
418
+ const record = replicas.get(locator)?.confirmed.record;
419
+ if (record === undefined || closed)
420
+ return;
421
+ const held = { text: record.documents, binaries: record.binaries };
422
+ const composition = compositionOf(record);
423
+ if (referencesIndexed(held, composition))
424
+ return;
425
+ await indexReferrers(held, composition);
426
+ }
427
+ }
428
+ async function prepareReplica(locator) {
429
+ await referenced(locator);
430
+ const replica = replicas.get(locator);
431
+ if (replica === undefined || closed || prepared.has(locator))
330
432
  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));
433
+ const visible = replica.visible;
434
+ const paths = [
435
+ ...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
436
+ ];
437
+ const entered = [];
438
+ projecting.add(locator);
439
+ try {
440
+ await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
441
+ const current = replicas.get(locator);
442
+ if (current === undefined || closed)
443
+ return;
444
+ const set = entering(locator, current, chunk);
445
+ readSide.apply(set);
446
+ entered.push(set);
447
+ });
448
+ if (!replicas.has(locator) || closed)
449
+ return;
450
+ prepared.add(locator);
341
451
  }
342
- if (sets.length === 0)
452
+ finally {
453
+ projecting.delete(locator);
454
+ }
455
+ conversations.changed(mergeChangeSets(entered));
456
+ readSide.recover();
457
+ queueReadiness();
458
+ }
459
+ function unprepared() {
460
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
461
+ }
462
+ function unbuilt(locator, error) {
463
+ unindexable.add(locator);
464
+ journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
465
+ queueReadiness();
466
+ }
467
+ function prepare(locators = []) {
468
+ for (const locator of locators)
469
+ unpreparedDemand.add(locator);
470
+ if (preparing)
343
471
  return;
344
- const set = mergeChangeSets(sets);
345
- readSide.apply(set);
346
- searchIndex.changed(set);
472
+ preparing = true;
473
+ void (async () => {
474
+ try {
475
+ for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
476
+ await yielded();
477
+ if (closed)
478
+ return;
479
+ unpreparedDemand.delete(waiting);
480
+ await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
481
+ }
482
+ }
483
+ finally {
484
+ preparing = false;
485
+ }
486
+ })();
347
487
  }
348
488
  function reachedRooms(plan) {
349
- if (plan.rooms !== null)
350
- return plan.rooms;
351
- if (plan.reach === null)
489
+ const reach = plan.reach;
490
+ const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
491
+ const named = plan.rooms ??
492
+ (reached === null
493
+ ? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
494
+ : [...reached]);
495
+ return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
496
+ }
497
+ function awaited(locator) {
498
+ return (!roomsListed ||
499
+ (rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
500
+ }
501
+ function covered(plan) {
502
+ const locators = reachedRooms(plan);
503
+ if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
504
+ return { pending: [] };
505
+ }
506
+ const pending = [];
507
+ const coverage = [];
508
+ const overlay = [];
509
+ for (const locator of locators) {
510
+ const replica = replicas.get(locator);
511
+ if (replica === undefined) {
512
+ if (awaited(locator))
513
+ pending.push({ room: locator, awaits: 'acquisition' });
514
+ continue;
515
+ }
516
+ if (!prepared.has(locator)) {
517
+ pending.push({ room: locator, awaits: 'preparation' });
518
+ continue;
519
+ }
520
+ coverage.push({
521
+ room: locator,
522
+ generation: replica.confirmed.record.transfer,
523
+ current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
524
+ });
525
+ for (const batch of replica.queue)
526
+ overlay.push(batch.id);
527
+ }
528
+ if (pending.length > 0)
529
+ return { pending };
530
+ return { coverage: { rooms: coverage, indexes: [], overlay } };
531
+ }
532
+ function awaitPreparation(cover) {
533
+ if (!('pending' in cover))
534
+ return;
535
+ const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
536
+ if (waiting.length > 0)
537
+ prepare(waiting.map(({ room }) => room));
538
+ }
539
+ function acquires(room) {
540
+ return (demanded(demand, room) ||
541
+ (replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
542
+ queuesWithoutReplica.has(room.locator));
543
+ }
544
+ function acquiredLocators() {
545
+ return [...discovered.values()]
546
+ .filter(acquires)
547
+ .map((room) => room.locator)
548
+ .sort();
549
+ }
550
+ function heldDemand(acquired) {
551
+ if (demand.whole)
352
552
  return null;
353
- return [...plan.reach.rooms, ...plan.reach.documents.map(roomOfKey)];
553
+ return { rooms: acquired.map((room) => room.locator), conversations: demand.conversations };
554
+ }
555
+ function applyDemand(next) {
556
+ const before = acquiredLocators();
557
+ const conversed = demand.conversations;
558
+ demand = next;
559
+ const after = acquiredLocators();
560
+ queueReadiness();
561
+ if (!conversed && next.conversations)
562
+ scheduleCurrency();
563
+ if (JSON.stringify(before) === JSON.stringify(after) && conversed === next.conversations)
564
+ return;
565
+ journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
566
+ if (options.adapter.roomStream === undefined)
567
+ void sync('unstreamed');
568
+ else if (socketOpen)
569
+ options.adapter.roomStream.reopen();
570
+ }
571
+ function acquire(next) {
572
+ if (closed)
573
+ return;
574
+ if (narrowing !== null)
575
+ clearTimeout(narrowing);
576
+ narrowing = null;
577
+ const widening = wider(next, demand);
578
+ if (!widening && !narrower(next, demand))
579
+ return;
580
+ if (widening || !roomsListed) {
581
+ applyDemand(next);
582
+ return;
583
+ }
584
+ // A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
585
+ narrowing = setTimeout(() => {
586
+ narrowing = null;
587
+ applyDemand(next);
588
+ }, 5_000);
589
+ }
590
+ function requiredRooms(reach) {
591
+ if (reach === null)
592
+ return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
593
+ const named = [...new Set(reach.rooms)].sort();
594
+ if (roomsListed)
595
+ return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
596
+ return { listed: named.every((room) => replicas.has(room)), rooms: named };
597
+ }
598
+ function roomState(locator, search) {
599
+ if (sourceRefusals.has(locator))
600
+ return 'refused';
601
+ const listedRoom = discovered.get(locator);
602
+ if (roomsListed &&
603
+ (listedRoom === undefined ||
604
+ listedRoom.replica.admission !== 'admitted' ||
605
+ unreadable.has(locator)))
606
+ return 'unadmitted';
607
+ if (!replicas.has(locator))
608
+ return 'acquiring';
609
+ if (!prepared.has(locator))
610
+ return 'preparing';
611
+ if (search && !searchIndex.rooms().has(locator))
612
+ return 'indexing';
613
+ return 'complete';
614
+ }
615
+ function conversationCoverage(reach, search) {
616
+ const declared = reach === null || reach.conversations;
617
+ if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
618
+ return 'undeclared';
619
+ if (!conversations.known() || !conversations.current())
620
+ return 'acquiring';
621
+ if (!search)
622
+ return 'complete';
623
+ const unindexed = conversations
624
+ .searchable()
625
+ .filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
626
+ if (unindexed.length === 0)
627
+ return 'complete';
628
+ index([], unindexed);
629
+ return 'indexing';
630
+ }
631
+ function connectionOf() {
632
+ if (socketOpen)
633
+ return 'connected';
634
+ if (status.providerUnreachable)
635
+ return 'unreachable';
636
+ return options.adapter.roomStream === undefined && status.lastCompleted !== null
637
+ ? 'connected'
638
+ : 'connecting';
639
+ }
640
+ function refusedIn(locators) {
641
+ return locators.flatMap((room) => {
642
+ const held = sourceRefusals.get(room);
643
+ if (held === undefined)
644
+ return [];
645
+ const { file, location } = held.fields;
646
+ return [
647
+ {
648
+ room,
649
+ file: typeof file === 'string' ? file : '',
650
+ rule: held.code,
651
+ location: typeof location === 'string' ? location : null,
652
+ },
653
+ ];
654
+ });
354
655
  }
355
- function rowsQuery(plan) {
356
- materialize(reachedRooms(plan));
357
- return readSide.query(plan);
656
+ function readinessFor(port, reach) {
657
+ const required = requiredRooms(reach);
658
+ const search = port.search === 'required';
659
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
660
+ const building = (state) => coverage
661
+ .filter((room) => room.state === state && !unindexable.has(room.room))
662
+ .map(({ room }) => room);
663
+ const preparing = building('preparing');
664
+ if (preparing.length > 0)
665
+ prepare(preparing);
666
+ const indexing = building('indexing');
667
+ if (indexing.length > 0)
668
+ index(indexing, []);
669
+ return readinessOf({
670
+ code: port.code,
671
+ coverage: {
672
+ listed: required.listed,
673
+ rooms: coverage,
674
+ conversations: conversationCoverage(reach, search),
675
+ search: search ? 'required' : 'unused',
676
+ },
677
+ persistence,
678
+ connection: connectionOf(),
679
+ house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
680
+ capacity: capacity !== null &&
681
+ coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
682
+ ? capacity
683
+ : port.capacity,
684
+ refused: refusedIn(required.rooms),
685
+ lost: storageLoss !== null,
686
+ established: targets[port.target]?.established === true,
687
+ });
688
+ }
689
+ function establish(name) {
690
+ targets = { ...targets, [name]: { established: true } };
691
+ queueReadiness();
692
+ void durably((local) => local.keepTargets(targets)).catch(() => {
693
+ journal.record(`target record failed ${name}`);
694
+ });
695
+ }
696
+ function deliverReadiness(watch) {
697
+ if (!readinessWatches.has(watch))
698
+ return;
699
+ const readiness = readinessFor(watch.port, watch.reach);
700
+ if (readiness.ready && targets[watch.port.target]?.established !== true) {
701
+ establish(watch.port.target);
702
+ }
703
+ const shown = JSON.stringify(readiness);
704
+ if (shown === watch.last)
705
+ return;
706
+ watch.last = shown;
707
+ watch.deliver(structuredClone(readiness));
708
+ }
709
+ function queueReadiness() {
710
+ if (readinessQueued || closed || readinessWatches.size === 0)
711
+ return;
712
+ readinessQueued = true;
713
+ queueMicrotask(() => {
714
+ readinessQueued = false;
715
+ if (closed)
716
+ return;
717
+ for (const watch of [...readinessWatches])
718
+ deliverReadiness(watch);
719
+ });
720
+ }
721
+ function watchReadiness(port, reach, deliver) {
722
+ const watch = { port, reach, deliver, last: null };
723
+ readinessWatches.add(watch);
724
+ void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
725
+ return () => {
726
+ readinessWatches.delete(watch);
727
+ };
728
+ }
729
+ function layerBytes(layer) {
730
+ let bytes = 0;
731
+ for (const [path, text] of Object.entries(layer.documents))
732
+ bytes += (path.length + text.length) * 2;
733
+ return bytes;
734
+ }
735
+ function grownBy(next, previous) {
736
+ if (previous === undefined)
737
+ return layerBytes(next);
738
+ const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
739
+ return layerBytes({ ...next, documents });
740
+ }
741
+ function exceeded(error) {
742
+ return (typeof error === 'object' &&
743
+ error !== null &&
744
+ error.name === 'QuotaExceededError');
745
+ }
746
+ async function estimated() {
747
+ const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
748
+ return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
749
+ }
750
+ function pause(locator, cause) {
751
+ paused.add(locator);
752
+ capacity = cause;
753
+ journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
754
+ queueReadiness();
755
+ if (rechecking !== null)
756
+ return;
757
+ // A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
758
+ rechecking = setInterval(() => {
759
+ void recheck();
760
+ }, 60_000);
761
+ }
762
+ async function recheck() {
763
+ const held = capacity;
764
+ if (held === null || closed)
765
+ return;
766
+ const available = await estimated().catch(() => null);
767
+ if (available === null || available < (held.needed ?? 0))
768
+ return;
769
+ if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
770
+ return;
771
+ resume();
772
+ }
773
+ function resume() {
774
+ if (rechecking !== null)
775
+ clearInterval(rechecking);
776
+ rechecking = null;
777
+ if (paused.size === 0 && capacity === null)
778
+ return;
779
+ paused.clear();
780
+ capacity = null;
781
+ journal.record('capacity resumed');
782
+ queueReadiness();
783
+ if (!closed && authorization !== undefined)
784
+ void sync('unstreamed');
785
+ }
786
+ async function reclaimed(local) {
787
+ const unreferenced = [...transfers.keys()].filter((locator) => {
788
+ const room = discovered.get(locator);
789
+ return room === undefined || !acquires(room);
790
+ });
791
+ for (const locator of unreferenced) {
792
+ await local.abandon(locator);
793
+ transfers.delete(locator);
794
+ }
795
+ if (unreferenced.length > 0)
796
+ journal.record(`capacity reclaimed ${unreferenced.length}`);
797
+ return unreferenced.length > 0;
798
+ }
799
+ async function admitted(local, locator, needed) {
800
+ if (paused.has(locator))
801
+ return false;
802
+ let available = await estimated().catch(() => null);
803
+ if (available === null || available >= needed)
804
+ return true;
805
+ if (await reclaimed(local))
806
+ available = await estimated().catch(() => null);
807
+ if (available === null || available >= needed)
808
+ return true;
809
+ pause(locator, { cause: 'insufficient-space', needed, available });
810
+ return false;
811
+ }
812
+ async function exceededOn(locator, bytes) {
813
+ const available = await estimated().catch(() => null);
814
+ pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
815
+ }
816
+ function seedRoomHeld(room, version) {
817
+ return prepared.has(room) && reachedBy(room, version);
818
+ }
819
+ function seedHeld(at) {
820
+ return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
821
+ }
822
+ function seedApplies(plan) {
823
+ const seed = seeds.get(plan.key);
824
+ if (seed === undefined)
825
+ return null;
826
+ if (!seedHeld(seed.at))
827
+ return seed;
828
+ seeds.delete(plan.key);
829
+ return null;
830
+ }
831
+ function seedBehind(plan, cover) {
832
+ const seed = seedApplies(plan);
833
+ if (seed === null || 'pending' in cover)
834
+ return cover;
835
+ return {
836
+ pending: Object.entries(seed.at)
837
+ .filter(([room, version]) => !seedRoomHeld(room, version))
838
+ .map(([room]) => ({ room, awaits: 'acquisition' })),
839
+ };
840
+ }
841
+ function seed(entry) {
842
+ if (seedHeld(entry.at))
843
+ return;
844
+ for (const { query, rows } of entry.queries) {
845
+ const key = queryKey(query);
846
+ seeds.set(key, { rows, at: entry.at });
847
+ for (const shown of seedWatchers.get(key) ?? [])
848
+ shown();
849
+ }
850
+ }
851
+ async function rowsQuery(plan) {
852
+ const before = covered(plan);
853
+ awaitPreparation(before);
854
+ const seeded = seedApplies(plan);
855
+ if (seeded !== null)
856
+ return seededResult(seeded).result;
857
+ if ('pending' in before)
858
+ return before;
859
+ const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
860
+ awaitPreparation(cover);
861
+ if ('pending' in cover)
862
+ return cover;
863
+ return 'groups' in rows
864
+ ? { groups: rows.groups, coverage: cover.coverage }
865
+ : { documents: rows.documents, coverage: cover.coverage };
866
+ }
867
+ async function indexConversation(conversation) {
868
+ searchIndex.indexMessages(conversation, [], false);
869
+ let after = null;
870
+ for (;;) {
871
+ const batch = await durably((local) => local.messagesAfter(conversation, after));
872
+ if (closed)
873
+ return;
874
+ searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
875
+ if (batch.length === 0)
876
+ return;
877
+ after = batch[batch.length - 1].identity;
878
+ }
879
+ }
880
+ function indexNext() {
881
+ const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
882
+ if (room !== undefined) {
883
+ roomsToIndex.delete(room);
884
+ return () => prepared.has(room)
885
+ ? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
886
+ : undefined;
887
+ }
888
+ const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
889
+ if (conversation === undefined)
890
+ return null;
891
+ conversationsToIndex.delete(conversation);
892
+ return () => indexConversation(conversation);
893
+ }
894
+ function index(rooms, named) {
895
+ for (const room of rooms)
896
+ roomsToIndex.add(room);
897
+ for (const conversation of named)
898
+ conversationsToIndex.add(conversation);
899
+ if (indexing)
900
+ return;
901
+ indexing = true;
902
+ void (async () => {
903
+ try {
904
+ for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
905
+ await yielded();
906
+ await unit();
907
+ queueReadiness();
908
+ }
909
+ }
910
+ finally {
911
+ indexing = false;
912
+ }
913
+ })().catch(() => { });
358
914
  }
359
915
  function publishChangeSet(set) {
360
916
  applyChangeSet(set);
@@ -371,34 +927,7 @@ export function composeRuntime(options) {
371
927
  emitPresentation(app);
372
928
  conversations.changed(set);
373
929
  }
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
930
  async function refuse(reason, provider = null) {
401
- stopTimer();
402
931
  discoveryPaused = true;
403
932
  try {
404
933
  if (reason === 'revoked-authorization') {
@@ -477,14 +1006,27 @@ export function composeRuntime(options) {
477
1006
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
478
1007
  });
479
1008
  }
480
- async function applyDiscovery(listed, local) {
1009
+ async function applyDiscovery(found, started) {
1010
+ for (const room of found) {
1011
+ const fence = fences.get(room.locator);
1012
+ if (fence !== undefined && fence.erased && fence.at < started)
1013
+ fences.delete(room.locator);
1014
+ }
1015
+ await loseRooms(found
1016
+ .filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
1017
+ .map((room) => room.locator));
1018
+ const listed = found.filter((room) => !fences.has(room.locator));
481
1019
  const listedLocators = new Set(listed.map((room) => room.locator));
482
1020
  for (const locator of listedLocators)
483
1021
  scopedRefusals.delete(locator);
484
1022
  discovered.clear();
485
1023
  for (const room of listed)
486
1024
  discovered.set(room.locator, room);
487
- await dropUnlisted(listedLocators, local);
1025
+ const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
1026
+ if (options.adapter.exhaustiveDiscovery === true)
1027
+ await loseRooms(unlisted);
1028
+ else
1029
+ unlistedKept(unlisted);
488
1030
  for (const room of listed) {
489
1031
  const held = replicas.get(room.locator)?.confirmed.record;
490
1032
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -495,6 +1037,13 @@ export function composeRuntime(options) {
495
1037
  }
496
1038
  roomsListed = true;
497
1039
  emitRooms();
1040
+ return listed;
1041
+ }
1042
+ function heldLocally(locator) {
1043
+ return (replicas.has(locator) ||
1044
+ roomDrafts.has(locator) ||
1045
+ transfers.has(locator) ||
1046
+ queuesWithoutReplica.has(locator));
498
1047
  }
499
1048
  function unlistedHeld(listed) {
500
1049
  return [
@@ -506,41 +1055,73 @@ export function composeRuntime(options) {
506
1055
  ]),
507
1056
  ].filter((locator) => !listed.has(locator));
508
1057
  }
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);
1058
+ function unlistedKept(unlisted) {
1059
+ for (const locator of unlisted) {
1060
+ const held = replicas.get(locator)?.confirmed.record;
1061
+ if (held !== undefined)
1062
+ list(held, false);
1063
+ else
525
1064
  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}`);
1065
+ }
1066
+ }
1067
+ function fence(locators) {
1068
+ const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1069
+ const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
1070
+ room: true,
1071
+ queue: true,
1072
+ drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
1073
+ }));
1074
+ for (const locator of fresh) {
1075
+ fences.set(locator, { at: discoveries, erased: false });
1076
+ lastFenced.set(locator, ++fencings);
1077
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1078
+ pendings.delete(batch.id);
1079
+ replicas.delete(locator);
1080
+ queuesWithoutReplica.delete(locator);
1081
+ rooms.delete(locator);
1082
+ roomDrafts.delete(locator);
1083
+ transfers.delete(locator);
1084
+ catching.delete(locator);
1085
+ discovered.delete(locator);
1086
+ announced.delete(locator);
1087
+ unreadable.delete(locator);
1088
+ stale.delete(locator);
1089
+ repairable.delete(locator);
1090
+ unindexable.delete(locator);
1091
+ paused.delete(locator);
1092
+ scopedRefusals.delete(locator);
1093
+ sourceRefusals.delete(locator);
1094
+ declarationRefusals.delete(locator);
1095
+ for (const attached of ['collection', 'typed-selection']) {
1096
+ const key = modelKey(locator, attached);
1097
+ models.delete(key);
1098
+ liveModels.delete(key);
1099
+ invalidModels.delete(key);
540
1100
  }
541
- publishChangeSet(mergeChangeSets(removed));
542
- moveTo({ ...status, ...attention() });
543
- });
1101
+ journal.record(`room lost ${locator}`);
1102
+ }
1103
+ if (fresh.length === 0)
1104
+ return fresh;
1105
+ publishChangeSet(mergeChangeSets(removed));
1106
+ for (const locator of fresh)
1107
+ prepared.delete(locator);
1108
+ searchIndex.forget(fresh);
1109
+ readSide.recover();
1110
+ moveTo({ ...status, ...attention() });
1111
+ return fresh;
1112
+ }
1113
+ async function erased(local, locators) {
1114
+ await local.erase(locators);
1115
+ for (const locator of locators) {
1116
+ const held = fences.get(locator);
1117
+ if (held !== undefined)
1118
+ held.erased = true;
1119
+ }
1120
+ }
1121
+ async function loseRooms(locators) {
1122
+ const fresh = fence(locators);
1123
+ if (fresh.length > 0)
1124
+ await durably((local) => erased(local, fresh));
544
1125
  }
545
1126
  function restored(key, persisted) {
546
1127
  const prior = models.get(key);
@@ -561,7 +1142,7 @@ export function composeRuntime(options) {
561
1142
  return model;
562
1143
  }
563
1144
  async function hydrate(opened) {
564
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
1145
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
565
1146
  opened.all(),
566
1147
  opened.queues(),
567
1148
  opened.roomDrafts(),
@@ -573,7 +1154,9 @@ export function composeRuntime(options) {
573
1154
  opened.capabilities(),
574
1155
  opened.member(),
575
1156
  opened.transfers(),
1157
+ opened.targets(),
576
1158
  ]);
1159
+ targets = heldTargets;
577
1160
  transfers.clear();
578
1161
  for (const record of staged)
579
1162
  transfers.set(record.locator, record);
@@ -635,6 +1218,7 @@ export function composeRuntime(options) {
635
1218
  async function readState(opened) {
636
1219
  const { lastCompleted, locators, read } = await hydrate(opened);
637
1220
  applyChangeSet(read);
1221
+ prepare();
638
1222
  if (replicas.size > 0)
639
1223
  emitRooms();
640
1224
  if (member !== null)
@@ -645,8 +1229,15 @@ export function composeRuntime(options) {
645
1229
  return lastCompleted;
646
1230
  }
647
1231
  async function open() {
648
- const opened = await openRuntimeStore();
1232
+ if (closed)
1233
+ throw new Error('runtime closed');
1234
+ const opened = await openRuntimeStore(isFenced);
649
1235
  store = opened;
1236
+ const interrupted = await opened.fences();
1237
+ for (const locator of interrupted) {
1238
+ fences.set(locator, { at: discoveries, erased: false });
1239
+ lastFenced.set(locator, ++fencings);
1240
+ }
650
1241
  const kept = options.shell.generation;
651
1242
  const storedGeneration = await opened.generation();
652
1243
  const loss = detectStorageLoss(storedGeneration, kept);
@@ -660,6 +1251,8 @@ export function composeRuntime(options) {
660
1251
  emitStorageLoss();
661
1252
  }
662
1253
  const lastCompleted = await readState(opened);
1254
+ if (interrupted.length > 0)
1255
+ await erased(opened, interrupted);
663
1256
  moveTo({ ...status, lastCompleted, ...attention() });
664
1257
  return opened;
665
1258
  }
@@ -741,17 +1334,19 @@ export function composeRuntime(options) {
741
1334
  moveTo({ ...status, ...attention() });
742
1335
  throw error;
743
1336
  }
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
- });
1337
+ return () => {
1338
+ queuesWithoutReplica.delete(locator);
1339
+ for (const { batch, returned } of unqueued) {
1340
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
1341
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1342
+ }
1343
+ replicas.set(locator, replicaOf(confirmed, kept, before));
1344
+ return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
1345
+ room: settling.room,
1346
+ queue: kept.length !== previousQueue.length,
1347
+ drafts: [...written.keys()],
1348
+ });
1349
+ };
755
1350
  }
756
1351
  function adoptModels(locator, record) {
757
1352
  for (const [attached, model] of [
@@ -784,11 +1379,16 @@ export function composeRuntime(options) {
784
1379
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
785
1380
  };
786
1381
  }
787
- function publish(local, room, confirmed, landing = {}) {
1382
+ function publish(local, room, confirmed, landing, live) {
788
1383
  const { base, stored } = landing;
789
1384
  return exclusive(async () => {
790
- if (!continuing())
1385
+ if (stored !== undefined) {
1386
+ await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
1387
+ }
1388
+ if (!live()) {
1389
+ caughtUpTo(room.locator);
791
1390
  return true;
1391
+ }
792
1392
  const prior = replicas.get(room.locator);
793
1393
  if (base !== undefined &&
794
1394
  (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
@@ -802,24 +1402,42 @@ export function composeRuntime(options) {
802
1402
  const accepted = pendings.get(batch.id)?.accepted;
803
1403
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
804
1404
  });
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)
1405
+ let settled;
1406
+ try {
1407
+ settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
1408
+ }
1409
+ catch (error) {
1410
+ if (!exceeded(error))
1411
+ throw error;
1412
+ await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
1413
+ caughtUpTo(room.locator);
1414
+ return true;
1415
+ }
1416
+ const set = settled();
1417
+ if (stored !== undefined) {
807
1418
  transfers.delete(room.locator);
1419
+ catching.delete(room.locator);
1420
+ }
808
1421
  for (const batch of accepted)
809
1422
  pendings.delete(batch.id);
810
1423
  stale.delete(room.locator);
1424
+ unindexable.delete(room.locator);
811
1425
  if (unreadable.delete(room.locator))
812
1426
  scopedRefusals.delete(room.locator);
813
1427
  sourceRefusals.delete(room.locator);
814
1428
  declarationRefusals.delete(room.locator);
815
1429
  list(room, true);
1430
+ if (prior === undefined)
1431
+ prepared.add(room.locator);
816
1432
  publishChangeSet(set);
817
1433
  moveTo({ ...status, ...attention() });
1434
+ options.adapter.roomStream?.published(room.locator, roomVersion);
818
1435
  return true;
819
1436
  });
820
1437
  }
821
1438
  async function saveRoomDrafts(locator, named, writtenBy) {
822
1439
  const local = await openLocal();
1440
+ await referenced(locator);
823
1441
  const replica = replicas.get(locator);
824
1442
  const room = rooms.get(locator);
825
1443
  if (replica === undefined || room === undefined) {
@@ -867,6 +1485,11 @@ export function composeRuntime(options) {
867
1485
  changes,
868
1486
  bases: parts.flatMap(({ bases }) => bases),
869
1487
  writtenBy,
1488
+ clean: {
1489
+ text: replica.confirmed.record.documents,
1490
+ binaries: replica.confirmed.record.binaries,
1491
+ composition: compositionOf(replica.confirmed.record),
1492
+ },
870
1493
  };
871
1494
  const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
872
1495
  if (checked?.kind === 'refused')
@@ -937,6 +1560,8 @@ export function composeRuntime(options) {
937
1560
  catch {
938
1561
  return { outcome: 'refused', code: 'durable-write', retryable: true };
939
1562
  }
1563
+ if (fences.has(locator))
1564
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
940
1565
  for (const origin of withdrawn.keys())
941
1566
  pendings.delete(origin);
942
1567
  for (const [id, drafts] of drafted) {
@@ -966,6 +1591,8 @@ export function composeRuntime(options) {
966
1591
  return;
967
1592
  }
968
1593
  await local.submit(locator, identifier, submission, lost);
1594
+ if (fences.has(locator))
1595
+ return;
969
1596
  pendings.set(identifier, { ...state, submission, lost });
970
1597
  const replica = replicas.get(locator);
971
1598
  if (replica !== undefined)
@@ -976,6 +1603,8 @@ export function composeRuntime(options) {
976
1603
  if (!continuing())
977
1604
  throw new Error('connection refused');
978
1605
  await local.keepUpload(locator, identifier, upload);
1606
+ if (fences.has(locator))
1607
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
979
1608
  pendings.set(identifier, { ...pendings.get(identifier), upload });
980
1609
  if (!continuing())
981
1610
  throw new Error('connection refused');
@@ -991,6 +1620,7 @@ export function composeRuntime(options) {
991
1620
  }
992
1621
  if (closed || authorization === undefined || refusal !== null)
993
1622
  return UNCERTAIN;
1623
+ const since = fencings;
994
1624
  const transfer = ['transfer', 'history', locator, path];
995
1625
  if (query.revision !== undefined)
996
1626
  transfer.push(query.revision);
@@ -1005,6 +1635,8 @@ export function composeRuntime(options) {
1005
1635
  ...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
1006
1636
  });
1007
1637
  journal.record(`outcome history ${result.kind}`);
1638
+ if (fencedSince(locator, since))
1639
+ return failure({ kind: 'room-not-readable', locator });
1008
1640
  if (result.kind === 'scoped-refusal') {
1009
1641
  const room = replicas.get(result.locator)?.confirmed.record;
1010
1642
  if (room !== undefined)
@@ -1027,6 +1659,12 @@ export function composeRuntime(options) {
1027
1659
  }
1028
1660
  return null;
1029
1661
  }
1662
+ function binaryRoom(request) {
1663
+ if (request.kind === 'path')
1664
+ return request.locator;
1665
+ const scope = options.adapter.referrerScope(request.referrer);
1666
+ return scope?.kind === 'room' ? scope.locator : null;
1667
+ }
1030
1668
  async function readBinary(request) {
1031
1669
  if (closed || refusal !== null)
1032
1670
  return UNCERTAIN;
@@ -1038,12 +1676,16 @@ export function composeRuntime(options) {
1038
1676
  }
1039
1677
  if (closed || authorization === undefined || refusal !== null)
1040
1678
  return UNCERTAIN;
1679
+ const since = fencings;
1041
1680
  journal.record(request.kind === 'path'
1042
1681
  ? `transfer binary ${request.locator} ${request.path}`
1043
1682
  : `transfer binary ${request.fileRef}`);
1044
1683
  try {
1045
1684
  const result = await options.adapter.binary(authorization, request);
1046
1685
  journal.record(`outcome binary ${result.kind}`);
1686
+ const locator = binaryRoom(request);
1687
+ if (locator !== null && fencedSince(locator, since))
1688
+ return failure({ kind: 'room-not-readable', locator });
1047
1689
  if (result.kind === 'scoped-refusal') {
1048
1690
  const room = replicas.get(result.locator)?.confirmed.record;
1049
1691
  if (room !== undefined)
@@ -1167,11 +1809,11 @@ export function composeRuntime(options) {
1167
1809
  options.adapter.reached(locator, written, roomVersion) &&
1168
1810
  !options.adapter.reached(locator, roomVersion, written));
1169
1811
  }
1170
- async function pullRefresh(authorized, confirmed) {
1171
- if (!continuing())
1812
+ async function pullRefresh(authorized, confirmed, live) {
1813
+ if (!live())
1172
1814
  return unconfirmed;
1173
1815
  try {
1174
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
1816
+ const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1175
1817
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
1176
1818
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
1177
1819
  return result;
@@ -1181,47 +1823,80 @@ export function composeRuntime(options) {
1181
1823
  throw error;
1182
1824
  }
1183
1825
  }
1184
- function staging(local) {
1826
+ function staging(local, live) {
1185
1827
  return {
1186
1828
  held: (locator) => transfers.get(locator) ?? null,
1187
1829
  staged: (record) => local.staged(record),
1188
1830
  stage: (record, page) => exclusive(async () => {
1189
- if (!continuing())
1831
+ if (!live())
1832
+ return false;
1833
+ const bytes = layerBytes(page);
1834
+ if (!(await admitted(local, record.locator, bytes)))
1190
1835
  return false;
1191
- await local.stage(record, page);
1836
+ try {
1837
+ await local.stage(record, page);
1838
+ }
1839
+ catch (error) {
1840
+ if (!exceeded(error))
1841
+ throw error;
1842
+ await exceededOn(record.locator, bytes);
1843
+ return false;
1844
+ }
1192
1845
  transfers.set(record.locator, record);
1846
+ readSide.recover();
1193
1847
  return true;
1194
1848
  }),
1195
1849
  abandon: (locator) => exclusive(async () => {
1196
- if (!continuing())
1850
+ if (!live())
1197
1851
  return;
1198
1852
  await local.abandon(locator);
1199
1853
  transfers.delete(locator);
1854
+ readSide.recover();
1200
1855
  }),
1201
1856
  };
1202
1857
  }
1203
- async function pullBootstrap(authorized, room, local) {
1204
- if (!continuing())
1858
+ function reusable(room) {
1859
+ const record = replicas.get(room.locator)?.confirmed.record;
1860
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1861
+ return null;
1862
+ return { documents: record.documents, revisions: record.revisions };
1863
+ }
1864
+ async function pullBootstrap(authorized, room, local, live) {
1865
+ if (!live())
1205
1866
  return unconfirmed;
1867
+ if (replicas.has(room.locator)) {
1868
+ catching.add(room.locator);
1869
+ readSide.recover();
1870
+ }
1206
1871
  try {
1207
1872
  const result = await bootstrapRoom(options.adapter, authorized, room, {
1208
1873
  collection: modelOf(room.locator),
1209
1874
  selection: modelOf(room.locator, 'typed-selection'),
1210
- }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
1875
+ }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
1211
1876
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
1212
1877
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1878
+ if (result.kind !== 'staged')
1879
+ caughtUpTo(room.locator);
1213
1880
  return result;
1214
1881
  }
1215
1882
  catch (error) {
1216
1883
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
1884
+ caughtUpTo(room.locator);
1217
1885
  throw error;
1218
1886
  }
1219
1887
  }
1220
- async function advance(room, local, authorized) {
1888
+ function caughtUpTo(locator) {
1889
+ if (catching.delete(locator))
1890
+ readSide.recover();
1891
+ }
1892
+ async function advance(room, local, authorized, live) {
1893
+ await referenced(room.locator);
1894
+ if (!live() || paused.has(room.locator))
1895
+ return unconfirmed;
1221
1896
  const held = replicas.get(room.locator);
1222
1897
  if (held !== undefined) {
1223
- const refreshed = await pullRefresh(authorized, held.confirmed);
1224
- if (!continuing())
1898
+ const refreshed = await pullRefresh(authorized, held.confirmed, live);
1899
+ if (!live())
1225
1900
  return unconfirmed;
1226
1901
  if (refreshed.kind === 'still-current') {
1227
1902
  stale.delete(room.locator);
@@ -1235,31 +1910,45 @@ export function composeRuntime(options) {
1235
1910
  return null;
1236
1911
  }
1237
1912
  if (refreshed.kind === 'advanced') {
1238
- if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
1913
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
1239
1914
  const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
1240
1915
  const reached = now !== undefined &&
1241
1916
  options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
1242
- return reached ? null : advance(room, local, authorized);
1917
+ return reached ? null : advance(room, local, authorized, live);
1243
1918
  }
1244
1919
  await retainModel(local, room, 'collection', modelOf(room.locator));
1245
1920
  return null;
1246
1921
  }
1247
1922
  if (refreshed.kind !== 'beyond-horizon')
1248
1923
  return refreshed;
1924
+ if (opened !== null) {
1925
+ owed.add(room.locator);
1926
+ readSide.recover();
1927
+ return unconfirmed;
1928
+ }
1249
1929
  }
1250
- const bootstrapped = await pullBootstrap(authorized, room, local);
1251
- if (!continuing())
1930
+ const bootstrapped = await pullBootstrap(authorized, room, local, live);
1931
+ if (!live())
1252
1932
  return unconfirmed;
1253
1933
  if (bootstrapped.kind !== 'staged')
1254
1934
  return bootstrapped;
1255
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1935
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
1256
1936
  return null;
1257
1937
  }
1258
1938
  async function withhold(withheld, room, local, progress) {
1939
+ if (withheld.kind === 'unconfirmed')
1940
+ return;
1259
1941
  if (withheld.kind === 'refused')
1260
1942
  stale.delete(room.locator);
1261
1943
  else
1262
1944
  stale.add(room.locator);
1945
+ if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
1946
+ const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
1947
+ repairable.set(room.locator, reset);
1948
+ repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
1949
+ }
1950
+ else
1951
+ repairable.delete(room.locator);
1263
1952
  if (withheld.kind === 'scoped-refusal') {
1264
1953
  await scoped(withheld, room, local);
1265
1954
  }
@@ -1295,6 +1984,7 @@ export function composeRuntime(options) {
1295
1984
  unreadable.add(room.locator);
1296
1985
  list(room, false);
1297
1986
  emitRooms();
1987
+ moveTo({ ...status, ...attention() });
1298
1988
  journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
1299
1989
  }
1300
1990
  function leaving(batch, result) {
@@ -1316,6 +2006,7 @@ export function composeRuntime(options) {
1316
2006
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1317
2007
  }
1318
2008
  async function adoptAccepted(room, local, submitted, accepted) {
2009
+ await referenced(room.locator);
1319
2010
  if (!continuing())
1320
2011
  return unconfirmed;
1321
2012
  const current = replicas.get(room.locator);
@@ -1332,16 +2023,17 @@ export function composeRuntime(options) {
1332
2023
  if (applied.kind !== 'staged') {
1333
2024
  const state = pendings.get(submitted.id);
1334
2025
  await local.accept(room.locator, submitted.id, accepted.roomVersion);
1335
- if (state !== undefined)
2026
+ if (state !== undefined && !fences.has(room.locator))
1336
2027
  pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1337
2028
  emitQueue(room.locator);
1338
2029
  return applied;
1339
2030
  }
1340
- const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
2031
+ const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1341
2032
  pendings.delete(submitted.id);
1342
2033
  unreadable.delete(room.locator);
1343
2034
  sourceRefusals.delete(room.locator);
1344
2035
  declarationRefusals.delete(room.locator);
2036
+ const set = settled();
1345
2037
  list(room, true);
1346
2038
  publishChangeSet(set);
1347
2039
  moveTo({ ...status, ...attention() });
@@ -1355,14 +2047,14 @@ export function composeRuntime(options) {
1355
2047
  refusedSource(room, failed);
1356
2048
  return false;
1357
2049
  }
1358
- async function submitRoom(room, local, authorized, progress) {
1359
- while (continuing() && authorization !== undefined) {
2050
+ async function submitRoom(room, local, authorized, progress, signal) {
2051
+ while (continuing() && authorization !== undefined && !signal.aborted) {
1360
2052
  const replica = replicas.get(room.locator);
1361
2053
  const head = replica?.queue[0];
1362
2054
  if (replica === undefined || head === undefined)
1363
- return;
2055
+ return 'done';
1364
2056
  if (pendings.get(head.id)?.accepted !== undefined)
1365
- return;
2057
+ return 'done';
1366
2058
  const state = pendings.get(head.id);
1367
2059
  const { submission, lost: earlier } = state ?? {
1368
2060
  submission: { state: 'waiting' },
@@ -1370,24 +2062,30 @@ export function composeRuntime(options) {
1370
2062
  };
1371
2063
  if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
1372
2064
  progress.complete = false;
1373
- return;
2065
+ return { at: Date.parse(submission.reset) };
1374
2066
  }
1375
2067
  if (writesHeld()) {
1376
2068
  progress.complete = false;
1377
2069
  journal.record(`submission ${room.locator} ${head.id} held`);
1378
- return;
2070
+ return 'done';
1379
2071
  }
1380
2072
  const lost = earlier || submission.state === 'submitted';
2073
+ if (lost && pastHorizon(head.id)) {
2074
+ await discardPending(room, local, signal);
2075
+ return 'done';
2076
+ }
1381
2077
  let result;
1382
2078
  journal.record(`submission ${room.locator} ${head.id}`);
1383
2079
  await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1384
- if (discoveryPaused)
1385
- return;
1386
- if (writesHeld()) {
2080
+ if (discoveryPaused || fences.has(room.locator))
2081
+ return 'done';
2082
+ if (signal.aborted || writesHeld()) {
1387
2083
  await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
2084
+ if (signal.aborted)
2085
+ return 'done';
1388
2086
  progress.complete = false;
1389
2087
  journal.record(`submission ${room.locator} ${head.id} held`);
1390
- return;
2088
+ return 'done';
1391
2089
  }
1392
2090
  const opensBefore = openSignals;
1393
2091
  const answered = awaitingAnswer(room.locator);
@@ -1403,32 +2101,36 @@ export function composeRuntime(options) {
1403
2101
  progress.complete = false;
1404
2102
  if (continuing())
1405
2103
  progress.reachable = false;
1406
- return;
2104
+ return 'again';
2105
+ }
2106
+ if (signal.aborted) {
2107
+ answered();
2108
+ return 'done';
1407
2109
  }
1408
2110
  if (result.kind !== 'accepted' || !continuing())
1409
2111
  answered();
1410
2112
  if (!continuing())
1411
- return;
2113
+ return 'done';
1412
2114
  if (result.kind === 'transport-uncertainty') {
1413
2115
  progress.complete = false;
1414
2116
  progress.reachable = false;
1415
- return;
2117
+ return 'again';
1416
2118
  }
1417
2119
  if (result.kind === 'room-not-readable') {
1418
2120
  progress.complete = false;
1419
2121
  unreadable.add(room.locator);
1420
2122
  list(room, false);
1421
2123
  emitRooms();
1422
- return;
2124
+ return 'done';
1423
2125
  }
1424
2126
  if (result.kind === 'revoked-authorization') {
1425
2127
  await refuse('revoked-authorization');
1426
- return;
2128
+ return 'done';
1427
2129
  }
1428
2130
  if (result.kind === 'scoped-refusal') {
1429
2131
  await scoped(result, room, local);
1430
2132
  progress.complete = false;
1431
- return;
2133
+ return 'done';
1432
2134
  }
1433
2135
  if (result.kind === 'write-halted') {
1434
2136
  journal.record(`outcome ${room.locator} ${head.id} write-halted`);
@@ -1437,16 +2139,24 @@ export function composeRuntime(options) {
1437
2139
  continue;
1438
2140
  haltedRefusal = true;
1439
2141
  progress.complete = false;
1440
- return;
2142
+ return 'done';
1441
2143
  }
1442
2144
  if (result.kind === 'conflicted' ||
1443
2145
  result.kind === 'rejected' ||
1444
2146
  result.kind === 'expired' ||
1445
2147
  result.kind === 'retryable-refusal') {
2148
+ if (result.kind === 'expired' && lost && result.restored !== true) {
2149
+ journal.record(`outcome ${room.locator} ${head.id} expired`);
2150
+ await discardPending(room, local, signal);
2151
+ return 'done';
2152
+ }
1446
2153
  if (result.kind === 'expired') {
1447
- const withheld = await advance(room, local, authorized);
1448
- if (withheld !== null)
1449
- return withhold(withheld, room, local, progress);
2154
+ await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
2155
+ const withheld = await advance(room, local, authorized, continuing);
2156
+ if (withheld !== null) {
2157
+ await withhold(withheld, room, local, progress);
2158
+ return 'done';
2159
+ }
1450
2160
  }
1451
2161
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
1452
2162
  (result.kind === 'conflicted' && result.roomVersion !== undefined
@@ -1459,8 +2169,7 @@ export function composeRuntime(options) {
1459
2169
  const reset = result.reset;
1460
2170
  progress.complete = false;
1461
2171
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1462
- armTimer();
1463
- return;
2172
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
1464
2173
  }
1465
2174
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
1466
2175
  const left = leaving(head, result);
@@ -1471,37 +2180,82 @@ export function composeRuntime(options) {
1471
2180
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
1472
2181
  return;
1473
2182
  }
1474
- const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
2183
+ const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1475
2184
  leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1476
2185
  (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1477
2186
  ? { ...left, returned: null }
1478
2187
  : left,
1479
2188
  });
1480
- publishChangeSet(set);
2189
+ publishChangeSet(settled());
1481
2190
  moveTo({ ...status, ...attention() });
1482
2191
  });
1483
2192
  if (result.kind === 'expired')
1484
2193
  continue;
1485
- const withheld = await advance(room, local, authorized);
1486
- if (withheld !== null)
1487
- return withhold(withheld, room, local, progress);
2194
+ const withheld = await advance(room, local, authorized, continuing);
2195
+ if (withheld !== null) {
2196
+ await withhold(withheld, room, local, progress);
2197
+ return 'done';
2198
+ }
1488
2199
  continue;
1489
2200
  }
1490
2201
  const kept = await adopted(room, local, head, result).finally(answered);
1491
2202
  if (!kept) {
1492
2203
  progress.complete = false;
1493
- return;
2204
+ return 'done';
1494
2205
  }
1495
2206
  }
2207
+ return 'done';
2208
+ }
2209
+ function pastHorizon(identity) {
2210
+ return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
2211
+ }
2212
+ async function discardPending(room, local, signal) {
2213
+ const { locator } = room;
2214
+ const discarded = await exclusive(async () => {
2215
+ if (fences.has(locator))
2216
+ return false;
2217
+ const before = replicas.get(locator);
2218
+ const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
2219
+ fences.set(locator, { at: discoveries, erased: false });
2220
+ lastFenced.set(locator, ++fencings);
2221
+ try {
2222
+ await local.discard(locator);
2223
+ }
2224
+ finally {
2225
+ fences.delete(locator);
2226
+ }
2227
+ for (const batch of queue)
2228
+ pendings.delete(batch.id);
2229
+ queuesWithoutReplica.delete(locator);
2230
+ const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
2231
+ if (after !== null)
2232
+ replicas.set(locator, after);
2233
+ journal.record(`room discarded ${locator}`);
2234
+ publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
2235
+ moveTo({ ...status, ...attention() });
2236
+ return true;
2237
+ });
2238
+ if (!discarded)
2239
+ return;
2240
+ const progress = { complete: true, reachable: true };
2241
+ await roomTask(locator, async (live) => {
2242
+ await Promise.race([
2243
+ reload(room, local, progress, () => live() && !signal.aborted),
2244
+ abandonedOn(signal),
2245
+ ]);
2246
+ });
2247
+ if (signal.aborted)
2248
+ return;
2249
+ await rest(progress);
1496
2250
  }
1497
2251
  function awaitingAnswer(locator) {
1498
2252
  let release = () => { };
1499
2253
  const awaited = new Promise((resolve) => {
1500
2254
  release = resolve;
1501
2255
  });
1502
- answering.set(locator, awaited);
2256
+ answering.set(locator, { generation, answered: awaited });
1503
2257
  return () => {
1504
- if (answering.get(locator) === awaited)
2258
+ if (answering.get(locator)?.answered === awaited)
1505
2259
  answering.delete(locator);
1506
2260
  release();
1507
2261
  };
@@ -1572,6 +2326,11 @@ export function composeRuntime(options) {
1572
2326
  });
1573
2327
  }
1574
2328
  async function scoped(result, room, local) {
2329
+ if ((result.scope === 'content' || result.scope === 'sync') && !result.retryable) {
2330
+ journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
2331
+ await loseRooms([room.locator]);
2332
+ return;
2333
+ }
1575
2334
  scopedRefusals.set(room.locator, {
1576
2335
  code: result.code,
1577
2336
  fields: result.fields,
@@ -1617,19 +2376,23 @@ export function composeRuntime(options) {
1617
2376
  ];
1618
2377
  });
1619
2378
  }
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) => {
2379
+ async function synchronize(listed, local, progress, attempt, live) {
2380
+ const acquired = listed.filter(acquires);
2381
+ const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
2382
+ if (!live())
2383
+ return;
2384
+ const ready = acquired.filter((_, index) => {
1623
2385
  const outcome = prepared[index];
1624
2386
  return outcome?.status === 'fulfilled' && outcome.value;
1625
2387
  });
1626
2388
  if (attempt === 'opened') {
1627
- options.adapter.roomStream?.hold(heldRooms(ready));
2389
+ options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
1628
2390
  handed = true;
1629
2391
  }
2392
+ discovering?.release();
1630
2393
  const loaded = await Promise.allSettled(ready
1631
2394
  .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1632
- .map((room) => loadRoom(room, local, progress)));
2395
+ .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
1633
2396
  const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1634
2397
  if (failed !== undefined)
1635
2398
  throw failed.reason;
@@ -1651,21 +2414,16 @@ export function composeRuntime(options) {
1651
2414
  await retainModel(local, room, 'collection', modelOf(room.locator));
1652
2415
  if (discoveryPaused || authorization === undefined)
1653
2416
  return false;
1654
- if (room.replica.admission !== 'admitted') {
1655
- if (replicas.has(room.locator))
1656
- progress.complete = false;
1657
- return false;
1658
- }
1659
- return true;
2417
+ return room.replica.admission === 'admitted';
1660
2418
  }
1661
- async function caughtUp(room, local, progress) {
2419
+ async function caughtUp(room, local, progress, live) {
1662
2420
  const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1663
- if (held === undefined || authorization === undefined)
2421
+ if (held === undefined || authorization === undefined || !live())
1664
2422
  return;
1665
2423
  const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1666
2424
  if (!behind)
1667
2425
  return;
1668
- const withheld = await advance(room, local, authorization);
2426
+ const withheld = await advance(room, local, authorization, live);
1669
2427
  if (withheld !== null)
1670
2428
  await withhold(withheld, room, local, progress);
1671
2429
  }
@@ -1675,39 +2433,46 @@ export function composeRuntime(options) {
1675
2433
  return model.status === 'unavailable' && model.reason === 'revoked';
1676
2434
  });
1677
2435
  }
1678
- async function loadRoom(room, local, progress) {
1679
- if (discoveryPaused || authorization === undefined)
2436
+ async function loadRoom(room, local, progress, live) {
2437
+ if (!live() || authorization === undefined)
1680
2438
  return;
1681
2439
  if (replicas.has(room.locator) && revoked(room.locator)) {
1682
- await reload(room, local, progress);
2440
+ await reload(room, local, progress, live);
1683
2441
  return;
1684
2442
  }
1685
- const withheld = await advance(room, local, authorization);
2443
+ const withheld = await advance(room, local, authorization, live);
1686
2444
  if (withheld !== null) {
1687
2445
  await withhold(withheld, room, local, progress);
1688
2446
  return;
1689
2447
  }
1690
- await caughtUp(room, local, progress);
2448
+ await caughtUp(room, local, progress, live);
1691
2449
  }
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;
2450
+ async function reload(room, local, progress, live) {
2451
+ try {
2452
+ if (!live() || authorization === undefined)
2453
+ return;
2454
+ const bootstrapped = await pullBootstrap(authorization, room, local, live);
2455
+ if (!live())
2456
+ return;
2457
+ if (bootstrapped.kind !== 'staged') {
2458
+ await withhold(bootstrapped, room, local, progress);
2459
+ return;
2460
+ }
2461
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
2462
+ await caughtUp(room, local, progress, live);
2463
+ }
2464
+ finally {
2465
+ caughtUpTo(room.locator);
1701
2466
  }
1702
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1703
- await caughtUp(room, local, progress);
1704
2467
  }
1705
- async function discoverRooms(local, progress, attempt) {
2468
+ async function discoverRooms(local, progress, attempt, live) {
1706
2469
  if (closed || authorization === undefined) {
1707
2470
  notifyReady();
1708
2471
  return;
1709
2472
  }
1710
2473
  moveTo(transferring(status));
2474
+ discoveries += 1;
2475
+ const started = discoveries;
1711
2476
  let result;
1712
2477
  try {
1713
2478
  result = await options.adapter.discover(authorization);
@@ -1715,7 +2480,8 @@ export function composeRuntime(options) {
1715
2480
  catch {
1716
2481
  result = { kind: 'transport-uncertainty' };
1717
2482
  }
1718
- if (closed) {
2483
+ if (!live()) {
2484
+ progress.complete = false;
1719
2485
  notifyReady();
1720
2486
  return;
1721
2487
  }
@@ -1773,6 +2539,7 @@ export function composeRuntime(options) {
1773
2539
  }
1774
2540
  const previousCapabilities = capabilities;
1775
2541
  capabilities = result.capabilities;
2542
+ outcomeRetention = result.retention.outcome;
1776
2543
  notifyReady();
1777
2544
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1778
2545
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
@@ -1785,8 +2552,7 @@ export function composeRuntime(options) {
1785
2552
  member = signedIn;
1786
2553
  emitMember();
1787
2554
  }
1788
- await applyDiscovery(result.rooms, local);
1789
- await synchronize(result.rooms, local, progress, attempt);
2555
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
1790
2556
  }
1791
2557
  async function ensureAuthorization() {
1792
2558
  if (authorization !== undefined)
@@ -1800,20 +2566,50 @@ export function composeRuntime(options) {
1800
2566
  throw error;
1801
2567
  }
1802
2568
  }
1803
- function task(work) {
2569
+ async function admittedRead(work) {
2570
+ if (roomReads.generation !== generation) {
2571
+ roomReads = { generation, free: ROOM_READS, waiting: [] };
2572
+ }
2573
+ const reads = roomReads;
2574
+ while (reads.free === 0)
2575
+ await new Promise((admit) => reads.waiting.push(admit));
2576
+ reads.free -= 1;
2577
+ try {
2578
+ await work();
2579
+ }
2580
+ finally {
2581
+ reads.free += 1;
2582
+ reads.waiting.shift()?.();
2583
+ }
2584
+ }
2585
+ function roomTask(locator, work) {
2586
+ return task(roomLane(locator), (live) => admittedRead(() => work(live)));
2587
+ }
2588
+ function task(lane, work) {
1804
2589
  if (stopping)
1805
- return chain;
1806
- chain = chain.then(work, work).catch(() => undefined);
1807
- return chain;
2590
+ return Promise.resolve();
2591
+ const held = lanes.get(lane);
2592
+ const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
2593
+ const turn = current.generation;
2594
+ const live = () => turn === generation && continuing();
2595
+ const run = current.chain.then(() => work(live)).catch(() => undefined);
2596
+ current.chain = run;
2597
+ lanes.set(lane, current);
2598
+ working.add(run);
2599
+ void run.finally(() => working.delete(run));
2600
+ return run;
1808
2601
  }
1809
2602
  function sync(attempt) {
1810
- return task(async () => {
1811
- if (closed || stopping || discoveryPaused) {
2603
+ return task(OPENING_LANE, async (live) => {
2604
+ if (!live()) {
1812
2605
  notifyReady();
1813
2606
  return;
1814
2607
  }
1815
2608
  const progress = { complete: true, reachable: true };
1816
- underway = attempt === 'opened' ? progress : null;
2609
+ const gate = discover();
2610
+ if (attempt === 'opened')
2611
+ opened = generation;
2612
+ underway = attempt === 'opened' ? { generation, progress } : null;
1817
2613
  if (attempt === 'opened')
1818
2614
  streamed = progress;
1819
2615
  complete = false;
@@ -1822,12 +2618,12 @@ export function composeRuntime(options) {
1822
2618
  handed = false;
1823
2619
  try {
1824
2620
  const local = await openLocal();
1825
- if (closed)
2621
+ if (!live())
1826
2622
  return;
1827
2623
  await ensureAuthorization();
1828
- if (closed)
2624
+ if (!live())
1829
2625
  return;
1830
- await discoverRooms(local, progress, attempt);
2626
+ await discoverRooms(local, progress, attempt, live);
1831
2627
  }
1832
2628
  catch {
1833
2629
  progress.complete = false;
@@ -1835,17 +2631,21 @@ export function composeRuntime(options) {
1835
2631
  progress.reachable = false;
1836
2632
  }
1837
2633
  finally {
2634
+ gate.release();
1838
2635
  notifyReady();
1839
2636
  }
2637
+ if (!live())
2638
+ return;
1840
2639
  if (attempt !== 'opened') {
1841
- await finishOpening(progress);
2640
+ await finishOpening(progress, live);
1842
2641
  return;
1843
2642
  }
1844
2643
  if (handed || closed || progress.reachable)
1845
2644
  return;
1846
2645
  underway = null;
2646
+ opened = null;
1847
2647
  journal.record('connection reopen');
1848
- await rest(progress);
2648
+ await rest(progress, live);
1849
2649
  options.adapter.roomStream?.reopen();
1850
2650
  });
1851
2651
  }
@@ -1866,24 +2666,41 @@ export function composeRuntime(options) {
1866
2666
  });
1867
2667
  }
1868
2668
  function applyOpening(expired) {
1869
- return task(async () => {
1870
- const progress = underway;
2669
+ const progress = underway?.generation === generation ? underway.progress : null;
2670
+ const cycle = progress ?? { complete: true, reachable: true };
2671
+ const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
2672
+ if (opened === generation)
2673
+ opened = null;
2674
+ return task(OPENING_LANE, async (live) => {
1871
2675
  underway = null;
1872
- if (closed || stopping)
2676
+ if (!live())
1873
2677
  return;
1874
2678
  if (progress === null) {
1875
- await reloadExpired(expired);
2679
+ await replacing;
2680
+ await rest(cycle, live);
1876
2681
  return;
1877
2682
  }
2683
+ await roomsCarried(expired);
2684
+ if (!live())
2685
+ return;
2686
+ const replaced = new Set(owed);
2687
+ owed.clear();
1878
2688
  try {
1879
2689
  const local = await openLocal();
1880
2690
  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)));
2691
+ const reloading = held.filter((room) => !expired.includes(room.locator) &&
2692
+ (replaced.has(room.locator) || revoked(room.locator)));
2693
+ for (const room of reloading)
2694
+ catching.add(room.locator);
2695
+ readSide.recover();
2696
+ const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
2697
+ const reloaded = [...(await reloads), ...(await replacing)];
1883
2698
  if (progress.reachable) {
1884
- for (const room of held)
1885
- if (!reloading.includes(room))
2699
+ for (const room of held) {
2700
+ if (!expired.includes(room.locator) && !reloading.includes(room)) {
1886
2701
  await confirmModels(room, local);
2702
+ }
2703
+ }
1887
2704
  }
1888
2705
  const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1889
2706
  if (failed !== undefined)
@@ -1893,21 +2710,21 @@ export function composeRuntime(options) {
1893
2710
  progress.complete = false;
1894
2711
  progress.reachable = false;
1895
2712
  }
1896
- await finishOpening(progress);
2713
+ if (live())
2714
+ await finishOpening(progress, live);
1897
2715
  });
1898
2716
  }
1899
- async function reloadExpired(expired) {
1900
- const progress = { complete: true, reachable: true };
2717
+ async function reloadNow(replaced, progress) {
1901
2718
  try {
1902
2719
  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)));
2720
+ return await Promise.allSettled([...discovered.values()]
2721
+ .filter((room) => replicas.has(room.locator) && replaced(room))
2722
+ .map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
1906
2723
  }
1907
2724
  catch {
1908
2725
  progress.reachable = false;
2726
+ return [];
1909
2727
  }
1910
- await rest(progress);
1911
2728
  }
1912
2729
  async function confirmModels(room, local) {
1913
2730
  for (const attached of ['collection', 'typed-selection']) {
@@ -1920,38 +2737,94 @@ export function composeRuntime(options) {
1920
2737
  function submitLater(locator) {
1921
2738
  if (localSaves)
1922
2739
  return;
1923
- const running = submitting.get(locator);
1924
- if (running !== undefined) {
1925
- running.again = true;
2740
+ void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
2741
+ }
2742
+ async function submitPass(locator, signal) {
2743
+ const room = discovered.get(locator);
2744
+ if (closed || stopping || discoveryPaused || room === undefined)
2745
+ return 'done';
2746
+ const progress = { complete: true, reachable: true };
2747
+ let next;
2748
+ try {
2749
+ await ensureAuthorization();
2750
+ if (authorization === undefined)
2751
+ return 'done';
2752
+ next = await submitRoom(room, await openLocal(), authorization, progress, signal);
2753
+ }
2754
+ catch {
2755
+ progress.reachable = false;
2756
+ next = 'again';
2757
+ }
2758
+ if (signal.aborted)
2759
+ return 'done';
2760
+ await rest(progress);
2761
+ return next === 'again' && !socketOpen ? 'done' : next;
2762
+ }
2763
+ function repairLater(locator, when) {
2764
+ if (!socketOpen || repairing.has(locator))
1926
2765
  return;
2766
+ void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
2767
+ }
2768
+ async function repairPass(locator, signal) {
2769
+ const room = discovered.get(locator);
2770
+ if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
2771
+ return 'done';
2772
+ if (room === undefined || room.replica.admission !== 'admitted')
2773
+ return 'done';
2774
+ const progress = { complete: true, reachable: true };
2775
+ repairing.add(locator);
2776
+ try {
2777
+ const local = await openLocal();
2778
+ await roomTask(locator, async (live) => {
2779
+ const current = () => live() && !signal.aborted;
2780
+ if (!current() || authorization === undefined)
2781
+ return;
2782
+ const withheld = await Promise.race([
2783
+ advance(room, local, authorization, current),
2784
+ abandonedOn(signal),
2785
+ ]);
2786
+ if (withheld !== null && current())
2787
+ await withhold(withheld, room, local, progress);
2788
+ });
1927
2789
  }
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
- })();
2790
+ catch {
2791
+ stale.add(locator);
2792
+ progress.reachable = false;
2793
+ }
2794
+ finally {
2795
+ repairing.delete(locator);
2796
+ }
2797
+ if (signal.aborted)
2798
+ return 'done';
2799
+ await rest(progress);
2800
+ if (!stale.has(locator)) {
2801
+ if ((replicas.get(locator)?.queue.length ?? 0) > 0)
2802
+ submitLater(locator);
2803
+ return 'done';
2804
+ }
2805
+ if (!repairable.has(locator))
2806
+ return 'done';
2807
+ const reset = repairable.get(locator);
2808
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
2809
+ }
2810
+ function resendLater() {
2811
+ if (!socketOpen)
2812
+ return;
2813
+ void recovery.attempt('resend', resendPass, { again: true });
2814
+ }
2815
+ async function resendPass(signal) {
2816
+ if (!socketOpen || closed || stopping || discoveryPaused)
2817
+ return 'done';
2818
+ const reading = { complete: true, reachable: true };
2819
+ try {
2820
+ await conversations.resend(reading);
2821
+ }
2822
+ catch {
2823
+ reading.reachable = false;
2824
+ }
2825
+ if (signal.aborted)
2826
+ return 'done';
2827
+ return reading.reachable && reading.complete ? 'done' : 'again';
1955
2828
  }
1956
2829
  function submitAll() {
1957
2830
  for (const [locator, replica] of replicas) {
@@ -1959,31 +2832,41 @@ export function composeRuntime(options) {
1959
2832
  submitLater(locator);
1960
2833
  }
1961
2834
  }
1962
- async function finishOpening(progress) {
2835
+ async function finishOpening(progress, live) {
1963
2836
  try {
1964
- if (closed || discoveryPaused) {
2837
+ if (!live()) {
1965
2838
  progress.complete = false;
1966
2839
  return;
1967
2840
  }
1968
2841
  submitAll();
2842
+ for (const locator of stale)
2843
+ if (repairable.has(locator))
2844
+ repairLater(locator, {});
1969
2845
  await conversations.reconcile();
1970
2846
  if (status.providerUnreachable)
1971
2847
  conversations.reconnected();
1972
2848
  const reading = { complete: true, reachable: true };
1973
- await conversations.catchUp(reading);
1974
- if (!reading.reachable)
2849
+ if (demand.conversations)
2850
+ await conversations.catchUp(reading);
2851
+ if (progress.reachable && reading.reachable)
2852
+ await conversations.resend(reading);
2853
+ if (!reading.reachable) {
1975
2854
  progress.reachable = false;
2855
+ resendLater();
2856
+ }
1976
2857
  }
1977
2858
  catch {
1978
2859
  progress.complete = false;
1979
2860
  }
1980
2861
  finally {
1981
- complete = progress.complete;
1982
- await rest(progress);
2862
+ if (live()) {
2863
+ complete = progress.complete;
2864
+ await rest(progress, live);
2865
+ }
1983
2866
  }
1984
2867
  }
1985
- async function rest(progress) {
1986
- if (closed)
2868
+ async function rest(progress, live = () => !closed) {
2869
+ if (!live())
1987
2870
  return;
1988
2871
  let lastCompleted = status.lastCompleted;
1989
2872
  let synced = complete && stale.size === 0 && !pending();
@@ -1991,6 +2874,8 @@ export function composeRuntime(options) {
1991
2874
  const completedAt = new Date().toISOString();
1992
2875
  try {
1993
2876
  await (await openLocal()).complete(completedAt);
2877
+ if (!live())
2878
+ return;
1994
2879
  recorded = true;
1995
2880
  lastCompleted = completedAt;
1996
2881
  }
@@ -2005,40 +2890,68 @@ export function composeRuntime(options) {
2005
2890
  ...attention(),
2006
2891
  });
2007
2892
  }
2893
+ function discover() {
2894
+ let release = () => { };
2895
+ const done = new Promise((resolve) => {
2896
+ release = resolve;
2897
+ });
2898
+ discovering = { generation, done, release };
2899
+ return discovering;
2900
+ }
2008
2901
  function applyRoom(locator, roomVersion) {
2009
2902
  announced.set(locator, roomVersion);
2010
- return task(async () => {
2011
- await answering.get(locator);
2012
- if (closed || stopping || discoveryPaused || authorization === undefined)
2903
+ return task(roomLane(locator), async (live) => {
2904
+ if (discovering?.generation === generation)
2905
+ await discovering.done;
2906
+ const saving = answering.get(locator);
2907
+ if (saving?.generation === generation)
2908
+ await saving.answered;
2909
+ if (!live() || authorization === undefined)
2013
2910
  return;
2014
2911
  const room = discovered.get(locator);
2015
2912
  const held = replicas.get(locator);
2016
- if (room === undefined || room.replica.admission !== 'admitted')
2913
+ if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
2017
2914
  return;
2018
2915
  if (held !== undefined &&
2019
2916
  (held.confirmed.record.replica.selection !== room.replica.selection ||
2020
2917
  options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
2021
2918
  return;
2022
2919
  }
2023
- const during = underway;
2920
+ const during = underway?.generation === generation ? underway.progress : null;
2024
2921
  const progress = during ?? { complete: true, reachable: true };
2025
2922
  if (during === null)
2026
2923
  moveTo(transferring(status));
2027
2924
  try {
2028
2925
  const local = await openLocal();
2029
- const withheld = await advance(room, local, authorization);
2030
- if (withheld !== null)
2031
- await withhold(withheld, room, local, progress);
2926
+ await admittedRead(async () => {
2927
+ if (!live() || authorization === undefined)
2928
+ return;
2929
+ const withheld = await advance(room, local, authorization, live);
2930
+ if (withheld !== null)
2931
+ await withhold(withheld, room, local, progress);
2932
+ });
2032
2933
  }
2033
2934
  catch {
2034
2935
  stale.add(locator);
2035
2936
  }
2036
2937
  finally {
2037
2938
  if (during === null)
2038
- await rest(progress);
2939
+ await rest(progress, live);
2039
2940
  }
2040
2941
  });
2041
2942
  }
2943
+ function roomsCarried(replacing) {
2944
+ const replaced = new Set(replacing.map(roomLane));
2945
+ return Promise.all([...lanes]
2946
+ .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
2947
+ .map(([, held]) => held.chain));
2948
+ }
2949
+ const receipts = {
2950
+ heads: async () => (await openLocal()).receivedHeads(),
2951
+ parts: async (key, count) => (await openLocal()).receivedParts(key, count),
2952
+ keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
2953
+ drop: async (keys) => (await openLocal()).dropReceived(keys),
2954
+ };
2042
2955
  function observeStream() {
2043
2956
  const stream = options.adapter.roomStream;
2044
2957
  if (stream === undefined || withdrawStream !== undefined)
@@ -2048,6 +2961,9 @@ export function composeRuntime(options) {
2048
2961
  return;
2049
2962
  if (event.kind === 'opened') {
2050
2963
  socketOpen = true;
2964
+ generation += 1;
2965
+ conversations.reopened();
2966
+ recovery.resume();
2051
2967
  void sync('opened');
2052
2968
  }
2053
2969
  else if (event.kind === 'closed')
@@ -2056,7 +2972,7 @@ export function composeRuntime(options) {
2056
2972
  void applyOpening(event.expired);
2057
2973
  else
2058
2974
  void applyRoom(event.locator, event.roomVersion);
2059
- });
2975
+ }, receipts);
2060
2976
  }
2061
2977
  function writesHeld() {
2062
2978
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -2071,6 +2987,7 @@ export function composeRuntime(options) {
2071
2987
  haltedRefusal = false;
2072
2988
  if (mode !== serviceMode) {
2073
2989
  serviceMode = mode;
2990
+ queueReadiness();
2074
2991
  journal.record(`service mode ${mode}`);
2075
2992
  for (const listener of serviceModeListeners)
2076
2993
  listener(mode);
@@ -2083,6 +3000,8 @@ export function composeRuntime(options) {
2083
3000
  if (stream === undefined || withdrawConversations !== undefined)
2084
3001
  return;
2085
3002
  withdrawConversations = stream.observe((event) => {
3003
+ if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
3004
+ queueReadiness();
2086
3005
  if (conversations.receive(event)) {
2087
3006
  void ready.then(scheduleCurrency);
2088
3007
  return;
@@ -2094,17 +3013,18 @@ export function composeRuntime(options) {
2094
3013
  });
2095
3014
  }
2096
3015
  function scheduleCurrency() {
2097
- if (currencyQueued || closed || stopping)
3016
+ if (currencyQueued === generation || closed || stopping)
2098
3017
  return;
2099
- currencyQueued = true;
2100
- void task(runCurrency);
3018
+ currencyQueued = generation;
3019
+ void task(CONVERSATION_LANE, runCurrency);
2101
3020
  }
2102
- async function runCurrency() {
2103
- currencyQueued = false;
2104
- if (closed || stopping || discoveryPaused)
3021
+ async function runCurrency(live) {
3022
+ if (currencyQueued === generation)
3023
+ currencyQueued = null;
3024
+ if (!live())
2105
3025
  return;
2106
3026
  await conversations.reconcile();
2107
- if (conversations.current()) {
3027
+ if (conversations.current() || !demand.conversations) {
2108
3028
  show();
2109
3029
  return;
2110
3030
  }
@@ -2116,19 +3036,24 @@ export function composeRuntime(options) {
2116
3036
  catch {
2117
3037
  progress.reachable = false;
2118
3038
  }
2119
- await rest(progress);
3039
+ await rest(progress, live);
2120
3040
  }
2121
3041
  function close() {
2122
3042
  if (closed)
2123
3043
  return;
2124
3044
  closed = true;
3045
+ if (narrowing !== null)
3046
+ clearTimeout(narrowing);
3047
+ if (rechecking !== null)
3048
+ clearInterval(rechecking);
3049
+ readinessWatches.clear();
2125
3050
  withdrawStream?.();
2126
3051
  withdrawStream = undefined;
2127
3052
  withdrawConversations?.();
2128
3053
  withdrawConversations = undefined;
2129
3054
  withdrawServiceMode?.();
2130
3055
  conversations.close();
2131
- stopTimer();
3056
+ void recovery.retire();
2132
3057
  store?.close();
2133
3058
  void opening?.then((opened) => {
2134
3059
  opened.close();
@@ -2146,9 +3071,8 @@ export function composeRuntime(options) {
2146
3071
  }
2147
3072
  async function stop() {
2148
3073
  stopping = true;
2149
- stopTimer();
2150
- await chain.catch(() => undefined);
2151
- await Promise.all([...submitting.values()].map((run) => run.done));
3074
+ await recovery.retire();
3075
+ await Promise.all(working);
2152
3076
  await mutations;
2153
3077
  close();
2154
3078
  }
@@ -2170,17 +3094,101 @@ export function composeRuntime(options) {
2170
3094
  return planOf(input, roomFacts(reach), planReach(reach));
2171
3095
  }
2172
3096
  function watchPlan(plan, deliver) {
2173
- materialize(reachedRooms(plan));
2174
- return readSide.watch(plan, identityOf(plan), deliver);
3097
+ let seeded = false;
3098
+ let pending = true;
3099
+ const shown = () => {
3100
+ const seed = seeds.get(plan.key);
3101
+ if (seed === undefined || seeded || !pending)
3102
+ return;
3103
+ seeded = true;
3104
+ deliver(seededResult(seed));
3105
+ };
3106
+ const watchers = seedWatchers.get(plan.key) ?? new Set();
3107
+ seedWatchers.set(plan.key, watchers);
3108
+ watchers.add(shown);
3109
+ const stop = readSide.watch(plan, identityOf(plan), () => {
3110
+ const cover = covered(plan);
3111
+ awaitPreparation(cover);
3112
+ return seedBehind(plan, cover);
3113
+ }, (message) => {
3114
+ const answered = message;
3115
+ pending = whole(answered) && 'pending' in answered.result;
3116
+ if (pending && seeds.has(plan.key)) {
3117
+ shown();
3118
+ return;
3119
+ }
3120
+ seeded = false;
3121
+ deliver(message);
3122
+ });
3123
+ return () => {
3124
+ watchers.delete(shown);
3125
+ if (watchers.size === 0)
3126
+ seedWatchers.delete(plan.key);
3127
+ stop();
3128
+ };
2175
3129
  }
2176
3130
  async function searched(input, reach) {
2177
3131
  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);
3132
+ const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
3133
+ for (;;) {
3134
+ const cover = covered(filter);
3135
+ awaitPreparation(cover);
3136
+ const pending = 'pending' in cover ? [...cover.pending] : [];
3137
+ const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3138
+ const held = covered({
3139
+ ...filter,
3140
+ rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
3141
+ });
3142
+ const covering = 'pending' in held ? [] : held.coverage.rooms;
3143
+ const unindexed = covering
3144
+ .filter(({ room }) => !searchIndex.rooms().has(room))
3145
+ .map(({ room }) => room);
3146
+ for (const room of unindexed)
3147
+ pending.push({ room, awaits: 'index' });
3148
+ const listed = conversations.searchable();
3149
+ const named = reach !== null && !reach.conversations
3150
+ ? []
3151
+ : (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
3152
+ const waiting = named.filter((one) => !searchIndex.conversations().has(one));
3153
+ for (const conversation of waiting)
3154
+ pending.push({ conversation, awaits: 'index' });
3155
+ if (unindexed.length > 0 || waiting.length > 0)
3156
+ index(unindexed, waiting);
3157
+ const complete = pending.length === 0 && !('pending' in cover);
3158
+ const answering = complete
3159
+ ? filter
3160
+ : {
3161
+ ...filter,
3162
+ rooms: covering
3163
+ .filter(({ room }) => searchIndex.rooms().has(room))
3164
+ .map(({ room }) => room),
3165
+ };
3166
+ const answered = covered(answering);
3167
+ if ('pending' in answered)
3168
+ continue;
3169
+ const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
3170
+ const seen = searchIndex.changes();
3171
+ await searchIndex.caughtUp();
3172
+ const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
3173
+ if (selected !== null && 'pending' in selected)
3174
+ continue;
3175
+ if (searchIndex.changes() !== seen ||
3176
+ JSON.stringify(covered(answering)) !== JSON.stringify(answered))
3177
+ continue;
3178
+ const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms === null ? null : new Set(answering.rooms), plan.collection
3179
+ ? new Map(answered.coverage.rooms.map(({ room }) => [
3180
+ room,
3181
+ replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
3182
+ ]))
3183
+ : null);
3184
+ const coverage = {
3185
+ rooms: answered.coverage.rooms,
3186
+ conversations: searchedConversations,
3187
+ index: searchIndex.generation(),
3188
+ overlay: answered.coverage.overlay,
3189
+ };
3190
+ return complete ? { ...hits, coverage } : { pending, ...hits, coverage };
3191
+ }
2184
3192
  }
2185
3193
  const watch = (topic, input, deliver) => {
2186
3194
  if (topic === 'room-drafts') {
@@ -2226,6 +3234,9 @@ export function composeRuntime(options) {
2226
3234
  presentationListeners.delete(listener);
2227
3235
  };
2228
3236
  }
3237
+ if (topic === 'readiness') {
3238
+ return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
3239
+ }
2229
3240
  if (topic === 'rooms') {
2230
3241
  roomListeners.add(deliver);
2231
3242
  deliver(listed());
@@ -2392,21 +3403,29 @@ export function composeRuntime(options) {
2392
3403
  return journal.export();
2393
3404
  },
2394
3405
  persistence() {
2395
- return options.shell.persistentStorage;
3406
+ return persistence === 'granted';
3407
+ },
3408
+ async retry() {
3409
+ journal.record('retry');
3410
+ unindexable.clear();
3411
+ queueReadiness();
3412
+ resume();
2396
3413
  },
2397
3414
  observe: observing(watch),
2398
3415
  async reauthorize() {
2399
3416
  if (refusal === null || !refusal.reauthorizationOffered)
2400
3417
  return;
2401
3418
  journal.record('connection reauthorize');
2402
- await chain.catch(() => undefined);
3419
+ await Promise.all([...lanes.values()]
3420
+ .filter((held) => held.generation === generation)
3421
+ .map((held) => held.chain));
2403
3422
  await mutations;
2404
3423
  authorization = undefined;
2405
3424
  discoveryPaused = false;
2406
3425
  if (options.adapter.roomStream === undefined)
2407
3426
  await sync('unstreamed');
2408
3427
  else
2409
- await task(ensureAuthorization);
3428
+ await task(OPENING_LANE, ensureAuthorization);
2410
3429
  },
2411
3430
  };
2412
3431
  const reachable = {
@@ -2417,6 +3436,22 @@ export function composeRuntime(options) {
2417
3436
  query: (plan) => rowsQuery(plan),
2418
3437
  watchPlan,
2419
3438
  search: searched,
3439
+ watchReadiness,
3440
+ };
3441
+ return {
3442
+ md,
3443
+ seed,
3444
+ stop,
3445
+ lose: loseRooms,
3446
+ watch,
3447
+ acquire,
3448
+ persisted(answer) {
3449
+ if (answer === persistence)
3450
+ return;
3451
+ persistence = answer;
3452
+ journal.record(`persistence ${answer}`);
3453
+ queueReadiness();
3454
+ },
3455
+ within: (reach, port) => narrowed(reachable, reach, port),
2420
3456
  };
2421
- return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2422
3457
  }