@agentshouse/mdruntime 0.1.0-cebcd51f70ed1 → 0.1.0-cedb03eaa48b6

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