@agentshouse/mdruntime 0.1.0-c99e5271060cd → 0.1.0-c9b1e59e5baeb

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