@agentshouse/mdruntime 0.1.0-c967d1ca66fee → 0.1.0-c969b8dee6cc8

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 (198) hide show
  1. package/AGENTS.md +280 -244
  2. package/README.md +18 -8
  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 +85 -20
  7. package/dist/adapter.d.ts.map +1 -1
  8. package/dist/attempt.d.ts +16 -0
  9. package/dist/attempt.d.ts.map +1 -0
  10. package/dist/attempt.js +70 -0
  11. package/dist/binary.d.ts +23 -0
  12. package/dist/binary.d.ts.map +1 -0
  13. package/dist/binary.js +8 -0
  14. package/dist/bootstrap.d.ts +3 -2
  15. package/dist/bootstrap.d.ts.map +1 -1
  16. package/dist/bootstrap.js +37 -14
  17. package/dist/change-set.d.ts +2 -0
  18. package/dist/change-set.d.ts.map +1 -1
  19. package/dist/change-set.js +40 -13
  20. package/dist/change.d.ts +11 -0
  21. package/dist/change.d.ts.map +1 -1
  22. package/dist/change.js +26 -1
  23. package/dist/chat/acts.d.ts +33 -0
  24. package/dist/chat/acts.d.ts.map +1 -0
  25. package/dist/chat/acts.js +36 -0
  26. package/dist/chat/context.d.ts +3 -0
  27. package/dist/chat/context.d.ts.map +1 -0
  28. package/dist/chat/context.js +10 -0
  29. package/dist/chat/earlier.d.ts +10 -0
  30. package/dist/chat/earlier.d.ts.map +1 -0
  31. package/dist/chat/earlier.js +54 -0
  32. package/dist/chat/input.d.ts +9 -0
  33. package/dist/chat/input.d.ts.map +1 -0
  34. package/dist/chat/input.js +99 -0
  35. package/dist/chat/kit.d.ts +61 -0
  36. package/dist/chat/kit.d.ts.map +1 -0
  37. package/dist/chat/kit.js +123 -0
  38. package/dist/chat/lasting.d.ts +6 -0
  39. package/dist/chat/lasting.d.ts.map +1 -0
  40. package/dist/chat/lasting.js +19 -0
  41. package/dist/chat/list.d.ts +27 -0
  42. package/dist/chat/list.d.ts.map +1 -0
  43. package/dist/chat/list.js +99 -0
  44. package/dist/chat/markdown.d.ts +20 -0
  45. package/dist/chat/markdown.d.ts.map +1 -0
  46. package/dist/chat/markdown.js +188 -0
  47. package/dist/chat/message.d.ts +6 -0
  48. package/dist/chat/message.d.ts.map +1 -0
  49. package/dist/chat/message.js +192 -0
  50. package/dist/chat/messages.d.ts +22 -0
  51. package/dist/chat/messages.d.ts.map +1 -0
  52. package/dist/chat/messages.js +155 -0
  53. package/dist/chat/name.d.ts +6 -0
  54. package/dist/chat/name.d.ts.map +1 -0
  55. package/dist/chat/name.js +37 -0
  56. package/dist/chat/opened.d.ts +11 -0
  57. package/dist/chat/opened.d.ts.map +1 -0
  58. package/dist/chat/opened.js +17 -0
  59. package/dist/chat/overlay.d.ts +31 -0
  60. package/dist/chat/overlay.d.ts.map +1 -0
  61. package/dist/chat/overlay.js +97 -0
  62. package/dist/chat/parts.d.ts +57 -0
  63. package/dist/chat/parts.d.ts.map +1 -0
  64. package/dist/chat/parts.js +119 -0
  65. package/dist/chat/process.d.ts +20 -0
  66. package/dist/chat/process.d.ts.map +1 -0
  67. package/dist/chat/process.js +32 -0
  68. package/dist/chat/provider.d.ts +63 -0
  69. package/dist/chat/provider.d.ts.map +1 -0
  70. package/dist/chat/provider.js +628 -0
  71. package/dist/chat/question.d.ts +9 -0
  72. package/dist/chat/question.d.ts.map +1 -0
  73. package/dist/chat/question.js +69 -0
  74. package/dist/chat/questions.d.ts +31 -0
  75. package/dist/chat/questions.d.ts.map +1 -0
  76. package/dist/chat/questions.js +111 -0
  77. package/dist/chat/read.d.ts +5 -0
  78. package/dist/chat/read.d.ts.map +1 -0
  79. package/dist/chat/read.js +13 -0
  80. package/dist/chat/ready.d.ts +34 -0
  81. package/dist/chat/ready.d.ts.map +1 -0
  82. package/dist/chat/ready.js +91 -0
  83. package/dist/chat/style.d.ts +4 -0
  84. package/dist/chat/style.d.ts.map +1 -0
  85. package/dist/chat/style.js +302 -0
  86. package/dist/chat.d.ts +14 -0
  87. package/dist/chat.d.ts.map +1 -0
  88. package/dist/chat.js +8 -0
  89. package/dist/client.d.ts +26 -11
  90. package/dist/client.d.ts.map +1 -1
  91. package/dist/client.js +27 -4
  92. package/dist/collection.d.ts +17 -3
  93. package/dist/collection.d.ts.map +1 -1
  94. package/dist/collection.js +230 -19
  95. package/dist/connection.d.ts +43 -13
  96. package/dist/connection.d.ts.map +1 -1
  97. package/dist/connection.js +1827 -386
  98. package/dist/conversation.d.ts +29 -9
  99. package/dist/conversation.d.ts.map +1 -1
  100. package/dist/conversation.js +492 -164
  101. package/dist/durability.d.ts +7 -1
  102. package/dist/durability.d.ts.map +1 -1
  103. package/dist/entry.d.ts +17 -0
  104. package/dist/entry.d.ts.map +1 -0
  105. package/dist/entry.js +156 -0
  106. package/dist/expectation.d.ts +21 -0
  107. package/dist/expectation.d.ts.map +1 -0
  108. package/dist/expectation.js +70 -0
  109. package/dist/fence.d.ts +6 -0
  110. package/dist/fence.d.ts.map +1 -0
  111. package/dist/fence.js +10 -0
  112. package/dist/host.d.ts +18 -3
  113. package/dist/host.d.ts.map +1 -1
  114. package/dist/host.js +102 -13
  115. package/dist/identity.d.ts +4 -0
  116. package/dist/identity.d.ts.map +1 -1
  117. package/dist/identity.js +12 -0
  118. package/dist/index.d.ts +15 -8
  119. package/dist/index.d.ts.map +1 -1
  120. package/dist/index.js +5 -0
  121. package/dist/memory.d.ts +11 -4
  122. package/dist/memory.d.ts.map +1 -1
  123. package/dist/memory.js +67 -18
  124. package/dist/model.d.ts +4 -2
  125. package/dist/model.d.ts.map +1 -1
  126. package/dist/model.js +10 -1
  127. package/dist/observation.d.ts +7 -2
  128. package/dist/observation.d.ts.map +1 -1
  129. package/dist/observation.js +29 -12
  130. package/dist/outcome.d.ts +17 -1
  131. package/dist/outcome.d.ts.map +1 -1
  132. package/dist/outcome.js +10 -0
  133. package/dist/overlay.d.ts +15 -2
  134. package/dist/overlay.d.ts.map +1 -1
  135. package/dist/overlay.js +99 -62
  136. package/dist/query.d.ts +58 -4
  137. package/dist/query.d.ts.map +1 -1
  138. package/dist/query.js +36 -7
  139. package/dist/reach.d.ts +8 -3
  140. package/dist/reach.d.ts.map +1 -1
  141. package/dist/reach.js +55 -8
  142. package/dist/react.d.ts +10 -7
  143. package/dist/react.d.ts.map +1 -1
  144. package/dist/react.js +7 -17
  145. package/dist/read-side.d.ts +13 -6
  146. package/dist/read-side.d.ts.map +1 -1
  147. package/dist/read-side.js +428 -94
  148. package/dist/readiness.d.ts +53 -0
  149. package/dist/readiness.d.ts.map +1 -0
  150. package/dist/readiness.js +33 -0
  151. package/dist/reads.d.ts +11 -0
  152. package/dist/reads.d.ts.map +1 -0
  153. package/dist/reads.js +98 -0
  154. package/dist/recovery.d.ts +25 -0
  155. package/dist/recovery.d.ts.map +1 -0
  156. package/dist/recovery.js +197 -0
  157. package/dist/refresh.d.ts +4 -4
  158. package/dist/refresh.d.ts.map +1 -1
  159. package/dist/refresh.js +27 -25
  160. package/dist/replica.d.ts +11 -4
  161. package/dist/replica.d.ts.map +1 -1
  162. package/dist/replica.js +106 -48
  163. package/dist/room-draft.d.ts +32 -3
  164. package/dist/room-draft.d.ts.map +1 -1
  165. package/dist/room-draft.js +121 -3
  166. package/dist/rows.d.ts +8 -13
  167. package/dist/rows.d.ts.map +1 -1
  168. package/dist/rows.js +12 -13
  169. package/dist/save.d.ts +29 -0
  170. package/dist/save.d.ts.map +1 -1
  171. package/dist/save.js +360 -30
  172. package/dist/search.d.ts +49 -6
  173. package/dist/search.d.ts.map +1 -1
  174. package/dist/search.js +192 -47
  175. package/dist/seed.d.ts +16 -0
  176. package/dist/seed.d.ts.map +1 -0
  177. package/dist/seed.js +75 -0
  178. package/dist/shared.d.ts +11 -0
  179. package/dist/shared.d.ts.map +1 -0
  180. package/dist/shared.js +368 -0
  181. package/dist/status.d.ts +4 -0
  182. package/dist/status.d.ts.map +1 -1
  183. package/dist/store.d.ts +69 -21
  184. package/dist/store.d.ts.map +1 -1
  185. package/dist/store.js +463 -206
  186. package/dist/turn.d.ts +7 -0
  187. package/dist/turn.d.ts.map +1 -0
  188. package/dist/turn.js +45 -0
  189. package/dist/windows.d.ts +26 -0
  190. package/dist/windows.d.ts.map +1 -0
  191. package/dist/windows.js +93 -0
  192. package/dist/wire.d.ts +153 -30
  193. package/dist/wire.d.ts.map +1 -1
  194. package/dist/wire.js +53 -29
  195. package/package.json +26 -14
  196. package/dist/attachment.d.ts +0 -3
  197. package/dist/attachment.d.ts.map +0 -1
  198. package/dist/attachment.js +0 -8
@@ -1,28 +1,48 @@
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, plain, queryKey, } from './query.js';
5
5
  import { createReadSide } from './read-side.js';
6
+ import { seededResult } from './seed.js';
6
7
  import { markdownPath, roomOfKey } from './rows.js';
7
8
  import { createSearchIndex, searchPlanOf, } from './search.js';
9
+ import { capped, MAXIMUM_WINDOW } from './windows.js';
8
10
  import { bootstrapRoom } from './bootstrap.js';
9
- import { changeSet, mergeChangeSets } from './change-set.js';
11
+ import { changeSet, entering, mergeChangeSets } from './change-set.js';
10
12
  import { createConversationStore, } from './conversation.js';
11
- import { validRoot } from './collection.js';
13
+ import { queuedFile } from './change.js';
14
+ import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
12
15
  import { runtimeError } from './failure.js';
13
16
  import { createLog } from './log.js';
14
- import { calledResult, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
17
+ import { binaryRecord } from './binary.js';
18
+ import { absentBinary, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
19
+ import { createRecovery } from './recovery.js';
20
+ import { doorReads } from './reads.js';
15
21
  import { refreshRoom } from './refresh.js';
16
- import { buildBatch } from './save.js';
17
- import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
18
- import { changedParts, changesOf, held, readParts, returnedBy, returnedInto, staleWrite, underneath, } from './room-draft.js';
22
+ import { buildBatch, joined, limitedIn, queuedDocuments, requeued, } from './save.js';
23
+ import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, standing, } from './replica.js';
24
+ import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
19
25
  import { same, saving, STARTING, transferring, } from './status.js';
20
26
  import { detectStorageLoss, nextGeneration } from './durability.js';
21
- import { roomDraftRevision } from './identity.js';
27
+ import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
28
+ import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
29
+ import { FencedMap } from './fence.js';
30
+ import { tally } from './shared.js';
31
+ import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
32
+ import { identityTime, roomDraftRevision } from './identity.js';
22
33
  import { beneath, normalize } from './paths.js';
23
34
  import { openRuntimeStore, } from './store.js';
24
35
  import { unconfirmed } from './transfer.js';
25
36
  import { governs, holds, narrowed, planReach, } from './reach.js';
37
+ function roomsAlone(plan) {
38
+ return (plan.reach === null &&
39
+ plan.root.types === null &&
40
+ plan.root.where === null &&
41
+ plan.root.pathPrefix === null &&
42
+ plan.paths === null &&
43
+ plan.hop === null &&
44
+ plan.backlinks === null);
45
+ }
26
46
  function within(root, path) {
27
47
  return root === '' || beneath(root, path);
28
48
  }
@@ -36,24 +56,68 @@ function declaredSelection(room) {
36
56
  root !== '' &&
37
57
  selection.roots.every((other, at) => at === index || !within(other, root)));
38
58
  }
59
+ const OPENING_LANE = 'opening';
60
+ const CONVERSATION_LANE = 'conversations';
61
+ const ROOM_READS = 256;
62
+ const ROOM_ATTEMPTS = 16;
63
+ function operationOf(input) {
64
+ const operation = input?.operation_id;
65
+ return typeof operation === 'string' ? operation : null;
66
+ }
67
+ const outside = new Map();
68
+ function outsideOf(roots) {
69
+ const key = JSON.stringify(roots);
70
+ const held = outside.get(key);
71
+ if (held !== undefined)
72
+ return held;
73
+ const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
74
+ const made = {
75
+ documents: (path) => (markdownPath(path) ? counted(path) : 0),
76
+ binaries: counted,
77
+ };
78
+ outside.set(key, made);
79
+ return made;
80
+ }
81
+ function abandonedOn(signal) {
82
+ return new Promise((resolve) => {
83
+ if (signal.aborted)
84
+ resolve(null);
85
+ else
86
+ signal.addEventListener('abort', () => resolve(null), { once: true });
87
+ });
88
+ }
89
+ function roomLane(locator) {
90
+ return `room ${locator}`;
91
+ }
39
92
  export function composeRuntime(options) {
40
93
  const localSaves = options.adapter.localSaves === true;
41
94
  let authorization = options.authorization;
42
95
  let opening;
43
96
  let store;
44
- const replicas = new Map();
97
+ const fences = new Map();
98
+ const isFenced = (locator) => fences.has(locator);
99
+ let fencings = 0;
100
+ const lastFenced = new Map();
101
+ const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
102
+ let discoveries = 0;
103
+ const replicas = new FencedMap(isFenced);
104
+ const queuesWithoutReplica = new FencedMap(isFenced);
45
105
  const pendings = new Map();
46
- const roomDrafts = new Map();
106
+ const seeds = new Map();
107
+ const seedWatchers = new Map();
108
+ const roomDrafts = new FencedMap(isFenced);
47
109
  const presentations = new Map();
48
110
  let limits = [];
49
111
  let capabilities = [];
50
- const rooms = new Map();
112
+ const rooms = new FencedMap(isFenced);
51
113
  const counted = new Map();
52
114
  const unreadable = new Set();
53
- const models = new Map();
54
- const liveModels = new Map();
115
+ const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
116
+ const models = new FencedMap(isModelFenced);
117
+ const liveModels = new FencedMap(isModelFenced);
55
118
  const invalidModels = new Set();
56
119
  const discovered = new Map();
120
+ const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
57
121
  let roomsListed = false;
58
122
  const scopedRefusals = new Map();
59
123
  const sourceRefusals = new Map();
@@ -81,7 +145,6 @@ export function composeRuntime(options) {
81
145
  let status = STARTING;
82
146
  let shown = STARTING;
83
147
  let discoveryPaused = false;
84
- let resetTimer;
85
148
  let closed = false;
86
149
  let stopping = false;
87
150
  let withdrawStream;
@@ -89,12 +152,22 @@ export function composeRuntime(options) {
89
152
  let underway = null;
90
153
  let streamed = null;
91
154
  let handed = false;
92
- const submitting = new Map();
155
+ const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
156
+ const repairing = new Set();
157
+ const calling = new Set();
158
+ const repairable = new Map();
159
+ let outcomeRetention = null;
93
160
  const answering = new Map();
94
161
  let complete = false;
95
162
  let recorded = false;
96
163
  const stale = new Set();
97
- let chain = Promise.resolve();
164
+ let generation = 0;
165
+ let discovering = null;
166
+ let opened = null;
167
+ const owed = new Set();
168
+ const lanes = new Map();
169
+ let roomReads = { generation, free: ROOM_READS, waiting: [] };
170
+ const working = new Set();
98
171
  let mutations = Promise.resolve();
99
172
  const roomListeners = new Set();
100
173
  const refusalListeners = new Set();
@@ -105,7 +178,7 @@ export function composeRuntime(options) {
105
178
  const presentationListeners = new Set();
106
179
  const readSide = createReadSide();
107
180
  let withdrawConversations;
108
- let currencyQueued = false;
181
+ let currencyQueued = null;
109
182
  let socketOpen = false;
110
183
  let serviceMode = 'open';
111
184
  let haltedRefusal = false;
@@ -113,24 +186,41 @@ export function composeRuntime(options) {
113
186
  const serviceModeListeners = new Set();
114
187
  let member = null;
115
188
  const memberListeners = new Set();
116
- const materialized = new Set();
117
- let everyRoom = false;
118
- const transfers = new Map();
189
+ const prepared = new Set();
190
+ const projecting = new Set();
191
+ const unpreparedDemand = new Set();
192
+ const unindexable = new Set();
193
+ let preparing = false;
194
+ let demand = WHOLE_DEMAND;
195
+ let narrowing = null;
196
+ let targets = {};
197
+ let persistence = options.shell.persistence;
198
+ let capacity = null;
199
+ const paused = new Set();
200
+ let rechecking = null;
201
+ const readinessWatches = new Set();
202
+ let readinessQueued = false;
203
+ const catching = new Set();
204
+ const transfers = new FencedMap(isFenced);
119
205
  const conversations = createConversationStore({
120
206
  agents: declaredAgents,
121
- documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
207
+ documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
122
208
  call: (name, input) => invokeAction(name, input),
123
209
  durably,
124
210
  stream: () => options.adapter.conversations,
125
211
  publish: publishChangeSet,
212
+ online: () => !status.providerUnreachable && refusal === null,
126
213
  closed: () => closed,
214
+ unanswered: resendLater,
215
+ expired: (operation) => pastHorizon(operation),
127
216
  });
128
217
  const searchIndex = createSearchIndex({
129
- documents: () => readSide.documents(),
218
+ room: (room) => readSide.room(room),
130
219
  document: (room, path) => readSide.document(room, path),
131
- messages: () => conversations.messages(),
132
- message: (conversation, identity) => conversations.message(conversation, identity),
133
220
  });
221
+ const roomsToIndex = new Set();
222
+ const conversationsToIndex = new Set();
223
+ let indexing = false;
134
224
  const journal = createLog();
135
225
  let notifyReady = () => { };
136
226
  const ready = new Promise((resolve) => {
@@ -185,6 +275,15 @@ export function composeRuntime(options) {
185
275
  const value = presentations.get(app);
186
276
  return value === undefined ? null : structuredClone(value);
187
277
  }
278
+ function keepPresentation(app, next) {
279
+ return exclusive(async () => {
280
+ const local = await openLocal();
281
+ const stored = next(presentations.get(app));
282
+ await local.present({ app, value: stored });
283
+ presentations.set(app, stored);
284
+ publishChangeSet(changeSet('', null, null, { presentation: app }));
285
+ });
286
+ }
188
287
  function emitPresentation(app) {
189
288
  if (closed)
190
289
  return;
@@ -194,7 +293,9 @@ export function composeRuntime(options) {
194
293
  }
195
294
  }
196
295
  function pending() {
197
- return !localSaves && [...replicas.values()].some((replica) => replica.queue.length > 0);
296
+ return (!localSaves &&
297
+ (queuesWithoutReplica.size > 0 ||
298
+ [...replicas.values()].some((replica) => replica.queue.length > 0)));
198
299
  }
199
300
  function attention() {
200
301
  const attentionRequired = [];
@@ -207,9 +308,16 @@ export function composeRuntime(options) {
207
308
  returned: structuredClone(record.returned),
208
309
  });
209
310
  }
311
+ for (const [room, refusal] of sourceRefusals)
312
+ attentionRequired.push({ room, refusal: structuredClone(refusal) });
210
313
  attentionRequired.sort((left, right) => {
211
314
  if (left.room !== right.room)
212
315
  return left.room < right.room ? -1 : 1;
316
+ if ('refusal' in left || 'refusal' in right) {
317
+ if ('refusal' in left && 'refusal' in right)
318
+ return 0;
319
+ return 'refusal' in left ? -1 : 1;
320
+ }
213
321
  if (left.path !== right.path)
214
322
  return left.path < right.path ? -1 : 1;
215
323
  return 0;
@@ -228,12 +336,9 @@ export function composeRuntime(options) {
228
336
  if (replica === undefined)
229
337
  return 0;
230
338
  const roots = replica.confirmed.record.typedSelection?.roots ?? [];
231
- let count = 0;
232
- for (const path of Object.keys(replica.visible.documents)) {
233
- if (markdownPath(path) && !roots.some((root) => beneath(root, path)))
234
- count += 1;
235
- }
236
- return count;
339
+ const weighed = outsideOf(roots);
340
+ return (tally(replica.visible.documents, weighed.documents) +
341
+ tally(replica.visible.binaries, weighed.binaries));
237
342
  }
238
343
  function listed() {
239
344
  return structuredClone([...rooms.values()].map((room) => {
@@ -260,15 +365,19 @@ export function composeRuntime(options) {
260
365
  return { listed: roomsListed, rooms: held };
261
366
  }
262
367
  function emitRooms() {
368
+ readSide.recover();
369
+ queueReadiness();
263
370
  const value = listed();
264
371
  for (const listener of roomListeners)
265
372
  listener(value);
266
373
  }
267
374
  function emitRefusal() {
375
+ queueReadiness();
268
376
  for (const listener of refusalListeners)
269
377
  listener(refusal);
270
378
  }
271
379
  function emitStorageLoss() {
380
+ queueReadiness();
272
381
  for (const listener of storageLossListeners)
273
382
  listener(storageLoss);
274
383
  }
@@ -285,7 +394,8 @@ export function composeRuntime(options) {
285
394
  function show() {
286
395
  if (closed)
287
396
  return;
288
- const next = status.state === 'synced' && !conversations.current()
397
+ queueReadiness();
398
+ const next = status.state === 'synced' && demand.conversations && !conversations.current()
289
399
  ? { ...status, state: 'syncing' }
290
400
  : status;
291
401
  if (same(shown, next))
@@ -304,10 +414,7 @@ export function composeRuntime(options) {
304
414
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
305
415
  }
306
416
  function entriesOf(locator) {
307
- const replica = replicas.get(locator);
308
- if (replica === undefined)
309
- return [];
310
- return replica.queue.map(entryOf);
417
+ return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
311
418
  }
312
419
  function emitQueue(locator) {
313
420
  const entries = entriesOf(locator);
@@ -317,42 +424,517 @@ export function composeRuntime(options) {
317
424
  }
318
425
  }
319
426
  function applyChangeSet(set) {
320
- const held = everyRoom
321
- ? set
322
- : { ...set, documents: set.documents.filter(({ locator }) => materialized.has(locator)) };
427
+ const held = {
428
+ ...set,
429
+ documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
430
+ };
323
431
  readSide.apply(held);
324
432
  searchIndex.changed(held);
325
433
  }
326
- function materialize(locators) {
327
- if (everyRoom)
434
+ async function referenced(locator) {
435
+ for (;;) {
436
+ const record = replicas.get(locator)?.confirmed.record;
437
+ if (record === undefined || closed)
438
+ return;
439
+ const held = { text: record.documents, binaries: record.binaries };
440
+ const composition = compositionOf(record);
441
+ if (referencesIndexed(held, composition))
442
+ return;
443
+ await indexReferrers(held, composition);
444
+ }
445
+ }
446
+ async function prepareReplica(locator) {
447
+ await referenced(locator);
448
+ const replica = replicas.get(locator);
449
+ if (replica === undefined || closed || prepared.has(locator))
328
450
  return;
329
- if (locators === null)
330
- everyRoom = true;
331
- const sets = [];
332
- for (const locator of locators ?? [...replicas.keys()]) {
333
- if (materialized.has(locator))
334
- continue;
335
- materialized.add(locator);
336
- const replica = replicas.get(locator);
337
- if (replica !== undefined)
338
- sets.push(changeSet(locator, null, replica));
451
+ const visible = replica.visible;
452
+ const paths = [
453
+ ...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
454
+ ];
455
+ const entered = [];
456
+ projecting.add(locator);
457
+ try {
458
+ await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
459
+ const current = replicas.get(locator);
460
+ if (current === undefined || closed)
461
+ return;
462
+ const set = entering(locator, current, chunk);
463
+ readSide.apply(set);
464
+ entered.push(set);
465
+ });
466
+ if (!replicas.has(locator) || closed)
467
+ return;
468
+ prepared.add(locator);
469
+ }
470
+ finally {
471
+ projecting.delete(locator);
339
472
  }
340
- if (sets.length === 0)
473
+ conversations.changed(mergeChangeSets(entered));
474
+ readSide.recover();
475
+ queueReadiness();
476
+ }
477
+ function unprepared() {
478
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
479
+ }
480
+ function unbuilt(locator, error) {
481
+ unindexable.add(locator);
482
+ journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
483
+ queueReadiness();
484
+ }
485
+ function prepare(locators = []) {
486
+ for (const locator of locators)
487
+ unpreparedDemand.add(locator);
488
+ if (preparing)
341
489
  return;
342
- const set = mergeChangeSets(sets);
343
- readSide.apply(set);
344
- searchIndex.changed(set);
490
+ preparing = true;
491
+ void (async () => {
492
+ try {
493
+ for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
494
+ await yielded();
495
+ if (closed)
496
+ return;
497
+ unpreparedDemand.delete(waiting);
498
+ await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
499
+ }
500
+ }
501
+ finally {
502
+ preparing = false;
503
+ }
504
+ })();
345
505
  }
346
506
  function reachedRooms(plan) {
347
- if (plan.rooms !== null)
348
- return plan.rooms;
349
- if (plan.reach === null)
507
+ const reach = plan.reach;
508
+ const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
509
+ const named = plan.rooms ??
510
+ (reached === null
511
+ ? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
512
+ : [...reached]);
513
+ return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
514
+ }
515
+ function awaited(locator) {
516
+ return (!roomsListed ||
517
+ (rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
518
+ }
519
+ function covered(plan) {
520
+ const locators = reachedRooms(plan);
521
+ if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
522
+ return { pending: [] };
523
+ }
524
+ const pending = [];
525
+ const coverage = [];
526
+ const overlay = [];
527
+ for (const locator of locators) {
528
+ const replica = replicas.get(locator);
529
+ if (replica === undefined) {
530
+ if (awaited(locator))
531
+ pending.push({ room: locator, awaits: 'acquisition' });
532
+ continue;
533
+ }
534
+ if (!prepared.has(locator)) {
535
+ pending.push({ room: locator, awaits: 'preparation' });
536
+ continue;
537
+ }
538
+ coverage.push({
539
+ room: locator,
540
+ generation: replica.confirmed.record.transfer,
541
+ current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
542
+ });
543
+ for (const batch of replica.queue)
544
+ overlay.push(batch.id);
545
+ }
546
+ if (pending.length > 0)
547
+ return { pending };
548
+ return { coverage: { rooms: coverage, indexes: [], overlay } };
549
+ }
550
+ function awaitPreparation(cover) {
551
+ if (!('pending' in cover))
552
+ return;
553
+ const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
554
+ if (waiting.length > 0)
555
+ prepare(waiting.map(({ room }) => room));
556
+ }
557
+ function acquires(room) {
558
+ return (demanded(demand, room) ||
559
+ (replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
560
+ queuesWithoutReplica.has(room.locator));
561
+ }
562
+ function acquiredLocators() {
563
+ return [...discovered.values()]
564
+ .filter(acquires)
565
+ .map((room) => room.locator)
566
+ .sort();
567
+ }
568
+ function heldLocators(acquired) {
569
+ return demand.whole ? acquired : [...new Set([...acquired, ...demand.rooms])].sort();
570
+ }
571
+ function heldDemand(acquired) {
572
+ if (demand.whole)
573
+ return null;
574
+ return {
575
+ rooms: heldLocators(acquired.map((room) => room.locator)),
576
+ conversations: demand.conversations,
577
+ };
578
+ }
579
+ function applyDemand(next) {
580
+ const before = heldLocators(acquiredLocators());
581
+ const { whole, conversations: conversed } = demand;
582
+ demand = next;
583
+ const after = heldLocators(acquiredLocators());
584
+ queueReadiness();
585
+ if (!conversed && next.conversations)
586
+ scheduleCurrency();
587
+ if (JSON.stringify(before) === JSON.stringify(after) &&
588
+ whole === next.whole &&
589
+ conversed === next.conversations)
590
+ return;
591
+ journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
592
+ if (options.adapter.roomStream === undefined)
593
+ void sync('unstreamed');
594
+ else if (socketOpen)
595
+ options.adapter.roomStream.reopen();
596
+ }
597
+ function acquire(next) {
598
+ if (closed)
599
+ return;
600
+ if (narrowing !== null)
601
+ clearTimeout(narrowing);
602
+ narrowing = null;
603
+ const widening = wider(next, demand);
604
+ if (!widening && !narrower(next, demand))
605
+ return;
606
+ if (widening || !roomsListed) {
607
+ applyDemand(next);
608
+ return;
609
+ }
610
+ // A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
611
+ narrowing = setTimeout(() => {
612
+ narrowing = null;
613
+ applyDemand(next);
614
+ }, 5_000);
615
+ }
616
+ function requiredRooms(reach) {
617
+ if (reach === null)
618
+ return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
619
+ const named = [...new Set(reach.rooms)].sort();
620
+ if (roomsListed)
621
+ return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
622
+ return { listed: named.every((room) => replicas.has(room)), rooms: named };
623
+ }
624
+ function roomState(locator, search) {
625
+ if (sourceRefusals.has(locator))
626
+ return 'refused';
627
+ const listedRoom = discovered.get(locator);
628
+ if (roomsListed &&
629
+ (listedRoom === undefined ||
630
+ listedRoom.replica.admission !== 'admitted' ||
631
+ unreadable.has(locator)))
632
+ return 'unadmitted';
633
+ if (!replicas.has(locator))
634
+ return 'acquiring';
635
+ if (!prepared.has(locator))
636
+ return 'preparing';
637
+ if (search && !searchIndex.rooms().has(locator))
638
+ return 'indexing';
639
+ return 'complete';
640
+ }
641
+ function conversationCoverage(reach, search) {
642
+ const declared = reach === null || reach.conversations;
643
+ if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
644
+ return 'undeclared';
645
+ if (!conversations.known() || !conversations.current())
646
+ return 'acquiring';
647
+ if (!search)
648
+ return 'complete';
649
+ const unindexed = conversations
650
+ .searchable()
651
+ .filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
652
+ if (unindexed.length === 0)
653
+ return 'complete';
654
+ index([], unindexed);
655
+ return 'indexing';
656
+ }
657
+ function connectionOf() {
658
+ if (socketOpen)
659
+ return 'connected';
660
+ if (status.providerUnreachable)
661
+ return 'unreachable';
662
+ return options.adapter.roomStream === undefined && status.lastCompleted !== null
663
+ ? 'connected'
664
+ : 'connecting';
665
+ }
666
+ function refusedIn(locators) {
667
+ return locators.flatMap((room) => {
668
+ const held = sourceRefusals.get(room);
669
+ if (held === undefined)
670
+ return [];
671
+ const { file, location } = held.fields;
672
+ return [
673
+ {
674
+ room,
675
+ file: typeof file === 'string' ? file : '',
676
+ rule: held.code,
677
+ location: typeof location === 'string' ? location : null,
678
+ },
679
+ ];
680
+ });
681
+ }
682
+ function readinessFor(port, reach) {
683
+ const required = requiredRooms(reach);
684
+ const search = port.search === 'required';
685
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
686
+ const building = (state) => coverage
687
+ .filter((room) => room.state === state && !unindexable.has(room.room))
688
+ .map(({ room }) => room);
689
+ const preparing = building('preparing');
690
+ if (preparing.length > 0)
691
+ prepare(preparing);
692
+ const indexing = building('indexing');
693
+ if (indexing.length > 0)
694
+ index(indexing, []);
695
+ return readinessOf({
696
+ code: port.code,
697
+ coverage: {
698
+ listed: required.listed,
699
+ rooms: coverage,
700
+ conversations: conversationCoverage(reach, search),
701
+ search: search ? 'required' : 'unused',
702
+ },
703
+ persistence,
704
+ connection: connectionOf(),
705
+ house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
706
+ capacity: capacity !== null &&
707
+ coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
708
+ ? capacity
709
+ : port.capacity,
710
+ refused: refusedIn(required.rooms),
711
+ lost: storageLoss !== null,
712
+ established: targets[port.target]?.established === true,
713
+ });
714
+ }
715
+ function establish(name) {
716
+ targets = { ...targets, [name]: { established: true } };
717
+ queueReadiness();
718
+ void durably((local) => local.keepTargets(targets)).catch(() => {
719
+ journal.record(`target record failed ${name}`);
720
+ });
721
+ }
722
+ function deliverReadiness(watch) {
723
+ if (!readinessWatches.has(watch))
724
+ return;
725
+ const readiness = readinessFor(watch.port, watch.reach);
726
+ if (readiness.ready && targets[watch.port.target]?.established !== true) {
727
+ establish(watch.port.target);
728
+ }
729
+ const shown = JSON.stringify(readiness);
730
+ if (shown === watch.last)
731
+ return;
732
+ watch.last = shown;
733
+ watch.deliver(structuredClone(readiness));
734
+ }
735
+ function queueReadiness() {
736
+ if (readinessQueued || closed || readinessWatches.size === 0)
737
+ return;
738
+ readinessQueued = true;
739
+ queueMicrotask(() => {
740
+ readinessQueued = false;
741
+ if (closed)
742
+ return;
743
+ for (const watch of [...readinessWatches])
744
+ deliverReadiness(watch);
745
+ });
746
+ }
747
+ function watchReadiness(port, reach, deliver) {
748
+ const watch = { port, reach, deliver, last: null };
749
+ readinessWatches.add(watch);
750
+ void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
751
+ return () => {
752
+ readinessWatches.delete(watch);
753
+ };
754
+ }
755
+ function layerBytes(layer) {
756
+ let bytes = 0;
757
+ for (const [path, text] of Object.entries(layer.documents))
758
+ bytes += (path.length + text.length) * 2;
759
+ return bytes;
760
+ }
761
+ function grownBy(next, previous) {
762
+ if (previous === undefined)
763
+ return layerBytes(next);
764
+ const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
765
+ return layerBytes({ ...next, documents });
766
+ }
767
+ function exceeded(error) {
768
+ return (typeof error === 'object' &&
769
+ error !== null &&
770
+ error.name === 'QuotaExceededError');
771
+ }
772
+ async function estimated() {
773
+ const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
774
+ return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
775
+ }
776
+ function pause(locator, cause) {
777
+ paused.add(locator);
778
+ capacity = cause;
779
+ journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
780
+ queueReadiness();
781
+ if (rechecking !== null)
782
+ return;
783
+ // A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
784
+ rechecking = setInterval(() => {
785
+ void recheck();
786
+ }, 60_000);
787
+ }
788
+ async function recheck() {
789
+ const held = capacity;
790
+ if (held === null || closed)
791
+ return;
792
+ const available = await estimated().catch(() => null);
793
+ if (available === null || available < (held.needed ?? 0))
794
+ return;
795
+ if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
796
+ return;
797
+ resume();
798
+ }
799
+ function resume() {
800
+ if (rechecking !== null)
801
+ clearInterval(rechecking);
802
+ rechecking = null;
803
+ if (paused.size === 0 && capacity === null)
804
+ return;
805
+ paused.clear();
806
+ capacity = null;
807
+ journal.record('capacity resumed');
808
+ queueReadiness();
809
+ if (!closed && authorization !== undefined)
810
+ void sync('unstreamed');
811
+ }
812
+ async function reclaimed(local) {
813
+ const unreferenced = [...transfers.keys()].filter((locator) => {
814
+ const room = discovered.get(locator);
815
+ return room === undefined || !acquires(room);
816
+ });
817
+ for (const locator of unreferenced) {
818
+ await local.abandon(locator);
819
+ transfers.delete(locator);
820
+ }
821
+ if (unreferenced.length > 0)
822
+ journal.record(`capacity reclaimed ${unreferenced.length}`);
823
+ return unreferenced.length > 0;
824
+ }
825
+ async function admitted(local, locator, needed) {
826
+ if (paused.has(locator))
827
+ return false;
828
+ let available = await estimated().catch(() => null);
829
+ if (available === null || available >= needed)
830
+ return true;
831
+ if (await reclaimed(local))
832
+ available = await estimated().catch(() => null);
833
+ if (available === null || available >= needed)
834
+ return true;
835
+ pause(locator, { cause: 'insufficient-space', needed, available });
836
+ return false;
837
+ }
838
+ async function exceededOn(locator, bytes) {
839
+ const available = await estimated().catch(() => null);
840
+ pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
841
+ }
842
+ function seedRoomHeld(room, version) {
843
+ return prepared.has(room) && reachedBy(room, version);
844
+ }
845
+ function seedHeld(at) {
846
+ return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
847
+ }
848
+ function seedApplies(plan) {
849
+ const seed = seeds.get(plan.key);
850
+ if (seed === undefined)
350
851
  return null;
351
- return [...plan.reach.rooms, ...plan.reach.documents.map(roomOfKey)];
852
+ if (!seedHeld(seed.at))
853
+ return seed;
854
+ seeds.delete(plan.key);
855
+ return null;
352
856
  }
353
- function rowsQuery(plan) {
354
- materialize(reachedRooms(plan));
355
- return readSide.query(plan);
857
+ function seedBehind(plan, cover) {
858
+ const seed = seedApplies(plan);
859
+ if (seed === null || 'pending' in cover)
860
+ return cover;
861
+ return {
862
+ pending: Object.entries(seed.at)
863
+ .filter(([room, version]) => !seedRoomHeld(room, version))
864
+ .map(([room]) => ({ room, awaits: 'acquisition' })),
865
+ };
866
+ }
867
+ function seed(entry) {
868
+ if (seedHeld(entry.at))
869
+ return;
870
+ for (const { query, rows } of entry.queries) {
871
+ const key = queryKey(query);
872
+ seeds.set(key, { rows, at: entry.at });
873
+ for (const shown of seedWatchers.get(key) ?? [])
874
+ shown();
875
+ }
876
+ }
877
+ async function rowsQuery(plan) {
878
+ const before = covered(plan);
879
+ awaitPreparation(before);
880
+ const seeded = seedApplies(plan);
881
+ if (seeded !== null)
882
+ return seededResult(seeded).result;
883
+ if ('pending' in before)
884
+ return before;
885
+ const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
886
+ awaitPreparation(cover);
887
+ if ('pending' in cover)
888
+ return cover;
889
+ return { ...rows, coverage: cover.coverage };
890
+ }
891
+ async function indexConversation(conversation) {
892
+ searchIndex.indexMessages(conversation, [], false);
893
+ let after = null;
894
+ for (;;) {
895
+ const batch = await durably((local) => local.messagesAfter(conversation, after));
896
+ if (closed)
897
+ return;
898
+ searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
899
+ if (batch.length === 0)
900
+ return;
901
+ after = batch[batch.length - 1].identity;
902
+ }
903
+ }
904
+ function indexNext() {
905
+ const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
906
+ if (room !== undefined) {
907
+ roomsToIndex.delete(room);
908
+ return () => prepared.has(room)
909
+ ? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
910
+ : undefined;
911
+ }
912
+ const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
913
+ if (conversation === undefined)
914
+ return null;
915
+ conversationsToIndex.delete(conversation);
916
+ return () => indexConversation(conversation);
917
+ }
918
+ function index(rooms, named) {
919
+ for (const room of rooms)
920
+ roomsToIndex.add(room);
921
+ for (const conversation of named)
922
+ conversationsToIndex.add(conversation);
923
+ if (indexing)
924
+ return;
925
+ indexing = true;
926
+ void (async () => {
927
+ try {
928
+ for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
929
+ await yielded();
930
+ await unit();
931
+ queueReadiness();
932
+ }
933
+ }
934
+ finally {
935
+ indexing = false;
936
+ }
937
+ })().catch(() => { });
356
938
  }
357
939
  function publishChangeSet(set) {
358
940
  applyChangeSet(set);
@@ -369,34 +951,7 @@ export function composeRuntime(options) {
369
951
  emitPresentation(app);
370
952
  conversations.changed(set);
371
953
  }
372
- function stopTimer() {
373
- if (resetTimer === undefined)
374
- return;
375
- clearTimeout(resetTimer);
376
- resetTimer = undefined;
377
- }
378
- function armTimer() {
379
- stopTimer();
380
- if (closed || stopping)
381
- return;
382
- let earliest = null;
383
- for (const { submission } of pendings.values()) {
384
- if (submission.state !== 'reset')
385
- continue;
386
- const at = Date.parse(submission.reset);
387
- if (earliest === null || at < earliest)
388
- earliest = at;
389
- }
390
- if (earliest === null)
391
- return;
392
- // A Save House held back goes again only once the reset its answer named has passed.
393
- resetTimer = setTimeout(() => {
394
- resetTimer = undefined;
395
- submitAll();
396
- }, Math.max(0, earliest - Date.now()));
397
- }
398
954
  async function refuse(reason, provider = null) {
399
- stopTimer();
400
955
  discoveryPaused = true;
401
956
  try {
402
957
  if (reason === 'revoked-authorization') {
@@ -404,10 +959,7 @@ export function composeRuntime(options) {
404
959
  const revoked = { status: 'unavailable', reason: 'revoked' };
405
960
  try {
406
961
  const local = await openLocal();
407
- await local.revokeModels([...replicas.values()].map((replica) => ({
408
- record: replica.confirmed.record,
409
- queue: replica.queue,
410
- })));
962
+ await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
411
963
  }
412
964
  catch {
413
965
  journal.record('model revocation persistence failed');
@@ -474,19 +1026,31 @@ export function composeRuntime(options) {
474
1026
  },
475
1027
  providerOwnedPaths: room.providerOwnedPaths,
476
1028
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
477
- attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
478
1029
  synchronizationAvailable,
479
1030
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
480
1031
  });
481
1032
  }
482
- async function applyDiscovery(listed, local) {
1033
+ async function applyDiscovery(found, started) {
1034
+ for (const room of found) {
1035
+ const fence = fences.get(room.locator);
1036
+ if (fence !== undefined && fence.erased && fence.at < started)
1037
+ fences.delete(room.locator);
1038
+ }
1039
+ await loseRooms(found
1040
+ .filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
1041
+ .map((room) => room.locator));
1042
+ const listed = found.filter((room) => !fences.has(room.locator));
483
1043
  const listedLocators = new Set(listed.map((room) => room.locator));
484
1044
  for (const locator of listedLocators)
485
1045
  scopedRefusals.delete(locator);
486
1046
  discovered.clear();
487
1047
  for (const room of listed)
488
1048
  discovered.set(room.locator, room);
489
- await dropUnlisted(listedLocators, local);
1049
+ const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
1050
+ if (options.adapter.exhaustiveDiscovery === true)
1051
+ await loseRooms(unlisted);
1052
+ else
1053
+ unlistedKept(unlisted);
490
1054
  for (const room of listed) {
491
1055
  const held = replicas.get(room.locator)?.confirmed.record;
492
1056
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -497,44 +1061,91 @@ export function composeRuntime(options) {
497
1061
  }
498
1062
  roomsListed = true;
499
1063
  emitRooms();
1064
+ return listed;
500
1065
  }
501
- function unlistedHeld(listed) {
502
- return [...new Set([...rooms.keys(), ...roomDrafts.keys(), ...transfers.keys()])].filter((locator) => !listed.has(locator));
1066
+ function heldLocally(locator) {
1067
+ return (replicas.has(locator) ||
1068
+ roomDrafts.has(locator) ||
1069
+ transfers.has(locator) ||
1070
+ queuesWithoutReplica.has(locator));
503
1071
  }
504
- async function dropUnlisted(listed, local) {
505
- if (unlistedHeld(listed).length === 0)
506
- return;
507
- await exclusive(async () => {
508
- const unlisted = unlistedHeld(listed);
509
- await local.dropRooms(unlisted);
510
- const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
511
- room: true,
512
- queue: true,
513
- drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
514
- }));
515
- for (const locator of unlisted) {
516
- for (const batch of replicas.get(locator)?.queue ?? [])
517
- pendings.delete(batch.id);
518
- replicas.delete(locator);
1072
+ function unlistedHeld(listed) {
1073
+ return [
1074
+ ...new Set([
1075
+ ...rooms.keys(),
1076
+ ...roomDrafts.keys(),
1077
+ ...transfers.keys(),
1078
+ ...queuesWithoutReplica.keys(),
1079
+ ]),
1080
+ ].filter((locator) => !listed.has(locator));
1081
+ }
1082
+ function unlistedKept(unlisted) {
1083
+ for (const locator of unlisted) {
1084
+ const held = replicas.get(locator)?.confirmed.record;
1085
+ if (held !== undefined)
1086
+ list(held, false);
1087
+ else
519
1088
  rooms.delete(locator);
520
- roomDrafts.delete(locator);
521
- transfers.delete(locator);
522
- unreadable.delete(locator);
523
- stale.delete(locator);
524
- scopedRefusals.delete(locator);
525
- sourceRefusals.delete(locator);
526
- declarationRefusals.delete(locator);
527
- for (const attached of ['collection', 'typed-selection']) {
528
- const key = modelKey(locator, attached);
529
- models.delete(key);
530
- liveModels.delete(key);
531
- invalidModels.delete(key);
532
- }
533
- journal.record(`room unlisted ${locator}`);
1089
+ }
1090
+ }
1091
+ function fence(locators) {
1092
+ const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1093
+ const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
1094
+ room: true,
1095
+ queue: true,
1096
+ drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
1097
+ }));
1098
+ for (const locator of fresh) {
1099
+ fences.set(locator, { at: discoveries, erased: false });
1100
+ lastFenced.set(locator, ++fencings);
1101
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1102
+ pendings.delete(batch.id);
1103
+ replicas.delete(locator);
1104
+ queuesWithoutReplica.delete(locator);
1105
+ rooms.delete(locator);
1106
+ roomDrafts.delete(locator);
1107
+ transfers.delete(locator);
1108
+ catching.delete(locator);
1109
+ discovered.delete(locator);
1110
+ announced.delete(locator);
1111
+ unreadable.delete(locator);
1112
+ stale.delete(locator);
1113
+ repairable.delete(locator);
1114
+ unindexable.delete(locator);
1115
+ paused.delete(locator);
1116
+ scopedRefusals.delete(locator);
1117
+ sourceRefusals.delete(locator);
1118
+ declarationRefusals.delete(locator);
1119
+ for (const attached of ['collection', 'typed-selection']) {
1120
+ const key = modelKey(locator, attached);
1121
+ models.delete(key);
1122
+ liveModels.delete(key);
1123
+ invalidModels.delete(key);
534
1124
  }
535
- publishChangeSet(mergeChangeSets(removed));
536
- moveTo({ ...status, ...attention() });
537
- });
1125
+ journal.record(`room lost ${locator}`);
1126
+ }
1127
+ if (fresh.length === 0)
1128
+ return fresh;
1129
+ publishChangeSet(mergeChangeSets(removed));
1130
+ for (const locator of fresh)
1131
+ prepared.delete(locator);
1132
+ searchIndex.forget(fresh);
1133
+ readSide.recover();
1134
+ moveTo({ ...status, ...attention() });
1135
+ return fresh;
1136
+ }
1137
+ async function erased(local, locators) {
1138
+ await local.erase(locators);
1139
+ for (const locator of locators) {
1140
+ const held = fences.get(locator);
1141
+ if (held !== undefined)
1142
+ held.erased = true;
1143
+ }
1144
+ }
1145
+ async function loseRooms(locators) {
1146
+ const fresh = fence(locators);
1147
+ if (fresh.length > 0)
1148
+ await durably((local) => erased(local, fresh));
538
1149
  }
539
1150
  function restored(key, persisted) {
540
1151
  const prior = models.get(key);
@@ -555,7 +1166,7 @@ export function composeRuntime(options) {
555
1166
  return model;
556
1167
  }
557
1168
  async function hydrate(opened) {
558
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
1169
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
559
1170
  opened.all(),
560
1171
  opened.queues(),
561
1172
  opened.roomDrafts(),
@@ -567,7 +1178,9 @@ export function composeRuntime(options) {
567
1178
  opened.capabilities(),
568
1179
  opened.member(),
569
1180
  opened.transfers(),
1181
+ opened.targets(),
570
1182
  ]);
1183
+ targets = heldTargets;
571
1184
  transfers.clear();
572
1185
  for (const record of staged)
573
1186
  transfers.set(record.locator, record);
@@ -589,6 +1202,12 @@ export function composeRuntime(options) {
589
1202
  }
590
1203
  const previousReplicas = new Map(replicas);
591
1204
  replicas.clear();
1205
+ queuesWithoutReplica.clear();
1206
+ const replicated = new Set(records.map((record) => record.locator));
1207
+ for (const [locator, queue] of queuedBatches) {
1208
+ if (!replicated.has(locator))
1209
+ queuesWithoutReplica.set(locator, queue);
1210
+ }
592
1211
  for (const record of records) {
593
1212
  const model = restored(modelKey(record.locator, 'collection'), record.model);
594
1213
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
@@ -602,7 +1221,6 @@ export function composeRuntime(options) {
602
1221
  rooms.set(record.locator, {
603
1222
  ...existing,
604
1223
  roomVersion: record.roomVersion,
605
- attachments: record.attachments,
606
1224
  operations: record.operations,
607
1225
  replica: record.replica,
608
1226
  model: modelTypes(model),
@@ -624,6 +1242,7 @@ export function composeRuntime(options) {
624
1242
  async function readState(opened) {
625
1243
  const { lastCompleted, locators, read } = await hydrate(opened);
626
1244
  applyChangeSet(read);
1245
+ prepare();
627
1246
  if (replicas.size > 0)
628
1247
  emitRooms();
629
1248
  if (member !== null)
@@ -634,8 +1253,15 @@ export function composeRuntime(options) {
634
1253
  return lastCompleted;
635
1254
  }
636
1255
  async function open() {
637
- const opened = await openRuntimeStore();
1256
+ if (closed)
1257
+ throw new Error('runtime closed');
1258
+ const opened = await openRuntimeStore(isFenced);
638
1259
  store = opened;
1260
+ const interrupted = await opened.fences();
1261
+ for (const locator of interrupted) {
1262
+ fences.set(locator, { at: discoveries, erased: false });
1263
+ lastFenced.set(locator, ++fencings);
1264
+ }
639
1265
  const kept = options.shell.generation;
640
1266
  const storedGeneration = await opened.generation();
641
1267
  const loss = detectStorageLoss(storedGeneration, kept);
@@ -649,6 +1275,8 @@ export function composeRuntime(options) {
649
1275
  emitStorageLoss();
650
1276
  }
651
1277
  const lastCompleted = await readState(opened);
1278
+ if (interrupted.length > 0)
1279
+ await erased(opened, interrupted);
652
1280
  moveTo({ ...status, lastCompleted, ...attention() });
653
1281
  return opened;
654
1282
  }
@@ -675,8 +1303,10 @@ export function composeRuntime(options) {
675
1303
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
676
1304
  }
677
1305
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
678
- const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
679
- const kept = queue.filter((batch) => !withdrawn.includes(batch));
1306
+ const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
1307
+ const { withdrawn, kept } = standing(confirmed, queue, withdrawable, localSaves);
1308
+ const founded = kept.filter((batch) => !queue.includes(batch));
1309
+ const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
680
1310
  const unqueued = withdrawn.map((batch) => ({
681
1311
  batch,
682
1312
  returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
@@ -700,30 +1330,49 @@ export function composeRuntime(options) {
700
1330
  });
701
1331
  }
702
1332
  }
703
- await local.write({
704
- next: confirmed.record,
705
- previous: before?.confirmed.record ?? null,
706
- ...(settling.stored === undefined ? {} : { stored: settling.stored }),
707
- queue: kept,
708
- previousQueue: before?.queue ?? [],
709
- roomDrafts: [...written.values()],
710
- });
711
- for (const batch of before?.queue ?? []) {
712
- if (!kept.includes(batch))
1333
+ await local.keepAuthored({ locator, queue: kept, founded, roomDrafts: [...written.values()] });
1334
+ const staying = new Set(kept.map((batch) => batch.id));
1335
+ for (const batch of previousQueue) {
1336
+ if (!staying.has(batch.id))
713
1337
  pendings.delete(batch.id);
714
1338
  }
715
1339
  for (const record of written.values())
716
1340
  rememberRoomDraft(record);
717
- for (const { batch, returned } of unqueued) {
718
- const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
719
- journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
720
- }
721
- replicas.set(locator, replicaOf(confirmed, kept, before));
722
- return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
723
- room: settling.room,
724
- queue: kept.length !== (before?.queue.length ?? 0),
725
- drafts: [...written.keys()],
726
- });
1341
+ try {
1342
+ await local.write({
1343
+ next: confirmed.record,
1344
+ previous: before?.confirmed.record ?? null,
1345
+ ...(settling.stored === undefined ? {} : { stored: settling.stored }),
1346
+ });
1347
+ }
1348
+ catch (error) {
1349
+ const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
1350
+ if (held !== null)
1351
+ replicas.set(locator, held);
1352
+ else if (kept.length > 0)
1353
+ queuesWithoutReplica.set(locator, kept);
1354
+ else
1355
+ queuesWithoutReplica.delete(locator);
1356
+ publishChangeSet(changeSet(locator, before ?? null, held, {
1357
+ queue: queueChanged,
1358
+ drafts: [...written.keys()],
1359
+ }));
1360
+ moveTo({ ...status, ...attention() });
1361
+ throw error;
1362
+ }
1363
+ return () => {
1364
+ queuesWithoutReplica.delete(locator);
1365
+ for (const { batch, returned } of unqueued) {
1366
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
1367
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1368
+ }
1369
+ replicas.set(locator, replicaOf(confirmed, kept, before));
1370
+ return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
1371
+ room: settling.room,
1372
+ queue: queueChanged,
1373
+ drafts: [...written.keys()],
1374
+ });
1375
+ };
727
1376
  }
728
1377
  function adoptModels(locator, record) {
729
1378
  for (const [attached, model] of [
@@ -756,11 +1405,16 @@ export function composeRuntime(options) {
756
1405
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
757
1406
  };
758
1407
  }
759
- function publish(local, room, confirmed, landing = {}) {
1408
+ function publish(local, room, confirmed, landing, live) {
760
1409
  const { base, stored } = landing;
761
1410
  return exclusive(async () => {
762
- if (!continuing())
1411
+ if (stored !== undefined) {
1412
+ await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
1413
+ }
1414
+ if (!live()) {
1415
+ caughtUpTo(room.locator);
763
1416
  return true;
1417
+ }
764
1418
  const prior = replicas.get(room.locator);
765
1419
  if (base !== undefined &&
766
1420
  (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
@@ -768,35 +1422,57 @@ export function composeRuntime(options) {
768
1422
  return false;
769
1423
  }
770
1424
  adoptModels(room.locator, confirmed.record);
771
- const queue = prior?.queue ?? [];
1425
+ const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
772
1426
  const roomVersion = confirmed.record.roomVersion;
773
1427
  const accepted = queue.filter((batch) => {
774
1428
  const accepted = pendings.get(batch.id)?.accepted;
775
1429
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
776
1430
  });
777
- const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
778
- if (stored !== undefined)
1431
+ let settled;
1432
+ try {
1433
+ settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
1434
+ }
1435
+ catch (error) {
1436
+ if (!exceeded(error))
1437
+ throw error;
1438
+ await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
1439
+ caughtUpTo(room.locator);
1440
+ return true;
1441
+ }
1442
+ const set = settled();
1443
+ if (stored !== undefined) {
779
1444
  transfers.delete(room.locator);
1445
+ catching.delete(room.locator);
1446
+ }
780
1447
  for (const batch of accepted)
781
1448
  pendings.delete(batch.id);
782
1449
  stale.delete(room.locator);
1450
+ unindexable.delete(room.locator);
783
1451
  if (unreadable.delete(room.locator))
784
1452
  scopedRefusals.delete(room.locator);
785
1453
  sourceRefusals.delete(room.locator);
786
1454
  declarationRefusals.delete(room.locator);
787
1455
  list(room, true);
1456
+ if (prior === undefined)
1457
+ prepared.add(room.locator);
788
1458
  publishChangeSet(set);
789
1459
  moveTo({ ...status, ...attention() });
1460
+ options.adapter.roomStream?.published(room.locator, roomVersion);
790
1461
  return true;
791
1462
  });
792
1463
  }
793
- async function saveRoomDrafts(locator, named, writtenBy) {
1464
+ async function savedRoom(locator) {
794
1465
  const local = await openLocal();
1466
+ await referenced(locator);
795
1467
  const replica = replicas.get(locator);
796
1468
  const room = rooms.get(locator);
797
1469
  if (replica === undefined || room === undefined) {
798
1470
  throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
799
1471
  }
1472
+ return { local, replica, room };
1473
+ }
1474
+ async function saveRoomDrafts(locator, named, writtenBy) {
1475
+ const target = await savedRoom(locator);
800
1476
  const saved = [];
801
1477
  for (const { path, revision } of named) {
802
1478
  const record = roomDrafts.get(locator)?.get(path);
@@ -806,11 +1482,60 @@ export function composeRuntime(options) {
806
1482
  if (!saved.some((draft) => draft.path === path))
807
1483
  saved.push({ path, parts: record.parts });
808
1484
  }
1485
+ return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
1486
+ }
1487
+ async function saveParts(locator, named, writtenBy) {
1488
+ const read = named.map(({ path, parts }) => ({ path, parts: readParts(parts) }));
1489
+ if (read.some(({ path }, at) => path === '' ||
1490
+ normalize(path) !== path ||
1491
+ read.findIndex((one) => one.path === path) !== at)) {
1492
+ throw runtimeError('room-draft-malformed');
1493
+ }
1494
+ const target = await savedRoom(locator);
1495
+ const saved = [];
1496
+ for (const { path, parts } of read) {
1497
+ const file = parts.file === undefined
1498
+ ? undefined
1499
+ : await heldFile(parts.file.blob, binaryBase(target.replica.visible, path));
1500
+ saved.push({ path, parts: held(undefined, parts, file) });
1501
+ }
1502
+ return committed(locator, target, saved, new Set(), writtenBy);
1503
+ }
1504
+ function carriedDrafts(locator, saved, consumed) {
1505
+ const holding = roomDrafts.get(locator);
1506
+ return new Map(saved.flatMap(({ path, parts }) => {
1507
+ const record = holding?.get(path);
1508
+ if (parts.move === undefined || record === undefined || consumed.has(path))
1509
+ return [];
1510
+ const to = moveTarget(parts.move);
1511
+ if (to === path || holding?.has(to) === true)
1512
+ return [];
1513
+ return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
1514
+ }));
1515
+ }
1516
+ async function committed(locator, { local, replica, room }, saved, consumed, writtenBy) {
809
1517
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
810
1518
  if (changed.length > 0)
811
1519
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
812
1520
  const parts = saved.map(({ path, parts }) => changesOf(path, parts));
813
- const built = buildBatch({
1521
+ const changes = parts.flatMap(({ changes }) => changes);
1522
+ const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
1523
+ if (requeue.kind === 'refused')
1524
+ return { outcome: 'refused', ...requeue.refusal };
1525
+ const foreign = [...requeue.withdrawn.keys()].some((origin) => {
1526
+ const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
1527
+ return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
1528
+ });
1529
+ if (foreign) {
1530
+ return {
1531
+ outcome: 'refused',
1532
+ code: 'unknown-revision',
1533
+ retryable: false,
1534
+ file: [...requeue.queued].at(-1),
1535
+ };
1536
+ }
1537
+ const withdrawing = requeue.withdrawn.size > 0;
1538
+ const input = {
814
1539
  local: localSaves,
815
1540
  baseRoomVersion: replica.confirmed.record.roomVersion,
816
1541
  composition: compositionFor(replica.confirmed.record),
@@ -819,40 +1544,115 @@ export function composeRuntime(options) {
819
1544
  providerLimits: limits,
820
1545
  handle: room.handle,
821
1546
  visible: replica.visible,
822
- changes: parts.flatMap(({ changes }) => changes),
1547
+ changes,
823
1548
  bases: parts.flatMap(({ bases }) => bases),
1549
+ queued: queuedDocuments(replica.queue),
824
1550
  writtenBy,
1551
+ clean: {
1552
+ text: replica.confirmed.record.documents,
1553
+ binaries: replica.confirmed.record.binaries,
1554
+ composition: compositionOf(replica.confirmed.record),
1555
+ },
1556
+ };
1557
+ const checked = withdrawing
1558
+ ? buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
1559
+ : null;
1560
+ if (checked?.kind === 'refused')
1561
+ return { outcome: 'refused', ...checked.refusal };
1562
+ const owners = [...requeue.owners].flatMap((origin) => {
1563
+ const batch = requeue.withdrawn.get(origin);
1564
+ return batch === undefined || batch === null ? [] : [{ origin, batch }];
825
1565
  });
826
- if (built.kind === 'refused')
1566
+ if (owners.length > 1) {
1567
+ return {
1568
+ outcome: 'refused',
1569
+ code: 'unknown-revision',
1570
+ retryable: false,
1571
+ file: [...requeue.queued].at(-1),
1572
+ };
1573
+ }
1574
+ const built = !withdrawing
1575
+ ? buildBatch(input)
1576
+ : requeue.changes.length === 0
1577
+ ? null
1578
+ : buildBatch({
1579
+ ...input,
1580
+ visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
1581
+ changes: requeue.changes,
1582
+ queued: queuedDocuments(requeue.queue),
1583
+ });
1584
+ if (built?.kind === 'refused')
827
1585
  return { outcome: 'refused', ...built.refusal };
828
- const batches = [...replica.queue, built.batch];
829
- const consumed = saved.map(({ path }) => path);
1586
+ const [owner] = owners;
1587
+ const join = built === null || owner === undefined
1588
+ ? null
1589
+ : joined(requeue.queue, owner.batch, built.batch);
1590
+ if (join?.kind === 'refused')
1591
+ return { outcome: 'refused', ...join.refusal };
1592
+ const withdrawn = new Map(requeue.withdrawn);
1593
+ if (join !== null && owner !== undefined)
1594
+ withdrawn.set(owner.origin, join.batch);
1595
+ const drafted = new Map();
1596
+ for (const [origin, replaced] of withdrawn) {
1597
+ if (replaced === null)
1598
+ continue;
1599
+ const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
1600
+ drafted.set(replaced.id, replaced === join?.batch
1601
+ ? [
1602
+ ...carried.map((held) => joinedDraft(held, saved)),
1603
+ ...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
1604
+ ]
1605
+ : carried);
1606
+ }
1607
+ if (built !== null && join === null)
1608
+ drafted.set(built.batch.id, saved);
1609
+ const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
1610
+ for (const replaced of withdrawn.values()) {
1611
+ if (replaced === null || !batches.includes(replaced))
1612
+ continue;
1613
+ const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
1614
+ if (limited !== null)
1615
+ return { outcome: 'refused', ...limited };
1616
+ }
1617
+ const carried = carriedDrafts(locator, saved, consumed);
1618
+ const left = [...consumed, ...carried.keys()];
1619
+ const arrived = [...carried.values()];
830
1620
  try {
831
- await local.write({
832
- next: replica.confirmed.record,
833
- previous: replica.confirmed.record,
1621
+ await local.keepAuthored({
1622
+ locator,
834
1623
  queue: batches,
835
- previousQueue: replica.queue,
836
- saved,
837
- consumedRoomDrafts: consumed,
1624
+ saved: drafted,
1625
+ consumedRoomDrafts: left,
1626
+ roomDrafts: arrived,
838
1627
  });
839
1628
  }
840
1629
  catch {
841
1630
  return { outcome: 'refused', code: 'durable-write', retryable: true };
842
1631
  }
843
- pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
1632
+ if (fences.has(locator))
1633
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1634
+ for (const origin of withdrawn.keys())
1635
+ pendings.delete(origin);
1636
+ for (const [id, drafts] of drafted) {
1637
+ pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
1638
+ }
844
1639
  if (!localSaves)
845
1640
  recorded = false;
846
- for (const path of consumed)
1641
+ for (const path of left)
847
1642
  forgetRoomDraft(locator, path);
1643
+ for (const record of arrived)
1644
+ rememberRoomDraft(record);
848
1645
  replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
849
1646
  publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
850
1647
  queue: true,
851
- drafts: consumed,
1648
+ drafts: [...left, ...arrived.map(({ path }) => path)],
852
1649
  }));
853
1650
  moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
854
1651
  submitLater(locator);
855
- return { outcome: 'saved', identifier: built.batch.id };
1652
+ const identifier = join?.batch.id ??
1653
+ built?.batch.id ??
1654
+ [...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
1655
+ return { outcome: 'saved', identifier };
856
1656
  }
857
1657
  async function mark(local, locator, identifier, submission, lost) {
858
1658
  const state = pendings.get(identifier);
@@ -862,6 +1662,8 @@ export function composeRuntime(options) {
862
1662
  return;
863
1663
  }
864
1664
  await local.submit(locator, identifier, submission, lost);
1665
+ if (fences.has(locator))
1666
+ return;
865
1667
  pendings.set(identifier, { ...state, submission, lost });
866
1668
  const replica = replicas.get(locator);
867
1669
  if (replica !== undefined)
@@ -872,6 +1674,8 @@ export function composeRuntime(options) {
872
1674
  if (!continuing())
873
1675
  throw new Error('connection refused');
874
1676
  await local.keepUpload(locator, identifier, upload);
1677
+ if (fences.has(locator))
1678
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
875
1679
  pendings.set(identifier, { ...pendings.get(identifier), upload });
876
1680
  if (!continuing())
877
1681
  throw new Error('connection refused');
@@ -887,6 +1691,7 @@ export function composeRuntime(options) {
887
1691
  }
888
1692
  if (closed || authorization === undefined || refusal !== null)
889
1693
  return UNCERTAIN;
1694
+ const since = fencings;
890
1695
  const transfer = ['transfer', 'history', locator, path];
891
1696
  if (query.revision !== undefined)
892
1697
  transfer.push(query.revision);
@@ -901,6 +1706,8 @@ export function composeRuntime(options) {
901
1706
  ...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
902
1707
  });
903
1708
  journal.record(`outcome history ${result.kind}`);
1709
+ if (fencedSince(locator, since))
1710
+ return failure({ kind: 'room-not-readable', locator });
904
1711
  if (result.kind === 'scoped-refusal') {
905
1712
  const room = replicas.get(result.locator)?.confirmed.record;
906
1713
  if (room !== undefined)
@@ -915,6 +1722,119 @@ export function composeRuntime(options) {
915
1722
  return UNCERTAIN;
916
1723
  }
917
1724
  }
1725
+ function queuedBlob(locator, path, localId) {
1726
+ for (const batch of replicas.get(locator)?.queue ?? []) {
1727
+ const change = batch.changes[queuedFile(batch, path, localId)];
1728
+ if (change?.kind === 'binary')
1729
+ return change.blob;
1730
+ }
1731
+ return null;
1732
+ }
1733
+ function binaryRoom(request) {
1734
+ if (request.kind === 'path')
1735
+ return request.locator;
1736
+ const scope = options.adapter.referrerScope(request.referrer);
1737
+ return scope?.kind === 'room' ? scope.locator : null;
1738
+ }
1739
+ async function transferBinary(request, transfer, answer) {
1740
+ if (closed || refusal !== null)
1741
+ return UNCERTAIN;
1742
+ try {
1743
+ await ensureAuthorization();
1744
+ }
1745
+ catch {
1746
+ return UNCERTAIN;
1747
+ }
1748
+ if (closed || authorization === undefined || refusal !== null)
1749
+ return UNCERTAIN;
1750
+ const since = fencings;
1751
+ journal.record(request.kind === 'path'
1752
+ ? `transfer binary ${request.locator} ${request.path}`
1753
+ : `transfer binary ${request.fileRef}`);
1754
+ try {
1755
+ const result = await transfer(authorization);
1756
+ const settled = result;
1757
+ journal.record(`outcome binary ${settled.kind}`);
1758
+ const locator = binaryRoom(request);
1759
+ if (locator !== null && fencedSince(locator, since))
1760
+ return failure({ kind: 'room-not-readable', locator });
1761
+ if (settled.kind === 'scoped-refusal') {
1762
+ const room = replicas.get(settled.locator)?.confirmed.record;
1763
+ if (room !== undefined)
1764
+ await scoped(settled, room, await openLocal());
1765
+ }
1766
+ if (settled.kind === 'revoked-authorization')
1767
+ await refuse('revoked-authorization');
1768
+ return answer(result);
1769
+ }
1770
+ catch {
1771
+ journal.record('outcome binary transport-uncertainty');
1772
+ return UNCERTAIN;
1773
+ }
1774
+ }
1775
+ function readBinary(request) {
1776
+ return transferBinary(request, (held) => options.adapter.binary(held, request), binaryAnswer);
1777
+ }
1778
+ function binaryPath(locator, path, revision) {
1779
+ if (revision !== undefined)
1780
+ return { kind: 'path', locator, path, revision };
1781
+ const visible = replicas.get(locator)?.visible;
1782
+ const held = visible?.binaries[path];
1783
+ if (visible === undefined || held === undefined)
1784
+ return null;
1785
+ if (held.state === 'pending')
1786
+ return { kind: 'pending', localId: held.localId };
1787
+ const record = binaryRecord(path, held, visible.revisions[path]);
1788
+ if (record.state !== 'accepted')
1789
+ return null;
1790
+ const replica = replicas.get(locator);
1791
+ const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
1792
+ const holds = (at) => {
1793
+ const kept = replica?.confirmed.record.binaries[at];
1794
+ return (kept?.state === 'accepted' &&
1795
+ kept.fileRef === record.fileRef &&
1796
+ replica?.confirmed.record.revisions[at] === record.revision);
1797
+ };
1798
+ let confirmed = path;
1799
+ for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
1800
+ const change = queued[index];
1801
+ if (change.kind === 'move' && change.to === confirmed)
1802
+ confirmed = change.path;
1803
+ }
1804
+ return {
1805
+ kind: 'path',
1806
+ locator,
1807
+ path: confirmed,
1808
+ revision: record.revision,
1809
+ fileRef: record.fileRef,
1810
+ };
1811
+ }
1812
+ async function readBinaryPath(locator, path, revision) {
1813
+ await ready;
1814
+ const asked = binaryPath(locator, path, revision);
1815
+ if (asked === null)
1816
+ return absentBinary(path);
1817
+ if (asked.kind === 'pending') {
1818
+ const blob = queuedBlob(locator, path, asked.localId);
1819
+ return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1820
+ }
1821
+ return readBinary(asked);
1822
+ }
1823
+ async function linkBinaryPath(locator, path, revision) {
1824
+ await ready;
1825
+ const asked = binaryPath(locator, path, revision);
1826
+ if (asked === null || asked.kind === 'pending')
1827
+ return absentBinary(path);
1828
+ return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
1829
+ }
1830
+ async function readBinaryReference(reference) {
1831
+ await ready;
1832
+ return readBinary({
1833
+ kind: 'reference',
1834
+ fileRef: reference.fileRef,
1835
+ referrer: reference.referrer,
1836
+ });
1837
+ }
918
1838
  function declaredAgents() {
919
1839
  for (const capability of capabilities) {
920
1840
  if (capability.conversationAgents !== undefined)
@@ -931,7 +1851,7 @@ export function composeRuntime(options) {
931
1851
  }
932
1852
  return undefined;
933
1853
  }
934
- async function invokeAction(name, input) {
1854
+ async function invokeAction(name, input, dispatching) {
935
1855
  await ready;
936
1856
  if (closed || refusal !== null)
937
1857
  return UNCERTAIN;
@@ -945,6 +1865,7 @@ export function composeRuntime(options) {
945
1865
  }
946
1866
  if (closed || authorization === undefined || refusal !== null)
947
1867
  return UNCERTAIN;
1868
+ await dispatching?.();
948
1869
  journal.record(`transfer action ${name}`);
949
1870
  try {
950
1871
  const result = await options.adapter.action(authorization, { name, input });
@@ -965,6 +1886,60 @@ export function composeRuntime(options) {
965
1886
  return UNCERTAIN;
966
1887
  }
967
1888
  }
1889
+ function unsettled(result) {
1890
+ return result.kind !== 'action' && result.retryable;
1891
+ }
1892
+ async function recoveredAction(name, input) {
1893
+ const operation = operationOf(input);
1894
+ if (operation === null)
1895
+ return invokeAction(name, input);
1896
+ const kept = async () => (await openLocal()).keepCall({ operation, name, input });
1897
+ calling.add(operation);
1898
+ try {
1899
+ let result = await invokeAction(name, input, kept);
1900
+ if (unsettled(result)) {
1901
+ await recovery.attempt(`action ${operation}`, async () => {
1902
+ if (closed || refusal !== null)
1903
+ return 'done';
1904
+ if (!socketOpen)
1905
+ return 'again';
1906
+ result = await invokeAction(name, input, kept);
1907
+ return unsettled(result) ? 'again' : 'done';
1908
+ }, { again: true });
1909
+ }
1910
+ if (!unsettled(result))
1911
+ await (await openLocal()).dropCall(operation);
1912
+ return result;
1913
+ }
1914
+ finally {
1915
+ calling.delete(operation);
1916
+ }
1917
+ }
1918
+ async function resendCalls(signal) {
1919
+ if (!socketOpen || closed || stopping)
1920
+ return 'done';
1921
+ const local = await openLocal();
1922
+ for (const held of await local.calls()) {
1923
+ if (calling.has(held.operation))
1924
+ continue;
1925
+ if (closed || signal.aborted)
1926
+ return 'done';
1927
+ if (pastHorizon(held.operation)) {
1928
+ await local.dropCall(held.operation);
1929
+ continue;
1930
+ }
1931
+ calling.add(held.operation);
1932
+ try {
1933
+ if (unsettled(await invokeAction(held.name, held.input)))
1934
+ return 'again';
1935
+ await local.dropCall(held.operation);
1936
+ }
1937
+ finally {
1938
+ calling.delete(held.operation);
1939
+ }
1940
+ }
1941
+ return 'done';
1942
+ }
968
1943
  function continuing() {
969
1944
  return !closed && !discoveryPaused;
970
1945
  }
@@ -979,11 +1954,12 @@ export function composeRuntime(options) {
979
1954
  options.adapter.reached(locator, written, roomVersion) &&
980
1955
  !options.adapter.reached(locator, roomVersion, written));
981
1956
  }
982
- async function pullRefresh(authorized, confirmed) {
983
- if (!continuing())
1957
+ async function pullRefresh(authorized, confirmed, live) {
1958
+ if (!live())
984
1959
  return unconfirmed;
1960
+ const turn = generation;
985
1961
  try {
986
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
1962
+ const result = await refreshRoom(reads, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
987
1963
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
988
1964
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
989
1965
  return result;
@@ -992,48 +1968,101 @@ export function composeRuntime(options) {
992
1968
  journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
993
1969
  throw error;
994
1970
  }
1971
+ finally {
1972
+ reads.finished(confirmed.record.locator, turn);
1973
+ }
995
1974
  }
996
- function staging(local) {
1975
+ function staging(local, live) {
997
1976
  return {
998
1977
  held: (locator) => transfers.get(locator) ?? null,
999
1978
  staged: (record) => local.staged(record),
1000
1979
  stage: (record, page) => exclusive(async () => {
1001
- if (!continuing())
1980
+ if (!live())
1981
+ return false;
1982
+ const bytes = layerBytes(page);
1983
+ if (!(await admitted(local, record.locator, bytes)))
1984
+ return false;
1985
+ try {
1986
+ await local.stage(record, page);
1987
+ }
1988
+ catch (error) {
1989
+ if (!exceeded(error))
1990
+ throw error;
1991
+ await exceededOn(record.locator, bytes);
1002
1992
  return false;
1003
- await local.stage(record, page);
1993
+ }
1004
1994
  transfers.set(record.locator, record);
1995
+ readSide.recover();
1005
1996
  return true;
1006
1997
  }),
1007
1998
  abandon: (locator) => exclusive(async () => {
1008
- if (!continuing())
1999
+ if (!live())
1009
2000
  return;
1010
2001
  await local.abandon(locator);
1011
2002
  transfers.delete(locator);
2003
+ readSide.recover();
1012
2004
  }),
1013
2005
  };
1014
2006
  }
1015
- async function pullBootstrap(authorized, room, local) {
1016
- if (!continuing())
2007
+ function reusable(room) {
2008
+ const record = replicas.get(room.locator)?.confirmed.record;
2009
+ if (record === undefined || record.replica.selection !== room.replica.selection)
2010
+ return null;
2011
+ return { documents: record.documents, revisions: record.revisions };
2012
+ }
2013
+ async function pullBootstrap(authorized, room, local, live) {
2014
+ if (!live())
1017
2015
  return unconfirmed;
2016
+ const turn = generation;
2017
+ if (replicas.has(room.locator)) {
2018
+ catching.add(room.locator);
2019
+ readSide.recover();
2020
+ }
1018
2021
  try {
1019
- const result = await bootstrapRoom(options.adapter, authorized, room, {
2022
+ const result = await bootstrapRoom(reads, authorized, room, {
1020
2023
  collection: modelOf(room.locator),
1021
2024
  selection: modelOf(room.locator, 'typed-selection'),
1022
- }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
2025
+ }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
1023
2026
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
1024
2027
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
2028
+ if (result.kind !== 'staged')
2029
+ caughtUpTo(room.locator);
1025
2030
  return result;
1026
2031
  }
1027
2032
  catch (error) {
1028
2033
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
2034
+ caughtUpTo(room.locator);
1029
2035
  throw error;
1030
2036
  }
2037
+ finally {
2038
+ reads.finished(room.locator, turn);
2039
+ }
2040
+ }
2041
+ function caughtUpTo(locator) {
2042
+ if (catching.delete(locator))
2043
+ readSide.recover();
1031
2044
  }
1032
- async function advance(room, local, authorized) {
2045
+ function behind(room) {
2046
+ const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
2047
+ return (held !== undefined &&
2048
+ [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
2049
+ }
2050
+ async function advance(room, local, authorized, live) {
2051
+ const carried = reads.carried(room.locator);
2052
+ const withheld = await advanceOnce(room, local, authorized, live);
2053
+ if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
2054
+ return withheld;
2055
+ }
2056
+ return advanceOnce(room, local, authorized, live);
2057
+ }
2058
+ async function advanceOnce(room, local, authorized, live) {
2059
+ await referenced(room.locator);
2060
+ if (!live() || paused.has(room.locator))
2061
+ return unconfirmed;
1033
2062
  const held = replicas.get(room.locator);
1034
2063
  if (held !== undefined) {
1035
- const refreshed = await pullRefresh(authorized, held.confirmed);
1036
- if (!continuing())
2064
+ const refreshed = await pullRefresh(authorized, held.confirmed, live);
2065
+ if (!live())
1037
2066
  return unconfirmed;
1038
2067
  if (refreshed.kind === 'still-current') {
1039
2068
  stale.delete(room.locator);
@@ -1047,31 +2076,45 @@ export function composeRuntime(options) {
1047
2076
  return null;
1048
2077
  }
1049
2078
  if (refreshed.kind === 'advanced') {
1050
- if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
2079
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
1051
2080
  const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
1052
2081
  const reached = now !== undefined &&
1053
2082
  options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
1054
- return reached ? null : advance(room, local, authorized);
2083
+ return reached ? null : advance(room, local, authorized, live);
1055
2084
  }
1056
2085
  await retainModel(local, room, 'collection', modelOf(room.locator));
1057
2086
  return null;
1058
2087
  }
1059
2088
  if (refreshed.kind !== 'beyond-horizon')
1060
2089
  return refreshed;
2090
+ if (opened !== null) {
2091
+ owed.add(room.locator);
2092
+ readSide.recover();
2093
+ return unconfirmed;
2094
+ }
1061
2095
  }
1062
- const bootstrapped = await pullBootstrap(authorized, room, local);
1063
- if (!continuing())
2096
+ const bootstrapped = await pullBootstrap(authorized, room, local, live);
2097
+ if (!live())
1064
2098
  return unconfirmed;
1065
2099
  if (bootstrapped.kind !== 'staged')
1066
2100
  return bootstrapped;
1067
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
2101
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
1068
2102
  return null;
1069
2103
  }
1070
2104
  async function withhold(withheld, room, local, progress) {
2105
+ if (withheld.kind === 'unconfirmed')
2106
+ return;
1071
2107
  if (withheld.kind === 'refused')
1072
2108
  stale.delete(room.locator);
1073
2109
  else
1074
2110
  stale.add(room.locator);
2111
+ if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
2112
+ const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
2113
+ repairable.set(room.locator, reset);
2114
+ repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
2115
+ }
2116
+ else
2117
+ repairable.delete(room.locator);
1075
2118
  if (withheld.kind === 'scoped-refusal') {
1076
2119
  await scoped(withheld, room, local);
1077
2120
  }
@@ -1107,6 +2150,7 @@ export function composeRuntime(options) {
1107
2150
  unreadable.add(room.locator);
1108
2151
  list(room, false);
1109
2152
  emitRooms();
2153
+ moveTo({ ...status, ...attention() });
1110
2154
  journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
1111
2155
  }
1112
2156
  function leaving(batch, result) {
@@ -1128,6 +2172,7 @@ export function composeRuntime(options) {
1128
2172
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1129
2173
  }
1130
2174
  async function adoptAccepted(room, local, submitted, accepted) {
2175
+ await referenced(room.locator);
1131
2176
  if (!continuing())
1132
2177
  return unconfirmed;
1133
2178
  const current = replicas.get(room.locator);
@@ -1138,22 +2183,23 @@ export function composeRuntime(options) {
1138
2183
  logAccepted(room.locator, submitted, accepted);
1139
2184
  const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1140
2185
  ? { kind: 'staged', confirmed: current.confirmed }
1141
- : applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.changes, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
2186
+ : applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
1142
2187
  ? accepted.roomVersion
1143
2188
  : held, accepted.roomVersion);
1144
2189
  if (applied.kind !== 'staged') {
1145
2190
  const state = pendings.get(submitted.id);
1146
2191
  await local.accept(room.locator, submitted.id, accepted.roomVersion);
1147
- if (state !== undefined)
2192
+ if (state !== undefined && !fences.has(room.locator))
1148
2193
  pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1149
2194
  emitQueue(room.locator);
1150
2195
  return applied;
1151
2196
  }
1152
- const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
2197
+ const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1153
2198
  pendings.delete(submitted.id);
1154
2199
  unreadable.delete(room.locator);
1155
2200
  sourceRefusals.delete(room.locator);
1156
2201
  declarationRefusals.delete(room.locator);
2202
+ const set = settled();
1157
2203
  list(room, true);
1158
2204
  publishChangeSet(set);
1159
2205
  moveTo({ ...status, ...attention() });
@@ -1167,14 +2213,14 @@ export function composeRuntime(options) {
1167
2213
  refusedSource(room, failed);
1168
2214
  return false;
1169
2215
  }
1170
- async function submitRoom(room, local, authorized, progress) {
1171
- while (continuing() && authorization !== undefined) {
2216
+ async function submitRoom(room, local, authorized, progress, signal) {
2217
+ while (continuing() && authorization !== undefined && !signal.aborted) {
1172
2218
  const replica = replicas.get(room.locator);
1173
2219
  const head = replica?.queue[0];
1174
2220
  if (replica === undefined || head === undefined)
1175
- return;
2221
+ return 'done';
1176
2222
  if (pendings.get(head.id)?.accepted !== undefined)
1177
- return;
2223
+ return 'done';
1178
2224
  const state = pendings.get(head.id);
1179
2225
  const { submission, lost: earlier } = state ?? {
1180
2226
  submission: { state: 'waiting' },
@@ -1182,24 +2228,30 @@ export function composeRuntime(options) {
1182
2228
  };
1183
2229
  if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
1184
2230
  progress.complete = false;
1185
- return;
2231
+ return { at: Date.parse(submission.reset) };
1186
2232
  }
1187
2233
  if (writesHeld()) {
1188
2234
  progress.complete = false;
1189
2235
  journal.record(`submission ${room.locator} ${head.id} held`);
1190
- return;
2236
+ return 'done';
1191
2237
  }
1192
2238
  const lost = earlier || submission.state === 'submitted';
2239
+ if (lost && pastHorizon(head.id)) {
2240
+ await discardPending(room, local, signal);
2241
+ return 'done';
2242
+ }
1193
2243
  let result;
1194
2244
  journal.record(`submission ${room.locator} ${head.id}`);
1195
2245
  await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1196
- if (discoveryPaused)
1197
- return;
1198
- if (writesHeld()) {
2246
+ if (discoveryPaused || fences.has(room.locator))
2247
+ return 'done';
2248
+ if (signal.aborted || writesHeld()) {
1199
2249
  await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
2250
+ if (signal.aborted)
2251
+ return 'done';
1200
2252
  progress.complete = false;
1201
2253
  journal.record(`submission ${room.locator} ${head.id} held`);
1202
- return;
2254
+ return 'done';
1203
2255
  }
1204
2256
  const opensBefore = openSignals;
1205
2257
  const answered = awaitingAnswer(room.locator);
@@ -1215,32 +2267,36 @@ export function composeRuntime(options) {
1215
2267
  progress.complete = false;
1216
2268
  if (continuing())
1217
2269
  progress.reachable = false;
1218
- return;
2270
+ return 'again';
2271
+ }
2272
+ if (signal.aborted) {
2273
+ answered();
2274
+ return 'done';
1219
2275
  }
1220
2276
  if (result.kind !== 'accepted' || !continuing())
1221
2277
  answered();
1222
2278
  if (!continuing())
1223
- return;
2279
+ return 'done';
1224
2280
  if (result.kind === 'transport-uncertainty') {
1225
2281
  progress.complete = false;
1226
2282
  progress.reachable = false;
1227
- return;
2283
+ return 'again';
1228
2284
  }
1229
2285
  if (result.kind === 'room-not-readable') {
1230
2286
  progress.complete = false;
1231
2287
  unreadable.add(room.locator);
1232
2288
  list(room, false);
1233
2289
  emitRooms();
1234
- return;
2290
+ return 'done';
1235
2291
  }
1236
2292
  if (result.kind === 'revoked-authorization') {
1237
2293
  await refuse('revoked-authorization');
1238
- return;
2294
+ return 'done';
1239
2295
  }
1240
2296
  if (result.kind === 'scoped-refusal') {
1241
2297
  await scoped(result, room, local);
1242
2298
  progress.complete = false;
1243
- return;
2299
+ return 'done';
1244
2300
  }
1245
2301
  if (result.kind === 'write-halted') {
1246
2302
  journal.record(`outcome ${room.locator} ${head.id} write-halted`);
@@ -1249,16 +2305,24 @@ export function composeRuntime(options) {
1249
2305
  continue;
1250
2306
  haltedRefusal = true;
1251
2307
  progress.complete = false;
1252
- return;
2308
+ return 'done';
1253
2309
  }
1254
2310
  if (result.kind === 'conflicted' ||
1255
2311
  result.kind === 'rejected' ||
1256
2312
  result.kind === 'expired' ||
1257
2313
  result.kind === 'retryable-refusal') {
2314
+ if (result.kind === 'expired' && lost && result.restored !== true) {
2315
+ journal.record(`outcome ${room.locator} ${head.id} expired`);
2316
+ await discardPending(room, local, signal);
2317
+ return 'done';
2318
+ }
1258
2319
  if (result.kind === 'expired') {
1259
- const withheld = await advance(room, local, authorized);
1260
- if (withheld !== null)
1261
- return withhold(withheld, room, local, progress);
2320
+ await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
2321
+ const withheld = await advance(room, local, authorized, continuing);
2322
+ if (withheld !== null) {
2323
+ await withhold(withheld, room, local, progress);
2324
+ return 'done';
2325
+ }
1262
2326
  }
1263
2327
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
1264
2328
  (result.kind === 'conflicted' && result.roomVersion !== undefined
@@ -1271,8 +2335,7 @@ export function composeRuntime(options) {
1271
2335
  const reset = result.reset;
1272
2336
  progress.complete = false;
1273
2337
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1274
- armTimer();
1275
- return;
2338
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
1276
2339
  }
1277
2340
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
1278
2341
  const left = leaving(head, result);
@@ -1283,37 +2346,82 @@ export function composeRuntime(options) {
1283
2346
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
1284
2347
  return;
1285
2348
  }
1286
- const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
2349
+ const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1287
2350
  leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1288
2351
  (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1289
2352
  ? { ...left, returned: null }
1290
2353
  : left,
1291
2354
  });
1292
- publishChangeSet(set);
2355
+ publishChangeSet(settled());
1293
2356
  moveTo({ ...status, ...attention() });
1294
2357
  });
1295
2358
  if (result.kind === 'expired')
1296
2359
  continue;
1297
- const withheld = await advance(room, local, authorized);
1298
- if (withheld !== null)
1299
- return withhold(withheld, room, local, progress);
2360
+ const withheld = await advance(room, local, authorized, continuing);
2361
+ if (withheld !== null) {
2362
+ await withhold(withheld, room, local, progress);
2363
+ return 'done';
2364
+ }
1300
2365
  continue;
1301
2366
  }
1302
2367
  const kept = await adopted(room, local, head, result).finally(answered);
1303
2368
  if (!kept) {
1304
2369
  progress.complete = false;
1305
- return;
2370
+ return 'done';
1306
2371
  }
1307
2372
  }
2373
+ return 'done';
2374
+ }
2375
+ function pastHorizon(identity) {
2376
+ return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
2377
+ }
2378
+ async function discardPending(room, local, signal) {
2379
+ const { locator } = room;
2380
+ const discarded = await exclusive(async () => {
2381
+ if (fences.has(locator))
2382
+ return false;
2383
+ const before = replicas.get(locator);
2384
+ const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
2385
+ fences.set(locator, { at: discoveries, erased: false });
2386
+ lastFenced.set(locator, ++fencings);
2387
+ try {
2388
+ await local.discard(locator);
2389
+ }
2390
+ finally {
2391
+ fences.delete(locator);
2392
+ }
2393
+ for (const batch of queue)
2394
+ pendings.delete(batch.id);
2395
+ queuesWithoutReplica.delete(locator);
2396
+ const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
2397
+ if (after !== null)
2398
+ replicas.set(locator, after);
2399
+ journal.record(`room discarded ${locator}`);
2400
+ publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
2401
+ moveTo({ ...status, ...attention() });
2402
+ return true;
2403
+ });
2404
+ if (!discarded)
2405
+ return;
2406
+ const progress = { complete: true, reachable: true };
2407
+ await roomTask(locator, async (live) => {
2408
+ await Promise.race([
2409
+ reload(room, local, progress, () => live() && !signal.aborted),
2410
+ abandonedOn(signal),
2411
+ ]);
2412
+ });
2413
+ if (signal.aborted)
2414
+ return;
2415
+ await rest(progress);
1308
2416
  }
1309
2417
  function awaitingAnswer(locator) {
1310
2418
  let release = () => { };
1311
2419
  const awaited = new Promise((resolve) => {
1312
2420
  release = resolve;
1313
2421
  });
1314
- answering.set(locator, awaited);
2422
+ answering.set(locator, { generation, answered: awaited });
1315
2423
  return () => {
1316
- if (answering.get(locator) === awaited)
2424
+ if (answering.get(locator)?.answered === awaited)
1317
2425
  answering.delete(locator);
1318
2426
  release();
1319
2427
  };
@@ -1357,11 +2465,7 @@ export function composeRuntime(options) {
1357
2465
  const read = reclassified.kind === 'deferred'
1358
2466
  ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
1359
2467
  : reclassified;
1360
- await local.write({
1361
- next: record,
1362
- previous: held.confirmed.record,
1363
- queue: held.queue,
1364
- });
2468
+ await local.write({ next: record, previous: held.confirmed.record });
1365
2469
  if (read.kind === 'staged') {
1366
2470
  replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1367
2471
  if (declarationRefusals.delete(room.locator)) {
@@ -1388,6 +2492,11 @@ export function composeRuntime(options) {
1388
2492
  });
1389
2493
  }
1390
2494
  async function scoped(result, room, local) {
2495
+ if ((result.scope === 'content' || result.scope === 'sync') && !result.retryable) {
2496
+ journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
2497
+ await loseRooms([room.locator]);
2498
+ return;
2499
+ }
1391
2500
  scopedRefusals.set(room.locator, {
1392
2501
  code: result.code,
1393
2502
  fields: result.fields,
@@ -1429,24 +2538,27 @@ export function composeRuntime(options) {
1429
2538
  roomVersion: record.roomVersion,
1430
2539
  documents: record.documents,
1431
2540
  revisions: record.revisions,
1432
- attachments: record.attachments,
1433
2541
  },
1434
2542
  ];
1435
2543
  });
1436
2544
  }
1437
- async function synchronize(listed, local, progress, attempt) {
1438
- const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1439
- const ready = listed.filter((_, index) => {
2545
+ async function synchronize(listed, local, progress, attempt, live) {
2546
+ const acquired = listed.filter(acquires);
2547
+ const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
2548
+ if (!live())
2549
+ return;
2550
+ const ready = acquired.filter((_, index) => {
1440
2551
  const outcome = prepared[index];
1441
2552
  return outcome?.status === 'fulfilled' && outcome.value;
1442
2553
  });
1443
2554
  if (attempt === 'opened') {
1444
- options.adapter.roomStream?.hold(heldRooms(ready));
2555
+ options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
1445
2556
  handed = true;
1446
2557
  }
2558
+ discovering?.release();
1447
2559
  const loaded = await Promise.allSettled(ready
1448
2560
  .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1449
- .map((room) => loadRoom(room, local, progress)));
2561
+ .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
1450
2562
  const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1451
2563
  if (failed !== undefined)
1452
2564
  throw failed.reason;
@@ -1468,21 +2580,12 @@ export function composeRuntime(options) {
1468
2580
  await retainModel(local, room, 'collection', modelOf(room.locator));
1469
2581
  if (discoveryPaused || authorization === undefined)
1470
2582
  return false;
1471
- if (room.replica.admission !== 'admitted') {
1472
- if (replicas.has(room.locator))
1473
- progress.complete = false;
1474
- return false;
1475
- }
1476
- return true;
2583
+ return room.replica.admission === 'admitted';
1477
2584
  }
1478
- async function caughtUp(room, local, progress) {
1479
- const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1480
- if (held === undefined || authorization === undefined)
2585
+ async function caughtUp(room, local, progress, live) {
2586
+ if (authorization === undefined || !live() || !behind(room))
1481
2587
  return;
1482
- const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1483
- if (!behind)
1484
- return;
1485
- const withheld = await advance(room, local, authorization);
2588
+ const withheld = await advance(room, local, authorization, live);
1486
2589
  if (withheld !== null)
1487
2590
  await withhold(withheld, room, local, progress);
1488
2591
  }
@@ -1492,39 +2595,46 @@ export function composeRuntime(options) {
1492
2595
  return model.status === 'unavailable' && model.reason === 'revoked';
1493
2596
  });
1494
2597
  }
1495
- async function loadRoom(room, local, progress) {
1496
- if (discoveryPaused || authorization === undefined)
2598
+ async function loadRoom(room, local, progress, live) {
2599
+ if (!live() || authorization === undefined)
1497
2600
  return;
1498
2601
  if (replicas.has(room.locator) && revoked(room.locator)) {
1499
- await reload(room, local, progress);
2602
+ await reload(room, local, progress, live);
1500
2603
  return;
1501
2604
  }
1502
- const withheld = await advance(room, local, authorization);
2605
+ const withheld = await advance(room, local, authorization, live);
1503
2606
  if (withheld !== null) {
1504
2607
  await withhold(withheld, room, local, progress);
1505
2608
  return;
1506
2609
  }
1507
- await caughtUp(room, local, progress);
2610
+ await caughtUp(room, local, progress, live);
1508
2611
  }
1509
- async function reload(room, local, progress) {
1510
- if (discoveryPaused || authorization === undefined)
1511
- return;
1512
- const bootstrapped = await pullBootstrap(authorization, room, local);
1513
- if (!continuing())
1514
- return;
1515
- if (bootstrapped.kind !== 'staged') {
1516
- await withhold(bootstrapped, room, local, progress);
1517
- return;
2612
+ async function reload(room, local, progress, live) {
2613
+ try {
2614
+ if (!live() || authorization === undefined)
2615
+ return;
2616
+ const bootstrapped = await pullBootstrap(authorization, room, local, live);
2617
+ if (!live())
2618
+ return;
2619
+ if (bootstrapped.kind !== 'staged') {
2620
+ await withhold(bootstrapped, room, local, progress);
2621
+ return;
2622
+ }
2623
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
2624
+ await caughtUp(room, local, progress, live);
2625
+ }
2626
+ finally {
2627
+ caughtUpTo(room.locator);
1518
2628
  }
1519
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1520
- await caughtUp(room, local, progress);
1521
2629
  }
1522
- async function discoverRooms(local, progress, attempt) {
2630
+ async function discoverRooms(local, progress, attempt, live) {
1523
2631
  if (closed || authorization === undefined) {
1524
2632
  notifyReady();
1525
2633
  return;
1526
2634
  }
1527
2635
  moveTo(transferring(status));
2636
+ discoveries += 1;
2637
+ const started = discoveries;
1528
2638
  let result;
1529
2639
  try {
1530
2640
  result = await options.adapter.discover(authorization);
@@ -1532,7 +2642,8 @@ export function composeRuntime(options) {
1532
2642
  catch {
1533
2643
  result = { kind: 'transport-uncertainty' };
1534
2644
  }
1535
- if (closed) {
2645
+ if (!live()) {
2646
+ progress.complete = false;
1536
2647
  notifyReady();
1537
2648
  return;
1538
2649
  }
@@ -1590,6 +2701,7 @@ export function composeRuntime(options) {
1590
2701
  }
1591
2702
  const previousCapabilities = capabilities;
1592
2703
  capabilities = result.capabilities;
2704
+ outcomeRetention = result.retention.outcome;
1593
2705
  notifyReady();
1594
2706
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1595
2707
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
@@ -1602,8 +2714,7 @@ export function composeRuntime(options) {
1602
2714
  member = signedIn;
1603
2715
  emitMember();
1604
2716
  }
1605
- await applyDiscovery(result.rooms, local);
1606
- await synchronize(result.rooms, local, progress, attempt);
2717
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
1607
2718
  }
1608
2719
  async function ensureAuthorization() {
1609
2720
  if (authorization !== undefined)
@@ -1617,20 +2728,50 @@ export function composeRuntime(options) {
1617
2728
  throw error;
1618
2729
  }
1619
2730
  }
1620
- function task(work) {
2731
+ async function admittedRead(work) {
2732
+ if (roomReads.generation !== generation) {
2733
+ roomReads = { generation, free: ROOM_READS, waiting: [] };
2734
+ }
2735
+ const reads = roomReads;
2736
+ while (reads.free === 0)
2737
+ await new Promise((admit) => reads.waiting.push(admit));
2738
+ reads.free -= 1;
2739
+ try {
2740
+ await work();
2741
+ }
2742
+ finally {
2743
+ reads.free += 1;
2744
+ reads.waiting.shift()?.();
2745
+ }
2746
+ }
2747
+ function roomTask(locator, work) {
2748
+ return task(roomLane(locator), (live) => admittedRead(() => work(live)));
2749
+ }
2750
+ function task(lane, work) {
1621
2751
  if (stopping)
1622
- return chain;
1623
- chain = chain.then(work, work).catch(() => undefined);
1624
- return chain;
2752
+ return Promise.resolve();
2753
+ const held = lanes.get(lane);
2754
+ const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
2755
+ const turn = current.generation;
2756
+ const live = () => turn === generation && continuing();
2757
+ const run = current.chain.then(() => work(live)).catch(() => undefined);
2758
+ current.chain = run;
2759
+ lanes.set(lane, current);
2760
+ working.add(run);
2761
+ void run.finally(() => working.delete(run));
2762
+ return run;
1625
2763
  }
1626
2764
  function sync(attempt) {
1627
- return task(async () => {
1628
- if (closed || stopping || discoveryPaused) {
2765
+ return task(OPENING_LANE, async (live) => {
2766
+ if (!live()) {
1629
2767
  notifyReady();
1630
2768
  return;
1631
2769
  }
1632
2770
  const progress = { complete: true, reachable: true };
1633
- underway = attempt === 'opened' ? progress : null;
2771
+ const gate = discover();
2772
+ if (attempt === 'opened')
2773
+ opened = generation;
2774
+ underway = attempt === 'opened' ? { generation, progress } : null;
1634
2775
  if (attempt === 'opened')
1635
2776
  streamed = progress;
1636
2777
  complete = false;
@@ -1639,12 +2780,12 @@ export function composeRuntime(options) {
1639
2780
  handed = false;
1640
2781
  try {
1641
2782
  const local = await openLocal();
1642
- if (closed)
2783
+ if (!live())
1643
2784
  return;
1644
2785
  await ensureAuthorization();
1645
- if (closed)
2786
+ if (!live())
1646
2787
  return;
1647
- await discoverRooms(local, progress, attempt);
2788
+ await discoverRooms(local, progress, attempt, live);
1648
2789
  }
1649
2790
  catch {
1650
2791
  progress.complete = false;
@@ -1652,23 +2793,61 @@ export function composeRuntime(options) {
1652
2793
  progress.reachable = false;
1653
2794
  }
1654
2795
  finally {
2796
+ gate.release();
1655
2797
  notifyReady();
1656
2798
  }
2799
+ if (!live())
2800
+ return;
1657
2801
  if (attempt !== 'opened') {
1658
- await finishOpening(progress);
2802
+ await finishOpening(progress, live);
1659
2803
  return;
1660
2804
  }
1661
2805
  if (handed || closed || progress.reachable)
1662
2806
  return;
1663
2807
  underway = null;
2808
+ opened = null;
1664
2809
  journal.record('connection reopen');
1665
- await rest(progress);
2810
+ await rest(progress, live);
1666
2811
  options.adapter.roomStream?.reopen();
1667
2812
  });
1668
2813
  }
1669
- function closedSocket(revoked) {
2814
+ function rediscover() {
2815
+ return task(OPENING_LANE, async (live) => {
2816
+ if (!live() || !socketOpen)
2817
+ return;
2818
+ const progress = { complete: true, reachable: true };
2819
+ const gate = discover();
2820
+ moveTo(transferring(status));
2821
+ try {
2822
+ const local = await openLocal();
2823
+ if (!live())
2824
+ return;
2825
+ await ensureAuthorization();
2826
+ if (!live())
2827
+ return;
2828
+ await discoverRooms(local, progress, 'authorized', live);
2829
+ }
2830
+ catch {
2831
+ progress.complete = false;
2832
+ }
2833
+ finally {
2834
+ gate.release();
2835
+ notifyReady();
2836
+ }
2837
+ if (!live())
2838
+ return;
2839
+ if (!progress.complete)
2840
+ complete = false;
2841
+ await rest(progress, live);
2842
+ });
2843
+ }
2844
+ function closedSocket(revoked, lost) {
1670
2845
  const opened = socketOpen;
1671
2846
  socketOpen = false;
2847
+ if (lost) {
2848
+ reads.lost();
2849
+ conversations.lost();
2850
+ }
1672
2851
  if (revoked) {
1673
2852
  void refuse('revoked-authorization');
1674
2853
  return;
@@ -1683,24 +2862,41 @@ export function composeRuntime(options) {
1683
2862
  });
1684
2863
  }
1685
2864
  function applyOpening(expired) {
1686
- return task(async () => {
1687
- const progress = underway;
2865
+ const progress = underway?.generation === generation ? underway.progress : null;
2866
+ const cycle = progress ?? { complete: true, reachable: true };
2867
+ const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
2868
+ if (opened === generation)
2869
+ opened = null;
2870
+ return task(OPENING_LANE, async (live) => {
1688
2871
  underway = null;
1689
- if (closed || stopping)
2872
+ if (!live())
1690
2873
  return;
1691
2874
  if (progress === null) {
1692
- await reloadExpired(expired);
2875
+ await replacing;
2876
+ await rest(cycle, live);
1693
2877
  return;
1694
2878
  }
2879
+ await roomsCarried(expired);
2880
+ if (!live())
2881
+ return;
2882
+ const replaced = new Set(owed);
2883
+ owed.clear();
1695
2884
  try {
1696
2885
  const local = await openLocal();
1697
2886
  const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1698
- const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1699
- const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
2887
+ const reloading = held.filter((room) => !expired.includes(room.locator) &&
2888
+ (replaced.has(room.locator) || revoked(room.locator)));
2889
+ for (const room of reloading)
2890
+ catching.add(room.locator);
2891
+ readSide.recover();
2892
+ const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
2893
+ const reloaded = [...(await reloads), ...(await replacing)];
1700
2894
  if (progress.reachable) {
1701
- for (const room of held)
1702
- if (!reloading.includes(room))
2895
+ for (const room of held) {
2896
+ if (!expired.includes(room.locator) && !reloading.includes(room)) {
1703
2897
  await confirmModels(room, local);
2898
+ }
2899
+ }
1704
2900
  }
1705
2901
  const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1706
2902
  if (failed !== undefined)
@@ -1710,21 +2906,21 @@ export function composeRuntime(options) {
1710
2906
  progress.complete = false;
1711
2907
  progress.reachable = false;
1712
2908
  }
1713
- await finishOpening(progress);
2909
+ if (live())
2910
+ await finishOpening(progress, live);
1714
2911
  });
1715
2912
  }
1716
- async function reloadExpired(expired) {
1717
- const progress = { complete: true, reachable: true };
2913
+ async function reloadNow(replaced, progress) {
1718
2914
  try {
1719
2915
  const local = await openLocal();
1720
- await Promise.allSettled([...discovered.values()]
1721
- .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1722
- .map((room) => reload(room, local, progress)));
2916
+ return await Promise.allSettled([...discovered.values()]
2917
+ .filter((room) => replicas.has(room.locator) && replaced(room))
2918
+ .map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
1723
2919
  }
1724
2920
  catch {
1725
2921
  progress.reachable = false;
2922
+ return [];
1726
2923
  }
1727
- await rest(progress);
1728
2924
  }
1729
2925
  async function confirmModels(room, local) {
1730
2926
  for (const attached of ['collection', 'typed-selection']) {
@@ -1737,38 +2933,94 @@ export function composeRuntime(options) {
1737
2933
  function submitLater(locator) {
1738
2934
  if (localSaves)
1739
2935
  return;
1740
- const running = submitting.get(locator);
1741
- if (running !== undefined) {
1742
- running.again = true;
2936
+ void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
2937
+ }
2938
+ async function submitPass(locator, signal) {
2939
+ const room = discovered.get(locator);
2940
+ if (closed || stopping || discoveryPaused || room === undefined)
2941
+ return 'done';
2942
+ const progress = { complete: true, reachable: true };
2943
+ let next;
2944
+ try {
2945
+ await ensureAuthorization();
2946
+ if (authorization === undefined)
2947
+ return 'done';
2948
+ next = await submitRoom(room, await openLocal(), authorization, progress, signal);
2949
+ }
2950
+ catch {
2951
+ progress.reachable = false;
2952
+ next = 'again';
2953
+ }
2954
+ if (signal.aborted)
2955
+ return 'done';
2956
+ await rest(progress);
2957
+ return next === 'again' && !socketOpen ? 'done' : next;
2958
+ }
2959
+ function repairLater(locator, when) {
2960
+ if (!socketOpen || repairing.has(locator))
1743
2961
  return;
2962
+ void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
2963
+ }
2964
+ async function repairPass(locator, signal) {
2965
+ const room = discovered.get(locator);
2966
+ if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
2967
+ return 'done';
2968
+ if (room === undefined || room.replica.admission !== 'admitted')
2969
+ return 'done';
2970
+ const progress = { complete: true, reachable: true };
2971
+ repairing.add(locator);
2972
+ try {
2973
+ const local = await openLocal();
2974
+ await roomTask(locator, async (live) => {
2975
+ const current = () => live() && !signal.aborted;
2976
+ if (!current() || authorization === undefined)
2977
+ return;
2978
+ const withheld = await Promise.race([
2979
+ advance(room, local, authorization, current),
2980
+ abandonedOn(signal),
2981
+ ]);
2982
+ if (withheld !== null && current())
2983
+ await withhold(withheld, room, local, progress);
2984
+ });
1744
2985
  }
1745
- const run = { again: false, done: Promise.resolve() };
1746
- submitting.set(locator, run);
1747
- run.done = (async () => {
1748
- try {
1749
- do {
1750
- run.again = false;
1751
- const room = discovered.get(locator);
1752
- if (closed || stopping || discoveryPaused || room === undefined)
1753
- return;
1754
- const progress = { complete: true, reachable: true };
1755
- try {
1756
- await ensureAuthorization();
1757
- if (authorization === undefined)
1758
- return;
1759
- await submitRoom(room, await openLocal(), authorization, progress);
1760
- }
1761
- catch {
1762
- progress.reachable = false;
1763
- }
1764
- armTimer();
1765
- await rest(progress);
1766
- } while (run.again);
1767
- }
1768
- finally {
1769
- submitting.delete(locator);
1770
- }
1771
- })();
2986
+ catch {
2987
+ stale.add(locator);
2988
+ progress.reachable = false;
2989
+ }
2990
+ finally {
2991
+ repairing.delete(locator);
2992
+ }
2993
+ if (signal.aborted)
2994
+ return 'done';
2995
+ await rest(progress);
2996
+ if (!stale.has(locator)) {
2997
+ if ((replicas.get(locator)?.queue.length ?? 0) > 0)
2998
+ submitLater(locator);
2999
+ return 'done';
3000
+ }
3001
+ if (!repairable.has(locator))
3002
+ return 'done';
3003
+ const reset = repairable.get(locator);
3004
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
3005
+ }
3006
+ function resendLater() {
3007
+ if (!socketOpen)
3008
+ return;
3009
+ void recovery.attempt('resend', resendPass, { again: true });
3010
+ }
3011
+ async function resendPass(signal) {
3012
+ if (!socketOpen || closed || stopping || discoveryPaused)
3013
+ return 'done';
3014
+ const reading = { complete: true, reachable: true };
3015
+ try {
3016
+ await conversations.resend(reading);
3017
+ }
3018
+ catch {
3019
+ reading.reachable = false;
3020
+ }
3021
+ if (signal.aborted)
3022
+ return 'done';
3023
+ return reading.reachable && reading.complete ? 'done' : 'again';
1772
3024
  }
1773
3025
  function submitAll() {
1774
3026
  for (const [locator, replica] of replicas) {
@@ -1776,31 +3028,44 @@ export function composeRuntime(options) {
1776
3028
  submitLater(locator);
1777
3029
  }
1778
3030
  }
1779
- async function finishOpening(progress) {
3031
+ async function finishOpening(progress, live) {
1780
3032
  try {
1781
- if (closed || discoveryPaused) {
3033
+ if (!live()) {
1782
3034
  progress.complete = false;
1783
3035
  return;
1784
3036
  }
3037
+ reads.applied();
1785
3038
  submitAll();
3039
+ for (const locator of stale)
3040
+ if (repairable.has(locator))
3041
+ repairLater(locator, {});
1786
3042
  await conversations.reconcile();
1787
3043
  if (status.providerUnreachable)
1788
3044
  conversations.reconnected();
1789
3045
  const reading = { complete: true, reachable: true };
1790
- await conversations.catchUp(reading);
1791
- if (!reading.reachable)
3046
+ if (demand.conversations)
3047
+ await conversations.catchUp(reading);
3048
+ if (progress.reachable && reading.reachable)
3049
+ await conversations.resend(reading);
3050
+ if (progress.reachable)
3051
+ void recovery.attempt('calls', resendCalls);
3052
+ if (!reading.reachable) {
1792
3053
  progress.reachable = false;
3054
+ resendLater();
3055
+ }
1793
3056
  }
1794
3057
  catch {
1795
3058
  progress.complete = false;
1796
3059
  }
1797
3060
  finally {
1798
- complete = progress.complete;
1799
- await rest(progress);
3061
+ if (live()) {
3062
+ complete = progress.complete;
3063
+ await rest(progress, live);
3064
+ }
1800
3065
  }
1801
3066
  }
1802
- async function rest(progress) {
1803
- if (closed)
3067
+ async function rest(progress, live = () => !closed) {
3068
+ if (!live())
1804
3069
  return;
1805
3070
  let lastCompleted = status.lastCompleted;
1806
3071
  let synced = complete && stale.size === 0 && !pending();
@@ -1808,6 +3073,8 @@ export function composeRuntime(options) {
1808
3073
  const completedAt = new Date().toISOString();
1809
3074
  try {
1810
3075
  await (await openLocal()).complete(completedAt);
3076
+ if (!live())
3077
+ return;
1811
3078
  recorded = true;
1812
3079
  lastCompleted = completedAt;
1813
3080
  }
@@ -1822,40 +3089,68 @@ export function composeRuntime(options) {
1822
3089
  ...attention(),
1823
3090
  });
1824
3091
  }
3092
+ function discover() {
3093
+ let release = () => { };
3094
+ const done = new Promise((resolve) => {
3095
+ release = resolve;
3096
+ });
3097
+ discovering = { generation, done, release };
3098
+ return discovering;
3099
+ }
1825
3100
  function applyRoom(locator, roomVersion) {
1826
3101
  announced.set(locator, roomVersion);
1827
- return task(async () => {
1828
- await answering.get(locator);
1829
- if (closed || stopping || discoveryPaused || authorization === undefined)
3102
+ return task(roomLane(locator), async (live) => {
3103
+ if (discovering?.generation === generation)
3104
+ await discovering.done;
3105
+ const saving = answering.get(locator);
3106
+ if (saving?.generation === generation)
3107
+ await saving.answered;
3108
+ if (!live() || authorization === undefined)
1830
3109
  return;
1831
3110
  const room = discovered.get(locator);
1832
3111
  const held = replicas.get(locator);
1833
- if (room === undefined || room.replica.admission !== 'admitted')
3112
+ if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
1834
3113
  return;
1835
3114
  if (held !== undefined &&
1836
3115
  (held.confirmed.record.replica.selection !== room.replica.selection ||
1837
3116
  options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
1838
3117
  return;
1839
3118
  }
1840
- const during = underway;
3119
+ const during = underway?.generation === generation ? underway.progress : null;
1841
3120
  const progress = during ?? { complete: true, reachable: true };
1842
3121
  if (during === null)
1843
3122
  moveTo(transferring(status));
1844
3123
  try {
1845
3124
  const local = await openLocal();
1846
- const withheld = await advance(room, local, authorization);
1847
- if (withheld !== null)
1848
- await withhold(withheld, room, local, progress);
3125
+ await admittedRead(async () => {
3126
+ if (!live() || authorization === undefined)
3127
+ return;
3128
+ const withheld = await advance(room, local, authorization, live);
3129
+ if (withheld !== null)
3130
+ await withhold(withheld, room, local, progress);
3131
+ });
1849
3132
  }
1850
3133
  catch {
1851
3134
  stale.add(locator);
1852
3135
  }
1853
3136
  finally {
1854
3137
  if (during === null)
1855
- await rest(progress);
3138
+ await rest(progress, live);
1856
3139
  }
1857
3140
  });
1858
3141
  }
3142
+ function roomsCarried(replacing) {
3143
+ const replaced = new Set(replacing.map(roomLane));
3144
+ return Promise.all([...lanes]
3145
+ .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
3146
+ .map(([, held]) => held.chain));
3147
+ }
3148
+ const receipts = {
3149
+ heads: async () => (await openLocal()).receivedHeads(),
3150
+ parts: async (key, count) => (await openLocal()).receivedParts(key, count),
3151
+ keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
3152
+ drop: async (keys) => (await openLocal()).dropReceived(keys),
3153
+ };
1859
3154
  function observeStream() {
1860
3155
  const stream = options.adapter.roomStream;
1861
3156
  if (stream === undefined || withdrawStream !== undefined)
@@ -1865,15 +3160,21 @@ export function composeRuntime(options) {
1865
3160
  return;
1866
3161
  if (event.kind === 'opened') {
1867
3162
  socketOpen = true;
3163
+ generation += 1;
3164
+ reads.opened();
3165
+ conversations.reopened();
3166
+ recovery.resume();
1868
3167
  void sync('opened');
1869
3168
  }
1870
3169
  else if (event.kind === 'closed')
1871
- closedSocket(event.revoked);
3170
+ closedSocket(event.revoked, event.lost);
1872
3171
  else if (event.kind === 'opening')
1873
3172
  void applyOpening(event.expired);
3173
+ else if (event.kind === 'authority-changed')
3174
+ void rediscover();
1874
3175
  else
1875
3176
  void applyRoom(event.locator, event.roomVersion);
1876
- });
3177
+ }, receipts);
1877
3178
  }
1878
3179
  function writesHeld() {
1879
3180
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -1888,6 +3189,7 @@ export function composeRuntime(options) {
1888
3189
  haltedRefusal = false;
1889
3190
  if (mode !== serviceMode) {
1890
3191
  serviceMode = mode;
3192
+ queueReadiness();
1891
3193
  journal.record(`service mode ${mode}`);
1892
3194
  for (const listener of serviceModeListeners)
1893
3195
  listener(mode);
@@ -1900,6 +3202,8 @@ export function composeRuntime(options) {
1900
3202
  if (stream === undefined || withdrawConversations !== undefined)
1901
3203
  return;
1902
3204
  withdrawConversations = stream.observe((event) => {
3205
+ if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
3206
+ queueReadiness();
1903
3207
  if (conversations.receive(event)) {
1904
3208
  void ready.then(scheduleCurrency);
1905
3209
  return;
@@ -1911,17 +3215,18 @@ export function composeRuntime(options) {
1911
3215
  });
1912
3216
  }
1913
3217
  function scheduleCurrency() {
1914
- if (currencyQueued || closed || stopping)
3218
+ if (currencyQueued === generation || closed || stopping)
1915
3219
  return;
1916
- currencyQueued = true;
1917
- void task(runCurrency);
3220
+ currencyQueued = generation;
3221
+ void task(CONVERSATION_LANE, runCurrency);
1918
3222
  }
1919
- async function runCurrency() {
1920
- currencyQueued = false;
1921
- if (closed || stopping || discoveryPaused)
3223
+ async function runCurrency(live) {
3224
+ if (currencyQueued === generation)
3225
+ currencyQueued = null;
3226
+ if (!live())
1922
3227
  return;
1923
3228
  await conversations.reconcile();
1924
- if (conversations.current()) {
3229
+ if (conversations.current() || !demand.conversations) {
1925
3230
  show();
1926
3231
  return;
1927
3232
  }
@@ -1933,19 +3238,24 @@ export function composeRuntime(options) {
1933
3238
  catch {
1934
3239
  progress.reachable = false;
1935
3240
  }
1936
- await rest(progress);
3241
+ await rest(progress, live);
1937
3242
  }
1938
3243
  function close() {
1939
3244
  if (closed)
1940
3245
  return;
1941
3246
  closed = true;
3247
+ if (narrowing !== null)
3248
+ clearTimeout(narrowing);
3249
+ if (rechecking !== null)
3250
+ clearInterval(rechecking);
3251
+ readinessWatches.clear();
1942
3252
  withdrawStream?.();
1943
3253
  withdrawStream = undefined;
1944
3254
  withdrawConversations?.();
1945
3255
  withdrawConversations = undefined;
1946
3256
  withdrawServiceMode?.();
1947
3257
  conversations.close();
1948
- stopTimer();
3258
+ void recovery.retire();
1949
3259
  store?.close();
1950
3260
  void opening?.then((opened) => {
1951
3261
  opened.close();
@@ -1963,9 +3273,8 @@ export function composeRuntime(options) {
1963
3273
  }
1964
3274
  async function stop() {
1965
3275
  stopping = true;
1966
- stopTimer();
1967
- await chain.catch(() => undefined);
1968
- await Promise.all([...submitting.values()].map((run) => run.done));
3276
+ await recovery.retire();
3277
+ await Promise.all(working);
1969
3278
  await mutations;
1970
3279
  close();
1971
3280
  }
@@ -1984,20 +3293,111 @@ export function composeRuntime(options) {
1984
3293
  if (options.adapter.roomStream === undefined)
1985
3294
  void sync('unstreamed');
1986
3295
  function planned(input, reach) {
1987
- return planOf(input, roomFacts(reach), planReach(reach));
3296
+ const plan = planOf(input, roomFacts(reach), planReach(reach));
3297
+ const named = plan.paths !== null &&
3298
+ plan.rooms !== null &&
3299
+ plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
3300
+ return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
1988
3301
  }
1989
3302
  function watchPlan(plan, deliver) {
1990
- materialize(reachedRooms(plan));
1991
- return readSide.watch(plan, identityOf(plan), deliver);
3303
+ let seeded = false;
3304
+ let pending = true;
3305
+ const shown = () => {
3306
+ const seed = seeds.get(plan.key);
3307
+ if (seed === undefined || seeded || !pending)
3308
+ return;
3309
+ seeded = true;
3310
+ deliver(seededResult(seed));
3311
+ };
3312
+ const watchers = seedWatchers.get(plan.key) ?? new Set();
3313
+ seedWatchers.set(plan.key, watchers);
3314
+ watchers.add(shown);
3315
+ const stop = readSide.watch(plan, identityOf(plan), () => {
3316
+ const cover = covered(plan);
3317
+ awaitPreparation(cover);
3318
+ return seedBehind(plan, cover);
3319
+ }, (message) => {
3320
+ const answered = message;
3321
+ pending = whole(answered) && 'pending' in answered.result;
3322
+ if (pending && seeds.has(plan.key)) {
3323
+ shown();
3324
+ return;
3325
+ }
3326
+ seeded = false;
3327
+ deliver(message);
3328
+ });
3329
+ return () => {
3330
+ watchers.delete(shown);
3331
+ if (watchers.size === 0)
3332
+ seedWatchers.delete(plan.key);
3333
+ stop();
3334
+ };
1992
3335
  }
1993
3336
  async function searched(input, reach) {
1994
- const plan = searchPlanOf(input);
1995
- const selected = plan.documents === undefined && reach === null
1996
- ? null
1997
- : (await rowsQuery(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
1998
- if (selected === null)
1999
- materialize(null);
2000
- return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
3337
+ const asked = searchPlanOf(input);
3338
+ const plan = { ...asked, limit: capped(asked.limit) };
3339
+ const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
3340
+ for (;;) {
3341
+ const cover = covered(filter);
3342
+ awaitPreparation(cover);
3343
+ const pending = 'pending' in cover ? [...cover.pending] : [];
3344
+ const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3345
+ const held = covered({
3346
+ ...filter,
3347
+ rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
3348
+ });
3349
+ const covering = 'pending' in held ? [] : held.coverage.rooms;
3350
+ const unindexed = covering
3351
+ .filter(({ room }) => !searchIndex.rooms().has(room))
3352
+ .map(({ room }) => room);
3353
+ for (const room of unindexed)
3354
+ pending.push({ room, awaits: 'index' });
3355
+ const listed = conversations.searchable();
3356
+ const named = reach !== null && !reach.conversations
3357
+ ? []
3358
+ : (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
3359
+ const waiting = named.filter((one) => !searchIndex.conversations().has(one));
3360
+ for (const conversation of waiting)
3361
+ pending.push({ conversation, awaits: 'index' });
3362
+ if (unindexed.length > 0 || waiting.length > 0)
3363
+ index(unindexed, waiting);
3364
+ const complete = pending.length === 0 && !('pending' in cover);
3365
+ const answering = complete
3366
+ ? filter
3367
+ : {
3368
+ ...filter,
3369
+ rooms: covering
3370
+ .filter(({ room }) => searchIndex.rooms().has(room))
3371
+ .map(({ room }) => room),
3372
+ };
3373
+ const answered = covered(answering);
3374
+ if ('pending' in answered)
3375
+ continue;
3376
+ const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
3377
+ const seen = searchIndex.changes();
3378
+ await searchIndex.caughtUp();
3379
+ const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
3380
+ if (selected !== null && 'pending' in selected)
3381
+ continue;
3382
+ if (searchIndex.changes() !== seen ||
3383
+ JSON.stringify(covered(answering)) !== JSON.stringify(answered))
3384
+ continue;
3385
+ const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms === null ? null : new Set(answering.rooms), plan.collection
3386
+ ? new Map(answered.coverage.rooms.map(({ room }) => [
3387
+ room,
3388
+ replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
3389
+ ]))
3390
+ : null);
3391
+ const coverage = {
3392
+ rooms: answered.coverage.rooms,
3393
+ conversations: searchedConversations,
3394
+ index: searchIndex.generation(),
3395
+ overlay: answered.coverage.overlay,
3396
+ };
3397
+ return complete
3398
+ ? { ...hits, coverage }
3399
+ : { pending, documents: hits.documents, messages: hits.messages, coverage };
3400
+ }
2001
3401
  }
2002
3402
  const watch = (topic, input, deliver) => {
2003
3403
  if (topic === 'room-drafts') {
@@ -2022,6 +3422,8 @@ export function composeRuntime(options) {
2022
3422
  }
2023
3423
  if (topic === 'conversations')
2024
3424
  return conversations.watchList(deliver);
3425
+ if (topic === 'agents')
3426
+ return conversations.watchAgents(deliver);
2025
3427
  if (topic === 'presentation') {
2026
3428
  const listener = {
2027
3429
  app: input[0],
@@ -2041,6 +3443,9 @@ export function composeRuntime(options) {
2041
3443
  presentationListeners.delete(listener);
2042
3444
  };
2043
3445
  }
3446
+ if (topic === 'readiness') {
3447
+ return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
3448
+ }
2044
3449
  if (topic === 'rooms') {
2045
3450
  roomListeners.add(deliver);
2046
3451
  deliver(listed());
@@ -2135,11 +3540,14 @@ export function composeRuntime(options) {
2135
3540
  if ((current?.revision ?? null) !== revision) {
2136
3541
  return staleWrite(current === undefined ? null : roomDraftOf(current));
2137
3542
  }
3543
+ const file = parts.file === undefined
3544
+ ? undefined
3545
+ : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
2138
3546
  const record = {
2139
3547
  locator: room,
2140
3548
  path,
2141
3549
  revision: roomDraftRevision(),
2142
- parts: held(current?.parts, parts),
3550
+ parts: held(current?.parts, parts, file),
2143
3551
  returned: current?.returned ?? null,
2144
3552
  };
2145
3553
  await local.keepRoomDraft(record);
@@ -2173,23 +3581,31 @@ export function composeRuntime(options) {
2173
3581
  });
2174
3582
  },
2175
3583
  write(app, value) {
2176
- return exclusive(async () => {
2177
- const local = await openLocal();
2178
- const stored = structuredClone(value);
2179
- await local.present({ app, value: stored });
2180
- presentations.set(app, stored);
2181
- publishChangeSet(changeSet('', null, null, { presentation: app }));
2182
- });
3584
+ return keepPresentation(app, () => structuredClone(value));
3585
+ },
3586
+ edit(app, members) {
3587
+ return keepPresentation(app, (held) => Object.fromEntries([
3588
+ ...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(members, member)),
3589
+ ...Object.entries(structuredClone(members)).filter(([, value]) => value !== null),
3590
+ ]));
2183
3591
  },
2184
3592
  },
2185
3593
  save(room, named, writtenBy) {
2186
3594
  return exclusive(() => saveRoomDrafts(room, named, writtenBy));
2187
3595
  },
3596
+ saveParts(room, parts, writtenBy) {
3597
+ return exclusive(() => saveParts(room, parts, writtenBy));
3598
+ },
2188
3599
  history(locator, path, query) {
2189
3600
  return readHistory(locator, path, query);
2190
3601
  },
3602
+ binaryFile: {
3603
+ read: readBinaryPath,
3604
+ link: linkBinaryPath,
3605
+ readReference: readBinaryReference,
3606
+ },
2191
3607
  call(name, input) {
2192
- return invokeAction(name, input);
3608
+ return recoveredAction(name, input);
2193
3609
  },
2194
3610
  reached(locator, roomVersion, path) {
2195
3611
  return path === undefined
@@ -2200,30 +3616,55 @@ export function composeRuntime(options) {
2200
3616
  return journal.export();
2201
3617
  },
2202
3618
  persistence() {
2203
- return options.shell.persistentStorage;
3619
+ return persistence === 'granted';
3620
+ },
3621
+ async retry() {
3622
+ journal.record('retry');
3623
+ unindexable.clear();
3624
+ queueReadiness();
3625
+ resume();
2204
3626
  },
2205
3627
  observe: observing(watch),
2206
3628
  async reauthorize() {
2207
3629
  if (refusal === null || !refusal.reauthorizationOffered)
2208
3630
  return;
2209
3631
  journal.record('connection reauthorize');
2210
- await chain.catch(() => undefined);
3632
+ await Promise.all([...lanes.values()]
3633
+ .filter((held) => held.generation === generation)
3634
+ .map((held) => held.chain));
2211
3635
  await mutations;
2212
3636
  authorization = undefined;
2213
3637
  discoveryPaused = false;
2214
3638
  if (options.adapter.roomStream === undefined)
2215
3639
  await sync('unstreamed');
2216
3640
  else
2217
- await task(ensureAuthorization);
3641
+ await task(OPENING_LANE, ensureAuthorization);
2218
3642
  },
2219
3643
  };
2220
3644
  const reachable = {
2221
3645
  md,
2222
3646
  watch,
3647
+ referrerScope: (referrer) => options.adapter.referrerScope(referrer),
2223
3648
  plan: planned,
2224
3649
  query: (plan) => rowsQuery(plan),
2225
3650
  watchPlan,
2226
3651
  search: searched,
3652
+ watchReadiness,
3653
+ };
3654
+ return {
3655
+ md,
3656
+ seed,
3657
+ stop,
3658
+ lose: loseRooms,
3659
+ watch,
3660
+ acquire,
3661
+ persisted(answer) {
3662
+ if (answer === persistence)
3663
+ return;
3664
+ persistence = answer;
3665
+ journal.record(`persistence ${answer}`);
3666
+ queueReadiness();
3667
+ },
3668
+ within: (reach, port) => narrowed(reachable, reach, port),
2227
3669
  };
2228
- return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2229
3670
  }