@agentshouse/mdruntime 0.1.0-c389df724fd2f → 0.1.0-c3b593308e82b

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