@agentshouse/mdruntime 0.1.0-cacb4c3f6734e → 0.1.0-cb0eb0d87ff49

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 (187) hide show
  1. package/AGENTS.md +164 -99
  2. package/README.md +13 -3
  3. package/dist/adapter.d.ts +72 -16
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/attached-model.d.ts.map +1 -1
  6. package/dist/attached-model.js +15 -2
  7. package/dist/attempt.d.ts +16 -0
  8. package/dist/attempt.d.ts.map +1 -0
  9. package/dist/attempt.js +70 -0
  10. package/dist/binary.d.ts +23 -0
  11. package/dist/binary.d.ts.map +1 -0
  12. package/dist/binary.js +8 -0
  13. package/dist/bootstrap.d.ts +17 -5
  14. package/dist/bootstrap.d.ts.map +1 -1
  15. package/dist/bootstrap.js +109 -37
  16. package/dist/change-set.d.ts +2 -0
  17. package/dist/change-set.d.ts.map +1 -1
  18. package/dist/change-set.js +48 -16
  19. package/dist/change.d.ts +15 -0
  20. package/dist/change.d.ts.map +1 -1
  21. package/dist/change.js +26 -1
  22. package/dist/chat/acts.d.ts +33 -0
  23. package/dist/chat/acts.d.ts.map +1 -0
  24. package/dist/chat/acts.js +36 -0
  25. package/dist/chat/context.d.ts +3 -0
  26. package/dist/chat/context.d.ts.map +1 -0
  27. package/dist/chat/context.js +10 -0
  28. package/dist/chat/input.d.ts +9 -0
  29. package/dist/chat/input.d.ts.map +1 -0
  30. package/dist/chat/input.js +99 -0
  31. package/dist/chat/kit.d.ts +60 -0
  32. package/dist/chat/kit.d.ts.map +1 -0
  33. package/dist/chat/kit.js +123 -0
  34. package/dist/chat/lasting.d.ts +6 -0
  35. package/dist/chat/lasting.d.ts.map +1 -0
  36. package/dist/chat/lasting.js +19 -0
  37. package/dist/chat/list.d.ts +26 -0
  38. package/dist/chat/list.d.ts.map +1 -0
  39. package/dist/chat/list.js +62 -0
  40. package/dist/chat/markdown.d.ts +20 -0
  41. package/dist/chat/markdown.d.ts.map +1 -0
  42. package/dist/chat/markdown.js +188 -0
  43. package/dist/chat/message.d.ts +6 -0
  44. package/dist/chat/message.d.ts.map +1 -0
  45. package/dist/chat/message.js +192 -0
  46. package/dist/chat/messages.d.ts +21 -0
  47. package/dist/chat/messages.d.ts.map +1 -0
  48. package/dist/chat/messages.js +151 -0
  49. package/dist/chat/name.d.ts +6 -0
  50. package/dist/chat/name.d.ts.map +1 -0
  51. package/dist/chat/name.js +37 -0
  52. package/dist/chat/opened.d.ts +11 -0
  53. package/dist/chat/opened.d.ts.map +1 -0
  54. package/dist/chat/opened.js +17 -0
  55. package/dist/chat/overlay.d.ts +31 -0
  56. package/dist/chat/overlay.d.ts.map +1 -0
  57. package/dist/chat/overlay.js +97 -0
  58. package/dist/chat/parts.d.ts +57 -0
  59. package/dist/chat/parts.d.ts.map +1 -0
  60. package/dist/chat/parts.js +119 -0
  61. package/dist/chat/process.d.ts +20 -0
  62. package/dist/chat/process.d.ts.map +1 -0
  63. package/dist/chat/process.js +32 -0
  64. package/dist/chat/provider.d.ts +63 -0
  65. package/dist/chat/provider.d.ts.map +1 -0
  66. package/dist/chat/provider.js +620 -0
  67. package/dist/chat/question.d.ts +9 -0
  68. package/dist/chat/question.d.ts.map +1 -0
  69. package/dist/chat/question.js +80 -0
  70. package/dist/chat/questions.d.ts +31 -0
  71. package/dist/chat/questions.d.ts.map +1 -0
  72. package/dist/chat/questions.js +111 -0
  73. package/dist/chat/read.d.ts +5 -0
  74. package/dist/chat/read.d.ts.map +1 -0
  75. package/dist/chat/read.js +13 -0
  76. package/dist/chat/ready.d.ts +33 -0
  77. package/dist/chat/ready.d.ts.map +1 -0
  78. package/dist/chat/ready.js +91 -0
  79. package/dist/chat/style.d.ts +4 -0
  80. package/dist/chat/style.d.ts.map +1 -0
  81. package/dist/chat/style.js +302 -0
  82. package/dist/chat.d.ts +14 -0
  83. package/dist/chat.d.ts.map +1 -0
  84. package/dist/chat.js +8 -0
  85. package/dist/client.d.ts +20 -11
  86. package/dist/client.d.ts.map +1 -1
  87. package/dist/client.js +66 -16
  88. package/dist/collection.d.ts +21 -12
  89. package/dist/collection.d.ts.map +1 -1
  90. package/dist/collection.js +271 -23
  91. package/dist/connection.d.ts +29 -14
  92. package/dist/connection.d.ts.map +1 -1
  93. package/dist/connection.js +1262 -377
  94. package/dist/conversation.d.ts +23 -10
  95. package/dist/conversation.d.ts.map +1 -1
  96. package/dist/conversation.js +304 -136
  97. package/dist/entry.d.ts +17 -0
  98. package/dist/entry.d.ts.map +1 -0
  99. package/dist/entry.js +156 -0
  100. package/dist/expectation.d.ts +21 -0
  101. package/dist/expectation.d.ts.map +1 -0
  102. package/dist/expectation.js +70 -0
  103. package/dist/failure.d.ts +13 -0
  104. package/dist/failure.d.ts.map +1 -0
  105. package/dist/failure.js +19 -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 +4 -1
  110. package/dist/host.d.ts.map +1 -1
  111. package/dist/host.js +17 -12
  112. package/dist/identity.d.ts +5 -0
  113. package/dist/identity.d.ts.map +1 -1
  114. package/dist/identity.js +19 -1
  115. package/dist/index.d.ts +12 -6
  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 +70 -21
  121. package/dist/model.d.ts +11 -2
  122. package/dist/model.d.ts.map +1 -1
  123. package/dist/model.js +11 -0
  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/open.d.ts +2 -0
  128. package/dist/open.d.ts.map +1 -0
  129. package/dist/open.js +1 -0
  130. package/dist/outcome.d.ts +32 -3
  131. package/dist/outcome.d.ts.map +1 -1
  132. package/dist/outcome.js +42 -1
  133. package/dist/overlay.d.ts +15 -3
  134. package/dist/overlay.d.ts.map +1 -1
  135. package/dist/overlay.js +94 -77
  136. package/dist/query.d.ts +32 -3
  137. package/dist/query.d.ts.map +1 -1
  138. package/dist/query.js +5 -31
  139. package/dist/reach.d.ts +2 -0
  140. package/dist/reach.d.ts.map +1 -1
  141. package/dist/reach.js +45 -21
  142. package/dist/react.d.ts +26 -17
  143. package/dist/react.d.ts.map +1 -1
  144. package/dist/react.js +8 -6
  145. package/dist/read-side.d.ts +13 -6
  146. package/dist/read-side.d.ts.map +1 -1
  147. package/dist/read-side.js +291 -85
  148. package/dist/recovery.d.ts +25 -0
  149. package/dist/recovery.d.ts.map +1 -0
  150. package/dist/recovery.js +197 -0
  151. package/dist/refresh.d.ts +10 -0
  152. package/dist/refresh.d.ts.map +1 -1
  153. package/dist/refresh.js +43 -38
  154. package/dist/replica.d.ts +9 -6
  155. package/dist/replica.d.ts.map +1 -1
  156. package/dist/replica.js +116 -68
  157. package/dist/room-draft.d.ts +26 -9
  158. package/dist/room-draft.d.ts.map +1 -1
  159. package/dist/room-draft.js +132 -23
  160. package/dist/rows.d.ts +8 -13
  161. package/dist/rows.d.ts.map +1 -1
  162. package/dist/rows.js +12 -14
  163. package/dist/save.d.ts +31 -1
  164. package/dist/save.d.ts.map +1 -1
  165. package/dist/save.js +376 -45
  166. package/dist/search.d.ts +31 -8
  167. package/dist/search.d.ts.map +1 -1
  168. package/dist/search.js +109 -44
  169. package/dist/shared.d.ts +11 -0
  170. package/dist/shared.d.ts.map +1 -0
  171. package/dist/shared.js +368 -0
  172. package/dist/status.d.ts +2 -2
  173. package/dist/status.d.ts.map +1 -1
  174. package/dist/status.js +0 -8
  175. package/dist/store.d.ts +66 -17
  176. package/dist/store.d.ts.map +1 -1
  177. package/dist/store.js +506 -196
  178. package/dist/turn.d.ts +7 -0
  179. package/dist/turn.d.ts.map +1 -0
  180. package/dist/turn.js +45 -0
  181. package/dist/wire.d.ts +7 -3
  182. package/dist/wire.d.ts.map +1 -1
  183. package/dist/wire.js +15 -2
  184. package/package.json +26 -14
  185. package/dist/attachment.d.ts +0 -3
  186. package/dist/attachment.d.ts.map +0 -1
  187. package/dist/attachment.js +0 -8
@@ -3,19 +3,28 @@ import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
3
3
  import { observing } from './observation.js';
4
4
  import { filterOf, identityOf, planOf, } from './query.js';
5
5
  import { createReadSide } from './read-side.js';
6
+ import { markdownPath, roomOfKey } from './rows.js';
6
7
  import { createSearchIndex, searchPlanOf, } from './search.js';
7
8
  import { bootstrapRoom } from './bootstrap.js';
8
- import { changeSet, mergeChangeSets } from './change-set.js';
9
+ import { changeSet, entering, mergeChangeSets } from './change-set.js';
9
10
  import { createConversationStore, } from './conversation.js';
10
- import { parseCollection, validRoot } from './collection.js';
11
+ import { queuedFile } from './change.js';
12
+ import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
13
+ import { runtimeError } from './failure.js';
11
14
  import { createLog } from './log.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';
12
18
  import { refreshRoom } from './refresh.js';
13
- import { buildBatch } from './save.js';
14
- import { applyReturned, compositionOf, landed, readConfirmed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
15
- import { changedParts, changesOf, held, readParts, returnedInto, underneath, } from './room-draft.js';
16
- import { settled, same, saving, STARTING, transferring, } from './status.js';
19
+ import { buildBatch, joined, limitedIn, requeued } from './save.js';
20
+ import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
21
+ import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
22
+ import { same, saving, STARTING, transferring, } from './status.js';
17
23
  import { detectStorageLoss, nextGeneration } from './durability.js';
18
- import { roomDraftRevision } from './identity.js';
24
+ import { FencedMap } from './fence.js';
25
+ import { tally } from './shared.js';
26
+ import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
27
+ import { identityTime, roomDraftRevision } from './identity.js';
19
28
  import { beneath, normalize } from './paths.js';
20
29
  import { openRuntimeStore, } from './store.js';
21
30
  import { unconfirmed } from './transfer.js';
@@ -33,20 +42,59 @@ function declaredSelection(room) {
33
42
  root !== '' &&
34
43
  selection.roots.every((other, at) => at === index || !within(other, root)));
35
44
  }
45
+ const OPENING_LANE = 'opening';
46
+ const CONVERSATION_LANE = 'conversations';
47
+ const ROOM_READS = 256;
48
+ const ROOM_ATTEMPTS = 16;
49
+ const outside = new Map();
50
+ function outsideOf(roots) {
51
+ const key = JSON.stringify(roots);
52
+ const held = outside.get(key);
53
+ if (held !== undefined)
54
+ return held;
55
+ const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
56
+ const made = {
57
+ documents: (path) => (markdownPath(path) ? counted(path) : 0),
58
+ binaries: counted,
59
+ };
60
+ outside.set(key, made);
61
+ return made;
62
+ }
63
+ function abandonedOn(signal) {
64
+ return new Promise((resolve) => {
65
+ if (signal.aborted)
66
+ resolve(null);
67
+ else
68
+ signal.addEventListener('abort', () => resolve(null), { once: true });
69
+ });
70
+ }
71
+ function roomLane(locator) {
72
+ return `room ${locator}`;
73
+ }
36
74
  export function composeRuntime(options) {
75
+ const localSaves = options.adapter.localSaves === true;
37
76
  let authorization = options.authorization;
38
77
  let opening;
39
78
  let store;
40
- const replicas = new Map();
79
+ const fences = new Map();
80
+ const isFenced = (locator) => fences.has(locator);
81
+ let fencings = 0;
82
+ const lastFenced = new Map();
83
+ const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
84
+ let discoveries = 0;
85
+ const replicas = new FencedMap(isFenced);
86
+ const queuesWithoutReplica = new FencedMap(isFenced);
41
87
  const pendings = new Map();
42
- const roomDrafts = new Map();
88
+ const roomDrafts = new FencedMap(isFenced);
43
89
  const presentations = new Map();
44
90
  let limits = [];
45
91
  let capabilities = [];
46
- const rooms = new Map();
92
+ const rooms = new FencedMap(isFenced);
93
+ const counted = new Map();
47
94
  const unreadable = new Set();
48
- const models = new Map();
49
- const liveModels = new Map();
95
+ const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
96
+ const models = new FencedMap(isModelFenced);
97
+ const liveModels = new FencedMap(isModelFenced);
50
98
  const invalidModels = new Set();
51
99
  const discovered = new Map();
52
100
  let roomsListed = false;
@@ -76,18 +124,28 @@ export function composeRuntime(options) {
76
124
  let status = STARTING;
77
125
  let shown = STARTING;
78
126
  let discoveryPaused = false;
79
- let resetTimer;
80
127
  let closed = false;
81
128
  let stopping = false;
82
129
  let withdrawStream;
83
130
  const announced = new Map();
84
131
  let underway = null;
85
- const submitting = new Map();
132
+ let streamed = null;
133
+ let handed = false;
134
+ const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
135
+ const repairing = new Set();
136
+ const repairable = new Map();
137
+ let outcomeRetention = null;
86
138
  const answering = new Map();
87
139
  let complete = false;
88
140
  let recorded = false;
89
141
  const stale = new Set();
90
- let chain = Promise.resolve();
142
+ let generation = 0;
143
+ let discovering = null;
144
+ let opened = null;
145
+ const owed = new Set();
146
+ const lanes = new Map();
147
+ let roomReads = { generation, free: ROOM_READS, waiting: [] };
148
+ const working = new Set();
91
149
  let mutations = Promise.resolve();
92
150
  const roomListeners = new Set();
93
151
  const refusalListeners = new Set();
@@ -98,27 +156,39 @@ export function composeRuntime(options) {
98
156
  const presentationListeners = new Set();
99
157
  const readSide = createReadSide();
100
158
  let withdrawConversations;
101
- let currencyQueued = false;
159
+ let currencyQueued = null;
102
160
  let socketOpen = false;
103
161
  let serviceMode = 'open';
104
162
  let haltedRefusal = false;
105
163
  let openSignals = 0;
106
164
  const serviceModeListeners = new Set();
165
+ let member = null;
166
+ const memberListeners = new Set();
167
+ const prepared = new Set();
168
+ const projecting = new Set();
169
+ const demanded = new Set();
170
+ let preparing = false;
171
+ const catching = new Set();
172
+ const transfers = new FencedMap(isFenced);
107
173
  const conversations = createConversationStore({
108
174
  agents: declaredAgents,
109
- documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
175
+ documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
110
176
  call: (name, input) => invokeAction(name, input),
111
177
  durably,
112
178
  stream: () => options.adapter.conversations,
113
179
  publish: publishChangeSet,
180
+ online: () => !status.providerUnreachable && refusal === null,
114
181
  closed: () => closed,
182
+ unanswered: resendLater,
183
+ expired: (operation) => pastHorizon(operation),
115
184
  });
116
185
  const searchIndex = createSearchIndex({
117
- documents: () => readSide.documents(),
186
+ room: (room) => readSide.room(room),
118
187
  document: (room, path) => readSide.document(room, path),
119
- messages: () => conversations.messages(),
120
- message: (conversation, identity) => conversations.message(conversation, identity),
121
188
  });
189
+ const roomsToIndex = new Set();
190
+ const conversationsToIndex = new Set();
191
+ let indexing = false;
122
192
  const journal = createLog();
123
193
  let notifyReady = () => { };
124
194
  const ready = new Promise((resolve) => {
@@ -182,7 +252,9 @@ export function composeRuntime(options) {
182
252
  }
183
253
  }
184
254
  function pending() {
185
- return [...replicas.values()].some((replica) => replica.queue.length > 0);
255
+ return (!localSaves &&
256
+ (queuesWithoutReplica.size > 0 ||
257
+ [...replicas.values()].some((replica) => replica.queue.length > 0)));
186
258
  }
187
259
  function attention() {
188
260
  const attentionRequired = [];
@@ -211,8 +283,21 @@ export function composeRuntime(options) {
211
283
  }
212
284
  return { attentionRequired, reset };
213
285
  }
286
+ function documentsIn(locator) {
287
+ const replica = replicas.get(locator);
288
+ if (replica === undefined)
289
+ return 0;
290
+ const roots = replica.confirmed.record.typedSelection?.roots ?? [];
291
+ const weighed = outsideOf(roots);
292
+ return (tally(replica.visible.documents, weighed.documents) +
293
+ tally(replica.visible.binaries, weighed.binaries));
294
+ }
214
295
  function listed() {
215
- return structuredClone([...rooms.values()]);
296
+ return structuredClone([...rooms.values()].map((room) => {
297
+ const documents = documentsIn(room.locator);
298
+ counted.set(room.locator, documents);
299
+ return { ...room, documents };
300
+ }));
216
301
  }
217
302
  function roomFacts(reach = null) {
218
303
  const held = [...rooms.values()]
@@ -232,6 +317,7 @@ export function composeRuntime(options) {
232
317
  return { listed: roomsListed, rooms: held };
233
318
  }
234
319
  function emitRooms() {
320
+ readSide.recover();
235
321
  const value = listed();
236
322
  for (const listener of roomListeners)
237
323
  listener(value);
@@ -244,6 +330,10 @@ export function composeRuntime(options) {
244
330
  for (const listener of storageLossListeners)
245
331
  listener(storageLoss);
246
332
  }
333
+ function emitMember() {
334
+ for (const listener of memberListeners)
335
+ listener(member);
336
+ }
247
337
  function moveTo(next) {
248
338
  if (closed)
249
339
  return;
@@ -263,6 +353,8 @@ export function composeRuntime(options) {
263
353
  listener(shown);
264
354
  }
265
355
  function entryOf(batch) {
356
+ if (localSaves)
357
+ return Object.freeze({ batch, state: 'local' });
266
358
  const state = pendings.get(batch.id);
267
359
  if (state?.accepted !== undefined) {
268
360
  return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
@@ -270,10 +362,7 @@ export function composeRuntime(options) {
270
362
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
271
363
  }
272
364
  function entriesOf(locator) {
273
- const replica = replicas.get(locator);
274
- if (replica === undefined)
275
- return [];
276
- return replica.queue.map(entryOf);
365
+ return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
277
366
  }
278
367
  function emitQueue(locator) {
279
368
  const entries = entriesOf(locator);
@@ -283,12 +372,191 @@ export function composeRuntime(options) {
283
372
  }
284
373
  }
285
374
  function applyChangeSet(set) {
286
- readSide.apply(set);
287
- searchIndex.changed(set);
375
+ const held = {
376
+ ...set,
377
+ documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
378
+ };
379
+ readSide.apply(held);
380
+ searchIndex.changed(held);
381
+ }
382
+ async function referenced(locator) {
383
+ for (;;) {
384
+ const record = replicas.get(locator)?.confirmed.record;
385
+ if (record === undefined || closed)
386
+ return;
387
+ const held = { text: record.documents, binaries: record.binaries };
388
+ const composition = compositionOf(record);
389
+ if (referencesIndexed(held, composition))
390
+ return;
391
+ await indexReferrers(held, composition);
392
+ }
393
+ }
394
+ async function prepareReplica(locator) {
395
+ await referenced(locator);
396
+ const replica = replicas.get(locator);
397
+ if (replica === undefined || closed || prepared.has(locator))
398
+ return;
399
+ const visible = replica.visible;
400
+ const paths = [
401
+ ...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
402
+ ];
403
+ const entered = [];
404
+ projecting.add(locator);
405
+ try {
406
+ await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
407
+ const current = replicas.get(locator);
408
+ if (current === undefined || closed)
409
+ return;
410
+ const set = entering(locator, current, chunk);
411
+ readSide.apply(set);
412
+ entered.push(set);
413
+ });
414
+ if (!replicas.has(locator) || closed)
415
+ return;
416
+ prepared.add(locator);
417
+ }
418
+ finally {
419
+ projecting.delete(locator);
420
+ }
421
+ conversations.changed(mergeChangeSets(entered));
422
+ readSide.recover();
423
+ }
424
+ function unprepared() {
425
+ return [...demanded, ...replicas.keys()].find((locator) => !prepared.has(locator));
426
+ }
427
+ function prepare(locators = []) {
428
+ for (const locator of locators)
429
+ demanded.add(locator);
430
+ if (preparing)
431
+ return;
432
+ preparing = true;
433
+ void (async () => {
434
+ try {
435
+ for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
436
+ await yielded();
437
+ if (closed)
438
+ return;
439
+ demanded.delete(waiting);
440
+ await prepareReplica(waiting);
441
+ }
442
+ }
443
+ finally {
444
+ preparing = false;
445
+ }
446
+ })();
447
+ }
448
+ function reachedRooms(plan) {
449
+ const reach = plan.reach;
450
+ const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
451
+ const named = plan.rooms ??
452
+ (reached === null
453
+ ? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
454
+ : [...reached]);
455
+ return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
456
+ }
457
+ function awaited(locator) {
458
+ return rooms.has(locator) || !roomsListed;
459
+ }
460
+ function covered(plan) {
461
+ const locators = reachedRooms(plan);
462
+ if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
463
+ return { pending: [] };
464
+ }
465
+ const pending = [];
466
+ const coverage = [];
467
+ const overlay = [];
468
+ for (const locator of locators) {
469
+ const replica = replicas.get(locator);
470
+ if (replica === undefined) {
471
+ if (awaited(locator))
472
+ pending.push({ room: locator, awaits: 'acquisition' });
473
+ continue;
474
+ }
475
+ if (!prepared.has(locator)) {
476
+ pending.push({ room: locator, awaits: 'preparation' });
477
+ continue;
478
+ }
479
+ coverage.push({
480
+ room: locator,
481
+ generation: replica.confirmed.record.transfer,
482
+ current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
483
+ });
484
+ for (const batch of replica.queue)
485
+ overlay.push(batch.id);
486
+ }
487
+ if (pending.length > 0)
488
+ return { pending };
489
+ return { coverage: { rooms: coverage, indexes: [], overlay } };
490
+ }
491
+ function awaitPreparation(cover) {
492
+ if (!('pending' in cover))
493
+ return;
494
+ const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
495
+ if (waiting.length > 0)
496
+ prepare(waiting.map(({ room }) => room));
497
+ }
498
+ async function rowsQuery(plan) {
499
+ const before = covered(plan);
500
+ awaitPreparation(before);
501
+ if ('pending' in before)
502
+ return before;
503
+ const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
504
+ awaitPreparation(cover);
505
+ if ('pending' in cover)
506
+ return cover;
507
+ return 'groups' in rows
508
+ ? { groups: rows.groups, coverage: cover.coverage }
509
+ : { documents: rows.documents, coverage: cover.coverage };
510
+ }
511
+ async function indexConversation(conversation) {
512
+ searchIndex.indexMessages(conversation, [], false);
513
+ let after = null;
514
+ for (;;) {
515
+ const batch = await durably((local) => local.messagesAfter(conversation, after));
516
+ if (closed)
517
+ return;
518
+ searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
519
+ if (batch.length === 0)
520
+ return;
521
+ after = batch[batch.length - 1].identity;
522
+ }
523
+ }
524
+ function indexNext() {
525
+ const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator));
526
+ if (room !== undefined) {
527
+ roomsToIndex.delete(room);
528
+ return () => (prepared.has(room) ? searchIndex.indexRoom(room) : undefined);
529
+ }
530
+ const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
531
+ if (conversation === undefined)
532
+ return null;
533
+ conversationsToIndex.delete(conversation);
534
+ return () => indexConversation(conversation);
535
+ }
536
+ function index(rooms, named) {
537
+ for (const room of rooms)
538
+ roomsToIndex.add(room);
539
+ for (const conversation of named)
540
+ conversationsToIndex.add(conversation);
541
+ if (indexing)
542
+ return;
543
+ indexing = true;
544
+ void (async () => {
545
+ try {
546
+ for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
547
+ await yielded();
548
+ await unit();
549
+ }
550
+ }
551
+ finally {
552
+ indexing = false;
553
+ }
554
+ })().catch(() => { });
288
555
  }
289
556
  function publishChangeSet(set) {
290
557
  applyChangeSet(set);
291
- if (set.rooms.length > 0)
558
+ if (set.rooms.length > 0 ||
559
+ [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
292
560
  emitRooms();
293
561
  for (const locator of set.queues)
294
562
  emitQueue(locator);
@@ -300,34 +568,7 @@ export function composeRuntime(options) {
300
568
  emitPresentation(app);
301
569
  conversations.changed(set);
302
570
  }
303
- function stopTimer() {
304
- if (resetTimer === undefined)
305
- return;
306
- clearTimeout(resetTimer);
307
- resetTimer = undefined;
308
- }
309
- function armTimer() {
310
- stopTimer();
311
- if (closed || stopping)
312
- return;
313
- let earliest = null;
314
- for (const { submission } of pendings.values()) {
315
- if (submission.state !== 'reset')
316
- continue;
317
- const at = Date.parse(submission.reset);
318
- if (earliest === null || at < earliest)
319
- earliest = at;
320
- }
321
- if (earliest === null)
322
- return;
323
- // A Save House held back goes again only once the reset its answer named has passed.
324
- resetTimer = setTimeout(() => {
325
- resetTimer = undefined;
326
- submitAll();
327
- }, Math.max(0, earliest - Date.now()));
328
- }
329
571
  async function refuse(reason, provider = null) {
330
- stopTimer();
331
572
  discoveryPaused = true;
332
573
  try {
333
574
  if (reason === 'revoked-authorization') {
@@ -335,10 +576,7 @@ export function composeRuntime(options) {
335
576
  const revoked = { status: 'unavailable', reason: 'revoked' };
336
577
  try {
337
578
  const local = await openLocal();
338
- await local.revokeModels([...replicas.values()].map((replica) => ({
339
- record: replica.confirmed.record,
340
- queue: replica.queue,
341
- })));
579
+ await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
342
580
  }
343
581
  catch {
344
582
  journal.record('model revocation persistence failed');
@@ -362,10 +600,7 @@ export function composeRuntime(options) {
362
600
  model: revoked,
363
601
  selectionModel: revoked,
364
602
  };
365
- const read = readConfirmed(record, before.confirmed);
366
- replicas.set(locator, read.kind === 'staged'
367
- ? replicaOf(read.confirmed, before.queue, before)
368
- : { ...before, confirmed: { ...before.confirmed, record } });
603
+ replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
369
604
  }
370
605
  rooms.set(locator, {
371
606
  ...room,
@@ -382,7 +617,8 @@ export function composeRuntime(options) {
382
617
  }
383
618
  finally {
384
619
  refusal = {
385
- reason,
620
+ code: reason,
621
+ retryable: false,
386
622
  reauthorizationOffered: reason === 'revoked-authorization',
387
623
  refusal: provider,
388
624
  };
@@ -407,19 +643,28 @@ export function composeRuntime(options) {
407
643
  },
408
644
  providerOwnedPaths: room.providerOwnedPaths,
409
645
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
410
- attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
411
646
  synchronizationAvailable,
412
647
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
413
648
  });
414
649
  }
415
- async function applyDiscovery(listed, local) {
650
+ async function applyDiscovery(found, started) {
651
+ for (const room of found) {
652
+ const fence = fences.get(room.locator);
653
+ if (fence !== undefined && fence.erased && fence.at < started)
654
+ fences.delete(room.locator);
655
+ }
656
+ const listed = found.filter((room) => !fences.has(room.locator));
416
657
  const listedLocators = new Set(listed.map((room) => room.locator));
417
658
  for (const locator of listedLocators)
418
659
  scopedRefusals.delete(locator);
419
660
  discovered.clear();
420
661
  for (const room of listed)
421
662
  discovered.set(room.locator, room);
422
- await dropUnlisted(listedLocators, local);
663
+ const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
664
+ if (options.adapter.exhaustiveDiscovery === true)
665
+ await loseRooms(unlisted);
666
+ else
667
+ unlistedKept(unlisted);
423
668
  for (const room of listed) {
424
669
  const held = replicas.get(room.locator)?.confirmed.record;
425
670
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -430,43 +675,83 @@ export function composeRuntime(options) {
430
675
  }
431
676
  roomsListed = true;
432
677
  emitRooms();
678
+ return listed;
433
679
  }
434
680
  function unlistedHeld(listed) {
435
- return [...new Set([...rooms.keys(), ...roomDrafts.keys()])].filter((locator) => !listed.has(locator));
436
- }
437
- async function dropUnlisted(listed, local) {
438
- if (unlistedHeld(listed).length === 0)
439
- return;
440
- await exclusive(async () => {
441
- const unlisted = unlistedHeld(listed);
442
- await local.dropRooms(unlisted);
443
- const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
444
- room: true,
445
- queue: true,
446
- drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
447
- }));
448
- for (const locator of unlisted) {
449
- for (const batch of replicas.get(locator)?.queue ?? [])
450
- pendings.delete(batch.id);
451
- replicas.delete(locator);
681
+ return [
682
+ ...new Set([
683
+ ...rooms.keys(),
684
+ ...roomDrafts.keys(),
685
+ ...transfers.keys(),
686
+ ...queuesWithoutReplica.keys(),
687
+ ]),
688
+ ].filter((locator) => !listed.has(locator));
689
+ }
690
+ function unlistedKept(unlisted) {
691
+ for (const locator of unlisted) {
692
+ const held = replicas.get(locator)?.confirmed.record;
693
+ if (held !== undefined)
694
+ list(held, false);
695
+ else
452
696
  rooms.delete(locator);
453
- roomDrafts.delete(locator);
454
- unreadable.delete(locator);
455
- stale.delete(locator);
456
- scopedRefusals.delete(locator);
457
- sourceRefusals.delete(locator);
458
- declarationRefusals.delete(locator);
459
- for (const attached of ['collection', 'typed-selection']) {
460
- const key = modelKey(locator, attached);
461
- models.delete(key);
462
- liveModels.delete(key);
463
- invalidModels.delete(key);
464
- }
465
- journal.record(`room unlisted ${locator}`);
697
+ }
698
+ }
699
+ function fence(locators) {
700
+ const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
701
+ const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
702
+ room: true,
703
+ queue: true,
704
+ drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
705
+ }));
706
+ for (const locator of fresh) {
707
+ fences.set(locator, { at: discoveries, erased: false });
708
+ lastFenced.set(locator, ++fencings);
709
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
710
+ pendings.delete(batch.id);
711
+ replicas.delete(locator);
712
+ queuesWithoutReplica.delete(locator);
713
+ rooms.delete(locator);
714
+ roomDrafts.delete(locator);
715
+ transfers.delete(locator);
716
+ catching.delete(locator);
717
+ discovered.delete(locator);
718
+ announced.delete(locator);
719
+ unreadable.delete(locator);
720
+ stale.delete(locator);
721
+ repairable.delete(locator);
722
+ scopedRefusals.delete(locator);
723
+ sourceRefusals.delete(locator);
724
+ declarationRefusals.delete(locator);
725
+ for (const attached of ['collection', 'typed-selection']) {
726
+ const key = modelKey(locator, attached);
727
+ models.delete(key);
728
+ liveModels.delete(key);
729
+ invalidModels.delete(key);
466
730
  }
467
- publishChangeSet(mergeChangeSets(removed));
468
- moveTo({ ...status, ...attention() });
469
- });
731
+ journal.record(`room lost ${locator}`);
732
+ }
733
+ if (fresh.length === 0)
734
+ return fresh;
735
+ publishChangeSet(mergeChangeSets(removed));
736
+ for (const locator of fresh)
737
+ prepared.delete(locator);
738
+ searchIndex.forget(fresh);
739
+ readSide.recover();
740
+ moveTo({ ...status, ...attention() });
741
+ return fresh;
742
+ }
743
+ async function erased(local, locators) {
744
+ await local.erase(locators);
745
+ for (const locator of locators) {
746
+ const held = fences.get(locator);
747
+ if (held !== undefined)
748
+ held.erased = true;
749
+ }
750
+ }
751
+ async function loseRooms(locators) {
752
+ const fresh = fence(locators);
753
+ if (fresh.length > 0)
754
+ await durably((local) => erased(local, fresh));
470
755
  }
471
756
  function restored(key, persisted) {
472
757
  const prior = models.get(key);
@@ -487,7 +772,7 @@ export function composeRuntime(options) {
487
772
  return model;
488
773
  }
489
774
  async function hydrate(opened) {
490
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
775
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
491
776
  opened.all(),
492
777
  opened.queues(),
493
778
  opened.roomDrafts(),
@@ -497,9 +782,15 @@ export function composeRuntime(options) {
497
782
  opened.lastCompleted(),
498
783
  opened.providerLimits(),
499
784
  opened.capabilities(),
785
+ opened.member(),
786
+ opened.transfers(),
500
787
  ]);
788
+ transfers.clear();
789
+ for (const record of staged)
790
+ transfers.set(record.locator, record);
501
791
  limits = declared;
502
792
  capabilities = named;
793
+ member = known;
503
794
  roomDrafts.clear();
504
795
  for (const record of kept)
505
796
  rememberRoomDraft(record);
@@ -515,18 +806,17 @@ export function composeRuntime(options) {
515
806
  }
516
807
  const previousReplicas = new Map(replicas);
517
808
  replicas.clear();
809
+ queuesWithoutReplica.clear();
810
+ const replicated = new Set(records.map((record) => record.locator));
811
+ for (const [locator, queue] of queuedBatches) {
812
+ if (!replicated.has(locator))
813
+ queuesWithoutReplica.set(locator, queue);
814
+ }
518
815
  for (const record of records) {
519
816
  const model = restored(modelKey(record.locator, 'collection'), record.model);
520
817
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
521
818
  const prior = previousReplicas.get(record.locator);
522
- const retyped = { ...record, model, selectionModel };
523
- const read = readConfirmed(retyped, prior?.confirmed);
524
- const confirmed = read.kind === 'staged'
525
- ? read.confirmed
526
- : {
527
- record: retyped,
528
- collection: parseCollection(retyped.documents, compositionOf(retyped)),
529
- };
819
+ const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
530
820
  replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
531
821
  const existing = rooms.get(record.locator);
532
822
  if (existing === undefined)
@@ -535,7 +825,6 @@ export function composeRuntime(options) {
535
825
  rooms.set(record.locator, {
536
826
  ...existing,
537
827
  roomVersion: record.roomVersion,
538
- attachments: record.attachments,
539
828
  operations: record.operations,
540
829
  replica: record.replica,
541
830
  model: modelTypes(model),
@@ -547,10 +836,6 @@ export function composeRuntime(options) {
547
836
  },
548
837
  });
549
838
  }
550
- if (read.kind === 'refused') {
551
- refusedSource(record, read);
552
- declarationRefusals.add(record.locator);
553
- }
554
839
  }
555
840
  return {
556
841
  lastCompleted,
@@ -561,16 +846,26 @@ export function composeRuntime(options) {
561
846
  async function readState(opened) {
562
847
  const { lastCompleted, locators, read } = await hydrate(opened);
563
848
  applyChangeSet(read);
849
+ prepare();
564
850
  if (replicas.size > 0)
565
851
  emitRooms();
852
+ if (member !== null)
853
+ emitMember();
566
854
  for (const locator of locators)
567
855
  emitQueue(locator);
568
856
  publishRoomDrafts(false);
569
857
  return lastCompleted;
570
858
  }
571
859
  async function open() {
572
- const opened = await openRuntimeStore();
860
+ if (closed)
861
+ throw new Error('runtime closed');
862
+ const opened = await openRuntimeStore(isFenced);
573
863
  store = opened;
864
+ const interrupted = await opened.fences();
865
+ for (const locator of interrupted) {
866
+ fences.set(locator, { at: discoveries, erased: false });
867
+ lastFenced.set(locator, ++fencings);
868
+ }
574
869
  const kept = options.shell.generation;
575
870
  const storedGeneration = await opened.generation();
576
871
  const loss = detectStorageLoss(storedGeneration, kept);
@@ -584,6 +879,8 @@ export function composeRuntime(options) {
584
879
  emitStorageLoss();
585
880
  }
586
881
  const lastCompleted = await readState(opened);
882
+ if (interrupted.length > 0)
883
+ await erased(opened, interrupted);
587
884
  moveTo({ ...status, lastCompleted, ...attention() });
588
885
  return opened;
589
886
  }
@@ -610,10 +907,15 @@ export function composeRuntime(options) {
610
907
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
611
908
  }
612
909
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
613
- const withdrawn = withdrawals(confirmed, queue, withdrawable);
910
+ const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
911
+ const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
614
912
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
913
+ const unqueued = withdrawn.map((batch) => ({
914
+ batch,
915
+ returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
916
+ }));
615
917
  const returning = [
616
- ...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
918
+ ...[...unqueued].reverse(),
617
919
  ...(settling.leaving === undefined ? [] : [settling.leaving]),
618
920
  ];
619
921
  const written = new Map();
@@ -631,27 +933,48 @@ export function composeRuntime(options) {
631
933
  });
632
934
  }
633
935
  }
634
- await local.write({
635
- next: confirmed.record,
636
- previous: before?.confirmed.record ?? null,
637
- queue: kept,
638
- previousQueue: before?.queue ?? [],
639
- roomDrafts: [...written.values()],
640
- });
641
- for (const batch of before?.queue ?? []) {
936
+ await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
937
+ for (const batch of previousQueue) {
642
938
  if (!kept.includes(batch))
643
939
  pendings.delete(batch.id);
644
940
  }
645
941
  for (const record of written.values())
646
942
  rememberRoomDraft(record);
647
- for (const batch of withdrawn)
648
- journal.record(`outcome ${locator} ${batch.id} withdrawn`);
649
- replicas.set(locator, replicaOf(confirmed, kept, before));
650
- return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
651
- room: settling.room,
652
- queue: kept.length !== (before?.queue.length ?? 0),
653
- drafts: [...written.keys()],
654
- });
943
+ try {
944
+ await local.write({
945
+ next: confirmed.record,
946
+ previous: before?.confirmed.record ?? null,
947
+ ...(settling.stored === undefined ? {} : { stored: settling.stored }),
948
+ });
949
+ }
950
+ catch (error) {
951
+ const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
952
+ if (held !== null)
953
+ replicas.set(locator, held);
954
+ else if (kept.length > 0)
955
+ queuesWithoutReplica.set(locator, kept);
956
+ else
957
+ queuesWithoutReplica.delete(locator);
958
+ publishChangeSet(changeSet(locator, before ?? null, held, {
959
+ queue: kept.length !== previousQueue.length,
960
+ drafts: [...written.keys()],
961
+ }));
962
+ moveTo({ ...status, ...attention() });
963
+ throw error;
964
+ }
965
+ return () => {
966
+ queuesWithoutReplica.delete(locator);
967
+ for (const { batch, returned } of unqueued) {
968
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
969
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
970
+ }
971
+ replicas.set(locator, replicaOf(confirmed, kept, before));
972
+ return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
973
+ room: settling.room,
974
+ queue: kept.length !== previousQueue.length,
975
+ drafts: [...written.keys()],
976
+ });
977
+ };
655
978
  }
656
979
  function adoptModels(locator, record) {
657
980
  for (const [attached, model] of [
@@ -684,10 +1007,16 @@ export function composeRuntime(options) {
684
1007
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
685
1008
  };
686
1009
  }
687
- function publish(local, room, confirmed, base) {
1010
+ function publish(local, room, confirmed, landing, live) {
1011
+ const { base, stored } = landing;
688
1012
  return exclusive(async () => {
689
- if (!continuing())
1013
+ if (stored !== undefined) {
1014
+ await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
1015
+ }
1016
+ if (!live()) {
1017
+ caughtUpTo(room.locator);
690
1018
  return true;
1019
+ }
691
1020
  const prior = replicas.get(room.locator);
692
1021
  if (base !== undefined &&
693
1022
  (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
@@ -695,13 +1024,17 @@ export function composeRuntime(options) {
695
1024
  return false;
696
1025
  }
697
1026
  adoptModels(room.locator, confirmed.record);
698
- const queue = prior?.queue ?? [];
1027
+ const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
699
1028
  const roomVersion = confirmed.record.roomVersion;
700
1029
  const accepted = queue.filter((batch) => {
701
1030
  const accepted = pendings.get(batch.id)?.accepted;
702
1031
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
703
1032
  });
704
- const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
1033
+ const set = (await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) }))();
1034
+ if (stored !== undefined) {
1035
+ transfers.delete(room.locator);
1036
+ catching.delete(room.locator);
1037
+ }
705
1038
  for (const batch of accepted)
706
1039
  pendings.delete(batch.id);
707
1040
  stale.delete(room.locator);
@@ -710,32 +1043,54 @@ export function composeRuntime(options) {
710
1043
  sourceRefusals.delete(room.locator);
711
1044
  declarationRefusals.delete(room.locator);
712
1045
  list(room, true);
1046
+ if (prior === undefined)
1047
+ prepared.add(room.locator);
713
1048
  publishChangeSet(set);
714
1049
  moveTo({ ...status, ...attention() });
1050
+ options.adapter.roomStream?.published(room.locator, roomVersion);
715
1051
  return true;
716
1052
  });
717
1053
  }
718
1054
  async function saveRoomDrafts(locator, named, writtenBy) {
719
1055
  const local = await openLocal();
1056
+ await referenced(locator);
720
1057
  const replica = replicas.get(locator);
721
1058
  const room = rooms.get(locator);
722
1059
  if (replica === undefined || room === undefined) {
723
- throw new Error(`No Local Replica for ${locator}`);
1060
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
724
1061
  }
725
1062
  const saved = [];
726
1063
  for (const { path, revision } of named) {
727
1064
  const record = roomDrafts.get(locator)?.get(path);
728
1065
  if (record === undefined || record.revision !== revision) {
729
- return { outcome: 'refused', rule: 'stale', file: path };
1066
+ return { outcome: 'refused', code: 'stale', retryable: false, file: path };
730
1067
  }
731
1068
  if (!saved.some((draft) => draft.path === path))
732
1069
  saved.push({ path, parts: record.parts });
733
1070
  }
734
1071
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
735
1072
  if (changed.length > 0)
736
- return { outcome: 'refused', rule: 'changed-underneath', parts: changed };
1073
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
737
1074
  const parts = saved.map(({ path, parts }) => changesOf(path, parts));
738
- const built = buildBatch({
1075
+ const changes = parts.flatMap(({ changes }) => changes);
1076
+ const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
1077
+ if (requeue.kind === 'refused')
1078
+ return { outcome: 'refused', ...requeue.refusal };
1079
+ const foreign = [...requeue.withdrawn.keys()].some((origin) => {
1080
+ const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
1081
+ return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
1082
+ });
1083
+ if (foreign) {
1084
+ return {
1085
+ outcome: 'refused',
1086
+ code: 'unknown-revision',
1087
+ retryable: false,
1088
+ file: [...requeue.queued].at(-1),
1089
+ };
1090
+ }
1091
+ const withdrawing = requeue.withdrawn.size > 0;
1092
+ const input = {
1093
+ local: localSaves,
739
1094
  baseRoomVersion: replica.confirmed.record.roomVersion,
740
1095
  composition: compositionFor(replica.confirmed.record),
741
1096
  operations: room.operations,
@@ -743,29 +1098,93 @@ export function composeRuntime(options) {
743
1098
  providerLimits: limits,
744
1099
  handle: room.handle,
745
1100
  visible: replica.visible,
746
- changes: parts.flatMap(({ changes }) => changes),
1101
+ changes,
747
1102
  bases: parts.flatMap(({ bases }) => bases),
748
1103
  writtenBy,
1104
+ clean: {
1105
+ text: replica.confirmed.record.documents,
1106
+ binaries: replica.confirmed.record.binaries,
1107
+ composition: compositionOf(replica.confirmed.record),
1108
+ },
1109
+ };
1110
+ const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
1111
+ if (checked?.kind === 'refused')
1112
+ return { outcome: 'refused', ...checked.refusal };
1113
+ const owners = [...requeue.owners].flatMap((origin) => {
1114
+ const batch = requeue.withdrawn.get(origin);
1115
+ return batch === undefined || batch === null ? [] : [{ origin, batch }];
749
1116
  });
750
- if (built.kind === 'refused')
1117
+ if (owners.length > 1) {
1118
+ return {
1119
+ outcome: 'refused',
1120
+ code: 'unknown-revision',
1121
+ retryable: false,
1122
+ file: [...requeue.queued].at(-1),
1123
+ };
1124
+ }
1125
+ const built = !withdrawing
1126
+ ? buildBatch(input)
1127
+ : requeue.changes.length === 0
1128
+ ? null
1129
+ : buildBatch({
1130
+ ...input,
1131
+ visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
1132
+ changes: requeue.changes,
1133
+ });
1134
+ if (built?.kind === 'refused')
751
1135
  return { outcome: 'refused', ...built.refusal };
752
- const batches = [...replica.queue, built.batch];
1136
+ const [owner] = owners;
1137
+ const join = built === null || owner === undefined
1138
+ ? null
1139
+ : joined(requeue.queue, owner.batch, built.batch);
1140
+ if (join?.kind === 'refused')
1141
+ return { outcome: 'refused', ...join.refusal };
1142
+ const withdrawn = new Map(requeue.withdrawn);
1143
+ if (join !== null && owner !== undefined)
1144
+ withdrawn.set(owner.origin, join.batch);
1145
+ const drafted = new Map();
1146
+ for (const [origin, replaced] of withdrawn) {
1147
+ if (replaced === null)
1148
+ continue;
1149
+ const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
1150
+ drafted.set(replaced.id, replaced === join?.batch
1151
+ ? [
1152
+ ...carried.map((held) => joinedDraft(held, saved)),
1153
+ ...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
1154
+ ]
1155
+ : carried);
1156
+ }
1157
+ if (built !== null && join === null)
1158
+ drafted.set(built.batch.id, saved);
1159
+ const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
1160
+ for (const replaced of withdrawn.values()) {
1161
+ if (replaced === null || !batches.includes(replaced))
1162
+ continue;
1163
+ const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
1164
+ if (limited !== null)
1165
+ return { outcome: 'refused', ...limited };
1166
+ }
753
1167
  const consumed = saved.map(({ path }) => path);
754
1168
  try {
755
- await local.write({
756
- next: replica.confirmed.record,
757
- previous: replica.confirmed.record,
1169
+ await local.keepAuthored({
1170
+ locator,
758
1171
  queue: batches,
759
- previousQueue: replica.queue,
760
- saved,
1172
+ saved: drafted,
761
1173
  consumedRoomDrafts: consumed,
762
1174
  });
763
1175
  }
764
1176
  catch {
765
- return { outcome: 'refused', rule: 'durable-write' };
1177
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
766
1178
  }
767
- pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
768
- recorded = false;
1179
+ if (fences.has(locator))
1180
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1181
+ for (const origin of withdrawn.keys())
1182
+ pendings.delete(origin);
1183
+ for (const [id, drafts] of drafted) {
1184
+ pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
1185
+ }
1186
+ if (!localSaves)
1187
+ recorded = false;
769
1188
  for (const path of consumed)
770
1189
  forgetRoomDraft(locator, path);
771
1190
  replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
@@ -773,9 +1192,12 @@ export function composeRuntime(options) {
773
1192
  queue: true,
774
1193
  drafts: consumed,
775
1194
  }));
776
- moveTo({ ...saving(status), ...attention() });
1195
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
777
1196
  submitLater(locator);
778
- return { outcome: 'saved', identifier: built.batch.id };
1197
+ const identifier = join?.batch.id ??
1198
+ built?.batch.id ??
1199
+ [...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
1200
+ return { outcome: 'saved', identifier };
779
1201
  }
780
1202
  async function mark(local, locator, identifier, submission, lost) {
781
1203
  const state = pendings.get(identifier);
@@ -785,6 +1207,8 @@ export function composeRuntime(options) {
785
1207
  return;
786
1208
  }
787
1209
  await local.submit(locator, identifier, submission, lost);
1210
+ if (fences.has(locator))
1211
+ return;
788
1212
  pendings.set(identifier, { ...state, submission, lost });
789
1213
  const replica = replicas.get(locator);
790
1214
  if (replica !== undefined)
@@ -795,22 +1219,24 @@ export function composeRuntime(options) {
795
1219
  if (!continuing())
796
1220
  throw new Error('connection refused');
797
1221
  await local.keepUpload(locator, identifier, upload);
1222
+ if (fences.has(locator))
1223
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
798
1224
  pendings.set(identifier, { ...pendings.get(identifier), upload });
799
1225
  if (!continuing())
800
1226
  throw new Error('connection refused');
801
1227
  }
802
1228
  async function readHistory(locator, path, query = {}) {
803
1229
  if (closed || refusal !== null)
804
- return { kind: 'transport-uncertainty' };
1230
+ return UNCERTAIN;
805
1231
  try {
806
1232
  await ensureAuthorization();
807
1233
  }
808
1234
  catch {
809
- return { kind: 'transport-uncertainty' };
810
- }
811
- if (closed || authorization === undefined || refusal !== null) {
812
- return { kind: 'transport-uncertainty' };
1235
+ return UNCERTAIN;
813
1236
  }
1237
+ if (closed || authorization === undefined || refusal !== null)
1238
+ return UNCERTAIN;
1239
+ const since = fencings;
814
1240
  const transfer = ['transfer', 'history', locator, path];
815
1241
  if (query.revision !== undefined)
816
1242
  transfer.push(query.revision);
@@ -825,6 +1251,8 @@ export function composeRuntime(options) {
825
1251
  ...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
826
1252
  });
827
1253
  journal.record(`outcome history ${result.kind}`);
1254
+ if (fencedSince(locator, since))
1255
+ return failure({ kind: 'room-not-readable', locator });
828
1256
  if (result.kind === 'scoped-refusal') {
829
1257
  const room = replicas.get(result.locator)?.confirmed.record;
830
1258
  if (room !== undefined)
@@ -832,12 +1260,106 @@ export function composeRuntime(options) {
832
1260
  }
833
1261
  if (result.kind === 'revoked-authorization')
834
1262
  await refuse('revoked-authorization');
835
- return result;
1263
+ return historyAnswer(result);
836
1264
  }
837
1265
  catch {
838
1266
  journal.record('outcome history transport-uncertainty');
839
- return { kind: 'transport-uncertainty' };
1267
+ return UNCERTAIN;
1268
+ }
1269
+ }
1270
+ function queuedBlob(locator, path, localId) {
1271
+ for (const batch of replicas.get(locator)?.queue ?? []) {
1272
+ const change = batch.changes[queuedFile(batch, path, localId)];
1273
+ if (change?.kind === 'binary')
1274
+ return change.blob;
1275
+ }
1276
+ return null;
1277
+ }
1278
+ function binaryRoom(request) {
1279
+ if (request.kind === 'path')
1280
+ return request.locator;
1281
+ const scope = options.adapter.referrerScope(request.referrer);
1282
+ return scope?.kind === 'room' ? scope.locator : null;
1283
+ }
1284
+ async function readBinary(request) {
1285
+ if (closed || refusal !== null)
1286
+ return UNCERTAIN;
1287
+ try {
1288
+ await ensureAuthorization();
1289
+ }
1290
+ catch {
1291
+ return UNCERTAIN;
1292
+ }
1293
+ if (closed || authorization === undefined || refusal !== null)
1294
+ return UNCERTAIN;
1295
+ const since = fencings;
1296
+ journal.record(request.kind === 'path'
1297
+ ? `transfer binary ${request.locator} ${request.path}`
1298
+ : `transfer binary ${request.fileRef}`);
1299
+ try {
1300
+ const result = await options.adapter.binary(authorization, request);
1301
+ journal.record(`outcome binary ${result.kind}`);
1302
+ const locator = binaryRoom(request);
1303
+ if (locator !== null && fencedSince(locator, since))
1304
+ return failure({ kind: 'room-not-readable', locator });
1305
+ if (result.kind === 'scoped-refusal') {
1306
+ const room = replicas.get(result.locator)?.confirmed.record;
1307
+ if (room !== undefined)
1308
+ await scoped(result, room, await openLocal());
1309
+ }
1310
+ if (result.kind === 'revoked-authorization')
1311
+ await refuse('revoked-authorization');
1312
+ return binaryAnswer(result);
840
1313
  }
1314
+ catch {
1315
+ journal.record('outcome binary transport-uncertainty');
1316
+ return UNCERTAIN;
1317
+ }
1318
+ }
1319
+ async function readBinaryPath(locator, path, revision) {
1320
+ await ready;
1321
+ if (revision !== undefined)
1322
+ return readBinary({ kind: 'path', locator, path, revision });
1323
+ const visible = replicas.get(locator)?.visible;
1324
+ const held = visible?.binaries[path];
1325
+ if (visible === undefined || held === undefined)
1326
+ return absentBinary(path);
1327
+ if (held.state === 'pending') {
1328
+ const blob = queuedBlob(locator, path, held.localId);
1329
+ return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1330
+ }
1331
+ const record = binaryRecord(path, held, visible.revisions[path]);
1332
+ if (record.state !== 'accepted')
1333
+ return absentBinary(path);
1334
+ const replica = replicas.get(locator);
1335
+ const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
1336
+ const holds = (at) => {
1337
+ const kept = replica?.confirmed.record.binaries[at];
1338
+ return (kept?.state === 'accepted' &&
1339
+ kept.fileRef === record.fileRef &&
1340
+ replica?.confirmed.record.revisions[at] === record.revision);
1341
+ };
1342
+ let confirmed = path;
1343
+ for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
1344
+ const change = queued[index];
1345
+ if (change.kind === 'move' && change.to === confirmed)
1346
+ confirmed = change.path;
1347
+ }
1348
+ return readBinary({
1349
+ kind: 'path',
1350
+ locator,
1351
+ path: confirmed,
1352
+ revision: record.revision,
1353
+ fileRef: record.fileRef,
1354
+ });
1355
+ }
1356
+ async function readBinaryReference(reference) {
1357
+ await ready;
1358
+ return readBinary({
1359
+ kind: 'reference',
1360
+ fileRef: reference.fileRef,
1361
+ referrer: reference.referrer,
1362
+ });
841
1363
  }
842
1364
  function declaredAgents() {
843
1365
  for (const capability of capabilities) {
@@ -858,18 +1380,17 @@ export function composeRuntime(options) {
858
1380
  async function invokeAction(name, input) {
859
1381
  await ready;
860
1382
  if (closed || refusal !== null)
861
- return { kind: 'transport-uncertainty' };
1383
+ return UNCERTAIN;
862
1384
  if (declaredAction(name) === undefined)
863
- return { kind: 'undeclared' };
1385
+ return undeclared(name);
864
1386
  try {
865
1387
  await ensureAuthorization();
866
1388
  }
867
1389
  catch {
868
- return { kind: 'transport-uncertainty' };
869
- }
870
- if (closed || authorization === undefined || refusal !== null) {
871
- return { kind: 'transport-uncertainty' };
1390
+ return UNCERTAIN;
872
1391
  }
1392
+ if (closed || authorization === undefined || refusal !== null)
1393
+ return UNCERTAIN;
873
1394
  journal.record(`transfer action ${name}`);
874
1395
  try {
875
1396
  const result = await options.adapter.action(authorization, { name, input });
@@ -883,11 +1404,11 @@ export function composeRuntime(options) {
883
1404
  journal.record(`outcome action ${result.kind}`);
884
1405
  if (result.kind === 'rejected')
885
1406
  journal.record(`refusal rejected ${result.code}`);
886
- return result;
1407
+ return calledResult(result);
887
1408
  }
888
1409
  catch {
889
1410
  journal.record('outcome action transport-uncertainty');
890
- return { kind: 'transport-uncertainty' };
1411
+ return UNCERTAIN;
891
1412
  }
892
1413
  }
893
1414
  function continuing() {
@@ -904,11 +1425,11 @@ export function composeRuntime(options) {
904
1425
  options.adapter.reached(locator, written, roomVersion) &&
905
1426
  !options.adapter.reached(locator, roomVersion, written));
906
1427
  }
907
- async function pullRefresh(authorized, confirmed) {
908
- if (!continuing())
1428
+ async function pullRefresh(authorized, confirmed, live) {
1429
+ if (!live())
909
1430
  return unconfirmed;
910
1431
  try {
911
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
1432
+ const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
912
1433
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
913
1434
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
914
1435
  return result;
@@ -918,28 +1439,69 @@ export function composeRuntime(options) {
918
1439
  throw error;
919
1440
  }
920
1441
  }
921
- async function pullBootstrap(authorized, room) {
922
- if (!continuing())
1442
+ function staging(local, live) {
1443
+ return {
1444
+ held: (locator) => transfers.get(locator) ?? null,
1445
+ staged: (record) => local.staged(record),
1446
+ stage: (record, page) => exclusive(async () => {
1447
+ if (!live())
1448
+ return false;
1449
+ await local.stage(record, page);
1450
+ transfers.set(record.locator, record);
1451
+ readSide.recover();
1452
+ return true;
1453
+ }),
1454
+ abandon: (locator) => exclusive(async () => {
1455
+ if (!live())
1456
+ return;
1457
+ await local.abandon(locator);
1458
+ transfers.delete(locator);
1459
+ readSide.recover();
1460
+ }),
1461
+ };
1462
+ }
1463
+ function reusable(room) {
1464
+ const record = replicas.get(room.locator)?.confirmed.record;
1465
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1466
+ return null;
1467
+ return { documents: record.documents, revisions: record.revisions };
1468
+ }
1469
+ async function pullBootstrap(authorized, room, local, live) {
1470
+ if (!live())
923
1471
  return unconfirmed;
1472
+ if (replicas.has(room.locator)) {
1473
+ catching.add(room.locator);
1474
+ readSide.recover();
1475
+ }
924
1476
  try {
925
1477
  const result = await bootstrapRoom(options.adapter, authorized, room, {
926
1478
  collection: modelOf(room.locator),
927
1479
  selection: modelOf(room.locator, 'typed-selection'),
928
- }, (models, held) => remodel(room.locator, models, held), continuing);
1480
+ }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
929
1481
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
930
1482
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1483
+ if (result.kind !== 'staged')
1484
+ caughtUpTo(room.locator);
931
1485
  return result;
932
1486
  }
933
1487
  catch (error) {
934
1488
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
1489
+ caughtUpTo(room.locator);
935
1490
  throw error;
936
1491
  }
937
1492
  }
938
- async function advance(room, local, authorized) {
1493
+ function caughtUpTo(locator) {
1494
+ if (catching.delete(locator))
1495
+ readSide.recover();
1496
+ }
1497
+ async function advance(room, local, authorized, live) {
1498
+ await referenced(room.locator);
1499
+ if (!live())
1500
+ return unconfirmed;
939
1501
  const held = replicas.get(room.locator);
940
1502
  if (held !== undefined) {
941
- const refreshed = await pullRefresh(authorized, held.confirmed);
942
- if (!continuing())
1503
+ const refreshed = await pullRefresh(authorized, held.confirmed, live);
1504
+ if (!live())
943
1505
  return unconfirmed;
944
1506
  if (refreshed.kind === 'still-current') {
945
1507
  stale.delete(room.locator);
@@ -953,31 +1515,45 @@ export function composeRuntime(options) {
953
1515
  return null;
954
1516
  }
955
1517
  if (refreshed.kind === 'advanced') {
956
- if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
1518
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
957
1519
  const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
958
1520
  const reached = now !== undefined &&
959
1521
  options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
960
- return reached ? null : advance(room, local, authorized);
1522
+ return reached ? null : advance(room, local, authorized, live);
961
1523
  }
962
1524
  await retainModel(local, room, 'collection', modelOf(room.locator));
963
1525
  return null;
964
1526
  }
965
1527
  if (refreshed.kind !== 'beyond-horizon')
966
1528
  return refreshed;
1529
+ if (opened !== null) {
1530
+ owed.add(room.locator);
1531
+ readSide.recover();
1532
+ return unconfirmed;
1533
+ }
967
1534
  }
968
- const bootstrapped = await pullBootstrap(authorized, room);
969
- if (!continuing())
1535
+ const bootstrapped = await pullBootstrap(authorized, room, local, live);
1536
+ if (!live())
970
1537
  return unconfirmed;
971
1538
  if (bootstrapped.kind !== 'staged')
972
1539
  return bootstrapped;
973
- await publish(local, room, bootstrapped.confirmed);
1540
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
974
1541
  return null;
975
1542
  }
976
1543
  async function withhold(withheld, room, local, progress) {
1544
+ if (withheld.kind === 'unconfirmed')
1545
+ return;
977
1546
  if (withheld.kind === 'refused')
978
1547
  stale.delete(room.locator);
979
1548
  else
980
1549
  stale.add(room.locator);
1550
+ if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
1551
+ const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
1552
+ repairable.set(room.locator, reset);
1553
+ repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
1554
+ }
1555
+ else
1556
+ repairable.delete(room.locator);
981
1557
  if (withheld.kind === 'scoped-refusal') {
982
1558
  await scoped(withheld, room, local);
983
1559
  }
@@ -1002,6 +1578,7 @@ export function composeRuntime(options) {
1002
1578
  function refusedSource(room, { refusal }) {
1003
1579
  sourceRefusals.set(room.locator, {
1004
1580
  code: refusal.rule,
1581
+ retryable: false,
1005
1582
  fields: {
1006
1583
  file: refusal.file,
1007
1584
  ...(refusal.location === undefined ? {} : { location: refusal.location }),
@@ -1016,8 +1593,16 @@ export function composeRuntime(options) {
1016
1593
  }
1017
1594
  function leaving(batch, result) {
1018
1595
  if (result.kind === 'expired')
1019
- return { batch, returned: { kind: 'expired' } };
1020
- return { batch, returned: { kind: result.kind, code: result.code, fields: result.fields } };
1596
+ return { batch, returned: returnedBy('expired') };
1597
+ return {
1598
+ batch,
1599
+ returned: {
1600
+ kind: result.kind,
1601
+ code: result.code,
1602
+ fields: result.fields,
1603
+ retryable: result.retryable,
1604
+ },
1605
+ };
1021
1606
  }
1022
1607
  function logAccepted(locator, submitted, accepted) {
1023
1608
  journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
@@ -1025,6 +1610,7 @@ export function composeRuntime(options) {
1025
1610
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1026
1611
  }
1027
1612
  async function adoptAccepted(room, local, submitted, accepted) {
1613
+ await referenced(room.locator);
1028
1614
  if (!continuing())
1029
1615
  return unconfirmed;
1030
1616
  const current = replicas.get(room.locator);
@@ -1035,22 +1621,23 @@ export function composeRuntime(options) {
1035
1621
  logAccepted(room.locator, submitted, accepted);
1036
1622
  const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1037
1623
  ? { kind: 'staged', confirmed: current.confirmed }
1038
- : applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
1624
+ : applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
1039
1625
  ? accepted.roomVersion
1040
1626
  : held, accepted.roomVersion);
1041
1627
  if (applied.kind !== 'staged') {
1042
1628
  const state = pendings.get(submitted.id);
1043
1629
  await local.accept(room.locator, submitted.id, accepted.roomVersion);
1044
- if (state !== undefined)
1630
+ if (state !== undefined && !fences.has(room.locator))
1045
1631
  pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1046
1632
  emitQueue(room.locator);
1047
1633
  return applied;
1048
1634
  }
1049
- const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1635
+ const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1050
1636
  pendings.delete(submitted.id);
1051
1637
  unreadable.delete(room.locator);
1052
1638
  sourceRefusals.delete(room.locator);
1053
1639
  declarationRefusals.delete(room.locator);
1640
+ const set = settled();
1054
1641
  list(room, true);
1055
1642
  publishChangeSet(set);
1056
1643
  moveTo({ ...status, ...attention() });
@@ -1064,14 +1651,14 @@ export function composeRuntime(options) {
1064
1651
  refusedSource(room, failed);
1065
1652
  return false;
1066
1653
  }
1067
- async function submitRoom(room, local, authorized, progress) {
1068
- while (continuing() && authorization !== undefined) {
1654
+ async function submitRoom(room, local, authorized, progress, signal) {
1655
+ while (continuing() && authorization !== undefined && !signal.aborted) {
1069
1656
  const replica = replicas.get(room.locator);
1070
1657
  const head = replica?.queue[0];
1071
1658
  if (replica === undefined || head === undefined)
1072
- return;
1659
+ return 'done';
1073
1660
  if (pendings.get(head.id)?.accepted !== undefined)
1074
- return;
1661
+ return 'done';
1075
1662
  const state = pendings.get(head.id);
1076
1663
  const { submission, lost: earlier } = state ?? {
1077
1664
  submission: { state: 'waiting' },
@@ -1079,24 +1666,30 @@ export function composeRuntime(options) {
1079
1666
  };
1080
1667
  if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
1081
1668
  progress.complete = false;
1082
- return;
1669
+ return { at: Date.parse(submission.reset) };
1083
1670
  }
1084
1671
  if (writesHeld()) {
1085
1672
  progress.complete = false;
1086
1673
  journal.record(`submission ${room.locator} ${head.id} held`);
1087
- return;
1674
+ return 'done';
1088
1675
  }
1089
1676
  const lost = earlier || submission.state === 'submitted';
1677
+ if (lost && pastHorizon(head.id)) {
1678
+ await discardPending(room, local, signal);
1679
+ return 'done';
1680
+ }
1090
1681
  let result;
1091
1682
  journal.record(`submission ${room.locator} ${head.id}`);
1092
1683
  await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1093
- if (discoveryPaused)
1094
- return;
1095
- if (writesHeld()) {
1684
+ if (discoveryPaused || fences.has(room.locator))
1685
+ return 'done';
1686
+ if (signal.aborted || writesHeld()) {
1096
1687
  await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
1688
+ if (signal.aborted)
1689
+ return 'done';
1097
1690
  progress.complete = false;
1098
1691
  journal.record(`submission ${room.locator} ${head.id} held`);
1099
- return;
1692
+ return 'done';
1100
1693
  }
1101
1694
  const opensBefore = openSignals;
1102
1695
  const answered = awaitingAnswer(room.locator);
@@ -1112,32 +1705,36 @@ export function composeRuntime(options) {
1112
1705
  progress.complete = false;
1113
1706
  if (continuing())
1114
1707
  progress.reachable = false;
1115
- return;
1708
+ return 'again';
1709
+ }
1710
+ if (signal.aborted) {
1711
+ answered();
1712
+ return 'done';
1116
1713
  }
1117
1714
  if (result.kind !== 'accepted' || !continuing())
1118
1715
  answered();
1119
1716
  if (!continuing())
1120
- return;
1717
+ return 'done';
1121
1718
  if (result.kind === 'transport-uncertainty') {
1122
1719
  progress.complete = false;
1123
1720
  progress.reachable = false;
1124
- return;
1721
+ return 'again';
1125
1722
  }
1126
1723
  if (result.kind === 'room-not-readable') {
1127
1724
  progress.complete = false;
1128
1725
  unreadable.add(room.locator);
1129
1726
  list(room, false);
1130
1727
  emitRooms();
1131
- return;
1728
+ return 'done';
1132
1729
  }
1133
1730
  if (result.kind === 'revoked-authorization') {
1134
1731
  await refuse('revoked-authorization');
1135
- return;
1732
+ return 'done';
1136
1733
  }
1137
1734
  if (result.kind === 'scoped-refusal') {
1138
1735
  await scoped(result, room, local);
1139
1736
  progress.complete = false;
1140
- return;
1737
+ return 'done';
1141
1738
  }
1142
1739
  if (result.kind === 'write-halted') {
1143
1740
  journal.record(`outcome ${room.locator} ${head.id} write-halted`);
@@ -1146,16 +1743,24 @@ export function composeRuntime(options) {
1146
1743
  continue;
1147
1744
  haltedRefusal = true;
1148
1745
  progress.complete = false;
1149
- return;
1746
+ return 'done';
1150
1747
  }
1151
1748
  if (result.kind === 'conflicted' ||
1152
1749
  result.kind === 'rejected' ||
1153
1750
  result.kind === 'expired' ||
1154
1751
  result.kind === 'retryable-refusal') {
1752
+ if (result.kind === 'expired' && lost && result.restored !== true) {
1753
+ journal.record(`outcome ${room.locator} ${head.id} expired`);
1754
+ await discardPending(room, local, signal);
1755
+ return 'done';
1756
+ }
1155
1757
  if (result.kind === 'expired') {
1156
- const withheld = await advance(room, local, authorized);
1157
- if (withheld !== null)
1158
- return withhold(withheld, room, local, progress);
1758
+ await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
1759
+ const withheld = await advance(room, local, authorized, continuing);
1760
+ if (withheld !== null) {
1761
+ await withhold(withheld, room, local, progress);
1762
+ return 'done';
1763
+ }
1159
1764
  }
1160
1765
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
1161
1766
  (result.kind === 'conflicted' && result.roomVersion !== undefined
@@ -1168,8 +1773,7 @@ export function composeRuntime(options) {
1168
1773
  const reset = result.reset;
1169
1774
  progress.complete = false;
1170
1775
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1171
- armTimer();
1172
- return;
1776
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
1173
1777
  }
1174
1778
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
1175
1779
  const left = leaving(head, result);
@@ -1180,36 +1784,82 @@ export function composeRuntime(options) {
1180
1784
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
1181
1785
  return;
1182
1786
  }
1183
- const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1184
- leaving: result.kind === 'expired' && landed(head, current.confirmed)
1787
+ const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1788
+ leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1789
+ (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1185
1790
  ? { ...left, returned: null }
1186
1791
  : left,
1187
1792
  });
1188
- publishChangeSet(set);
1793
+ publishChangeSet(settled());
1189
1794
  moveTo({ ...status, ...attention() });
1190
1795
  });
1191
1796
  if (result.kind === 'expired')
1192
1797
  continue;
1193
- const withheld = await advance(room, local, authorized);
1194
- if (withheld !== null)
1195
- return withhold(withheld, room, local, progress);
1798
+ const withheld = await advance(room, local, authorized, continuing);
1799
+ if (withheld !== null) {
1800
+ await withhold(withheld, room, local, progress);
1801
+ return 'done';
1802
+ }
1196
1803
  continue;
1197
1804
  }
1198
1805
  const kept = await adopted(room, local, head, result).finally(answered);
1199
1806
  if (!kept) {
1200
1807
  progress.complete = false;
1201
- return;
1808
+ return 'done';
1202
1809
  }
1203
1810
  }
1811
+ return 'done';
1812
+ }
1813
+ function pastHorizon(identity) {
1814
+ return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
1815
+ }
1816
+ async function discardPending(room, local, signal) {
1817
+ const { locator } = room;
1818
+ const discarded = await exclusive(async () => {
1819
+ if (fences.has(locator))
1820
+ return false;
1821
+ const before = replicas.get(locator);
1822
+ const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
1823
+ fences.set(locator, { at: discoveries, erased: false });
1824
+ lastFenced.set(locator, ++fencings);
1825
+ try {
1826
+ await local.discard(locator);
1827
+ }
1828
+ finally {
1829
+ fences.delete(locator);
1830
+ }
1831
+ for (const batch of queue)
1832
+ pendings.delete(batch.id);
1833
+ queuesWithoutReplica.delete(locator);
1834
+ const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
1835
+ if (after !== null)
1836
+ replicas.set(locator, after);
1837
+ journal.record(`room discarded ${locator}`);
1838
+ publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
1839
+ moveTo({ ...status, ...attention() });
1840
+ return true;
1841
+ });
1842
+ if (!discarded)
1843
+ return;
1844
+ const progress = { complete: true, reachable: true };
1845
+ await roomTask(locator, async (live) => {
1846
+ await Promise.race([
1847
+ reload(room, local, progress, () => live() && !signal.aborted),
1848
+ abandonedOn(signal),
1849
+ ]);
1850
+ });
1851
+ if (signal.aborted)
1852
+ return;
1853
+ await rest(progress);
1204
1854
  }
1205
1855
  function awaitingAnswer(locator) {
1206
1856
  let release = () => { };
1207
1857
  const awaited = new Promise((resolve) => {
1208
1858
  release = resolve;
1209
1859
  });
1210
- answering.set(locator, awaited);
1860
+ answering.set(locator, { generation, answered: awaited });
1211
1861
  return () => {
1212
- if (answering.get(locator) === awaited)
1862
+ if (answering.get(locator)?.answered === awaited)
1213
1863
  answering.delete(locator);
1214
1864
  release();
1215
1865
  };
@@ -1242,7 +1892,7 @@ export function composeRuntime(options) {
1242
1892
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1243
1893
  };
1244
1894
  const reclassified = sameMembership(declared, held.confirmed.record)
1245
- ? readConfirmed(declared, held.confirmed)
1895
+ ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
1246
1896
  : readReclassified(declared, held.confirmed);
1247
1897
  const record = reclassified.kind === 'staged'
1248
1898
  ? declared
@@ -1250,12 +1900,10 @@ export function composeRuntime(options) {
1250
1900
  ...declared,
1251
1901
  typedSelection: held.confirmed.record.typedSelection,
1252
1902
  };
1253
- const read = reclassified.kind === 'deferred' ? readConfirmed(record, held.confirmed) : reclassified;
1254
- await local.write({
1255
- next: record,
1256
- previous: held.confirmed.record,
1257
- queue: held.queue,
1258
- });
1903
+ const read = reclassified.kind === 'deferred'
1904
+ ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
1905
+ : reclassified;
1906
+ await local.write({ next: record, previous: held.confirmed.record });
1259
1907
  if (read.kind === 'staged') {
1260
1908
  replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1261
1909
  if (declarationRefusals.delete(room.locator)) {
@@ -1282,7 +1930,11 @@ export function composeRuntime(options) {
1282
1930
  });
1283
1931
  }
1284
1932
  async function scoped(result, room, local) {
1285
- scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
1933
+ scopedRefusals.set(room.locator, {
1934
+ code: result.code,
1935
+ fields: result.fields,
1936
+ retryable: result.retryable,
1937
+ });
1286
1938
  let scopedAway = room;
1287
1939
  if (result.scope === 'model')
1288
1940
  await retainModel(local, room, 'collection', {
@@ -1319,22 +1971,26 @@ export function composeRuntime(options) {
1319
1971
  roomVersion: record.roomVersion,
1320
1972
  documents: record.documents,
1321
1973
  revisions: record.revisions,
1322
- attachments: record.attachments,
1323
1974
  },
1324
1975
  ];
1325
1976
  });
1326
1977
  }
1327
- async function synchronize(listed, local, progress, attempt) {
1978
+ async function synchronize(listed, local, progress, attempt, live) {
1328
1979
  const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1980
+ if (!live())
1981
+ return;
1329
1982
  const ready = listed.filter((_, index) => {
1330
1983
  const outcome = prepared[index];
1331
1984
  return outcome?.status === 'fulfilled' && outcome.value;
1332
1985
  });
1333
- if (attempt === 'opened')
1986
+ if (attempt === 'opened') {
1334
1987
  options.adapter.roomStream?.hold(heldRooms(ready));
1988
+ handed = true;
1989
+ }
1990
+ discovering?.release();
1335
1991
  const loaded = await Promise.allSettled(ready
1336
1992
  .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1337
- .map((room) => loadRoom(room, local, progress)));
1993
+ .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
1338
1994
  const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1339
1995
  if (failed !== undefined)
1340
1996
  throw failed.reason;
@@ -1363,14 +2019,14 @@ export function composeRuntime(options) {
1363
2019
  }
1364
2020
  return true;
1365
2021
  }
1366
- async function caughtUp(room, local, progress) {
2022
+ async function caughtUp(room, local, progress, live) {
1367
2023
  const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1368
- if (held === undefined || authorization === undefined)
2024
+ if (held === undefined || authorization === undefined || !live())
1369
2025
  return;
1370
2026
  const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1371
2027
  if (!behind)
1372
2028
  return;
1373
- const withheld = await advance(room, local, authorization);
2029
+ const withheld = await advance(room, local, authorization, live);
1374
2030
  if (withheld !== null)
1375
2031
  await withhold(withheld, room, local, progress);
1376
2032
  }
@@ -1380,39 +2036,46 @@ export function composeRuntime(options) {
1380
2036
  return model.status === 'unavailable' && model.reason === 'revoked';
1381
2037
  });
1382
2038
  }
1383
- async function loadRoom(room, local, progress) {
1384
- if (discoveryPaused || authorization === undefined)
2039
+ async function loadRoom(room, local, progress, live) {
2040
+ if (!live() || authorization === undefined)
1385
2041
  return;
1386
2042
  if (replicas.has(room.locator) && revoked(room.locator)) {
1387
- await reload(room, local, progress);
2043
+ await reload(room, local, progress, live);
1388
2044
  return;
1389
2045
  }
1390
- const withheld = await advance(room, local, authorization);
2046
+ const withheld = await advance(room, local, authorization, live);
1391
2047
  if (withheld !== null) {
1392
2048
  await withhold(withheld, room, local, progress);
1393
2049
  return;
1394
2050
  }
1395
- await caughtUp(room, local, progress);
2051
+ await caughtUp(room, local, progress, live);
1396
2052
  }
1397
- async function reload(room, local, progress) {
1398
- if (discoveryPaused || authorization === undefined)
1399
- return;
1400
- const bootstrapped = await pullBootstrap(authorization, room);
1401
- if (!continuing())
1402
- return;
1403
- if (bootstrapped.kind !== 'staged') {
1404
- await withhold(bootstrapped, room, local, progress);
1405
- return;
2053
+ async function reload(room, local, progress, live) {
2054
+ try {
2055
+ if (!live() || authorization === undefined)
2056
+ return;
2057
+ const bootstrapped = await pullBootstrap(authorization, room, local, live);
2058
+ if (!live())
2059
+ return;
2060
+ if (bootstrapped.kind !== 'staged') {
2061
+ await withhold(bootstrapped, room, local, progress);
2062
+ return;
2063
+ }
2064
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
2065
+ await caughtUp(room, local, progress, live);
2066
+ }
2067
+ finally {
2068
+ caughtUpTo(room.locator);
1406
2069
  }
1407
- await publish(local, room, bootstrapped.confirmed);
1408
- await caughtUp(room, local, progress);
1409
2070
  }
1410
- async function discoverRooms(local, progress, attempt) {
2071
+ async function discoverRooms(local, progress, attempt, live) {
1411
2072
  if (closed || authorization === undefined) {
1412
2073
  notifyReady();
1413
2074
  return;
1414
2075
  }
1415
2076
  moveTo(transferring(status));
2077
+ discoveries += 1;
2078
+ const started = discoveries;
1416
2079
  let result;
1417
2080
  try {
1418
2081
  result = await options.adapter.discover(authorization);
@@ -1420,7 +2083,8 @@ export function composeRuntime(options) {
1420
2083
  catch {
1421
2084
  result = { kind: 'transport-uncertainty' };
1422
2085
  }
1423
- if (closed) {
2086
+ if (!live()) {
2087
+ progress.complete = false;
1424
2088
  notifyReady();
1425
2089
  return;
1426
2090
  }
@@ -1434,15 +2098,16 @@ export function composeRuntime(options) {
1434
2098
  }
1435
2099
  progress.reachable = false;
1436
2100
  progress.complete = false;
1437
- if (attempt === 'opened') {
1438
- options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1439
- }
1440
2101
  notifyReady();
1441
2102
  return;
1442
2103
  }
1443
2104
  if (result.kind === 'contract-unavailable') {
1444
2105
  journal.record(`connection contract unavailable ${result.code}`);
1445
- await refuse('missing-required-guarantee', { code: result.code, fields: result.fields });
2106
+ await refuse('missing-required-guarantee', {
2107
+ code: result.code,
2108
+ fields: result.fields,
2109
+ retryable: result.retryable,
2110
+ });
1446
2111
  notifyReady();
1447
2112
  return;
1448
2113
  }
@@ -1477,14 +2142,20 @@ export function composeRuntime(options) {
1477
2142
  }
1478
2143
  const previousCapabilities = capabilities;
1479
2144
  capabilities = result.capabilities;
2145
+ outcomeRetention = result.retention.outcome;
1480
2146
  notifyReady();
1481
2147
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1482
2148
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
1483
2149
  limits = result.providerLimits;
1484
2150
  await local.declare(limits, capabilities);
1485
2151
  }
1486
- await applyDiscovery(result.rooms, local);
1487
- await synchronize(result.rooms, local, progress, attempt);
2152
+ const signedIn = result.member ?? null;
2153
+ if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
2154
+ await local.keepMember(signedIn);
2155
+ member = signedIn;
2156
+ emitMember();
2157
+ }
2158
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
1488
2159
  }
1489
2160
  async function ensureAuthorization() {
1490
2161
  if (authorization !== undefined)
@@ -1498,44 +2169,87 @@ export function composeRuntime(options) {
1498
2169
  throw error;
1499
2170
  }
1500
2171
  }
1501
- function task(work) {
2172
+ async function admittedRead(work) {
2173
+ if (roomReads.generation !== generation) {
2174
+ roomReads = { generation, free: ROOM_READS, waiting: [] };
2175
+ }
2176
+ const reads = roomReads;
2177
+ while (reads.free === 0)
2178
+ await new Promise((admit) => reads.waiting.push(admit));
2179
+ reads.free -= 1;
2180
+ try {
2181
+ await work();
2182
+ }
2183
+ finally {
2184
+ reads.free += 1;
2185
+ reads.waiting.shift()?.();
2186
+ }
2187
+ }
2188
+ function roomTask(locator, work) {
2189
+ return task(roomLane(locator), (live) => admittedRead(() => work(live)));
2190
+ }
2191
+ function task(lane, work) {
1502
2192
  if (stopping)
1503
- return chain;
1504
- chain = chain.then(work, work).catch(() => undefined);
1505
- return chain;
2193
+ return Promise.resolve();
2194
+ const held = lanes.get(lane);
2195
+ const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
2196
+ const turn = current.generation;
2197
+ const live = () => turn === generation && continuing();
2198
+ const run = current.chain.then(() => work(live)).catch(() => undefined);
2199
+ current.chain = run;
2200
+ lanes.set(lane, current);
2201
+ working.add(run);
2202
+ void run.finally(() => working.delete(run));
2203
+ return run;
1506
2204
  }
1507
2205
  function sync(attempt) {
1508
- return task(async () => {
1509
- if (closed || stopping || discoveryPaused) {
2206
+ return task(OPENING_LANE, async (live) => {
2207
+ if (!live()) {
1510
2208
  notifyReady();
1511
2209
  return;
1512
2210
  }
1513
2211
  const progress = { complete: true, reachable: true };
1514
- underway = attempt === 'opened' ? progress : null;
2212
+ const gate = discover();
2213
+ if (attempt === 'opened')
2214
+ opened = generation;
2215
+ underway = attempt === 'opened' ? { generation, progress } : null;
2216
+ if (attempt === 'opened')
2217
+ streamed = progress;
1515
2218
  complete = false;
1516
2219
  recorded = false;
1517
2220
  moveTo(transferring(status));
2221
+ handed = false;
1518
2222
  try {
1519
2223
  const local = await openLocal();
1520
- if (closed)
2224
+ if (!live())
1521
2225
  return;
1522
2226
  await ensureAuthorization();
1523
- if (closed)
2227
+ if (!live())
1524
2228
  return;
1525
- await discoverRooms(local, progress, attempt);
2229
+ await discoverRooms(local, progress, attempt, live);
1526
2230
  }
1527
2231
  catch {
1528
2232
  progress.complete = false;
1529
- if (attempt === 'opened') {
1530
- options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1531
- }
2233
+ if (attempt === 'opened' && !handed)
2234
+ progress.reachable = false;
1532
2235
  }
1533
2236
  finally {
2237
+ gate.release();
1534
2238
  notifyReady();
1535
2239
  }
1536
- if (attempt === 'opened')
2240
+ if (!live())
2241
+ return;
2242
+ if (attempt !== 'opened') {
2243
+ await finishOpening(progress, live);
1537
2244
  return;
1538
- await finishOpening(progress);
2245
+ }
2246
+ if (handed || closed || progress.reachable)
2247
+ return;
2248
+ underway = null;
2249
+ opened = null;
2250
+ journal.record('connection reopen');
2251
+ await rest(progress, live);
2252
+ options.adapter.roomStream?.reopen();
1539
2253
  });
1540
2254
  }
1541
2255
  function closedSocket(revoked) {
@@ -1545,29 +2259,51 @@ export function composeRuntime(options) {
1545
2259
  void refuse('revoked-authorization');
1546
2260
  return;
1547
2261
  }
1548
- if (opened || status.providerUnreachable)
1549
- return;
1550
- moveTo({ ...status, providerUnreachable: true });
2262
+ complete = false;
2263
+ if (streamed !== null)
2264
+ streamed.complete = false;
2265
+ moveTo({
2266
+ ...status,
2267
+ state: status.state === 'synced' ? 'syncing' : status.state,
2268
+ providerUnreachable: status.providerUnreachable || !opened,
2269
+ });
1551
2270
  }
1552
2271
  function applyOpening(expired) {
1553
- return task(async () => {
1554
- const progress = underway;
2272
+ const progress = underway?.generation === generation ? underway.progress : null;
2273
+ const cycle = progress ?? { complete: true, reachable: true };
2274
+ const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
2275
+ return task(OPENING_LANE, async (live) => {
1555
2276
  underway = null;
1556
- if (closed || stopping)
2277
+ if (!live())
1557
2278
  return;
1558
2279
  if (progress === null) {
1559
- await reloadExpired(expired);
2280
+ await replacing;
2281
+ await rest(cycle, live);
1560
2282
  return;
1561
2283
  }
2284
+ await roomsCarried(expired);
2285
+ if (!live())
2286
+ return;
2287
+ if (opened === generation)
2288
+ opened = null;
2289
+ const replaced = new Set(owed);
2290
+ owed.clear();
1562
2291
  try {
1563
2292
  const local = await openLocal();
1564
2293
  const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1565
- const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1566
- const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
2294
+ const reloading = held.filter((room) => !expired.includes(room.locator) &&
2295
+ (replaced.has(room.locator) || revoked(room.locator)));
2296
+ for (const room of reloading)
2297
+ catching.add(room.locator);
2298
+ readSide.recover();
2299
+ const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
2300
+ const reloaded = [...(await reloads), ...(await replacing)];
1567
2301
  if (progress.reachable) {
1568
- for (const room of held)
1569
- if (!reloading.includes(room))
2302
+ for (const room of held) {
2303
+ if (!expired.includes(room.locator) && !reloading.includes(room)) {
1570
2304
  await confirmModels(room, local);
2305
+ }
2306
+ }
1571
2307
  }
1572
2308
  const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1573
2309
  if (failed !== undefined)
@@ -1577,21 +2313,21 @@ export function composeRuntime(options) {
1577
2313
  progress.complete = false;
1578
2314
  progress.reachable = false;
1579
2315
  }
1580
- await finishOpening(progress);
2316
+ if (live())
2317
+ await finishOpening(progress, live);
1581
2318
  });
1582
2319
  }
1583
- async function reloadExpired(expired) {
1584
- const progress = { complete: true, reachable: true };
2320
+ async function reloadNow(replaced, progress) {
1585
2321
  try {
1586
2322
  const local = await openLocal();
1587
- await Promise.allSettled([...discovered.values()]
1588
- .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1589
- .map((room) => reload(room, local, progress)));
2323
+ return await Promise.allSettled([...discovered.values()]
2324
+ .filter((room) => replicas.has(room.locator) && replaced(room))
2325
+ .map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
1590
2326
  }
1591
2327
  catch {
1592
2328
  progress.reachable = false;
2329
+ return [];
1593
2330
  }
1594
- await rest(progress);
1595
2331
  }
1596
2332
  async function confirmModels(room, local) {
1597
2333
  for (const attached of ['collection', 'typed-selection']) {
@@ -1602,38 +2338,96 @@ export function composeRuntime(options) {
1602
2338
  }
1603
2339
  }
1604
2340
  function submitLater(locator) {
1605
- const running = submitting.get(locator);
1606
- if (running !== undefined) {
1607
- running.again = true;
2341
+ if (localSaves)
1608
2342
  return;
2343
+ void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
2344
+ }
2345
+ async function submitPass(locator, signal) {
2346
+ const room = discovered.get(locator);
2347
+ if (closed || stopping || discoveryPaused || room === undefined)
2348
+ return 'done';
2349
+ const progress = { complete: true, reachable: true };
2350
+ let next;
2351
+ try {
2352
+ await ensureAuthorization();
2353
+ if (authorization === undefined)
2354
+ return 'done';
2355
+ next = await submitRoom(room, await openLocal(), authorization, progress, signal);
1609
2356
  }
1610
- const run = { again: false, done: Promise.resolve() };
1611
- submitting.set(locator, run);
1612
- run.done = (async () => {
1613
- try {
1614
- do {
1615
- run.again = false;
1616
- const room = discovered.get(locator);
1617
- if (closed || stopping || discoveryPaused || room === undefined)
1618
- return;
1619
- const progress = { complete: true, reachable: true };
1620
- try {
1621
- await ensureAuthorization();
1622
- if (authorization === undefined)
1623
- return;
1624
- await submitRoom(room, await openLocal(), authorization, progress);
1625
- }
1626
- catch {
1627
- progress.reachable = false;
1628
- }
1629
- armTimer();
1630
- await rest(progress);
1631
- } while (run.again);
1632
- }
1633
- finally {
1634
- submitting.delete(locator);
1635
- }
1636
- })();
2357
+ catch {
2358
+ progress.reachable = false;
2359
+ next = 'again';
2360
+ }
2361
+ if (signal.aborted)
2362
+ return 'done';
2363
+ await rest(progress);
2364
+ return next === 'again' && !socketOpen ? 'done' : next;
2365
+ }
2366
+ function repairLater(locator, when) {
2367
+ if (!socketOpen || repairing.has(locator))
2368
+ return;
2369
+ void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
2370
+ }
2371
+ async function repairPass(locator, signal) {
2372
+ const room = discovered.get(locator);
2373
+ if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
2374
+ return 'done';
2375
+ if (room === undefined || room.replica.admission !== 'admitted')
2376
+ return 'done';
2377
+ const progress = { complete: true, reachable: true };
2378
+ repairing.add(locator);
2379
+ try {
2380
+ const local = await openLocal();
2381
+ await roomTask(locator, async (live) => {
2382
+ const current = () => live() && !signal.aborted;
2383
+ if (!current() || authorization === undefined)
2384
+ return;
2385
+ const withheld = await Promise.race([
2386
+ advance(room, local, authorization, current),
2387
+ abandonedOn(signal),
2388
+ ]);
2389
+ if (withheld !== null && current())
2390
+ await withhold(withheld, room, local, progress);
2391
+ });
2392
+ }
2393
+ catch {
2394
+ stale.add(locator);
2395
+ progress.reachable = false;
2396
+ }
2397
+ finally {
2398
+ repairing.delete(locator);
2399
+ }
2400
+ if (signal.aborted)
2401
+ return 'done';
2402
+ await rest(progress);
2403
+ if (!stale.has(locator)) {
2404
+ if ((replicas.get(locator)?.queue.length ?? 0) > 0)
2405
+ submitLater(locator);
2406
+ return 'done';
2407
+ }
2408
+ if (!repairable.has(locator))
2409
+ return 'done';
2410
+ const reset = repairable.get(locator);
2411
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
2412
+ }
2413
+ function resendLater() {
2414
+ if (!socketOpen)
2415
+ return;
2416
+ void recovery.attempt('resend', resendPass, { again: true });
2417
+ }
2418
+ async function resendPass(signal) {
2419
+ if (!socketOpen || closed || stopping || discoveryPaused)
2420
+ return 'done';
2421
+ const reading = { complete: true, reachable: true };
2422
+ try {
2423
+ await conversations.resend(reading);
2424
+ }
2425
+ catch {
2426
+ reading.reachable = false;
2427
+ }
2428
+ if (signal.aborted)
2429
+ return 'done';
2430
+ return reading.reachable && reading.complete ? 'done' : 'again';
1637
2431
  }
1638
2432
  function submitAll() {
1639
2433
  for (const [locator, replica] of replicas) {
@@ -1641,31 +2435,40 @@ export function composeRuntime(options) {
1641
2435
  submitLater(locator);
1642
2436
  }
1643
2437
  }
1644
- async function finishOpening(progress) {
2438
+ async function finishOpening(progress, live) {
1645
2439
  try {
1646
- if (closed || discoveryPaused) {
2440
+ if (!live()) {
1647
2441
  progress.complete = false;
1648
2442
  return;
1649
2443
  }
1650
2444
  submitAll();
2445
+ for (const locator of stale)
2446
+ if (repairable.has(locator))
2447
+ repairLater(locator, {});
1651
2448
  await conversations.reconcile();
1652
2449
  if (status.providerUnreachable)
1653
2450
  conversations.reconnected();
1654
2451
  const reading = { complete: true, reachable: true };
1655
2452
  await conversations.catchUp(reading);
1656
- if (!reading.reachable)
2453
+ if (progress.reachable && reading.reachable)
2454
+ await conversations.resend(reading);
2455
+ if (!reading.reachable) {
1657
2456
  progress.reachable = false;
2457
+ resendLater();
2458
+ }
1658
2459
  }
1659
2460
  catch {
1660
2461
  progress.complete = false;
1661
2462
  }
1662
2463
  finally {
1663
- complete = progress.complete;
1664
- await rest(progress);
2464
+ if (live()) {
2465
+ complete = progress.complete;
2466
+ await rest(progress, live);
2467
+ }
1665
2468
  }
1666
2469
  }
1667
- async function rest(progress) {
1668
- if (closed)
2470
+ async function rest(progress, live = () => !closed) {
2471
+ if (!live())
1669
2472
  return;
1670
2473
  let lastCompleted = status.lastCompleted;
1671
2474
  let synced = complete && stale.size === 0 && !pending();
@@ -1673,6 +2476,8 @@ export function composeRuntime(options) {
1673
2476
  const completedAt = new Date().toISOString();
1674
2477
  try {
1675
2478
  await (await openLocal()).complete(completedAt);
2479
+ if (!live())
2480
+ return;
1676
2481
  recorded = true;
1677
2482
  lastCompleted = completedAt;
1678
2483
  }
@@ -1681,17 +2486,29 @@ export function composeRuntime(options) {
1681
2486
  }
1682
2487
  }
1683
2488
  moveTo({
1684
- state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
2489
+ state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
1685
2490
  lastCompleted,
1686
2491
  providerUnreachable: !progress.reachable,
1687
2492
  ...attention(),
1688
2493
  });
1689
2494
  }
2495
+ function discover() {
2496
+ let release = () => { };
2497
+ const done = new Promise((resolve) => {
2498
+ release = resolve;
2499
+ });
2500
+ discovering = { generation, done, release };
2501
+ return discovering;
2502
+ }
1690
2503
  function applyRoom(locator, roomVersion) {
1691
2504
  announced.set(locator, roomVersion);
1692
- return task(async () => {
1693
- await answering.get(locator);
1694
- if (closed || stopping || discoveryPaused || authorization === undefined)
2505
+ return task(roomLane(locator), async (live) => {
2506
+ if (discovering?.generation === generation)
2507
+ await discovering.done;
2508
+ const saving = answering.get(locator);
2509
+ if (saving?.generation === generation)
2510
+ await saving.answered;
2511
+ if (!live() || authorization === undefined)
1695
2512
  return;
1696
2513
  const room = discovered.get(locator);
1697
2514
  const held = replicas.get(locator);
@@ -1702,25 +2519,41 @@ export function composeRuntime(options) {
1702
2519
  options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
1703
2520
  return;
1704
2521
  }
1705
- const during = underway;
2522
+ const during = underway?.generation === generation ? underway.progress : null;
1706
2523
  const progress = during ?? { complete: true, reachable: true };
1707
2524
  if (during === null)
1708
2525
  moveTo(transferring(status));
1709
2526
  try {
1710
2527
  const local = await openLocal();
1711
- const withheld = await advance(room, local, authorization);
1712
- if (withheld !== null)
1713
- await withhold(withheld, room, local, progress);
2528
+ await admittedRead(async () => {
2529
+ if (!live() || authorization === undefined)
2530
+ return;
2531
+ const withheld = await advance(room, local, authorization, live);
2532
+ if (withheld !== null)
2533
+ await withhold(withheld, room, local, progress);
2534
+ });
1714
2535
  }
1715
2536
  catch {
1716
2537
  stale.add(locator);
1717
2538
  }
1718
2539
  finally {
1719
2540
  if (during === null)
1720
- await rest(progress);
2541
+ await rest(progress, live);
1721
2542
  }
1722
2543
  });
1723
2544
  }
2545
+ function roomsCarried(replacing) {
2546
+ const replaced = new Set(replacing.map(roomLane));
2547
+ return Promise.all([...lanes]
2548
+ .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
2549
+ .map(([, held]) => held.chain));
2550
+ }
2551
+ const receipts = {
2552
+ heads: async () => (await openLocal()).receivedHeads(),
2553
+ parts: async (key, count) => (await openLocal()).receivedParts(key, count),
2554
+ keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
2555
+ drop: async (keys) => (await openLocal()).dropReceived(keys),
2556
+ };
1724
2557
  function observeStream() {
1725
2558
  const stream = options.adapter.roomStream;
1726
2559
  if (stream === undefined || withdrawStream !== undefined)
@@ -1730,6 +2563,9 @@ export function composeRuntime(options) {
1730
2563
  return;
1731
2564
  if (event.kind === 'opened') {
1732
2565
  socketOpen = true;
2566
+ generation += 1;
2567
+ conversations.reopened();
2568
+ recovery.resume();
1733
2569
  void sync('opened');
1734
2570
  }
1735
2571
  else if (event.kind === 'closed')
@@ -1738,7 +2574,7 @@ export function composeRuntime(options) {
1738
2574
  void applyOpening(event.expired);
1739
2575
  else
1740
2576
  void applyRoom(event.locator, event.roomVersion);
1741
- });
2577
+ }, receipts);
1742
2578
  }
1743
2579
  function writesHeld() {
1744
2580
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -1776,34 +2612,30 @@ export function composeRuntime(options) {
1776
2612
  });
1777
2613
  }
1778
2614
  function scheduleCurrency() {
1779
- if (currencyQueued || closed || stopping)
2615
+ if (currencyQueued === generation || closed || stopping)
1780
2616
  return;
1781
- currencyQueued = true;
1782
- void task(runCurrency);
2617
+ currencyQueued = generation;
2618
+ void task(CONVERSATION_LANE, runCurrency);
1783
2619
  }
1784
- async function runCurrency() {
1785
- currencyQueued = false;
1786
- if (closed || stopping || discoveryPaused)
2620
+ async function runCurrency(live) {
2621
+ if (currencyQueued === generation)
2622
+ currencyQueued = null;
2623
+ if (!live())
1787
2624
  return;
1788
2625
  await conversations.reconcile();
1789
2626
  if (conversations.current()) {
1790
2627
  show();
1791
2628
  return;
1792
2629
  }
1793
- const resting = status;
1794
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1795
- moveTo(transferring(resting));
2630
+ const progress = { complete: false, reachable: !status.providerUnreachable };
2631
+ moveTo(transferring(status));
1796
2632
  try {
1797
2633
  await conversations.catchUp(progress);
1798
2634
  }
1799
2635
  catch {
1800
2636
  progress.reachable = false;
1801
2637
  }
1802
- finally {
1803
- if (!closed) {
1804
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1805
- }
1806
- }
2638
+ await rest(progress, live);
1807
2639
  }
1808
2640
  function close() {
1809
2641
  if (closed)
@@ -1815,7 +2647,7 @@ export function composeRuntime(options) {
1815
2647
  withdrawConversations = undefined;
1816
2648
  withdrawServiceMode?.();
1817
2649
  conversations.close();
1818
- stopTimer();
2650
+ void recovery.retire();
1819
2651
  store?.close();
1820
2652
  void opening?.then((opened) => {
1821
2653
  opened.close();
@@ -1825,6 +2657,7 @@ export function composeRuntime(options) {
1825
2657
  storageLossListeners.clear();
1826
2658
  statusListeners.clear();
1827
2659
  serviceModeListeners.clear();
2660
+ memberListeners.clear();
1828
2661
  readSide.close();
1829
2662
  queueSubscriptions.clear();
1830
2663
  roomDraftListeners.clear();
@@ -1832,9 +2665,8 @@ export function composeRuntime(options) {
1832
2665
  }
1833
2666
  async function stop() {
1834
2667
  stopping = true;
1835
- stopTimer();
1836
- await chain.catch(() => undefined);
1837
- await Promise.all([...submitting.values()].map((run) => run.done));
2668
+ await recovery.retire();
2669
+ await Promise.all(working);
1838
2670
  await mutations;
1839
2671
  close();
1840
2672
  }
@@ -1856,14 +2688,54 @@ export function composeRuntime(options) {
1856
2688
  return planOf(input, roomFacts(reach), planReach(reach));
1857
2689
  }
1858
2690
  function watchPlan(plan, deliver) {
1859
- return readSide.watch(plan, identityOf(plan), deliver);
2691
+ return readSide.watch(plan, identityOf(plan), () => {
2692
+ const cover = covered(plan);
2693
+ awaitPreparation(cover);
2694
+ return cover;
2695
+ }, deliver);
1860
2696
  }
1861
2697
  async function searched(input, reach) {
1862
2698
  const plan = searchPlanOf(input);
1863
- const selected = plan.documents === undefined && reach === null
1864
- ? null
1865
- : (await readSide.query(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
1866
- return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
2699
+ const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
2700
+ for (;;) {
2701
+ const cover = covered(filter);
2702
+ awaitPreparation(cover);
2703
+ const pending = 'pending' in cover ? [...cover.pending] : [];
2704
+ const covering = 'pending' in cover ? [] : cover.coverage.rooms;
2705
+ const unindexed = covering
2706
+ .filter(({ room }) => !searchIndex.rooms().has(room))
2707
+ .map(({ room }) => room);
2708
+ for (const room of unindexed)
2709
+ pending.push({ room, awaits: 'index' });
2710
+ const held = conversations.searchable();
2711
+ const named = reach !== null && !reach.conversations
2712
+ ? []
2713
+ : (plan.conversations ?? held).filter((one) => held.includes(one) && conversations.acquired(one));
2714
+ const waiting = named.filter((one) => !searchIndex.conversations().has(one));
2715
+ for (const conversation of waiting)
2716
+ pending.push({ conversation, awaits: 'index' });
2717
+ if (unindexed.length > 0 || waiting.length > 0)
2718
+ index(unindexed, waiting);
2719
+ if (pending.length > 0 || 'pending' in cover)
2720
+ return { pending };
2721
+ const seen = searchIndex.changes();
2722
+ await searchIndex.caughtUp();
2723
+ const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(filter);
2724
+ if (selected !== null && 'pending' in selected)
2725
+ return selected;
2726
+ if (searchIndex.changes() !== seen ||
2727
+ JSON.stringify(covered(filter)) !== JSON.stringify(cover))
2728
+ continue;
2729
+ return {
2730
+ ...searchIndex.search(plan, selected === null ? null : selected.documents, named),
2731
+ coverage: {
2732
+ rooms: covering,
2733
+ conversations: named,
2734
+ index: searchIndex.generation(),
2735
+ overlay: cover.coverage.overlay,
2736
+ },
2737
+ };
2738
+ }
1867
2739
  }
1868
2740
  const watch = (topic, input, deliver) => {
1869
2741
  if (topic === 'room-drafts') {
@@ -1888,6 +2760,8 @@ export function composeRuntime(options) {
1888
2760
  }
1889
2761
  if (topic === 'conversations')
1890
2762
  return conversations.watchList(deliver);
2763
+ if (topic === 'agents')
2764
+ return conversations.watchAgents(deliver);
1891
2765
  if (topic === 'presentation') {
1892
2766
  const listener = {
1893
2767
  app: input[0],
@@ -1942,6 +2816,13 @@ export function composeRuntime(options) {
1942
2816
  serviceModeListeners.delete(deliver);
1943
2817
  };
1944
2818
  }
2819
+ if (topic === 'member') {
2820
+ memberListeners.add(deliver);
2821
+ deliver(member);
2822
+ return () => {
2823
+ memberListeners.delete(deliver);
2824
+ };
2825
+ }
1945
2826
  if (topic === 'queue') {
1946
2827
  const queue = { locator: input[0], notify: deliver };
1947
2828
  queueSubscriptions.add(queue);
@@ -1959,7 +2840,7 @@ export function composeRuntime(options) {
1959
2840
  rooms() {
1960
2841
  return listed();
1961
2842
  },
1962
- query: ((input) => readSide.query(planned(input, null))),
2843
+ query: ((input) => rowsQuery(planned(input, null))),
1963
2844
  search: (input) => searched(input, null),
1964
2845
  types(locator) {
1965
2846
  return {
@@ -1988,20 +2869,20 @@ export function composeRuntime(options) {
1988
2869
  return exclusive(async () => {
1989
2870
  const parts = readParts(input);
1990
2871
  if (path === '' || normalize(path) !== path)
1991
- throw new Error('room-draft-malformed');
2872
+ throw runtimeError('room-draft-malformed');
1992
2873
  const local = await openLocal();
1993
2874
  const current = roomDrafts.get(room)?.get(path);
1994
2875
  if ((current?.revision ?? null) !== revision) {
1995
- return {
1996
- outcome: 'stale',
1997
- current: current === undefined ? null : roomDraftOf(current),
1998
- };
2876
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
1999
2877
  }
2878
+ const file = parts.file === undefined
2879
+ ? undefined
2880
+ : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
2000
2881
  const record = {
2001
2882
  locator: room,
2002
2883
  path,
2003
2884
  revision: roomDraftRevision(),
2004
- parts: held(current?.parts, parts),
2885
+ parts: held(current?.parts, parts, file),
2005
2886
  returned: current?.returned ?? null,
2006
2887
  };
2007
2888
  await local.keepRoomDraft(record);
@@ -2015,10 +2896,7 @@ export function composeRuntime(options) {
2015
2896
  const local = await openLocal();
2016
2897
  const current = roomDrafts.get(room)?.get(path);
2017
2898
  if (current === undefined || current.revision !== revision) {
2018
- return {
2019
- outcome: 'stale',
2020
- current: current === undefined ? null : roomDraftOf(current),
2021
- };
2899
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2022
2900
  }
2023
2901
  await local.discardRoomDraft(room, path);
2024
2902
  forgetRoomDraft(room, path);
@@ -2053,6 +2931,10 @@ export function composeRuntime(options) {
2053
2931
  history(locator, path, query) {
2054
2932
  return readHistory(locator, path, query);
2055
2933
  },
2934
+ binaryFile: {
2935
+ read: readBinaryPath,
2936
+ readReference: readBinaryReference,
2937
+ },
2056
2938
  call(name, input) {
2057
2939
  return invokeAction(name, input);
2058
2940
  },
@@ -2072,23 +2954,26 @@ export function composeRuntime(options) {
2072
2954
  if (refusal === null || !refusal.reauthorizationOffered)
2073
2955
  return;
2074
2956
  journal.record('connection reauthorize');
2075
- await chain.catch(() => undefined);
2957
+ await Promise.all([...lanes.values()]
2958
+ .filter((held) => held.generation === generation)
2959
+ .map((held) => held.chain));
2076
2960
  await mutations;
2077
2961
  authorization = undefined;
2078
2962
  discoveryPaused = false;
2079
2963
  if (options.adapter.roomStream === undefined)
2080
2964
  await sync('unstreamed');
2081
2965
  else
2082
- await task(ensureAuthorization);
2966
+ await task(OPENING_LANE, ensureAuthorization);
2083
2967
  },
2084
2968
  };
2085
2969
  const reachable = {
2086
2970
  md,
2087
2971
  watch,
2972
+ referrerScope: (referrer) => options.adapter.referrerScope(referrer),
2088
2973
  plan: planned,
2089
- query: (plan) => readSide.query(plan),
2974
+ query: (plan) => rowsQuery(plan),
2090
2975
  watchPlan,
2091
2976
  search: searched,
2092
2977
  };
2093
- return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2978
+ return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
2094
2979
  }