@agentshouse/mdruntime 0.1.0-c7bd7ca3faee4 → 0.1.0-c7f51acc2629c

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