@agentshouse/mdruntime 0.1.0-c7bd7ca3faee4 → 0.1.0-c89fa07b05d4b

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 (185) hide show
  1. package/AGENTS.md +259 -216
  2. package/README.md +11 -1
  3. package/dist/adapter.d.ts +66 -16
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/attempt.d.ts +16 -0
  6. package/dist/attempt.d.ts.map +1 -0
  7. package/dist/attempt.js +70 -0
  8. package/dist/binary.d.ts +23 -0
  9. package/dist/binary.d.ts.map +1 -0
  10. package/dist/binary.js +8 -0
  11. package/dist/bootstrap.d.ts +17 -5
  12. package/dist/bootstrap.d.ts.map +1 -1
  13. package/dist/bootstrap.js +109 -37
  14. package/dist/change-set.d.ts +2 -0
  15. package/dist/change-set.d.ts.map +1 -1
  16. package/dist/change-set.js +48 -16
  17. package/dist/change.d.ts +15 -0
  18. package/dist/change.d.ts.map +1 -1
  19. package/dist/change.js +26 -1
  20. package/dist/chat/acts.d.ts +33 -0
  21. package/dist/chat/acts.d.ts.map +1 -0
  22. package/dist/chat/acts.js +36 -0
  23. package/dist/chat/context.d.ts +3 -0
  24. package/dist/chat/context.d.ts.map +1 -0
  25. package/dist/chat/context.js +10 -0
  26. package/dist/chat/input.d.ts +9 -0
  27. package/dist/chat/input.d.ts.map +1 -0
  28. package/dist/chat/input.js +99 -0
  29. package/dist/chat/kit.d.ts +60 -0
  30. package/dist/chat/kit.d.ts.map +1 -0
  31. package/dist/chat/kit.js +123 -0
  32. package/dist/chat/lasting.d.ts +6 -0
  33. package/dist/chat/lasting.d.ts.map +1 -0
  34. package/dist/chat/lasting.js +19 -0
  35. package/dist/chat/list.d.ts +26 -0
  36. package/dist/chat/list.d.ts.map +1 -0
  37. package/dist/chat/list.js +62 -0
  38. package/dist/chat/markdown.d.ts +20 -0
  39. package/dist/chat/markdown.d.ts.map +1 -0
  40. package/dist/chat/markdown.js +188 -0
  41. package/dist/chat/message.d.ts +6 -0
  42. package/dist/chat/message.d.ts.map +1 -0
  43. package/dist/chat/message.js +192 -0
  44. package/dist/chat/messages.d.ts +21 -0
  45. package/dist/chat/messages.d.ts.map +1 -0
  46. package/dist/chat/messages.js +151 -0
  47. package/dist/chat/name.d.ts +6 -0
  48. package/dist/chat/name.d.ts.map +1 -0
  49. package/dist/chat/name.js +37 -0
  50. package/dist/chat/opened.d.ts +11 -0
  51. package/dist/chat/opened.d.ts.map +1 -0
  52. package/dist/chat/opened.js +17 -0
  53. package/dist/chat/overlay.d.ts +31 -0
  54. package/dist/chat/overlay.d.ts.map +1 -0
  55. package/dist/chat/overlay.js +97 -0
  56. package/dist/chat/parts.d.ts +57 -0
  57. package/dist/chat/parts.d.ts.map +1 -0
  58. package/dist/chat/parts.js +119 -0
  59. package/dist/chat/process.d.ts +20 -0
  60. package/dist/chat/process.d.ts.map +1 -0
  61. package/dist/chat/process.js +32 -0
  62. package/dist/chat/provider.d.ts +63 -0
  63. package/dist/chat/provider.d.ts.map +1 -0
  64. package/dist/chat/provider.js +620 -0
  65. package/dist/chat/question.d.ts +9 -0
  66. package/dist/chat/question.d.ts.map +1 -0
  67. package/dist/chat/question.js +80 -0
  68. package/dist/chat/questions.d.ts +31 -0
  69. package/dist/chat/questions.d.ts.map +1 -0
  70. package/dist/chat/questions.js +111 -0
  71. package/dist/chat/read.d.ts +5 -0
  72. package/dist/chat/read.d.ts.map +1 -0
  73. package/dist/chat/read.js +13 -0
  74. package/dist/chat/ready.d.ts +33 -0
  75. package/dist/chat/ready.d.ts.map +1 -0
  76. package/dist/chat/ready.js +91 -0
  77. package/dist/chat/style.d.ts +4 -0
  78. package/dist/chat/style.d.ts.map +1 -0
  79. package/dist/chat/style.js +302 -0
  80. package/dist/chat.d.ts +14 -0
  81. package/dist/chat.d.ts.map +1 -0
  82. package/dist/chat.js +8 -0
  83. package/dist/client.d.ts +18 -11
  84. package/dist/client.d.ts.map +1 -1
  85. package/dist/client.js +31 -16
  86. package/dist/collection.d.ts +21 -12
  87. package/dist/collection.d.ts.map +1 -1
  88. package/dist/collection.js +271 -23
  89. package/dist/connection.d.ts +28 -14
  90. package/dist/connection.d.ts.map +1 -1
  91. package/dist/connection.js +1218 -370
  92. package/dist/conversation.d.ts +23 -10
  93. package/dist/conversation.d.ts.map +1 -1
  94. package/dist/conversation.js +304 -136
  95. package/dist/entry.d.ts +17 -0
  96. package/dist/entry.d.ts.map +1 -0
  97. package/dist/entry.js +156 -0
  98. package/dist/expectation.d.ts +21 -0
  99. package/dist/expectation.d.ts.map +1 -0
  100. package/dist/expectation.js +70 -0
  101. package/dist/failure.d.ts +13 -0
  102. package/dist/failure.d.ts.map +1 -0
  103. package/dist/failure.js +19 -0
  104. package/dist/fence.d.ts +6 -0
  105. package/dist/fence.d.ts.map +1 -0
  106. package/dist/fence.js +10 -0
  107. package/dist/host.d.ts +4 -2
  108. package/dist/host.d.ts.map +1 -1
  109. package/dist/host.js +17 -13
  110. package/dist/identity.d.ts +5 -0
  111. package/dist/identity.d.ts.map +1 -1
  112. package/dist/identity.js +19 -1
  113. package/dist/index.d.ts +12 -6
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js +4 -0
  116. package/dist/memory.d.ts +4 -3
  117. package/dist/memory.d.ts.map +1 -1
  118. package/dist/memory.js +63 -21
  119. package/dist/model.d.ts +4 -2
  120. package/dist/model.d.ts.map +1 -1
  121. package/dist/model.js +9 -0
  122. package/dist/observation.d.ts +5 -2
  123. package/dist/observation.d.ts.map +1 -1
  124. package/dist/observation.js +22 -11
  125. package/dist/open.d.ts +2 -0
  126. package/dist/open.d.ts.map +1 -0
  127. package/dist/open.js +1 -0
  128. package/dist/outcome.d.ts +32 -3
  129. package/dist/outcome.d.ts.map +1 -1
  130. package/dist/outcome.js +42 -1
  131. package/dist/overlay.d.ts +15 -3
  132. package/dist/overlay.d.ts.map +1 -1
  133. package/dist/overlay.js +94 -77
  134. package/dist/query.d.ts +32 -3
  135. package/dist/query.d.ts.map +1 -1
  136. package/dist/query.js +3 -0
  137. package/dist/reach.d.ts +2 -0
  138. package/dist/reach.d.ts.map +1 -1
  139. package/dist/reach.js +45 -21
  140. package/dist/react.d.ts +7 -5
  141. package/dist/react.d.ts.map +1 -1
  142. package/dist/react.js +3 -2
  143. package/dist/read-side.d.ts +13 -6
  144. package/dist/read-side.d.ts.map +1 -1
  145. package/dist/read-side.js +291 -85
  146. package/dist/recovery.d.ts +25 -0
  147. package/dist/recovery.d.ts.map +1 -0
  148. package/dist/recovery.js +197 -0
  149. package/dist/refresh.d.ts +10 -0
  150. package/dist/refresh.d.ts.map +1 -1
  151. package/dist/refresh.js +43 -38
  152. package/dist/replica.d.ts +8 -5
  153. package/dist/replica.d.ts.map +1 -1
  154. package/dist/replica.js +114 -67
  155. package/dist/room-draft.d.ts +26 -9
  156. package/dist/room-draft.d.ts.map +1 -1
  157. package/dist/room-draft.js +132 -23
  158. package/dist/rows.d.ts +8 -13
  159. package/dist/rows.d.ts.map +1 -1
  160. package/dist/rows.js +12 -14
  161. package/dist/save.d.ts +31 -1
  162. package/dist/save.d.ts.map +1 -1
  163. package/dist/save.js +376 -45
  164. package/dist/search.d.ts +31 -8
  165. package/dist/search.d.ts.map +1 -1
  166. package/dist/search.js +109 -44
  167. package/dist/shared.d.ts +11 -0
  168. package/dist/shared.d.ts.map +1 -0
  169. package/dist/shared.js +368 -0
  170. package/dist/status.d.ts +2 -2
  171. package/dist/status.d.ts.map +1 -1
  172. package/dist/status.js +0 -8
  173. package/dist/store.d.ts +64 -18
  174. package/dist/store.d.ts.map +1 -1
  175. package/dist/store.js +494 -195
  176. package/dist/turn.d.ts +7 -0
  177. package/dist/turn.d.ts.map +1 -0
  178. package/dist/turn.js +45 -0
  179. package/dist/wire.d.ts +6 -3
  180. package/dist/wire.d.ts.map +1 -1
  181. package/dist/wire.js +14 -2
  182. package/package.json +26 -14
  183. package/dist/attachment.d.ts +0 -3
  184. package/dist/attachment.d.ts.map +0 -1
  185. 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,19 +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;
132
+ let streamed = null;
85
133
  let handed = false;
86
- const submitting = new Map();
134
+ const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
135
+ const repairing = new Set();
136
+ const repairable = new Map();
137
+ let outcomeRetention = null;
87
138
  const answering = new Map();
88
139
  let complete = false;
89
140
  let recorded = false;
90
141
  const stale = new Set();
91
- 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();
92
149
  let mutations = Promise.resolve();
93
150
  const roomListeners = new Set();
94
151
  const refusalListeners = new Set();
@@ -99,7 +156,7 @@ export function composeRuntime(options) {
99
156
  const presentationListeners = new Set();
100
157
  const readSide = createReadSide();
101
158
  let withdrawConversations;
102
- let currencyQueued = false;
159
+ let currencyQueued = null;
103
160
  let socketOpen = false;
104
161
  let serviceMode = 'open';
105
162
  let haltedRefusal = false;
@@ -107,21 +164,31 @@ export function composeRuntime(options) {
107
164
  const serviceModeListeners = new Set();
108
165
  let member = null;
109
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);
110
173
  const conversations = createConversationStore({
111
174
  agents: declaredAgents,
112
- documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
175
+ documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
113
176
  call: (name, input) => invokeAction(name, input),
114
177
  durably,
115
178
  stream: () => options.adapter.conversations,
116
179
  publish: publishChangeSet,
180
+ online: () => !status.providerUnreachable && refusal === null,
117
181
  closed: () => closed,
182
+ unanswered: resendLater,
183
+ expired: (operation) => pastHorizon(operation),
118
184
  });
119
185
  const searchIndex = createSearchIndex({
120
- documents: () => readSide.documents(),
186
+ room: (room) => readSide.room(room),
121
187
  document: (room, path) => readSide.document(room, path),
122
- messages: () => conversations.messages(),
123
- message: (conversation, identity) => conversations.message(conversation, identity),
124
188
  });
189
+ const roomsToIndex = new Set();
190
+ const conversationsToIndex = new Set();
191
+ let indexing = false;
125
192
  const journal = createLog();
126
193
  let notifyReady = () => { };
127
194
  const ready = new Promise((resolve) => {
@@ -185,7 +252,9 @@ export function composeRuntime(options) {
185
252
  }
186
253
  }
187
254
  function pending() {
188
- 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)));
189
258
  }
190
259
  function attention() {
191
260
  const attentionRequired = [];
@@ -214,8 +283,21 @@ export function composeRuntime(options) {
214
283
  }
215
284
  return { attentionRequired, reset };
216
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
+ }
217
295
  function listed() {
218
- 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
+ }));
219
301
  }
220
302
  function roomFacts(reach = null) {
221
303
  const held = [...rooms.values()]
@@ -235,6 +317,7 @@ export function composeRuntime(options) {
235
317
  return { listed: roomsListed, rooms: held };
236
318
  }
237
319
  function emitRooms() {
320
+ readSide.recover();
238
321
  const value = listed();
239
322
  for (const listener of roomListeners)
240
323
  listener(value);
@@ -270,6 +353,8 @@ export function composeRuntime(options) {
270
353
  listener(shown);
271
354
  }
272
355
  function entryOf(batch) {
356
+ if (localSaves)
357
+ return Object.freeze({ batch, state: 'local' });
273
358
  const state = pendings.get(batch.id);
274
359
  if (state?.accepted !== undefined) {
275
360
  return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
@@ -277,10 +362,7 @@ export function composeRuntime(options) {
277
362
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
278
363
  }
279
364
  function entriesOf(locator) {
280
- const replica = replicas.get(locator);
281
- if (replica === undefined)
282
- return [];
283
- return replica.queue.map(entryOf);
365
+ return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
284
366
  }
285
367
  function emitQueue(locator) {
286
368
  const entries = entriesOf(locator);
@@ -290,12 +372,191 @@ export function composeRuntime(options) {
290
372
  }
291
373
  }
292
374
  function applyChangeSet(set) {
293
- readSide.apply(set);
294
- 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(() => { });
295
555
  }
296
556
  function publishChangeSet(set) {
297
557
  applyChangeSet(set);
298
- 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)))
299
560
  emitRooms();
300
561
  for (const locator of set.queues)
301
562
  emitQueue(locator);
@@ -307,34 +568,7 @@ export function composeRuntime(options) {
307
568
  emitPresentation(app);
308
569
  conversations.changed(set);
309
570
  }
310
- function stopTimer() {
311
- if (resetTimer === undefined)
312
- return;
313
- clearTimeout(resetTimer);
314
- resetTimer = undefined;
315
- }
316
- function armTimer() {
317
- stopTimer();
318
- if (closed || stopping)
319
- return;
320
- let earliest = null;
321
- for (const { submission } of pendings.values()) {
322
- if (submission.state !== 'reset')
323
- continue;
324
- const at = Date.parse(submission.reset);
325
- if (earliest === null || at < earliest)
326
- earliest = at;
327
- }
328
- if (earliest === null)
329
- return;
330
- // A Save House held back goes again only once the reset its answer named has passed.
331
- resetTimer = setTimeout(() => {
332
- resetTimer = undefined;
333
- submitAll();
334
- }, Math.max(0, earliest - Date.now()));
335
- }
336
571
  async function refuse(reason, provider = null) {
337
- stopTimer();
338
572
  discoveryPaused = true;
339
573
  try {
340
574
  if (reason === 'revoked-authorization') {
@@ -342,10 +576,7 @@ export function composeRuntime(options) {
342
576
  const revoked = { status: 'unavailable', reason: 'revoked' };
343
577
  try {
344
578
  const local = await openLocal();
345
- await local.revokeModels([...replicas.values()].map((replica) => ({
346
- record: replica.confirmed.record,
347
- queue: replica.queue,
348
- })));
579
+ await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
349
580
  }
350
581
  catch {
351
582
  journal.record('model revocation persistence failed');
@@ -369,10 +600,7 @@ export function composeRuntime(options) {
369
600
  model: revoked,
370
601
  selectionModel: revoked,
371
602
  };
372
- const read = readConfirmed(record, before.confirmed);
373
- replicas.set(locator, read.kind === 'staged'
374
- ? replicaOf(read.confirmed, before.queue, before)
375
- : { ...before, confirmed: { ...before.confirmed, record } });
603
+ replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
376
604
  }
377
605
  rooms.set(locator, {
378
606
  ...room,
@@ -389,7 +617,8 @@ export function composeRuntime(options) {
389
617
  }
390
618
  finally {
391
619
  refusal = {
392
- reason,
620
+ code: reason,
621
+ retryable: false,
393
622
  reauthorizationOffered: reason === 'revoked-authorization',
394
623
  refusal: provider,
395
624
  };
@@ -414,19 +643,28 @@ export function composeRuntime(options) {
414
643
  },
415
644
  providerOwnedPaths: room.providerOwnedPaths,
416
645
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
417
- attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
418
646
  synchronizationAvailable,
419
647
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
420
648
  });
421
649
  }
422
- 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));
423
657
  const listedLocators = new Set(listed.map((room) => room.locator));
424
658
  for (const locator of listedLocators)
425
659
  scopedRefusals.delete(locator);
426
660
  discovered.clear();
427
661
  for (const room of listed)
428
662
  discovered.set(room.locator, room);
429
- 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);
430
668
  for (const room of listed) {
431
669
  const held = replicas.get(room.locator)?.confirmed.record;
432
670
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -437,43 +675,83 @@ export function composeRuntime(options) {
437
675
  }
438
676
  roomsListed = true;
439
677
  emitRooms();
678
+ return listed;
440
679
  }
441
680
  function unlistedHeld(listed) {
442
- return [...new Set([...rooms.keys(), ...roomDrafts.keys()])].filter((locator) => !listed.has(locator));
443
- }
444
- async function dropUnlisted(listed, local) {
445
- if (unlistedHeld(listed).length === 0)
446
- return;
447
- await exclusive(async () => {
448
- const unlisted = unlistedHeld(listed);
449
- await local.dropRooms(unlisted);
450
- const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
451
- room: true,
452
- queue: true,
453
- drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
454
- }));
455
- for (const locator of unlisted) {
456
- for (const batch of replicas.get(locator)?.queue ?? [])
457
- pendings.delete(batch.id);
458
- 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
459
696
  rooms.delete(locator);
460
- roomDrafts.delete(locator);
461
- unreadable.delete(locator);
462
- stale.delete(locator);
463
- scopedRefusals.delete(locator);
464
- sourceRefusals.delete(locator);
465
- declarationRefusals.delete(locator);
466
- for (const attached of ['collection', 'typed-selection']) {
467
- const key = modelKey(locator, attached);
468
- models.delete(key);
469
- liveModels.delete(key);
470
- invalidModels.delete(key);
471
- }
472
- 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);
473
730
  }
474
- publishChangeSet(mergeChangeSets(removed));
475
- moveTo({ ...status, ...attention() });
476
- });
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));
477
755
  }
478
756
  function restored(key, persisted) {
479
757
  const prior = models.get(key);
@@ -494,7 +772,7 @@ export function composeRuntime(options) {
494
772
  return model;
495
773
  }
496
774
  async function hydrate(opened) {
497
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known,] = await Promise.all([
775
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
498
776
  opened.all(),
499
777
  opened.queues(),
500
778
  opened.roomDrafts(),
@@ -505,7 +783,11 @@ export function composeRuntime(options) {
505
783
  opened.providerLimits(),
506
784
  opened.capabilities(),
507
785
  opened.member(),
786
+ opened.transfers(),
508
787
  ]);
788
+ transfers.clear();
789
+ for (const record of staged)
790
+ transfers.set(record.locator, record);
509
791
  limits = declared;
510
792
  capabilities = named;
511
793
  member = known;
@@ -524,18 +806,17 @@ export function composeRuntime(options) {
524
806
  }
525
807
  const previousReplicas = new Map(replicas);
526
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
+ }
527
815
  for (const record of records) {
528
816
  const model = restored(modelKey(record.locator, 'collection'), record.model);
529
817
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
530
818
  const prior = previousReplicas.get(record.locator);
531
- const retyped = { ...record, model, selectionModel };
532
- const read = readConfirmed(retyped, prior?.confirmed);
533
- const confirmed = read.kind === 'staged'
534
- ? read.confirmed
535
- : {
536
- record: retyped,
537
- collection: parseCollection(retyped.documents, compositionOf(retyped)),
538
- };
819
+ const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
539
820
  replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
540
821
  const existing = rooms.get(record.locator);
541
822
  if (existing === undefined)
@@ -544,7 +825,6 @@ export function composeRuntime(options) {
544
825
  rooms.set(record.locator, {
545
826
  ...existing,
546
827
  roomVersion: record.roomVersion,
547
- attachments: record.attachments,
548
828
  operations: record.operations,
549
829
  replica: record.replica,
550
830
  model: modelTypes(model),
@@ -556,10 +836,6 @@ export function composeRuntime(options) {
556
836
  },
557
837
  });
558
838
  }
559
- if (read.kind === 'refused') {
560
- refusedSource(record, read);
561
- declarationRefusals.add(record.locator);
562
- }
563
839
  }
564
840
  return {
565
841
  lastCompleted,
@@ -570,6 +846,7 @@ export function composeRuntime(options) {
570
846
  async function readState(opened) {
571
847
  const { lastCompleted, locators, read } = await hydrate(opened);
572
848
  applyChangeSet(read);
849
+ prepare();
573
850
  if (replicas.size > 0)
574
851
  emitRooms();
575
852
  if (member !== null)
@@ -580,8 +857,15 @@ export function composeRuntime(options) {
580
857
  return lastCompleted;
581
858
  }
582
859
  async function open() {
583
- const opened = await openRuntimeStore();
860
+ if (closed)
861
+ throw new Error('runtime closed');
862
+ const opened = await openRuntimeStore(isFenced);
584
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
+ }
585
869
  const kept = options.shell.generation;
586
870
  const storedGeneration = await opened.generation();
587
871
  const loss = detectStorageLoss(storedGeneration, kept);
@@ -595,6 +879,8 @@ export function composeRuntime(options) {
595
879
  emitStorageLoss();
596
880
  }
597
881
  const lastCompleted = await readState(opened);
882
+ if (interrupted.length > 0)
883
+ await erased(opened, interrupted);
598
884
  moveTo({ ...status, lastCompleted, ...attention() });
599
885
  return opened;
600
886
  }
@@ -621,11 +907,12 @@ export function composeRuntime(options) {
621
907
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
622
908
  }
623
909
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
624
- const withdrawn = withdrawals(confirmed, queue, withdrawable);
910
+ const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
911
+ const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
625
912
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
626
913
  const unqueued = withdrawn.map((batch) => ({
627
914
  batch,
628
- returned: landed(batch, confirmed) ? null : { kind: 'withdrawn' },
915
+ returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
629
916
  }));
630
917
  const returning = [
631
918
  ...[...unqueued].reverse(),
@@ -646,28 +933,48 @@ export function composeRuntime(options) {
646
933
  });
647
934
  }
648
935
  }
649
- await local.write({
650
- next: confirmed.record,
651
- previous: before?.confirmed.record ?? null,
652
- queue: kept,
653
- previousQueue: before?.queue ?? [],
654
- roomDrafts: [...written.values()],
655
- });
656
- for (const batch of before?.queue ?? []) {
936
+ await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
937
+ for (const batch of previousQueue) {
657
938
  if (!kept.includes(batch))
658
939
  pendings.delete(batch.id);
659
940
  }
660
941
  for (const record of written.values())
661
942
  rememberRoomDraft(record);
662
- for (const { batch, returned } of unqueued) {
663
- journal.record(`outcome ${locator} ${batch.id} ${returned === null ? 'landed' : 'withdrawn'}`);
664
- }
665
- replicas.set(locator, replicaOf(confirmed, kept, before));
666
- return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
667
- room: settling.room,
668
- queue: kept.length !== (before?.queue.length ?? 0),
669
- drafts: [...written.keys()],
670
- });
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
+ };
671
978
  }
672
979
  function adoptModels(locator, record) {
673
980
  for (const [attached, model] of [
@@ -700,10 +1007,16 @@ export function composeRuntime(options) {
700
1007
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
701
1008
  };
702
1009
  }
703
- function publish(local, room, confirmed, base) {
1010
+ function publish(local, room, confirmed, landing, live) {
1011
+ const { base, stored } = landing;
704
1012
  return exclusive(async () => {
705
- 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);
706
1018
  return true;
1019
+ }
707
1020
  const prior = replicas.get(room.locator);
708
1021
  if (base !== undefined &&
709
1022
  (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
@@ -711,13 +1024,17 @@ export function composeRuntime(options) {
711
1024
  return false;
712
1025
  }
713
1026
  adoptModels(room.locator, confirmed.record);
714
- const queue = prior?.queue ?? [];
1027
+ const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
715
1028
  const roomVersion = confirmed.record.roomVersion;
716
1029
  const accepted = queue.filter((batch) => {
717
1030
  const accepted = pendings.get(batch.id)?.accepted;
718
1031
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
719
1032
  });
720
- 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
+ }
721
1038
  for (const batch of accepted)
722
1039
  pendings.delete(batch.id);
723
1040
  stale.delete(room.locator);
@@ -726,32 +1043,54 @@ export function composeRuntime(options) {
726
1043
  sourceRefusals.delete(room.locator);
727
1044
  declarationRefusals.delete(room.locator);
728
1045
  list(room, true);
1046
+ if (prior === undefined)
1047
+ prepared.add(room.locator);
729
1048
  publishChangeSet(set);
730
1049
  moveTo({ ...status, ...attention() });
1050
+ options.adapter.roomStream?.published(room.locator, roomVersion);
731
1051
  return true;
732
1052
  });
733
1053
  }
734
1054
  async function saveRoomDrafts(locator, named, writtenBy) {
735
1055
  const local = await openLocal();
1056
+ await referenced(locator);
736
1057
  const replica = replicas.get(locator);
737
1058
  const room = rooms.get(locator);
738
1059
  if (replica === undefined || room === undefined) {
739
- throw new Error(`No Local Replica for ${locator}`);
1060
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
740
1061
  }
741
1062
  const saved = [];
742
1063
  for (const { path, revision } of named) {
743
1064
  const record = roomDrafts.get(locator)?.get(path);
744
1065
  if (record === undefined || record.revision !== revision) {
745
- return { outcome: 'refused', rule: 'stale', file: path };
1066
+ return { outcome: 'refused', code: 'stale', retryable: false, file: path };
746
1067
  }
747
1068
  if (!saved.some((draft) => draft.path === path))
748
1069
  saved.push({ path, parts: record.parts });
749
1070
  }
750
1071
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
751
1072
  if (changed.length > 0)
752
- return { outcome: 'refused', rule: 'changed-underneath', parts: changed };
1073
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
753
1074
  const parts = saved.map(({ path, parts }) => changesOf(path, parts));
754
- 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,
755
1094
  baseRoomVersion: replica.confirmed.record.roomVersion,
756
1095
  composition: compositionFor(replica.confirmed.record),
757
1096
  operations: room.operations,
@@ -759,29 +1098,93 @@ export function composeRuntime(options) {
759
1098
  providerLimits: limits,
760
1099
  handle: room.handle,
761
1100
  visible: replica.visible,
762
- changes: parts.flatMap(({ changes }) => changes),
1101
+ changes,
763
1102
  bases: parts.flatMap(({ bases }) => bases),
764
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 }];
765
1116
  });
766
- 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')
767
1135
  return { outcome: 'refused', ...built.refusal };
768
- 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
+ }
769
1167
  const consumed = saved.map(({ path }) => path);
770
1168
  try {
771
- await local.write({
772
- next: replica.confirmed.record,
773
- previous: replica.confirmed.record,
1169
+ await local.keepAuthored({
1170
+ locator,
774
1171
  queue: batches,
775
- previousQueue: replica.queue,
776
- saved,
1172
+ saved: drafted,
777
1173
  consumedRoomDrafts: consumed,
778
1174
  });
779
1175
  }
780
1176
  catch {
781
- return { outcome: 'refused', rule: 'durable-write' };
1177
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
1178
+ }
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 });
782
1185
  }
783
- pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
784
- recorded = false;
1186
+ if (!localSaves)
1187
+ recorded = false;
785
1188
  for (const path of consumed)
786
1189
  forgetRoomDraft(locator, path);
787
1190
  replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
@@ -789,9 +1192,12 @@ export function composeRuntime(options) {
789
1192
  queue: true,
790
1193
  drafts: consumed,
791
1194
  }));
792
- moveTo({ ...saving(status), ...attention() });
1195
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
793
1196
  submitLater(locator);
794
- 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 };
795
1201
  }
796
1202
  async function mark(local, locator, identifier, submission, lost) {
797
1203
  const state = pendings.get(identifier);
@@ -801,6 +1207,8 @@ export function composeRuntime(options) {
801
1207
  return;
802
1208
  }
803
1209
  await local.submit(locator, identifier, submission, lost);
1210
+ if (fences.has(locator))
1211
+ return;
804
1212
  pendings.set(identifier, { ...state, submission, lost });
805
1213
  const replica = replicas.get(locator);
806
1214
  if (replica !== undefined)
@@ -811,22 +1219,24 @@ export function composeRuntime(options) {
811
1219
  if (!continuing())
812
1220
  throw new Error('connection refused');
813
1221
  await local.keepUpload(locator, identifier, upload);
1222
+ if (fences.has(locator))
1223
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
814
1224
  pendings.set(identifier, { ...pendings.get(identifier), upload });
815
1225
  if (!continuing())
816
1226
  throw new Error('connection refused');
817
1227
  }
818
1228
  async function readHistory(locator, path, query = {}) {
819
1229
  if (closed || refusal !== null)
820
- return { kind: 'transport-uncertainty' };
1230
+ return UNCERTAIN;
821
1231
  try {
822
1232
  await ensureAuthorization();
823
1233
  }
824
1234
  catch {
825
- return { kind: 'transport-uncertainty' };
826
- }
827
- if (closed || authorization === undefined || refusal !== null) {
828
- return { kind: 'transport-uncertainty' };
1235
+ return UNCERTAIN;
829
1236
  }
1237
+ if (closed || authorization === undefined || refusal !== null)
1238
+ return UNCERTAIN;
1239
+ const since = fencings;
830
1240
  const transfer = ['transfer', 'history', locator, path];
831
1241
  if (query.revision !== undefined)
832
1242
  transfer.push(query.revision);
@@ -841,6 +1251,8 @@ export function composeRuntime(options) {
841
1251
  ...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
842
1252
  });
843
1253
  journal.record(`outcome history ${result.kind}`);
1254
+ if (fencedSince(locator, since))
1255
+ return failure({ kind: 'room-not-readable', locator });
844
1256
  if (result.kind === 'scoped-refusal') {
845
1257
  const room = replicas.get(result.locator)?.confirmed.record;
846
1258
  if (room !== undefined)
@@ -848,12 +1260,106 @@ export function composeRuntime(options) {
848
1260
  }
849
1261
  if (result.kind === 'revoked-authorization')
850
1262
  await refuse('revoked-authorization');
851
- return result;
1263
+ return historyAnswer(result);
852
1264
  }
853
1265
  catch {
854
1266
  journal.record('outcome history transport-uncertainty');
855
- 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();
856
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);
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
+ });
857
1363
  }
858
1364
  function declaredAgents() {
859
1365
  for (const capability of capabilities) {
@@ -874,18 +1380,17 @@ export function composeRuntime(options) {
874
1380
  async function invokeAction(name, input) {
875
1381
  await ready;
876
1382
  if (closed || refusal !== null)
877
- return { kind: 'transport-uncertainty' };
1383
+ return UNCERTAIN;
878
1384
  if (declaredAction(name) === undefined)
879
- return { kind: 'undeclared' };
1385
+ return undeclared(name);
880
1386
  try {
881
1387
  await ensureAuthorization();
882
1388
  }
883
1389
  catch {
884
- return { kind: 'transport-uncertainty' };
885
- }
886
- if (closed || authorization === undefined || refusal !== null) {
887
- return { kind: 'transport-uncertainty' };
1390
+ return UNCERTAIN;
888
1391
  }
1392
+ if (closed || authorization === undefined || refusal !== null)
1393
+ return UNCERTAIN;
889
1394
  journal.record(`transfer action ${name}`);
890
1395
  try {
891
1396
  const result = await options.adapter.action(authorization, { name, input });
@@ -899,11 +1404,11 @@ export function composeRuntime(options) {
899
1404
  journal.record(`outcome action ${result.kind}`);
900
1405
  if (result.kind === 'rejected')
901
1406
  journal.record(`refusal rejected ${result.code}`);
902
- return result;
1407
+ return calledResult(result);
903
1408
  }
904
1409
  catch {
905
1410
  journal.record('outcome action transport-uncertainty');
906
- return { kind: 'transport-uncertainty' };
1411
+ return UNCERTAIN;
907
1412
  }
908
1413
  }
909
1414
  function continuing() {
@@ -920,11 +1425,11 @@ export function composeRuntime(options) {
920
1425
  options.adapter.reached(locator, written, roomVersion) &&
921
1426
  !options.adapter.reached(locator, roomVersion, written));
922
1427
  }
923
- async function pullRefresh(authorized, confirmed) {
924
- if (!continuing())
1428
+ async function pullRefresh(authorized, confirmed, live) {
1429
+ if (!live())
925
1430
  return unconfirmed;
926
1431
  try {
927
- 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);
928
1433
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
929
1434
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
930
1435
  return result;
@@ -934,28 +1439,69 @@ export function composeRuntime(options) {
934
1439
  throw error;
935
1440
  }
936
1441
  }
937
- async function pullBootstrap(authorized, room) {
938
- 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())
939
1471
  return unconfirmed;
1472
+ if (replicas.has(room.locator)) {
1473
+ catching.add(room.locator);
1474
+ readSide.recover();
1475
+ }
940
1476
  try {
941
1477
  const result = await bootstrapRoom(options.adapter, authorized, room, {
942
1478
  collection: modelOf(room.locator),
943
1479
  selection: modelOf(room.locator, 'typed-selection'),
944
- }, (models, held) => remodel(room.locator, models, held), continuing);
1480
+ }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
945
1481
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
946
1482
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1483
+ if (result.kind !== 'staged')
1484
+ caughtUpTo(room.locator);
947
1485
  return result;
948
1486
  }
949
1487
  catch (error) {
950
1488
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
1489
+ caughtUpTo(room.locator);
951
1490
  throw error;
952
1491
  }
953
1492
  }
954
- 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;
955
1501
  const held = replicas.get(room.locator);
956
1502
  if (held !== undefined) {
957
- const refreshed = await pullRefresh(authorized, held.confirmed);
958
- if (!continuing())
1503
+ const refreshed = await pullRefresh(authorized, held.confirmed, live);
1504
+ if (!live())
959
1505
  return unconfirmed;
960
1506
  if (refreshed.kind === 'still-current') {
961
1507
  stale.delete(room.locator);
@@ -969,31 +1515,45 @@ export function composeRuntime(options) {
969
1515
  return null;
970
1516
  }
971
1517
  if (refreshed.kind === 'advanced') {
972
- if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
1518
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
973
1519
  const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
974
1520
  const reached = now !== undefined &&
975
1521
  options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
976
- return reached ? null : advance(room, local, authorized);
1522
+ return reached ? null : advance(room, local, authorized, live);
977
1523
  }
978
1524
  await retainModel(local, room, 'collection', modelOf(room.locator));
979
1525
  return null;
980
1526
  }
981
1527
  if (refreshed.kind !== 'beyond-horizon')
982
1528
  return refreshed;
1529
+ if (opened !== null) {
1530
+ owed.add(room.locator);
1531
+ readSide.recover();
1532
+ return unconfirmed;
1533
+ }
983
1534
  }
984
- const bootstrapped = await pullBootstrap(authorized, room);
985
- if (!continuing())
1535
+ const bootstrapped = await pullBootstrap(authorized, room, local, live);
1536
+ if (!live())
986
1537
  return unconfirmed;
987
1538
  if (bootstrapped.kind !== 'staged')
988
1539
  return bootstrapped;
989
- await publish(local, room, bootstrapped.confirmed);
1540
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
990
1541
  return null;
991
1542
  }
992
1543
  async function withhold(withheld, room, local, progress) {
1544
+ if (withheld.kind === 'unconfirmed')
1545
+ return;
993
1546
  if (withheld.kind === 'refused')
994
1547
  stale.delete(room.locator);
995
1548
  else
996
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);
997
1557
  if (withheld.kind === 'scoped-refusal') {
998
1558
  await scoped(withheld, room, local);
999
1559
  }
@@ -1018,6 +1578,7 @@ export function composeRuntime(options) {
1018
1578
  function refusedSource(room, { refusal }) {
1019
1579
  sourceRefusals.set(room.locator, {
1020
1580
  code: refusal.rule,
1581
+ retryable: false,
1021
1582
  fields: {
1022
1583
  file: refusal.file,
1023
1584
  ...(refusal.location === undefined ? {} : { location: refusal.location }),
@@ -1032,8 +1593,16 @@ export function composeRuntime(options) {
1032
1593
  }
1033
1594
  function leaving(batch, result) {
1034
1595
  if (result.kind === 'expired')
1035
- return { batch, returned: { kind: 'expired' } };
1036
- 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
+ };
1037
1606
  }
1038
1607
  function logAccepted(locator, submitted, accepted) {
1039
1608
  journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
@@ -1041,6 +1610,7 @@ export function composeRuntime(options) {
1041
1610
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1042
1611
  }
1043
1612
  async function adoptAccepted(room, local, submitted, accepted) {
1613
+ await referenced(room.locator);
1044
1614
  if (!continuing())
1045
1615
  return unconfirmed;
1046
1616
  const current = replicas.get(room.locator);
@@ -1051,22 +1621,23 @@ export function composeRuntime(options) {
1051
1621
  logAccepted(room.locator, submitted, accepted);
1052
1622
  const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1053
1623
  ? { kind: 'staged', confirmed: current.confirmed }
1054
- : applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.changes, 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)
1055
1625
  ? accepted.roomVersion
1056
1626
  : held, accepted.roomVersion);
1057
1627
  if (applied.kind !== 'staged') {
1058
1628
  const state = pendings.get(submitted.id);
1059
1629
  await local.accept(room.locator, submitted.id, accepted.roomVersion);
1060
- if (state !== undefined)
1630
+ if (state !== undefined && !fences.has(room.locator))
1061
1631
  pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1062
1632
  emitQueue(room.locator);
1063
1633
  return applied;
1064
1634
  }
1065
- 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 });
1066
1636
  pendings.delete(submitted.id);
1067
1637
  unreadable.delete(room.locator);
1068
1638
  sourceRefusals.delete(room.locator);
1069
1639
  declarationRefusals.delete(room.locator);
1640
+ const set = settled();
1070
1641
  list(room, true);
1071
1642
  publishChangeSet(set);
1072
1643
  moveTo({ ...status, ...attention() });
@@ -1080,14 +1651,14 @@ export function composeRuntime(options) {
1080
1651
  refusedSource(room, failed);
1081
1652
  return false;
1082
1653
  }
1083
- async function submitRoom(room, local, authorized, progress) {
1084
- while (continuing() && authorization !== undefined) {
1654
+ async function submitRoom(room, local, authorized, progress, signal) {
1655
+ while (continuing() && authorization !== undefined && !signal.aborted) {
1085
1656
  const replica = replicas.get(room.locator);
1086
1657
  const head = replica?.queue[0];
1087
1658
  if (replica === undefined || head === undefined)
1088
- return;
1659
+ return 'done';
1089
1660
  if (pendings.get(head.id)?.accepted !== undefined)
1090
- return;
1661
+ return 'done';
1091
1662
  const state = pendings.get(head.id);
1092
1663
  const { submission, lost: earlier } = state ?? {
1093
1664
  submission: { state: 'waiting' },
@@ -1095,24 +1666,30 @@ export function composeRuntime(options) {
1095
1666
  };
1096
1667
  if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
1097
1668
  progress.complete = false;
1098
- return;
1669
+ return { at: Date.parse(submission.reset) };
1099
1670
  }
1100
1671
  if (writesHeld()) {
1101
1672
  progress.complete = false;
1102
1673
  journal.record(`submission ${room.locator} ${head.id} held`);
1103
- return;
1674
+ return 'done';
1104
1675
  }
1105
1676
  const lost = earlier || submission.state === 'submitted';
1677
+ if (lost && pastHorizon(head.id)) {
1678
+ await discardPending(room, local, signal);
1679
+ return 'done';
1680
+ }
1106
1681
  let result;
1107
1682
  journal.record(`submission ${room.locator} ${head.id}`);
1108
1683
  await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1109
- if (discoveryPaused)
1110
- return;
1111
- if (writesHeld()) {
1684
+ if (discoveryPaused || fences.has(room.locator))
1685
+ return 'done';
1686
+ if (signal.aborted || writesHeld()) {
1112
1687
  await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
1688
+ if (signal.aborted)
1689
+ return 'done';
1113
1690
  progress.complete = false;
1114
1691
  journal.record(`submission ${room.locator} ${head.id} held`);
1115
- return;
1692
+ return 'done';
1116
1693
  }
1117
1694
  const opensBefore = openSignals;
1118
1695
  const answered = awaitingAnswer(room.locator);
@@ -1128,32 +1705,36 @@ export function composeRuntime(options) {
1128
1705
  progress.complete = false;
1129
1706
  if (continuing())
1130
1707
  progress.reachable = false;
1131
- return;
1708
+ return 'again';
1709
+ }
1710
+ if (signal.aborted) {
1711
+ answered();
1712
+ return 'done';
1132
1713
  }
1133
1714
  if (result.kind !== 'accepted' || !continuing())
1134
1715
  answered();
1135
1716
  if (!continuing())
1136
- return;
1717
+ return 'done';
1137
1718
  if (result.kind === 'transport-uncertainty') {
1138
1719
  progress.complete = false;
1139
1720
  progress.reachable = false;
1140
- return;
1721
+ return 'again';
1141
1722
  }
1142
1723
  if (result.kind === 'room-not-readable') {
1143
1724
  progress.complete = false;
1144
1725
  unreadable.add(room.locator);
1145
1726
  list(room, false);
1146
1727
  emitRooms();
1147
- return;
1728
+ return 'done';
1148
1729
  }
1149
1730
  if (result.kind === 'revoked-authorization') {
1150
1731
  await refuse('revoked-authorization');
1151
- return;
1732
+ return 'done';
1152
1733
  }
1153
1734
  if (result.kind === 'scoped-refusal') {
1154
1735
  await scoped(result, room, local);
1155
1736
  progress.complete = false;
1156
- return;
1737
+ return 'done';
1157
1738
  }
1158
1739
  if (result.kind === 'write-halted') {
1159
1740
  journal.record(`outcome ${room.locator} ${head.id} write-halted`);
@@ -1162,16 +1743,24 @@ export function composeRuntime(options) {
1162
1743
  continue;
1163
1744
  haltedRefusal = true;
1164
1745
  progress.complete = false;
1165
- return;
1746
+ return 'done';
1166
1747
  }
1167
1748
  if (result.kind === 'conflicted' ||
1168
1749
  result.kind === 'rejected' ||
1169
1750
  result.kind === 'expired' ||
1170
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
+ }
1171
1757
  if (result.kind === 'expired') {
1172
- const withheld = await advance(room, local, authorized);
1173
- if (withheld !== null)
1174
- 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
+ }
1175
1764
  }
1176
1765
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
1177
1766
  (result.kind === 'conflicted' && result.roomVersion !== undefined
@@ -1184,8 +1773,7 @@ export function composeRuntime(options) {
1184
1773
  const reset = result.reset;
1185
1774
  progress.complete = false;
1186
1775
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1187
- armTimer();
1188
- return;
1776
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
1189
1777
  }
1190
1778
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
1191
1779
  const left = leaving(head, result);
@@ -1196,37 +1784,82 @@ export function composeRuntime(options) {
1196
1784
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
1197
1785
  return;
1198
1786
  }
1199
- const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1787
+ const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1200
1788
  leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1201
1789
  (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1202
1790
  ? { ...left, returned: null }
1203
1791
  : left,
1204
1792
  });
1205
- publishChangeSet(set);
1793
+ publishChangeSet(settled());
1206
1794
  moveTo({ ...status, ...attention() });
1207
1795
  });
1208
1796
  if (result.kind === 'expired')
1209
1797
  continue;
1210
- const withheld = await advance(room, local, authorized);
1211
- if (withheld !== null)
1212
- 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
+ }
1213
1803
  continue;
1214
1804
  }
1215
1805
  const kept = await adopted(room, local, head, result).finally(answered);
1216
1806
  if (!kept) {
1217
1807
  progress.complete = false;
1218
- return;
1808
+ return 'done';
1219
1809
  }
1220
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);
1221
1854
  }
1222
1855
  function awaitingAnswer(locator) {
1223
1856
  let release = () => { };
1224
1857
  const awaited = new Promise((resolve) => {
1225
1858
  release = resolve;
1226
1859
  });
1227
- answering.set(locator, awaited);
1860
+ answering.set(locator, { generation, answered: awaited });
1228
1861
  return () => {
1229
- if (answering.get(locator) === awaited)
1862
+ if (answering.get(locator)?.answered === awaited)
1230
1863
  answering.delete(locator);
1231
1864
  release();
1232
1865
  };
@@ -1259,7 +1892,7 @@ export function composeRuntime(options) {
1259
1892
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1260
1893
  };
1261
1894
  const reclassified = sameMembership(declared, held.confirmed.record)
1262
- ? readConfirmed(declared, held.confirmed)
1895
+ ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
1263
1896
  : readReclassified(declared, held.confirmed);
1264
1897
  const record = reclassified.kind === 'staged'
1265
1898
  ? declared
@@ -1267,12 +1900,10 @@ export function composeRuntime(options) {
1267
1900
  ...declared,
1268
1901
  typedSelection: held.confirmed.record.typedSelection,
1269
1902
  };
1270
- const read = reclassified.kind === 'deferred' ? readConfirmed(record, held.confirmed) : reclassified;
1271
- await local.write({
1272
- next: record,
1273
- previous: held.confirmed.record,
1274
- queue: held.queue,
1275
- });
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 });
1276
1907
  if (read.kind === 'staged') {
1277
1908
  replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1278
1909
  if (declarationRefusals.delete(room.locator)) {
@@ -1299,7 +1930,11 @@ export function composeRuntime(options) {
1299
1930
  });
1300
1931
  }
1301
1932
  async function scoped(result, room, local) {
1302
- 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
+ });
1303
1938
  let scopedAway = room;
1304
1939
  if (result.scope === 'model')
1305
1940
  await retainModel(local, room, 'collection', {
@@ -1336,13 +1971,14 @@ export function composeRuntime(options) {
1336
1971
  roomVersion: record.roomVersion,
1337
1972
  documents: record.documents,
1338
1973
  revisions: record.revisions,
1339
- attachments: record.attachments,
1340
1974
  },
1341
1975
  ];
1342
1976
  });
1343
1977
  }
1344
- async function synchronize(listed, local, progress, attempt) {
1978
+ async function synchronize(listed, local, progress, attempt, live) {
1345
1979
  const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1980
+ if (!live())
1981
+ return;
1346
1982
  const ready = listed.filter((_, index) => {
1347
1983
  const outcome = prepared[index];
1348
1984
  return outcome?.status === 'fulfilled' && outcome.value;
@@ -1351,9 +1987,10 @@ export function composeRuntime(options) {
1351
1987
  options.adapter.roomStream?.hold(heldRooms(ready));
1352
1988
  handed = true;
1353
1989
  }
1990
+ discovering?.release();
1354
1991
  const loaded = await Promise.allSettled(ready
1355
1992
  .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1356
- .map((room) => loadRoom(room, local, progress)));
1993
+ .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
1357
1994
  const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1358
1995
  if (failed !== undefined)
1359
1996
  throw failed.reason;
@@ -1382,14 +2019,14 @@ export function composeRuntime(options) {
1382
2019
  }
1383
2020
  return true;
1384
2021
  }
1385
- async function caughtUp(room, local, progress) {
2022
+ async function caughtUp(room, local, progress, live) {
1386
2023
  const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1387
- if (held === undefined || authorization === undefined)
2024
+ if (held === undefined || authorization === undefined || !live())
1388
2025
  return;
1389
2026
  const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1390
2027
  if (!behind)
1391
2028
  return;
1392
- const withheld = await advance(room, local, authorization);
2029
+ const withheld = await advance(room, local, authorization, live);
1393
2030
  if (withheld !== null)
1394
2031
  await withhold(withheld, room, local, progress);
1395
2032
  }
@@ -1399,39 +2036,46 @@ export function composeRuntime(options) {
1399
2036
  return model.status === 'unavailable' && model.reason === 'revoked';
1400
2037
  });
1401
2038
  }
1402
- async function loadRoom(room, local, progress) {
1403
- if (discoveryPaused || authorization === undefined)
2039
+ async function loadRoom(room, local, progress, live) {
2040
+ if (!live() || authorization === undefined)
1404
2041
  return;
1405
2042
  if (replicas.has(room.locator) && revoked(room.locator)) {
1406
- await reload(room, local, progress);
2043
+ await reload(room, local, progress, live);
1407
2044
  return;
1408
2045
  }
1409
- const withheld = await advance(room, local, authorization);
2046
+ const withheld = await advance(room, local, authorization, live);
1410
2047
  if (withheld !== null) {
1411
2048
  await withhold(withheld, room, local, progress);
1412
2049
  return;
1413
2050
  }
1414
- await caughtUp(room, local, progress);
2051
+ await caughtUp(room, local, progress, live);
1415
2052
  }
1416
- async function reload(room, local, progress) {
1417
- if (discoveryPaused || authorization === undefined)
1418
- return;
1419
- const bootstrapped = await pullBootstrap(authorization, room);
1420
- if (!continuing())
1421
- return;
1422
- if (bootstrapped.kind !== 'staged') {
1423
- await withhold(bootstrapped, room, local, progress);
1424
- 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);
1425
2069
  }
1426
- await publish(local, room, bootstrapped.confirmed);
1427
- await caughtUp(room, local, progress);
1428
2070
  }
1429
- async function discoverRooms(local, progress, attempt) {
2071
+ async function discoverRooms(local, progress, attempt, live) {
1430
2072
  if (closed || authorization === undefined) {
1431
2073
  notifyReady();
1432
2074
  return;
1433
2075
  }
1434
2076
  moveTo(transferring(status));
2077
+ discoveries += 1;
2078
+ const started = discoveries;
1435
2079
  let result;
1436
2080
  try {
1437
2081
  result = await options.adapter.discover(authorization);
@@ -1439,7 +2083,8 @@ export function composeRuntime(options) {
1439
2083
  catch {
1440
2084
  result = { kind: 'transport-uncertainty' };
1441
2085
  }
1442
- if (closed) {
2086
+ if (!live()) {
2087
+ progress.complete = false;
1443
2088
  notifyReady();
1444
2089
  return;
1445
2090
  }
@@ -1458,7 +2103,11 @@ export function composeRuntime(options) {
1458
2103
  }
1459
2104
  if (result.kind === 'contract-unavailable') {
1460
2105
  journal.record(`connection contract unavailable ${result.code}`);
1461
- 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
+ });
1462
2111
  notifyReady();
1463
2112
  return;
1464
2113
  }
@@ -1493,6 +2142,7 @@ export function composeRuntime(options) {
1493
2142
  }
1494
2143
  const previousCapabilities = capabilities;
1495
2144
  capabilities = result.capabilities;
2145
+ outcomeRetention = result.retention.outcome;
1496
2146
  notifyReady();
1497
2147
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1498
2148
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
@@ -1505,8 +2155,7 @@ export function composeRuntime(options) {
1505
2155
  member = signedIn;
1506
2156
  emitMember();
1507
2157
  }
1508
- await applyDiscovery(result.rooms, local);
1509
- await synchronize(result.rooms, local, progress, attempt);
2158
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
1510
2159
  }
1511
2160
  async function ensureAuthorization() {
1512
2161
  if (authorization !== undefined)
@@ -1520,32 +2169,64 @@ export function composeRuntime(options) {
1520
2169
  throw error;
1521
2170
  }
1522
2171
  }
1523
- 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) {
1524
2192
  if (stopping)
1525
- return chain;
1526
- chain = chain.then(work, work).catch(() => undefined);
1527
- 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;
1528
2204
  }
1529
2205
  function sync(attempt) {
1530
- return task(async () => {
1531
- if (closed || stopping || discoveryPaused) {
2206
+ return task(OPENING_LANE, async (live) => {
2207
+ if (!live()) {
1532
2208
  notifyReady();
1533
2209
  return;
1534
2210
  }
1535
2211
  const progress = { complete: true, reachable: true };
1536
- 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;
1537
2218
  complete = false;
1538
2219
  recorded = false;
1539
2220
  moveTo(transferring(status));
1540
2221
  handed = false;
1541
2222
  try {
1542
2223
  const local = await openLocal();
1543
- if (closed)
2224
+ if (!live())
1544
2225
  return;
1545
2226
  await ensureAuthorization();
1546
- if (closed)
2227
+ if (!live())
1547
2228
  return;
1548
- await discoverRooms(local, progress, attempt);
2229
+ await discoverRooms(local, progress, attempt, live);
1549
2230
  }
1550
2231
  catch {
1551
2232
  progress.complete = false;
@@ -1553,17 +2234,21 @@ export function composeRuntime(options) {
1553
2234
  progress.reachable = false;
1554
2235
  }
1555
2236
  finally {
2237
+ gate.release();
1556
2238
  notifyReady();
1557
2239
  }
2240
+ if (!live())
2241
+ return;
1558
2242
  if (attempt !== 'opened') {
1559
- await finishOpening(progress);
2243
+ await finishOpening(progress, live);
1560
2244
  return;
1561
2245
  }
1562
2246
  if (handed || closed || progress.reachable)
1563
2247
  return;
1564
2248
  underway = null;
2249
+ opened = null;
1565
2250
  journal.record('connection reopen');
1566
- await rest(progress);
2251
+ await rest(progress, live);
1567
2252
  options.adapter.roomStream?.reopen();
1568
2253
  });
1569
2254
  }
@@ -1574,29 +2259,51 @@ export function composeRuntime(options) {
1574
2259
  void refuse('revoked-authorization');
1575
2260
  return;
1576
2261
  }
1577
- if (opened || status.providerUnreachable)
1578
- return;
1579
- 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
+ });
1580
2270
  }
1581
2271
  function applyOpening(expired) {
1582
- return task(async () => {
1583
- 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
+ if (opened === generation)
2276
+ opened = null;
2277
+ return task(OPENING_LANE, async (live) => {
1584
2278
  underway = null;
1585
- if (closed || stopping)
2279
+ if (!live())
1586
2280
  return;
1587
2281
  if (progress === null) {
1588
- await reloadExpired(expired);
2282
+ await replacing;
2283
+ await rest(cycle, live);
1589
2284
  return;
1590
2285
  }
2286
+ await roomsCarried(expired);
2287
+ if (!live())
2288
+ return;
2289
+ const replaced = new Set(owed);
2290
+ owed.clear();
1591
2291
  try {
1592
2292
  const local = await openLocal();
1593
2293
  const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1594
- const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1595
- 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)];
1596
2301
  if (progress.reachable) {
1597
- for (const room of held)
1598
- if (!reloading.includes(room))
2302
+ for (const room of held) {
2303
+ if (!expired.includes(room.locator) && !reloading.includes(room)) {
1599
2304
  await confirmModels(room, local);
2305
+ }
2306
+ }
1600
2307
  }
1601
2308
  const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1602
2309
  if (failed !== undefined)
@@ -1606,21 +2313,21 @@ export function composeRuntime(options) {
1606
2313
  progress.complete = false;
1607
2314
  progress.reachable = false;
1608
2315
  }
1609
- await finishOpening(progress);
2316
+ if (live())
2317
+ await finishOpening(progress, live);
1610
2318
  });
1611
2319
  }
1612
- async function reloadExpired(expired) {
1613
- const progress = { complete: true, reachable: true };
2320
+ async function reloadNow(replaced, progress) {
1614
2321
  try {
1615
2322
  const local = await openLocal();
1616
- await Promise.allSettled([...discovered.values()]
1617
- .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1618
- .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))));
1619
2326
  }
1620
2327
  catch {
1621
2328
  progress.reachable = false;
2329
+ return [];
1622
2330
  }
1623
- await rest(progress);
1624
2331
  }
1625
2332
  async function confirmModels(room, local) {
1626
2333
  for (const attached of ['collection', 'typed-selection']) {
@@ -1631,38 +2338,96 @@ export function composeRuntime(options) {
1631
2338
  }
1632
2339
  }
1633
2340
  function submitLater(locator) {
1634
- const running = submitting.get(locator);
1635
- if (running !== undefined) {
1636
- running.again = true;
2341
+ if (localSaves)
1637
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);
1638
2356
  }
1639
- const run = { again: false, done: Promise.resolve() };
1640
- submitting.set(locator, run);
1641
- run.done = (async () => {
1642
- try {
1643
- do {
1644
- run.again = false;
1645
- const room = discovered.get(locator);
1646
- if (closed || stopping || discoveryPaused || room === undefined)
1647
- return;
1648
- const progress = { complete: true, reachable: true };
1649
- try {
1650
- await ensureAuthorization();
1651
- if (authorization === undefined)
1652
- return;
1653
- await submitRoom(room, await openLocal(), authorization, progress);
1654
- }
1655
- catch {
1656
- progress.reachable = false;
1657
- }
1658
- armTimer();
1659
- await rest(progress);
1660
- } while (run.again);
1661
- }
1662
- finally {
1663
- submitting.delete(locator);
1664
- }
1665
- })();
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';
1666
2431
  }
1667
2432
  function submitAll() {
1668
2433
  for (const [locator, replica] of replicas) {
@@ -1670,31 +2435,40 @@ export function composeRuntime(options) {
1670
2435
  submitLater(locator);
1671
2436
  }
1672
2437
  }
1673
- async function finishOpening(progress) {
2438
+ async function finishOpening(progress, live) {
1674
2439
  try {
1675
- if (closed || discoveryPaused) {
2440
+ if (!live()) {
1676
2441
  progress.complete = false;
1677
2442
  return;
1678
2443
  }
1679
2444
  submitAll();
2445
+ for (const locator of stale)
2446
+ if (repairable.has(locator))
2447
+ repairLater(locator, {});
1680
2448
  await conversations.reconcile();
1681
2449
  if (status.providerUnreachable)
1682
2450
  conversations.reconnected();
1683
2451
  const reading = { complete: true, reachable: true };
1684
2452
  await conversations.catchUp(reading);
1685
- if (!reading.reachable)
2453
+ if (progress.reachable && reading.reachable)
2454
+ await conversations.resend(reading);
2455
+ if (!reading.reachable) {
1686
2456
  progress.reachable = false;
2457
+ resendLater();
2458
+ }
1687
2459
  }
1688
2460
  catch {
1689
2461
  progress.complete = false;
1690
2462
  }
1691
2463
  finally {
1692
- complete = progress.complete;
1693
- await rest(progress);
2464
+ if (live()) {
2465
+ complete = progress.complete;
2466
+ await rest(progress, live);
2467
+ }
1694
2468
  }
1695
2469
  }
1696
- async function rest(progress) {
1697
- if (closed)
2470
+ async function rest(progress, live = () => !closed) {
2471
+ if (!live())
1698
2472
  return;
1699
2473
  let lastCompleted = status.lastCompleted;
1700
2474
  let synced = complete && stale.size === 0 && !pending();
@@ -1702,6 +2476,8 @@ export function composeRuntime(options) {
1702
2476
  const completedAt = new Date().toISOString();
1703
2477
  try {
1704
2478
  await (await openLocal()).complete(completedAt);
2479
+ if (!live())
2480
+ return;
1705
2481
  recorded = true;
1706
2482
  lastCompleted = completedAt;
1707
2483
  }
@@ -1710,17 +2486,29 @@ export function composeRuntime(options) {
1710
2486
  }
1711
2487
  }
1712
2488
  moveTo({
1713
- state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
2489
+ state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
1714
2490
  lastCompleted,
1715
2491
  providerUnreachable: !progress.reachable,
1716
2492
  ...attention(),
1717
2493
  });
1718
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
+ }
1719
2503
  function applyRoom(locator, roomVersion) {
1720
2504
  announced.set(locator, roomVersion);
1721
- return task(async () => {
1722
- await answering.get(locator);
1723
- 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)
1724
2512
  return;
1725
2513
  const room = discovered.get(locator);
1726
2514
  const held = replicas.get(locator);
@@ -1731,25 +2519,41 @@ export function composeRuntime(options) {
1731
2519
  options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
1732
2520
  return;
1733
2521
  }
1734
- const during = underway;
2522
+ const during = underway?.generation === generation ? underway.progress : null;
1735
2523
  const progress = during ?? { complete: true, reachable: true };
1736
2524
  if (during === null)
1737
2525
  moveTo(transferring(status));
1738
2526
  try {
1739
2527
  const local = await openLocal();
1740
- const withheld = await advance(room, local, authorization);
1741
- if (withheld !== null)
1742
- 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
+ });
1743
2535
  }
1744
2536
  catch {
1745
2537
  stale.add(locator);
1746
2538
  }
1747
2539
  finally {
1748
2540
  if (during === null)
1749
- await rest(progress);
2541
+ await rest(progress, live);
1750
2542
  }
1751
2543
  });
1752
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
+ };
1753
2557
  function observeStream() {
1754
2558
  const stream = options.adapter.roomStream;
1755
2559
  if (stream === undefined || withdrawStream !== undefined)
@@ -1759,6 +2563,9 @@ export function composeRuntime(options) {
1759
2563
  return;
1760
2564
  if (event.kind === 'opened') {
1761
2565
  socketOpen = true;
2566
+ generation += 1;
2567
+ conversations.reopened();
2568
+ recovery.resume();
1762
2569
  void sync('opened');
1763
2570
  }
1764
2571
  else if (event.kind === 'closed')
@@ -1767,7 +2574,7 @@ export function composeRuntime(options) {
1767
2574
  void applyOpening(event.expired);
1768
2575
  else
1769
2576
  void applyRoom(event.locator, event.roomVersion);
1770
- });
2577
+ }, receipts);
1771
2578
  }
1772
2579
  function writesHeld() {
1773
2580
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -1805,34 +2612,30 @@ export function composeRuntime(options) {
1805
2612
  });
1806
2613
  }
1807
2614
  function scheduleCurrency() {
1808
- if (currencyQueued || closed || stopping)
2615
+ if (currencyQueued === generation || closed || stopping)
1809
2616
  return;
1810
- currencyQueued = true;
1811
- void task(runCurrency);
2617
+ currencyQueued = generation;
2618
+ void task(CONVERSATION_LANE, runCurrency);
1812
2619
  }
1813
- async function runCurrency() {
1814
- currencyQueued = false;
1815
- if (closed || stopping || discoveryPaused)
2620
+ async function runCurrency(live) {
2621
+ if (currencyQueued === generation)
2622
+ currencyQueued = null;
2623
+ if (!live())
1816
2624
  return;
1817
2625
  await conversations.reconcile();
1818
2626
  if (conversations.current()) {
1819
2627
  show();
1820
2628
  return;
1821
2629
  }
1822
- const resting = status;
1823
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1824
- moveTo(transferring(resting));
2630
+ const progress = { complete: false, reachable: !status.providerUnreachable };
2631
+ moveTo(transferring(status));
1825
2632
  try {
1826
2633
  await conversations.catchUp(progress);
1827
2634
  }
1828
2635
  catch {
1829
2636
  progress.reachable = false;
1830
2637
  }
1831
- finally {
1832
- if (!closed) {
1833
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1834
- }
1835
- }
2638
+ await rest(progress, live);
1836
2639
  }
1837
2640
  function close() {
1838
2641
  if (closed)
@@ -1844,7 +2647,7 @@ export function composeRuntime(options) {
1844
2647
  withdrawConversations = undefined;
1845
2648
  withdrawServiceMode?.();
1846
2649
  conversations.close();
1847
- stopTimer();
2650
+ void recovery.retire();
1848
2651
  store?.close();
1849
2652
  void opening?.then((opened) => {
1850
2653
  opened.close();
@@ -1862,9 +2665,8 @@ export function composeRuntime(options) {
1862
2665
  }
1863
2666
  async function stop() {
1864
2667
  stopping = true;
1865
- stopTimer();
1866
- await chain.catch(() => undefined);
1867
- await Promise.all([...submitting.values()].map((run) => run.done));
2668
+ await recovery.retire();
2669
+ await Promise.all(working);
1868
2670
  await mutations;
1869
2671
  close();
1870
2672
  }
@@ -1886,14 +2688,54 @@ export function composeRuntime(options) {
1886
2688
  return planOf(input, roomFacts(reach), planReach(reach));
1887
2689
  }
1888
2690
  function watchPlan(plan, deliver) {
1889
- 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);
1890
2696
  }
1891
2697
  async function searched(input, reach) {
1892
2698
  const plan = searchPlanOf(input);
1893
- const selected = plan.documents === undefined && reach === null
1894
- ? null
1895
- : (await readSide.query(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
1896
- 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
+ }
1897
2739
  }
1898
2740
  const watch = (topic, input, deliver) => {
1899
2741
  if (topic === 'room-drafts') {
@@ -1918,6 +2760,8 @@ export function composeRuntime(options) {
1918
2760
  }
1919
2761
  if (topic === 'conversations')
1920
2762
  return conversations.watchList(deliver);
2763
+ if (topic === 'agents')
2764
+ return conversations.watchAgents(deliver);
1921
2765
  if (topic === 'presentation') {
1922
2766
  const listener = {
1923
2767
  app: input[0],
@@ -1996,7 +2840,7 @@ export function composeRuntime(options) {
1996
2840
  rooms() {
1997
2841
  return listed();
1998
2842
  },
1999
- query: ((input) => readSide.query(planned(input, null))),
2843
+ query: ((input) => rowsQuery(planned(input, null))),
2000
2844
  search: (input) => searched(input, null),
2001
2845
  types(locator) {
2002
2846
  return {
@@ -2025,20 +2869,20 @@ export function composeRuntime(options) {
2025
2869
  return exclusive(async () => {
2026
2870
  const parts = readParts(input);
2027
2871
  if (path === '' || normalize(path) !== path)
2028
- throw new Error('room-draft-malformed');
2872
+ throw runtimeError('room-draft-malformed');
2029
2873
  const local = await openLocal();
2030
2874
  const current = roomDrafts.get(room)?.get(path);
2031
2875
  if ((current?.revision ?? null) !== revision) {
2032
- return {
2033
- outcome: 'stale',
2034
- current: current === undefined ? null : roomDraftOf(current),
2035
- };
2876
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2036
2877
  }
2878
+ const file = parts.file === undefined
2879
+ ? undefined
2880
+ : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
2037
2881
  const record = {
2038
2882
  locator: room,
2039
2883
  path,
2040
2884
  revision: roomDraftRevision(),
2041
- parts: held(current?.parts, parts),
2885
+ parts: held(current?.parts, parts, file),
2042
2886
  returned: current?.returned ?? null,
2043
2887
  };
2044
2888
  await local.keepRoomDraft(record);
@@ -2052,10 +2896,7 @@ export function composeRuntime(options) {
2052
2896
  const local = await openLocal();
2053
2897
  const current = roomDrafts.get(room)?.get(path);
2054
2898
  if (current === undefined || current.revision !== revision) {
2055
- return {
2056
- outcome: 'stale',
2057
- current: current === undefined ? null : roomDraftOf(current),
2058
- };
2899
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2059
2900
  }
2060
2901
  await local.discardRoomDraft(room, path);
2061
2902
  forgetRoomDraft(room, path);
@@ -2090,6 +2931,10 @@ export function composeRuntime(options) {
2090
2931
  history(locator, path, query) {
2091
2932
  return readHistory(locator, path, query);
2092
2933
  },
2934
+ binaryFile: {
2935
+ read: readBinaryPath,
2936
+ readReference: readBinaryReference,
2937
+ },
2093
2938
  call(name, input) {
2094
2939
  return invokeAction(name, input);
2095
2940
  },
@@ -2109,23 +2954,26 @@ export function composeRuntime(options) {
2109
2954
  if (refusal === null || !refusal.reauthorizationOffered)
2110
2955
  return;
2111
2956
  journal.record('connection reauthorize');
2112
- await chain.catch(() => undefined);
2957
+ await Promise.all([...lanes.values()]
2958
+ .filter((held) => held.generation === generation)
2959
+ .map((held) => held.chain));
2113
2960
  await mutations;
2114
2961
  authorization = undefined;
2115
2962
  discoveryPaused = false;
2116
2963
  if (options.adapter.roomStream === undefined)
2117
2964
  await sync('unstreamed');
2118
2965
  else
2119
- await task(ensureAuthorization);
2966
+ await task(OPENING_LANE, ensureAuthorization);
2120
2967
  },
2121
2968
  };
2122
2969
  const reachable = {
2123
2970
  md,
2124
2971
  watch,
2972
+ referrerScope: (referrer) => options.adapter.referrerScope(referrer),
2125
2973
  plan: planned,
2126
- query: (plan) => readSide.query(plan),
2974
+ query: (plan) => rowsQuery(plan),
2127
2975
  watchPlan,
2128
2976
  search: searched,
2129
2977
  };
2130
- return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2978
+ return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
2131
2979
  }