@agentshouse/mdruntime 0.1.0-cfbf80d8d4644 → 0.1.0-cfcbb238a5fa0

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