@agentshouse/mdruntime 0.1.0-cacb4c3f6734e → 0.1.0-cb2e390ee7b89

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