@agentshouse/mdruntime 0.1.0-cdf82f77fae68 → 0.1.0-ceb97106cc2fd

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