@agentshouse/mdruntime 0.1.0-c967d1ca66fee → 0.1.0-c97456d63baba

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 (183) hide show
  1. package/AGENTS.md +257 -227
  2. package/README.md +11 -1
  3. package/dist/acquisition.d.ts +14 -0
  4. package/dist/acquisition.d.ts.map +1 -0
  5. package/dist/acquisition.js +24 -0
  6. package/dist/adapter.d.ts +69 -20
  7. package/dist/adapter.d.ts.map +1 -1
  8. package/dist/attempt.d.ts +16 -0
  9. package/dist/attempt.d.ts.map +1 -0
  10. package/dist/attempt.js +70 -0
  11. package/dist/binary.d.ts +23 -0
  12. package/dist/binary.d.ts.map +1 -0
  13. package/dist/binary.js +8 -0
  14. package/dist/bootstrap.d.ts +2 -2
  15. package/dist/bootstrap.d.ts.map +1 -1
  16. package/dist/bootstrap.js +33 -14
  17. package/dist/change-set.d.ts +2 -0
  18. package/dist/change-set.d.ts.map +1 -1
  19. package/dist/change-set.js +39 -13
  20. package/dist/change.d.ts +11 -0
  21. package/dist/change.d.ts.map +1 -1
  22. package/dist/change.js +26 -1
  23. package/dist/chat/acts.d.ts +33 -0
  24. package/dist/chat/acts.d.ts.map +1 -0
  25. package/dist/chat/acts.js +36 -0
  26. package/dist/chat/context.d.ts +3 -0
  27. package/dist/chat/context.d.ts.map +1 -0
  28. package/dist/chat/context.js +10 -0
  29. package/dist/chat/input.d.ts +9 -0
  30. package/dist/chat/input.d.ts.map +1 -0
  31. package/dist/chat/input.js +99 -0
  32. package/dist/chat/kit.d.ts +61 -0
  33. package/dist/chat/kit.d.ts.map +1 -0
  34. package/dist/chat/kit.js +123 -0
  35. package/dist/chat/lasting.d.ts +6 -0
  36. package/dist/chat/lasting.d.ts.map +1 -0
  37. package/dist/chat/lasting.js +19 -0
  38. package/dist/chat/list.d.ts +27 -0
  39. package/dist/chat/list.d.ts.map +1 -0
  40. package/dist/chat/list.js +63 -0
  41. package/dist/chat/markdown.d.ts +20 -0
  42. package/dist/chat/markdown.d.ts.map +1 -0
  43. package/dist/chat/markdown.js +188 -0
  44. package/dist/chat/message.d.ts +6 -0
  45. package/dist/chat/message.d.ts.map +1 -0
  46. package/dist/chat/message.js +192 -0
  47. package/dist/chat/messages.d.ts +22 -0
  48. package/dist/chat/messages.d.ts.map +1 -0
  49. package/dist/chat/messages.js +155 -0
  50. package/dist/chat/name.d.ts +6 -0
  51. package/dist/chat/name.d.ts.map +1 -0
  52. package/dist/chat/name.js +37 -0
  53. package/dist/chat/opened.d.ts +11 -0
  54. package/dist/chat/opened.d.ts.map +1 -0
  55. package/dist/chat/opened.js +17 -0
  56. package/dist/chat/overlay.d.ts +31 -0
  57. package/dist/chat/overlay.d.ts.map +1 -0
  58. package/dist/chat/overlay.js +97 -0
  59. package/dist/chat/parts.d.ts +57 -0
  60. package/dist/chat/parts.d.ts.map +1 -0
  61. package/dist/chat/parts.js +119 -0
  62. package/dist/chat/process.d.ts +20 -0
  63. package/dist/chat/process.d.ts.map +1 -0
  64. package/dist/chat/process.js +32 -0
  65. package/dist/chat/provider.d.ts +63 -0
  66. package/dist/chat/provider.d.ts.map +1 -0
  67. package/dist/chat/provider.js +629 -0
  68. package/dist/chat/question.d.ts +9 -0
  69. package/dist/chat/question.d.ts.map +1 -0
  70. package/dist/chat/question.js +80 -0
  71. package/dist/chat/questions.d.ts +31 -0
  72. package/dist/chat/questions.d.ts.map +1 -0
  73. package/dist/chat/questions.js +111 -0
  74. package/dist/chat/read.d.ts +5 -0
  75. package/dist/chat/read.d.ts.map +1 -0
  76. package/dist/chat/read.js +13 -0
  77. package/dist/chat/ready.d.ts +34 -0
  78. package/dist/chat/ready.d.ts.map +1 -0
  79. package/dist/chat/ready.js +91 -0
  80. package/dist/chat/style.d.ts +4 -0
  81. package/dist/chat/style.d.ts.map +1 -0
  82. package/dist/chat/style.js +302 -0
  83. package/dist/chat.d.ts +14 -0
  84. package/dist/chat.d.ts.map +1 -0
  85. package/dist/chat.js +8 -0
  86. package/dist/client.d.ts +21 -10
  87. package/dist/client.d.ts.map +1 -1
  88. package/dist/client.js +14 -2
  89. package/dist/collection.d.ts +17 -3
  90. package/dist/collection.d.ts.map +1 -1
  91. package/dist/collection.js +230 -19
  92. package/dist/connection.d.ts +32 -12
  93. package/dist/connection.d.ts.map +1 -1
  94. package/dist/connection.js +1482 -351
  95. package/dist/conversation.d.ts +23 -9
  96. package/dist/conversation.d.ts.map +1 -1
  97. package/dist/conversation.js +372 -142
  98. package/dist/durability.d.ts +7 -1
  99. package/dist/durability.d.ts.map +1 -1
  100. package/dist/entry.d.ts +17 -0
  101. package/dist/entry.d.ts.map +1 -0
  102. package/dist/entry.js +156 -0
  103. package/dist/expectation.d.ts +21 -0
  104. package/dist/expectation.d.ts.map +1 -0
  105. package/dist/expectation.js +70 -0
  106. package/dist/fence.d.ts +6 -0
  107. package/dist/fence.d.ts.map +1 -0
  108. package/dist/fence.js +10 -0
  109. package/dist/host.d.ts +13 -3
  110. package/dist/host.d.ts.map +1 -1
  111. package/dist/host.js +48 -11
  112. package/dist/identity.d.ts +4 -0
  113. package/dist/identity.d.ts.map +1 -1
  114. package/dist/identity.js +12 -0
  115. package/dist/index.d.ts +13 -7
  116. package/dist/index.d.ts.map +1 -1
  117. package/dist/index.js +4 -0
  118. package/dist/memory.d.ts +5 -3
  119. package/dist/memory.d.ts.map +1 -1
  120. package/dist/memory.js +47 -13
  121. package/dist/model.d.ts +4 -2
  122. package/dist/model.d.ts.map +1 -1
  123. package/dist/model.js +10 -1
  124. package/dist/observation.d.ts +5 -2
  125. package/dist/observation.d.ts.map +1 -1
  126. package/dist/observation.js +22 -11
  127. package/dist/outcome.d.ts +11 -1
  128. package/dist/outcome.d.ts.map +1 -1
  129. package/dist/outcome.js +7 -0
  130. package/dist/overlay.d.ts +14 -2
  131. package/dist/overlay.d.ts.map +1 -1
  132. package/dist/overlay.js +75 -62
  133. package/dist/query.d.ts +31 -3
  134. package/dist/query.d.ts.map +1 -1
  135. package/dist/reach.d.ts +8 -2
  136. package/dist/reach.d.ts.map +1 -1
  137. package/dist/reach.js +42 -8
  138. package/dist/react.d.ts +7 -4
  139. package/dist/react.d.ts.map +1 -1
  140. package/dist/read-side.d.ts +13 -6
  141. package/dist/read-side.d.ts.map +1 -1
  142. package/dist/read-side.js +291 -85
  143. package/dist/readiness.d.ts +53 -0
  144. package/dist/readiness.d.ts.map +1 -0
  145. package/dist/readiness.js +33 -0
  146. package/dist/recovery.d.ts +25 -0
  147. package/dist/recovery.d.ts.map +1 -0
  148. package/dist/recovery.js +197 -0
  149. package/dist/refresh.d.ts.map +1 -1
  150. package/dist/refresh.js +21 -25
  151. package/dist/replica.d.ts +6 -3
  152. package/dist/replica.d.ts.map +1 -1
  153. package/dist/replica.js +78 -45
  154. package/dist/room-draft.d.ts +20 -3
  155. package/dist/room-draft.d.ts.map +1 -1
  156. package/dist/room-draft.js +119 -1
  157. package/dist/rows.d.ts +6 -13
  158. package/dist/rows.d.ts.map +1 -1
  159. package/dist/rows.js +5 -13
  160. package/dist/save.d.ts +28 -0
  161. package/dist/save.d.ts.map +1 -1
  162. package/dist/save.js +343 -29
  163. package/dist/search.d.ts +28 -6
  164. package/dist/search.d.ts.map +1 -1
  165. package/dist/search.js +108 -45
  166. package/dist/shared.d.ts +11 -0
  167. package/dist/shared.d.ts.map +1 -0
  168. package/dist/shared.js +368 -0
  169. package/dist/status.d.ts +4 -0
  170. package/dist/status.d.ts.map +1 -1
  171. package/dist/store.d.ts +55 -21
  172. package/dist/store.d.ts.map +1 -1
  173. package/dist/store.js +424 -206
  174. package/dist/turn.d.ts +7 -0
  175. package/dist/turn.d.ts.map +1 -0
  176. package/dist/turn.js +45 -0
  177. package/dist/wire.d.ts +137 -30
  178. package/dist/wire.d.ts.map +1 -1
  179. package/dist/wire.js +49 -29
  180. package/package.json +26 -14
  181. package/dist/attachment.d.ts +0 -3
  182. package/dist/attachment.d.ts.map +0 -1
  183. package/dist/attachment.js +0 -8
@@ -6,19 +6,27 @@ import { createReadSide } from './read-side.js';
6
6
  import { markdownPath, roomOfKey } from './rows.js';
7
7
  import { createSearchIndex, searchPlanOf, } from './search.js';
8
8
  import { bootstrapRoom } from './bootstrap.js';
9
- import { changeSet, mergeChangeSets } from './change-set.js';
9
+ import { changeSet, entering, mergeChangeSets } from './change-set.js';
10
10
  import { createConversationStore, } from './conversation.js';
11
- import { validRoot } from './collection.js';
11
+ import { queuedFile } from './change.js';
12
+ import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
12
13
  import { runtimeError } from './failure.js';
13
14
  import { createLog } from './log.js';
14
- import { calledResult, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
15
+ import { binaryRecord } from './binary.js';
16
+ import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
17
+ import { createRecovery } from './recovery.js';
15
18
  import { refreshRoom } from './refresh.js';
16
- import { buildBatch } from './save.js';
19
+ import { buildBatch, joined, limitedIn, requeued } from './save.js';
17
20
  import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
18
- import { changedParts, changesOf, held, readParts, returnedBy, returnedInto, staleWrite, underneath, } from './room-draft.js';
21
+ import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
19
22
  import { same, saving, STARTING, transferring, } from './status.js';
20
23
  import { detectStorageLoss, nextGeneration } from './durability.js';
21
- import { roomDraftRevision } from './identity.js';
24
+ import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
25
+ import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
26
+ import { FencedMap } from './fence.js';
27
+ import { tally } from './shared.js';
28
+ import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
29
+ import { identityTime, roomDraftRevision } from './identity.js';
22
30
  import { beneath, normalize } from './paths.js';
23
31
  import { openRuntimeStore, } from './store.js';
24
32
  import { unconfirmed } from './transfer.js';
@@ -36,22 +44,59 @@ function declaredSelection(room) {
36
44
  root !== '' &&
37
45
  selection.roots.every((other, at) => at === index || !within(other, root)));
38
46
  }
47
+ const OPENING_LANE = 'opening';
48
+ const CONVERSATION_LANE = 'conversations';
49
+ const ROOM_READS = 256;
50
+ const ROOM_ATTEMPTS = 16;
51
+ const outside = new Map();
52
+ function outsideOf(roots) {
53
+ const key = JSON.stringify(roots);
54
+ const held = outside.get(key);
55
+ if (held !== undefined)
56
+ return held;
57
+ const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
58
+ const made = {
59
+ documents: (path) => (markdownPath(path) ? counted(path) : 0),
60
+ binaries: counted,
61
+ };
62
+ outside.set(key, made);
63
+ return made;
64
+ }
65
+ function abandonedOn(signal) {
66
+ return new Promise((resolve) => {
67
+ if (signal.aborted)
68
+ resolve(null);
69
+ else
70
+ signal.addEventListener('abort', () => resolve(null), { once: true });
71
+ });
72
+ }
73
+ function roomLane(locator) {
74
+ return `room ${locator}`;
75
+ }
39
76
  export function composeRuntime(options) {
40
77
  const localSaves = options.adapter.localSaves === true;
41
78
  let authorization = options.authorization;
42
79
  let opening;
43
80
  let store;
44
- const replicas = new Map();
81
+ const fences = new Map();
82
+ const isFenced = (locator) => fences.has(locator);
83
+ let fencings = 0;
84
+ const lastFenced = new Map();
85
+ const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
86
+ let discoveries = 0;
87
+ const replicas = new FencedMap(isFenced);
88
+ const queuesWithoutReplica = new FencedMap(isFenced);
45
89
  const pendings = new Map();
46
- const roomDrafts = new Map();
90
+ const roomDrafts = new FencedMap(isFenced);
47
91
  const presentations = new Map();
48
92
  let limits = [];
49
93
  let capabilities = [];
50
- const rooms = new Map();
94
+ const rooms = new FencedMap(isFenced);
51
95
  const counted = new Map();
52
96
  const unreadable = new Set();
53
- const models = new Map();
54
- const liveModels = new Map();
97
+ const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
98
+ const models = new FencedMap(isModelFenced);
99
+ const liveModels = new FencedMap(isModelFenced);
55
100
  const invalidModels = new Set();
56
101
  const discovered = new Map();
57
102
  let roomsListed = false;
@@ -81,7 +126,6 @@ export function composeRuntime(options) {
81
126
  let status = STARTING;
82
127
  let shown = STARTING;
83
128
  let discoveryPaused = false;
84
- let resetTimer;
85
129
  let closed = false;
86
130
  let stopping = false;
87
131
  let withdrawStream;
@@ -89,12 +133,21 @@ export function composeRuntime(options) {
89
133
  let underway = null;
90
134
  let streamed = null;
91
135
  let handed = false;
92
- const submitting = new Map();
136
+ const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
137
+ const repairing = new Set();
138
+ const repairable = new Map();
139
+ let outcomeRetention = null;
93
140
  const answering = new Map();
94
141
  let complete = false;
95
142
  let recorded = false;
96
143
  const stale = new Set();
97
- let chain = Promise.resolve();
144
+ let generation = 0;
145
+ let discovering = null;
146
+ let opened = null;
147
+ const owed = new Set();
148
+ const lanes = new Map();
149
+ let roomReads = { generation, free: ROOM_READS, waiting: [] };
150
+ const working = new Set();
98
151
  let mutations = Promise.resolve();
99
152
  const roomListeners = new Set();
100
153
  const refusalListeners = new Set();
@@ -105,7 +158,7 @@ export function composeRuntime(options) {
105
158
  const presentationListeners = new Set();
106
159
  const readSide = createReadSide();
107
160
  let withdrawConversations;
108
- let currencyQueued = false;
161
+ let currencyQueued = null;
109
162
  let socketOpen = false;
110
163
  let serviceMode = 'open';
111
164
  let haltedRefusal = false;
@@ -113,24 +166,41 @@ export function composeRuntime(options) {
113
166
  const serviceModeListeners = new Set();
114
167
  let member = null;
115
168
  const memberListeners = new Set();
116
- const materialized = new Set();
117
- let everyRoom = false;
118
- const transfers = new Map();
169
+ const prepared = new Set();
170
+ const projecting = new Set();
171
+ const unpreparedDemand = new Set();
172
+ const unindexable = new Set();
173
+ let preparing = false;
174
+ let demand = WHOLE_DEMAND;
175
+ let narrowing = null;
176
+ let targets = {};
177
+ let persistence = options.shell.persistence;
178
+ let capacity = null;
179
+ const paused = new Set();
180
+ let rechecking = null;
181
+ const readinessWatches = new Set();
182
+ let readinessQueued = false;
183
+ const catching = new Set();
184
+ const transfers = new FencedMap(isFenced);
119
185
  const conversations = createConversationStore({
120
186
  agents: declaredAgents,
121
- documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
187
+ documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
122
188
  call: (name, input) => invokeAction(name, input),
123
189
  durably,
124
190
  stream: () => options.adapter.conversations,
125
191
  publish: publishChangeSet,
192
+ online: () => !status.providerUnreachable && refusal === null,
126
193
  closed: () => closed,
194
+ unanswered: resendLater,
195
+ expired: (operation) => pastHorizon(operation),
127
196
  });
128
197
  const searchIndex = createSearchIndex({
129
- documents: () => readSide.documents(),
198
+ room: (room) => readSide.room(room),
130
199
  document: (room, path) => readSide.document(room, path),
131
- messages: () => conversations.messages(),
132
- message: (conversation, identity) => conversations.message(conversation, identity),
133
200
  });
201
+ const roomsToIndex = new Set();
202
+ const conversationsToIndex = new Set();
203
+ let indexing = false;
134
204
  const journal = createLog();
135
205
  let notifyReady = () => { };
136
206
  const ready = new Promise((resolve) => {
@@ -194,7 +264,9 @@ export function composeRuntime(options) {
194
264
  }
195
265
  }
196
266
  function pending() {
197
- return !localSaves && [...replicas.values()].some((replica) => replica.queue.length > 0);
267
+ return (!localSaves &&
268
+ (queuesWithoutReplica.size > 0 ||
269
+ [...replicas.values()].some((replica) => replica.queue.length > 0)));
198
270
  }
199
271
  function attention() {
200
272
  const attentionRequired = [];
@@ -207,9 +279,16 @@ export function composeRuntime(options) {
207
279
  returned: structuredClone(record.returned),
208
280
  });
209
281
  }
282
+ for (const [room, refusal] of sourceRefusals)
283
+ attentionRequired.push({ room, refusal: structuredClone(refusal) });
210
284
  attentionRequired.sort((left, right) => {
211
285
  if (left.room !== right.room)
212
286
  return left.room < right.room ? -1 : 1;
287
+ if ('refusal' in left || 'refusal' in right) {
288
+ if ('refusal' in left && 'refusal' in right)
289
+ return 0;
290
+ return 'refusal' in left ? -1 : 1;
291
+ }
213
292
  if (left.path !== right.path)
214
293
  return left.path < right.path ? -1 : 1;
215
294
  return 0;
@@ -228,12 +307,9 @@ export function composeRuntime(options) {
228
307
  if (replica === undefined)
229
308
  return 0;
230
309
  const roots = replica.confirmed.record.typedSelection?.roots ?? [];
231
- let count = 0;
232
- for (const path of Object.keys(replica.visible.documents)) {
233
- if (markdownPath(path) && !roots.some((root) => beneath(root, path)))
234
- count += 1;
235
- }
236
- return count;
310
+ const weighed = outsideOf(roots);
311
+ return (tally(replica.visible.documents, weighed.documents) +
312
+ tally(replica.visible.binaries, weighed.binaries));
237
313
  }
238
314
  function listed() {
239
315
  return structuredClone([...rooms.values()].map((room) => {
@@ -260,15 +336,19 @@ export function composeRuntime(options) {
260
336
  return { listed: roomsListed, rooms: held };
261
337
  }
262
338
  function emitRooms() {
339
+ readSide.recover();
340
+ queueReadiness();
263
341
  const value = listed();
264
342
  for (const listener of roomListeners)
265
343
  listener(value);
266
344
  }
267
345
  function emitRefusal() {
346
+ queueReadiness();
268
347
  for (const listener of refusalListeners)
269
348
  listener(refusal);
270
349
  }
271
350
  function emitStorageLoss() {
351
+ queueReadiness();
272
352
  for (const listener of storageLossListeners)
273
353
  listener(storageLoss);
274
354
  }
@@ -285,7 +365,8 @@ export function composeRuntime(options) {
285
365
  function show() {
286
366
  if (closed)
287
367
  return;
288
- const next = status.state === 'synced' && !conversations.current()
368
+ queueReadiness();
369
+ const next = status.state === 'synced' && demand.conversations && !conversations.current()
289
370
  ? { ...status, state: 'syncing' }
290
371
  : status;
291
372
  if (same(shown, next))
@@ -304,10 +385,7 @@ export function composeRuntime(options) {
304
385
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
305
386
  }
306
387
  function entriesOf(locator) {
307
- const replica = replicas.get(locator);
308
- if (replica === undefined)
309
- return [];
310
- return replica.queue.map(entryOf);
388
+ return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
311
389
  }
312
390
  function emitQueue(locator) {
313
391
  const entries = entriesOf(locator);
@@ -317,42 +395,481 @@ export function composeRuntime(options) {
317
395
  }
318
396
  }
319
397
  function applyChangeSet(set) {
320
- const held = everyRoom
321
- ? set
322
- : { ...set, documents: set.documents.filter(({ locator }) => materialized.has(locator)) };
398
+ const held = {
399
+ ...set,
400
+ documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
401
+ };
323
402
  readSide.apply(held);
324
403
  searchIndex.changed(held);
325
404
  }
326
- function materialize(locators) {
327
- if (everyRoom)
405
+ async function referenced(locator) {
406
+ for (;;) {
407
+ const record = replicas.get(locator)?.confirmed.record;
408
+ if (record === undefined || closed)
409
+ return;
410
+ const held = { text: record.documents, binaries: record.binaries };
411
+ const composition = compositionOf(record);
412
+ if (referencesIndexed(held, composition))
413
+ return;
414
+ await indexReferrers(held, composition);
415
+ }
416
+ }
417
+ async function prepareReplica(locator) {
418
+ await referenced(locator);
419
+ const replica = replicas.get(locator);
420
+ if (replica === undefined || closed || prepared.has(locator))
328
421
  return;
329
- if (locators === null)
330
- everyRoom = true;
331
- const sets = [];
332
- for (const locator of locators ?? [...replicas.keys()]) {
333
- if (materialized.has(locator))
334
- continue;
335
- materialized.add(locator);
336
- const replica = replicas.get(locator);
337
- if (replica !== undefined)
338
- sets.push(changeSet(locator, null, replica));
422
+ const visible = replica.visible;
423
+ const paths = [
424
+ ...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
425
+ ];
426
+ const entered = [];
427
+ projecting.add(locator);
428
+ try {
429
+ await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
430
+ const current = replicas.get(locator);
431
+ if (current === undefined || closed)
432
+ return;
433
+ const set = entering(locator, current, chunk);
434
+ readSide.apply(set);
435
+ entered.push(set);
436
+ });
437
+ if (!replicas.has(locator) || closed)
438
+ return;
439
+ prepared.add(locator);
339
440
  }
340
- if (sets.length === 0)
441
+ finally {
442
+ projecting.delete(locator);
443
+ }
444
+ conversations.changed(mergeChangeSets(entered));
445
+ readSide.recover();
446
+ queueReadiness();
447
+ }
448
+ function unprepared() {
449
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
450
+ }
451
+ function unbuilt(locator, error) {
452
+ unindexable.add(locator);
453
+ journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
454
+ queueReadiness();
455
+ }
456
+ function prepare(locators = []) {
457
+ for (const locator of locators)
458
+ unpreparedDemand.add(locator);
459
+ if (preparing)
341
460
  return;
342
- const set = mergeChangeSets(sets);
343
- readSide.apply(set);
344
- searchIndex.changed(set);
461
+ preparing = true;
462
+ void (async () => {
463
+ try {
464
+ for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
465
+ await yielded();
466
+ if (closed)
467
+ return;
468
+ unpreparedDemand.delete(waiting);
469
+ await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
470
+ }
471
+ }
472
+ finally {
473
+ preparing = false;
474
+ }
475
+ })();
345
476
  }
346
477
  function reachedRooms(plan) {
347
- if (plan.rooms !== null)
348
- return plan.rooms;
349
- if (plan.reach === null)
478
+ const reach = plan.reach;
479
+ const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
480
+ const named = plan.rooms ??
481
+ (reached === null
482
+ ? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
483
+ : [...reached]);
484
+ return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
485
+ }
486
+ function awaited(locator) {
487
+ return rooms.has(locator) || !roomsListed;
488
+ }
489
+ function covered(plan) {
490
+ const locators = reachedRooms(plan);
491
+ if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
492
+ return { pending: [] };
493
+ }
494
+ const pending = [];
495
+ const coverage = [];
496
+ const overlay = [];
497
+ for (const locator of locators) {
498
+ const replica = replicas.get(locator);
499
+ if (replica === undefined) {
500
+ if (awaited(locator))
501
+ pending.push({ room: locator, awaits: 'acquisition' });
502
+ continue;
503
+ }
504
+ if (!prepared.has(locator)) {
505
+ pending.push({ room: locator, awaits: 'preparation' });
506
+ continue;
507
+ }
508
+ coverage.push({
509
+ room: locator,
510
+ generation: replica.confirmed.record.transfer,
511
+ current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
512
+ });
513
+ for (const batch of replica.queue)
514
+ overlay.push(batch.id);
515
+ }
516
+ if (pending.length > 0)
517
+ return { pending };
518
+ return { coverage: { rooms: coverage, indexes: [], overlay } };
519
+ }
520
+ function awaitPreparation(cover) {
521
+ if (!('pending' in cover))
522
+ return;
523
+ const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
524
+ if (waiting.length > 0)
525
+ prepare(waiting.map(({ room }) => room));
526
+ }
527
+ function acquires(room) {
528
+ return (demanded(demand, room) ||
529
+ (replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
530
+ queuesWithoutReplica.has(room.locator));
531
+ }
532
+ function acquiredLocators() {
533
+ return [...discovered.values()]
534
+ .filter(acquires)
535
+ .map((room) => room.locator)
536
+ .sort();
537
+ }
538
+ function heldDemand(acquired) {
539
+ if (demand.whole)
350
540
  return null;
351
- return [...plan.reach.rooms, ...plan.reach.documents.map(roomOfKey)];
541
+ return { rooms: acquired.map((room) => room.locator), conversations: demand.conversations };
542
+ }
543
+ function applyDemand(next) {
544
+ const before = acquiredLocators();
545
+ const conversed = demand.conversations;
546
+ demand = next;
547
+ const after = acquiredLocators();
548
+ queueReadiness();
549
+ if (!conversed && next.conversations)
550
+ scheduleCurrency();
551
+ if (JSON.stringify(before) === JSON.stringify(after) && conversed === next.conversations)
552
+ return;
553
+ journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
554
+ if (options.adapter.roomStream === undefined)
555
+ void sync('unstreamed');
556
+ else if (socketOpen)
557
+ options.adapter.roomStream.reopen();
558
+ }
559
+ function acquire(next) {
560
+ if (closed)
561
+ return;
562
+ if (narrowing !== null)
563
+ clearTimeout(narrowing);
564
+ narrowing = null;
565
+ const widening = wider(next, demand);
566
+ if (!widening && !narrower(next, demand))
567
+ return;
568
+ if (widening || !roomsListed) {
569
+ applyDemand(next);
570
+ return;
571
+ }
572
+ // A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
573
+ narrowing = setTimeout(() => {
574
+ narrowing = null;
575
+ applyDemand(next);
576
+ }, 5_000);
577
+ }
578
+ function requiredRooms(reach) {
579
+ if (reach === null)
580
+ return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
581
+ const named = [...new Set(reach.rooms)].sort();
582
+ if (roomsListed)
583
+ return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
584
+ return { listed: named.every((room) => replicas.has(room)), rooms: named };
585
+ }
586
+ function roomState(locator, search) {
587
+ if (sourceRefusals.has(locator))
588
+ return 'refused';
589
+ const listedRoom = discovered.get(locator);
590
+ if (roomsListed &&
591
+ (listedRoom === undefined ||
592
+ listedRoom.replica.admission !== 'admitted' ||
593
+ unreadable.has(locator)))
594
+ return 'unadmitted';
595
+ if (!replicas.has(locator))
596
+ return 'acquiring';
597
+ if (!prepared.has(locator))
598
+ return 'preparing';
599
+ if (search && !searchIndex.rooms().has(locator))
600
+ return 'indexing';
601
+ return 'complete';
602
+ }
603
+ function conversationCoverage(reach, search) {
604
+ const declared = reach === null || reach.conversations;
605
+ if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
606
+ return 'undeclared';
607
+ if (!conversations.known() || !conversations.current())
608
+ return 'acquiring';
609
+ if (!search)
610
+ return 'complete';
611
+ const unindexed = conversations
612
+ .searchable()
613
+ .filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
614
+ if (unindexed.length === 0)
615
+ return 'complete';
616
+ index([], unindexed);
617
+ return 'indexing';
618
+ }
619
+ function connectionOf() {
620
+ if (socketOpen)
621
+ return 'connected';
622
+ if (status.providerUnreachable)
623
+ return 'unreachable';
624
+ return options.adapter.roomStream === undefined && status.lastCompleted !== null
625
+ ? 'connected'
626
+ : 'connecting';
627
+ }
628
+ function refusedIn(locators) {
629
+ return locators.flatMap((room) => {
630
+ const held = sourceRefusals.get(room);
631
+ if (held === undefined)
632
+ return [];
633
+ const { file, location } = held.fields;
634
+ return [
635
+ {
636
+ room,
637
+ file: typeof file === 'string' ? file : '',
638
+ rule: held.code,
639
+ location: typeof location === 'string' ? location : null,
640
+ },
641
+ ];
642
+ });
643
+ }
644
+ function readinessFor(port, reach) {
645
+ const required = requiredRooms(reach);
646
+ const target = targets[port.target];
647
+ const search = target?.search === true;
648
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
649
+ const building = (state) => coverage
650
+ .filter((room) => room.state === state && !unindexable.has(room.room))
651
+ .map(({ room }) => room);
652
+ const preparing = building('preparing');
653
+ if (preparing.length > 0)
654
+ prepare(preparing);
655
+ const indexing = building('indexing');
656
+ if (indexing.length > 0)
657
+ index(indexing, []);
658
+ return readinessOf({
659
+ code: port.code,
660
+ coverage: {
661
+ listed: required.listed,
662
+ rooms: coverage,
663
+ conversations: conversationCoverage(reach, search),
664
+ search: search ? 'required' : 'unused',
665
+ },
666
+ persistence,
667
+ connection: connectionOf(),
668
+ house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
669
+ capacity: capacity !== null &&
670
+ coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
671
+ ? capacity
672
+ : port.capacity,
673
+ refused: refusedIn(required.rooms),
674
+ lost: storageLoss !== null,
675
+ established: target?.established === true,
676
+ });
677
+ }
678
+ function keepTarget(name, change) {
679
+ const held = targets[name] ?? { established: false, search: false };
680
+ const next = { ...held, ...change };
681
+ if (next.established === held.established && next.search === held.search)
682
+ return;
683
+ targets = { ...targets, [name]: next };
684
+ queueReadiness();
685
+ void durably((local) => local.keepTargets(targets)).catch(() => {
686
+ journal.record(`target record failed ${name}`);
687
+ });
688
+ }
689
+ function deliverReadiness(watch) {
690
+ if (!readinessWatches.has(watch))
691
+ return;
692
+ const readiness = readinessFor(watch.port, watch.reach);
693
+ if (readiness.ready && targets[watch.port.target]?.established !== true) {
694
+ keepTarget(watch.port.target, { established: true });
695
+ }
696
+ const shown = JSON.stringify(readiness);
697
+ if (shown === watch.last)
698
+ return;
699
+ watch.last = shown;
700
+ watch.deliver(structuredClone(readiness));
701
+ }
702
+ function queueReadiness() {
703
+ if (readinessQueued || closed || readinessWatches.size === 0)
704
+ return;
705
+ readinessQueued = true;
706
+ queueMicrotask(() => {
707
+ readinessQueued = false;
708
+ if (closed)
709
+ return;
710
+ for (const watch of [...readinessWatches])
711
+ deliverReadiness(watch);
712
+ });
713
+ }
714
+ function watchReadiness(port, reach, deliver) {
715
+ const watch = { port, reach, deliver, last: null };
716
+ readinessWatches.add(watch);
717
+ void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
718
+ return () => {
719
+ readinessWatches.delete(watch);
720
+ };
721
+ }
722
+ function searching(target) {
723
+ if (targets[target]?.search !== true)
724
+ keepTarget(target, { search: true });
725
+ }
726
+ function layerBytes(layer) {
727
+ let bytes = 0;
728
+ for (const [path, text] of Object.entries(layer.documents))
729
+ bytes += (path.length + text.length) * 2;
730
+ return bytes;
731
+ }
732
+ function grownBy(next, previous) {
733
+ if (previous === undefined)
734
+ return layerBytes(next);
735
+ const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
736
+ return layerBytes({ ...next, documents });
737
+ }
738
+ function exceeded(error) {
739
+ return (typeof error === 'object' &&
740
+ error !== null &&
741
+ error.name === 'QuotaExceededError');
742
+ }
743
+ async function estimated() {
744
+ const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
745
+ return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
746
+ }
747
+ function pause(locator, cause) {
748
+ paused.add(locator);
749
+ capacity = cause;
750
+ journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
751
+ queueReadiness();
752
+ if (rechecking !== null)
753
+ return;
754
+ // A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
755
+ rechecking = setInterval(() => {
756
+ void recheck();
757
+ }, 60_000);
758
+ }
759
+ async function recheck() {
760
+ const held = capacity;
761
+ if (held === null || closed)
762
+ return;
763
+ const available = await estimated().catch(() => null);
764
+ if (available === null || available < (held.needed ?? 0))
765
+ return;
766
+ if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
767
+ return;
768
+ resume();
769
+ }
770
+ function resume() {
771
+ if (rechecking !== null)
772
+ clearInterval(rechecking);
773
+ rechecking = null;
774
+ if (paused.size === 0 && capacity === null)
775
+ return;
776
+ paused.clear();
777
+ capacity = null;
778
+ journal.record('capacity resumed');
779
+ queueReadiness();
780
+ if (!closed && authorization !== undefined)
781
+ void sync('unstreamed');
782
+ }
783
+ async function reclaimed(local) {
784
+ const unreferenced = [...transfers.keys()].filter((locator) => {
785
+ const room = discovered.get(locator);
786
+ return room === undefined || !acquires(room);
787
+ });
788
+ for (const locator of unreferenced) {
789
+ await local.abandon(locator);
790
+ transfers.delete(locator);
791
+ }
792
+ if (unreferenced.length > 0)
793
+ journal.record(`capacity reclaimed ${unreferenced.length}`);
794
+ return unreferenced.length > 0;
795
+ }
796
+ async function admitted(local, locator, needed) {
797
+ if (paused.has(locator))
798
+ return false;
799
+ let available = await estimated().catch(() => null);
800
+ if (available === null || available >= needed)
801
+ return true;
802
+ if (await reclaimed(local))
803
+ available = await estimated().catch(() => null);
804
+ if (available === null || available >= needed)
805
+ return true;
806
+ pause(locator, { cause: 'insufficient-space', needed, available });
807
+ return false;
808
+ }
809
+ async function exceededOn(locator, bytes) {
810
+ const available = await estimated().catch(() => null);
811
+ pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
812
+ }
813
+ async function rowsQuery(plan) {
814
+ const before = covered(plan);
815
+ awaitPreparation(before);
816
+ if ('pending' in before)
817
+ return before;
818
+ const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
819
+ awaitPreparation(cover);
820
+ if ('pending' in cover)
821
+ return cover;
822
+ return 'groups' in rows
823
+ ? { groups: rows.groups, coverage: cover.coverage }
824
+ : { documents: rows.documents, coverage: cover.coverage };
825
+ }
826
+ async function indexConversation(conversation) {
827
+ searchIndex.indexMessages(conversation, [], false);
828
+ let after = null;
829
+ for (;;) {
830
+ const batch = await durably((local) => local.messagesAfter(conversation, after));
831
+ if (closed)
832
+ return;
833
+ searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
834
+ if (batch.length === 0)
835
+ return;
836
+ after = batch[batch.length - 1].identity;
837
+ }
352
838
  }
353
- function rowsQuery(plan) {
354
- materialize(reachedRooms(plan));
355
- return readSide.query(plan);
839
+ function indexNext() {
840
+ const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
841
+ if (room !== undefined) {
842
+ roomsToIndex.delete(room);
843
+ return () => prepared.has(room)
844
+ ? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
845
+ : undefined;
846
+ }
847
+ const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
848
+ if (conversation === undefined)
849
+ return null;
850
+ conversationsToIndex.delete(conversation);
851
+ return () => indexConversation(conversation);
852
+ }
853
+ function index(rooms, named) {
854
+ for (const room of rooms)
855
+ roomsToIndex.add(room);
856
+ for (const conversation of named)
857
+ conversationsToIndex.add(conversation);
858
+ if (indexing)
859
+ return;
860
+ indexing = true;
861
+ void (async () => {
862
+ try {
863
+ for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
864
+ await yielded();
865
+ await unit();
866
+ queueReadiness();
867
+ }
868
+ }
869
+ finally {
870
+ indexing = false;
871
+ }
872
+ })().catch(() => { });
356
873
  }
357
874
  function publishChangeSet(set) {
358
875
  applyChangeSet(set);
@@ -369,34 +886,7 @@ export function composeRuntime(options) {
369
886
  emitPresentation(app);
370
887
  conversations.changed(set);
371
888
  }
372
- function stopTimer() {
373
- if (resetTimer === undefined)
374
- return;
375
- clearTimeout(resetTimer);
376
- resetTimer = undefined;
377
- }
378
- function armTimer() {
379
- stopTimer();
380
- if (closed || stopping)
381
- return;
382
- let earliest = null;
383
- for (const { submission } of pendings.values()) {
384
- if (submission.state !== 'reset')
385
- continue;
386
- const at = Date.parse(submission.reset);
387
- if (earliest === null || at < earliest)
388
- earliest = at;
389
- }
390
- if (earliest === null)
391
- return;
392
- // A Save House held back goes again only once the reset its answer named has passed.
393
- resetTimer = setTimeout(() => {
394
- resetTimer = undefined;
395
- submitAll();
396
- }, Math.max(0, earliest - Date.now()));
397
- }
398
889
  async function refuse(reason, provider = null) {
399
- stopTimer();
400
890
  discoveryPaused = true;
401
891
  try {
402
892
  if (reason === 'revoked-authorization') {
@@ -404,10 +894,7 @@ export function composeRuntime(options) {
404
894
  const revoked = { status: 'unavailable', reason: 'revoked' };
405
895
  try {
406
896
  const local = await openLocal();
407
- await local.revokeModels([...replicas.values()].map((replica) => ({
408
- record: replica.confirmed.record,
409
- queue: replica.queue,
410
- })));
897
+ await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
411
898
  }
412
899
  catch {
413
900
  journal.record('model revocation persistence failed');
@@ -474,19 +961,28 @@ export function composeRuntime(options) {
474
961
  },
475
962
  providerOwnedPaths: room.providerOwnedPaths,
476
963
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
477
- attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
478
964
  synchronizationAvailable,
479
965
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
480
966
  });
481
967
  }
482
- async function applyDiscovery(listed, local) {
968
+ async function applyDiscovery(found, started) {
969
+ for (const room of found) {
970
+ const fence = fences.get(room.locator);
971
+ if (fence !== undefined && fence.erased && fence.at < started)
972
+ fences.delete(room.locator);
973
+ }
974
+ const listed = found.filter((room) => !fences.has(room.locator));
483
975
  const listedLocators = new Set(listed.map((room) => room.locator));
484
976
  for (const locator of listedLocators)
485
977
  scopedRefusals.delete(locator);
486
978
  discovered.clear();
487
979
  for (const room of listed)
488
980
  discovered.set(room.locator, room);
489
- await dropUnlisted(listedLocators, local);
981
+ const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
982
+ if (options.adapter.exhaustiveDiscovery === true)
983
+ await loseRooms(unlisted);
984
+ else
985
+ unlistedKept(unlisted);
490
986
  for (const room of listed) {
491
987
  const held = replicas.get(room.locator)?.confirmed.record;
492
988
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -497,44 +993,85 @@ export function composeRuntime(options) {
497
993
  }
498
994
  roomsListed = true;
499
995
  emitRooms();
996
+ return listed;
500
997
  }
501
998
  function unlistedHeld(listed) {
502
- return [...new Set([...rooms.keys(), ...roomDrafts.keys(), ...transfers.keys()])].filter((locator) => !listed.has(locator));
503
- }
504
- async function dropUnlisted(listed, local) {
505
- if (unlistedHeld(listed).length === 0)
506
- return;
507
- await exclusive(async () => {
508
- const unlisted = unlistedHeld(listed);
509
- await local.dropRooms(unlisted);
510
- const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
511
- room: true,
512
- queue: true,
513
- drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
514
- }));
515
- for (const locator of unlisted) {
516
- for (const batch of replicas.get(locator)?.queue ?? [])
517
- pendings.delete(batch.id);
518
- replicas.delete(locator);
999
+ return [
1000
+ ...new Set([
1001
+ ...rooms.keys(),
1002
+ ...roomDrafts.keys(),
1003
+ ...transfers.keys(),
1004
+ ...queuesWithoutReplica.keys(),
1005
+ ]),
1006
+ ].filter((locator) => !listed.has(locator));
1007
+ }
1008
+ function unlistedKept(unlisted) {
1009
+ for (const locator of unlisted) {
1010
+ const held = replicas.get(locator)?.confirmed.record;
1011
+ if (held !== undefined)
1012
+ list(held, false);
1013
+ else
519
1014
  rooms.delete(locator);
520
- roomDrafts.delete(locator);
521
- transfers.delete(locator);
522
- unreadable.delete(locator);
523
- stale.delete(locator);
524
- scopedRefusals.delete(locator);
525
- sourceRefusals.delete(locator);
526
- declarationRefusals.delete(locator);
527
- for (const attached of ['collection', 'typed-selection']) {
528
- const key = modelKey(locator, attached);
529
- models.delete(key);
530
- liveModels.delete(key);
531
- invalidModels.delete(key);
532
- }
533
- journal.record(`room unlisted ${locator}`);
1015
+ }
1016
+ }
1017
+ function fence(locators) {
1018
+ const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1019
+ const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
1020
+ room: true,
1021
+ queue: true,
1022
+ drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
1023
+ }));
1024
+ for (const locator of fresh) {
1025
+ fences.set(locator, { at: discoveries, erased: false });
1026
+ lastFenced.set(locator, ++fencings);
1027
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1028
+ pendings.delete(batch.id);
1029
+ replicas.delete(locator);
1030
+ queuesWithoutReplica.delete(locator);
1031
+ rooms.delete(locator);
1032
+ roomDrafts.delete(locator);
1033
+ transfers.delete(locator);
1034
+ catching.delete(locator);
1035
+ discovered.delete(locator);
1036
+ announced.delete(locator);
1037
+ unreadable.delete(locator);
1038
+ stale.delete(locator);
1039
+ repairable.delete(locator);
1040
+ unindexable.delete(locator);
1041
+ paused.delete(locator);
1042
+ scopedRefusals.delete(locator);
1043
+ sourceRefusals.delete(locator);
1044
+ declarationRefusals.delete(locator);
1045
+ for (const attached of ['collection', 'typed-selection']) {
1046
+ const key = modelKey(locator, attached);
1047
+ models.delete(key);
1048
+ liveModels.delete(key);
1049
+ invalidModels.delete(key);
534
1050
  }
535
- publishChangeSet(mergeChangeSets(removed));
536
- moveTo({ ...status, ...attention() });
537
- });
1051
+ journal.record(`room lost ${locator}`);
1052
+ }
1053
+ if (fresh.length === 0)
1054
+ return fresh;
1055
+ publishChangeSet(mergeChangeSets(removed));
1056
+ for (const locator of fresh)
1057
+ prepared.delete(locator);
1058
+ searchIndex.forget(fresh);
1059
+ readSide.recover();
1060
+ moveTo({ ...status, ...attention() });
1061
+ return fresh;
1062
+ }
1063
+ async function erased(local, locators) {
1064
+ await local.erase(locators);
1065
+ for (const locator of locators) {
1066
+ const held = fences.get(locator);
1067
+ if (held !== undefined)
1068
+ held.erased = true;
1069
+ }
1070
+ }
1071
+ async function loseRooms(locators) {
1072
+ const fresh = fence(locators);
1073
+ if (fresh.length > 0)
1074
+ await durably((local) => erased(local, fresh));
538
1075
  }
539
1076
  function restored(key, persisted) {
540
1077
  const prior = models.get(key);
@@ -555,7 +1092,7 @@ export function composeRuntime(options) {
555
1092
  return model;
556
1093
  }
557
1094
  async function hydrate(opened) {
558
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
1095
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
559
1096
  opened.all(),
560
1097
  opened.queues(),
561
1098
  opened.roomDrafts(),
@@ -567,7 +1104,9 @@ export function composeRuntime(options) {
567
1104
  opened.capabilities(),
568
1105
  opened.member(),
569
1106
  opened.transfers(),
1107
+ opened.targets(),
570
1108
  ]);
1109
+ targets = heldTargets;
571
1110
  transfers.clear();
572
1111
  for (const record of staged)
573
1112
  transfers.set(record.locator, record);
@@ -589,6 +1128,12 @@ export function composeRuntime(options) {
589
1128
  }
590
1129
  const previousReplicas = new Map(replicas);
591
1130
  replicas.clear();
1131
+ queuesWithoutReplica.clear();
1132
+ const replicated = new Set(records.map((record) => record.locator));
1133
+ for (const [locator, queue] of queuedBatches) {
1134
+ if (!replicated.has(locator))
1135
+ queuesWithoutReplica.set(locator, queue);
1136
+ }
592
1137
  for (const record of records) {
593
1138
  const model = restored(modelKey(record.locator, 'collection'), record.model);
594
1139
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
@@ -602,7 +1147,6 @@ export function composeRuntime(options) {
602
1147
  rooms.set(record.locator, {
603
1148
  ...existing,
604
1149
  roomVersion: record.roomVersion,
605
- attachments: record.attachments,
606
1150
  operations: record.operations,
607
1151
  replica: record.replica,
608
1152
  model: modelTypes(model),
@@ -624,6 +1168,7 @@ export function composeRuntime(options) {
624
1168
  async function readState(opened) {
625
1169
  const { lastCompleted, locators, read } = await hydrate(opened);
626
1170
  applyChangeSet(read);
1171
+ prepare();
627
1172
  if (replicas.size > 0)
628
1173
  emitRooms();
629
1174
  if (member !== null)
@@ -634,8 +1179,15 @@ export function composeRuntime(options) {
634
1179
  return lastCompleted;
635
1180
  }
636
1181
  async function open() {
637
- const opened = await openRuntimeStore();
1182
+ if (closed)
1183
+ throw new Error('runtime closed');
1184
+ const opened = await openRuntimeStore(isFenced);
638
1185
  store = opened;
1186
+ const interrupted = await opened.fences();
1187
+ for (const locator of interrupted) {
1188
+ fences.set(locator, { at: discoveries, erased: false });
1189
+ lastFenced.set(locator, ++fencings);
1190
+ }
639
1191
  const kept = options.shell.generation;
640
1192
  const storedGeneration = await opened.generation();
641
1193
  const loss = detectStorageLoss(storedGeneration, kept);
@@ -649,6 +1201,8 @@ export function composeRuntime(options) {
649
1201
  emitStorageLoss();
650
1202
  }
651
1203
  const lastCompleted = await readState(opened);
1204
+ if (interrupted.length > 0)
1205
+ await erased(opened, interrupted);
652
1206
  moveTo({ ...status, lastCompleted, ...attention() });
653
1207
  return opened;
654
1208
  }
@@ -675,6 +1229,7 @@ export function composeRuntime(options) {
675
1229
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
676
1230
  }
677
1231
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
1232
+ const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
678
1233
  const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
679
1234
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
680
1235
  const unqueued = withdrawn.map((batch) => ({
@@ -700,30 +1255,48 @@ export function composeRuntime(options) {
700
1255
  });
701
1256
  }
702
1257
  }
703
- await local.write({
704
- next: confirmed.record,
705
- previous: before?.confirmed.record ?? null,
706
- ...(settling.stored === undefined ? {} : { stored: settling.stored }),
707
- queue: kept,
708
- previousQueue: before?.queue ?? [],
709
- roomDrafts: [...written.values()],
710
- });
711
- for (const batch of before?.queue ?? []) {
1258
+ await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
1259
+ for (const batch of previousQueue) {
712
1260
  if (!kept.includes(batch))
713
1261
  pendings.delete(batch.id);
714
1262
  }
715
1263
  for (const record of written.values())
716
1264
  rememberRoomDraft(record);
717
- for (const { batch, returned } of unqueued) {
718
- const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
719
- journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
720
- }
721
- replicas.set(locator, replicaOf(confirmed, kept, before));
722
- return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
723
- room: settling.room,
724
- queue: kept.length !== (before?.queue.length ?? 0),
725
- drafts: [...written.keys()],
726
- });
1265
+ try {
1266
+ await local.write({
1267
+ next: confirmed.record,
1268
+ previous: before?.confirmed.record ?? null,
1269
+ ...(settling.stored === undefined ? {} : { stored: settling.stored }),
1270
+ });
1271
+ }
1272
+ catch (error) {
1273
+ const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
1274
+ if (held !== null)
1275
+ replicas.set(locator, held);
1276
+ else if (kept.length > 0)
1277
+ queuesWithoutReplica.set(locator, kept);
1278
+ else
1279
+ queuesWithoutReplica.delete(locator);
1280
+ publishChangeSet(changeSet(locator, before ?? null, held, {
1281
+ queue: kept.length !== previousQueue.length,
1282
+ drafts: [...written.keys()],
1283
+ }));
1284
+ moveTo({ ...status, ...attention() });
1285
+ throw error;
1286
+ }
1287
+ return () => {
1288
+ queuesWithoutReplica.delete(locator);
1289
+ for (const { batch, returned } of unqueued) {
1290
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
1291
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1292
+ }
1293
+ replicas.set(locator, replicaOf(confirmed, kept, before));
1294
+ return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
1295
+ room: settling.room,
1296
+ queue: kept.length !== previousQueue.length,
1297
+ drafts: [...written.keys()],
1298
+ });
1299
+ };
727
1300
  }
728
1301
  function adoptModels(locator, record) {
729
1302
  for (const [attached, model] of [
@@ -756,11 +1329,16 @@ export function composeRuntime(options) {
756
1329
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
757
1330
  };
758
1331
  }
759
- function publish(local, room, confirmed, landing = {}) {
1332
+ function publish(local, room, confirmed, landing, live) {
760
1333
  const { base, stored } = landing;
761
1334
  return exclusive(async () => {
762
- if (!continuing())
1335
+ if (stored !== undefined) {
1336
+ await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
1337
+ }
1338
+ if (!live()) {
1339
+ caughtUpTo(room.locator);
763
1340
  return true;
1341
+ }
764
1342
  const prior = replicas.get(room.locator);
765
1343
  if (base !== undefined &&
766
1344
  (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
@@ -768,30 +1346,48 @@ export function composeRuntime(options) {
768
1346
  return false;
769
1347
  }
770
1348
  adoptModels(room.locator, confirmed.record);
771
- const queue = prior?.queue ?? [];
1349
+ const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
772
1350
  const roomVersion = confirmed.record.roomVersion;
773
1351
  const accepted = queue.filter((batch) => {
774
1352
  const accepted = pendings.get(batch.id)?.accepted;
775
1353
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
776
1354
  });
777
- const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
778
- if (stored !== undefined)
1355
+ let settled;
1356
+ try {
1357
+ settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
1358
+ }
1359
+ catch (error) {
1360
+ if (!exceeded(error))
1361
+ throw error;
1362
+ await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
1363
+ caughtUpTo(room.locator);
1364
+ return true;
1365
+ }
1366
+ const set = settled();
1367
+ if (stored !== undefined) {
779
1368
  transfers.delete(room.locator);
1369
+ catching.delete(room.locator);
1370
+ }
780
1371
  for (const batch of accepted)
781
1372
  pendings.delete(batch.id);
782
1373
  stale.delete(room.locator);
1374
+ unindexable.delete(room.locator);
783
1375
  if (unreadable.delete(room.locator))
784
1376
  scopedRefusals.delete(room.locator);
785
1377
  sourceRefusals.delete(room.locator);
786
1378
  declarationRefusals.delete(room.locator);
787
1379
  list(room, true);
1380
+ if (prior === undefined)
1381
+ prepared.add(room.locator);
788
1382
  publishChangeSet(set);
789
1383
  moveTo({ ...status, ...attention() });
1384
+ options.adapter.roomStream?.published(room.locator, roomVersion);
790
1385
  return true;
791
1386
  });
792
1387
  }
793
1388
  async function saveRoomDrafts(locator, named, writtenBy) {
794
1389
  const local = await openLocal();
1390
+ await referenced(locator);
795
1391
  const replica = replicas.get(locator);
796
1392
  const room = rooms.get(locator);
797
1393
  if (replica === undefined || room === undefined) {
@@ -810,7 +1406,24 @@ export function composeRuntime(options) {
810
1406
  if (changed.length > 0)
811
1407
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
812
1408
  const parts = saved.map(({ path, parts }) => changesOf(path, parts));
813
- const built = buildBatch({
1409
+ const changes = parts.flatMap(({ changes }) => changes);
1410
+ const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
1411
+ if (requeue.kind === 'refused')
1412
+ return { outcome: 'refused', ...requeue.refusal };
1413
+ const foreign = [...requeue.withdrawn.keys()].some((origin) => {
1414
+ const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
1415
+ return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
1416
+ });
1417
+ if (foreign) {
1418
+ return {
1419
+ outcome: 'refused',
1420
+ code: 'unknown-revision',
1421
+ retryable: false,
1422
+ file: [...requeue.queued].at(-1),
1423
+ };
1424
+ }
1425
+ const withdrawing = requeue.withdrawn.size > 0;
1426
+ const input = {
814
1427
  local: localSaves,
815
1428
  baseRoomVersion: replica.confirmed.record.roomVersion,
816
1429
  composition: compositionFor(replica.confirmed.record),
@@ -819,28 +1432,91 @@ export function composeRuntime(options) {
819
1432
  providerLimits: limits,
820
1433
  handle: room.handle,
821
1434
  visible: replica.visible,
822
- changes: parts.flatMap(({ changes }) => changes),
1435
+ changes,
823
1436
  bases: parts.flatMap(({ bases }) => bases),
824
1437
  writtenBy,
1438
+ clean: {
1439
+ text: replica.confirmed.record.documents,
1440
+ binaries: replica.confirmed.record.binaries,
1441
+ composition: compositionOf(replica.confirmed.record),
1442
+ },
1443
+ };
1444
+ const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
1445
+ if (checked?.kind === 'refused')
1446
+ return { outcome: 'refused', ...checked.refusal };
1447
+ const owners = [...requeue.owners].flatMap((origin) => {
1448
+ const batch = requeue.withdrawn.get(origin);
1449
+ return batch === undefined || batch === null ? [] : [{ origin, batch }];
825
1450
  });
826
- if (built.kind === 'refused')
1451
+ if (owners.length > 1) {
1452
+ return {
1453
+ outcome: 'refused',
1454
+ code: 'unknown-revision',
1455
+ retryable: false,
1456
+ file: [...requeue.queued].at(-1),
1457
+ };
1458
+ }
1459
+ const built = !withdrawing
1460
+ ? buildBatch(input)
1461
+ : requeue.changes.length === 0
1462
+ ? null
1463
+ : buildBatch({
1464
+ ...input,
1465
+ visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
1466
+ changes: requeue.changes,
1467
+ });
1468
+ if (built?.kind === 'refused')
827
1469
  return { outcome: 'refused', ...built.refusal };
828
- const batches = [...replica.queue, built.batch];
1470
+ const [owner] = owners;
1471
+ const join = built === null || owner === undefined
1472
+ ? null
1473
+ : joined(requeue.queue, owner.batch, built.batch);
1474
+ if (join?.kind === 'refused')
1475
+ return { outcome: 'refused', ...join.refusal };
1476
+ const withdrawn = new Map(requeue.withdrawn);
1477
+ if (join !== null && owner !== undefined)
1478
+ withdrawn.set(owner.origin, join.batch);
1479
+ const drafted = new Map();
1480
+ for (const [origin, replaced] of withdrawn) {
1481
+ if (replaced === null)
1482
+ continue;
1483
+ const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
1484
+ drafted.set(replaced.id, replaced === join?.batch
1485
+ ? [
1486
+ ...carried.map((held) => joinedDraft(held, saved)),
1487
+ ...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
1488
+ ]
1489
+ : carried);
1490
+ }
1491
+ if (built !== null && join === null)
1492
+ drafted.set(built.batch.id, saved);
1493
+ const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
1494
+ for (const replaced of withdrawn.values()) {
1495
+ if (replaced === null || !batches.includes(replaced))
1496
+ continue;
1497
+ const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
1498
+ if (limited !== null)
1499
+ return { outcome: 'refused', ...limited };
1500
+ }
829
1501
  const consumed = saved.map(({ path }) => path);
830
1502
  try {
831
- await local.write({
832
- next: replica.confirmed.record,
833
- previous: replica.confirmed.record,
1503
+ await local.keepAuthored({
1504
+ locator,
834
1505
  queue: batches,
835
- previousQueue: replica.queue,
836
- saved,
1506
+ saved: drafted,
837
1507
  consumedRoomDrafts: consumed,
838
1508
  });
839
1509
  }
840
1510
  catch {
841
1511
  return { outcome: 'refused', code: 'durable-write', retryable: true };
842
1512
  }
843
- pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
1513
+ if (fences.has(locator))
1514
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1515
+ for (const origin of withdrawn.keys())
1516
+ pendings.delete(origin);
1517
+ for (const [id, drafts] of drafted) {
1518
+ pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
1519
+ }
844
1520
  if (!localSaves)
845
1521
  recorded = false;
846
1522
  for (const path of consumed)
@@ -852,7 +1528,10 @@ export function composeRuntime(options) {
852
1528
  }));
853
1529
  moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
854
1530
  submitLater(locator);
855
- return { outcome: 'saved', identifier: built.batch.id };
1531
+ const identifier = join?.batch.id ??
1532
+ built?.batch.id ??
1533
+ [...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
1534
+ return { outcome: 'saved', identifier };
856
1535
  }
857
1536
  async function mark(local, locator, identifier, submission, lost) {
858
1537
  const state = pendings.get(identifier);
@@ -862,6 +1541,8 @@ export function composeRuntime(options) {
862
1541
  return;
863
1542
  }
864
1543
  await local.submit(locator, identifier, submission, lost);
1544
+ if (fences.has(locator))
1545
+ return;
865
1546
  pendings.set(identifier, { ...state, submission, lost });
866
1547
  const replica = replicas.get(locator);
867
1548
  if (replica !== undefined)
@@ -872,6 +1553,8 @@ export function composeRuntime(options) {
872
1553
  if (!continuing())
873
1554
  throw new Error('connection refused');
874
1555
  await local.keepUpload(locator, identifier, upload);
1556
+ if (fences.has(locator))
1557
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
875
1558
  pendings.set(identifier, { ...pendings.get(identifier), upload });
876
1559
  if (!continuing())
877
1560
  throw new Error('connection refused');
@@ -887,6 +1570,7 @@ export function composeRuntime(options) {
887
1570
  }
888
1571
  if (closed || authorization === undefined || refusal !== null)
889
1572
  return UNCERTAIN;
1573
+ const since = fencings;
890
1574
  const transfer = ['transfer', 'history', locator, path];
891
1575
  if (query.revision !== undefined)
892
1576
  transfer.push(query.revision);
@@ -901,6 +1585,8 @@ export function composeRuntime(options) {
901
1585
  ...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
902
1586
  });
903
1587
  journal.record(`outcome history ${result.kind}`);
1588
+ if (fencedSince(locator, since))
1589
+ return failure({ kind: 'room-not-readable', locator });
904
1590
  if (result.kind === 'scoped-refusal') {
905
1591
  const room = replicas.get(result.locator)?.confirmed.record;
906
1592
  if (room !== undefined)
@@ -915,6 +1601,100 @@ export function composeRuntime(options) {
915
1601
  return UNCERTAIN;
916
1602
  }
917
1603
  }
1604
+ function queuedBlob(locator, path, localId) {
1605
+ for (const batch of replicas.get(locator)?.queue ?? []) {
1606
+ const change = batch.changes[queuedFile(batch, path, localId)];
1607
+ if (change?.kind === 'binary')
1608
+ return change.blob;
1609
+ }
1610
+ return null;
1611
+ }
1612
+ function binaryRoom(request) {
1613
+ if (request.kind === 'path')
1614
+ return request.locator;
1615
+ const scope = options.adapter.referrerScope(request.referrer);
1616
+ return scope?.kind === 'room' ? scope.locator : null;
1617
+ }
1618
+ async function readBinary(request) {
1619
+ if (closed || refusal !== null)
1620
+ return UNCERTAIN;
1621
+ try {
1622
+ await ensureAuthorization();
1623
+ }
1624
+ catch {
1625
+ return UNCERTAIN;
1626
+ }
1627
+ if (closed || authorization === undefined || refusal !== null)
1628
+ return UNCERTAIN;
1629
+ const since = fencings;
1630
+ journal.record(request.kind === 'path'
1631
+ ? `transfer binary ${request.locator} ${request.path}`
1632
+ : `transfer binary ${request.fileRef}`);
1633
+ try {
1634
+ const result = await options.adapter.binary(authorization, request);
1635
+ journal.record(`outcome binary ${result.kind}`);
1636
+ const locator = binaryRoom(request);
1637
+ if (locator !== null && fencedSince(locator, since))
1638
+ return failure({ kind: 'room-not-readable', locator });
1639
+ if (result.kind === 'scoped-refusal') {
1640
+ const room = replicas.get(result.locator)?.confirmed.record;
1641
+ if (room !== undefined)
1642
+ await scoped(result, room, await openLocal());
1643
+ }
1644
+ if (result.kind === 'revoked-authorization')
1645
+ await refuse('revoked-authorization');
1646
+ return binaryAnswer(result);
1647
+ }
1648
+ catch {
1649
+ journal.record('outcome binary transport-uncertainty');
1650
+ return UNCERTAIN;
1651
+ }
1652
+ }
1653
+ async function readBinaryPath(locator, path, revision) {
1654
+ await ready;
1655
+ if (revision !== undefined)
1656
+ return readBinary({ kind: 'path', locator, path, revision });
1657
+ const visible = replicas.get(locator)?.visible;
1658
+ const held = visible?.binaries[path];
1659
+ if (visible === undefined || held === undefined)
1660
+ return absentBinary(path);
1661
+ if (held.state === 'pending') {
1662
+ const blob = queuedBlob(locator, path, held.localId);
1663
+ return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1664
+ }
1665
+ const record = binaryRecord(path, held, visible.revisions[path]);
1666
+ if (record.state !== 'accepted')
1667
+ return absentBinary(path);
1668
+ const replica = replicas.get(locator);
1669
+ const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
1670
+ const holds = (at) => {
1671
+ const kept = replica?.confirmed.record.binaries[at];
1672
+ return (kept?.state === 'accepted' &&
1673
+ kept.fileRef === record.fileRef &&
1674
+ replica?.confirmed.record.revisions[at] === record.revision);
1675
+ };
1676
+ let confirmed = path;
1677
+ for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
1678
+ const change = queued[index];
1679
+ if (change.kind === 'move' && change.to === confirmed)
1680
+ confirmed = change.path;
1681
+ }
1682
+ return readBinary({
1683
+ kind: 'path',
1684
+ locator,
1685
+ path: confirmed,
1686
+ revision: record.revision,
1687
+ fileRef: record.fileRef,
1688
+ });
1689
+ }
1690
+ async function readBinaryReference(reference) {
1691
+ await ready;
1692
+ return readBinary({
1693
+ kind: 'reference',
1694
+ fileRef: reference.fileRef,
1695
+ referrer: reference.referrer,
1696
+ });
1697
+ }
918
1698
  function declaredAgents() {
919
1699
  for (const capability of capabilities) {
920
1700
  if (capability.conversationAgents !== undefined)
@@ -979,11 +1759,11 @@ export function composeRuntime(options) {
979
1759
  options.adapter.reached(locator, written, roomVersion) &&
980
1760
  !options.adapter.reached(locator, roomVersion, written));
981
1761
  }
982
- async function pullRefresh(authorized, confirmed) {
983
- if (!continuing())
1762
+ async function pullRefresh(authorized, confirmed, live) {
1763
+ if (!live())
984
1764
  return unconfirmed;
985
1765
  try {
986
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
1766
+ const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
987
1767
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
988
1768
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
989
1769
  return result;
@@ -993,47 +1773,80 @@ export function composeRuntime(options) {
993
1773
  throw error;
994
1774
  }
995
1775
  }
996
- function staging(local) {
1776
+ function staging(local, live) {
997
1777
  return {
998
1778
  held: (locator) => transfers.get(locator) ?? null,
999
1779
  staged: (record) => local.staged(record),
1000
1780
  stage: (record, page) => exclusive(async () => {
1001
- if (!continuing())
1781
+ if (!live())
1782
+ return false;
1783
+ const bytes = layerBytes(page);
1784
+ if (!(await admitted(local, record.locator, bytes)))
1002
1785
  return false;
1003
- await local.stage(record, page);
1786
+ try {
1787
+ await local.stage(record, page);
1788
+ }
1789
+ catch (error) {
1790
+ if (!exceeded(error))
1791
+ throw error;
1792
+ await exceededOn(record.locator, bytes);
1793
+ return false;
1794
+ }
1004
1795
  transfers.set(record.locator, record);
1796
+ readSide.recover();
1005
1797
  return true;
1006
1798
  }),
1007
1799
  abandon: (locator) => exclusive(async () => {
1008
- if (!continuing())
1800
+ if (!live())
1009
1801
  return;
1010
1802
  await local.abandon(locator);
1011
1803
  transfers.delete(locator);
1804
+ readSide.recover();
1012
1805
  }),
1013
1806
  };
1014
1807
  }
1015
- async function pullBootstrap(authorized, room, local) {
1016
- if (!continuing())
1808
+ function reusable(room) {
1809
+ const record = replicas.get(room.locator)?.confirmed.record;
1810
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1811
+ return null;
1812
+ return { documents: record.documents, revisions: record.revisions };
1813
+ }
1814
+ async function pullBootstrap(authorized, room, local, live) {
1815
+ if (!live())
1017
1816
  return unconfirmed;
1817
+ if (replicas.has(room.locator)) {
1818
+ catching.add(room.locator);
1819
+ readSide.recover();
1820
+ }
1018
1821
  try {
1019
1822
  const result = await bootstrapRoom(options.adapter, authorized, room, {
1020
1823
  collection: modelOf(room.locator),
1021
1824
  selection: modelOf(room.locator, 'typed-selection'),
1022
- }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
1825
+ }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
1023
1826
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
1024
1827
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1828
+ if (result.kind !== 'staged')
1829
+ caughtUpTo(room.locator);
1025
1830
  return result;
1026
1831
  }
1027
1832
  catch (error) {
1028
1833
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
1834
+ caughtUpTo(room.locator);
1029
1835
  throw error;
1030
1836
  }
1031
1837
  }
1032
- async function advance(room, local, authorized) {
1838
+ function caughtUpTo(locator) {
1839
+ if (catching.delete(locator))
1840
+ readSide.recover();
1841
+ }
1842
+ async function advance(room, local, authorized, live) {
1843
+ await referenced(room.locator);
1844
+ if (!live() || paused.has(room.locator))
1845
+ return unconfirmed;
1033
1846
  const held = replicas.get(room.locator);
1034
1847
  if (held !== undefined) {
1035
- const refreshed = await pullRefresh(authorized, held.confirmed);
1036
- if (!continuing())
1848
+ const refreshed = await pullRefresh(authorized, held.confirmed, live);
1849
+ if (!live())
1037
1850
  return unconfirmed;
1038
1851
  if (refreshed.kind === 'still-current') {
1039
1852
  stale.delete(room.locator);
@@ -1047,31 +1860,45 @@ export function composeRuntime(options) {
1047
1860
  return null;
1048
1861
  }
1049
1862
  if (refreshed.kind === 'advanced') {
1050
- if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
1863
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
1051
1864
  const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
1052
1865
  const reached = now !== undefined &&
1053
1866
  options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
1054
- return reached ? null : advance(room, local, authorized);
1867
+ return reached ? null : advance(room, local, authorized, live);
1055
1868
  }
1056
1869
  await retainModel(local, room, 'collection', modelOf(room.locator));
1057
1870
  return null;
1058
1871
  }
1059
1872
  if (refreshed.kind !== 'beyond-horizon')
1060
1873
  return refreshed;
1874
+ if (opened !== null) {
1875
+ owed.add(room.locator);
1876
+ readSide.recover();
1877
+ return unconfirmed;
1878
+ }
1061
1879
  }
1062
- const bootstrapped = await pullBootstrap(authorized, room, local);
1063
- if (!continuing())
1880
+ const bootstrapped = await pullBootstrap(authorized, room, local, live);
1881
+ if (!live())
1064
1882
  return unconfirmed;
1065
1883
  if (bootstrapped.kind !== 'staged')
1066
1884
  return bootstrapped;
1067
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1885
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
1068
1886
  return null;
1069
1887
  }
1070
1888
  async function withhold(withheld, room, local, progress) {
1889
+ if (withheld.kind === 'unconfirmed')
1890
+ return;
1071
1891
  if (withheld.kind === 'refused')
1072
1892
  stale.delete(room.locator);
1073
1893
  else
1074
1894
  stale.add(room.locator);
1895
+ if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
1896
+ const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
1897
+ repairable.set(room.locator, reset);
1898
+ repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
1899
+ }
1900
+ else
1901
+ repairable.delete(room.locator);
1075
1902
  if (withheld.kind === 'scoped-refusal') {
1076
1903
  await scoped(withheld, room, local);
1077
1904
  }
@@ -1107,6 +1934,7 @@ export function composeRuntime(options) {
1107
1934
  unreadable.add(room.locator);
1108
1935
  list(room, false);
1109
1936
  emitRooms();
1937
+ moveTo({ ...status, ...attention() });
1110
1938
  journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
1111
1939
  }
1112
1940
  function leaving(batch, result) {
@@ -1128,6 +1956,7 @@ export function composeRuntime(options) {
1128
1956
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1129
1957
  }
1130
1958
  async function adoptAccepted(room, local, submitted, accepted) {
1959
+ await referenced(room.locator);
1131
1960
  if (!continuing())
1132
1961
  return unconfirmed;
1133
1962
  const current = replicas.get(room.locator);
@@ -1138,22 +1967,23 @@ export function composeRuntime(options) {
1138
1967
  logAccepted(room.locator, submitted, accepted);
1139
1968
  const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1140
1969
  ? { kind: 'staged', confirmed: current.confirmed }
1141
- : applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.changes, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
1970
+ : applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
1142
1971
  ? accepted.roomVersion
1143
1972
  : held, accepted.roomVersion);
1144
1973
  if (applied.kind !== 'staged') {
1145
1974
  const state = pendings.get(submitted.id);
1146
1975
  await local.accept(room.locator, submitted.id, accepted.roomVersion);
1147
- if (state !== undefined)
1976
+ if (state !== undefined && !fences.has(room.locator))
1148
1977
  pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1149
1978
  emitQueue(room.locator);
1150
1979
  return applied;
1151
1980
  }
1152
- const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1981
+ const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1153
1982
  pendings.delete(submitted.id);
1154
1983
  unreadable.delete(room.locator);
1155
1984
  sourceRefusals.delete(room.locator);
1156
1985
  declarationRefusals.delete(room.locator);
1986
+ const set = settled();
1157
1987
  list(room, true);
1158
1988
  publishChangeSet(set);
1159
1989
  moveTo({ ...status, ...attention() });
@@ -1167,14 +1997,14 @@ export function composeRuntime(options) {
1167
1997
  refusedSource(room, failed);
1168
1998
  return false;
1169
1999
  }
1170
- async function submitRoom(room, local, authorized, progress) {
1171
- while (continuing() && authorization !== undefined) {
2000
+ async function submitRoom(room, local, authorized, progress, signal) {
2001
+ while (continuing() && authorization !== undefined && !signal.aborted) {
1172
2002
  const replica = replicas.get(room.locator);
1173
2003
  const head = replica?.queue[0];
1174
2004
  if (replica === undefined || head === undefined)
1175
- return;
2005
+ return 'done';
1176
2006
  if (pendings.get(head.id)?.accepted !== undefined)
1177
- return;
2007
+ return 'done';
1178
2008
  const state = pendings.get(head.id);
1179
2009
  const { submission, lost: earlier } = state ?? {
1180
2010
  submission: { state: 'waiting' },
@@ -1182,24 +2012,30 @@ export function composeRuntime(options) {
1182
2012
  };
1183
2013
  if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
1184
2014
  progress.complete = false;
1185
- return;
2015
+ return { at: Date.parse(submission.reset) };
1186
2016
  }
1187
2017
  if (writesHeld()) {
1188
2018
  progress.complete = false;
1189
2019
  journal.record(`submission ${room.locator} ${head.id} held`);
1190
- return;
2020
+ return 'done';
1191
2021
  }
1192
2022
  const lost = earlier || submission.state === 'submitted';
2023
+ if (lost && pastHorizon(head.id)) {
2024
+ await discardPending(room, local, signal);
2025
+ return 'done';
2026
+ }
1193
2027
  let result;
1194
2028
  journal.record(`submission ${room.locator} ${head.id}`);
1195
2029
  await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1196
- if (discoveryPaused)
1197
- return;
1198
- if (writesHeld()) {
2030
+ if (discoveryPaused || fences.has(room.locator))
2031
+ return 'done';
2032
+ if (signal.aborted || writesHeld()) {
1199
2033
  await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
2034
+ if (signal.aborted)
2035
+ return 'done';
1200
2036
  progress.complete = false;
1201
2037
  journal.record(`submission ${room.locator} ${head.id} held`);
1202
- return;
2038
+ return 'done';
1203
2039
  }
1204
2040
  const opensBefore = openSignals;
1205
2041
  const answered = awaitingAnswer(room.locator);
@@ -1215,32 +2051,36 @@ export function composeRuntime(options) {
1215
2051
  progress.complete = false;
1216
2052
  if (continuing())
1217
2053
  progress.reachable = false;
1218
- return;
2054
+ return 'again';
2055
+ }
2056
+ if (signal.aborted) {
2057
+ answered();
2058
+ return 'done';
1219
2059
  }
1220
2060
  if (result.kind !== 'accepted' || !continuing())
1221
2061
  answered();
1222
2062
  if (!continuing())
1223
- return;
2063
+ return 'done';
1224
2064
  if (result.kind === 'transport-uncertainty') {
1225
2065
  progress.complete = false;
1226
2066
  progress.reachable = false;
1227
- return;
2067
+ return 'again';
1228
2068
  }
1229
2069
  if (result.kind === 'room-not-readable') {
1230
2070
  progress.complete = false;
1231
2071
  unreadable.add(room.locator);
1232
2072
  list(room, false);
1233
2073
  emitRooms();
1234
- return;
2074
+ return 'done';
1235
2075
  }
1236
2076
  if (result.kind === 'revoked-authorization') {
1237
2077
  await refuse('revoked-authorization');
1238
- return;
2078
+ return 'done';
1239
2079
  }
1240
2080
  if (result.kind === 'scoped-refusal') {
1241
2081
  await scoped(result, room, local);
1242
2082
  progress.complete = false;
1243
- return;
2083
+ return 'done';
1244
2084
  }
1245
2085
  if (result.kind === 'write-halted') {
1246
2086
  journal.record(`outcome ${room.locator} ${head.id} write-halted`);
@@ -1249,16 +2089,24 @@ export function composeRuntime(options) {
1249
2089
  continue;
1250
2090
  haltedRefusal = true;
1251
2091
  progress.complete = false;
1252
- return;
2092
+ return 'done';
1253
2093
  }
1254
2094
  if (result.kind === 'conflicted' ||
1255
2095
  result.kind === 'rejected' ||
1256
2096
  result.kind === 'expired' ||
1257
2097
  result.kind === 'retryable-refusal') {
2098
+ if (result.kind === 'expired' && lost && result.restored !== true) {
2099
+ journal.record(`outcome ${room.locator} ${head.id} expired`);
2100
+ await discardPending(room, local, signal);
2101
+ return 'done';
2102
+ }
1258
2103
  if (result.kind === 'expired') {
1259
- const withheld = await advance(room, local, authorized);
1260
- if (withheld !== null)
1261
- return withhold(withheld, room, local, progress);
2104
+ await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
2105
+ const withheld = await advance(room, local, authorized, continuing);
2106
+ if (withheld !== null) {
2107
+ await withhold(withheld, room, local, progress);
2108
+ return 'done';
2109
+ }
1262
2110
  }
1263
2111
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
1264
2112
  (result.kind === 'conflicted' && result.roomVersion !== undefined
@@ -1271,8 +2119,7 @@ export function composeRuntime(options) {
1271
2119
  const reset = result.reset;
1272
2120
  progress.complete = false;
1273
2121
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1274
- armTimer();
1275
- return;
2122
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
1276
2123
  }
1277
2124
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
1278
2125
  const left = leaving(head, result);
@@ -1283,37 +2130,82 @@ export function composeRuntime(options) {
1283
2130
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
1284
2131
  return;
1285
2132
  }
1286
- const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
2133
+ const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1287
2134
  leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1288
2135
  (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1289
2136
  ? { ...left, returned: null }
1290
2137
  : left,
1291
2138
  });
1292
- publishChangeSet(set);
2139
+ publishChangeSet(settled());
1293
2140
  moveTo({ ...status, ...attention() });
1294
2141
  });
1295
2142
  if (result.kind === 'expired')
1296
2143
  continue;
1297
- const withheld = await advance(room, local, authorized);
1298
- if (withheld !== null)
1299
- return withhold(withheld, room, local, progress);
2144
+ const withheld = await advance(room, local, authorized, continuing);
2145
+ if (withheld !== null) {
2146
+ await withhold(withheld, room, local, progress);
2147
+ return 'done';
2148
+ }
1300
2149
  continue;
1301
2150
  }
1302
2151
  const kept = await adopted(room, local, head, result).finally(answered);
1303
2152
  if (!kept) {
1304
2153
  progress.complete = false;
1305
- return;
2154
+ return 'done';
1306
2155
  }
1307
2156
  }
2157
+ return 'done';
2158
+ }
2159
+ function pastHorizon(identity) {
2160
+ return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
2161
+ }
2162
+ async function discardPending(room, local, signal) {
2163
+ const { locator } = room;
2164
+ const discarded = await exclusive(async () => {
2165
+ if (fences.has(locator))
2166
+ return false;
2167
+ const before = replicas.get(locator);
2168
+ const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
2169
+ fences.set(locator, { at: discoveries, erased: false });
2170
+ lastFenced.set(locator, ++fencings);
2171
+ try {
2172
+ await local.discard(locator);
2173
+ }
2174
+ finally {
2175
+ fences.delete(locator);
2176
+ }
2177
+ for (const batch of queue)
2178
+ pendings.delete(batch.id);
2179
+ queuesWithoutReplica.delete(locator);
2180
+ const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
2181
+ if (after !== null)
2182
+ replicas.set(locator, after);
2183
+ journal.record(`room discarded ${locator}`);
2184
+ publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
2185
+ moveTo({ ...status, ...attention() });
2186
+ return true;
2187
+ });
2188
+ if (!discarded)
2189
+ return;
2190
+ const progress = { complete: true, reachable: true };
2191
+ await roomTask(locator, async (live) => {
2192
+ await Promise.race([
2193
+ reload(room, local, progress, () => live() && !signal.aborted),
2194
+ abandonedOn(signal),
2195
+ ]);
2196
+ });
2197
+ if (signal.aborted)
2198
+ return;
2199
+ await rest(progress);
1308
2200
  }
1309
2201
  function awaitingAnswer(locator) {
1310
2202
  let release = () => { };
1311
2203
  const awaited = new Promise((resolve) => {
1312
2204
  release = resolve;
1313
2205
  });
1314
- answering.set(locator, awaited);
2206
+ answering.set(locator, { generation, answered: awaited });
1315
2207
  return () => {
1316
- if (answering.get(locator) === awaited)
2208
+ if (answering.get(locator)?.answered === awaited)
1317
2209
  answering.delete(locator);
1318
2210
  release();
1319
2211
  };
@@ -1357,11 +2249,7 @@ export function composeRuntime(options) {
1357
2249
  const read = reclassified.kind === 'deferred'
1358
2250
  ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
1359
2251
  : reclassified;
1360
- await local.write({
1361
- next: record,
1362
- previous: held.confirmed.record,
1363
- queue: held.queue,
1364
- });
2252
+ await local.write({ next: record, previous: held.confirmed.record });
1365
2253
  if (read.kind === 'staged') {
1366
2254
  replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1367
2255
  if (declarationRefusals.delete(room.locator)) {
@@ -1429,24 +2317,27 @@ export function composeRuntime(options) {
1429
2317
  roomVersion: record.roomVersion,
1430
2318
  documents: record.documents,
1431
2319
  revisions: record.revisions,
1432
- attachments: record.attachments,
1433
2320
  },
1434
2321
  ];
1435
2322
  });
1436
2323
  }
1437
- async function synchronize(listed, local, progress, attempt) {
1438
- const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1439
- const ready = listed.filter((_, index) => {
2324
+ async function synchronize(listed, local, progress, attempt, live) {
2325
+ const acquired = listed.filter(acquires);
2326
+ const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
2327
+ if (!live())
2328
+ return;
2329
+ const ready = acquired.filter((_, index) => {
1440
2330
  const outcome = prepared[index];
1441
2331
  return outcome?.status === 'fulfilled' && outcome.value;
1442
2332
  });
1443
2333
  if (attempt === 'opened') {
1444
- options.adapter.roomStream?.hold(heldRooms(ready));
2334
+ options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
1445
2335
  handed = true;
1446
2336
  }
2337
+ discovering?.release();
1447
2338
  const loaded = await Promise.allSettled(ready
1448
2339
  .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1449
- .map((room) => loadRoom(room, local, progress)));
2340
+ .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
1450
2341
  const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1451
2342
  if (failed !== undefined)
1452
2343
  throw failed.reason;
@@ -1475,14 +2366,14 @@ export function composeRuntime(options) {
1475
2366
  }
1476
2367
  return true;
1477
2368
  }
1478
- async function caughtUp(room, local, progress) {
2369
+ async function caughtUp(room, local, progress, live) {
1479
2370
  const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1480
- if (held === undefined || authorization === undefined)
2371
+ if (held === undefined || authorization === undefined || !live())
1481
2372
  return;
1482
2373
  const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1483
2374
  if (!behind)
1484
2375
  return;
1485
- const withheld = await advance(room, local, authorization);
2376
+ const withheld = await advance(room, local, authorization, live);
1486
2377
  if (withheld !== null)
1487
2378
  await withhold(withheld, room, local, progress);
1488
2379
  }
@@ -1492,39 +2383,46 @@ export function composeRuntime(options) {
1492
2383
  return model.status === 'unavailable' && model.reason === 'revoked';
1493
2384
  });
1494
2385
  }
1495
- async function loadRoom(room, local, progress) {
1496
- if (discoveryPaused || authorization === undefined)
2386
+ async function loadRoom(room, local, progress, live) {
2387
+ if (!live() || authorization === undefined)
1497
2388
  return;
1498
2389
  if (replicas.has(room.locator) && revoked(room.locator)) {
1499
- await reload(room, local, progress);
2390
+ await reload(room, local, progress, live);
1500
2391
  return;
1501
2392
  }
1502
- const withheld = await advance(room, local, authorization);
2393
+ const withheld = await advance(room, local, authorization, live);
1503
2394
  if (withheld !== null) {
1504
2395
  await withhold(withheld, room, local, progress);
1505
2396
  return;
1506
2397
  }
1507
- await caughtUp(room, local, progress);
2398
+ await caughtUp(room, local, progress, live);
1508
2399
  }
1509
- async function reload(room, local, progress) {
1510
- if (discoveryPaused || authorization === undefined)
1511
- return;
1512
- const bootstrapped = await pullBootstrap(authorization, room, local);
1513
- if (!continuing())
1514
- return;
1515
- if (bootstrapped.kind !== 'staged') {
1516
- await withhold(bootstrapped, room, local, progress);
1517
- return;
2400
+ async function reload(room, local, progress, live) {
2401
+ try {
2402
+ if (!live() || authorization === undefined)
2403
+ return;
2404
+ const bootstrapped = await pullBootstrap(authorization, room, local, live);
2405
+ if (!live())
2406
+ return;
2407
+ if (bootstrapped.kind !== 'staged') {
2408
+ await withhold(bootstrapped, room, local, progress);
2409
+ return;
2410
+ }
2411
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
2412
+ await caughtUp(room, local, progress, live);
2413
+ }
2414
+ finally {
2415
+ caughtUpTo(room.locator);
1518
2416
  }
1519
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1520
- await caughtUp(room, local, progress);
1521
2417
  }
1522
- async function discoverRooms(local, progress, attempt) {
2418
+ async function discoverRooms(local, progress, attempt, live) {
1523
2419
  if (closed || authorization === undefined) {
1524
2420
  notifyReady();
1525
2421
  return;
1526
2422
  }
1527
2423
  moveTo(transferring(status));
2424
+ discoveries += 1;
2425
+ const started = discoveries;
1528
2426
  let result;
1529
2427
  try {
1530
2428
  result = await options.adapter.discover(authorization);
@@ -1532,7 +2430,8 @@ export function composeRuntime(options) {
1532
2430
  catch {
1533
2431
  result = { kind: 'transport-uncertainty' };
1534
2432
  }
1535
- if (closed) {
2433
+ if (!live()) {
2434
+ progress.complete = false;
1536
2435
  notifyReady();
1537
2436
  return;
1538
2437
  }
@@ -1590,6 +2489,7 @@ export function composeRuntime(options) {
1590
2489
  }
1591
2490
  const previousCapabilities = capabilities;
1592
2491
  capabilities = result.capabilities;
2492
+ outcomeRetention = result.retention.outcome;
1593
2493
  notifyReady();
1594
2494
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1595
2495
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
@@ -1602,8 +2502,7 @@ export function composeRuntime(options) {
1602
2502
  member = signedIn;
1603
2503
  emitMember();
1604
2504
  }
1605
- await applyDiscovery(result.rooms, local);
1606
- await synchronize(result.rooms, local, progress, attempt);
2505
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
1607
2506
  }
1608
2507
  async function ensureAuthorization() {
1609
2508
  if (authorization !== undefined)
@@ -1617,20 +2516,50 @@ export function composeRuntime(options) {
1617
2516
  throw error;
1618
2517
  }
1619
2518
  }
1620
- function task(work) {
2519
+ async function admittedRead(work) {
2520
+ if (roomReads.generation !== generation) {
2521
+ roomReads = { generation, free: ROOM_READS, waiting: [] };
2522
+ }
2523
+ const reads = roomReads;
2524
+ while (reads.free === 0)
2525
+ await new Promise((admit) => reads.waiting.push(admit));
2526
+ reads.free -= 1;
2527
+ try {
2528
+ await work();
2529
+ }
2530
+ finally {
2531
+ reads.free += 1;
2532
+ reads.waiting.shift()?.();
2533
+ }
2534
+ }
2535
+ function roomTask(locator, work) {
2536
+ return task(roomLane(locator), (live) => admittedRead(() => work(live)));
2537
+ }
2538
+ function task(lane, work) {
1621
2539
  if (stopping)
1622
- return chain;
1623
- chain = chain.then(work, work).catch(() => undefined);
1624
- return chain;
2540
+ return Promise.resolve();
2541
+ const held = lanes.get(lane);
2542
+ const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
2543
+ const turn = current.generation;
2544
+ const live = () => turn === generation && continuing();
2545
+ const run = current.chain.then(() => work(live)).catch(() => undefined);
2546
+ current.chain = run;
2547
+ lanes.set(lane, current);
2548
+ working.add(run);
2549
+ void run.finally(() => working.delete(run));
2550
+ return run;
1625
2551
  }
1626
2552
  function sync(attempt) {
1627
- return task(async () => {
1628
- if (closed || stopping || discoveryPaused) {
2553
+ return task(OPENING_LANE, async (live) => {
2554
+ if (!live()) {
1629
2555
  notifyReady();
1630
2556
  return;
1631
2557
  }
1632
2558
  const progress = { complete: true, reachable: true };
1633
- underway = attempt === 'opened' ? progress : null;
2559
+ const gate = discover();
2560
+ if (attempt === 'opened')
2561
+ opened = generation;
2562
+ underway = attempt === 'opened' ? { generation, progress } : null;
1634
2563
  if (attempt === 'opened')
1635
2564
  streamed = progress;
1636
2565
  complete = false;
@@ -1639,12 +2568,12 @@ export function composeRuntime(options) {
1639
2568
  handed = false;
1640
2569
  try {
1641
2570
  const local = await openLocal();
1642
- if (closed)
2571
+ if (!live())
1643
2572
  return;
1644
2573
  await ensureAuthorization();
1645
- if (closed)
2574
+ if (!live())
1646
2575
  return;
1647
- await discoverRooms(local, progress, attempt);
2576
+ await discoverRooms(local, progress, attempt, live);
1648
2577
  }
1649
2578
  catch {
1650
2579
  progress.complete = false;
@@ -1652,17 +2581,21 @@ export function composeRuntime(options) {
1652
2581
  progress.reachable = false;
1653
2582
  }
1654
2583
  finally {
2584
+ gate.release();
1655
2585
  notifyReady();
1656
2586
  }
2587
+ if (!live())
2588
+ return;
1657
2589
  if (attempt !== 'opened') {
1658
- await finishOpening(progress);
2590
+ await finishOpening(progress, live);
1659
2591
  return;
1660
2592
  }
1661
2593
  if (handed || closed || progress.reachable)
1662
2594
  return;
1663
2595
  underway = null;
2596
+ opened = null;
1664
2597
  journal.record('connection reopen');
1665
- await rest(progress);
2598
+ await rest(progress, live);
1666
2599
  options.adapter.roomStream?.reopen();
1667
2600
  });
1668
2601
  }
@@ -1683,24 +2616,41 @@ export function composeRuntime(options) {
1683
2616
  });
1684
2617
  }
1685
2618
  function applyOpening(expired) {
1686
- return task(async () => {
1687
- const progress = underway;
2619
+ const progress = underway?.generation === generation ? underway.progress : null;
2620
+ const cycle = progress ?? { complete: true, reachable: true };
2621
+ const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
2622
+ if (opened === generation)
2623
+ opened = null;
2624
+ return task(OPENING_LANE, async (live) => {
1688
2625
  underway = null;
1689
- if (closed || stopping)
2626
+ if (!live())
1690
2627
  return;
1691
2628
  if (progress === null) {
1692
- await reloadExpired(expired);
2629
+ await replacing;
2630
+ await rest(cycle, live);
1693
2631
  return;
1694
2632
  }
2633
+ await roomsCarried(expired);
2634
+ if (!live())
2635
+ return;
2636
+ const replaced = new Set(owed);
2637
+ owed.clear();
1695
2638
  try {
1696
2639
  const local = await openLocal();
1697
2640
  const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1698
- const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1699
- const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
2641
+ const reloading = held.filter((room) => !expired.includes(room.locator) &&
2642
+ (replaced.has(room.locator) || revoked(room.locator)));
2643
+ for (const room of reloading)
2644
+ catching.add(room.locator);
2645
+ readSide.recover();
2646
+ const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
2647
+ const reloaded = [...(await reloads), ...(await replacing)];
1700
2648
  if (progress.reachable) {
1701
- for (const room of held)
1702
- if (!reloading.includes(room))
2649
+ for (const room of held) {
2650
+ if (!expired.includes(room.locator) && !reloading.includes(room)) {
1703
2651
  await confirmModels(room, local);
2652
+ }
2653
+ }
1704
2654
  }
1705
2655
  const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1706
2656
  if (failed !== undefined)
@@ -1710,21 +2660,21 @@ export function composeRuntime(options) {
1710
2660
  progress.complete = false;
1711
2661
  progress.reachable = false;
1712
2662
  }
1713
- await finishOpening(progress);
2663
+ if (live())
2664
+ await finishOpening(progress, live);
1714
2665
  });
1715
2666
  }
1716
- async function reloadExpired(expired) {
1717
- const progress = { complete: true, reachable: true };
2667
+ async function reloadNow(replaced, progress) {
1718
2668
  try {
1719
2669
  const local = await openLocal();
1720
- await Promise.allSettled([...discovered.values()]
1721
- .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1722
- .map((room) => reload(room, local, progress)));
2670
+ return await Promise.allSettled([...discovered.values()]
2671
+ .filter((room) => replicas.has(room.locator) && replaced(room))
2672
+ .map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
1723
2673
  }
1724
2674
  catch {
1725
2675
  progress.reachable = false;
2676
+ return [];
1726
2677
  }
1727
- await rest(progress);
1728
2678
  }
1729
2679
  async function confirmModels(room, local) {
1730
2680
  for (const attached of ['collection', 'typed-selection']) {
@@ -1737,38 +2687,94 @@ export function composeRuntime(options) {
1737
2687
  function submitLater(locator) {
1738
2688
  if (localSaves)
1739
2689
  return;
1740
- const running = submitting.get(locator);
1741
- if (running !== undefined) {
1742
- running.again = true;
2690
+ void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
2691
+ }
2692
+ async function submitPass(locator, signal) {
2693
+ const room = discovered.get(locator);
2694
+ if (closed || stopping || discoveryPaused || room === undefined)
2695
+ return 'done';
2696
+ const progress = { complete: true, reachable: true };
2697
+ let next;
2698
+ try {
2699
+ await ensureAuthorization();
2700
+ if (authorization === undefined)
2701
+ return 'done';
2702
+ next = await submitRoom(room, await openLocal(), authorization, progress, signal);
2703
+ }
2704
+ catch {
2705
+ progress.reachable = false;
2706
+ next = 'again';
2707
+ }
2708
+ if (signal.aborted)
2709
+ return 'done';
2710
+ await rest(progress);
2711
+ return next === 'again' && !socketOpen ? 'done' : next;
2712
+ }
2713
+ function repairLater(locator, when) {
2714
+ if (!socketOpen || repairing.has(locator))
1743
2715
  return;
2716
+ void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
2717
+ }
2718
+ async function repairPass(locator, signal) {
2719
+ const room = discovered.get(locator);
2720
+ if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
2721
+ return 'done';
2722
+ if (room === undefined || room.replica.admission !== 'admitted')
2723
+ return 'done';
2724
+ const progress = { complete: true, reachable: true };
2725
+ repairing.add(locator);
2726
+ try {
2727
+ const local = await openLocal();
2728
+ await roomTask(locator, async (live) => {
2729
+ const current = () => live() && !signal.aborted;
2730
+ if (!current() || authorization === undefined)
2731
+ return;
2732
+ const withheld = await Promise.race([
2733
+ advance(room, local, authorization, current),
2734
+ abandonedOn(signal),
2735
+ ]);
2736
+ if (withheld !== null && current())
2737
+ await withhold(withheld, room, local, progress);
2738
+ });
1744
2739
  }
1745
- const run = { again: false, done: Promise.resolve() };
1746
- submitting.set(locator, run);
1747
- run.done = (async () => {
1748
- try {
1749
- do {
1750
- run.again = false;
1751
- const room = discovered.get(locator);
1752
- if (closed || stopping || discoveryPaused || room === undefined)
1753
- return;
1754
- const progress = { complete: true, reachable: true };
1755
- try {
1756
- await ensureAuthorization();
1757
- if (authorization === undefined)
1758
- return;
1759
- await submitRoom(room, await openLocal(), authorization, progress);
1760
- }
1761
- catch {
1762
- progress.reachable = false;
1763
- }
1764
- armTimer();
1765
- await rest(progress);
1766
- } while (run.again);
1767
- }
1768
- finally {
1769
- submitting.delete(locator);
1770
- }
1771
- })();
2740
+ catch {
2741
+ stale.add(locator);
2742
+ progress.reachable = false;
2743
+ }
2744
+ finally {
2745
+ repairing.delete(locator);
2746
+ }
2747
+ if (signal.aborted)
2748
+ return 'done';
2749
+ await rest(progress);
2750
+ if (!stale.has(locator)) {
2751
+ if ((replicas.get(locator)?.queue.length ?? 0) > 0)
2752
+ submitLater(locator);
2753
+ return 'done';
2754
+ }
2755
+ if (!repairable.has(locator))
2756
+ return 'done';
2757
+ const reset = repairable.get(locator);
2758
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
2759
+ }
2760
+ function resendLater() {
2761
+ if (!socketOpen)
2762
+ return;
2763
+ void recovery.attempt('resend', resendPass, { again: true });
2764
+ }
2765
+ async function resendPass(signal) {
2766
+ if (!socketOpen || closed || stopping || discoveryPaused)
2767
+ return 'done';
2768
+ const reading = { complete: true, reachable: true };
2769
+ try {
2770
+ await conversations.resend(reading);
2771
+ }
2772
+ catch {
2773
+ reading.reachable = false;
2774
+ }
2775
+ if (signal.aborted)
2776
+ return 'done';
2777
+ return reading.reachable && reading.complete ? 'done' : 'again';
1772
2778
  }
1773
2779
  function submitAll() {
1774
2780
  for (const [locator, replica] of replicas) {
@@ -1776,31 +2782,41 @@ export function composeRuntime(options) {
1776
2782
  submitLater(locator);
1777
2783
  }
1778
2784
  }
1779
- async function finishOpening(progress) {
2785
+ async function finishOpening(progress, live) {
1780
2786
  try {
1781
- if (closed || discoveryPaused) {
2787
+ if (!live()) {
1782
2788
  progress.complete = false;
1783
2789
  return;
1784
2790
  }
1785
2791
  submitAll();
2792
+ for (const locator of stale)
2793
+ if (repairable.has(locator))
2794
+ repairLater(locator, {});
1786
2795
  await conversations.reconcile();
1787
2796
  if (status.providerUnreachable)
1788
2797
  conversations.reconnected();
1789
2798
  const reading = { complete: true, reachable: true };
1790
- await conversations.catchUp(reading);
1791
- if (!reading.reachable)
2799
+ if (demand.conversations)
2800
+ await conversations.catchUp(reading);
2801
+ if (progress.reachable && reading.reachable)
2802
+ await conversations.resend(reading);
2803
+ if (!reading.reachable) {
1792
2804
  progress.reachable = false;
2805
+ resendLater();
2806
+ }
1793
2807
  }
1794
2808
  catch {
1795
2809
  progress.complete = false;
1796
2810
  }
1797
2811
  finally {
1798
- complete = progress.complete;
1799
- await rest(progress);
2812
+ if (live()) {
2813
+ complete = progress.complete;
2814
+ await rest(progress, live);
2815
+ }
1800
2816
  }
1801
2817
  }
1802
- async function rest(progress) {
1803
- if (closed)
2818
+ async function rest(progress, live = () => !closed) {
2819
+ if (!live())
1804
2820
  return;
1805
2821
  let lastCompleted = status.lastCompleted;
1806
2822
  let synced = complete && stale.size === 0 && !pending();
@@ -1808,6 +2824,8 @@ export function composeRuntime(options) {
1808
2824
  const completedAt = new Date().toISOString();
1809
2825
  try {
1810
2826
  await (await openLocal()).complete(completedAt);
2827
+ if (!live())
2828
+ return;
1811
2829
  recorded = true;
1812
2830
  lastCompleted = completedAt;
1813
2831
  }
@@ -1822,40 +2840,68 @@ export function composeRuntime(options) {
1822
2840
  ...attention(),
1823
2841
  });
1824
2842
  }
2843
+ function discover() {
2844
+ let release = () => { };
2845
+ const done = new Promise((resolve) => {
2846
+ release = resolve;
2847
+ });
2848
+ discovering = { generation, done, release };
2849
+ return discovering;
2850
+ }
1825
2851
  function applyRoom(locator, roomVersion) {
1826
2852
  announced.set(locator, roomVersion);
1827
- return task(async () => {
1828
- await answering.get(locator);
1829
- if (closed || stopping || discoveryPaused || authorization === undefined)
2853
+ return task(roomLane(locator), async (live) => {
2854
+ if (discovering?.generation === generation)
2855
+ await discovering.done;
2856
+ const saving = answering.get(locator);
2857
+ if (saving?.generation === generation)
2858
+ await saving.answered;
2859
+ if (!live() || authorization === undefined)
1830
2860
  return;
1831
2861
  const room = discovered.get(locator);
1832
2862
  const held = replicas.get(locator);
1833
- if (room === undefined || room.replica.admission !== 'admitted')
2863
+ if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
1834
2864
  return;
1835
2865
  if (held !== undefined &&
1836
2866
  (held.confirmed.record.replica.selection !== room.replica.selection ||
1837
2867
  options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
1838
2868
  return;
1839
2869
  }
1840
- const during = underway;
2870
+ const during = underway?.generation === generation ? underway.progress : null;
1841
2871
  const progress = during ?? { complete: true, reachable: true };
1842
2872
  if (during === null)
1843
2873
  moveTo(transferring(status));
1844
2874
  try {
1845
2875
  const local = await openLocal();
1846
- const withheld = await advance(room, local, authorization);
1847
- if (withheld !== null)
1848
- await withhold(withheld, room, local, progress);
2876
+ await admittedRead(async () => {
2877
+ if (!live() || authorization === undefined)
2878
+ return;
2879
+ const withheld = await advance(room, local, authorization, live);
2880
+ if (withheld !== null)
2881
+ await withhold(withheld, room, local, progress);
2882
+ });
1849
2883
  }
1850
2884
  catch {
1851
2885
  stale.add(locator);
1852
2886
  }
1853
2887
  finally {
1854
2888
  if (during === null)
1855
- await rest(progress);
2889
+ await rest(progress, live);
1856
2890
  }
1857
2891
  });
1858
2892
  }
2893
+ function roomsCarried(replacing) {
2894
+ const replaced = new Set(replacing.map(roomLane));
2895
+ return Promise.all([...lanes]
2896
+ .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
2897
+ .map(([, held]) => held.chain));
2898
+ }
2899
+ const receipts = {
2900
+ heads: async () => (await openLocal()).receivedHeads(),
2901
+ parts: async (key, count) => (await openLocal()).receivedParts(key, count),
2902
+ keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
2903
+ drop: async (keys) => (await openLocal()).dropReceived(keys),
2904
+ };
1859
2905
  function observeStream() {
1860
2906
  const stream = options.adapter.roomStream;
1861
2907
  if (stream === undefined || withdrawStream !== undefined)
@@ -1865,6 +2911,9 @@ export function composeRuntime(options) {
1865
2911
  return;
1866
2912
  if (event.kind === 'opened') {
1867
2913
  socketOpen = true;
2914
+ generation += 1;
2915
+ conversations.reopened();
2916
+ recovery.resume();
1868
2917
  void sync('opened');
1869
2918
  }
1870
2919
  else if (event.kind === 'closed')
@@ -1873,7 +2922,7 @@ export function composeRuntime(options) {
1873
2922
  void applyOpening(event.expired);
1874
2923
  else
1875
2924
  void applyRoom(event.locator, event.roomVersion);
1876
- });
2925
+ }, receipts);
1877
2926
  }
1878
2927
  function writesHeld() {
1879
2928
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -1888,6 +2937,7 @@ export function composeRuntime(options) {
1888
2937
  haltedRefusal = false;
1889
2938
  if (mode !== serviceMode) {
1890
2939
  serviceMode = mode;
2940
+ queueReadiness();
1891
2941
  journal.record(`service mode ${mode}`);
1892
2942
  for (const listener of serviceModeListeners)
1893
2943
  listener(mode);
@@ -1900,6 +2950,8 @@ export function composeRuntime(options) {
1900
2950
  if (stream === undefined || withdrawConversations !== undefined)
1901
2951
  return;
1902
2952
  withdrawConversations = stream.observe((event) => {
2953
+ if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
2954
+ queueReadiness();
1903
2955
  if (conversations.receive(event)) {
1904
2956
  void ready.then(scheduleCurrency);
1905
2957
  return;
@@ -1911,17 +2963,18 @@ export function composeRuntime(options) {
1911
2963
  });
1912
2964
  }
1913
2965
  function scheduleCurrency() {
1914
- if (currencyQueued || closed || stopping)
2966
+ if (currencyQueued === generation || closed || stopping)
1915
2967
  return;
1916
- currencyQueued = true;
1917
- void task(runCurrency);
2968
+ currencyQueued = generation;
2969
+ void task(CONVERSATION_LANE, runCurrency);
1918
2970
  }
1919
- async function runCurrency() {
1920
- currencyQueued = false;
1921
- if (closed || stopping || discoveryPaused)
2971
+ async function runCurrency(live) {
2972
+ if (currencyQueued === generation)
2973
+ currencyQueued = null;
2974
+ if (!live())
1922
2975
  return;
1923
2976
  await conversations.reconcile();
1924
- if (conversations.current()) {
2977
+ if (conversations.current() || !demand.conversations) {
1925
2978
  show();
1926
2979
  return;
1927
2980
  }
@@ -1933,19 +2986,24 @@ export function composeRuntime(options) {
1933
2986
  catch {
1934
2987
  progress.reachable = false;
1935
2988
  }
1936
- await rest(progress);
2989
+ await rest(progress, live);
1937
2990
  }
1938
2991
  function close() {
1939
2992
  if (closed)
1940
2993
  return;
1941
2994
  closed = true;
2995
+ if (narrowing !== null)
2996
+ clearTimeout(narrowing);
2997
+ if (rechecking !== null)
2998
+ clearInterval(rechecking);
2999
+ readinessWatches.clear();
1942
3000
  withdrawStream?.();
1943
3001
  withdrawStream = undefined;
1944
3002
  withdrawConversations?.();
1945
3003
  withdrawConversations = undefined;
1946
3004
  withdrawServiceMode?.();
1947
3005
  conversations.close();
1948
- stopTimer();
3006
+ void recovery.retire();
1949
3007
  store?.close();
1950
3008
  void opening?.then((opened) => {
1951
3009
  opened.close();
@@ -1963,9 +3021,8 @@ export function composeRuntime(options) {
1963
3021
  }
1964
3022
  async function stop() {
1965
3023
  stopping = true;
1966
- stopTimer();
1967
- await chain.catch(() => undefined);
1968
- await Promise.all([...submitting.values()].map((run) => run.done));
3024
+ await recovery.retire();
3025
+ await Promise.all(working);
1969
3026
  await mutations;
1970
3027
  close();
1971
3028
  }
@@ -1987,17 +3044,54 @@ export function composeRuntime(options) {
1987
3044
  return planOf(input, roomFacts(reach), planReach(reach));
1988
3045
  }
1989
3046
  function watchPlan(plan, deliver) {
1990
- materialize(reachedRooms(plan));
1991
- return readSide.watch(plan, identityOf(plan), deliver);
3047
+ return readSide.watch(plan, identityOf(plan), () => {
3048
+ const cover = covered(plan);
3049
+ awaitPreparation(cover);
3050
+ return cover;
3051
+ }, deliver);
1992
3052
  }
1993
3053
  async function searched(input, reach) {
1994
3054
  const plan = searchPlanOf(input);
1995
- const selected = plan.documents === undefined && reach === null
1996
- ? null
1997
- : (await rowsQuery(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
1998
- if (selected === null)
1999
- materialize(null);
2000
- return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
3055
+ const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
3056
+ for (;;) {
3057
+ const cover = covered(filter);
3058
+ awaitPreparation(cover);
3059
+ const pending = 'pending' in cover ? [...cover.pending] : [];
3060
+ const covering = 'pending' in cover ? [] : cover.coverage.rooms;
3061
+ const unindexed = covering
3062
+ .filter(({ room }) => !searchIndex.rooms().has(room))
3063
+ .map(({ room }) => room);
3064
+ for (const room of unindexed)
3065
+ pending.push({ room, awaits: 'index' });
3066
+ const held = conversations.searchable();
3067
+ const named = reach !== null && !reach.conversations
3068
+ ? []
3069
+ : (plan.conversations ?? held).filter((one) => held.includes(one) && conversations.acquired(one));
3070
+ const waiting = named.filter((one) => !searchIndex.conversations().has(one));
3071
+ for (const conversation of waiting)
3072
+ pending.push({ conversation, awaits: 'index' });
3073
+ if (unindexed.length > 0 || waiting.length > 0)
3074
+ index(unindexed, waiting);
3075
+ if (pending.length > 0 || 'pending' in cover)
3076
+ return { pending };
3077
+ const seen = searchIndex.changes();
3078
+ await searchIndex.caughtUp();
3079
+ const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(filter);
3080
+ if (selected !== null && 'pending' in selected)
3081
+ return selected;
3082
+ if (searchIndex.changes() !== seen ||
3083
+ JSON.stringify(covered(filter)) !== JSON.stringify(cover))
3084
+ continue;
3085
+ return {
3086
+ ...searchIndex.search(plan, selected === null ? null : selected.documents, named),
3087
+ coverage: {
3088
+ rooms: covering,
3089
+ conversations: named,
3090
+ index: searchIndex.generation(),
3091
+ overlay: cover.coverage.overlay,
3092
+ },
3093
+ };
3094
+ }
2001
3095
  }
2002
3096
  const watch = (topic, input, deliver) => {
2003
3097
  if (topic === 'room-drafts') {
@@ -2022,6 +3116,8 @@ export function composeRuntime(options) {
2022
3116
  }
2023
3117
  if (topic === 'conversations')
2024
3118
  return conversations.watchList(deliver);
3119
+ if (topic === 'agents')
3120
+ return conversations.watchAgents(deliver);
2025
3121
  if (topic === 'presentation') {
2026
3122
  const listener = {
2027
3123
  app: input[0],
@@ -2041,6 +3137,9 @@ export function composeRuntime(options) {
2041
3137
  presentationListeners.delete(listener);
2042
3138
  };
2043
3139
  }
3140
+ if (topic === 'readiness') {
3141
+ return watchReadiness({ target: '', code: 'stored', capacity: null }, null, deliver);
3142
+ }
2044
3143
  if (topic === 'rooms') {
2045
3144
  roomListeners.add(deliver);
2046
3145
  deliver(listed());
@@ -2135,11 +3234,14 @@ export function composeRuntime(options) {
2135
3234
  if ((current?.revision ?? null) !== revision) {
2136
3235
  return staleWrite(current === undefined ? null : roomDraftOf(current));
2137
3236
  }
3237
+ const file = parts.file === undefined
3238
+ ? undefined
3239
+ : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
2138
3240
  const record = {
2139
3241
  locator: room,
2140
3242
  path,
2141
3243
  revision: roomDraftRevision(),
2142
- parts: held(current?.parts, parts),
3244
+ parts: held(current?.parts, parts, file),
2143
3245
  returned: current?.returned ?? null,
2144
3246
  };
2145
3247
  await local.keepRoomDraft(record);
@@ -2188,6 +3290,10 @@ export function composeRuntime(options) {
2188
3290
  history(locator, path, query) {
2189
3291
  return readHistory(locator, path, query);
2190
3292
  },
3293
+ binaryFile: {
3294
+ read: readBinaryPath,
3295
+ readReference: readBinaryReference,
3296
+ },
2191
3297
  call(name, input) {
2192
3298
  return invokeAction(name, input);
2193
3299
  },
@@ -2200,30 +3306,55 @@ export function composeRuntime(options) {
2200
3306
  return journal.export();
2201
3307
  },
2202
3308
  persistence() {
2203
- return options.shell.persistentStorage;
3309
+ return persistence === 'granted';
3310
+ },
3311
+ async retry() {
3312
+ journal.record('retry');
3313
+ unindexable.clear();
3314
+ queueReadiness();
3315
+ resume();
2204
3316
  },
2205
3317
  observe: observing(watch),
2206
3318
  async reauthorize() {
2207
3319
  if (refusal === null || !refusal.reauthorizationOffered)
2208
3320
  return;
2209
3321
  journal.record('connection reauthorize');
2210
- await chain.catch(() => undefined);
3322
+ await Promise.all([...lanes.values()]
3323
+ .filter((held) => held.generation === generation)
3324
+ .map((held) => held.chain));
2211
3325
  await mutations;
2212
3326
  authorization = undefined;
2213
3327
  discoveryPaused = false;
2214
3328
  if (options.adapter.roomStream === undefined)
2215
3329
  await sync('unstreamed');
2216
3330
  else
2217
- await task(ensureAuthorization);
3331
+ await task(OPENING_LANE, ensureAuthorization);
2218
3332
  },
2219
3333
  };
2220
3334
  const reachable = {
2221
3335
  md,
2222
3336
  watch,
3337
+ referrerScope: (referrer) => options.adapter.referrerScope(referrer),
2223
3338
  plan: planned,
2224
3339
  query: (plan) => rowsQuery(plan),
2225
3340
  watchPlan,
2226
3341
  search: searched,
3342
+ searching,
3343
+ watchReadiness,
3344
+ };
3345
+ return {
3346
+ md,
3347
+ stop,
3348
+ lose: loseRooms,
3349
+ watch,
3350
+ acquire,
3351
+ persisted(answer) {
3352
+ if (answer === persistence)
3353
+ return;
3354
+ persistence = answer;
3355
+ journal.record(`persistence ${answer}`);
3356
+ queueReadiness();
3357
+ },
3358
+ within: (reach, port) => narrowed(reachable, reach, port),
2227
3359
  };
2228
- return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2229
3360
  }