@agentshouse/mdruntime 0.1.0-ca1eff92bbe73 → 0.1.0-ca68e8b4bbebc

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 (199) hide show
  1. package/AGENTS.md +269 -214
  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 +81 -20
  7. package/dist/adapter.d.ts.map +1 -1
  8. package/dist/attempt.d.ts +16 -0
  9. package/dist/attempt.d.ts.map +1 -0
  10. package/dist/attempt.js +70 -0
  11. package/dist/binary.d.ts +23 -0
  12. package/dist/binary.d.ts.map +1 -0
  13. package/dist/binary.js +8 -0
  14. package/dist/bootstrap.d.ts +18 -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 +25 -12
  87. package/dist/client.d.ts.map +1 -1
  88. package/dist/client.js +75 -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 +44 -15
  93. package/dist/connection.d.ts.map +1 -1
  94. package/dist/connection.js +1832 -395
  95. package/dist/conversation.d.ts +26 -10
  96. package/dist/conversation.d.ts.map +1 -1
  97. package/dist/conversation.js +414 -148
  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 +18 -2
  113. package/dist/host.d.ts.map +1 -1
  114. package/dist/host.js +97 -19
  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 +16 -8
  119. package/dist/index.d.ts.map +1 -1
  120. package/dist/index.js +5 -0
  121. package/dist/memory.d.ts +11 -4
  122. package/dist/memory.d.ts.map +1 -1
  123. package/dist/memory.js +83 -27
  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 +39 -3
  140. package/dist/query.d.ts.map +1 -1
  141. package/dist/query.js +13 -33
  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 +30 -18
  146. package/dist/react.d.ts.map +1 -1
  147. package/dist/react.js +14 -10
  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/reads.d.ts +11 -0
  155. package/dist/reads.d.ts.map +1 -0
  156. package/dist/reads.js +98 -0
  157. package/dist/recovery.d.ts +25 -0
  158. package/dist/recovery.d.ts.map +1 -0
  159. package/dist/recovery.js +197 -0
  160. package/dist/refresh.d.ts +13 -3
  161. package/dist/refresh.d.ts.map +1 -1
  162. package/dist/refresh.js +49 -38
  163. package/dist/replica.d.ts +8 -5
  164. package/dist/replica.d.ts.map +1 -1
  165. package/dist/replica.js +124 -67
  166. package/dist/room-draft.d.ts +38 -9
  167. package/dist/room-draft.d.ts.map +1 -1
  168. package/dist/room-draft.js +134 -25
  169. package/dist/rows.d.ts +9 -13
  170. package/dist/rows.d.ts.map +1 -1
  171. package/dist/rows.js +14 -14
  172. package/dist/save.d.ts +31 -1
  173. package/dist/save.d.ts.map +1 -1
  174. package/dist/save.js +376 -45
  175. package/dist/search.d.ts +39 -8
  176. package/dist/search.d.ts.map +1 -1
  177. package/dist/search.js +132 -46
  178. package/dist/seed.d.ts +16 -0
  179. package/dist/seed.d.ts.map +1 -0
  180. package/dist/seed.js +61 -0
  181. package/dist/shared.d.ts +11 -0
  182. package/dist/shared.d.ts.map +1 -0
  183. package/dist/shared.js +368 -0
  184. package/dist/status.d.ts +6 -2
  185. package/dist/status.d.ts.map +1 -1
  186. package/dist/status.js +0 -8
  187. package/dist/store.d.ts +83 -18
  188. package/dist/store.d.ts.map +1 -1
  189. package/dist/store.js +555 -200
  190. package/dist/turn.d.ts +7 -0
  191. package/dist/turn.d.ts.map +1 -0
  192. package/dist/turn.js +45 -0
  193. package/dist/wire.d.ts +141 -33
  194. package/dist/wire.d.ts.map +1 -1
  195. package/dist/wire.js +61 -31
  196. package/package.json +26 -14
  197. package/dist/attachment.d.ts +0 -3
  198. package/dist/attachment.d.ts.map +0 -1
  199. package/dist/attachment.js +0 -8
@@ -1,25 +1,46 @@
1
1
  import { attachedModel } from './attached-model.js';
2
2
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
3
- import { observing } from './observation.js';
4
- import { filterOf, identityOf, planOf, } from './query.js';
3
+ import { observing, whole } from './observation.js';
4
+ import { filterOf, identityOf, planOf, queryKey, } from './query.js';
5
5
  import { createReadSide } from './read-side.js';
6
+ import { seededResult } from './seed.js';
7
+ import { markdownPath, roomOfKey } from './rows.js';
6
8
  import { createSearchIndex, searchPlanOf, } from './search.js';
7
9
  import { bootstrapRoom } from './bootstrap.js';
8
- import { changeSet, mergeChangeSets } from './change-set.js';
10
+ import { changeSet, entering, mergeChangeSets } from './change-set.js';
9
11
  import { createConversationStore, } from './conversation.js';
10
- import { parseCollection, validRoot } from './collection.js';
12
+ import { queuedFile } from './change.js';
13
+ import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
14
+ import { runtimeError } from './failure.js';
11
15
  import { createLog } from './log.js';
16
+ import { binaryRecord } from './binary.js';
17
+ import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
18
+ import { createRecovery } from './recovery.js';
19
+ import { doorReads } from './reads.js';
12
20
  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';
21
+ import { buildBatch, joined, limitedIn, requeued } from './save.js';
22
+ import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
23
+ import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
24
+ import { same, saving, STARTING, transferring, } from './status.js';
17
25
  import { detectStorageLoss, nextGeneration } from './durability.js';
18
- import { roomDraftRevision } from './identity.js';
26
+ import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
27
+ import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
28
+ import { FencedMap } from './fence.js';
29
+ import { tally } from './shared.js';
30
+ import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
31
+ import { identityTime, roomDraftRevision } from './identity.js';
19
32
  import { beneath, normalize } from './paths.js';
20
33
  import { openRuntimeStore, } from './store.js';
21
34
  import { unconfirmed } from './transfer.js';
22
35
  import { governs, holds, narrowed, planReach, } from './reach.js';
36
+ function roomsAlone(plan) {
37
+ return (plan.reach === null &&
38
+ plan.root.types === null &&
39
+ plan.root.where === null &&
40
+ plan.root.pathPrefix === null &&
41
+ plan.hop === null &&
42
+ plan.backlinks === null);
43
+ }
23
44
  function within(root, path) {
24
45
  return root === '' || beneath(root, path);
25
46
  }
@@ -33,22 +54,68 @@ function declaredSelection(room) {
33
54
  root !== '' &&
34
55
  selection.roots.every((other, at) => at === index || !within(other, root)));
35
56
  }
57
+ const OPENING_LANE = 'opening';
58
+ const CONVERSATION_LANE = 'conversations';
59
+ const ROOM_READS = 256;
60
+ const ROOM_ATTEMPTS = 16;
61
+ function operationOf(input) {
62
+ const operation = input?.operation_id;
63
+ return typeof operation === 'string' ? operation : null;
64
+ }
65
+ const outside = new Map();
66
+ function outsideOf(roots) {
67
+ const key = JSON.stringify(roots);
68
+ const held = outside.get(key);
69
+ if (held !== undefined)
70
+ return held;
71
+ const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
72
+ const made = {
73
+ documents: (path) => (markdownPath(path) ? counted(path) : 0),
74
+ binaries: counted,
75
+ };
76
+ outside.set(key, made);
77
+ return made;
78
+ }
79
+ function abandonedOn(signal) {
80
+ return new Promise((resolve) => {
81
+ if (signal.aborted)
82
+ resolve(null);
83
+ else
84
+ signal.addEventListener('abort', () => resolve(null), { once: true });
85
+ });
86
+ }
87
+ function roomLane(locator) {
88
+ return `room ${locator}`;
89
+ }
36
90
  export function composeRuntime(options) {
91
+ const localSaves = options.adapter.localSaves === true;
37
92
  let authorization = options.authorization;
38
93
  let opening;
39
94
  let store;
40
- const replicas = new Map();
95
+ const fences = new Map();
96
+ const isFenced = (locator) => fences.has(locator);
97
+ let fencings = 0;
98
+ const lastFenced = new Map();
99
+ const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
100
+ let discoveries = 0;
101
+ const replicas = new FencedMap(isFenced);
102
+ const queuesWithoutReplica = new FencedMap(isFenced);
41
103
  const pendings = new Map();
42
- const roomDrafts = new Map();
104
+ const seeds = new Map();
105
+ const seedWatchers = new Map();
106
+ const roomDrafts = new FencedMap(isFenced);
43
107
  const presentations = new Map();
44
108
  let limits = [];
45
109
  let capabilities = [];
46
- const rooms = new Map();
110
+ const rooms = new FencedMap(isFenced);
111
+ const counted = new Map();
47
112
  const unreadable = new Set();
48
- const models = new Map();
49
- const liveModels = new Map();
113
+ const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
114
+ const models = new FencedMap(isModelFenced);
115
+ const liveModels = new FencedMap(isModelFenced);
50
116
  const invalidModels = new Set();
51
117
  const discovered = new Map();
118
+ const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
52
119
  let roomsListed = false;
53
120
  const scopedRefusals = new Map();
54
121
  const sourceRefusals = new Map();
@@ -76,19 +143,29 @@ export function composeRuntime(options) {
76
143
  let status = STARTING;
77
144
  let shown = STARTING;
78
145
  let discoveryPaused = false;
79
- let resetTimer;
80
146
  let closed = false;
81
147
  let stopping = false;
82
148
  let withdrawStream;
83
149
  const announced = new Map();
84
150
  let underway = null;
151
+ let streamed = null;
85
152
  let handed = false;
86
- const submitting = new Map();
153
+ const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
154
+ const repairing = new Set();
155
+ const calling = new Set();
156
+ const repairable = new Map();
157
+ let outcomeRetention = null;
87
158
  const answering = new Map();
88
159
  let complete = false;
89
160
  let recorded = false;
90
161
  const stale = new Set();
91
- let chain = Promise.resolve();
162
+ let generation = 0;
163
+ let discovering = null;
164
+ let opened = null;
165
+ const owed = new Set();
166
+ const lanes = new Map();
167
+ let roomReads = { generation, free: ROOM_READS, waiting: [] };
168
+ const working = new Set();
92
169
  let mutations = Promise.resolve();
93
170
  const roomListeners = new Set();
94
171
  const refusalListeners = new Set();
@@ -99,27 +176,49 @@ export function composeRuntime(options) {
99
176
  const presentationListeners = new Set();
100
177
  const readSide = createReadSide();
101
178
  let withdrawConversations;
102
- let currencyQueued = false;
179
+ let currencyQueued = null;
103
180
  let socketOpen = false;
104
181
  let serviceMode = 'open';
105
182
  let haltedRefusal = false;
106
183
  let openSignals = 0;
107
184
  const serviceModeListeners = new Set();
185
+ let member = null;
186
+ const memberListeners = new Set();
187
+ const prepared = new Set();
188
+ const projecting = new Set();
189
+ const unpreparedDemand = new Set();
190
+ const unindexable = new Set();
191
+ let preparing = false;
192
+ let demand = WHOLE_DEMAND;
193
+ let narrowing = null;
194
+ let targets = {};
195
+ let persistence = options.shell.persistence;
196
+ let capacity = null;
197
+ const paused = new Set();
198
+ let rechecking = null;
199
+ const readinessWatches = new Set();
200
+ let readinessQueued = false;
201
+ const catching = new Set();
202
+ const transfers = new FencedMap(isFenced);
108
203
  const conversations = createConversationStore({
109
204
  agents: declaredAgents,
110
- documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
205
+ documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
111
206
  call: (name, input) => invokeAction(name, input),
112
207
  durably,
113
208
  stream: () => options.adapter.conversations,
114
209
  publish: publishChangeSet,
210
+ online: () => !status.providerUnreachable && refusal === null,
115
211
  closed: () => closed,
212
+ unanswered: resendLater,
213
+ expired: (operation) => pastHorizon(operation),
116
214
  });
117
215
  const searchIndex = createSearchIndex({
118
- documents: () => readSide.documents(),
216
+ room: (room) => readSide.room(room),
119
217
  document: (room, path) => readSide.document(room, path),
120
- messages: () => conversations.messages(),
121
- message: (conversation, identity) => conversations.message(conversation, identity),
122
218
  });
219
+ const roomsToIndex = new Set();
220
+ const conversationsToIndex = new Set();
221
+ let indexing = false;
123
222
  const journal = createLog();
124
223
  let notifyReady = () => { };
125
224
  const ready = new Promise((resolve) => {
@@ -183,7 +282,9 @@ export function composeRuntime(options) {
183
282
  }
184
283
  }
185
284
  function pending() {
186
- return [...replicas.values()].some((replica) => replica.queue.length > 0);
285
+ return (!localSaves &&
286
+ (queuesWithoutReplica.size > 0 ||
287
+ [...replicas.values()].some((replica) => replica.queue.length > 0)));
187
288
  }
188
289
  function attention() {
189
290
  const attentionRequired = [];
@@ -196,9 +297,16 @@ export function composeRuntime(options) {
196
297
  returned: structuredClone(record.returned),
197
298
  });
198
299
  }
300
+ for (const [room, refusal] of sourceRefusals)
301
+ attentionRequired.push({ room, refusal: structuredClone(refusal) });
199
302
  attentionRequired.sort((left, right) => {
200
303
  if (left.room !== right.room)
201
304
  return left.room < right.room ? -1 : 1;
305
+ if ('refusal' in left || 'refusal' in right) {
306
+ if ('refusal' in left && 'refusal' in right)
307
+ return 0;
308
+ return 'refusal' in left ? -1 : 1;
309
+ }
202
310
  if (left.path !== right.path)
203
311
  return left.path < right.path ? -1 : 1;
204
312
  return 0;
@@ -212,8 +320,21 @@ export function composeRuntime(options) {
212
320
  }
213
321
  return { attentionRequired, reset };
214
322
  }
323
+ function documentsIn(locator) {
324
+ const replica = replicas.get(locator);
325
+ if (replica === undefined)
326
+ return 0;
327
+ const roots = replica.confirmed.record.typedSelection?.roots ?? [];
328
+ const weighed = outsideOf(roots);
329
+ return (tally(replica.visible.documents, weighed.documents) +
330
+ tally(replica.visible.binaries, weighed.binaries));
331
+ }
215
332
  function listed() {
216
- return structuredClone([...rooms.values()]);
333
+ return structuredClone([...rooms.values()].map((room) => {
334
+ const documents = documentsIn(room.locator);
335
+ counted.set(room.locator, documents);
336
+ return { ...room, documents };
337
+ }));
217
338
  }
218
339
  function roomFacts(reach = null) {
219
340
  const held = [...rooms.values()]
@@ -233,18 +354,26 @@ export function composeRuntime(options) {
233
354
  return { listed: roomsListed, rooms: held };
234
355
  }
235
356
  function emitRooms() {
357
+ readSide.recover();
358
+ queueReadiness();
236
359
  const value = listed();
237
360
  for (const listener of roomListeners)
238
361
  listener(value);
239
362
  }
240
363
  function emitRefusal() {
364
+ queueReadiness();
241
365
  for (const listener of refusalListeners)
242
366
  listener(refusal);
243
367
  }
244
368
  function emitStorageLoss() {
369
+ queueReadiness();
245
370
  for (const listener of storageLossListeners)
246
371
  listener(storageLoss);
247
372
  }
373
+ function emitMember() {
374
+ for (const listener of memberListeners)
375
+ listener(member);
376
+ }
248
377
  function moveTo(next) {
249
378
  if (closed)
250
379
  return;
@@ -254,7 +383,8 @@ export function composeRuntime(options) {
254
383
  function show() {
255
384
  if (closed)
256
385
  return;
257
- const next = status.state === 'synced' && !conversations.current()
386
+ queueReadiness();
387
+ const next = status.state === 'synced' && demand.conversations && !conversations.current()
258
388
  ? { ...status, state: 'syncing' }
259
389
  : status;
260
390
  if (same(shown, next))
@@ -264,6 +394,8 @@ export function composeRuntime(options) {
264
394
  listener(shown);
265
395
  }
266
396
  function entryOf(batch) {
397
+ if (localSaves)
398
+ return Object.freeze({ batch, state: 'local' });
267
399
  const state = pendings.get(batch.id);
268
400
  if (state?.accepted !== undefined) {
269
401
  return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
@@ -271,10 +403,7 @@ export function composeRuntime(options) {
271
403
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
272
404
  }
273
405
  function entriesOf(locator) {
274
- const replica = replicas.get(locator);
275
- if (replica === undefined)
276
- return [];
277
- return replica.queue.map(entryOf);
406
+ return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
278
407
  }
279
408
  function emitQueue(locator) {
280
409
  const entries = entriesOf(locator);
@@ -284,12 +413,516 @@ export function composeRuntime(options) {
284
413
  }
285
414
  }
286
415
  function applyChangeSet(set) {
287
- readSide.apply(set);
288
- searchIndex.changed(set);
416
+ const held = {
417
+ ...set,
418
+ documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
419
+ };
420
+ readSide.apply(held);
421
+ searchIndex.changed(held);
422
+ }
423
+ async function referenced(locator) {
424
+ for (;;) {
425
+ const record = replicas.get(locator)?.confirmed.record;
426
+ if (record === undefined || closed)
427
+ return;
428
+ const held = { text: record.documents, binaries: record.binaries };
429
+ const composition = compositionOf(record);
430
+ if (referencesIndexed(held, composition))
431
+ return;
432
+ await indexReferrers(held, composition);
433
+ }
434
+ }
435
+ async function prepareReplica(locator) {
436
+ await referenced(locator);
437
+ const replica = replicas.get(locator);
438
+ if (replica === undefined || closed || prepared.has(locator))
439
+ return;
440
+ const visible = replica.visible;
441
+ const paths = [
442
+ ...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
443
+ ];
444
+ const entered = [];
445
+ projecting.add(locator);
446
+ try {
447
+ await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
448
+ const current = replicas.get(locator);
449
+ if (current === undefined || closed)
450
+ return;
451
+ const set = entering(locator, current, chunk);
452
+ readSide.apply(set);
453
+ entered.push(set);
454
+ });
455
+ if (!replicas.has(locator) || closed)
456
+ return;
457
+ prepared.add(locator);
458
+ }
459
+ finally {
460
+ projecting.delete(locator);
461
+ }
462
+ conversations.changed(mergeChangeSets(entered));
463
+ readSide.recover();
464
+ queueReadiness();
465
+ }
466
+ function unprepared() {
467
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
468
+ }
469
+ function unbuilt(locator, error) {
470
+ unindexable.add(locator);
471
+ journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
472
+ queueReadiness();
473
+ }
474
+ function prepare(locators = []) {
475
+ for (const locator of locators)
476
+ unpreparedDemand.add(locator);
477
+ if (preparing)
478
+ return;
479
+ preparing = true;
480
+ void (async () => {
481
+ try {
482
+ for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
483
+ await yielded();
484
+ if (closed)
485
+ return;
486
+ unpreparedDemand.delete(waiting);
487
+ await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
488
+ }
489
+ }
490
+ finally {
491
+ preparing = false;
492
+ }
493
+ })();
494
+ }
495
+ function reachedRooms(plan) {
496
+ const reach = plan.reach;
497
+ const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
498
+ const named = plan.rooms ??
499
+ (reached === null
500
+ ? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
501
+ : [...reached]);
502
+ return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
503
+ }
504
+ function awaited(locator) {
505
+ return (!roomsListed ||
506
+ (rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
507
+ }
508
+ function covered(plan) {
509
+ const locators = reachedRooms(plan);
510
+ if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
511
+ return { pending: [] };
512
+ }
513
+ const pending = [];
514
+ const coverage = [];
515
+ const overlay = [];
516
+ for (const locator of locators) {
517
+ const replica = replicas.get(locator);
518
+ if (replica === undefined) {
519
+ if (awaited(locator))
520
+ pending.push({ room: locator, awaits: 'acquisition' });
521
+ continue;
522
+ }
523
+ if (!prepared.has(locator)) {
524
+ pending.push({ room: locator, awaits: 'preparation' });
525
+ continue;
526
+ }
527
+ coverage.push({
528
+ room: locator,
529
+ generation: replica.confirmed.record.transfer,
530
+ current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
531
+ });
532
+ for (const batch of replica.queue)
533
+ overlay.push(batch.id);
534
+ }
535
+ if (pending.length > 0)
536
+ return { pending };
537
+ return { coverage: { rooms: coverage, indexes: [], overlay } };
538
+ }
539
+ function awaitPreparation(cover) {
540
+ if (!('pending' in cover))
541
+ return;
542
+ const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
543
+ if (waiting.length > 0)
544
+ prepare(waiting.map(({ room }) => room));
545
+ }
546
+ function acquires(room) {
547
+ return (demanded(demand, room) ||
548
+ (replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
549
+ queuesWithoutReplica.has(room.locator));
550
+ }
551
+ function acquiredLocators() {
552
+ return [...discovered.values()]
553
+ .filter(acquires)
554
+ .map((room) => room.locator)
555
+ .sort();
556
+ }
557
+ function heldDemand(acquired) {
558
+ if (demand.whole)
559
+ return null;
560
+ return { rooms: acquired.map((room) => room.locator), conversations: demand.conversations };
561
+ }
562
+ function applyDemand(next) {
563
+ const before = acquiredLocators();
564
+ const conversed = demand.conversations;
565
+ demand = next;
566
+ const after = acquiredLocators();
567
+ queueReadiness();
568
+ if (!conversed && next.conversations)
569
+ scheduleCurrency();
570
+ if (JSON.stringify(before) === JSON.stringify(after) && conversed === next.conversations)
571
+ return;
572
+ journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
573
+ if (options.adapter.roomStream === undefined)
574
+ void sync('unstreamed');
575
+ else if (socketOpen)
576
+ options.adapter.roomStream.reopen();
577
+ }
578
+ function acquire(next) {
579
+ if (closed)
580
+ return;
581
+ if (narrowing !== null)
582
+ clearTimeout(narrowing);
583
+ narrowing = null;
584
+ const widening = wider(next, demand);
585
+ if (!widening && !narrower(next, demand))
586
+ return;
587
+ if (widening || !roomsListed) {
588
+ applyDemand(next);
589
+ return;
590
+ }
591
+ // A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
592
+ narrowing = setTimeout(() => {
593
+ narrowing = null;
594
+ applyDemand(next);
595
+ }, 5_000);
596
+ }
597
+ function requiredRooms(reach) {
598
+ if (reach === null)
599
+ return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
600
+ const named = [...new Set(reach.rooms)].sort();
601
+ if (roomsListed)
602
+ return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
603
+ return { listed: named.every((room) => replicas.has(room)), rooms: named };
604
+ }
605
+ function roomState(locator, search) {
606
+ if (sourceRefusals.has(locator))
607
+ return 'refused';
608
+ const listedRoom = discovered.get(locator);
609
+ if (roomsListed &&
610
+ (listedRoom === undefined ||
611
+ listedRoom.replica.admission !== 'admitted' ||
612
+ unreadable.has(locator)))
613
+ return 'unadmitted';
614
+ if (!replicas.has(locator))
615
+ return 'acquiring';
616
+ if (!prepared.has(locator))
617
+ return 'preparing';
618
+ if (search && !searchIndex.rooms().has(locator))
619
+ return 'indexing';
620
+ return 'complete';
621
+ }
622
+ function conversationCoverage(reach, search) {
623
+ const declared = reach === null || reach.conversations;
624
+ if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
625
+ return 'undeclared';
626
+ if (!conversations.known() || !conversations.current())
627
+ return 'acquiring';
628
+ if (!search)
629
+ return 'complete';
630
+ const unindexed = conversations
631
+ .searchable()
632
+ .filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
633
+ if (unindexed.length === 0)
634
+ return 'complete';
635
+ index([], unindexed);
636
+ return 'indexing';
637
+ }
638
+ function connectionOf() {
639
+ if (socketOpen)
640
+ return 'connected';
641
+ if (status.providerUnreachable)
642
+ return 'unreachable';
643
+ return options.adapter.roomStream === undefined && status.lastCompleted !== null
644
+ ? 'connected'
645
+ : 'connecting';
646
+ }
647
+ function refusedIn(locators) {
648
+ return locators.flatMap((room) => {
649
+ const held = sourceRefusals.get(room);
650
+ if (held === undefined)
651
+ return [];
652
+ const { file, location } = held.fields;
653
+ return [
654
+ {
655
+ room,
656
+ file: typeof file === 'string' ? file : '',
657
+ rule: held.code,
658
+ location: typeof location === 'string' ? location : null,
659
+ },
660
+ ];
661
+ });
662
+ }
663
+ function readinessFor(port, reach) {
664
+ const required = requiredRooms(reach);
665
+ const search = port.search === 'required';
666
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
667
+ const building = (state) => coverage
668
+ .filter((room) => room.state === state && !unindexable.has(room.room))
669
+ .map(({ room }) => room);
670
+ const preparing = building('preparing');
671
+ if (preparing.length > 0)
672
+ prepare(preparing);
673
+ const indexing = building('indexing');
674
+ if (indexing.length > 0)
675
+ index(indexing, []);
676
+ return readinessOf({
677
+ code: port.code,
678
+ coverage: {
679
+ listed: required.listed,
680
+ rooms: coverage,
681
+ conversations: conversationCoverage(reach, search),
682
+ search: search ? 'required' : 'unused',
683
+ },
684
+ persistence,
685
+ connection: connectionOf(),
686
+ house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
687
+ capacity: capacity !== null &&
688
+ coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
689
+ ? capacity
690
+ : port.capacity,
691
+ refused: refusedIn(required.rooms),
692
+ lost: storageLoss !== null,
693
+ established: targets[port.target]?.established === true,
694
+ });
695
+ }
696
+ function establish(name) {
697
+ targets = { ...targets, [name]: { established: true } };
698
+ queueReadiness();
699
+ void durably((local) => local.keepTargets(targets)).catch(() => {
700
+ journal.record(`target record failed ${name}`);
701
+ });
702
+ }
703
+ function deliverReadiness(watch) {
704
+ if (!readinessWatches.has(watch))
705
+ return;
706
+ const readiness = readinessFor(watch.port, watch.reach);
707
+ if (readiness.ready && targets[watch.port.target]?.established !== true) {
708
+ establish(watch.port.target);
709
+ }
710
+ const shown = JSON.stringify(readiness);
711
+ if (shown === watch.last)
712
+ return;
713
+ watch.last = shown;
714
+ watch.deliver(structuredClone(readiness));
715
+ }
716
+ function queueReadiness() {
717
+ if (readinessQueued || closed || readinessWatches.size === 0)
718
+ return;
719
+ readinessQueued = true;
720
+ queueMicrotask(() => {
721
+ readinessQueued = false;
722
+ if (closed)
723
+ return;
724
+ for (const watch of [...readinessWatches])
725
+ deliverReadiness(watch);
726
+ });
727
+ }
728
+ function watchReadiness(port, reach, deliver) {
729
+ const watch = { port, reach, deliver, last: null };
730
+ readinessWatches.add(watch);
731
+ void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
732
+ return () => {
733
+ readinessWatches.delete(watch);
734
+ };
735
+ }
736
+ function layerBytes(layer) {
737
+ let bytes = 0;
738
+ for (const [path, text] of Object.entries(layer.documents))
739
+ bytes += (path.length + text.length) * 2;
740
+ return bytes;
741
+ }
742
+ function grownBy(next, previous) {
743
+ if (previous === undefined)
744
+ return layerBytes(next);
745
+ const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
746
+ return layerBytes({ ...next, documents });
747
+ }
748
+ function exceeded(error) {
749
+ return (typeof error === 'object' &&
750
+ error !== null &&
751
+ error.name === 'QuotaExceededError');
752
+ }
753
+ async function estimated() {
754
+ const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
755
+ return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
756
+ }
757
+ function pause(locator, cause) {
758
+ paused.add(locator);
759
+ capacity = cause;
760
+ journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
761
+ queueReadiness();
762
+ if (rechecking !== null)
763
+ return;
764
+ // A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
765
+ rechecking = setInterval(() => {
766
+ void recheck();
767
+ }, 60_000);
768
+ }
769
+ async function recheck() {
770
+ const held = capacity;
771
+ if (held === null || closed)
772
+ return;
773
+ const available = await estimated().catch(() => null);
774
+ if (available === null || available < (held.needed ?? 0))
775
+ return;
776
+ if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
777
+ return;
778
+ resume();
779
+ }
780
+ function resume() {
781
+ if (rechecking !== null)
782
+ clearInterval(rechecking);
783
+ rechecking = null;
784
+ if (paused.size === 0 && capacity === null)
785
+ return;
786
+ paused.clear();
787
+ capacity = null;
788
+ journal.record('capacity resumed');
789
+ queueReadiness();
790
+ if (!closed && authorization !== undefined)
791
+ void sync('unstreamed');
792
+ }
793
+ async function reclaimed(local) {
794
+ const unreferenced = [...transfers.keys()].filter((locator) => {
795
+ const room = discovered.get(locator);
796
+ return room === undefined || !acquires(room);
797
+ });
798
+ for (const locator of unreferenced) {
799
+ await local.abandon(locator);
800
+ transfers.delete(locator);
801
+ }
802
+ if (unreferenced.length > 0)
803
+ journal.record(`capacity reclaimed ${unreferenced.length}`);
804
+ return unreferenced.length > 0;
805
+ }
806
+ async function admitted(local, locator, needed) {
807
+ if (paused.has(locator))
808
+ return false;
809
+ let available = await estimated().catch(() => null);
810
+ if (available === null || available >= needed)
811
+ return true;
812
+ if (await reclaimed(local))
813
+ available = await estimated().catch(() => null);
814
+ if (available === null || available >= needed)
815
+ return true;
816
+ pause(locator, { cause: 'insufficient-space', needed, available });
817
+ return false;
818
+ }
819
+ async function exceededOn(locator, bytes) {
820
+ const available = await estimated().catch(() => null);
821
+ pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
822
+ }
823
+ function seedRoomHeld(room, version) {
824
+ return prepared.has(room) && reachedBy(room, version);
825
+ }
826
+ function seedHeld(at) {
827
+ return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
828
+ }
829
+ function seedApplies(plan) {
830
+ const seed = seeds.get(plan.key);
831
+ if (seed === undefined)
832
+ return null;
833
+ if (!seedHeld(seed.at))
834
+ return seed;
835
+ seeds.delete(plan.key);
836
+ return null;
837
+ }
838
+ function seedBehind(plan, cover) {
839
+ const seed = seedApplies(plan);
840
+ if (seed === null || 'pending' in cover)
841
+ return cover;
842
+ return {
843
+ pending: Object.entries(seed.at)
844
+ .filter(([room, version]) => !seedRoomHeld(room, version))
845
+ .map(([room]) => ({ room, awaits: 'acquisition' })),
846
+ };
847
+ }
848
+ function seed(entry) {
849
+ if (seedHeld(entry.at))
850
+ return;
851
+ for (const { query, rows } of entry.queries) {
852
+ const key = queryKey(query);
853
+ seeds.set(key, { rows, at: entry.at });
854
+ for (const shown of seedWatchers.get(key) ?? [])
855
+ shown();
856
+ }
857
+ }
858
+ async function rowsQuery(plan) {
859
+ const before = covered(plan);
860
+ awaitPreparation(before);
861
+ const seeded = seedApplies(plan);
862
+ if (seeded !== null)
863
+ return seededResult(seeded).result;
864
+ if ('pending' in before)
865
+ return before;
866
+ const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
867
+ awaitPreparation(cover);
868
+ if ('pending' in cover)
869
+ return cover;
870
+ return 'groups' in rows
871
+ ? { groups: rows.groups, coverage: cover.coverage }
872
+ : { documents: rows.documents, coverage: cover.coverage };
873
+ }
874
+ async function indexConversation(conversation) {
875
+ searchIndex.indexMessages(conversation, [], false);
876
+ let after = null;
877
+ for (;;) {
878
+ const batch = await durably((local) => local.messagesAfter(conversation, after));
879
+ if (closed)
880
+ return;
881
+ searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
882
+ if (batch.length === 0)
883
+ return;
884
+ after = batch[batch.length - 1].identity;
885
+ }
886
+ }
887
+ function indexNext() {
888
+ const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
889
+ if (room !== undefined) {
890
+ roomsToIndex.delete(room);
891
+ return () => prepared.has(room)
892
+ ? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
893
+ : undefined;
894
+ }
895
+ const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
896
+ if (conversation === undefined)
897
+ return null;
898
+ conversationsToIndex.delete(conversation);
899
+ return () => indexConversation(conversation);
900
+ }
901
+ function index(rooms, named) {
902
+ for (const room of rooms)
903
+ roomsToIndex.add(room);
904
+ for (const conversation of named)
905
+ conversationsToIndex.add(conversation);
906
+ if (indexing)
907
+ return;
908
+ indexing = true;
909
+ void (async () => {
910
+ try {
911
+ for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
912
+ await yielded();
913
+ await unit();
914
+ queueReadiness();
915
+ }
916
+ }
917
+ finally {
918
+ indexing = false;
919
+ }
920
+ })().catch(() => { });
289
921
  }
290
922
  function publishChangeSet(set) {
291
923
  applyChangeSet(set);
292
- if (set.rooms.length > 0)
924
+ if (set.rooms.length > 0 ||
925
+ [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
293
926
  emitRooms();
294
927
  for (const locator of set.queues)
295
928
  emitQueue(locator);
@@ -301,34 +934,7 @@ export function composeRuntime(options) {
301
934
  emitPresentation(app);
302
935
  conversations.changed(set);
303
936
  }
304
- function stopTimer() {
305
- if (resetTimer === undefined)
306
- return;
307
- clearTimeout(resetTimer);
308
- resetTimer = undefined;
309
- }
310
- function armTimer() {
311
- stopTimer();
312
- if (closed || stopping)
313
- return;
314
- let earliest = null;
315
- for (const { submission } of pendings.values()) {
316
- if (submission.state !== 'reset')
317
- continue;
318
- const at = Date.parse(submission.reset);
319
- if (earliest === null || at < earliest)
320
- earliest = at;
321
- }
322
- if (earliest === null)
323
- return;
324
- // A Save House held back goes again only once the reset its answer named has passed.
325
- resetTimer = setTimeout(() => {
326
- resetTimer = undefined;
327
- submitAll();
328
- }, Math.max(0, earliest - Date.now()));
329
- }
330
937
  async function refuse(reason, provider = null) {
331
- stopTimer();
332
938
  discoveryPaused = true;
333
939
  try {
334
940
  if (reason === 'revoked-authorization') {
@@ -336,10 +942,7 @@ export function composeRuntime(options) {
336
942
  const revoked = { status: 'unavailable', reason: 'revoked' };
337
943
  try {
338
944
  const local = await openLocal();
339
- await local.revokeModels([...replicas.values()].map((replica) => ({
340
- record: replica.confirmed.record,
341
- queue: replica.queue,
342
- })));
945
+ await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
343
946
  }
344
947
  catch {
345
948
  journal.record('model revocation persistence failed');
@@ -363,10 +966,7 @@ export function composeRuntime(options) {
363
966
  model: revoked,
364
967
  selectionModel: revoked,
365
968
  };
366
- const read = readConfirmed(record, before.confirmed);
367
- replicas.set(locator, read.kind === 'staged'
368
- ? replicaOf(read.confirmed, before.queue, before)
369
- : { ...before, confirmed: { ...before.confirmed, record } });
969
+ replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
370
970
  }
371
971
  rooms.set(locator, {
372
972
  ...room,
@@ -383,7 +983,8 @@ export function composeRuntime(options) {
383
983
  }
384
984
  finally {
385
985
  refusal = {
386
- reason,
986
+ code: reason,
987
+ retryable: false,
387
988
  reauthorizationOffered: reason === 'revoked-authorization',
388
989
  refusal: provider,
389
990
  };
@@ -408,19 +1009,31 @@ export function composeRuntime(options) {
408
1009
  },
409
1010
  providerOwnedPaths: room.providerOwnedPaths,
410
1011
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
411
- attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
412
1012
  synchronizationAvailable,
413
1013
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
414
1014
  });
415
1015
  }
416
- async function applyDiscovery(listed, local) {
1016
+ async function applyDiscovery(found, started) {
1017
+ for (const room of found) {
1018
+ const fence = fences.get(room.locator);
1019
+ if (fence !== undefined && fence.erased && fence.at < started)
1020
+ fences.delete(room.locator);
1021
+ }
1022
+ await loseRooms(found
1023
+ .filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
1024
+ .map((room) => room.locator));
1025
+ const listed = found.filter((room) => !fences.has(room.locator));
417
1026
  const listedLocators = new Set(listed.map((room) => room.locator));
418
1027
  for (const locator of listedLocators)
419
1028
  scopedRefusals.delete(locator);
420
1029
  discovered.clear();
421
1030
  for (const room of listed)
422
1031
  discovered.set(room.locator, room);
423
- await dropUnlisted(listedLocators, local);
1032
+ const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
1033
+ if (options.adapter.exhaustiveDiscovery === true)
1034
+ await loseRooms(unlisted);
1035
+ else
1036
+ unlistedKept(unlisted);
424
1037
  for (const room of listed) {
425
1038
  const held = replicas.get(room.locator)?.confirmed.record;
426
1039
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -431,43 +1044,91 @@ export function composeRuntime(options) {
431
1044
  }
432
1045
  roomsListed = true;
433
1046
  emitRooms();
1047
+ return listed;
434
1048
  }
435
- function unlistedHeld(listed) {
436
- return [...new Set([...rooms.keys(), ...roomDrafts.keys()])].filter((locator) => !listed.has(locator));
1049
+ function heldLocally(locator) {
1050
+ return (replicas.has(locator) ||
1051
+ roomDrafts.has(locator) ||
1052
+ transfers.has(locator) ||
1053
+ queuesWithoutReplica.has(locator));
437
1054
  }
438
- async function dropUnlisted(listed, local) {
439
- if (unlistedHeld(listed).length === 0)
440
- return;
441
- await exclusive(async () => {
442
- const unlisted = unlistedHeld(listed);
443
- await local.dropRooms(unlisted);
444
- const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
445
- room: true,
446
- queue: true,
447
- drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
448
- }));
449
- for (const locator of unlisted) {
450
- for (const batch of replicas.get(locator)?.queue ?? [])
451
- pendings.delete(batch.id);
452
- replicas.delete(locator);
1055
+ function unlistedHeld(listed) {
1056
+ return [
1057
+ ...new Set([
1058
+ ...rooms.keys(),
1059
+ ...roomDrafts.keys(),
1060
+ ...transfers.keys(),
1061
+ ...queuesWithoutReplica.keys(),
1062
+ ]),
1063
+ ].filter((locator) => !listed.has(locator));
1064
+ }
1065
+ function unlistedKept(unlisted) {
1066
+ for (const locator of unlisted) {
1067
+ const held = replicas.get(locator)?.confirmed.record;
1068
+ if (held !== undefined)
1069
+ list(held, false);
1070
+ else
453
1071
  rooms.delete(locator);
454
- roomDrafts.delete(locator);
455
- unreadable.delete(locator);
456
- stale.delete(locator);
457
- scopedRefusals.delete(locator);
458
- sourceRefusals.delete(locator);
459
- declarationRefusals.delete(locator);
460
- for (const attached of ['collection', 'typed-selection']) {
461
- const key = modelKey(locator, attached);
462
- models.delete(key);
463
- liveModels.delete(key);
464
- invalidModels.delete(key);
465
- }
466
- journal.record(`room unlisted ${locator}`);
1072
+ }
1073
+ }
1074
+ function fence(locators) {
1075
+ const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1076
+ const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
1077
+ room: true,
1078
+ queue: true,
1079
+ drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
1080
+ }));
1081
+ for (const locator of fresh) {
1082
+ fences.set(locator, { at: discoveries, erased: false });
1083
+ lastFenced.set(locator, ++fencings);
1084
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1085
+ pendings.delete(batch.id);
1086
+ replicas.delete(locator);
1087
+ queuesWithoutReplica.delete(locator);
1088
+ rooms.delete(locator);
1089
+ roomDrafts.delete(locator);
1090
+ transfers.delete(locator);
1091
+ catching.delete(locator);
1092
+ discovered.delete(locator);
1093
+ announced.delete(locator);
1094
+ unreadable.delete(locator);
1095
+ stale.delete(locator);
1096
+ repairable.delete(locator);
1097
+ unindexable.delete(locator);
1098
+ paused.delete(locator);
1099
+ scopedRefusals.delete(locator);
1100
+ sourceRefusals.delete(locator);
1101
+ declarationRefusals.delete(locator);
1102
+ for (const attached of ['collection', 'typed-selection']) {
1103
+ const key = modelKey(locator, attached);
1104
+ models.delete(key);
1105
+ liveModels.delete(key);
1106
+ invalidModels.delete(key);
467
1107
  }
468
- publishChangeSet(mergeChangeSets(removed));
469
- moveTo({ ...status, ...attention() });
470
- });
1108
+ journal.record(`room lost ${locator}`);
1109
+ }
1110
+ if (fresh.length === 0)
1111
+ return fresh;
1112
+ publishChangeSet(mergeChangeSets(removed));
1113
+ for (const locator of fresh)
1114
+ prepared.delete(locator);
1115
+ searchIndex.forget(fresh);
1116
+ readSide.recover();
1117
+ moveTo({ ...status, ...attention() });
1118
+ return fresh;
1119
+ }
1120
+ async function erased(local, locators) {
1121
+ await local.erase(locators);
1122
+ for (const locator of locators) {
1123
+ const held = fences.get(locator);
1124
+ if (held !== undefined)
1125
+ held.erased = true;
1126
+ }
1127
+ }
1128
+ async function loseRooms(locators) {
1129
+ const fresh = fence(locators);
1130
+ if (fresh.length > 0)
1131
+ await durably((local) => erased(local, fresh));
471
1132
  }
472
1133
  function restored(key, persisted) {
473
1134
  const prior = models.get(key);
@@ -488,7 +1149,7 @@ export function composeRuntime(options) {
488
1149
  return model;
489
1150
  }
490
1151
  async function hydrate(opened) {
491
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
1152
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
492
1153
  opened.all(),
493
1154
  opened.queues(),
494
1155
  opened.roomDrafts(),
@@ -498,9 +1159,17 @@ export function composeRuntime(options) {
498
1159
  opened.lastCompleted(),
499
1160
  opened.providerLimits(),
500
1161
  opened.capabilities(),
1162
+ opened.member(),
1163
+ opened.transfers(),
1164
+ opened.targets(),
501
1165
  ]);
1166
+ targets = heldTargets;
1167
+ transfers.clear();
1168
+ for (const record of staged)
1169
+ transfers.set(record.locator, record);
502
1170
  limits = declared;
503
1171
  capabilities = named;
1172
+ member = known;
504
1173
  roomDrafts.clear();
505
1174
  for (const record of kept)
506
1175
  rememberRoomDraft(record);
@@ -516,18 +1185,17 @@ export function composeRuntime(options) {
516
1185
  }
517
1186
  const previousReplicas = new Map(replicas);
518
1187
  replicas.clear();
1188
+ queuesWithoutReplica.clear();
1189
+ const replicated = new Set(records.map((record) => record.locator));
1190
+ for (const [locator, queue] of queuedBatches) {
1191
+ if (!replicated.has(locator))
1192
+ queuesWithoutReplica.set(locator, queue);
1193
+ }
519
1194
  for (const record of records) {
520
1195
  const model = restored(modelKey(record.locator, 'collection'), record.model);
521
1196
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
522
1197
  const prior = previousReplicas.get(record.locator);
523
- const retyped = { ...record, model, selectionModel };
524
- const read = readConfirmed(retyped, prior?.confirmed);
525
- const confirmed = read.kind === 'staged'
526
- ? read.confirmed
527
- : {
528
- record: retyped,
529
- collection: parseCollection(retyped.documents, compositionOf(retyped)),
530
- };
1198
+ const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
531
1199
  replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
532
1200
  const existing = rooms.get(record.locator);
533
1201
  if (existing === undefined)
@@ -536,7 +1204,6 @@ export function composeRuntime(options) {
536
1204
  rooms.set(record.locator, {
537
1205
  ...existing,
538
1206
  roomVersion: record.roomVersion,
539
- attachments: record.attachments,
540
1207
  operations: record.operations,
541
1208
  replica: record.replica,
542
1209
  model: modelTypes(model),
@@ -548,10 +1215,6 @@ export function composeRuntime(options) {
548
1215
  },
549
1216
  });
550
1217
  }
551
- if (read.kind === 'refused') {
552
- refusedSource(record, read);
553
- declarationRefusals.add(record.locator);
554
- }
555
1218
  }
556
1219
  return {
557
1220
  lastCompleted,
@@ -562,16 +1225,26 @@ export function composeRuntime(options) {
562
1225
  async function readState(opened) {
563
1226
  const { lastCompleted, locators, read } = await hydrate(opened);
564
1227
  applyChangeSet(read);
1228
+ prepare();
565
1229
  if (replicas.size > 0)
566
1230
  emitRooms();
1231
+ if (member !== null)
1232
+ emitMember();
567
1233
  for (const locator of locators)
568
1234
  emitQueue(locator);
569
1235
  publishRoomDrafts(false);
570
1236
  return lastCompleted;
571
1237
  }
572
1238
  async function open() {
573
- const opened = await openRuntimeStore();
1239
+ if (closed)
1240
+ throw new Error('runtime closed');
1241
+ const opened = await openRuntimeStore(isFenced);
574
1242
  store = opened;
1243
+ const interrupted = await opened.fences();
1244
+ for (const locator of interrupted) {
1245
+ fences.set(locator, { at: discoveries, erased: false });
1246
+ lastFenced.set(locator, ++fencings);
1247
+ }
575
1248
  const kept = options.shell.generation;
576
1249
  const storedGeneration = await opened.generation();
577
1250
  const loss = detectStorageLoss(storedGeneration, kept);
@@ -585,6 +1258,8 @@ export function composeRuntime(options) {
585
1258
  emitStorageLoss();
586
1259
  }
587
1260
  const lastCompleted = await readState(opened);
1261
+ if (interrupted.length > 0)
1262
+ await erased(opened, interrupted);
588
1263
  moveTo({ ...status, lastCompleted, ...attention() });
589
1264
  return opened;
590
1265
  }
@@ -611,11 +1286,12 @@ export function composeRuntime(options) {
611
1286
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
612
1287
  }
613
1288
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
614
- const withdrawn = withdrawals(confirmed, queue, withdrawable);
1289
+ const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
1290
+ const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
615
1291
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
616
1292
  const unqueued = withdrawn.map((batch) => ({
617
1293
  batch,
618
- returned: landed(batch, confirmed) ? null : { kind: 'withdrawn' },
1294
+ returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
619
1295
  }));
620
1296
  const returning = [
621
1297
  ...[...unqueued].reverse(),
@@ -636,28 +1312,48 @@ export function composeRuntime(options) {
636
1312
  });
637
1313
  }
638
1314
  }
639
- await local.write({
640
- next: confirmed.record,
641
- previous: before?.confirmed.record ?? null,
642
- queue: kept,
643
- previousQueue: before?.queue ?? [],
644
- roomDrafts: [...written.values()],
645
- });
646
- for (const batch of before?.queue ?? []) {
1315
+ await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
1316
+ for (const batch of previousQueue) {
647
1317
  if (!kept.includes(batch))
648
1318
  pendings.delete(batch.id);
649
1319
  }
650
1320
  for (const record of written.values())
651
1321
  rememberRoomDraft(record);
652
- for (const { batch, returned } of unqueued) {
653
- journal.record(`outcome ${locator} ${batch.id} ${returned === null ? 'landed' : 'withdrawn'}`);
654
- }
655
- replicas.set(locator, replicaOf(confirmed, kept, before));
656
- return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
657
- room: settling.room,
658
- queue: kept.length !== (before?.queue.length ?? 0),
659
- drafts: [...written.keys()],
660
- });
1322
+ try {
1323
+ await local.write({
1324
+ next: confirmed.record,
1325
+ previous: before?.confirmed.record ?? null,
1326
+ ...(settling.stored === undefined ? {} : { stored: settling.stored }),
1327
+ });
1328
+ }
1329
+ catch (error) {
1330
+ const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
1331
+ if (held !== null)
1332
+ replicas.set(locator, held);
1333
+ else if (kept.length > 0)
1334
+ queuesWithoutReplica.set(locator, kept);
1335
+ else
1336
+ queuesWithoutReplica.delete(locator);
1337
+ publishChangeSet(changeSet(locator, before ?? null, held, {
1338
+ queue: kept.length !== previousQueue.length,
1339
+ drafts: [...written.keys()],
1340
+ }));
1341
+ moveTo({ ...status, ...attention() });
1342
+ throw error;
1343
+ }
1344
+ return () => {
1345
+ queuesWithoutReplica.delete(locator);
1346
+ for (const { batch, returned } of unqueued) {
1347
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
1348
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1349
+ }
1350
+ replicas.set(locator, replicaOf(confirmed, kept, before));
1351
+ return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
1352
+ room: settling.room,
1353
+ queue: kept.length !== previousQueue.length,
1354
+ drafts: [...written.keys()],
1355
+ });
1356
+ };
661
1357
  }
662
1358
  function adoptModels(locator, record) {
663
1359
  for (const [attached, model] of [
@@ -690,10 +1386,16 @@ export function composeRuntime(options) {
690
1386
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
691
1387
  };
692
1388
  }
693
- function publish(local, room, confirmed, base) {
1389
+ function publish(local, room, confirmed, landing, live) {
1390
+ const { base, stored } = landing;
694
1391
  return exclusive(async () => {
695
- if (!continuing())
1392
+ if (stored !== undefined) {
1393
+ await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
1394
+ }
1395
+ if (!live()) {
1396
+ caughtUpTo(room.locator);
696
1397
  return true;
1398
+ }
697
1399
  const prior = replicas.get(room.locator);
698
1400
  if (base !== undefined &&
699
1401
  (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
@@ -701,47 +1403,85 @@ export function composeRuntime(options) {
701
1403
  return false;
702
1404
  }
703
1405
  adoptModels(room.locator, confirmed.record);
704
- const queue = prior?.queue ?? [];
1406
+ const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
705
1407
  const roomVersion = confirmed.record.roomVersion;
706
1408
  const accepted = queue.filter((batch) => {
707
1409
  const accepted = pendings.get(batch.id)?.accepted;
708
1410
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
709
1411
  });
710
- const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
1412
+ let settled;
1413
+ try {
1414
+ settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
1415
+ }
1416
+ catch (error) {
1417
+ if (!exceeded(error))
1418
+ throw error;
1419
+ await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
1420
+ caughtUpTo(room.locator);
1421
+ return true;
1422
+ }
1423
+ const set = settled();
1424
+ if (stored !== undefined) {
1425
+ transfers.delete(room.locator);
1426
+ catching.delete(room.locator);
1427
+ }
711
1428
  for (const batch of accepted)
712
1429
  pendings.delete(batch.id);
713
1430
  stale.delete(room.locator);
1431
+ unindexable.delete(room.locator);
714
1432
  if (unreadable.delete(room.locator))
715
1433
  scopedRefusals.delete(room.locator);
716
1434
  sourceRefusals.delete(room.locator);
717
1435
  declarationRefusals.delete(room.locator);
718
1436
  list(room, true);
1437
+ if (prior === undefined)
1438
+ prepared.add(room.locator);
719
1439
  publishChangeSet(set);
720
1440
  moveTo({ ...status, ...attention() });
1441
+ options.adapter.roomStream?.published(room.locator, roomVersion);
721
1442
  return true;
722
1443
  });
723
1444
  }
724
1445
  async function saveRoomDrafts(locator, named, writtenBy) {
725
1446
  const local = await openLocal();
1447
+ await referenced(locator);
726
1448
  const replica = replicas.get(locator);
727
1449
  const room = rooms.get(locator);
728
1450
  if (replica === undefined || room === undefined) {
729
- throw new Error(`No Local Replica for ${locator}`);
1451
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
730
1452
  }
731
1453
  const saved = [];
732
1454
  for (const { path, revision } of named) {
733
1455
  const record = roomDrafts.get(locator)?.get(path);
734
1456
  if (record === undefined || record.revision !== revision) {
735
- return { outcome: 'refused', rule: 'stale', file: path };
1457
+ return { outcome: 'refused', code: 'stale', retryable: false, file: path };
736
1458
  }
737
1459
  if (!saved.some((draft) => draft.path === path))
738
1460
  saved.push({ path, parts: record.parts });
739
1461
  }
740
1462
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
741
1463
  if (changed.length > 0)
742
- return { outcome: 'refused', rule: 'changed-underneath', parts: changed };
1464
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
743
1465
  const parts = saved.map(({ path, parts }) => changesOf(path, parts));
744
- const built = buildBatch({
1466
+ const changes = parts.flatMap(({ changes }) => changes);
1467
+ const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
1468
+ if (requeue.kind === 'refused')
1469
+ return { outcome: 'refused', ...requeue.refusal };
1470
+ const foreign = [...requeue.withdrawn.keys()].some((origin) => {
1471
+ const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
1472
+ return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
1473
+ });
1474
+ if (foreign) {
1475
+ return {
1476
+ outcome: 'refused',
1477
+ code: 'unknown-revision',
1478
+ retryable: false,
1479
+ file: [...requeue.queued].at(-1),
1480
+ };
1481
+ }
1482
+ const withdrawing = requeue.withdrawn.size > 0;
1483
+ const input = {
1484
+ local: localSaves,
745
1485
  baseRoomVersion: replica.confirmed.record.roomVersion,
746
1486
  composition: compositionFor(replica.confirmed.record),
747
1487
  operations: room.operations,
@@ -749,29 +1489,93 @@ export function composeRuntime(options) {
749
1489
  providerLimits: limits,
750
1490
  handle: room.handle,
751
1491
  visible: replica.visible,
752
- changes: parts.flatMap(({ changes }) => changes),
1492
+ changes,
753
1493
  bases: parts.flatMap(({ bases }) => bases),
754
1494
  writtenBy,
1495
+ clean: {
1496
+ text: replica.confirmed.record.documents,
1497
+ binaries: replica.confirmed.record.binaries,
1498
+ composition: compositionOf(replica.confirmed.record),
1499
+ },
1500
+ };
1501
+ const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
1502
+ if (checked?.kind === 'refused')
1503
+ return { outcome: 'refused', ...checked.refusal };
1504
+ const owners = [...requeue.owners].flatMap((origin) => {
1505
+ const batch = requeue.withdrawn.get(origin);
1506
+ return batch === undefined || batch === null ? [] : [{ origin, batch }];
755
1507
  });
756
- if (built.kind === 'refused')
1508
+ if (owners.length > 1) {
1509
+ return {
1510
+ outcome: 'refused',
1511
+ code: 'unknown-revision',
1512
+ retryable: false,
1513
+ file: [...requeue.queued].at(-1),
1514
+ };
1515
+ }
1516
+ const built = !withdrawing
1517
+ ? buildBatch(input)
1518
+ : requeue.changes.length === 0
1519
+ ? null
1520
+ : buildBatch({
1521
+ ...input,
1522
+ visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
1523
+ changes: requeue.changes,
1524
+ });
1525
+ if (built?.kind === 'refused')
757
1526
  return { outcome: 'refused', ...built.refusal };
758
- const batches = [...replica.queue, built.batch];
1527
+ const [owner] = owners;
1528
+ const join = built === null || owner === undefined
1529
+ ? null
1530
+ : joined(requeue.queue, owner.batch, built.batch);
1531
+ if (join?.kind === 'refused')
1532
+ return { outcome: 'refused', ...join.refusal };
1533
+ const withdrawn = new Map(requeue.withdrawn);
1534
+ if (join !== null && owner !== undefined)
1535
+ withdrawn.set(owner.origin, join.batch);
1536
+ const drafted = new Map();
1537
+ for (const [origin, replaced] of withdrawn) {
1538
+ if (replaced === null)
1539
+ continue;
1540
+ const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
1541
+ drafted.set(replaced.id, replaced === join?.batch
1542
+ ? [
1543
+ ...carried.map((held) => joinedDraft(held, saved)),
1544
+ ...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
1545
+ ]
1546
+ : carried);
1547
+ }
1548
+ if (built !== null && join === null)
1549
+ drafted.set(built.batch.id, saved);
1550
+ const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
1551
+ for (const replaced of withdrawn.values()) {
1552
+ if (replaced === null || !batches.includes(replaced))
1553
+ continue;
1554
+ const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
1555
+ if (limited !== null)
1556
+ return { outcome: 'refused', ...limited };
1557
+ }
759
1558
  const consumed = saved.map(({ path }) => path);
760
1559
  try {
761
- await local.write({
762
- next: replica.confirmed.record,
763
- previous: replica.confirmed.record,
1560
+ await local.keepAuthored({
1561
+ locator,
764
1562
  queue: batches,
765
- previousQueue: replica.queue,
766
- saved,
1563
+ saved: drafted,
767
1564
  consumedRoomDrafts: consumed,
768
1565
  });
769
1566
  }
770
1567
  catch {
771
- return { outcome: 'refused', rule: 'durable-write' };
1568
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
772
1569
  }
773
- pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
774
- recorded = false;
1570
+ if (fences.has(locator))
1571
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1572
+ for (const origin of withdrawn.keys())
1573
+ pendings.delete(origin);
1574
+ for (const [id, drafts] of drafted) {
1575
+ pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
1576
+ }
1577
+ if (!localSaves)
1578
+ recorded = false;
775
1579
  for (const path of consumed)
776
1580
  forgetRoomDraft(locator, path);
777
1581
  replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
@@ -779,9 +1583,12 @@ export function composeRuntime(options) {
779
1583
  queue: true,
780
1584
  drafts: consumed,
781
1585
  }));
782
- moveTo({ ...saving(status), ...attention() });
1586
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
783
1587
  submitLater(locator);
784
- return { outcome: 'saved', identifier: built.batch.id };
1588
+ const identifier = join?.batch.id ??
1589
+ built?.batch.id ??
1590
+ [...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
1591
+ return { outcome: 'saved', identifier };
785
1592
  }
786
1593
  async function mark(local, locator, identifier, submission, lost) {
787
1594
  const state = pendings.get(identifier);
@@ -791,6 +1598,8 @@ export function composeRuntime(options) {
791
1598
  return;
792
1599
  }
793
1600
  await local.submit(locator, identifier, submission, lost);
1601
+ if (fences.has(locator))
1602
+ return;
794
1603
  pendings.set(identifier, { ...state, submission, lost });
795
1604
  const replica = replicas.get(locator);
796
1605
  if (replica !== undefined)
@@ -801,22 +1610,24 @@ export function composeRuntime(options) {
801
1610
  if (!continuing())
802
1611
  throw new Error('connection refused');
803
1612
  await local.keepUpload(locator, identifier, upload);
1613
+ if (fences.has(locator))
1614
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
804
1615
  pendings.set(identifier, { ...pendings.get(identifier), upload });
805
1616
  if (!continuing())
806
1617
  throw new Error('connection refused');
807
1618
  }
808
1619
  async function readHistory(locator, path, query = {}) {
809
1620
  if (closed || refusal !== null)
810
- return { kind: 'transport-uncertainty' };
1621
+ return UNCERTAIN;
811
1622
  try {
812
1623
  await ensureAuthorization();
813
1624
  }
814
1625
  catch {
815
- return { kind: 'transport-uncertainty' };
816
- }
817
- if (closed || authorization === undefined || refusal !== null) {
818
- return { kind: 'transport-uncertainty' };
1626
+ return UNCERTAIN;
819
1627
  }
1628
+ if (closed || authorization === undefined || refusal !== null)
1629
+ return UNCERTAIN;
1630
+ const since = fencings;
820
1631
  const transfer = ['transfer', 'history', locator, path];
821
1632
  if (query.revision !== undefined)
822
1633
  transfer.push(query.revision);
@@ -831,6 +1642,8 @@ export function composeRuntime(options) {
831
1642
  ...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
832
1643
  });
833
1644
  journal.record(`outcome history ${result.kind}`);
1645
+ if (fencedSince(locator, since))
1646
+ return failure({ kind: 'room-not-readable', locator });
834
1647
  if (result.kind === 'scoped-refusal') {
835
1648
  const room = replicas.get(result.locator)?.confirmed.record;
836
1649
  if (room !== undefined)
@@ -838,12 +1651,106 @@ export function composeRuntime(options) {
838
1651
  }
839
1652
  if (result.kind === 'revoked-authorization')
840
1653
  await refuse('revoked-authorization');
841
- return result;
1654
+ return historyAnswer(result);
842
1655
  }
843
1656
  catch {
844
1657
  journal.record('outcome history transport-uncertainty');
845
- return { kind: 'transport-uncertainty' };
1658
+ return UNCERTAIN;
1659
+ }
1660
+ }
1661
+ function queuedBlob(locator, path, localId) {
1662
+ for (const batch of replicas.get(locator)?.queue ?? []) {
1663
+ const change = batch.changes[queuedFile(batch, path, localId)];
1664
+ if (change?.kind === 'binary')
1665
+ return change.blob;
846
1666
  }
1667
+ return null;
1668
+ }
1669
+ function binaryRoom(request) {
1670
+ if (request.kind === 'path')
1671
+ return request.locator;
1672
+ const scope = options.adapter.referrerScope(request.referrer);
1673
+ return scope?.kind === 'room' ? scope.locator : null;
1674
+ }
1675
+ async function readBinary(request) {
1676
+ if (closed || refusal !== null)
1677
+ return UNCERTAIN;
1678
+ try {
1679
+ await ensureAuthorization();
1680
+ }
1681
+ catch {
1682
+ return UNCERTAIN;
1683
+ }
1684
+ if (closed || authorization === undefined || refusal !== null)
1685
+ return UNCERTAIN;
1686
+ const since = fencings;
1687
+ journal.record(request.kind === 'path'
1688
+ ? `transfer binary ${request.locator} ${request.path}`
1689
+ : `transfer binary ${request.fileRef}`);
1690
+ try {
1691
+ const result = await options.adapter.binary(authorization, request);
1692
+ journal.record(`outcome binary ${result.kind}`);
1693
+ const locator = binaryRoom(request);
1694
+ if (locator !== null && fencedSince(locator, since))
1695
+ return failure({ kind: 'room-not-readable', locator });
1696
+ if (result.kind === 'scoped-refusal') {
1697
+ const room = replicas.get(result.locator)?.confirmed.record;
1698
+ if (room !== undefined)
1699
+ await scoped(result, room, await openLocal());
1700
+ }
1701
+ if (result.kind === 'revoked-authorization')
1702
+ await refuse('revoked-authorization');
1703
+ return binaryAnswer(result);
1704
+ }
1705
+ catch {
1706
+ journal.record('outcome binary transport-uncertainty');
1707
+ return UNCERTAIN;
1708
+ }
1709
+ }
1710
+ async function readBinaryPath(locator, path, revision) {
1711
+ await ready;
1712
+ if (revision !== undefined)
1713
+ return readBinary({ kind: 'path', locator, path, revision });
1714
+ const visible = replicas.get(locator)?.visible;
1715
+ const held = visible?.binaries[path];
1716
+ if (visible === undefined || held === undefined)
1717
+ return absentBinary(path);
1718
+ if (held.state === 'pending') {
1719
+ const blob = queuedBlob(locator, path, held.localId);
1720
+ return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1721
+ }
1722
+ const record = binaryRecord(path, held, visible.revisions[path]);
1723
+ if (record.state !== 'accepted')
1724
+ return absentBinary(path);
1725
+ const replica = replicas.get(locator);
1726
+ const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
1727
+ const holds = (at) => {
1728
+ const kept = replica?.confirmed.record.binaries[at];
1729
+ return (kept?.state === 'accepted' &&
1730
+ kept.fileRef === record.fileRef &&
1731
+ replica?.confirmed.record.revisions[at] === record.revision);
1732
+ };
1733
+ let confirmed = path;
1734
+ for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
1735
+ const change = queued[index];
1736
+ if (change.kind === 'move' && change.to === confirmed)
1737
+ confirmed = change.path;
1738
+ }
1739
+ return readBinary({
1740
+ kind: 'path',
1741
+ locator,
1742
+ path: confirmed,
1743
+ revision: record.revision,
1744
+ fileRef: record.fileRef,
1745
+ });
1746
+ }
1747
+ async function readBinaryReference(reference) {
1748
+ await ready;
1749
+ return readBinary({
1750
+ kind: 'reference',
1751
+ fileRef: reference.fileRef,
1752
+ referrer: reference.referrer,
1753
+ });
847
1754
  }
848
1755
  function declaredAgents() {
849
1756
  for (const capability of capabilities) {
@@ -861,21 +1768,21 @@ export function composeRuntime(options) {
861
1768
  }
862
1769
  return undefined;
863
1770
  }
864
- async function invokeAction(name, input) {
1771
+ async function invokeAction(name, input, dispatching) {
865
1772
  await ready;
866
1773
  if (closed || refusal !== null)
867
- return { kind: 'transport-uncertainty' };
1774
+ return UNCERTAIN;
868
1775
  if (declaredAction(name) === undefined)
869
- return { kind: 'undeclared' };
1776
+ return undeclared(name);
870
1777
  try {
871
1778
  await ensureAuthorization();
872
1779
  }
873
1780
  catch {
874
- return { kind: 'transport-uncertainty' };
875
- }
876
- if (closed || authorization === undefined || refusal !== null) {
877
- return { kind: 'transport-uncertainty' };
1781
+ return UNCERTAIN;
878
1782
  }
1783
+ if (closed || authorization === undefined || refusal !== null)
1784
+ return UNCERTAIN;
1785
+ await dispatching?.();
879
1786
  journal.record(`transfer action ${name}`);
880
1787
  try {
881
1788
  const result = await options.adapter.action(authorization, { name, input });
@@ -889,13 +1796,67 @@ export function composeRuntime(options) {
889
1796
  journal.record(`outcome action ${result.kind}`);
890
1797
  if (result.kind === 'rejected')
891
1798
  journal.record(`refusal rejected ${result.code}`);
892
- return result;
1799
+ return calledResult(result);
893
1800
  }
894
1801
  catch {
895
1802
  journal.record('outcome action transport-uncertainty');
896
- return { kind: 'transport-uncertainty' };
1803
+ return UNCERTAIN;
1804
+ }
1805
+ }
1806
+ function unsettled(result) {
1807
+ return result.kind !== 'action' && result.retryable;
1808
+ }
1809
+ async function recoveredAction(name, input) {
1810
+ const operation = operationOf(input);
1811
+ if (operation === null)
1812
+ return invokeAction(name, input);
1813
+ const kept = async () => (await openLocal()).keepCall({ operation, name, input });
1814
+ calling.add(operation);
1815
+ try {
1816
+ let result = await invokeAction(name, input, kept);
1817
+ if (unsettled(result)) {
1818
+ await recovery.attempt(`action ${operation}`, async () => {
1819
+ if (closed || refusal !== null)
1820
+ return 'done';
1821
+ if (!socketOpen)
1822
+ return 'again';
1823
+ result = await invokeAction(name, input, kept);
1824
+ return unsettled(result) ? 'again' : 'done';
1825
+ }, { again: true });
1826
+ }
1827
+ if (!unsettled(result))
1828
+ await (await openLocal()).dropCall(operation);
1829
+ return result;
1830
+ }
1831
+ finally {
1832
+ calling.delete(operation);
897
1833
  }
898
1834
  }
1835
+ async function resendCalls(signal) {
1836
+ if (!socketOpen || closed || stopping)
1837
+ return 'done';
1838
+ const local = await openLocal();
1839
+ for (const held of await local.calls()) {
1840
+ if (calling.has(held.operation))
1841
+ continue;
1842
+ if (closed || signal.aborted)
1843
+ return 'done';
1844
+ if (pastHorizon(held.operation)) {
1845
+ await local.dropCall(held.operation);
1846
+ continue;
1847
+ }
1848
+ calling.add(held.operation);
1849
+ try {
1850
+ if (unsettled(await invokeAction(held.name, held.input)))
1851
+ return 'again';
1852
+ await local.dropCall(held.operation);
1853
+ }
1854
+ finally {
1855
+ calling.delete(held.operation);
1856
+ }
1857
+ }
1858
+ return 'done';
1859
+ }
899
1860
  function continuing() {
900
1861
  return !closed && !discoveryPaused;
901
1862
  }
@@ -910,11 +1871,12 @@ export function composeRuntime(options) {
910
1871
  options.adapter.reached(locator, written, roomVersion) &&
911
1872
  !options.adapter.reached(locator, roomVersion, written));
912
1873
  }
913
- async function pullRefresh(authorized, confirmed) {
914
- if (!continuing())
1874
+ async function pullRefresh(authorized, confirmed, live) {
1875
+ if (!live())
915
1876
  return unconfirmed;
1877
+ const turn = generation;
916
1878
  try {
917
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
1879
+ const result = await refreshRoom(reads, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
918
1880
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
919
1881
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
920
1882
  return result;
@@ -923,29 +1885,101 @@ export function composeRuntime(options) {
923
1885
  journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
924
1886
  throw error;
925
1887
  }
1888
+ finally {
1889
+ reads.finished(confirmed.record.locator, turn);
1890
+ }
926
1891
  }
927
- async function pullBootstrap(authorized, room) {
928
- if (!continuing())
1892
+ function staging(local, live) {
1893
+ return {
1894
+ held: (locator) => transfers.get(locator) ?? null,
1895
+ staged: (record) => local.staged(record),
1896
+ stage: (record, page) => exclusive(async () => {
1897
+ if (!live())
1898
+ return false;
1899
+ const bytes = layerBytes(page);
1900
+ if (!(await admitted(local, record.locator, bytes)))
1901
+ return false;
1902
+ try {
1903
+ await local.stage(record, page);
1904
+ }
1905
+ catch (error) {
1906
+ if (!exceeded(error))
1907
+ throw error;
1908
+ await exceededOn(record.locator, bytes);
1909
+ return false;
1910
+ }
1911
+ transfers.set(record.locator, record);
1912
+ readSide.recover();
1913
+ return true;
1914
+ }),
1915
+ abandon: (locator) => exclusive(async () => {
1916
+ if (!live())
1917
+ return;
1918
+ await local.abandon(locator);
1919
+ transfers.delete(locator);
1920
+ readSide.recover();
1921
+ }),
1922
+ };
1923
+ }
1924
+ function reusable(room) {
1925
+ const record = replicas.get(room.locator)?.confirmed.record;
1926
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1927
+ return null;
1928
+ return { documents: record.documents, revisions: record.revisions };
1929
+ }
1930
+ async function pullBootstrap(authorized, room, local, live) {
1931
+ if (!live())
929
1932
  return unconfirmed;
1933
+ const turn = generation;
1934
+ if (replicas.has(room.locator)) {
1935
+ catching.add(room.locator);
1936
+ readSide.recover();
1937
+ }
930
1938
  try {
931
- const result = await bootstrapRoom(options.adapter, authorized, room, {
1939
+ const result = await bootstrapRoom(reads, authorized, room, {
932
1940
  collection: modelOf(room.locator),
933
1941
  selection: modelOf(room.locator, 'typed-selection'),
934
- }, (models, held) => remodel(room.locator, models, held), continuing);
1942
+ }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
935
1943
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
936
1944
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1945
+ if (result.kind !== 'staged')
1946
+ caughtUpTo(room.locator);
937
1947
  return result;
938
1948
  }
939
1949
  catch (error) {
940
1950
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
1951
+ caughtUpTo(room.locator);
941
1952
  throw error;
942
1953
  }
1954
+ finally {
1955
+ reads.finished(room.locator, turn);
1956
+ }
1957
+ }
1958
+ function caughtUpTo(locator) {
1959
+ if (catching.delete(locator))
1960
+ readSide.recover();
943
1961
  }
944
- async function advance(room, local, authorized) {
1962
+ function behind(room) {
1963
+ const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1964
+ return (held !== undefined &&
1965
+ [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
1966
+ }
1967
+ async function advance(room, local, authorized, live) {
1968
+ const carried = reads.carried(room.locator);
1969
+ const withheld = await advanceOnce(room, local, authorized, live);
1970
+ if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
1971
+ return withheld;
1972
+ }
1973
+ return advanceOnce(room, local, authorized, live);
1974
+ }
1975
+ async function advanceOnce(room, local, authorized, live) {
1976
+ await referenced(room.locator);
1977
+ if (!live() || paused.has(room.locator))
1978
+ return unconfirmed;
945
1979
  const held = replicas.get(room.locator);
946
1980
  if (held !== undefined) {
947
- const refreshed = await pullRefresh(authorized, held.confirmed);
948
- if (!continuing())
1981
+ const refreshed = await pullRefresh(authorized, held.confirmed, live);
1982
+ if (!live())
949
1983
  return unconfirmed;
950
1984
  if (refreshed.kind === 'still-current') {
951
1985
  stale.delete(room.locator);
@@ -959,31 +1993,45 @@ export function composeRuntime(options) {
959
1993
  return null;
960
1994
  }
961
1995
  if (refreshed.kind === 'advanced') {
962
- if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
1996
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
963
1997
  const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
964
1998
  const reached = now !== undefined &&
965
1999
  options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
966
- return reached ? null : advance(room, local, authorized);
2000
+ return reached ? null : advance(room, local, authorized, live);
967
2001
  }
968
2002
  await retainModel(local, room, 'collection', modelOf(room.locator));
969
2003
  return null;
970
2004
  }
971
2005
  if (refreshed.kind !== 'beyond-horizon')
972
2006
  return refreshed;
2007
+ if (opened !== null) {
2008
+ owed.add(room.locator);
2009
+ readSide.recover();
2010
+ return unconfirmed;
2011
+ }
973
2012
  }
974
- const bootstrapped = await pullBootstrap(authorized, room);
975
- if (!continuing())
2013
+ const bootstrapped = await pullBootstrap(authorized, room, local, live);
2014
+ if (!live())
976
2015
  return unconfirmed;
977
2016
  if (bootstrapped.kind !== 'staged')
978
2017
  return bootstrapped;
979
- await publish(local, room, bootstrapped.confirmed);
2018
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
980
2019
  return null;
981
2020
  }
982
2021
  async function withhold(withheld, room, local, progress) {
2022
+ if (withheld.kind === 'unconfirmed')
2023
+ return;
983
2024
  if (withheld.kind === 'refused')
984
2025
  stale.delete(room.locator);
985
2026
  else
986
2027
  stale.add(room.locator);
2028
+ if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
2029
+ const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
2030
+ repairable.set(room.locator, reset);
2031
+ repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
2032
+ }
2033
+ else
2034
+ repairable.delete(room.locator);
987
2035
  if (withheld.kind === 'scoped-refusal') {
988
2036
  await scoped(withheld, room, local);
989
2037
  }
@@ -1008,6 +2056,7 @@ export function composeRuntime(options) {
1008
2056
  function refusedSource(room, { refusal }) {
1009
2057
  sourceRefusals.set(room.locator, {
1010
2058
  code: refusal.rule,
2059
+ retryable: false,
1011
2060
  fields: {
1012
2061
  file: refusal.file,
1013
2062
  ...(refusal.location === undefined ? {} : { location: refusal.location }),
@@ -1018,12 +2067,21 @@ export function composeRuntime(options) {
1018
2067
  unreadable.add(room.locator);
1019
2068
  list(room, false);
1020
2069
  emitRooms();
2070
+ moveTo({ ...status, ...attention() });
1021
2071
  journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
1022
2072
  }
1023
2073
  function leaving(batch, result) {
1024
2074
  if (result.kind === 'expired')
1025
- return { batch, returned: { kind: 'expired' } };
1026
- return { batch, returned: { kind: result.kind, code: result.code, fields: result.fields } };
2075
+ return { batch, returned: returnedBy('expired') };
2076
+ return {
2077
+ batch,
2078
+ returned: {
2079
+ kind: result.kind,
2080
+ code: result.code,
2081
+ fields: result.fields,
2082
+ retryable: result.retryable,
2083
+ },
2084
+ };
1027
2085
  }
1028
2086
  function logAccepted(locator, submitted, accepted) {
1029
2087
  journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
@@ -1031,6 +2089,7 @@ export function composeRuntime(options) {
1031
2089
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1032
2090
  }
1033
2091
  async function adoptAccepted(room, local, submitted, accepted) {
2092
+ await referenced(room.locator);
1034
2093
  if (!continuing())
1035
2094
  return unconfirmed;
1036
2095
  const current = replicas.get(room.locator);
@@ -1041,22 +2100,23 @@ export function composeRuntime(options) {
1041
2100
  logAccepted(room.locator, submitted, accepted);
1042
2101
  const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1043
2102
  ? { kind: 'staged', confirmed: current.confirmed }
1044
- : applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
2103
+ : applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
1045
2104
  ? accepted.roomVersion
1046
2105
  : held, accepted.roomVersion);
1047
2106
  if (applied.kind !== 'staged') {
1048
2107
  const state = pendings.get(submitted.id);
1049
2108
  await local.accept(room.locator, submitted.id, accepted.roomVersion);
1050
- if (state !== undefined)
2109
+ if (state !== undefined && !fences.has(room.locator))
1051
2110
  pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1052
2111
  emitQueue(room.locator);
1053
2112
  return applied;
1054
2113
  }
1055
- const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
2114
+ const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1056
2115
  pendings.delete(submitted.id);
1057
2116
  unreadable.delete(room.locator);
1058
2117
  sourceRefusals.delete(room.locator);
1059
2118
  declarationRefusals.delete(room.locator);
2119
+ const set = settled();
1060
2120
  list(room, true);
1061
2121
  publishChangeSet(set);
1062
2122
  moveTo({ ...status, ...attention() });
@@ -1070,14 +2130,14 @@ export function composeRuntime(options) {
1070
2130
  refusedSource(room, failed);
1071
2131
  return false;
1072
2132
  }
1073
- async function submitRoom(room, local, authorized, progress) {
1074
- while (continuing() && authorization !== undefined) {
2133
+ async function submitRoom(room, local, authorized, progress, signal) {
2134
+ while (continuing() && authorization !== undefined && !signal.aborted) {
1075
2135
  const replica = replicas.get(room.locator);
1076
2136
  const head = replica?.queue[0];
1077
2137
  if (replica === undefined || head === undefined)
1078
- return;
2138
+ return 'done';
1079
2139
  if (pendings.get(head.id)?.accepted !== undefined)
1080
- return;
2140
+ return 'done';
1081
2141
  const state = pendings.get(head.id);
1082
2142
  const { submission, lost: earlier } = state ?? {
1083
2143
  submission: { state: 'waiting' },
@@ -1085,24 +2145,30 @@ export function composeRuntime(options) {
1085
2145
  };
1086
2146
  if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
1087
2147
  progress.complete = false;
1088
- return;
2148
+ return { at: Date.parse(submission.reset) };
1089
2149
  }
1090
2150
  if (writesHeld()) {
1091
2151
  progress.complete = false;
1092
2152
  journal.record(`submission ${room.locator} ${head.id} held`);
1093
- return;
2153
+ return 'done';
1094
2154
  }
1095
2155
  const lost = earlier || submission.state === 'submitted';
2156
+ if (lost && pastHorizon(head.id)) {
2157
+ await discardPending(room, local, signal);
2158
+ return 'done';
2159
+ }
1096
2160
  let result;
1097
2161
  journal.record(`submission ${room.locator} ${head.id}`);
1098
2162
  await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1099
- if (discoveryPaused)
1100
- return;
1101
- if (writesHeld()) {
2163
+ if (discoveryPaused || fences.has(room.locator))
2164
+ return 'done';
2165
+ if (signal.aborted || writesHeld()) {
1102
2166
  await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
2167
+ if (signal.aborted)
2168
+ return 'done';
1103
2169
  progress.complete = false;
1104
2170
  journal.record(`submission ${room.locator} ${head.id} held`);
1105
- return;
2171
+ return 'done';
1106
2172
  }
1107
2173
  const opensBefore = openSignals;
1108
2174
  const answered = awaitingAnswer(room.locator);
@@ -1118,32 +2184,36 @@ export function composeRuntime(options) {
1118
2184
  progress.complete = false;
1119
2185
  if (continuing())
1120
2186
  progress.reachable = false;
1121
- return;
2187
+ return 'again';
2188
+ }
2189
+ if (signal.aborted) {
2190
+ answered();
2191
+ return 'done';
1122
2192
  }
1123
2193
  if (result.kind !== 'accepted' || !continuing())
1124
2194
  answered();
1125
2195
  if (!continuing())
1126
- return;
2196
+ return 'done';
1127
2197
  if (result.kind === 'transport-uncertainty') {
1128
2198
  progress.complete = false;
1129
2199
  progress.reachable = false;
1130
- return;
2200
+ return 'again';
1131
2201
  }
1132
2202
  if (result.kind === 'room-not-readable') {
1133
2203
  progress.complete = false;
1134
2204
  unreadable.add(room.locator);
1135
2205
  list(room, false);
1136
2206
  emitRooms();
1137
- return;
2207
+ return 'done';
1138
2208
  }
1139
2209
  if (result.kind === 'revoked-authorization') {
1140
2210
  await refuse('revoked-authorization');
1141
- return;
2211
+ return 'done';
1142
2212
  }
1143
2213
  if (result.kind === 'scoped-refusal') {
1144
2214
  await scoped(result, room, local);
1145
2215
  progress.complete = false;
1146
- return;
2216
+ return 'done';
1147
2217
  }
1148
2218
  if (result.kind === 'write-halted') {
1149
2219
  journal.record(`outcome ${room.locator} ${head.id} write-halted`);
@@ -1152,16 +2222,24 @@ export function composeRuntime(options) {
1152
2222
  continue;
1153
2223
  haltedRefusal = true;
1154
2224
  progress.complete = false;
1155
- return;
2225
+ return 'done';
1156
2226
  }
1157
2227
  if (result.kind === 'conflicted' ||
1158
2228
  result.kind === 'rejected' ||
1159
2229
  result.kind === 'expired' ||
1160
2230
  result.kind === 'retryable-refusal') {
2231
+ if (result.kind === 'expired' && lost && result.restored !== true) {
2232
+ journal.record(`outcome ${room.locator} ${head.id} expired`);
2233
+ await discardPending(room, local, signal);
2234
+ return 'done';
2235
+ }
1161
2236
  if (result.kind === 'expired') {
1162
- const withheld = await advance(room, local, authorized);
1163
- if (withheld !== null)
1164
- return withhold(withheld, room, local, progress);
2237
+ await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
2238
+ const withheld = await advance(room, local, authorized, continuing);
2239
+ if (withheld !== null) {
2240
+ await withhold(withheld, room, local, progress);
2241
+ return 'done';
2242
+ }
1165
2243
  }
1166
2244
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
1167
2245
  (result.kind === 'conflicted' && result.roomVersion !== undefined
@@ -1174,8 +2252,7 @@ export function composeRuntime(options) {
1174
2252
  const reset = result.reset;
1175
2253
  progress.complete = false;
1176
2254
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1177
- armTimer();
1178
- return;
2255
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
1179
2256
  }
1180
2257
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
1181
2258
  const left = leaving(head, result);
@@ -1186,37 +2263,82 @@ export function composeRuntime(options) {
1186
2263
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
1187
2264
  return;
1188
2265
  }
1189
- const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
2266
+ const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1190
2267
  leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1191
2268
  (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1192
2269
  ? { ...left, returned: null }
1193
2270
  : left,
1194
2271
  });
1195
- publishChangeSet(set);
2272
+ publishChangeSet(settled());
1196
2273
  moveTo({ ...status, ...attention() });
1197
2274
  });
1198
2275
  if (result.kind === 'expired')
1199
2276
  continue;
1200
- const withheld = await advance(room, local, authorized);
1201
- if (withheld !== null)
1202
- return withhold(withheld, room, local, progress);
2277
+ const withheld = await advance(room, local, authorized, continuing);
2278
+ if (withheld !== null) {
2279
+ await withhold(withheld, room, local, progress);
2280
+ return 'done';
2281
+ }
1203
2282
  continue;
1204
2283
  }
1205
2284
  const kept = await adopted(room, local, head, result).finally(answered);
1206
2285
  if (!kept) {
1207
2286
  progress.complete = false;
1208
- return;
2287
+ return 'done';
1209
2288
  }
1210
2289
  }
2290
+ return 'done';
2291
+ }
2292
+ function pastHorizon(identity) {
2293
+ return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
2294
+ }
2295
+ async function discardPending(room, local, signal) {
2296
+ const { locator } = room;
2297
+ const discarded = await exclusive(async () => {
2298
+ if (fences.has(locator))
2299
+ return false;
2300
+ const before = replicas.get(locator);
2301
+ const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
2302
+ fences.set(locator, { at: discoveries, erased: false });
2303
+ lastFenced.set(locator, ++fencings);
2304
+ try {
2305
+ await local.discard(locator);
2306
+ }
2307
+ finally {
2308
+ fences.delete(locator);
2309
+ }
2310
+ for (const batch of queue)
2311
+ pendings.delete(batch.id);
2312
+ queuesWithoutReplica.delete(locator);
2313
+ const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
2314
+ if (after !== null)
2315
+ replicas.set(locator, after);
2316
+ journal.record(`room discarded ${locator}`);
2317
+ publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
2318
+ moveTo({ ...status, ...attention() });
2319
+ return true;
2320
+ });
2321
+ if (!discarded)
2322
+ return;
2323
+ const progress = { complete: true, reachable: true };
2324
+ await roomTask(locator, async (live) => {
2325
+ await Promise.race([
2326
+ reload(room, local, progress, () => live() && !signal.aborted),
2327
+ abandonedOn(signal),
2328
+ ]);
2329
+ });
2330
+ if (signal.aborted)
2331
+ return;
2332
+ await rest(progress);
1211
2333
  }
1212
2334
  function awaitingAnswer(locator) {
1213
2335
  let release = () => { };
1214
2336
  const awaited = new Promise((resolve) => {
1215
2337
  release = resolve;
1216
2338
  });
1217
- answering.set(locator, awaited);
2339
+ answering.set(locator, { generation, answered: awaited });
1218
2340
  return () => {
1219
- if (answering.get(locator) === awaited)
2341
+ if (answering.get(locator)?.answered === awaited)
1220
2342
  answering.delete(locator);
1221
2343
  release();
1222
2344
  };
@@ -1249,7 +2371,7 @@ export function composeRuntime(options) {
1249
2371
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1250
2372
  };
1251
2373
  const reclassified = sameMembership(declared, held.confirmed.record)
1252
- ? readConfirmed(declared, held.confirmed)
2374
+ ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
1253
2375
  : readReclassified(declared, held.confirmed);
1254
2376
  const record = reclassified.kind === 'staged'
1255
2377
  ? declared
@@ -1257,12 +2379,10 @@ export function composeRuntime(options) {
1257
2379
  ...declared,
1258
2380
  typedSelection: held.confirmed.record.typedSelection,
1259
2381
  };
1260
- const read = reclassified.kind === 'deferred' ? readConfirmed(record, held.confirmed) : reclassified;
1261
- await local.write({
1262
- next: record,
1263
- previous: held.confirmed.record,
1264
- queue: held.queue,
1265
- });
2382
+ const read = reclassified.kind === 'deferred'
2383
+ ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
2384
+ : reclassified;
2385
+ await local.write({ next: record, previous: held.confirmed.record });
1266
2386
  if (read.kind === 'staged') {
1267
2387
  replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1268
2388
  if (declarationRefusals.delete(room.locator)) {
@@ -1289,7 +2409,16 @@ export function composeRuntime(options) {
1289
2409
  });
1290
2410
  }
1291
2411
  async function scoped(result, room, local) {
1292
- scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
2412
+ if ((result.scope === 'content' || result.scope === 'sync') && !result.retryable) {
2413
+ journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
2414
+ await loseRooms([room.locator]);
2415
+ return;
2416
+ }
2417
+ scopedRefusals.set(room.locator, {
2418
+ code: result.code,
2419
+ fields: result.fields,
2420
+ retryable: result.retryable,
2421
+ });
1293
2422
  let scopedAway = room;
1294
2423
  if (result.scope === 'model')
1295
2424
  await retainModel(local, room, 'collection', {
@@ -1326,24 +2455,27 @@ export function composeRuntime(options) {
1326
2455
  roomVersion: record.roomVersion,
1327
2456
  documents: record.documents,
1328
2457
  revisions: record.revisions,
1329
- attachments: record.attachments,
1330
2458
  },
1331
2459
  ];
1332
2460
  });
1333
2461
  }
1334
- async function synchronize(listed, local, progress, attempt) {
1335
- const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1336
- const ready = listed.filter((_, index) => {
2462
+ async function synchronize(listed, local, progress, attempt, live) {
2463
+ const acquired = listed.filter(acquires);
2464
+ const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
2465
+ if (!live())
2466
+ return;
2467
+ const ready = acquired.filter((_, index) => {
1337
2468
  const outcome = prepared[index];
1338
2469
  return outcome?.status === 'fulfilled' && outcome.value;
1339
2470
  });
1340
2471
  if (attempt === 'opened') {
1341
- options.adapter.roomStream?.hold(heldRooms(ready));
2472
+ options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
1342
2473
  handed = true;
1343
2474
  }
2475
+ discovering?.release();
1344
2476
  const loaded = await Promise.allSettled(ready
1345
2477
  .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1346
- .map((room) => loadRoom(room, local, progress)));
2478
+ .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
1347
2479
  const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1348
2480
  if (failed !== undefined)
1349
2481
  throw failed.reason;
@@ -1365,21 +2497,12 @@ export function composeRuntime(options) {
1365
2497
  await retainModel(local, room, 'collection', modelOf(room.locator));
1366
2498
  if (discoveryPaused || authorization === undefined)
1367
2499
  return false;
1368
- if (room.replica.admission !== 'admitted') {
1369
- if (replicas.has(room.locator))
1370
- progress.complete = false;
1371
- return false;
1372
- }
1373
- return true;
2500
+ return room.replica.admission === 'admitted';
1374
2501
  }
1375
- async function caughtUp(room, local, progress) {
1376
- const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1377
- if (held === undefined || authorization === undefined)
1378
- return;
1379
- const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1380
- if (!behind)
2502
+ async function caughtUp(room, local, progress, live) {
2503
+ if (authorization === undefined || !live() || !behind(room))
1381
2504
  return;
1382
- const withheld = await advance(room, local, authorization);
2505
+ const withheld = await advance(room, local, authorization, live);
1383
2506
  if (withheld !== null)
1384
2507
  await withhold(withheld, room, local, progress);
1385
2508
  }
@@ -1389,39 +2512,46 @@ export function composeRuntime(options) {
1389
2512
  return model.status === 'unavailable' && model.reason === 'revoked';
1390
2513
  });
1391
2514
  }
1392
- async function loadRoom(room, local, progress) {
1393
- if (discoveryPaused || authorization === undefined)
2515
+ async function loadRoom(room, local, progress, live) {
2516
+ if (!live() || authorization === undefined)
1394
2517
  return;
1395
2518
  if (replicas.has(room.locator) && revoked(room.locator)) {
1396
- await reload(room, local, progress);
2519
+ await reload(room, local, progress, live);
1397
2520
  return;
1398
2521
  }
1399
- const withheld = await advance(room, local, authorization);
2522
+ const withheld = await advance(room, local, authorization, live);
1400
2523
  if (withheld !== null) {
1401
2524
  await withhold(withheld, room, local, progress);
1402
2525
  return;
1403
2526
  }
1404
- await caughtUp(room, local, progress);
2527
+ await caughtUp(room, local, progress, live);
1405
2528
  }
1406
- async function reload(room, local, progress) {
1407
- if (discoveryPaused || authorization === undefined)
1408
- return;
1409
- const bootstrapped = await pullBootstrap(authorization, room);
1410
- if (!continuing())
1411
- return;
1412
- if (bootstrapped.kind !== 'staged') {
1413
- await withhold(bootstrapped, room, local, progress);
1414
- return;
2529
+ async function reload(room, local, progress, live) {
2530
+ try {
2531
+ if (!live() || authorization === undefined)
2532
+ return;
2533
+ const bootstrapped = await pullBootstrap(authorization, room, local, live);
2534
+ if (!live())
2535
+ return;
2536
+ if (bootstrapped.kind !== 'staged') {
2537
+ await withhold(bootstrapped, room, local, progress);
2538
+ return;
2539
+ }
2540
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
2541
+ await caughtUp(room, local, progress, live);
2542
+ }
2543
+ finally {
2544
+ caughtUpTo(room.locator);
1415
2545
  }
1416
- await publish(local, room, bootstrapped.confirmed);
1417
- await caughtUp(room, local, progress);
1418
2546
  }
1419
- async function discoverRooms(local, progress, attempt) {
2547
+ async function discoverRooms(local, progress, attempt, live) {
1420
2548
  if (closed || authorization === undefined) {
1421
2549
  notifyReady();
1422
2550
  return;
1423
2551
  }
1424
2552
  moveTo(transferring(status));
2553
+ discoveries += 1;
2554
+ const started = discoveries;
1425
2555
  let result;
1426
2556
  try {
1427
2557
  result = await options.adapter.discover(authorization);
@@ -1429,7 +2559,8 @@ export function composeRuntime(options) {
1429
2559
  catch {
1430
2560
  result = { kind: 'transport-uncertainty' };
1431
2561
  }
1432
- if (closed) {
2562
+ if (!live()) {
2563
+ progress.complete = false;
1433
2564
  notifyReady();
1434
2565
  return;
1435
2566
  }
@@ -1448,7 +2579,11 @@ export function composeRuntime(options) {
1448
2579
  }
1449
2580
  if (result.kind === 'contract-unavailable') {
1450
2581
  journal.record(`connection contract unavailable ${result.code}`);
1451
- await refuse('missing-required-guarantee', { code: result.code, fields: result.fields });
2582
+ await refuse('missing-required-guarantee', {
2583
+ code: result.code,
2584
+ fields: result.fields,
2585
+ retryable: result.retryable,
2586
+ });
1452
2587
  notifyReady();
1453
2588
  return;
1454
2589
  }
@@ -1483,14 +2618,20 @@ export function composeRuntime(options) {
1483
2618
  }
1484
2619
  const previousCapabilities = capabilities;
1485
2620
  capabilities = result.capabilities;
2621
+ outcomeRetention = result.retention.outcome;
1486
2622
  notifyReady();
1487
2623
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1488
2624
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
1489
2625
  limits = result.providerLimits;
1490
2626
  await local.declare(limits, capabilities);
1491
2627
  }
1492
- await applyDiscovery(result.rooms, local);
1493
- await synchronize(result.rooms, local, progress, attempt);
2628
+ const signedIn = result.member ?? null;
2629
+ if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
2630
+ await local.keepMember(signedIn);
2631
+ member = signedIn;
2632
+ emitMember();
2633
+ }
2634
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
1494
2635
  }
1495
2636
  async function ensureAuthorization() {
1496
2637
  if (authorization !== undefined)
@@ -1504,32 +2645,64 @@ export function composeRuntime(options) {
1504
2645
  throw error;
1505
2646
  }
1506
2647
  }
1507
- function task(work) {
2648
+ async function admittedRead(work) {
2649
+ if (roomReads.generation !== generation) {
2650
+ roomReads = { generation, free: ROOM_READS, waiting: [] };
2651
+ }
2652
+ const reads = roomReads;
2653
+ while (reads.free === 0)
2654
+ await new Promise((admit) => reads.waiting.push(admit));
2655
+ reads.free -= 1;
2656
+ try {
2657
+ await work();
2658
+ }
2659
+ finally {
2660
+ reads.free += 1;
2661
+ reads.waiting.shift()?.();
2662
+ }
2663
+ }
2664
+ function roomTask(locator, work) {
2665
+ return task(roomLane(locator), (live) => admittedRead(() => work(live)));
2666
+ }
2667
+ function task(lane, work) {
1508
2668
  if (stopping)
1509
- return chain;
1510
- chain = chain.then(work, work).catch(() => undefined);
1511
- return chain;
2669
+ return Promise.resolve();
2670
+ const held = lanes.get(lane);
2671
+ const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
2672
+ const turn = current.generation;
2673
+ const live = () => turn === generation && continuing();
2674
+ const run = current.chain.then(() => work(live)).catch(() => undefined);
2675
+ current.chain = run;
2676
+ lanes.set(lane, current);
2677
+ working.add(run);
2678
+ void run.finally(() => working.delete(run));
2679
+ return run;
1512
2680
  }
1513
2681
  function sync(attempt) {
1514
- return task(async () => {
1515
- if (closed || stopping || discoveryPaused) {
2682
+ return task(OPENING_LANE, async (live) => {
2683
+ if (!live()) {
1516
2684
  notifyReady();
1517
2685
  return;
1518
2686
  }
1519
2687
  const progress = { complete: true, reachable: true };
1520
- underway = attempt === 'opened' ? progress : null;
2688
+ const gate = discover();
2689
+ if (attempt === 'opened')
2690
+ opened = generation;
2691
+ underway = attempt === 'opened' ? { generation, progress } : null;
2692
+ if (attempt === 'opened')
2693
+ streamed = progress;
1521
2694
  complete = false;
1522
2695
  recorded = false;
1523
2696
  moveTo(transferring(status));
1524
2697
  handed = false;
1525
2698
  try {
1526
2699
  const local = await openLocal();
1527
- if (closed)
2700
+ if (!live())
1528
2701
  return;
1529
2702
  await ensureAuthorization();
1530
- if (closed)
2703
+ if (!live())
1531
2704
  return;
1532
- await discoverRooms(local, progress, attempt);
2705
+ await discoverRooms(local, progress, attempt, live);
1533
2706
  }
1534
2707
  catch {
1535
2708
  progress.complete = false;
@@ -1537,50 +2710,80 @@ export function composeRuntime(options) {
1537
2710
  progress.reachable = false;
1538
2711
  }
1539
2712
  finally {
2713
+ gate.release();
1540
2714
  notifyReady();
1541
2715
  }
2716
+ if (!live())
2717
+ return;
1542
2718
  if (attempt !== 'opened') {
1543
- await finishOpening(progress);
2719
+ await finishOpening(progress, live);
1544
2720
  return;
1545
2721
  }
1546
2722
  if (handed || closed || progress.reachable)
1547
2723
  return;
1548
2724
  underway = null;
2725
+ opened = null;
1549
2726
  journal.record('connection reopen');
1550
- await rest(progress);
2727
+ await rest(progress, live);
1551
2728
  options.adapter.roomStream?.reopen();
1552
2729
  });
1553
2730
  }
1554
- function closedSocket(revoked) {
2731
+ function closedSocket(revoked, lost) {
1555
2732
  const opened = socketOpen;
1556
2733
  socketOpen = false;
2734
+ if (lost) {
2735
+ reads.lost();
2736
+ conversations.lost();
2737
+ }
1557
2738
  if (revoked) {
1558
2739
  void refuse('revoked-authorization');
1559
2740
  return;
1560
2741
  }
1561
- if (opened || status.providerUnreachable)
1562
- return;
1563
- moveTo({ ...status, providerUnreachable: true });
2742
+ complete = false;
2743
+ if (streamed !== null)
2744
+ streamed.complete = false;
2745
+ moveTo({
2746
+ ...status,
2747
+ state: status.state === 'synced' ? 'syncing' : status.state,
2748
+ providerUnreachable: status.providerUnreachable || !opened,
2749
+ });
1564
2750
  }
1565
2751
  function applyOpening(expired) {
1566
- return task(async () => {
1567
- const progress = underway;
2752
+ const progress = underway?.generation === generation ? underway.progress : null;
2753
+ const cycle = progress ?? { complete: true, reachable: true };
2754
+ const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
2755
+ if (opened === generation)
2756
+ opened = null;
2757
+ return task(OPENING_LANE, async (live) => {
1568
2758
  underway = null;
1569
- if (closed || stopping)
2759
+ if (!live())
1570
2760
  return;
1571
2761
  if (progress === null) {
1572
- await reloadExpired(expired);
2762
+ await replacing;
2763
+ await rest(cycle, live);
1573
2764
  return;
1574
2765
  }
2766
+ await roomsCarried(expired);
2767
+ if (!live())
2768
+ return;
2769
+ const replaced = new Set(owed);
2770
+ owed.clear();
1575
2771
  try {
1576
2772
  const local = await openLocal();
1577
2773
  const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1578
- const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1579
- const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
2774
+ const reloading = held.filter((room) => !expired.includes(room.locator) &&
2775
+ (replaced.has(room.locator) || revoked(room.locator)));
2776
+ for (const room of reloading)
2777
+ catching.add(room.locator);
2778
+ readSide.recover();
2779
+ const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
2780
+ const reloaded = [...(await reloads), ...(await replacing)];
1580
2781
  if (progress.reachable) {
1581
- for (const room of held)
1582
- if (!reloading.includes(room))
2782
+ for (const room of held) {
2783
+ if (!expired.includes(room.locator) && !reloading.includes(room)) {
1583
2784
  await confirmModels(room, local);
2785
+ }
2786
+ }
1584
2787
  }
1585
2788
  const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1586
2789
  if (failed !== undefined)
@@ -1590,21 +2793,21 @@ export function composeRuntime(options) {
1590
2793
  progress.complete = false;
1591
2794
  progress.reachable = false;
1592
2795
  }
1593
- await finishOpening(progress);
2796
+ if (live())
2797
+ await finishOpening(progress, live);
1594
2798
  });
1595
2799
  }
1596
- async function reloadExpired(expired) {
1597
- const progress = { complete: true, reachable: true };
2800
+ async function reloadNow(replaced, progress) {
1598
2801
  try {
1599
2802
  const local = await openLocal();
1600
- await Promise.allSettled([...discovered.values()]
1601
- .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1602
- .map((room) => reload(room, local, progress)));
2803
+ return await Promise.allSettled([...discovered.values()]
2804
+ .filter((room) => replicas.has(room.locator) && replaced(room))
2805
+ .map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
1603
2806
  }
1604
2807
  catch {
1605
2808
  progress.reachable = false;
2809
+ return [];
1606
2810
  }
1607
- await rest(progress);
1608
2811
  }
1609
2812
  async function confirmModels(room, local) {
1610
2813
  for (const attached of ['collection', 'typed-selection']) {
@@ -1615,38 +2818,96 @@ export function composeRuntime(options) {
1615
2818
  }
1616
2819
  }
1617
2820
  function submitLater(locator) {
1618
- const running = submitting.get(locator);
1619
- if (running !== undefined) {
1620
- running.again = true;
2821
+ if (localSaves)
1621
2822
  return;
2823
+ void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
2824
+ }
2825
+ async function submitPass(locator, signal) {
2826
+ const room = discovered.get(locator);
2827
+ if (closed || stopping || discoveryPaused || room === undefined)
2828
+ return 'done';
2829
+ const progress = { complete: true, reachable: true };
2830
+ let next;
2831
+ try {
2832
+ await ensureAuthorization();
2833
+ if (authorization === undefined)
2834
+ return 'done';
2835
+ next = await submitRoom(room, await openLocal(), authorization, progress, signal);
1622
2836
  }
1623
- const run = { again: false, done: Promise.resolve() };
1624
- submitting.set(locator, run);
1625
- run.done = (async () => {
1626
- try {
1627
- do {
1628
- run.again = false;
1629
- const room = discovered.get(locator);
1630
- if (closed || stopping || discoveryPaused || room === undefined)
1631
- return;
1632
- const progress = { complete: true, reachable: true };
1633
- try {
1634
- await ensureAuthorization();
1635
- if (authorization === undefined)
1636
- return;
1637
- await submitRoom(room, await openLocal(), authorization, progress);
1638
- }
1639
- catch {
1640
- progress.reachable = false;
1641
- }
1642
- armTimer();
1643
- await rest(progress);
1644
- } while (run.again);
1645
- }
1646
- finally {
1647
- submitting.delete(locator);
1648
- }
1649
- })();
2837
+ catch {
2838
+ progress.reachable = false;
2839
+ next = 'again';
2840
+ }
2841
+ if (signal.aborted)
2842
+ return 'done';
2843
+ await rest(progress);
2844
+ return next === 'again' && !socketOpen ? 'done' : next;
2845
+ }
2846
+ function repairLater(locator, when) {
2847
+ if (!socketOpen || repairing.has(locator))
2848
+ return;
2849
+ void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
2850
+ }
2851
+ async function repairPass(locator, signal) {
2852
+ const room = discovered.get(locator);
2853
+ if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
2854
+ return 'done';
2855
+ if (room === undefined || room.replica.admission !== 'admitted')
2856
+ return 'done';
2857
+ const progress = { complete: true, reachable: true };
2858
+ repairing.add(locator);
2859
+ try {
2860
+ const local = await openLocal();
2861
+ await roomTask(locator, async (live) => {
2862
+ const current = () => live() && !signal.aborted;
2863
+ if (!current() || authorization === undefined)
2864
+ return;
2865
+ const withheld = await Promise.race([
2866
+ advance(room, local, authorization, current),
2867
+ abandonedOn(signal),
2868
+ ]);
2869
+ if (withheld !== null && current())
2870
+ await withhold(withheld, room, local, progress);
2871
+ });
2872
+ }
2873
+ catch {
2874
+ stale.add(locator);
2875
+ progress.reachable = false;
2876
+ }
2877
+ finally {
2878
+ repairing.delete(locator);
2879
+ }
2880
+ if (signal.aborted)
2881
+ return 'done';
2882
+ await rest(progress);
2883
+ if (!stale.has(locator)) {
2884
+ if ((replicas.get(locator)?.queue.length ?? 0) > 0)
2885
+ submitLater(locator);
2886
+ return 'done';
2887
+ }
2888
+ if (!repairable.has(locator))
2889
+ return 'done';
2890
+ const reset = repairable.get(locator);
2891
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
2892
+ }
2893
+ function resendLater() {
2894
+ if (!socketOpen)
2895
+ return;
2896
+ void recovery.attempt('resend', resendPass, { again: true });
2897
+ }
2898
+ async function resendPass(signal) {
2899
+ if (!socketOpen || closed || stopping || discoveryPaused)
2900
+ return 'done';
2901
+ const reading = { complete: true, reachable: true };
2902
+ try {
2903
+ await conversations.resend(reading);
2904
+ }
2905
+ catch {
2906
+ reading.reachable = false;
2907
+ }
2908
+ if (signal.aborted)
2909
+ return 'done';
2910
+ return reading.reachable && reading.complete ? 'done' : 'again';
1650
2911
  }
1651
2912
  function submitAll() {
1652
2913
  for (const [locator, replica] of replicas) {
@@ -1654,31 +2915,44 @@ export function composeRuntime(options) {
1654
2915
  submitLater(locator);
1655
2916
  }
1656
2917
  }
1657
- async function finishOpening(progress) {
2918
+ async function finishOpening(progress, live) {
1658
2919
  try {
1659
- if (closed || discoveryPaused) {
2920
+ if (!live()) {
1660
2921
  progress.complete = false;
1661
2922
  return;
1662
2923
  }
2924
+ reads.applied();
1663
2925
  submitAll();
2926
+ for (const locator of stale)
2927
+ if (repairable.has(locator))
2928
+ repairLater(locator, {});
1664
2929
  await conversations.reconcile();
1665
2930
  if (status.providerUnreachable)
1666
2931
  conversations.reconnected();
1667
2932
  const reading = { complete: true, reachable: true };
1668
- await conversations.catchUp(reading);
1669
- if (!reading.reachable)
2933
+ if (demand.conversations)
2934
+ await conversations.catchUp(reading);
2935
+ if (progress.reachable && reading.reachable)
2936
+ await conversations.resend(reading);
2937
+ if (progress.reachable)
2938
+ void recovery.attempt('calls', resendCalls);
2939
+ if (!reading.reachable) {
1670
2940
  progress.reachable = false;
2941
+ resendLater();
2942
+ }
1671
2943
  }
1672
2944
  catch {
1673
2945
  progress.complete = false;
1674
2946
  }
1675
2947
  finally {
1676
- complete = progress.complete;
1677
- await rest(progress);
2948
+ if (live()) {
2949
+ complete = progress.complete;
2950
+ await rest(progress, live);
2951
+ }
1678
2952
  }
1679
2953
  }
1680
- async function rest(progress) {
1681
- if (closed)
2954
+ async function rest(progress, live = () => !closed) {
2955
+ if (!live())
1682
2956
  return;
1683
2957
  let lastCompleted = status.lastCompleted;
1684
2958
  let synced = complete && stale.size === 0 && !pending();
@@ -1686,6 +2960,8 @@ export function composeRuntime(options) {
1686
2960
  const completedAt = new Date().toISOString();
1687
2961
  try {
1688
2962
  await (await openLocal()).complete(completedAt);
2963
+ if (!live())
2964
+ return;
1689
2965
  recorded = true;
1690
2966
  lastCompleted = completedAt;
1691
2967
  }
@@ -1694,46 +2970,74 @@ export function composeRuntime(options) {
1694
2970
  }
1695
2971
  }
1696
2972
  moveTo({
1697
- state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
2973
+ state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
1698
2974
  lastCompleted,
1699
2975
  providerUnreachable: !progress.reachable,
1700
2976
  ...attention(),
1701
2977
  });
1702
2978
  }
2979
+ function discover() {
2980
+ let release = () => { };
2981
+ const done = new Promise((resolve) => {
2982
+ release = resolve;
2983
+ });
2984
+ discovering = { generation, done, release };
2985
+ return discovering;
2986
+ }
1703
2987
  function applyRoom(locator, roomVersion) {
1704
2988
  announced.set(locator, roomVersion);
1705
- return task(async () => {
1706
- await answering.get(locator);
1707
- if (closed || stopping || discoveryPaused || authorization === undefined)
2989
+ return task(roomLane(locator), async (live) => {
2990
+ if (discovering?.generation === generation)
2991
+ await discovering.done;
2992
+ const saving = answering.get(locator);
2993
+ if (saving?.generation === generation)
2994
+ await saving.answered;
2995
+ if (!live() || authorization === undefined)
1708
2996
  return;
1709
2997
  const room = discovered.get(locator);
1710
2998
  const held = replicas.get(locator);
1711
- if (room === undefined || room.replica.admission !== 'admitted')
2999
+ if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
1712
3000
  return;
1713
3001
  if (held !== undefined &&
1714
3002
  (held.confirmed.record.replica.selection !== room.replica.selection ||
1715
3003
  options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
1716
3004
  return;
1717
3005
  }
1718
- const during = underway;
3006
+ const during = underway?.generation === generation ? underway.progress : null;
1719
3007
  const progress = during ?? { complete: true, reachable: true };
1720
3008
  if (during === null)
1721
3009
  moveTo(transferring(status));
1722
3010
  try {
1723
3011
  const local = await openLocal();
1724
- const withheld = await advance(room, local, authorization);
1725
- if (withheld !== null)
1726
- await withhold(withheld, room, local, progress);
3012
+ await admittedRead(async () => {
3013
+ if (!live() || authorization === undefined)
3014
+ return;
3015
+ const withheld = await advance(room, local, authorization, live);
3016
+ if (withheld !== null)
3017
+ await withhold(withheld, room, local, progress);
3018
+ });
1727
3019
  }
1728
3020
  catch {
1729
3021
  stale.add(locator);
1730
3022
  }
1731
3023
  finally {
1732
3024
  if (during === null)
1733
- await rest(progress);
3025
+ await rest(progress, live);
1734
3026
  }
1735
3027
  });
1736
3028
  }
3029
+ function roomsCarried(replacing) {
3030
+ const replaced = new Set(replacing.map(roomLane));
3031
+ return Promise.all([...lanes]
3032
+ .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
3033
+ .map(([, held]) => held.chain));
3034
+ }
3035
+ const receipts = {
3036
+ heads: async () => (await openLocal()).receivedHeads(),
3037
+ parts: async (key, count) => (await openLocal()).receivedParts(key, count),
3038
+ keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
3039
+ drop: async (keys) => (await openLocal()).dropReceived(keys),
3040
+ };
1737
3041
  function observeStream() {
1738
3042
  const stream = options.adapter.roomStream;
1739
3043
  if (stream === undefined || withdrawStream !== undefined)
@@ -1743,15 +3047,19 @@ export function composeRuntime(options) {
1743
3047
  return;
1744
3048
  if (event.kind === 'opened') {
1745
3049
  socketOpen = true;
3050
+ generation += 1;
3051
+ reads.opened();
3052
+ conversations.reopened();
3053
+ recovery.resume();
1746
3054
  void sync('opened');
1747
3055
  }
1748
3056
  else if (event.kind === 'closed')
1749
- closedSocket(event.revoked);
3057
+ closedSocket(event.revoked, event.lost);
1750
3058
  else if (event.kind === 'opening')
1751
3059
  void applyOpening(event.expired);
1752
3060
  else
1753
3061
  void applyRoom(event.locator, event.roomVersion);
1754
- });
3062
+ }, receipts);
1755
3063
  }
1756
3064
  function writesHeld() {
1757
3065
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -1766,6 +3074,7 @@ export function composeRuntime(options) {
1766
3074
  haltedRefusal = false;
1767
3075
  if (mode !== serviceMode) {
1768
3076
  serviceMode = mode;
3077
+ queueReadiness();
1769
3078
  journal.record(`service mode ${mode}`);
1770
3079
  for (const listener of serviceModeListeners)
1771
3080
  listener(mode);
@@ -1778,6 +3087,8 @@ export function composeRuntime(options) {
1778
3087
  if (stream === undefined || withdrawConversations !== undefined)
1779
3088
  return;
1780
3089
  withdrawConversations = stream.observe((event) => {
3090
+ if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
3091
+ queueReadiness();
1781
3092
  if (conversations.receive(event)) {
1782
3093
  void ready.then(scheduleCurrency);
1783
3094
  return;
@@ -1789,46 +3100,47 @@ export function composeRuntime(options) {
1789
3100
  });
1790
3101
  }
1791
3102
  function scheduleCurrency() {
1792
- if (currencyQueued || closed || stopping)
3103
+ if (currencyQueued === generation || closed || stopping)
1793
3104
  return;
1794
- currencyQueued = true;
1795
- void task(runCurrency);
3105
+ currencyQueued = generation;
3106
+ void task(CONVERSATION_LANE, runCurrency);
1796
3107
  }
1797
- async function runCurrency() {
1798
- currencyQueued = false;
1799
- if (closed || stopping || discoveryPaused)
3108
+ async function runCurrency(live) {
3109
+ if (currencyQueued === generation)
3110
+ currencyQueued = null;
3111
+ if (!live())
1800
3112
  return;
1801
3113
  await conversations.reconcile();
1802
- if (conversations.current()) {
3114
+ if (conversations.current() || !demand.conversations) {
1803
3115
  show();
1804
3116
  return;
1805
3117
  }
1806
- const resting = status;
1807
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1808
- moveTo(transferring(resting));
3118
+ const progress = { complete: false, reachable: !status.providerUnreachable };
3119
+ moveTo(transferring(status));
1809
3120
  try {
1810
3121
  await conversations.catchUp(progress);
1811
3122
  }
1812
3123
  catch {
1813
3124
  progress.reachable = false;
1814
3125
  }
1815
- finally {
1816
- if (!closed) {
1817
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1818
- }
1819
- }
3126
+ await rest(progress, live);
1820
3127
  }
1821
3128
  function close() {
1822
3129
  if (closed)
1823
3130
  return;
1824
3131
  closed = true;
3132
+ if (narrowing !== null)
3133
+ clearTimeout(narrowing);
3134
+ if (rechecking !== null)
3135
+ clearInterval(rechecking);
3136
+ readinessWatches.clear();
1825
3137
  withdrawStream?.();
1826
3138
  withdrawStream = undefined;
1827
3139
  withdrawConversations?.();
1828
3140
  withdrawConversations = undefined;
1829
3141
  withdrawServiceMode?.();
1830
3142
  conversations.close();
1831
- stopTimer();
3143
+ void recovery.retire();
1832
3144
  store?.close();
1833
3145
  void opening?.then((opened) => {
1834
3146
  opened.close();
@@ -1838,6 +3150,7 @@ export function composeRuntime(options) {
1838
3150
  storageLossListeners.clear();
1839
3151
  statusListeners.clear();
1840
3152
  serviceModeListeners.clear();
3153
+ memberListeners.clear();
1841
3154
  readSide.close();
1842
3155
  queueSubscriptions.clear();
1843
3156
  roomDraftListeners.clear();
@@ -1845,9 +3158,8 @@ export function composeRuntime(options) {
1845
3158
  }
1846
3159
  async function stop() {
1847
3160
  stopping = true;
1848
- stopTimer();
1849
- await chain.catch(() => undefined);
1850
- await Promise.all([...submitting.values()].map((run) => run.done));
3161
+ await recovery.retire();
3162
+ await Promise.all(working);
1851
3163
  await mutations;
1852
3164
  close();
1853
3165
  }
@@ -1869,14 +3181,101 @@ export function composeRuntime(options) {
1869
3181
  return planOf(input, roomFacts(reach), planReach(reach));
1870
3182
  }
1871
3183
  function watchPlan(plan, deliver) {
1872
- return readSide.watch(plan, identityOf(plan), deliver);
3184
+ let seeded = false;
3185
+ let pending = true;
3186
+ const shown = () => {
3187
+ const seed = seeds.get(plan.key);
3188
+ if (seed === undefined || seeded || !pending)
3189
+ return;
3190
+ seeded = true;
3191
+ deliver(seededResult(seed));
3192
+ };
3193
+ const watchers = seedWatchers.get(plan.key) ?? new Set();
3194
+ seedWatchers.set(plan.key, watchers);
3195
+ watchers.add(shown);
3196
+ const stop = readSide.watch(plan, identityOf(plan), () => {
3197
+ const cover = covered(plan);
3198
+ awaitPreparation(cover);
3199
+ return seedBehind(plan, cover);
3200
+ }, (message) => {
3201
+ const answered = message;
3202
+ pending = whole(answered) && 'pending' in answered.result;
3203
+ if (pending && seeds.has(plan.key)) {
3204
+ shown();
3205
+ return;
3206
+ }
3207
+ seeded = false;
3208
+ deliver(message);
3209
+ });
3210
+ return () => {
3211
+ watchers.delete(shown);
3212
+ if (watchers.size === 0)
3213
+ seedWatchers.delete(plan.key);
3214
+ stop();
3215
+ };
1873
3216
  }
1874
3217
  async function searched(input, reach) {
1875
3218
  const plan = searchPlanOf(input);
1876
- const selected = plan.documents === undefined && reach === null
1877
- ? null
1878
- : (await readSide.query(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
1879
- return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
3219
+ const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
3220
+ for (;;) {
3221
+ const cover = covered(filter);
3222
+ awaitPreparation(cover);
3223
+ const pending = 'pending' in cover ? [...cover.pending] : [];
3224
+ const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3225
+ const held = covered({
3226
+ ...filter,
3227
+ rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
3228
+ });
3229
+ const covering = 'pending' in held ? [] : held.coverage.rooms;
3230
+ const unindexed = covering
3231
+ .filter(({ room }) => !searchIndex.rooms().has(room))
3232
+ .map(({ room }) => room);
3233
+ for (const room of unindexed)
3234
+ pending.push({ room, awaits: 'index' });
3235
+ const listed = conversations.searchable();
3236
+ const named = reach !== null && !reach.conversations
3237
+ ? []
3238
+ : (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
3239
+ const waiting = named.filter((one) => !searchIndex.conversations().has(one));
3240
+ for (const conversation of waiting)
3241
+ pending.push({ conversation, awaits: 'index' });
3242
+ if (unindexed.length > 0 || waiting.length > 0)
3243
+ index(unindexed, waiting);
3244
+ const complete = pending.length === 0 && !('pending' in cover);
3245
+ const answering = complete
3246
+ ? filter
3247
+ : {
3248
+ ...filter,
3249
+ rooms: covering
3250
+ .filter(({ room }) => searchIndex.rooms().has(room))
3251
+ .map(({ room }) => room),
3252
+ };
3253
+ const answered = covered(answering);
3254
+ if ('pending' in answered)
3255
+ continue;
3256
+ const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
3257
+ const seen = searchIndex.changes();
3258
+ await searchIndex.caughtUp();
3259
+ const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
3260
+ if (selected !== null && 'pending' in selected)
3261
+ continue;
3262
+ if (searchIndex.changes() !== seen ||
3263
+ JSON.stringify(covered(answering)) !== JSON.stringify(answered))
3264
+ continue;
3265
+ const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms === null ? null : new Set(answering.rooms), plan.collection
3266
+ ? new Map(answered.coverage.rooms.map(({ room }) => [
3267
+ room,
3268
+ replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
3269
+ ]))
3270
+ : null);
3271
+ const coverage = {
3272
+ rooms: answered.coverage.rooms,
3273
+ conversations: searchedConversations,
3274
+ index: searchIndex.generation(),
3275
+ overlay: answered.coverage.overlay,
3276
+ };
3277
+ return complete ? { ...hits, coverage } : { pending, ...hits, coverage };
3278
+ }
1880
3279
  }
1881
3280
  const watch = (topic, input, deliver) => {
1882
3281
  if (topic === 'room-drafts') {
@@ -1901,6 +3300,8 @@ export function composeRuntime(options) {
1901
3300
  }
1902
3301
  if (topic === 'conversations')
1903
3302
  return conversations.watchList(deliver);
3303
+ if (topic === 'agents')
3304
+ return conversations.watchAgents(deliver);
1904
3305
  if (topic === 'presentation') {
1905
3306
  const listener = {
1906
3307
  app: input[0],
@@ -1920,6 +3321,9 @@ export function composeRuntime(options) {
1920
3321
  presentationListeners.delete(listener);
1921
3322
  };
1922
3323
  }
3324
+ if (topic === 'readiness') {
3325
+ return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
3326
+ }
1923
3327
  if (topic === 'rooms') {
1924
3328
  roomListeners.add(deliver);
1925
3329
  deliver(listed());
@@ -1955,6 +3359,13 @@ export function composeRuntime(options) {
1955
3359
  serviceModeListeners.delete(deliver);
1956
3360
  };
1957
3361
  }
3362
+ if (topic === 'member') {
3363
+ memberListeners.add(deliver);
3364
+ deliver(member);
3365
+ return () => {
3366
+ memberListeners.delete(deliver);
3367
+ };
3368
+ }
1958
3369
  if (topic === 'queue') {
1959
3370
  const queue = { locator: input[0], notify: deliver };
1960
3371
  queueSubscriptions.add(queue);
@@ -1972,7 +3383,7 @@ export function composeRuntime(options) {
1972
3383
  rooms() {
1973
3384
  return listed();
1974
3385
  },
1975
- query: ((input) => readSide.query(planned(input, null))),
3386
+ query: ((input) => rowsQuery(planned(input, null))),
1976
3387
  search: (input) => searched(input, null),
1977
3388
  types(locator) {
1978
3389
  return {
@@ -2001,20 +3412,20 @@ export function composeRuntime(options) {
2001
3412
  return exclusive(async () => {
2002
3413
  const parts = readParts(input);
2003
3414
  if (path === '' || normalize(path) !== path)
2004
- throw new Error('room-draft-malformed');
3415
+ throw runtimeError('room-draft-malformed');
2005
3416
  const local = await openLocal();
2006
3417
  const current = roomDrafts.get(room)?.get(path);
2007
3418
  if ((current?.revision ?? null) !== revision) {
2008
- return {
2009
- outcome: 'stale',
2010
- current: current === undefined ? null : roomDraftOf(current),
2011
- };
3419
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2012
3420
  }
3421
+ const file = parts.file === undefined
3422
+ ? undefined
3423
+ : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
2013
3424
  const record = {
2014
3425
  locator: room,
2015
3426
  path,
2016
3427
  revision: roomDraftRevision(),
2017
- parts: held(current?.parts, parts),
3428
+ parts: held(current?.parts, parts, file),
2018
3429
  returned: current?.returned ?? null,
2019
3430
  };
2020
3431
  await local.keepRoomDraft(record);
@@ -2028,10 +3439,7 @@ export function composeRuntime(options) {
2028
3439
  const local = await openLocal();
2029
3440
  const current = roomDrafts.get(room)?.get(path);
2030
3441
  if (current === undefined || current.revision !== revision) {
2031
- return {
2032
- outcome: 'stale',
2033
- current: current === undefined ? null : roomDraftOf(current),
2034
- };
3442
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2035
3443
  }
2036
3444
  await local.discardRoomDraft(room, path);
2037
3445
  forgetRoomDraft(room, path);
@@ -2066,8 +3474,12 @@ export function composeRuntime(options) {
2066
3474
  history(locator, path, query) {
2067
3475
  return readHistory(locator, path, query);
2068
3476
  },
3477
+ binaryFile: {
3478
+ read: readBinaryPath,
3479
+ readReference: readBinaryReference,
3480
+ },
2069
3481
  call(name, input) {
2070
- return invokeAction(name, input);
3482
+ return recoveredAction(name, input);
2071
3483
  },
2072
3484
  reached(locator, roomVersion, path) {
2073
3485
  return path === undefined
@@ -2078,30 +3490,55 @@ export function composeRuntime(options) {
2078
3490
  return journal.export();
2079
3491
  },
2080
3492
  persistence() {
2081
- return options.shell.persistentStorage;
3493
+ return persistence === 'granted';
3494
+ },
3495
+ async retry() {
3496
+ journal.record('retry');
3497
+ unindexable.clear();
3498
+ queueReadiness();
3499
+ resume();
2082
3500
  },
2083
3501
  observe: observing(watch),
2084
3502
  async reauthorize() {
2085
3503
  if (refusal === null || !refusal.reauthorizationOffered)
2086
3504
  return;
2087
3505
  journal.record('connection reauthorize');
2088
- await chain.catch(() => undefined);
3506
+ await Promise.all([...lanes.values()]
3507
+ .filter((held) => held.generation === generation)
3508
+ .map((held) => held.chain));
2089
3509
  await mutations;
2090
3510
  authorization = undefined;
2091
3511
  discoveryPaused = false;
2092
3512
  if (options.adapter.roomStream === undefined)
2093
3513
  await sync('unstreamed');
2094
3514
  else
2095
- await task(ensureAuthorization);
3515
+ await task(OPENING_LANE, ensureAuthorization);
2096
3516
  },
2097
3517
  };
2098
3518
  const reachable = {
2099
3519
  md,
2100
3520
  watch,
3521
+ referrerScope: (referrer) => options.adapter.referrerScope(referrer),
2101
3522
  plan: planned,
2102
- query: (plan) => readSide.query(plan),
3523
+ query: (plan) => rowsQuery(plan),
2103
3524
  watchPlan,
2104
3525
  search: searched,
3526
+ watchReadiness,
3527
+ };
3528
+ return {
3529
+ md,
3530
+ seed,
3531
+ stop,
3532
+ lose: loseRooms,
3533
+ watch,
3534
+ acquire,
3535
+ persisted(answer) {
3536
+ if (answer === persistence)
3537
+ return;
3538
+ persistence = answer;
3539
+ journal.record(`persistence ${answer}`);
3540
+ queueReadiness();
3541
+ },
3542
+ within: (reach, port) => narrowed(reachable, reach, port),
2105
3543
  };
2106
- return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2107
3544
  }