@agentshouse/mdruntime 0.1.0-cdf82f77fae68 → 0.1.0-ce131cb435f8b

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 (146) hide show
  1. package/AGENTS.md +271 -290
  2. package/README.md +8 -8
  3. package/dist/adapter.d.ts +51 -9
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/attempt.d.ts.map +1 -1
  6. package/dist/attempt.js +14 -1
  7. package/dist/bootstrap.d.ts +3 -2
  8. package/dist/bootstrap.d.ts.map +1 -1
  9. package/dist/bootstrap.js +33 -7
  10. package/dist/change-set.d.ts +2 -0
  11. package/dist/change-set.d.ts.map +1 -1
  12. package/dist/change-set.js +36 -10
  13. package/dist/chat/acts.d.ts +3 -4
  14. package/dist/chat/acts.d.ts.map +1 -1
  15. package/dist/chat/acts.js +10 -10
  16. package/dist/chat/earlier.d.ts +10 -0
  17. package/dist/chat/earlier.d.ts.map +1 -0
  18. package/dist/chat/earlier.js +54 -0
  19. package/dist/chat/kit.d.ts +2 -1
  20. package/dist/chat/kit.d.ts.map +1 -1
  21. package/dist/chat/kit.js +3 -3
  22. package/dist/chat/list.d.ts +8 -1
  23. package/dist/chat/list.d.ts.map +1 -1
  24. package/dist/chat/list.js +48 -8
  25. package/dist/chat/markdown.d.ts +2 -1
  26. package/dist/chat/markdown.d.ts.map +1 -1
  27. package/dist/chat/markdown.js +7 -6
  28. package/dist/chat/message.d.ts.map +1 -1
  29. package/dist/chat/message.js +2 -2
  30. package/dist/chat/messages.d.ts +1 -0
  31. package/dist/chat/messages.d.ts.map +1 -1
  32. package/dist/chat/messages.js +4 -0
  33. package/dist/chat/provider.d.ts +1 -1
  34. package/dist/chat/provider.d.ts.map +1 -1
  35. package/dist/chat/provider.js +62 -79
  36. package/dist/chat/question.d.ts +1 -1
  37. package/dist/chat/question.d.ts.map +1 -1
  38. package/dist/chat/question.js +7 -18
  39. package/dist/chat/ready.d.ts +1 -0
  40. package/dist/chat/ready.d.ts.map +1 -1
  41. package/dist/chat/ready.js +4 -4
  42. package/dist/chat/style.js +4 -4
  43. package/dist/client.d.ts +17 -9
  44. package/dist/client.d.ts.map +1 -1
  45. package/dist/client.js +19 -6
  46. package/dist/collection.d.ts +20 -5
  47. package/dist/collection.d.ts.map +1 -1
  48. package/dist/collection.js +309 -33
  49. package/dist/connection.d.ts +31 -9
  50. package/dist/connection.d.ts.map +1 -1
  51. package/dist/connection.js +1573 -372
  52. package/dist/conversation.d.ts +20 -7
  53. package/dist/conversation.d.ts.map +1 -1
  54. package/dist/conversation.js +474 -163
  55. package/dist/durability.d.ts +7 -1
  56. package/dist/durability.d.ts.map +1 -1
  57. package/dist/entry.d.ts +17 -0
  58. package/dist/entry.d.ts.map +1 -0
  59. package/dist/entry.js +157 -0
  60. package/dist/fence.d.ts +6 -0
  61. package/dist/fence.d.ts.map +1 -0
  62. package/dist/fence.js +10 -0
  63. package/dist/host.d.ts +15 -3
  64. package/dist/host.d.ts.map +1 -1
  65. package/dist/host.js +94 -12
  66. package/dist/identity.d.ts +2 -0
  67. package/dist/identity.d.ts.map +1 -1
  68. package/dist/identity.js +6 -0
  69. package/dist/index.d.ts +10 -7
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +2 -0
  72. package/dist/memory.d.ts +8 -2
  73. package/dist/memory.d.ts.map +1 -1
  74. package/dist/memory.js +33 -6
  75. package/dist/model.d.ts +4 -2
  76. package/dist/model.d.ts.map +1 -1
  77. package/dist/model.js +10 -1
  78. package/dist/observation.d.ts +7 -2
  79. package/dist/observation.d.ts.map +1 -1
  80. package/dist/observation.js +29 -12
  81. package/dist/outcome.d.ts +12 -2
  82. package/dist/outcome.d.ts.map +1 -1
  83. package/dist/outcome.js +4 -0
  84. package/dist/overlay.d.ts +3 -0
  85. package/dist/overlay.d.ts.map +1 -1
  86. package/dist/overlay.js +62 -55
  87. package/dist/query.d.ts +56 -4
  88. package/dist/query.d.ts.map +1 -1
  89. package/dist/query.js +36 -7
  90. package/dist/reach.d.ts +6 -3
  91. package/dist/reach.d.ts.map +1 -1
  92. package/dist/reach.js +30 -5
  93. package/dist/react.d.ts +8 -6
  94. package/dist/react.d.ts.map +1 -1
  95. package/dist/react.js +7 -17
  96. package/dist/read-side.d.ts +16 -7
  97. package/dist/read-side.d.ts.map +1 -1
  98. package/dist/read-side.js +484 -100
  99. package/dist/readiness.d.ts +53 -0
  100. package/dist/readiness.d.ts.map +1 -0
  101. package/dist/readiness.js +33 -0
  102. package/dist/reads.d.ts +11 -0
  103. package/dist/reads.d.ts.map +1 -0
  104. package/dist/reads.js +98 -0
  105. package/dist/recovery.d.ts +25 -0
  106. package/dist/recovery.d.ts.map +1 -0
  107. package/dist/recovery.js +197 -0
  108. package/dist/refresh.d.ts +4 -4
  109. package/dist/refresh.d.ts.map +1 -1
  110. package/dist/refresh.js +18 -11
  111. package/dist/replica.d.ts +10 -5
  112. package/dist/replica.d.ts.map +1 -1
  113. package/dist/replica.js +95 -32
  114. package/dist/room-draft.d.ts +13 -1
  115. package/dist/room-draft.d.ts.map +1 -1
  116. package/dist/room-draft.js +2 -2
  117. package/dist/rows.d.ts +6 -13
  118. package/dist/rows.d.ts.map +1 -1
  119. package/dist/rows.js +10 -13
  120. package/dist/save.d.ts +9 -3
  121. package/dist/save.d.ts.map +1 -1
  122. package/dist/save.js +68 -22
  123. package/dist/search.d.ts +49 -6
  124. package/dist/search.d.ts.map +1 -1
  125. package/dist/search.js +192 -47
  126. package/dist/seed.d.ts +16 -0
  127. package/dist/seed.d.ts.map +1 -0
  128. package/dist/seed.js +75 -0
  129. package/dist/shared.d.ts +14 -0
  130. package/dist/shared.d.ts.map +1 -0
  131. package/dist/shared.js +394 -0
  132. package/dist/status.d.ts +4 -0
  133. package/dist/status.d.ts.map +1 -1
  134. package/dist/store.d.ts +59 -12
  135. package/dist/store.d.ts.map +1 -1
  136. package/dist/store.js +350 -86
  137. package/dist/turn.d.ts +9 -0
  138. package/dist/turn.d.ts.map +1 -0
  139. package/dist/turn.js +64 -0
  140. package/dist/windows.d.ts +26 -0
  141. package/dist/windows.d.ts.map +1 -0
  142. package/dist/windows.js +93 -0
  143. package/dist/wire.d.ts +152 -32
  144. package/dist/wire.d.ts.map +1 -1
  145. package/dist/wire.js +52 -31
  146. package/package.json +12 -8
@@ -1,30 +1,47 @@
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, plain, 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';
9
+ import { capped, MAXIMUM_WINDOW } from './windows.js';
8
10
  import { bootstrapRoom } from './bootstrap.js';
9
- import { changeSet, mergeChangeSets } from './change-set.js';
11
+ import { changeSet, entering, mergeChangeSets } from './change-set.js';
10
12
  import { createConversationStore, } from './conversation.js';
11
13
  import { queuedFile } from './change.js';
12
- import { validRoot } from './collection.js';
14
+ import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
13
15
  import { runtimeError } from './failure.js';
14
16
  import { createLog } from './log.js';
15
17
  import { binaryRecord } from './binary.js';
16
- import { absentBinary, binaryAnswer, calledResult, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
18
+ import { absentBinary, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
19
+ import { createRecovery } from './recovery.js';
20
+ import { doorReads } from './reads.js';
17
21
  import { refreshRoom } from './refresh.js';
18
- import { buildBatch, joined, limitedIn, requeued } from './save.js';
19
- import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
20
- import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
22
+ import { buildBatch, joined, limitedIn, queuedDocuments, requeued, } from './save.js';
23
+ import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, standing, } from './replica.js';
24
+ import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
21
25
  import { same, saving, STARTING, transferring, } from './status.js';
22
26
  import { detectStorageLoss, nextGeneration } from './durability.js';
23
- import { roomDraftRevision } from './identity.js';
27
+ import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
28
+ import { FencedMap } from './fence.js';
29
+ import { tally } from './shared.js';
30
+ import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
31
+ import { identityTime, roomDraftRevision } from './identity.js';
24
32
  import { beneath, normalize } from './paths.js';
25
33
  import { openRuntimeStore, } from './store.js';
26
34
  import { unconfirmed } from './transfer.js';
27
35
  import { governs, holds, narrowed, planReach, } from './reach.js';
36
+ function roomsAlone(plan) {
37
+ return (plan.reach === null &&
38
+ plan.root.types === null &&
39
+ plan.root.where === null &&
40
+ plan.root.pathPrefix === null &&
41
+ plan.paths === null &&
42
+ plan.hop === null &&
43
+ plan.backlinks === null);
44
+ }
28
45
  function within(root, path) {
29
46
  return root === '' || beneath(root, path);
30
47
  }
@@ -38,25 +55,68 @@ function declaredSelection(room) {
38
55
  root !== '' &&
39
56
  selection.roots.every((other, at) => at === index || !within(other, root)));
40
57
  }
58
+ const OPENING_LANE = 'opening';
59
+ const CONVERSATION_LANE = 'conversations';
60
+ const ROOM_READS = 256;
61
+ const ROOM_ATTEMPTS = 16;
62
+ function operationOf(input) {
63
+ const operation = input?.operation_id;
64
+ return typeof operation === 'string' ? operation : null;
65
+ }
66
+ const outside = new Map();
67
+ function outsideOf(roots) {
68
+ const key = JSON.stringify(roots);
69
+ const held = outside.get(key);
70
+ if (held !== undefined)
71
+ return held;
72
+ const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
73
+ const made = {
74
+ documents: (path) => (markdownPath(path) ? counted(path) : 0),
75
+ binaries: counted,
76
+ };
77
+ outside.set(key, made);
78
+ return made;
79
+ }
80
+ function abandonedOn(signal) {
81
+ return new Promise((resolve) => {
82
+ if (signal.aborted)
83
+ resolve(null);
84
+ else
85
+ signal.addEventListener('abort', () => resolve(null), { once: true });
86
+ });
87
+ }
88
+ function roomLane(locator) {
89
+ return `room ${locator}`;
90
+ }
41
91
  export function composeRuntime(options) {
42
92
  const localSaves = options.adapter.localSaves === true;
43
93
  let authorization = options.authorization;
44
94
  let opening;
45
95
  let store;
46
- const replicas = new Map();
47
- const queuesWithoutReplica = new Map();
96
+ const fences = new Map();
97
+ const isFenced = (locator) => fences.has(locator);
98
+ let fencings = 0;
99
+ const lastFenced = new Map();
100
+ const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
101
+ let discoveries = 0;
102
+ const replicas = new FencedMap(isFenced);
103
+ const queuesWithoutReplica = new FencedMap(isFenced);
48
104
  const pendings = new Map();
49
- const roomDrafts = new Map();
105
+ const seeds = new Map();
106
+ const seedWatchers = new Map();
107
+ const roomDrafts = new FencedMap(isFenced);
50
108
  const presentations = new Map();
51
109
  let limits = [];
52
110
  let capabilities = [];
53
- const rooms = new Map();
111
+ const rooms = new FencedMap(isFenced);
54
112
  const counted = new Map();
55
113
  const unreadable = new Set();
56
- const models = new Map();
57
- const liveModels = new Map();
114
+ const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
115
+ const models = new FencedMap(isModelFenced);
116
+ const liveModels = new FencedMap(isModelFenced);
58
117
  const invalidModels = new Set();
59
118
  const discovered = new Map();
119
+ const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
60
120
  let roomsListed = false;
61
121
  const scopedRefusals = new Map();
62
122
  const sourceRefusals = new Map();
@@ -84,7 +144,6 @@ export function composeRuntime(options) {
84
144
  let status = STARTING;
85
145
  let shown = STARTING;
86
146
  let discoveryPaused = false;
87
- let resetTimer;
88
147
  let closed = false;
89
148
  let stopping = false;
90
149
  let withdrawStream;
@@ -92,12 +151,23 @@ export function composeRuntime(options) {
92
151
  let underway = null;
93
152
  let streamed = null;
94
153
  let handed = false;
95
- const submitting = new Map();
154
+ const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
155
+ const repairing = new Set();
156
+ const calling = new Set();
157
+ const repairable = new Map();
158
+ let outcomeRetention = null;
96
159
  const answering = new Map();
97
160
  let complete = false;
98
161
  let recorded = false;
99
162
  const stale = new Set();
100
- let chain = Promise.resolve();
163
+ let generation = 0;
164
+ let discovering = null;
165
+ let opened = null;
166
+ const owed = new Set();
167
+ const reloadsDue = new Map();
168
+ const lanes = new Map();
169
+ let roomReads = { generation, free: ROOM_READS, waiting: [] };
170
+ const working = new Set();
101
171
  let mutations = Promise.resolve();
102
172
  const roomListeners = new Set();
103
173
  const refusalListeners = new Set();
@@ -108,7 +178,7 @@ export function composeRuntime(options) {
108
178
  const presentationListeners = new Set();
109
179
  const readSide = createReadSide();
110
180
  let withdrawConversations;
111
- let currencyQueued = false;
181
+ let currencyQueued = null;
112
182
  let socketOpen = false;
113
183
  let serviceMode = 'open';
114
184
  let haltedRefusal = false;
@@ -116,24 +186,39 @@ export function composeRuntime(options) {
116
186
  const serviceModeListeners = new Set();
117
187
  let member = null;
118
188
  const memberListeners = new Set();
119
- const materialized = new Set();
120
- let everyRoom = false;
121
- const transfers = new Map();
189
+ const prepared = new Set();
190
+ const projecting = new Set();
191
+ const unpreparedDemand = new Set();
192
+ const unindexable = new Set();
193
+ let preparing = false;
194
+ let targets = {};
195
+ let persistence = options.shell.persistence;
196
+ let capacity = null;
197
+ const paused = new Set();
198
+ let rechecking = null;
199
+ const readinessWatches = new Set();
200
+ let readinessQueued = false;
201
+ const catching = new Set();
202
+ const transfers = new FencedMap(isFenced);
122
203
  const conversations = createConversationStore({
123
204
  agents: declaredAgents,
124
- documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
205
+ documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
125
206
  call: (name, input) => invokeAction(name, input),
126
207
  durably,
127
208
  stream: () => options.adapter.conversations,
128
209
  publish: publishChangeSet,
210
+ online: () => connectionOf() !== 'unreachable' && refusal === null,
129
211
  closed: () => closed,
212
+ unanswered: resendLater,
213
+ expired: (operation) => pastHorizon(operation),
130
214
  });
131
215
  const searchIndex = createSearchIndex({
132
- documents: () => readSide.documents(),
216
+ room: (room) => readSide.room(room),
133
217
  document: (room, path) => readSide.document(room, path),
134
- messages: () => conversations.messages(),
135
- message: (conversation, identity) => conversations.message(conversation, identity),
136
218
  });
219
+ const roomsToIndex = new Set();
220
+ const conversationsToIndex = new Set();
221
+ let indexing = false;
137
222
  const journal = createLog();
138
223
  let notifyReady = () => { };
139
224
  const ready = new Promise((resolve) => {
@@ -188,6 +273,15 @@ export function composeRuntime(options) {
188
273
  const value = presentations.get(app);
189
274
  return value === undefined ? null : structuredClone(value);
190
275
  }
276
+ function keepPresentation(app, next) {
277
+ return exclusive(async () => {
278
+ const local = await openLocal();
279
+ const stored = next(presentations.get(app));
280
+ await local.present({ app, value: stored });
281
+ presentations.set(app, stored);
282
+ publishChangeSet(changeSet('', null, null, { presentation: app }));
283
+ });
284
+ }
191
285
  function emitPresentation(app) {
192
286
  if (closed)
193
287
  return;
@@ -212,9 +306,16 @@ export function composeRuntime(options) {
212
306
  returned: structuredClone(record.returned),
213
307
  });
214
308
  }
309
+ for (const [room, refusal] of sourceRefusals)
310
+ attentionRequired.push({ room, refusal: structuredClone(refusal) });
215
311
  attentionRequired.sort((left, right) => {
216
312
  if (left.room !== right.room)
217
313
  return left.room < right.room ? -1 : 1;
314
+ if ('refusal' in left || 'refusal' in right) {
315
+ if ('refusal' in left && 'refusal' in right)
316
+ return 0;
317
+ return 'refusal' in left ? -1 : 1;
318
+ }
218
319
  if (left.path !== right.path)
219
320
  return left.path < right.path ? -1 : 1;
220
321
  return 0;
@@ -233,12 +334,9 @@ export function composeRuntime(options) {
233
334
  if (replica === undefined)
234
335
  return 0;
235
336
  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;
337
+ const weighed = outsideOf(roots);
338
+ return (tally(replica.visible.documents, weighed.documents) +
339
+ tally(replica.visible.binaries, weighed.binaries));
242
340
  }
243
341
  function listed() {
244
342
  return structuredClone([...rooms.values()].map((room) => {
@@ -265,15 +363,19 @@ export function composeRuntime(options) {
265
363
  return { listed: roomsListed, rooms: held };
266
364
  }
267
365
  function emitRooms() {
366
+ readSide.recover();
367
+ queueReadiness();
268
368
  const value = listed();
269
369
  for (const listener of roomListeners)
270
370
  listener(value);
271
371
  }
272
372
  function emitRefusal() {
373
+ queueReadiness();
273
374
  for (const listener of refusalListeners)
274
375
  listener(refusal);
275
376
  }
276
377
  function emitStorageLoss() {
378
+ queueReadiness();
277
379
  for (const listener of storageLossListeners)
278
380
  listener(storageLoss);
279
381
  }
@@ -290,6 +392,7 @@ export function composeRuntime(options) {
290
392
  function show() {
291
393
  if (closed)
292
394
  return;
395
+ queueReadiness();
293
396
  const next = status.state === 'synced' && !conversations.current()
294
397
  ? { ...status, state: 'syncing' }
295
398
  : status;
@@ -319,42 +422,459 @@ export function composeRuntime(options) {
319
422
  }
320
423
  }
321
424
  function applyChangeSet(set) {
322
- const held = everyRoom
323
- ? set
324
- : { ...set, documents: set.documents.filter(({ locator }) => materialized.has(locator)) };
325
- readSide.apply(held);
326
- searchIndex.changed(held);
327
- }
328
- function materialize(locators) {
329
- if (everyRoom)
425
+ const held = {
426
+ ...set,
427
+ documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
428
+ };
429
+ const applied = readSide.apply(held, () => searchIndex.changed(held));
430
+ if (applied instanceof Promise)
431
+ void applied.catch(unpublished);
432
+ }
433
+ function unpublished(error) {
434
+ journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
435
+ }
436
+ async function referenced(locator) {
437
+ for (;;) {
438
+ const record = replicas.get(locator)?.confirmed.record;
439
+ if (record === undefined || closed)
440
+ return;
441
+ const held = { text: record.documents, binaries: record.binaries };
442
+ const composition = compositionOf(record);
443
+ if (referencesIndexed(held, composition))
444
+ return;
445
+ await indexReferrers(held, composition);
446
+ }
447
+ }
448
+ async function prepareReplica(locator) {
449
+ await referenced(locator);
450
+ const replica = replicas.get(locator);
451
+ if (replica === undefined || closed || prepared.has(locator))
330
452
  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));
453
+ const visible = replica.visible;
454
+ const paths = [
455
+ ...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
456
+ ];
457
+ const entered = [];
458
+ projecting.add(locator);
459
+ try {
460
+ await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
461
+ const current = replicas.get(locator);
462
+ if (current === undefined || closed)
463
+ return;
464
+ const set = entering(locator, current, chunk);
465
+ entered.push(set);
466
+ return readSide.apply(set);
467
+ });
468
+ if (!replicas.has(locator) || closed)
469
+ return;
470
+ prepared.add(locator);
471
+ }
472
+ finally {
473
+ projecting.delete(locator);
341
474
  }
342
- if (sets.length === 0)
475
+ conversations.changed(mergeChangeSets(entered));
476
+ readSide.recover();
477
+ queueReadiness();
478
+ }
479
+ function unprepared() {
480
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
481
+ }
482
+ function unbuilt(locator, error) {
483
+ unindexable.add(locator);
484
+ journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
485
+ queueReadiness();
486
+ }
487
+ function prepare(locators = []) {
488
+ for (const locator of locators)
489
+ unpreparedDemand.add(locator);
490
+ if (preparing)
343
491
  return;
344
- const set = mergeChangeSets(sets);
345
- readSide.apply(set);
346
- searchIndex.changed(set);
492
+ preparing = true;
493
+ void (async () => {
494
+ try {
495
+ for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
496
+ await yielded();
497
+ if (closed)
498
+ return;
499
+ unpreparedDemand.delete(waiting);
500
+ await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
501
+ }
502
+ }
503
+ finally {
504
+ preparing = false;
505
+ }
506
+ })();
347
507
  }
348
508
  function reachedRooms(plan) {
349
- if (plan.rooms !== null)
350
- return plan.rooms;
351
- if (plan.reach === null)
509
+ const reach = plan.reach;
510
+ const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
511
+ const named = plan.rooms ??
512
+ (reached === null
513
+ ? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
514
+ : [...reached]);
515
+ return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
516
+ }
517
+ function awaited(locator) {
518
+ return (!roomsListed ||
519
+ (rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
520
+ }
521
+ function covered(plan) {
522
+ const locators = reachedRooms(plan);
523
+ if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
524
+ return { pending: [] };
525
+ }
526
+ const pending = [];
527
+ const coverage = [];
528
+ const overlay = [];
529
+ for (const locator of locators) {
530
+ const replica = replicas.get(locator);
531
+ if (replica === undefined) {
532
+ if (awaited(locator))
533
+ pending.push({ room: locator, awaits: 'acquisition' });
534
+ continue;
535
+ }
536
+ if (!prepared.has(locator)) {
537
+ pending.push({ room: locator, awaits: 'preparation' });
538
+ continue;
539
+ }
540
+ coverage.push({
541
+ room: locator,
542
+ generation: replica.confirmed.record.transfer,
543
+ current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
544
+ });
545
+ for (const batch of replica.queue)
546
+ overlay.push(batch.id);
547
+ }
548
+ if (pending.length > 0)
549
+ return { pending };
550
+ return { coverage: { rooms: coverage, indexes: [], overlay } };
551
+ }
552
+ function awaitPreparation(cover) {
553
+ if (!('pending' in cover))
554
+ return;
555
+ const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
556
+ if (waiting.length > 0)
557
+ prepare(waiting.map(({ room }) => room));
558
+ }
559
+ function requiredRooms(reach) {
560
+ if (reach === null)
561
+ return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
562
+ const named = [...new Set(reach.rooms)].sort();
563
+ if (roomsListed)
564
+ return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
565
+ return { listed: named.every((room) => replicas.has(room)), rooms: named };
566
+ }
567
+ function roomState(locator, search) {
568
+ if (sourceRefusals.has(locator))
569
+ return 'refused';
570
+ const listedRoom = discovered.get(locator);
571
+ if (roomsListed &&
572
+ (listedRoom === undefined ||
573
+ listedRoom.replica.admission !== 'admitted' ||
574
+ unreadable.has(locator)))
575
+ return 'unadmitted';
576
+ if (!replicas.has(locator))
577
+ return 'acquiring';
578
+ if (!prepared.has(locator))
579
+ return 'preparing';
580
+ if (search && !searchIndex.rooms().has(locator))
581
+ return 'indexing';
582
+ return 'complete';
583
+ }
584
+ function conversationCoverage(reach, search) {
585
+ const declared = reach === null || reach.conversations;
586
+ if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
587
+ return 'undeclared';
588
+ if (!conversations.known() || !conversations.current())
589
+ return 'acquiring';
590
+ if (!search)
591
+ return 'complete';
592
+ const unindexed = conversations
593
+ .searchable()
594
+ .filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
595
+ if (unindexed.length === 0)
596
+ return 'complete';
597
+ index([], unindexed);
598
+ return 'indexing';
599
+ }
600
+ function connectionOf() {
601
+ if (socketOpen)
602
+ return 'connected';
603
+ if (status.providerUnreachable)
604
+ return 'unreachable';
605
+ return options.adapter.roomStream === undefined && status.lastCompleted !== null
606
+ ? 'connected'
607
+ : 'connecting';
608
+ }
609
+ function refusedIn(locators) {
610
+ return locators.flatMap((room) => {
611
+ const held = sourceRefusals.get(room);
612
+ if (held === undefined)
613
+ return [];
614
+ const { file, location } = held.fields;
615
+ return [
616
+ {
617
+ room,
618
+ file: typeof file === 'string' ? file : '',
619
+ rule: held.code,
620
+ location: typeof location === 'string' ? location : null,
621
+ },
622
+ ];
623
+ });
624
+ }
625
+ function readinessFor(port, reach) {
626
+ const required = requiredRooms(reach);
627
+ const search = port.search === 'required';
628
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
629
+ const building = (state) => coverage
630
+ .filter((room) => room.state === state && !unindexable.has(room.room))
631
+ .map(({ room }) => room);
632
+ const preparing = building('preparing');
633
+ if (preparing.length > 0)
634
+ prepare(preparing);
635
+ const indexing = building('indexing');
636
+ if (indexing.length > 0)
637
+ index(indexing, []);
638
+ return readinessOf({
639
+ code: port.code,
640
+ coverage: {
641
+ listed: required.listed,
642
+ rooms: coverage,
643
+ conversations: conversationCoverage(reach, search),
644
+ search: search ? 'required' : 'unused',
645
+ },
646
+ persistence,
647
+ connection: connectionOf(),
648
+ house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
649
+ capacity: capacity !== null &&
650
+ coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
651
+ ? capacity
652
+ : port.capacity,
653
+ refused: refusedIn(required.rooms),
654
+ lost: storageLoss !== null,
655
+ established: targets[port.target]?.established === true,
656
+ });
657
+ }
658
+ function establish(name) {
659
+ targets = { ...targets, [name]: { established: true } };
660
+ queueReadiness();
661
+ void durably((local) => local.keepTargets(targets)).catch(() => {
662
+ journal.record(`target record failed ${name}`);
663
+ });
664
+ }
665
+ function deliverReadiness(watch) {
666
+ if (!readinessWatches.has(watch))
667
+ return;
668
+ const readiness = readinessFor(watch.port, watch.reach);
669
+ if (readiness.ready && targets[watch.port.target]?.established !== true) {
670
+ establish(watch.port.target);
671
+ }
672
+ const shown = JSON.stringify(readiness);
673
+ if (shown === watch.last)
674
+ return;
675
+ watch.last = shown;
676
+ watch.deliver(structuredClone(readiness));
677
+ }
678
+ function queueReadiness() {
679
+ if (readinessQueued || closed || readinessWatches.size === 0)
680
+ return;
681
+ readinessQueued = true;
682
+ queueMicrotask(() => {
683
+ readinessQueued = false;
684
+ if (closed)
685
+ return;
686
+ for (const watch of [...readinessWatches])
687
+ deliverReadiness(watch);
688
+ });
689
+ }
690
+ function watchReadiness(port, reach, deliver) {
691
+ const watch = { port, reach, deliver, last: null };
692
+ readinessWatches.add(watch);
693
+ void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
694
+ return () => {
695
+ readinessWatches.delete(watch);
696
+ };
697
+ }
698
+ function layerBytes(layer) {
699
+ let bytes = 0;
700
+ for (const [path, text] of Object.entries(layer.documents))
701
+ bytes += (path.length + text.length) * 2;
702
+ return bytes;
703
+ }
704
+ function grownBy(next, previous) {
705
+ if (previous === undefined)
706
+ return layerBytes(next);
707
+ const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
708
+ return layerBytes({ ...next, documents });
709
+ }
710
+ function exceeded(error) {
711
+ return (typeof error === 'object' &&
712
+ error !== null &&
713
+ error.name === 'QuotaExceededError');
714
+ }
715
+ async function estimated() {
716
+ const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
717
+ return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
718
+ }
719
+ function pause(locator, cause) {
720
+ paused.add(locator);
721
+ capacity = cause;
722
+ journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
723
+ queueReadiness();
724
+ if (rechecking !== null)
725
+ return;
726
+ // A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
727
+ rechecking = setInterval(() => {
728
+ void recheck();
729
+ }, 60_000);
730
+ }
731
+ async function recheck() {
732
+ const held = capacity;
733
+ if (held === null || closed)
734
+ return;
735
+ const available = await estimated().catch(() => null);
736
+ if (available === null || available < (held.needed ?? 0))
737
+ return;
738
+ if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
739
+ return;
740
+ resume();
741
+ }
742
+ function resume() {
743
+ if (rechecking !== null)
744
+ clearInterval(rechecking);
745
+ rechecking = null;
746
+ if (paused.size === 0 && capacity === null)
747
+ return;
748
+ paused.clear();
749
+ capacity = null;
750
+ journal.record('capacity resumed');
751
+ queueReadiness();
752
+ if (!closed && authorization !== undefined)
753
+ void sync('unstreamed');
754
+ }
755
+ async function reclaimed(local) {
756
+ const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
757
+ for (const locator of unreferenced) {
758
+ await local.abandon(locator);
759
+ transfers.delete(locator);
760
+ }
761
+ if (unreferenced.length > 0)
762
+ journal.record(`capacity reclaimed ${unreferenced.length}`);
763
+ return unreferenced.length > 0;
764
+ }
765
+ async function admitted(local, locator, needed) {
766
+ if (paused.has(locator))
767
+ return false;
768
+ let available = await estimated().catch(() => null);
769
+ if (available === null || available >= needed)
770
+ return true;
771
+ if (await reclaimed(local))
772
+ available = await estimated().catch(() => null);
773
+ if (available === null || available >= needed)
774
+ return true;
775
+ pause(locator, { cause: 'insufficient-space', needed, available });
776
+ return false;
777
+ }
778
+ async function exceededOn(locator, bytes) {
779
+ const available = await estimated().catch(() => null);
780
+ pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
781
+ }
782
+ function seedRoomHeld(room, version) {
783
+ return prepared.has(room) && reachedBy(room, version);
784
+ }
785
+ function seedHeld(at) {
786
+ return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
787
+ }
788
+ function seedApplies(plan) {
789
+ const seed = seeds.get(plan.key);
790
+ if (seed === undefined)
352
791
  return null;
353
- return [...plan.reach.rooms, ...plan.reach.documents.map(roomOfKey)];
792
+ if (!seedHeld(seed.at))
793
+ return seed;
794
+ seeds.delete(plan.key);
795
+ return null;
354
796
  }
355
- function rowsQuery(plan) {
356
- materialize(reachedRooms(plan));
357
- return readSide.query(plan);
797
+ function seedBehind(plan, cover) {
798
+ const seed = seedApplies(plan);
799
+ if (seed === null || 'pending' in cover)
800
+ return cover;
801
+ return {
802
+ pending: Object.entries(seed.at)
803
+ .filter(([room, version]) => !seedRoomHeld(room, version))
804
+ .map(([room]) => ({ room, awaits: 'acquisition' })),
805
+ };
806
+ }
807
+ function seed(entry) {
808
+ if (seedHeld(entry.at))
809
+ return;
810
+ for (const { query, rows } of entry.queries) {
811
+ const key = queryKey(query);
812
+ seeds.set(key, { rows, at: entry.at });
813
+ for (const shown of seedWatchers.get(key) ?? [])
814
+ shown();
815
+ }
816
+ }
817
+ async function rowsQuery(plan) {
818
+ const before = covered(plan);
819
+ awaitPreparation(before);
820
+ const seeded = seedApplies(plan);
821
+ if (seeded !== null)
822
+ return seededResult(seeded).result;
823
+ if ('pending' in before)
824
+ return before;
825
+ const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
826
+ awaitPreparation(cover);
827
+ if ('pending' in cover)
828
+ return cover;
829
+ return { ...rows, coverage: cover.coverage };
830
+ }
831
+ async function indexConversation(conversation) {
832
+ searchIndex.indexMessages(conversation, [], false);
833
+ let after = null;
834
+ for (;;) {
835
+ const batch = await durably((local) => local.messagesAfter(conversation, after));
836
+ if (closed)
837
+ return;
838
+ searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
839
+ if (batch.length === 0)
840
+ return;
841
+ after = batch[batch.length - 1].identity;
842
+ }
843
+ }
844
+ function indexNext() {
845
+ const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
846
+ if (room !== undefined) {
847
+ roomsToIndex.delete(room);
848
+ return () => prepared.has(room)
849
+ ? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
850
+ : undefined;
851
+ }
852
+ const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
853
+ if (conversation === undefined)
854
+ return null;
855
+ conversationsToIndex.delete(conversation);
856
+ return () => indexConversation(conversation);
857
+ }
858
+ function index(rooms, named) {
859
+ for (const room of rooms)
860
+ roomsToIndex.add(room);
861
+ for (const conversation of named)
862
+ conversationsToIndex.add(conversation);
863
+ if (indexing)
864
+ return;
865
+ indexing = true;
866
+ void (async () => {
867
+ try {
868
+ for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
869
+ await yielded();
870
+ await unit();
871
+ queueReadiness();
872
+ }
873
+ }
874
+ finally {
875
+ indexing = false;
876
+ }
877
+ })().catch(() => { });
358
878
  }
359
879
  function publishChangeSet(set) {
360
880
  applyChangeSet(set);
@@ -371,34 +891,7 @@ export function composeRuntime(options) {
371
891
  emitPresentation(app);
372
892
  conversations.changed(set);
373
893
  }
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
894
  async function refuse(reason, provider = null) {
401
- stopTimer();
402
895
  discoveryPaused = true;
403
896
  try {
404
897
  if (reason === 'revoked-authorization') {
@@ -477,14 +970,31 @@ export function composeRuntime(options) {
477
970
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
478
971
  });
479
972
  }
480
- async function applyDiscovery(listed, local) {
973
+ async function applyDiscovery(found, started, covered) {
974
+ for (const room of found) {
975
+ const fence = fences.get(room.locator);
976
+ if (fence !== undefined && fence.erased && fence.at < started)
977
+ fences.delete(room.locator);
978
+ }
979
+ await loseRooms(found
980
+ .filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
981
+ .map((room) => room.locator));
982
+ const listed = found.filter((room) => !fences.has(room.locator));
481
983
  const listedLocators = new Set(listed.map((room) => room.locator));
482
984
  for (const locator of listedLocators)
483
985
  scopedRefusals.delete(locator);
484
- discovered.clear();
986
+ if (covered === undefined)
987
+ discovered.clear();
988
+ else
989
+ for (const locator of covered)
990
+ discovered.delete(locator);
485
991
  for (const room of listed)
486
992
  discovered.set(room.locator, room);
487
- await dropUnlisted(listedLocators, local);
993
+ const unlisted = unlistedHeld(new Set(found.map((room) => room.locator))).filter((locator) => covered === undefined || covered.includes(locator));
994
+ if (options.adapter.exhaustiveDiscovery === true)
995
+ await loseRooms(unlisted);
996
+ else
997
+ unlistedKept(unlisted);
488
998
  for (const room of listed) {
489
999
  const held = replicas.get(room.locator)?.confirmed.record;
490
1000
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -495,6 +1005,13 @@ export function composeRuntime(options) {
495
1005
  }
496
1006
  roomsListed = true;
497
1007
  emitRooms();
1008
+ return listed;
1009
+ }
1010
+ function heldLocally(locator) {
1011
+ return (replicas.has(locator) ||
1012
+ roomDrafts.has(locator) ||
1013
+ transfers.has(locator) ||
1014
+ queuesWithoutReplica.has(locator));
498
1015
  }
499
1016
  function unlistedHeld(listed) {
500
1017
  return [
@@ -506,41 +1023,73 @@ export function composeRuntime(options) {
506
1023
  ]),
507
1024
  ].filter((locator) => !listed.has(locator));
508
1025
  }
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);
1026
+ function unlistedKept(unlisted) {
1027
+ for (const locator of unlisted) {
1028
+ const held = replicas.get(locator)?.confirmed.record;
1029
+ if (held !== undefined)
1030
+ list(held, false);
1031
+ else
525
1032
  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}`);
1033
+ }
1034
+ }
1035
+ function fence(locators) {
1036
+ const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1037
+ const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
1038
+ room: true,
1039
+ queue: true,
1040
+ drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
1041
+ }));
1042
+ for (const locator of fresh) {
1043
+ fences.set(locator, { at: discoveries, erased: false });
1044
+ lastFenced.set(locator, ++fencings);
1045
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1046
+ pendings.delete(batch.id);
1047
+ replicas.delete(locator);
1048
+ queuesWithoutReplica.delete(locator);
1049
+ rooms.delete(locator);
1050
+ roomDrafts.delete(locator);
1051
+ transfers.delete(locator);
1052
+ catching.delete(locator);
1053
+ discovered.delete(locator);
1054
+ announced.delete(locator);
1055
+ unreadable.delete(locator);
1056
+ stale.delete(locator);
1057
+ repairable.delete(locator);
1058
+ unindexable.delete(locator);
1059
+ paused.delete(locator);
1060
+ scopedRefusals.delete(locator);
1061
+ sourceRefusals.delete(locator);
1062
+ declarationRefusals.delete(locator);
1063
+ for (const attached of ['collection', 'typed-selection']) {
1064
+ const key = modelKey(locator, attached);
1065
+ models.delete(key);
1066
+ liveModels.delete(key);
1067
+ invalidModels.delete(key);
540
1068
  }
541
- publishChangeSet(mergeChangeSets(removed));
542
- moveTo({ ...status, ...attention() });
543
- });
1069
+ journal.record(`room lost ${locator}`);
1070
+ }
1071
+ if (fresh.length === 0)
1072
+ return fresh;
1073
+ publishChangeSet(mergeChangeSets(removed));
1074
+ for (const locator of fresh)
1075
+ prepared.delete(locator);
1076
+ searchIndex.forget(fresh);
1077
+ readSide.recover();
1078
+ moveTo({ ...status, ...attention() });
1079
+ return fresh;
1080
+ }
1081
+ async function erased(local, locators) {
1082
+ await local.erase(locators);
1083
+ for (const locator of locators) {
1084
+ const held = fences.get(locator);
1085
+ if (held !== undefined)
1086
+ held.erased = true;
1087
+ }
1088
+ }
1089
+ async function loseRooms(locators) {
1090
+ const fresh = fence(locators);
1091
+ if (fresh.length > 0)
1092
+ await durably((local) => erased(local, fresh));
544
1093
  }
545
1094
  function restored(key, persisted) {
546
1095
  const prior = models.get(key);
@@ -561,7 +1110,7 @@ export function composeRuntime(options) {
561
1110
  return model;
562
1111
  }
563
1112
  async function hydrate(opened) {
564
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
1113
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
565
1114
  opened.all(),
566
1115
  opened.queues(),
567
1116
  opened.roomDrafts(),
@@ -573,7 +1122,9 @@ export function composeRuntime(options) {
573
1122
  opened.capabilities(),
574
1123
  opened.member(),
575
1124
  opened.transfers(),
1125
+ opened.targets(),
576
1126
  ]);
1127
+ targets = heldTargets;
577
1128
  transfers.clear();
578
1129
  for (const record of staged)
579
1130
  transfers.set(record.locator, record);
@@ -635,6 +1186,7 @@ export function composeRuntime(options) {
635
1186
  async function readState(opened) {
636
1187
  const { lastCompleted, locators, read } = await hydrate(opened);
637
1188
  applyChangeSet(read);
1189
+ prepare();
638
1190
  if (replicas.size > 0)
639
1191
  emitRooms();
640
1192
  if (member !== null)
@@ -645,8 +1197,15 @@ export function composeRuntime(options) {
645
1197
  return lastCompleted;
646
1198
  }
647
1199
  async function open() {
648
- const opened = await openRuntimeStore();
1200
+ if (closed)
1201
+ throw new Error('runtime closed');
1202
+ const opened = await openRuntimeStore(isFenced);
649
1203
  store = opened;
1204
+ const interrupted = await opened.fences();
1205
+ for (const locator of interrupted) {
1206
+ fences.set(locator, { at: discoveries, erased: false });
1207
+ lastFenced.set(locator, ++fencings);
1208
+ }
650
1209
  const kept = options.shell.generation;
651
1210
  const storedGeneration = await opened.generation();
652
1211
  const loss = detectStorageLoss(storedGeneration, kept);
@@ -660,6 +1219,8 @@ export function composeRuntime(options) {
660
1219
  emitStorageLoss();
661
1220
  }
662
1221
  const lastCompleted = await readState(opened);
1222
+ if (interrupted.length > 0)
1223
+ await erased(opened, interrupted);
663
1224
  moveTo({ ...status, lastCompleted, ...attention() });
664
1225
  return opened;
665
1226
  }
@@ -687,8 +1248,9 @@ export function composeRuntime(options) {
687
1248
  }
688
1249
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
689
1250
  const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
690
- const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
691
- const kept = queue.filter((batch) => !withdrawn.includes(batch));
1251
+ const { withdrawn, kept } = standing(confirmed, queue, withdrawable, localSaves);
1252
+ const founded = kept.filter((batch) => !queue.includes(batch));
1253
+ const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
692
1254
  const unqueued = withdrawn.map((batch) => ({
693
1255
  batch,
694
1256
  returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
@@ -712,9 +1274,10 @@ export function composeRuntime(options) {
712
1274
  });
713
1275
  }
714
1276
  }
715
- await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
1277
+ await local.keepAuthored({ locator, queue: kept, founded, roomDrafts: [...written.values()] });
1278
+ const staying = new Set(kept.map((batch) => batch.id));
716
1279
  for (const batch of previousQueue) {
717
- if (!kept.includes(batch))
1280
+ if (!staying.has(batch.id))
718
1281
  pendings.delete(batch.id);
719
1282
  }
720
1283
  for (const record of written.values())
@@ -735,23 +1298,25 @@ export function composeRuntime(options) {
735
1298
  else
736
1299
  queuesWithoutReplica.delete(locator);
737
1300
  publishChangeSet(changeSet(locator, before ?? null, held, {
738
- queue: kept.length !== previousQueue.length,
1301
+ queue: queueChanged,
739
1302
  drafts: [...written.keys()],
740
1303
  }));
741
1304
  moveTo({ ...status, ...attention() });
742
1305
  throw error;
743
1306
  }
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
- });
1307
+ return () => {
1308
+ queuesWithoutReplica.delete(locator);
1309
+ for (const { batch, returned } of unqueued) {
1310
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
1311
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1312
+ }
1313
+ replicas.set(locator, replicaOf(confirmed, kept, before));
1314
+ return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
1315
+ room: settling.room,
1316
+ queue: queueChanged,
1317
+ drafts: [...written.keys()],
1318
+ });
1319
+ };
755
1320
  }
756
1321
  function adoptModels(locator, record) {
757
1322
  for (const [attached, model] of [
@@ -784,11 +1349,16 @@ export function composeRuntime(options) {
784
1349
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
785
1350
  };
786
1351
  }
787
- function publish(local, room, confirmed, landing = {}) {
1352
+ function publish(local, room, confirmed, landing, live) {
788
1353
  const { base, stored } = landing;
789
1354
  return exclusive(async () => {
790
- if (!continuing())
1355
+ if (stored !== undefined) {
1356
+ await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
1357
+ }
1358
+ if (!live()) {
1359
+ caughtUpTo(room.locator);
791
1360
  return true;
1361
+ }
792
1362
  const prior = replicas.get(room.locator);
793
1363
  if (base !== undefined &&
794
1364
  (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
@@ -802,29 +1372,51 @@ export function composeRuntime(options) {
802
1372
  const accepted = pendings.get(batch.id)?.accepted;
803
1373
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
804
1374
  });
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)
1375
+ let settled;
1376
+ try {
1377
+ settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
1378
+ }
1379
+ catch (error) {
1380
+ if (!exceeded(error))
1381
+ throw error;
1382
+ await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
1383
+ caughtUpTo(room.locator);
1384
+ return true;
1385
+ }
1386
+ const set = settled();
1387
+ if (stored !== undefined) {
807
1388
  transfers.delete(room.locator);
1389
+ catching.delete(room.locator);
1390
+ }
808
1391
  for (const batch of accepted)
809
1392
  pendings.delete(batch.id);
810
1393
  stale.delete(room.locator);
1394
+ unindexable.delete(room.locator);
811
1395
  if (unreadable.delete(room.locator))
812
1396
  scopedRefusals.delete(room.locator);
813
1397
  sourceRefusals.delete(room.locator);
814
1398
  declarationRefusals.delete(room.locator);
815
1399
  list(room, true);
1400
+ if (prior === undefined)
1401
+ prepared.add(room.locator);
816
1402
  publishChangeSet(set);
817
1403
  moveTo({ ...status, ...attention() });
1404
+ options.adapter.roomStream?.published(room.locator, roomVersion);
818
1405
  return true;
819
1406
  });
820
1407
  }
821
- async function saveRoomDrafts(locator, named, writtenBy) {
1408
+ async function savedRoom(locator) {
822
1409
  const local = await openLocal();
1410
+ await referenced(locator);
823
1411
  const replica = replicas.get(locator);
824
1412
  const room = rooms.get(locator);
825
1413
  if (replica === undefined || room === undefined) {
826
1414
  throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
827
1415
  }
1416
+ return { local, replica, room };
1417
+ }
1418
+ async function saveRoomDrafts(locator, named, writtenBy) {
1419
+ const target = await savedRoom(locator);
828
1420
  const saved = [];
829
1421
  for (const { path, revision } of named) {
830
1422
  const record = roomDrafts.get(locator)?.get(path);
@@ -834,6 +1426,38 @@ export function composeRuntime(options) {
834
1426
  if (!saved.some((draft) => draft.path === path))
835
1427
  saved.push({ path, parts: record.parts });
836
1428
  }
1429
+ return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
1430
+ }
1431
+ async function saveParts(locator, named, writtenBy) {
1432
+ const read = named.map(({ path, parts }) => ({ path, parts: readParts(parts) }));
1433
+ if (read.some(({ path }, at) => path === '' ||
1434
+ normalize(path) !== path ||
1435
+ read.findIndex((one) => one.path === path) !== at)) {
1436
+ throw runtimeError('room-draft-malformed');
1437
+ }
1438
+ const target = await savedRoom(locator);
1439
+ const saved = [];
1440
+ for (const { path, parts } of read) {
1441
+ const file = parts.file === undefined
1442
+ ? undefined
1443
+ : await heldFile(parts.file.blob, binaryBase(target.replica.visible, path));
1444
+ saved.push({ path, parts: held(undefined, parts, file) });
1445
+ }
1446
+ return committed(locator, target, saved, new Set(), writtenBy);
1447
+ }
1448
+ function carriedDrafts(locator, saved, consumed) {
1449
+ const holding = roomDrafts.get(locator);
1450
+ return new Map(saved.flatMap(({ path, parts }) => {
1451
+ const record = holding?.get(path);
1452
+ if (parts.move === undefined || record === undefined || consumed.has(path))
1453
+ return [];
1454
+ const to = moveTarget(parts.move);
1455
+ if (to === path || holding?.has(to) === true)
1456
+ return [];
1457
+ return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
1458
+ }));
1459
+ }
1460
+ async function committed(locator, { local, replica, room }, saved, consumed, writtenBy) {
837
1461
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
838
1462
  if (changed.length > 0)
839
1463
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
@@ -866,9 +1490,17 @@ export function composeRuntime(options) {
866
1490
  visible: replica.visible,
867
1491
  changes,
868
1492
  bases: parts.flatMap(({ bases }) => bases),
1493
+ queued: queuedDocuments(replica.queue),
869
1494
  writtenBy,
1495
+ clean: {
1496
+ text: replica.confirmed.record.documents,
1497
+ binaries: replica.confirmed.record.binaries,
1498
+ composition: compositionOf(replica.confirmed.record),
1499
+ },
870
1500
  };
871
- const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
1501
+ const checked = withdrawing
1502
+ ? await buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
1503
+ : null;
872
1504
  if (checked?.kind === 'refused')
873
1505
  return { outcome: 'refused', ...checked.refusal };
874
1506
  const owners = [...requeue.owners].flatMap((origin) => {
@@ -884,13 +1516,14 @@ export function composeRuntime(options) {
884
1516
  };
885
1517
  }
886
1518
  const built = !withdrawing
887
- ? buildBatch(input)
1519
+ ? await buildBatch(input)
888
1520
  : requeue.changes.length === 0
889
1521
  ? null
890
- : buildBatch({
1522
+ : await buildBatch({
891
1523
  ...input,
892
1524
  visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
893
1525
  changes: requeue.changes,
1526
+ queued: queuedDocuments(requeue.queue),
894
1527
  });
895
1528
  if (built?.kind === 'refused')
896
1529
  return { outcome: 'refused', ...built.refusal };
@@ -925,18 +1558,23 @@ export function composeRuntime(options) {
925
1558
  if (limited !== null)
926
1559
  return { outcome: 'refused', ...limited };
927
1560
  }
928
- const consumed = saved.map(({ path }) => path);
1561
+ const carried = carriedDrafts(locator, saved, consumed);
1562
+ const left = [...consumed, ...carried.keys()];
1563
+ const arrived = [...carried.values()];
929
1564
  try {
930
1565
  await local.keepAuthored({
931
1566
  locator,
932
1567
  queue: batches,
933
1568
  saved: drafted,
934
- consumedRoomDrafts: consumed,
1569
+ consumedRoomDrafts: left,
1570
+ roomDrafts: arrived,
935
1571
  });
936
1572
  }
937
1573
  catch {
938
1574
  return { outcome: 'refused', code: 'durable-write', retryable: true };
939
1575
  }
1576
+ if (fences.has(locator))
1577
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
940
1578
  for (const origin of withdrawn.keys())
941
1579
  pendings.delete(origin);
942
1580
  for (const [id, drafts] of drafted) {
@@ -944,12 +1582,14 @@ export function composeRuntime(options) {
944
1582
  }
945
1583
  if (!localSaves)
946
1584
  recorded = false;
947
- for (const path of consumed)
1585
+ for (const path of left)
948
1586
  forgetRoomDraft(locator, path);
1587
+ for (const record of arrived)
1588
+ rememberRoomDraft(record);
949
1589
  replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
950
1590
  publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
951
1591
  queue: true,
952
- drafts: consumed,
1592
+ drafts: [...left, ...arrived.map(({ path }) => path)],
953
1593
  }));
954
1594
  moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
955
1595
  submitLater(locator);
@@ -966,6 +1606,8 @@ export function composeRuntime(options) {
966
1606
  return;
967
1607
  }
968
1608
  await local.submit(locator, identifier, submission, lost);
1609
+ if (fences.has(locator))
1610
+ return;
969
1611
  pendings.set(identifier, { ...state, submission, lost });
970
1612
  const replica = replicas.get(locator);
971
1613
  if (replica !== undefined)
@@ -976,6 +1618,8 @@ export function composeRuntime(options) {
976
1618
  if (!continuing())
977
1619
  throw new Error('connection refused');
978
1620
  await local.keepUpload(locator, identifier, upload);
1621
+ if (fences.has(locator))
1622
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
979
1623
  pendings.set(identifier, { ...pendings.get(identifier), upload });
980
1624
  if (!continuing())
981
1625
  throw new Error('connection refused');
@@ -991,6 +1635,7 @@ export function composeRuntime(options) {
991
1635
  }
992
1636
  if (closed || authorization === undefined || refusal !== null)
993
1637
  return UNCERTAIN;
1638
+ const since = fencings;
994
1639
  const transfer = ['transfer', 'history', locator, path];
995
1640
  if (query.revision !== undefined)
996
1641
  transfer.push(query.revision);
@@ -1005,6 +1650,8 @@ export function composeRuntime(options) {
1005
1650
  ...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
1006
1651
  });
1007
1652
  journal.record(`outcome history ${result.kind}`);
1653
+ if (fencedSince(locator, since))
1654
+ return failure({ kind: 'room-not-readable', locator });
1008
1655
  if (result.kind === 'scoped-refusal') {
1009
1656
  const room = replicas.get(result.locator)?.confirmed.record;
1010
1657
  if (room !== undefined)
@@ -1027,7 +1674,13 @@ export function composeRuntime(options) {
1027
1674
  }
1028
1675
  return null;
1029
1676
  }
1030
- async function readBinary(request) {
1677
+ function binaryRoom(request) {
1678
+ if (request.kind === 'path')
1679
+ return request.locator;
1680
+ const scope = options.adapter.referrerScope(request.referrer);
1681
+ return scope?.kind === 'room' ? scope.locator : null;
1682
+ }
1683
+ async function transferBinary(request, transfer, answer) {
1031
1684
  if (closed || refusal !== null)
1032
1685
  return UNCERTAIN;
1033
1686
  try {
@@ -1038,41 +1691,46 @@ export function composeRuntime(options) {
1038
1691
  }
1039
1692
  if (closed || authorization === undefined || refusal !== null)
1040
1693
  return UNCERTAIN;
1694
+ const since = fencings;
1041
1695
  journal.record(request.kind === 'path'
1042
1696
  ? `transfer binary ${request.locator} ${request.path}`
1043
1697
  : `transfer binary ${request.fileRef}`);
1044
1698
  try {
1045
- const result = await options.adapter.binary(authorization, request);
1046
- journal.record(`outcome binary ${result.kind}`);
1047
- if (result.kind === 'scoped-refusal') {
1048
- const room = replicas.get(result.locator)?.confirmed.record;
1699
+ const result = await transfer(authorization);
1700
+ const settled = result;
1701
+ journal.record(`outcome binary ${settled.kind}`);
1702
+ const locator = binaryRoom(request);
1703
+ if (locator !== null && fencedSince(locator, since))
1704
+ return failure({ kind: 'room-not-readable', locator });
1705
+ if (settled.kind === 'scoped-refusal') {
1706
+ const room = replicas.get(settled.locator)?.confirmed.record;
1049
1707
  if (room !== undefined)
1050
- await scoped(result, room, await openLocal());
1708
+ await scoped(settled, room, await openLocal());
1051
1709
  }
1052
- if (result.kind === 'revoked-authorization')
1710
+ if (settled.kind === 'revoked-authorization')
1053
1711
  await refuse('revoked-authorization');
1054
- return binaryAnswer(result);
1712
+ return answer(result);
1055
1713
  }
1056
1714
  catch {
1057
1715
  journal.record('outcome binary transport-uncertainty');
1058
1716
  return UNCERTAIN;
1059
1717
  }
1060
1718
  }
1061
- async function readBinaryPath(locator, path, revision) {
1062
- await ready;
1719
+ function readBinary(request) {
1720
+ return transferBinary(request, (held) => options.adapter.binary(held, request), binaryAnswer);
1721
+ }
1722
+ function binaryPath(locator, path, revision) {
1063
1723
  if (revision !== undefined)
1064
- return readBinary({ kind: 'path', locator, path, revision });
1724
+ return { kind: 'path', locator, path, revision };
1065
1725
  const visible = replicas.get(locator)?.visible;
1066
1726
  const held = visible?.binaries[path];
1067
1727
  if (visible === undefined || held === undefined)
1068
- return absentBinary(path);
1069
- if (held.state === 'pending') {
1070
- const blob = queuedBlob(locator, path, held.localId);
1071
- return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1072
- }
1728
+ return null;
1729
+ if (held.state === 'pending')
1730
+ return { kind: 'pending', localId: held.localId };
1073
1731
  const record = binaryRecord(path, held, visible.revisions[path]);
1074
1732
  if (record.state !== 'accepted')
1075
- return absentBinary(path);
1733
+ return null;
1076
1734
  const replica = replicas.get(locator);
1077
1735
  const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
1078
1736
  const holds = (at) => {
@@ -1087,13 +1745,31 @@ export function composeRuntime(options) {
1087
1745
  if (change.kind === 'move' && change.to === confirmed)
1088
1746
  confirmed = change.path;
1089
1747
  }
1090
- return readBinary({
1748
+ return {
1091
1749
  kind: 'path',
1092
1750
  locator,
1093
1751
  path: confirmed,
1094
1752
  revision: record.revision,
1095
1753
  fileRef: record.fileRef,
1096
- });
1754
+ };
1755
+ }
1756
+ async function readBinaryPath(locator, path, revision) {
1757
+ await ready;
1758
+ const asked = binaryPath(locator, path, revision);
1759
+ if (asked === null)
1760
+ return absentBinary(path);
1761
+ if (asked.kind === 'pending') {
1762
+ const blob = queuedBlob(locator, path, asked.localId);
1763
+ return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1764
+ }
1765
+ return readBinary(asked);
1766
+ }
1767
+ async function linkBinaryPath(locator, path, revision) {
1768
+ await ready;
1769
+ const asked = binaryPath(locator, path, revision);
1770
+ if (asked === null || asked.kind === 'pending')
1771
+ return absentBinary(path);
1772
+ return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
1097
1773
  }
1098
1774
  async function readBinaryReference(reference) {
1099
1775
  await ready;
@@ -1119,7 +1795,7 @@ export function composeRuntime(options) {
1119
1795
  }
1120
1796
  return undefined;
1121
1797
  }
1122
- async function invokeAction(name, input) {
1798
+ async function invokeAction(name, input, dispatching) {
1123
1799
  await ready;
1124
1800
  if (closed || refusal !== null)
1125
1801
  return UNCERTAIN;
@@ -1133,6 +1809,7 @@ export function composeRuntime(options) {
1133
1809
  }
1134
1810
  if (closed || authorization === undefined || refusal !== null)
1135
1811
  return UNCERTAIN;
1812
+ await dispatching?.();
1136
1813
  journal.record(`transfer action ${name}`);
1137
1814
  try {
1138
1815
  const result = await options.adapter.action(authorization, { name, input });
@@ -1153,6 +1830,60 @@ export function composeRuntime(options) {
1153
1830
  return UNCERTAIN;
1154
1831
  }
1155
1832
  }
1833
+ function unsettled(result) {
1834
+ return result.kind !== 'action' && result.retryable;
1835
+ }
1836
+ async function recoveredAction(name, input) {
1837
+ const operation = operationOf(input);
1838
+ if (operation === null)
1839
+ return invokeAction(name, input);
1840
+ const kept = async () => (await openLocal()).keepCall({ operation, name, input });
1841
+ calling.add(operation);
1842
+ try {
1843
+ let result = await invokeAction(name, input, kept);
1844
+ if (unsettled(result)) {
1845
+ await recovery.attempt(`action ${operation}`, async () => {
1846
+ if (closed || refusal !== null)
1847
+ return 'done';
1848
+ if (!socketOpen)
1849
+ return 'again';
1850
+ result = await invokeAction(name, input, kept);
1851
+ return unsettled(result) ? 'again' : 'done';
1852
+ }, { again: true });
1853
+ }
1854
+ if (!unsettled(result))
1855
+ await (await openLocal()).dropCall(operation);
1856
+ return result;
1857
+ }
1858
+ finally {
1859
+ calling.delete(operation);
1860
+ }
1861
+ }
1862
+ async function resendCalls(signal) {
1863
+ if (!socketOpen || closed || stopping)
1864
+ return 'done';
1865
+ const local = await openLocal();
1866
+ for (const held of await local.calls()) {
1867
+ if (calling.has(held.operation))
1868
+ continue;
1869
+ if (closed || signal.aborted)
1870
+ return 'done';
1871
+ if (pastHorizon(held.operation)) {
1872
+ await local.dropCall(held.operation);
1873
+ continue;
1874
+ }
1875
+ calling.add(held.operation);
1876
+ try {
1877
+ if (unsettled(await invokeAction(held.name, held.input)))
1878
+ return 'again';
1879
+ await local.dropCall(held.operation);
1880
+ }
1881
+ finally {
1882
+ calling.delete(held.operation);
1883
+ }
1884
+ }
1885
+ return 'done';
1886
+ }
1156
1887
  function continuing() {
1157
1888
  return !closed && !discoveryPaused;
1158
1889
  }
@@ -1167,11 +1898,12 @@ export function composeRuntime(options) {
1167
1898
  options.adapter.reached(locator, written, roomVersion) &&
1168
1899
  !options.adapter.reached(locator, roomVersion, written));
1169
1900
  }
1170
- async function pullRefresh(authorized, confirmed) {
1171
- if (!continuing())
1901
+ async function pullRefresh(authorized, confirmed, live) {
1902
+ if (!live())
1172
1903
  return unconfirmed;
1904
+ const turn = generation;
1173
1905
  try {
1174
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
1906
+ const result = await refreshRoom(reads, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1175
1907
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
1176
1908
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
1177
1909
  return result;
@@ -1180,48 +1912,101 @@ export function composeRuntime(options) {
1180
1912
  journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
1181
1913
  throw error;
1182
1914
  }
1915
+ finally {
1916
+ reads.finished(confirmed.record.locator, turn);
1917
+ }
1183
1918
  }
1184
- function staging(local) {
1919
+ function staging(local, live) {
1185
1920
  return {
1186
1921
  held: (locator) => transfers.get(locator) ?? null,
1187
1922
  staged: (record) => local.staged(record),
1188
1923
  stage: (record, page) => exclusive(async () => {
1189
- if (!continuing())
1924
+ if (!live())
1925
+ return false;
1926
+ const bytes = layerBytes(page);
1927
+ if (!(await admitted(local, record.locator, bytes)))
1928
+ return false;
1929
+ try {
1930
+ await local.stage(record, page);
1931
+ }
1932
+ catch (error) {
1933
+ if (!exceeded(error))
1934
+ throw error;
1935
+ await exceededOn(record.locator, bytes);
1190
1936
  return false;
1191
- await local.stage(record, page);
1937
+ }
1192
1938
  transfers.set(record.locator, record);
1939
+ readSide.recover();
1193
1940
  return true;
1194
1941
  }),
1195
1942
  abandon: (locator) => exclusive(async () => {
1196
- if (!continuing())
1943
+ if (!live())
1197
1944
  return;
1198
1945
  await local.abandon(locator);
1199
1946
  transfers.delete(locator);
1947
+ readSide.recover();
1200
1948
  }),
1201
1949
  };
1202
1950
  }
1203
- async function pullBootstrap(authorized, room, local) {
1204
- if (!continuing())
1951
+ function reusable(room) {
1952
+ const record = replicas.get(room.locator)?.confirmed.record;
1953
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1954
+ return null;
1955
+ return { documents: record.documents, revisions: record.revisions };
1956
+ }
1957
+ async function pullBootstrap(authorized, room, local, live) {
1958
+ if (!live())
1205
1959
  return unconfirmed;
1960
+ const turn = generation;
1961
+ if (replicas.has(room.locator)) {
1962
+ catching.add(room.locator);
1963
+ readSide.recover();
1964
+ }
1206
1965
  try {
1207
- const result = await bootstrapRoom(options.adapter, authorized, room, {
1966
+ const result = await bootstrapRoom(reads, authorized, room, {
1208
1967
  collection: modelOf(room.locator),
1209
1968
  selection: modelOf(room.locator, 'typed-selection'),
1210
- }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
1969
+ }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
1211
1970
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
1212
1971
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1972
+ if (result.kind !== 'staged')
1973
+ caughtUpTo(room.locator);
1213
1974
  return result;
1214
1975
  }
1215
1976
  catch (error) {
1216
1977
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
1978
+ caughtUpTo(room.locator);
1217
1979
  throw error;
1218
1980
  }
1981
+ finally {
1982
+ reads.finished(room.locator, turn);
1983
+ }
1219
1984
  }
1220
- async function advance(room, local, authorized) {
1985
+ function caughtUpTo(locator) {
1986
+ if (catching.delete(locator))
1987
+ readSide.recover();
1988
+ }
1989
+ function behind(room) {
1990
+ const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1991
+ return (held !== undefined &&
1992
+ [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
1993
+ }
1994
+ async function advance(room, local, authorized, live) {
1995
+ const carried = reads.carried(room.locator);
1996
+ const withheld = await advanceOnce(room, local, authorized, live);
1997
+ if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
1998
+ return withheld;
1999
+ }
2000
+ return advanceOnce(room, local, authorized, live);
2001
+ }
2002
+ async function advanceOnce(room, local, authorized, live) {
2003
+ await referenced(room.locator);
2004
+ if (!live() || paused.has(room.locator))
2005
+ return unconfirmed;
1221
2006
  const held = replicas.get(room.locator);
1222
2007
  if (held !== undefined) {
1223
- const refreshed = await pullRefresh(authorized, held.confirmed);
1224
- if (!continuing())
2008
+ const refreshed = await pullRefresh(authorized, held.confirmed, live);
2009
+ if (!live())
1225
2010
  return unconfirmed;
1226
2011
  if (refreshed.kind === 'still-current') {
1227
2012
  stale.delete(room.locator);
@@ -1235,31 +2020,47 @@ export function composeRuntime(options) {
1235
2020
  return null;
1236
2021
  }
1237
2022
  if (refreshed.kind === 'advanced') {
1238
- if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
2023
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
1239
2024
  const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
1240
2025
  const reached = now !== undefined &&
1241
2026
  options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
1242
- return reached ? null : advance(room, local, authorized);
2027
+ return reached ? null : advance(room, local, authorized, live);
1243
2028
  }
1244
2029
  await retainModel(local, room, 'collection', modelOf(room.locator));
1245
2030
  return null;
1246
2031
  }
1247
2032
  if (refreshed.kind !== 'beyond-horizon')
1248
2033
  return refreshed;
2034
+ if (opened !== null) {
2035
+ owed.add(room.locator);
2036
+ readSide.recover();
2037
+ return unconfirmed;
2038
+ }
2039
+ if (reloadsDue.get(room.locator) === generation)
2040
+ return unconfirmed;
1249
2041
  }
1250
- const bootstrapped = await pullBootstrap(authorized, room, local);
1251
- if (!continuing())
2042
+ const bootstrapped = await pullBootstrap(authorized, room, local, live);
2043
+ if (!live())
1252
2044
  return unconfirmed;
1253
2045
  if (bootstrapped.kind !== 'staged')
1254
2046
  return bootstrapped;
1255
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
2047
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
1256
2048
  return null;
1257
2049
  }
1258
2050
  async function withhold(withheld, room, local, progress) {
2051
+ if (withheld.kind === 'unconfirmed')
2052
+ return;
1259
2053
  if (withheld.kind === 'refused')
1260
2054
  stale.delete(room.locator);
1261
2055
  else
1262
2056
  stale.add(room.locator);
2057
+ if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
2058
+ const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
2059
+ repairable.set(room.locator, reset);
2060
+ repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
2061
+ }
2062
+ else
2063
+ repairable.delete(room.locator);
1263
2064
  if (withheld.kind === 'scoped-refusal') {
1264
2065
  await scoped(withheld, room, local);
1265
2066
  }
@@ -1295,6 +2096,7 @@ export function composeRuntime(options) {
1295
2096
  unreadable.add(room.locator);
1296
2097
  list(room, false);
1297
2098
  emitRooms();
2099
+ moveTo({ ...status, ...attention() });
1298
2100
  journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
1299
2101
  }
1300
2102
  function leaving(batch, result) {
@@ -1316,6 +2118,7 @@ export function composeRuntime(options) {
1316
2118
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1317
2119
  }
1318
2120
  async function adoptAccepted(room, local, submitted, accepted) {
2121
+ await referenced(room.locator);
1319
2122
  if (!continuing())
1320
2123
  return unconfirmed;
1321
2124
  const current = replicas.get(room.locator);
@@ -1326,22 +2129,23 @@ export function composeRuntime(options) {
1326
2129
  logAccepted(room.locator, submitted, accepted);
1327
2130
  const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1328
2131
  ? { kind: 'staged', confirmed: current.confirmed }
1329
- : applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
2132
+ : await applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
1330
2133
  ? accepted.roomVersion
1331
2134
  : held, accepted.roomVersion);
1332
2135
  if (applied.kind !== 'staged') {
1333
2136
  const state = pendings.get(submitted.id);
1334
2137
  await local.accept(room.locator, submitted.id, accepted.roomVersion);
1335
- if (state !== undefined)
2138
+ if (state !== undefined && !fences.has(room.locator))
1336
2139
  pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1337
2140
  emitQueue(room.locator);
1338
2141
  return applied;
1339
2142
  }
1340
- const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
2143
+ const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1341
2144
  pendings.delete(submitted.id);
1342
2145
  unreadable.delete(room.locator);
1343
2146
  sourceRefusals.delete(room.locator);
1344
2147
  declarationRefusals.delete(room.locator);
2148
+ const set = settled();
1345
2149
  list(room, true);
1346
2150
  publishChangeSet(set);
1347
2151
  moveTo({ ...status, ...attention() });
@@ -1355,14 +2159,14 @@ export function composeRuntime(options) {
1355
2159
  refusedSource(room, failed);
1356
2160
  return false;
1357
2161
  }
1358
- async function submitRoom(room, local, authorized, progress) {
1359
- while (continuing() && authorization !== undefined) {
2162
+ async function submitRoom(room, local, authorized, progress, signal) {
2163
+ while (continuing() && authorization !== undefined && !signal.aborted) {
1360
2164
  const replica = replicas.get(room.locator);
1361
2165
  const head = replica?.queue[0];
1362
2166
  if (replica === undefined || head === undefined)
1363
- return;
2167
+ return 'done';
1364
2168
  if (pendings.get(head.id)?.accepted !== undefined)
1365
- return;
2169
+ return 'done';
1366
2170
  const state = pendings.get(head.id);
1367
2171
  const { submission, lost: earlier } = state ?? {
1368
2172
  submission: { state: 'waiting' },
@@ -1370,24 +2174,30 @@ export function composeRuntime(options) {
1370
2174
  };
1371
2175
  if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
1372
2176
  progress.complete = false;
1373
- return;
2177
+ return { at: Date.parse(submission.reset) };
1374
2178
  }
1375
2179
  if (writesHeld()) {
1376
2180
  progress.complete = false;
1377
2181
  journal.record(`submission ${room.locator} ${head.id} held`);
1378
- return;
2182
+ return 'done';
1379
2183
  }
1380
2184
  const lost = earlier || submission.state === 'submitted';
2185
+ if (lost && pastHorizon(head.id)) {
2186
+ await discardPending(room, local, signal);
2187
+ return 'done';
2188
+ }
1381
2189
  let result;
1382
2190
  journal.record(`submission ${room.locator} ${head.id}`);
1383
2191
  await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1384
- if (discoveryPaused)
1385
- return;
1386
- if (writesHeld()) {
2192
+ if (discoveryPaused || fences.has(room.locator))
2193
+ return 'done';
2194
+ if (signal.aborted || writesHeld()) {
1387
2195
  await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
2196
+ if (signal.aborted)
2197
+ return 'done';
1388
2198
  progress.complete = false;
1389
2199
  journal.record(`submission ${room.locator} ${head.id} held`);
1390
- return;
2200
+ return 'done';
1391
2201
  }
1392
2202
  const opensBefore = openSignals;
1393
2203
  const answered = awaitingAnswer(room.locator);
@@ -1403,32 +2213,36 @@ export function composeRuntime(options) {
1403
2213
  progress.complete = false;
1404
2214
  if (continuing())
1405
2215
  progress.reachable = false;
1406
- return;
2216
+ return 'again';
2217
+ }
2218
+ if (signal.aborted) {
2219
+ answered();
2220
+ return 'done';
1407
2221
  }
1408
2222
  if (result.kind !== 'accepted' || !continuing())
1409
2223
  answered();
1410
2224
  if (!continuing())
1411
- return;
2225
+ return 'done';
1412
2226
  if (result.kind === 'transport-uncertainty') {
1413
2227
  progress.complete = false;
1414
2228
  progress.reachable = false;
1415
- return;
2229
+ return 'again';
1416
2230
  }
1417
2231
  if (result.kind === 'room-not-readable') {
1418
2232
  progress.complete = false;
1419
2233
  unreadable.add(room.locator);
1420
2234
  list(room, false);
1421
2235
  emitRooms();
1422
- return;
2236
+ return 'done';
1423
2237
  }
1424
2238
  if (result.kind === 'revoked-authorization') {
1425
2239
  await refuse('revoked-authorization');
1426
- return;
2240
+ return 'done';
1427
2241
  }
1428
2242
  if (result.kind === 'scoped-refusal') {
1429
2243
  await scoped(result, room, local);
1430
2244
  progress.complete = false;
1431
- return;
2245
+ return 'done';
1432
2246
  }
1433
2247
  if (result.kind === 'write-halted') {
1434
2248
  journal.record(`outcome ${room.locator} ${head.id} write-halted`);
@@ -1437,16 +2251,24 @@ export function composeRuntime(options) {
1437
2251
  continue;
1438
2252
  haltedRefusal = true;
1439
2253
  progress.complete = false;
1440
- return;
2254
+ return 'done';
1441
2255
  }
1442
2256
  if (result.kind === 'conflicted' ||
1443
2257
  result.kind === 'rejected' ||
1444
2258
  result.kind === 'expired' ||
1445
2259
  result.kind === 'retryable-refusal') {
2260
+ if (result.kind === 'expired' && lost && result.restored !== true) {
2261
+ journal.record(`outcome ${room.locator} ${head.id} expired`);
2262
+ await discardPending(room, local, signal);
2263
+ return 'done';
2264
+ }
1446
2265
  if (result.kind === 'expired') {
1447
- const withheld = await advance(room, local, authorized);
1448
- if (withheld !== null)
1449
- return withhold(withheld, room, local, progress);
2266
+ await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
2267
+ const withheld = await advance(room, local, authorized, continuing);
2268
+ if (withheld !== null) {
2269
+ await withhold(withheld, room, local, progress);
2270
+ return 'done';
2271
+ }
1450
2272
  }
1451
2273
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
1452
2274
  (result.kind === 'conflicted' && result.roomVersion !== undefined
@@ -1459,8 +2281,7 @@ export function composeRuntime(options) {
1459
2281
  const reset = result.reset;
1460
2282
  progress.complete = false;
1461
2283
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1462
- armTimer();
1463
- return;
2284
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
1464
2285
  }
1465
2286
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
1466
2287
  const left = leaving(head, result);
@@ -1471,37 +2292,82 @@ export function composeRuntime(options) {
1471
2292
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
1472
2293
  return;
1473
2294
  }
1474
- const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
2295
+ const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1475
2296
  leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1476
2297
  (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1477
2298
  ? { ...left, returned: null }
1478
2299
  : left,
1479
2300
  });
1480
- publishChangeSet(set);
2301
+ publishChangeSet(settled());
1481
2302
  moveTo({ ...status, ...attention() });
1482
2303
  });
1483
2304
  if (result.kind === 'expired')
1484
2305
  continue;
1485
- const withheld = await advance(room, local, authorized);
1486
- if (withheld !== null)
1487
- return withhold(withheld, room, local, progress);
2306
+ const withheld = await advance(room, local, authorized, continuing);
2307
+ if (withheld !== null) {
2308
+ await withhold(withheld, room, local, progress);
2309
+ return 'done';
2310
+ }
1488
2311
  continue;
1489
2312
  }
1490
2313
  const kept = await adopted(room, local, head, result).finally(answered);
1491
2314
  if (!kept) {
1492
2315
  progress.complete = false;
1493
- return;
2316
+ return 'done';
1494
2317
  }
1495
2318
  }
2319
+ return 'done';
2320
+ }
2321
+ function pastHorizon(identity) {
2322
+ return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
2323
+ }
2324
+ async function discardPending(room, local, signal) {
2325
+ const { locator } = room;
2326
+ const discarded = await exclusive(async () => {
2327
+ if (fences.has(locator))
2328
+ return false;
2329
+ const before = replicas.get(locator);
2330
+ const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
2331
+ fences.set(locator, { at: discoveries, erased: false });
2332
+ lastFenced.set(locator, ++fencings);
2333
+ try {
2334
+ await local.discard(locator);
2335
+ }
2336
+ finally {
2337
+ fences.delete(locator);
2338
+ }
2339
+ for (const batch of queue)
2340
+ pendings.delete(batch.id);
2341
+ queuesWithoutReplica.delete(locator);
2342
+ const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
2343
+ if (after !== null)
2344
+ replicas.set(locator, after);
2345
+ journal.record(`room discarded ${locator}`);
2346
+ publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
2347
+ moveTo({ ...status, ...attention() });
2348
+ return true;
2349
+ });
2350
+ if (!discarded)
2351
+ return;
2352
+ const progress = { complete: true, reachable: true };
2353
+ await roomTask(locator, async (live) => {
2354
+ await Promise.race([
2355
+ reload(room, local, progress, () => live() && !signal.aborted),
2356
+ abandonedOn(signal),
2357
+ ]);
2358
+ });
2359
+ if (signal.aborted)
2360
+ return;
2361
+ await rest(progress);
1496
2362
  }
1497
2363
  function awaitingAnswer(locator) {
1498
2364
  let release = () => { };
1499
2365
  const awaited = new Promise((resolve) => {
1500
2366
  release = resolve;
1501
2367
  });
1502
- answering.set(locator, awaited);
2368
+ answering.set(locator, { generation, answered: awaited });
1503
2369
  return () => {
1504
- if (answering.get(locator) === awaited)
2370
+ if (answering.get(locator)?.answered === awaited)
1505
2371
  answering.delete(locator);
1506
2372
  release();
1507
2373
  };
@@ -1535,7 +2401,7 @@ export function composeRuntime(options) {
1535
2401
  };
1536
2402
  const reclassified = sameMembership(declared, held.confirmed.record)
1537
2403
  ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
1538
- : readReclassified(declared, held.confirmed);
2404
+ : await readReclassified(declared, held.confirmed);
1539
2405
  const record = reclassified.kind === 'staged'
1540
2406
  ? declared
1541
2407
  : {
@@ -1572,6 +2438,11 @@ export function composeRuntime(options) {
1572
2438
  });
1573
2439
  }
1574
2440
  async function scoped(result, room, local) {
2441
+ if ((result.scope === 'content' || result.scope === 'sync') && !result.retryable) {
2442
+ journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
2443
+ await loseRooms([room.locator]);
2444
+ return;
2445
+ }
1575
2446
  scopedRefusals.set(room.locator, {
1576
2447
  code: result.code,
1577
2448
  fields: result.fields,
@@ -1617,8 +2488,10 @@ export function composeRuntime(options) {
1617
2488
  ];
1618
2489
  });
1619
2490
  }
1620
- async function synchronize(listed, local, progress, attempt) {
2491
+ async function synchronize(listed, local, progress, attempt, live) {
1621
2492
  const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
2493
+ if (!live())
2494
+ return;
1622
2495
  const ready = listed.filter((_, index) => {
1623
2496
  const outcome = prepared[index];
1624
2497
  return outcome?.status === 'fulfilled' && outcome.value;
@@ -1627,9 +2500,10 @@ export function composeRuntime(options) {
1627
2500
  options.adapter.roomStream?.hold(heldRooms(ready));
1628
2501
  handed = true;
1629
2502
  }
2503
+ discovering?.release();
1630
2504
  const loaded = await Promise.allSettled(ready
1631
2505
  .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1632
- .map((room) => loadRoom(room, local, progress)));
2506
+ .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
1633
2507
  const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1634
2508
  if (failed !== undefined)
1635
2509
  throw failed.reason;
@@ -1651,21 +2525,12 @@ export function composeRuntime(options) {
1651
2525
  await retainModel(local, room, 'collection', modelOf(room.locator));
1652
2526
  if (discoveryPaused || authorization === undefined)
1653
2527
  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;
2528
+ return room.replica.admission === 'admitted';
1660
2529
  }
1661
- async function caughtUp(room, local, progress) {
1662
- const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1663
- if (held === undefined || authorization === undefined)
1664
- return;
1665
- const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1666
- if (!behind)
2530
+ async function caughtUp(room, local, progress, live) {
2531
+ if (authorization === undefined || !live() || !behind(room))
1667
2532
  return;
1668
- const withheld = await advance(room, local, authorization);
2533
+ const withheld = await advance(room, local, authorization, live);
1669
2534
  if (withheld !== null)
1670
2535
  await withhold(withheld, room, local, progress);
1671
2536
  }
@@ -1675,47 +2540,57 @@ export function composeRuntime(options) {
1675
2540
  return model.status === 'unavailable' && model.reason === 'revoked';
1676
2541
  });
1677
2542
  }
1678
- async function loadRoom(room, local, progress) {
1679
- if (discoveryPaused || authorization === undefined)
2543
+ async function loadRoom(room, local, progress, live) {
2544
+ if (!live() || authorization === undefined)
1680
2545
  return;
1681
2546
  if (replicas.has(room.locator) && revoked(room.locator)) {
1682
- await reload(room, local, progress);
2547
+ await reload(room, local, progress, live);
1683
2548
  return;
1684
2549
  }
1685
- const withheld = await advance(room, local, authorization);
2550
+ const withheld = await advance(room, local, authorization, live);
1686
2551
  if (withheld !== null) {
1687
2552
  await withhold(withheld, room, local, progress);
1688
2553
  return;
1689
2554
  }
1690
- await caughtUp(room, local, progress);
2555
+ await caughtUp(room, local, progress, live);
1691
2556
  }
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;
2557
+ async function reload(room, local, progress, live) {
2558
+ try {
2559
+ if (live())
2560
+ reloadsDue.delete(room.locator);
2561
+ if (!live() || authorization === undefined)
2562
+ return;
2563
+ const bootstrapped = await pullBootstrap(authorization, room, local, live);
2564
+ if (!live())
2565
+ return;
2566
+ if (bootstrapped.kind !== 'staged') {
2567
+ await withhold(bootstrapped, room, local, progress);
2568
+ return;
2569
+ }
2570
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
2571
+ await caughtUp(room, local, progress, live);
2572
+ }
2573
+ finally {
2574
+ caughtUpTo(room.locator);
1701
2575
  }
1702
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1703
- await caughtUp(room, local, progress);
1704
2576
  }
1705
- async function discoverRooms(local, progress, attempt) {
2577
+ async function discoverRooms(local, progress, attempt, live, scope) {
1706
2578
  if (closed || authorization === undefined) {
1707
2579
  notifyReady();
1708
2580
  return;
1709
2581
  }
1710
2582
  moveTo(transferring(status));
2583
+ discoveries += 1;
2584
+ const started = discoveries;
1711
2585
  let result;
1712
2586
  try {
1713
- result = await options.adapter.discover(authorization);
2587
+ result = await options.adapter.discover(authorization, scope);
1714
2588
  }
1715
2589
  catch {
1716
2590
  result = { kind: 'transport-uncertainty' };
1717
2591
  }
1718
- if (closed) {
2592
+ if (!live()) {
2593
+ progress.complete = false;
1719
2594
  notifyReady();
1720
2595
  return;
1721
2596
  }
@@ -1773,6 +2648,7 @@ export function composeRuntime(options) {
1773
2648
  }
1774
2649
  const previousCapabilities = capabilities;
1775
2650
  capabilities = result.capabilities;
2651
+ outcomeRetention = result.retention.outcome;
1776
2652
  notifyReady();
1777
2653
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1778
2654
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
@@ -1785,8 +2661,7 @@ export function composeRuntime(options) {
1785
2661
  member = signedIn;
1786
2662
  emitMember();
1787
2663
  }
1788
- await applyDiscovery(result.rooms, local);
1789
- await synchronize(result.rooms, local, progress, attempt);
2664
+ await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
1790
2665
  }
1791
2666
  async function ensureAuthorization() {
1792
2667
  if (authorization !== undefined)
@@ -1800,20 +2675,50 @@ export function composeRuntime(options) {
1800
2675
  throw error;
1801
2676
  }
1802
2677
  }
1803
- function task(work) {
2678
+ async function admittedRead(work) {
2679
+ if (roomReads.generation !== generation) {
2680
+ roomReads = { generation, free: ROOM_READS, waiting: [] };
2681
+ }
2682
+ const reads = roomReads;
2683
+ while (reads.free === 0)
2684
+ await new Promise((admit) => reads.waiting.push(admit));
2685
+ reads.free -= 1;
2686
+ try {
2687
+ await work();
2688
+ }
2689
+ finally {
2690
+ reads.free += 1;
2691
+ reads.waiting.shift()?.();
2692
+ }
2693
+ }
2694
+ function roomTask(locator, work) {
2695
+ return task(roomLane(locator), (live) => admittedRead(() => work(live)));
2696
+ }
2697
+ function task(lane, work) {
1804
2698
  if (stopping)
1805
- return chain;
1806
- chain = chain.then(work, work).catch(() => undefined);
1807
- return chain;
2699
+ return Promise.resolve();
2700
+ const held = lanes.get(lane);
2701
+ const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
2702
+ const turn = current.generation;
2703
+ const live = () => turn === generation && continuing();
2704
+ const run = current.chain.then(() => work(live)).catch(() => undefined);
2705
+ current.chain = run;
2706
+ lanes.set(lane, current);
2707
+ working.add(run);
2708
+ void run.finally(() => working.delete(run));
2709
+ return run;
1808
2710
  }
1809
2711
  function sync(attempt) {
1810
- return task(async () => {
1811
- if (closed || stopping || discoveryPaused) {
2712
+ return task(OPENING_LANE, async (live) => {
2713
+ if (!live()) {
1812
2714
  notifyReady();
1813
2715
  return;
1814
2716
  }
1815
2717
  const progress = { complete: true, reachable: true };
1816
- underway = attempt === 'opened' ? progress : null;
2718
+ const gate = discover();
2719
+ if (attempt === 'opened')
2720
+ opened = generation;
2721
+ underway = attempt === 'opened' ? { generation, progress } : null;
1817
2722
  if (attempt === 'opened')
1818
2723
  streamed = progress;
1819
2724
  complete = false;
@@ -1822,12 +2727,12 @@ export function composeRuntime(options) {
1822
2727
  handed = false;
1823
2728
  try {
1824
2729
  const local = await openLocal();
1825
- if (closed)
2730
+ if (!live())
1826
2731
  return;
1827
2732
  await ensureAuthorization();
1828
- if (closed)
2733
+ if (!live())
1829
2734
  return;
1830
- await discoverRooms(local, progress, attempt);
2735
+ await discoverRooms(local, progress, attempt, live);
1831
2736
  }
1832
2737
  catch {
1833
2738
  progress.complete = false;
@@ -1835,23 +2740,61 @@ export function composeRuntime(options) {
1835
2740
  progress.reachable = false;
1836
2741
  }
1837
2742
  finally {
2743
+ gate.release();
1838
2744
  notifyReady();
1839
2745
  }
2746
+ if (!live())
2747
+ return;
1840
2748
  if (attempt !== 'opened') {
1841
- await finishOpening(progress);
2749
+ await finishOpening(progress, live);
1842
2750
  return;
1843
2751
  }
1844
2752
  if (handed || closed || progress.reachable)
1845
2753
  return;
1846
2754
  underway = null;
2755
+ opened = null;
1847
2756
  journal.record('connection reopen');
1848
- await rest(progress);
2757
+ await rest(progress, live);
1849
2758
  options.adapter.roomStream?.reopen();
1850
2759
  });
1851
2760
  }
1852
- function closedSocket(revoked) {
2761
+ function rediscover(scope) {
2762
+ return task(OPENING_LANE, async (live) => {
2763
+ if (!live() || !socketOpen)
2764
+ return;
2765
+ const progress = { complete: true, reachable: true };
2766
+ const gate = discover();
2767
+ moveTo(transferring(status));
2768
+ try {
2769
+ const local = await openLocal();
2770
+ if (!live())
2771
+ return;
2772
+ await ensureAuthorization();
2773
+ if (!live())
2774
+ return;
2775
+ await discoverRooms(local, progress, 'authorized', live, scope);
2776
+ }
2777
+ catch {
2778
+ progress.complete = false;
2779
+ }
2780
+ finally {
2781
+ gate.release();
2782
+ notifyReady();
2783
+ }
2784
+ if (!live())
2785
+ return;
2786
+ if (!progress.complete)
2787
+ complete = false;
2788
+ await rest(progress, live);
2789
+ });
2790
+ }
2791
+ function closedSocket(revoked, lost) {
1853
2792
  const opened = socketOpen;
1854
2793
  socketOpen = false;
2794
+ if (lost) {
2795
+ reads.lost();
2796
+ conversations.lost();
2797
+ }
1855
2798
  if (revoked) {
1856
2799
  void refuse('revoked-authorization');
1857
2800
  return;
@@ -1866,24 +2809,35 @@ export function composeRuntime(options) {
1866
2809
  });
1867
2810
  }
1868
2811
  function applyOpening(expired) {
1869
- return task(async () => {
1870
- const progress = underway;
2812
+ const progress = underway?.generation === generation ? underway.progress : null;
2813
+ const cycle = progress ?? { complete: true, reachable: true };
2814
+ if (opened === generation)
2815
+ opened = null;
2816
+ const reloading = progress === null ? [] : owedReloads(expired);
2817
+ const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
2818
+ return task(OPENING_LANE, async (live) => {
1871
2819
  underway = null;
1872
- if (closed || stopping)
2820
+ if (!live())
1873
2821
  return;
1874
2822
  if (progress === null) {
1875
- await reloadExpired(expired);
2823
+ await replacing;
2824
+ await rest(cycle, live);
1876
2825
  return;
1877
2826
  }
2827
+ await roomsCarried([...expired, ...reloading]);
2828
+ if (!live())
2829
+ return;
1878
2830
  try {
1879
2831
  const local = await openLocal();
1880
- 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)));
2832
+ const reloaded = await replacing;
1883
2833
  if (progress.reachable) {
1884
- for (const room of held)
1885
- if (!reloading.includes(room))
2834
+ for (const room of discovered.values()) {
2835
+ if (replicas.has(room.locator) &&
2836
+ !expired.includes(room.locator) &&
2837
+ !reloading.includes(room.locator)) {
1886
2838
  await confirmModels(room, local);
2839
+ }
2840
+ }
1887
2841
  }
1888
2842
  const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1889
2843
  if (failed !== undefined)
@@ -1893,21 +2847,36 @@ export function composeRuntime(options) {
1893
2847
  progress.complete = false;
1894
2848
  progress.reachable = false;
1895
2849
  }
1896
- await finishOpening(progress);
2850
+ if (live())
2851
+ await finishOpening(progress, live);
1897
2852
  });
1898
2853
  }
1899
- async function reloadExpired(expired) {
1900
- const progress = { complete: true, reachable: true };
2854
+ function owedReloads(expired) {
2855
+ const reloading = [...discovered.values()]
2856
+ .filter((room) => replicas.has(room.locator) &&
2857
+ !expired.includes(room.locator) &&
2858
+ (owed.has(room.locator) || revoked(room.locator)))
2859
+ .map((room) => room.locator);
2860
+ owed.clear();
2861
+ for (const locator of reloading)
2862
+ catching.add(locator);
2863
+ readSide.recover();
2864
+ return reloading;
2865
+ }
2866
+ async function reloadNow(replaced, progress) {
2867
+ const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
2868
+ for (const room of reloaded)
2869
+ reloadsDue.set(room.locator, generation);
1901
2870
  try {
1902
2871
  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)));
2872
+ return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
1906
2873
  }
1907
2874
  catch {
2875
+ for (const room of reloaded)
2876
+ reloadsDue.delete(room.locator);
1908
2877
  progress.reachable = false;
2878
+ return [];
1909
2879
  }
1910
- await rest(progress);
1911
2880
  }
1912
2881
  async function confirmModels(room, local) {
1913
2882
  for (const attached of ['collection', 'typed-selection']) {
@@ -1920,38 +2889,94 @@ export function composeRuntime(options) {
1920
2889
  function submitLater(locator) {
1921
2890
  if (localSaves)
1922
2891
  return;
1923
- const running = submitting.get(locator);
1924
- if (running !== undefined) {
1925
- running.again = true;
2892
+ void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
2893
+ }
2894
+ async function submitPass(locator, signal) {
2895
+ const room = discovered.get(locator);
2896
+ if (closed || stopping || discoveryPaused || room === undefined)
2897
+ return 'done';
2898
+ const progress = { complete: true, reachable: true };
2899
+ let next;
2900
+ try {
2901
+ await ensureAuthorization();
2902
+ if (authorization === undefined)
2903
+ return 'done';
2904
+ next = await submitRoom(room, await openLocal(), authorization, progress, signal);
2905
+ }
2906
+ catch {
2907
+ progress.reachable = false;
2908
+ next = 'again';
2909
+ }
2910
+ if (signal.aborted)
2911
+ return 'done';
2912
+ await rest(progress);
2913
+ return next === 'again' && !socketOpen ? 'done' : next;
2914
+ }
2915
+ function repairLater(locator, when) {
2916
+ if (!socketOpen || repairing.has(locator))
1926
2917
  return;
2918
+ void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
2919
+ }
2920
+ async function repairPass(locator, signal) {
2921
+ const room = discovered.get(locator);
2922
+ if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
2923
+ return 'done';
2924
+ if (room === undefined || room.replica.admission !== 'admitted')
2925
+ return 'done';
2926
+ const progress = { complete: true, reachable: true };
2927
+ repairing.add(locator);
2928
+ try {
2929
+ const local = await openLocal();
2930
+ await roomTask(locator, async (live) => {
2931
+ const current = () => live() && !signal.aborted;
2932
+ if (!current() || authorization === undefined)
2933
+ return;
2934
+ const withheld = await Promise.race([
2935
+ advance(room, local, authorization, current),
2936
+ abandonedOn(signal),
2937
+ ]);
2938
+ if (withheld !== null && current())
2939
+ await withhold(withheld, room, local, progress);
2940
+ });
1927
2941
  }
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
- })();
2942
+ catch {
2943
+ stale.add(locator);
2944
+ progress.reachable = false;
2945
+ }
2946
+ finally {
2947
+ repairing.delete(locator);
2948
+ }
2949
+ if (signal.aborted)
2950
+ return 'done';
2951
+ await rest(progress);
2952
+ if (!stale.has(locator)) {
2953
+ if ((replicas.get(locator)?.queue.length ?? 0) > 0)
2954
+ submitLater(locator);
2955
+ return 'done';
2956
+ }
2957
+ if (!repairable.has(locator))
2958
+ return 'done';
2959
+ const reset = repairable.get(locator);
2960
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
2961
+ }
2962
+ function resendLater() {
2963
+ if (!socketOpen)
2964
+ return;
2965
+ void recovery.attempt('resend', resendPass, { again: true });
2966
+ }
2967
+ async function resendPass(signal) {
2968
+ if (!socketOpen || closed || stopping || discoveryPaused)
2969
+ return 'done';
2970
+ const reading = { complete: true, reachable: true };
2971
+ try {
2972
+ await conversations.resend(reading);
2973
+ }
2974
+ catch {
2975
+ reading.reachable = false;
2976
+ }
2977
+ if (signal.aborted)
2978
+ return 'done';
2979
+ return reading.reachable && reading.complete ? 'done' : 'again';
1955
2980
  }
1956
2981
  function submitAll() {
1957
2982
  for (const [locator, replica] of replicas) {
@@ -1959,31 +2984,43 @@ export function composeRuntime(options) {
1959
2984
  submitLater(locator);
1960
2985
  }
1961
2986
  }
1962
- async function finishOpening(progress) {
2987
+ async function finishOpening(progress, live) {
1963
2988
  try {
1964
- if (closed || discoveryPaused) {
2989
+ if (!live()) {
1965
2990
  progress.complete = false;
1966
2991
  return;
1967
2992
  }
2993
+ reads.applied();
1968
2994
  submitAll();
2995
+ for (const locator of stale)
2996
+ if (repairable.has(locator))
2997
+ repairLater(locator, {});
1969
2998
  await conversations.reconcile();
1970
2999
  if (status.providerUnreachable)
1971
3000
  conversations.reconnected();
1972
3001
  const reading = { complete: true, reachable: true };
1973
3002
  await conversations.catchUp(reading);
1974
- if (!reading.reachable)
3003
+ if (progress.reachable && reading.reachable)
3004
+ await conversations.resend(reading);
3005
+ if (progress.reachable)
3006
+ void recovery.attempt('calls', resendCalls);
3007
+ if (!reading.reachable) {
1975
3008
  progress.reachable = false;
3009
+ resendLater();
3010
+ }
1976
3011
  }
1977
3012
  catch {
1978
3013
  progress.complete = false;
1979
3014
  }
1980
3015
  finally {
1981
- complete = progress.complete;
1982
- await rest(progress);
3016
+ if (live()) {
3017
+ complete = progress.complete;
3018
+ await rest(progress, live);
3019
+ }
1983
3020
  }
1984
3021
  }
1985
- async function rest(progress) {
1986
- if (closed)
3022
+ async function rest(progress, live = () => !closed) {
3023
+ if (!live())
1987
3024
  return;
1988
3025
  let lastCompleted = status.lastCompleted;
1989
3026
  let synced = complete && stale.size === 0 && !pending();
@@ -1991,6 +3028,8 @@ export function composeRuntime(options) {
1991
3028
  const completedAt = new Date().toISOString();
1992
3029
  try {
1993
3030
  await (await openLocal()).complete(completedAt);
3031
+ if (!live())
3032
+ return;
1994
3033
  recorded = true;
1995
3034
  lastCompleted = completedAt;
1996
3035
  }
@@ -2005,11 +3044,23 @@ export function composeRuntime(options) {
2005
3044
  ...attention(),
2006
3045
  });
2007
3046
  }
3047
+ function discover() {
3048
+ let release = () => { };
3049
+ const done = new Promise((resolve) => {
3050
+ release = resolve;
3051
+ });
3052
+ discovering = { generation, done, release };
3053
+ return discovering;
3054
+ }
2008
3055
  function applyRoom(locator, roomVersion) {
2009
3056
  announced.set(locator, roomVersion);
2010
- return task(async () => {
2011
- await answering.get(locator);
2012
- if (closed || stopping || discoveryPaused || authorization === undefined)
3057
+ return task(roomLane(locator), async (live) => {
3058
+ if (discovering?.generation === generation)
3059
+ await discovering.done;
3060
+ const saving = answering.get(locator);
3061
+ if (saving?.generation === generation)
3062
+ await saving.answered;
3063
+ if (!live() || authorization === undefined)
2013
3064
  return;
2014
3065
  const room = discovered.get(locator);
2015
3066
  const held = replicas.get(locator);
@@ -2020,25 +3071,41 @@ export function composeRuntime(options) {
2020
3071
  options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
2021
3072
  return;
2022
3073
  }
2023
- const during = underway;
3074
+ const during = underway?.generation === generation ? underway.progress : null;
2024
3075
  const progress = during ?? { complete: true, reachable: true };
2025
3076
  if (during === null)
2026
3077
  moveTo(transferring(status));
2027
3078
  try {
2028
3079
  const local = await openLocal();
2029
- const withheld = await advance(room, local, authorization);
2030
- if (withheld !== null)
2031
- await withhold(withheld, room, local, progress);
3080
+ await admittedRead(async () => {
3081
+ if (!live() || authorization === undefined)
3082
+ return;
3083
+ const withheld = await advance(room, local, authorization, live);
3084
+ if (withheld !== null)
3085
+ await withhold(withheld, room, local, progress);
3086
+ });
2032
3087
  }
2033
3088
  catch {
2034
3089
  stale.add(locator);
2035
3090
  }
2036
3091
  finally {
2037
3092
  if (during === null)
2038
- await rest(progress);
3093
+ await rest(progress, live);
2039
3094
  }
2040
3095
  });
2041
3096
  }
3097
+ function roomsCarried(replacing) {
3098
+ const replaced = new Set(replacing.map(roomLane));
3099
+ return Promise.all([...lanes]
3100
+ .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
3101
+ .map(([, held]) => held.chain));
3102
+ }
3103
+ const receipts = {
3104
+ heads: async () => (await openLocal()).receivedHeads(),
3105
+ parts: async (key, count) => (await openLocal()).receivedParts(key, count),
3106
+ keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
3107
+ drop: async (keys) => (await openLocal()).dropReceived(keys),
3108
+ };
2042
3109
  function observeStream() {
2043
3110
  const stream = options.adapter.roomStream;
2044
3111
  if (stream === undefined || withdrawStream !== undefined)
@@ -2048,15 +3115,21 @@ export function composeRuntime(options) {
2048
3115
  return;
2049
3116
  if (event.kind === 'opened') {
2050
3117
  socketOpen = true;
3118
+ generation += 1;
3119
+ reads.opened();
3120
+ conversations.reopened();
3121
+ recovery.resume();
2051
3122
  void sync('opened');
2052
3123
  }
2053
3124
  else if (event.kind === 'closed')
2054
- closedSocket(event.revoked);
3125
+ closedSocket(event.revoked, event.lost);
2055
3126
  else if (event.kind === 'opening')
2056
3127
  void applyOpening(event.expired);
3128
+ else if (event.kind === 'authority-changed')
3129
+ void rediscover(event.scope);
2057
3130
  else
2058
3131
  void applyRoom(event.locator, event.roomVersion);
2059
- });
3132
+ }, receipts);
2060
3133
  }
2061
3134
  function writesHeld() {
2062
3135
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -2071,6 +3144,7 @@ export function composeRuntime(options) {
2071
3144
  haltedRefusal = false;
2072
3145
  if (mode !== serviceMode) {
2073
3146
  serviceMode = mode;
3147
+ queueReadiness();
2074
3148
  journal.record(`service mode ${mode}`);
2075
3149
  for (const listener of serviceModeListeners)
2076
3150
  listener(mode);
@@ -2083,6 +3157,8 @@ export function composeRuntime(options) {
2083
3157
  if (stream === undefined || withdrawConversations !== undefined)
2084
3158
  return;
2085
3159
  withdrawConversations = stream.observe((event) => {
3160
+ if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
3161
+ queueReadiness();
2086
3162
  if (conversations.receive(event)) {
2087
3163
  void ready.then(scheduleCurrency);
2088
3164
  return;
@@ -2094,14 +3170,15 @@ export function composeRuntime(options) {
2094
3170
  });
2095
3171
  }
2096
3172
  function scheduleCurrency() {
2097
- if (currencyQueued || closed || stopping)
3173
+ if (currencyQueued === generation || closed || stopping)
2098
3174
  return;
2099
- currencyQueued = true;
2100
- void task(runCurrency);
3175
+ currencyQueued = generation;
3176
+ void task(CONVERSATION_LANE, runCurrency);
2101
3177
  }
2102
- async function runCurrency() {
2103
- currencyQueued = false;
2104
- if (closed || stopping || discoveryPaused)
3178
+ async function runCurrency(live) {
3179
+ if (currencyQueued === generation)
3180
+ currencyQueued = null;
3181
+ if (!live())
2105
3182
  return;
2106
3183
  await conversations.reconcile();
2107
3184
  if (conversations.current()) {
@@ -2116,19 +3193,22 @@ export function composeRuntime(options) {
2116
3193
  catch {
2117
3194
  progress.reachable = false;
2118
3195
  }
2119
- await rest(progress);
3196
+ await rest(progress, live);
2120
3197
  }
2121
3198
  function close() {
2122
3199
  if (closed)
2123
3200
  return;
2124
3201
  closed = true;
3202
+ if (rechecking !== null)
3203
+ clearInterval(rechecking);
3204
+ readinessWatches.clear();
2125
3205
  withdrawStream?.();
2126
3206
  withdrawStream = undefined;
2127
3207
  withdrawConversations?.();
2128
3208
  withdrawConversations = undefined;
2129
3209
  withdrawServiceMode?.();
2130
3210
  conversations.close();
2131
- stopTimer();
3211
+ void recovery.retire();
2132
3212
  store?.close();
2133
3213
  void opening?.then((opened) => {
2134
3214
  opened.close();
@@ -2146,9 +3226,8 @@ export function composeRuntime(options) {
2146
3226
  }
2147
3227
  async function stop() {
2148
3228
  stopping = true;
2149
- stopTimer();
2150
- await chain.catch(() => undefined);
2151
- await Promise.all([...submitting.values()].map((run) => run.done));
3229
+ await recovery.retire();
3230
+ await Promise.all(working);
2152
3231
  await mutations;
2153
3232
  close();
2154
3233
  }
@@ -2167,20 +3246,112 @@ export function composeRuntime(options) {
2167
3246
  if (options.adapter.roomStream === undefined)
2168
3247
  void sync('unstreamed');
2169
3248
  function planned(input, reach) {
2170
- return planOf(input, roomFacts(reach), planReach(reach));
3249
+ const plan = planOf(input, roomFacts(reach), planReach(reach));
3250
+ const named = plan.paths !== null &&
3251
+ plan.rooms !== null &&
3252
+ plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
3253
+ return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
2171
3254
  }
2172
3255
  function watchPlan(plan, deliver) {
2173
- materialize(reachedRooms(plan));
2174
- return readSide.watch(plan, identityOf(plan), deliver);
3256
+ let seeded = false;
3257
+ let pending = true;
3258
+ const shown = () => {
3259
+ const seed = seeds.get(plan.key);
3260
+ if (seed === undefined || seeded || !pending)
3261
+ return;
3262
+ seeded = true;
3263
+ deliver(seededResult(seed));
3264
+ };
3265
+ const watchers = seedWatchers.get(plan.key) ?? new Set();
3266
+ seedWatchers.set(plan.key, watchers);
3267
+ watchers.add(shown);
3268
+ const stop = readSide.watch(plan, identityOf(plan), () => {
3269
+ const cover = covered(plan);
3270
+ awaitPreparation(cover);
3271
+ return seedBehind(plan, cover);
3272
+ }, (message) => {
3273
+ const answered = message;
3274
+ pending = whole(answered) && 'pending' in answered.result;
3275
+ if (pending && seeds.has(plan.key)) {
3276
+ shown();
3277
+ return;
3278
+ }
3279
+ seeded = false;
3280
+ deliver(message);
3281
+ });
3282
+ return () => {
3283
+ watchers.delete(shown);
3284
+ if (watchers.size === 0)
3285
+ seedWatchers.delete(plan.key);
3286
+ stop();
3287
+ };
2175
3288
  }
2176
3289
  async function searched(input, reach) {
2177
- 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);
3290
+ const asked = searchPlanOf(input);
3291
+ const plan = { ...asked, limit: capped(asked.limit) };
3292
+ const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
3293
+ for (;;) {
3294
+ const cover = covered(filter);
3295
+ awaitPreparation(cover);
3296
+ const pending = 'pending' in cover ? [...cover.pending] : [];
3297
+ const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3298
+ const held = covered({
3299
+ ...filter,
3300
+ rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
3301
+ });
3302
+ const covering = 'pending' in held ? [] : held.coverage.rooms;
3303
+ const unindexed = covering
3304
+ .filter(({ room }) => !searchIndex.rooms().has(room))
3305
+ .map(({ room }) => room);
3306
+ for (const room of unindexed)
3307
+ pending.push({ room, awaits: 'index' });
3308
+ const listed = conversations.searchable();
3309
+ const named = reach !== null && !reach.conversations
3310
+ ? []
3311
+ : (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
3312
+ const waiting = named.filter((one) => !searchIndex.conversations().has(one));
3313
+ for (const conversation of waiting)
3314
+ pending.push({ conversation, awaits: 'index' });
3315
+ if (unindexed.length > 0 || waiting.length > 0)
3316
+ index(unindexed, waiting);
3317
+ const complete = pending.length === 0 && !('pending' in cover);
3318
+ const answering = complete
3319
+ ? filter
3320
+ : {
3321
+ ...filter,
3322
+ rooms: covering
3323
+ .filter(({ room }) => searchIndex.rooms().has(room))
3324
+ .map(({ room }) => room),
3325
+ };
3326
+ const answered = covered(answering);
3327
+ if ('pending' in answered)
3328
+ continue;
3329
+ const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
3330
+ await readSide.settled();
3331
+ const seen = searchIndex.changes();
3332
+ await searchIndex.caughtUp();
3333
+ const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
3334
+ if (selected !== null && 'pending' in selected)
3335
+ continue;
3336
+ if (searchIndex.changes() !== seen ||
3337
+ JSON.stringify(covered(answering)) !== JSON.stringify(answered))
3338
+ continue;
3339
+ const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms === null ? null : new Set(answering.rooms), plan.collection
3340
+ ? new Map(answered.coverage.rooms.map(({ room }) => [
3341
+ room,
3342
+ replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
3343
+ ]))
3344
+ : null);
3345
+ const coverage = {
3346
+ rooms: answered.coverage.rooms,
3347
+ conversations: searchedConversations,
3348
+ index: searchIndex.generation(),
3349
+ overlay: answered.coverage.overlay,
3350
+ };
3351
+ return complete
3352
+ ? { ...hits, coverage }
3353
+ : { pending, documents: hits.documents, messages: hits.messages, coverage };
3354
+ }
2184
3355
  }
2185
3356
  const watch = (topic, input, deliver) => {
2186
3357
  if (topic === 'room-drafts') {
@@ -2226,6 +3397,9 @@ export function composeRuntime(options) {
2226
3397
  presentationListeners.delete(listener);
2227
3398
  };
2228
3399
  }
3400
+ if (topic === 'readiness') {
3401
+ return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
3402
+ }
2229
3403
  if (topic === 'rooms') {
2230
3404
  roomListeners.add(deliver);
2231
3405
  deliver(listed());
@@ -2361,27 +3535,31 @@ export function composeRuntime(options) {
2361
3535
  });
2362
3536
  },
2363
3537
  write(app, value) {
2364
- return exclusive(async () => {
2365
- const local = await openLocal();
2366
- const stored = structuredClone(value);
2367
- await local.present({ app, value: stored });
2368
- presentations.set(app, stored);
2369
- publishChangeSet(changeSet('', null, null, { presentation: app }));
2370
- });
3538
+ return keepPresentation(app, () => structuredClone(value));
3539
+ },
3540
+ edit(app, members) {
3541
+ return keepPresentation(app, (held) => Object.fromEntries([
3542
+ ...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(members, member)),
3543
+ ...Object.entries(structuredClone(members)).filter(([, value]) => value !== null),
3544
+ ]));
2371
3545
  },
2372
3546
  },
2373
3547
  save(room, named, writtenBy) {
2374
3548
  return exclusive(() => saveRoomDrafts(room, named, writtenBy));
2375
3549
  },
3550
+ saveParts(room, parts, writtenBy) {
3551
+ return exclusive(() => saveParts(room, parts, writtenBy));
3552
+ },
2376
3553
  history(locator, path, query) {
2377
3554
  return readHistory(locator, path, query);
2378
3555
  },
2379
3556
  binaryFile: {
2380
3557
  read: readBinaryPath,
3558
+ link: linkBinaryPath,
2381
3559
  readReference: readBinaryReference,
2382
3560
  },
2383
3561
  call(name, input) {
2384
- return invokeAction(name, input);
3562
+ return recoveredAction(name, input);
2385
3563
  },
2386
3564
  reached(locator, roomVersion, path) {
2387
3565
  return path === undefined
@@ -2392,21 +3570,29 @@ export function composeRuntime(options) {
2392
3570
  return journal.export();
2393
3571
  },
2394
3572
  persistence() {
2395
- return options.shell.persistentStorage;
3573
+ return persistence === 'granted';
3574
+ },
3575
+ async retry() {
3576
+ journal.record('retry');
3577
+ unindexable.clear();
3578
+ queueReadiness();
3579
+ resume();
2396
3580
  },
2397
3581
  observe: observing(watch),
2398
3582
  async reauthorize() {
2399
3583
  if (refusal === null || !refusal.reauthorizationOffered)
2400
3584
  return;
2401
3585
  journal.record('connection reauthorize');
2402
- await chain.catch(() => undefined);
3586
+ await Promise.all([...lanes.values()]
3587
+ .filter((held) => held.generation === generation)
3588
+ .map((held) => held.chain));
2403
3589
  await mutations;
2404
3590
  authorization = undefined;
2405
3591
  discoveryPaused = false;
2406
3592
  if (options.adapter.roomStream === undefined)
2407
3593
  await sync('unstreamed');
2408
3594
  else
2409
- await task(ensureAuthorization);
3595
+ await task(OPENING_LANE, ensureAuthorization);
2410
3596
  },
2411
3597
  };
2412
3598
  const reachable = {
@@ -2417,6 +3603,21 @@ export function composeRuntime(options) {
2417
3603
  query: (plan) => rowsQuery(plan),
2418
3604
  watchPlan,
2419
3605
  search: searched,
3606
+ watchReadiness,
3607
+ };
3608
+ return {
3609
+ md,
3610
+ seed,
3611
+ stop,
3612
+ lose: loseRooms,
3613
+ watch,
3614
+ persisted(answer) {
3615
+ if (answer === persistence)
3616
+ return;
3617
+ persistence = answer;
3618
+ journal.record(`persistence ${answer}`);
3619
+ queueReadiness();
3620
+ },
3621
+ within: (reach, port) => narrowed(reachable, reach, port),
2420
3622
  };
2421
- return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2422
3623
  }