@agentshouse/mdruntime 0.1.0-cc5745cd9af96 → 0.1.0-ccbe92df1c4f6

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 (188) hide show
  1. package/AGENTS.md +96 -78
  2. package/README.md +9 -8
  3. package/dist/adapter.d.ts +40 -32
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/attached-model.d.ts +4 -1
  6. package/dist/attached-model.d.ts.map +1 -1
  7. package/dist/attached-model.js +13 -3
  8. package/dist/binary.d.ts +0 -5
  9. package/dist/binary.d.ts.map +1 -1
  10. package/dist/binary.js +1 -1
  11. package/dist/bootstrap.d.ts +24 -6
  12. package/dist/bootstrap.d.ts.map +1 -1
  13. package/dist/bootstrap.js +148 -82
  14. package/dist/chain.d.ts +11 -0
  15. package/dist/chain.d.ts.map +1 -0
  16. package/dist/chain.js +64 -0
  17. package/dist/change-set.d.ts +6 -16
  18. package/dist/change-set.d.ts.map +1 -1
  19. package/dist/change-set.js +11 -94
  20. package/dist/change.d.ts +31 -1
  21. package/dist/change.d.ts.map +1 -1
  22. package/dist/change.js +10 -1
  23. package/dist/chat/house.d.ts +25 -0
  24. package/dist/chat/house.d.ts.map +1 -0
  25. package/dist/chat/house.js +38 -0
  26. package/dist/chat/input.d.ts +3 -1
  27. package/dist/chat/input.d.ts.map +1 -1
  28. package/dist/chat/input.js +11 -18
  29. package/dist/chat/list.d.ts.map +1 -1
  30. package/dist/chat/list.js +1 -1
  31. package/dist/chat/markdown.d.ts +1 -2
  32. package/dist/chat/markdown.d.ts.map +1 -1
  33. package/dist/chat/markdown.js +2 -19
  34. package/dist/chat/message.d.ts.map +1 -1
  35. package/dist/chat/message.js +63 -11
  36. package/dist/chat/messages.d.ts +12 -1
  37. package/dist/chat/messages.d.ts.map +1 -1
  38. package/dist/chat/messages.js +36 -13
  39. package/dist/chat/name.d.ts +1 -0
  40. package/dist/chat/name.d.ts.map +1 -1
  41. package/dist/chat/name.js +1 -1
  42. package/dist/chat/opened.d.ts +1 -0
  43. package/dist/chat/opened.d.ts.map +1 -1
  44. package/dist/chat/opened.js +2 -0
  45. package/dist/chat/overlay.d.ts.map +1 -1
  46. package/dist/chat/overlay.js +1 -5
  47. package/dist/chat/parts.d.ts +1 -0
  48. package/dist/chat/parts.d.ts.map +1 -1
  49. package/dist/chat/parts.js +3 -2
  50. package/dist/chat/provider.d.ts +7 -1
  51. package/dist/chat/provider.d.ts.map +1 -1
  52. package/dist/chat/provider.js +75 -78
  53. package/dist/chat/ready.d.ts +4 -1
  54. package/dist/chat/ready.d.ts.map +1 -1
  55. package/dist/chat/ready.js +7 -5
  56. package/dist/chat/room-file.d.ts +8 -0
  57. package/dist/chat/room-file.d.ts.map +1 -0
  58. package/dist/chat/room-file.js +72 -0
  59. package/dist/chat/style.d.ts.map +1 -1
  60. package/dist/chat/style.js +5 -0
  61. package/dist/chat.d.ts +1 -0
  62. package/dist/chat.d.ts.map +1 -1
  63. package/dist/client.d.ts +6 -3
  64. package/dist/client.d.ts.map +1 -1
  65. package/dist/client.js +28 -2
  66. package/dist/collection.d.ts +6 -20
  67. package/dist/collection.d.ts.map +1 -1
  68. package/dist/collection.js +30 -352
  69. package/dist/compile.d.ts +16 -0
  70. package/dist/compile.d.ts.map +1 -0
  71. package/dist/compile.js +721 -0
  72. package/dist/connection.d.ts +13 -8
  73. package/dist/connection.d.ts.map +1 -1
  74. package/dist/connection.js +2179 -817
  75. package/dist/conversation.d.ts +5 -3
  76. package/dist/conversation.d.ts.map +1 -1
  77. package/dist/conversation.js +84 -65
  78. package/dist/digest.d.ts +7 -0
  79. package/dist/digest.d.ts.map +1 -0
  80. package/dist/digest.js +113 -0
  81. package/dist/durability.d.ts +3 -5
  82. package/dist/durability.d.ts.map +1 -1
  83. package/dist/durability.js +5 -0
  84. package/dist/entry.d.ts +3 -1
  85. package/dist/entry.d.ts.map +1 -1
  86. package/dist/entry.js +18 -24
  87. package/dist/file.d.ts +2 -10
  88. package/dist/file.d.ts.map +1 -1
  89. package/dist/file.js +3 -120
  90. package/dist/fulltext.d.ts +27 -0
  91. package/dist/fulltext.d.ts.map +1 -0
  92. package/dist/fulltext.js +368 -0
  93. package/dist/host.d.ts +4 -4
  94. package/dist/host.d.ts.map +1 -1
  95. package/dist/host.js +16 -8
  96. package/dist/identity.d.ts +0 -1
  97. package/dist/identity.d.ts.map +1 -1
  98. package/dist/identity.js +0 -3
  99. package/dist/index.d.ts +12 -9
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +4 -1
  102. package/dist/live.d.ts +22 -0
  103. package/dist/live.d.ts.map +1 -0
  104. package/dist/live.js +123 -0
  105. package/dist/memory.d.ts +1 -0
  106. package/dist/memory.d.ts.map +1 -1
  107. package/dist/memory.js +20 -15
  108. package/dist/messages.d.ts +28 -0
  109. package/dist/messages.d.ts.map +1 -0
  110. package/dist/messages.js +90 -0
  111. package/dist/name.d.ts +4 -0
  112. package/dist/name.d.ts.map +1 -0
  113. package/dist/name.js +122 -0
  114. package/dist/observation.d.ts +12 -0
  115. package/dist/observation.d.ts.map +1 -1
  116. package/dist/observation.js +84 -0
  117. package/dist/opened.d.ts +22 -0
  118. package/dist/opened.d.ts.map +1 -0
  119. package/dist/opened.js +107 -0
  120. package/dist/outcome.d.ts +28 -4
  121. package/dist/outcome.d.ts.map +1 -1
  122. package/dist/outcome.js +22 -0
  123. package/dist/overlay.d.ts +1 -4
  124. package/dist/overlay.d.ts.map +1 -1
  125. package/dist/overlay.js +22 -148
  126. package/dist/query.d.ts +5 -1
  127. package/dist/query.d.ts.map +1 -1
  128. package/dist/query.js +13 -1
  129. package/dist/reach.d.ts +5 -3
  130. package/dist/reach.d.ts.map +1 -1
  131. package/dist/reach.js +149 -61
  132. package/dist/readiness.d.ts +19 -9
  133. package/dist/readiness.d.ts.map +1 -1
  134. package/dist/readiness.js +2 -2
  135. package/dist/reads.d.ts.map +1 -1
  136. package/dist/reads.js +4 -2
  137. package/dist/recovery.d.ts +2 -2
  138. package/dist/recovery.d.ts.map +1 -1
  139. package/dist/recovery.js +8 -7
  140. package/dist/refresh.d.ts +15 -14
  141. package/dist/refresh.d.ts.map +1 -1
  142. package/dist/refresh.js +112 -70
  143. package/dist/replica.d.ts +57 -39
  144. package/dist/replica.d.ts.map +1 -1
  145. package/dist/replica.js +275 -195
  146. package/dist/room-draft.d.ts +3 -1
  147. package/dist/room-draft.d.ts.map +1 -1
  148. package/dist/room-draft.js +21 -13
  149. package/dist/rows.d.ts +29 -31
  150. package/dist/rows.d.ts.map +1 -1
  151. package/dist/rows.js +113 -69
  152. package/dist/save.d.ts +49 -14
  153. package/dist/save.d.ts.map +1 -1
  154. package/dist/save.js +352 -98
  155. package/dist/search.d.ts +11 -43
  156. package/dist/search.d.ts.map +1 -1
  157. package/dist/search.js +0 -213
  158. package/dist/shared.d.ts +4 -3
  159. package/dist/shared.d.ts.map +1 -1
  160. package/dist/shared.js +67 -34
  161. package/dist/store.d.ts +87 -94
  162. package/dist/store.d.ts.map +1 -1
  163. package/dist/store.js +314 -720
  164. package/dist/stored.d.ts +145 -0
  165. package/dist/stored.d.ts.map +1 -0
  166. package/dist/stored.js +567 -0
  167. package/dist/tables.d.ts +344 -0
  168. package/dist/tables.d.ts.map +1 -0
  169. package/dist/tables.js +580 -0
  170. package/dist/turn.d.ts +1 -1
  171. package/dist/turn.d.ts.map +1 -1
  172. package/dist/turn.js +31 -20
  173. package/dist/validation.d.ts +30 -0
  174. package/dist/validation.d.ts.map +1 -0
  175. package/dist/validation.js +162 -0
  176. package/dist/windows.d.ts +2 -6
  177. package/dist/windows.d.ts.map +1 -1
  178. package/dist/windows.js +30 -34
  179. package/dist/wire.d.ts +13 -1
  180. package/dist/wire.d.ts.map +1 -1
  181. package/dist/wire.js +4 -1
  182. package/dist/words.d.ts +19 -0
  183. package/dist/words.d.ts.map +1 -0
  184. package/dist/words.js +150 -0
  185. package/package.json +11 -5
  186. package/dist/read-side.d.ts +0 -27
  187. package/dist/read-side.d.ts.map +0 -1
  188. package/dist/read-side.js +0 -826
@@ -1,47 +1,41 @@
1
- import { attachedModel } from './attached-model.js';
1
+ import { escapePointer, parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
2
+ import { attachedModel, NO_ROOM_TYPES, roomTypesOf } from './attached-model.js';
2
3
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
3
4
  import { observing, whole } from './observation.js';
4
5
  import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
5
- import { createReadSide } from './read-side.js';
6
+ import { createLiveQueries } from './live.js';
6
7
  import { seededResult } from './seed.js';
7
- import { markdownPath, roomOfKey } from './rows.js';
8
- import { createSearchIndex, searchPlanOf, } from './search.js';
8
+ import { documentKey, roomOfKey } from './rows.js';
9
+ import { searchPlanOf } from './search.js';
9
10
  import { capped, MAXIMUM_WINDOW } from './windows.js';
10
11
  import { bootstrapRoom } from './bootstrap.js';
11
- import { changeSet, changing, entering, mergeChangeSets } from './change-set.js';
12
+ import { changeSet, mergeChangeSets } from './change-set.js';
12
13
  import { createConversationStore, } from './conversation.js';
13
- import { queuedFile } from './change.js';
14
- import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
14
+ import { leaving as leavingIn, partnerOf, queuedFile, } from './change.js';
15
+ import { available, collectionOf, unreadable as unreadableIn, validRoot, } from './collection.js';
15
16
  import { runtimeError } from './failure.js';
16
17
  import { createLog } from './log.js';
17
18
  import { binaryRecord } from './binary.js';
18
- import { absentBinary, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
19
+ import { openedFiles } from './opened.js';
20
+ import { ABSENT_TARGET, absentBinary, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
19
21
  import { createRecovery } from './recovery.js';
20
22
  import { doorReads } from './reads.js';
21
- import { refreshRoom } from './refresh.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';
23
+ import { advancement } from './refresh.js';
24
+ import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
25
+ import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, sameMembership, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
24
26
  import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
25
27
  import { same, saving, STARTING, transferring, } from './status.js';
26
- import { detectStorageLoss, nextGeneration } from './durability.js';
27
- import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
28
+ import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
29
+ import { readinessOf, } from './readiness.js';
28
30
  import { FencedMap } from './fence.js';
29
- import { tally } from './shared.js';
30
- import { inChunks, ROW_WEIGHT, stepped, yielded } from './turn.js';
31
+ import { sameBinary } from './overlay.js';
32
+ import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
31
33
  import { identityTime, roomDraftRevision } from './identity.js';
32
34
  import { beneath, normalize } from './paths.js';
33
35
  import { openRuntimeStore, } from './store.js';
36
+ import { changedCleanly, sameComposition, validatedCollection, } from './validation.js';
34
37
  import { unconfirmed } from './transfer.js';
35
38
  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
- }
45
39
  function within(root, path) {
46
40
  return root === '' || beneath(root, path);
47
41
  }
@@ -59,24 +53,30 @@ const OPENING_LANE = 'opening';
59
53
  const CONVERSATION_LANE = 'conversations';
60
54
  const ROOM_READS = 256;
61
55
  const ROOM_ATTEMPTS = 16;
56
+ const ERASE_BATCH = 512;
57
+ const REDERIVE_BATCH = 256;
58
+ const WALK_BYTES = 1 << 20;
59
+ const QUERY_BYTES = 1 << 20;
60
+ const STAGE_BYTES = 1 << 20;
61
+ const STAGE_FLOOR = 1 << 16;
62
+ // A batch aims at this much database work, so a Save or a query waits behind at most one short write.
63
+ const STAGE_AIM_MS = 50;
64
+ const PARTS_HELD = 4;
65
+ // Bytes still arriving are reported at most this often, well inside the 2 s progress bound, so readiness is not rebuilt for every chunk the network hands over.
66
+ const ARRIVAL_REPORT_MS = 250;
67
+ const INSTALL_STEPS = [
68
+ 'writing',
69
+ 'downloading',
70
+ 'replaying',
71
+ 'listing',
72
+ ];
73
+ const CLEAN_CHANGES = 2_000;
74
+ // A Room that locks offline stays readable this long after the connection drops, so a laptop waking or changing network keeps working while a disconnected device locks soon after.
75
+ const LOCK_GRACE_MS = 120_000;
62
76
  function operationOf(input) {
63
77
  const operation = input?.operation_id;
64
78
  return typeof operation === 'string' ? operation : null;
65
79
  }
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
80
  function abandonedOn(signal) {
81
81
  return new Promise((resolve) => {
82
82
  if (signal.aborted)
@@ -88,11 +88,14 @@ function abandonedOn(signal) {
88
88
  function roomLane(locator) {
89
89
  return `room ${locator}`;
90
90
  }
91
+ function pendingKey(locator, id) {
92
+ return `${locator}\u0000${id}`;
93
+ }
91
94
  export function composeRuntime(options) {
92
95
  const localSaves = options.adapter.localSaves === true;
96
+ const roomTypes = roomTypesOf(options.adapter.roomTypes);
93
97
  let authorization = options.authorization;
94
98
  let opening;
95
- let store;
96
99
  const fences = new Map();
97
100
  const isFenced = (locator) => fences.has(locator);
98
101
  let fencings = 0;
@@ -176,50 +179,79 @@ export function composeRuntime(options) {
176
179
  const queueSubscriptions = new Set();
177
180
  const roomDraftListeners = new Set();
178
181
  const presentationListeners = new Set();
179
- const readSide = createReadSide();
182
+ const liveQueries = createLiveQueries((plan, cover) => answeredPlan(plan, cover));
180
183
  let withdrawConversations;
181
184
  let currencyQueued = null;
182
185
  let socketOpen = false;
186
+ const confirmed = new Set();
187
+ let lockGrace = null;
188
+ const lockListeners = new Set();
183
189
  let serviceMode = 'open';
184
190
  let haltedRefusal = false;
185
191
  let openSignals = 0;
186
192
  const serviceModeListeners = new Set();
187
193
  let member = null;
188
194
  const memberListeners = new Set();
189
- const prepared = new Set();
190
- const projecting = new Set();
191
195
  const unpreparedDemand = new Set();
192
- const unindexable = new Set();
196
+ const unpreparable = new Set();
193
197
  let preparing = false;
194
198
  let targets = {};
195
199
  let persistence = options.shell.persistence;
196
- let capacity = null;
197
- const paused = new Set();
198
- let rechecking = null;
200
+ let full = false;
201
+ let probing = null;
202
+ let testing = null;
203
+ const database = {
204
+ async ask(request) {
205
+ try {
206
+ return await options.database.ask(request);
207
+ }
208
+ catch (error) {
209
+ if (refusedForSpace(error))
210
+ filled();
211
+ throw error;
212
+ }
213
+ },
214
+ };
199
215
  const readinessWatches = new Set();
200
216
  let readinessQueued = false;
201
217
  const catching = new Set();
202
218
  const transfers = new FencedMap(isFenced);
219
+ const installs = new Map();
220
+ const arrivals = new Map();
221
+ let arrivalReported = 0;
222
+ let onScreen = options.shell.onScreen;
223
+ const shownWaiters = [];
224
+ let partsFree = PARTS_HELD;
225
+ const partWaiters = [];
226
+ let stageBytes = STAGE_FLOOR * 4;
227
+ let installWrites = Promise.resolve();
203
228
  const conversations = createConversationStore({
204
229
  agents: declaredAgents,
205
- documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
230
+ documents: (declaration, after) => rowsQuery(planOf({
231
+ rooms: [declaration.locator],
232
+ types: [declaration.type],
233
+ select: {
234
+ fields: [
235
+ declaration.identity,
236
+ declaration.name,
237
+ ...(declaration.retired === undefined ? [] : [declaration.retired]),
238
+ ].map((field) => `/${escapePointer(field)}`),
239
+ },
240
+ limit: MAXIMUM_WINDOW,
241
+ ...(after === undefined ? {} : { after }),
242
+ }, roomFacts())),
206
243
  call: (name, input) => invokeAction(name, input),
207
244
  durably,
208
245
  stream: () => options.adapter.conversations,
209
246
  publish: publishChangeSet,
210
247
  online: () => connectionOf() !== 'unreachable' && refusal === null,
248
+ full: () => full,
211
249
  closed: () => closed,
212
250
  unanswered: resendLater,
213
251
  expired: (operation) => pastHorizon(operation),
214
252
  });
215
- const searchIndex = createSearchIndex({
216
- room: (room) => readSide.room(room),
217
- document: (room, path) => readSide.document(room, path),
218
- });
219
- const roomsToIndex = new Set();
220
- const conversationsToIndex = new Set();
221
- let indexing = false;
222
253
  const journal = createLog();
254
+ const keptFiles = openedFiles(database, isFenced);
223
255
  let notifyReady = () => { };
224
256
  const ready = new Promise((resolve) => {
225
257
  notifyReady = resolve;
@@ -237,16 +269,36 @@ export function composeRuntime(options) {
237
269
  if (room.size === 0)
238
270
  roomDrafts.delete(locator);
239
271
  }
240
- function roomDraftOf(record) {
272
+ async function visibleOf(locator, paths) {
273
+ const held = replicas.get(locator);
274
+ if (held === undefined)
275
+ return undefined;
276
+ const local = await openLocal();
277
+ const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
278
+ return { ...slice, collection: collectionOf(slice.documents, compositionOf(held.record)) };
279
+ }
280
+ function roomDraftOf(record, visible) {
241
281
  return structuredClone({
242
282
  room: record.locator,
243
283
  path: record.path,
244
284
  revision: record.revision,
245
285
  parts: record.parts,
246
- changedUnderneath: underneath(record.parts, record.path, replicas.get(record.locator)?.visible),
286
+ changedUnderneath: underneath(record.parts, record.path, visible),
247
287
  returned: record.returned,
248
288
  });
249
289
  }
290
+ async function roomDraftsOf(records) {
291
+ const rooms = new Map();
292
+ for (const record of records) {
293
+ const paths = rooms.get(record.locator) ?? [];
294
+ paths.push(record.path);
295
+ rooms.set(record.locator, paths);
296
+ }
297
+ const layers = new Map();
298
+ for (const [locator, paths] of rooms)
299
+ layers.set(locator, await visibleOf(locator, paths));
300
+ return records.map((record) => roomDraftOf(record, layers.get(record.locator)));
301
+ }
250
302
  function snapshotRoomDrafts() {
251
303
  const records = [...roomDrafts.values()].flatMap((room) => [...room.values()]);
252
304
  records.sort((left, right) => {
@@ -256,18 +308,24 @@ export function composeRuntime(options) {
256
308
  return left.path < right.path ? -1 : 1;
257
309
  return 0;
258
310
  });
259
- return records.map(roomDraftOf);
311
+ return roomDraftsOf(records);
260
312
  }
313
+ let draftsShown = Promise.resolve();
261
314
  function publishRoomDrafts(waitingOnly) {
262
315
  if (closed)
263
316
  return;
264
- const value = snapshotRoomDrafts();
265
- for (const listener of roomDraftListeners) {
266
- if (waitingOnly && listener.ready)
267
- continue;
268
- listener.ready = true;
269
- listener.notify(value);
270
- }
317
+ const snapshot = snapshotRoomDrafts();
318
+ draftsShown = draftsShown.then(async () => {
319
+ const value = await snapshot.catch(() => null);
320
+ if (value === null || closed)
321
+ return;
322
+ for (const listener of roomDraftListeners) {
323
+ if (waitingOnly && listener.ready)
324
+ continue;
325
+ listener.ready = true;
326
+ listener.notify(value);
327
+ }
328
+ });
271
329
  }
272
330
  function presentationOf(app) {
273
331
  const value = presentations.get(app);
@@ -279,7 +337,7 @@ export function composeRuntime(options) {
279
337
  const stored = next(presentations.get(app));
280
338
  await local.present({ app, value: stored });
281
339
  presentations.set(app, stored);
282
- publishChangeSet(changeSet('', null, null, { presentation: app }));
340
+ publishChangeSet(changeSet('', { presentation: app }));
283
341
  });
284
342
  }
285
343
  function emitPresentation(app) {
@@ -330,21 +388,69 @@ export function composeRuntime(options) {
330
388
  return { attentionRequired, reset };
331
389
  }
332
390
  function documentsIn(locator) {
333
- const replica = replicas.get(locator);
334
- if (replica === undefined)
335
- return 0;
336
- const roots = replica.confirmed.record.typedSelection?.roots ?? [];
337
- const weighed = outsideOf(roots);
338
- return (tally(replica.visible.documents, weighed.documents) +
339
- tally(replica.visible.binaries, weighed.binaries));
391
+ return replicas.get(locator)?.count ?? 0;
340
392
  }
341
393
  function listed() {
342
394
  return structuredClone([...rooms.values()].map((room) => {
343
395
  const documents = documentsIn(room.locator);
344
396
  counted.set(room.locator, documents);
345
- return { ...room, documents };
397
+ return locked(room.locator)
398
+ ? {
399
+ ...room,
400
+ model: modelTypes(NO_MODEL),
401
+ typedSelection: null,
402
+ documents: 0,
403
+ locked: true,
404
+ }
405
+ : { ...room, documents, locked: false };
346
406
  }));
347
407
  }
408
+ function locked(locator) {
409
+ return rooms.get(locator)?.replica.offline === 'locked' && !confirmed.has(locator);
410
+ }
411
+ function lockedRooms() {
412
+ return [...rooms.keys()].filter(locked).sort();
413
+ }
414
+ function relocked(before) {
415
+ const after = lockedRooms();
416
+ if (JSON.stringify(after) === JSON.stringify(before))
417
+ return;
418
+ journal.record(`locked ${after.length}`);
419
+ emitRooms();
420
+ for (const notify of [...lockListeners])
421
+ notify();
422
+ }
423
+ function confirmLocks(listed) {
424
+ if (!socketOpen)
425
+ return;
426
+ for (const room of listed) {
427
+ if (room.replica.admission === 'admitted')
428
+ confirmed.add(room.locator);
429
+ }
430
+ }
431
+ function connectionLost() {
432
+ if (lockGrace !== null || closed)
433
+ return;
434
+ lockGrace = setTimeout(() => {
435
+ lockGrace = null;
436
+ if (socketOpen || closed)
437
+ return;
438
+ const before = lockedRooms();
439
+ confirmed.clear();
440
+ relocked(before);
441
+ }, LOCK_GRACE_MS);
442
+ }
443
+ function connectionLive() {
444
+ if (lockGrace !== null)
445
+ clearTimeout(lockGrace);
446
+ lockGrace = null;
447
+ }
448
+ function watchLocks(notify) {
449
+ lockListeners.add(notify);
450
+ return () => {
451
+ lockListeners.delete(notify);
452
+ };
453
+ }
348
454
  function roomFacts(reach = null) {
349
455
  const held = [...rooms.values()]
350
456
  .filter((room) => holds(reach, room.locator))
@@ -363,7 +469,7 @@ export function composeRuntime(options) {
363
469
  return { listed: roomsListed, rooms: held };
364
470
  }
365
471
  function emitRooms() {
366
- readSide.recover();
472
+ liveQueries.recover();
367
473
  queueReadiness();
368
474
  const value = listed();
369
475
  for (const listener of roomListeners)
@@ -402,17 +508,17 @@ export function composeRuntime(options) {
402
508
  for (const listener of statusListeners)
403
509
  listener(shown);
404
510
  }
405
- function entryOf(batch) {
511
+ function entryOf(locator, batch) {
406
512
  if (localSaves)
407
513
  return Object.freeze({ batch, state: 'local' });
408
- const state = pendings.get(batch.id);
514
+ const state = pendings.get(pendingKey(locator, batch.id));
409
515
  if (state?.accepted !== undefined) {
410
516
  return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
411
517
  }
412
518
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
413
519
  }
414
520
  function entriesOf(locator) {
415
- return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
521
+ return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map((batch) => entryOf(locator, batch));
416
522
  }
417
523
  function emitQueue(locator) {
418
524
  const entries = entriesOf(locator);
@@ -422,66 +528,39 @@ export function composeRuntime(options) {
422
528
  }
423
529
  }
424
530
  function applyChangeSet(set) {
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);
531
+ liveQueries.touched(set);
432
532
  }
433
533
  function unpublished(error) {
434
534
  journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
435
535
  }
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
- }
536
+ function indexed(locator) {
537
+ return replicas.get(locator)?.indexed === true;
447
538
  }
448
539
  async function prepareReplica(locator) {
449
- await referenced(locator);
450
- const replica = replicas.get(locator);
451
- if (replica === undefined || closed || prepared.has(locator))
452
- return;
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)
540
+ for (;;) {
541
+ const held = replicas.get(locator);
542
+ if (held === undefined || closed || held.indexed)
469
543
  return;
470
- prepared.add(locator);
471
- }
472
- finally {
473
- projecting.delete(locator);
544
+ const key = held.record.key;
545
+ const done = await exclusive(async () => (await openLocal()).rederive(key, REDERIVE_BATCH));
546
+ const now = replicas.get(locator);
547
+ if (now?.record.key !== key)
548
+ continue;
549
+ if (!done)
550
+ continue;
551
+ replicas.set(locator, { ...now, indexed: true });
552
+ publishChangeSet(changeSet(locator, { documents: null }));
553
+ liveQueries.recover();
554
+ queueReadiness();
555
+ return;
474
556
  }
475
- conversations.changed(mergeChangeSets(entered));
476
- readSide.recover();
477
- queueReadiness();
478
557
  }
479
558
  function unprepared() {
480
- return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
559
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
481
560
  }
482
- function unbuilt(locator, error) {
483
- unindexable.add(locator);
484
- journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
561
+ function unpreparedBy(locator, error) {
562
+ unpreparable.add(locator);
563
+ journal.record(`preparation failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
485
564
  queueReadiness();
486
565
  }
487
566
  function prepare(locators = []) {
@@ -497,7 +576,7 @@ export function composeRuntime(options) {
497
576
  if (closed)
498
577
  return;
499
578
  unpreparedDemand.delete(waiting);
500
- await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
579
+ await prepareReplica(waiting).catch((error) => unpreparedBy(waiting, error));
501
580
  }
502
581
  }
503
582
  finally {
@@ -514,7 +593,9 @@ export function composeRuntime(options) {
514
593
  (reached === null
515
594
  ? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
516
595
  : [...reached]);
517
- return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
596
+ return [...new Set(named)]
597
+ .filter((locator) => (reached === null || reached.has(locator)) && !plan.locked.includes(locator))
598
+ .sort();
518
599
  }
519
600
  function awaited(locator) {
520
601
  return (!roomsListed ||
@@ -528,6 +609,7 @@ export function composeRuntime(options) {
528
609
  const pending = [];
529
610
  const coverage = [];
530
611
  const overlay = [];
612
+ const read = [];
531
613
  for (const locator of locators) {
532
614
  const replica = replicas.get(locator);
533
615
  if (replica === undefined) {
@@ -535,21 +617,22 @@ export function composeRuntime(options) {
535
617
  pending.push({ room: locator, awaits: 'acquisition' });
536
618
  continue;
537
619
  }
538
- if (!prepared.has(locator)) {
620
+ if (!replica.indexed) {
539
621
  pending.push({ room: locator, awaits: 'preparation' });
540
622
  continue;
541
623
  }
542
624
  coverage.push({
543
625
  room: locator,
544
- generation: replica.confirmed.record.transfer,
626
+ generation: replica.record.transfer,
545
627
  current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
546
628
  });
629
+ read.push({ locator, key: replica.record.key });
547
630
  for (const batch of replica.queue)
548
631
  overlay.push(batch.id);
549
632
  }
550
633
  if (pending.length > 0)
551
634
  return { pending };
552
- return { coverage: { rooms: coverage, indexes: [], overlay } };
635
+ return { coverage: { rooms: coverage, indexes: [], overlay }, rooms: read };
553
636
  }
554
637
  function awaitPreparation(cover) {
555
638
  if (!('pending' in cover))
@@ -567,7 +650,7 @@ export function composeRuntime(options) {
567
650
  return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
568
651
  return { listed: named.every((room) => replicas.has(room)), rooms: named };
569
652
  }
570
- function roomState(locator, search) {
653
+ function roomState(locator) {
571
654
  if (sourceRefusals.has(locator))
572
655
  return 'refused';
573
656
  const listedRoom = discovered.get(locator);
@@ -578,27 +661,17 @@ export function composeRuntime(options) {
578
661
  return 'unadmitted';
579
662
  if (!replicas.has(locator))
580
663
  return 'acquiring';
581
- if (!prepared.has(locator))
664
+ if (!indexed(locator))
582
665
  return 'preparing';
583
- if (search && !searchIndex.rooms().has(locator))
584
- return 'indexing';
585
666
  return 'complete';
586
667
  }
587
- function conversationCoverage(reach, search) {
668
+ function conversationCoverage(reach) {
588
669
  const declared = reach === null || reach.conversations;
589
670
  if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
590
671
  return 'undeclared';
591
672
  if (!conversations.known() || !conversations.current())
592
673
  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';
674
+ return 'complete';
602
675
  }
603
676
  function connectionOf() {
604
677
  if (socketOpen)
@@ -625,34 +698,73 @@ export function composeRuntime(options) {
625
698
  ];
626
699
  });
627
700
  }
701
+ function installationOf(coverage) {
702
+ const rooms = { done: 0, total: 0 };
703
+ const parts = { done: 0, total: 0 };
704
+ const bytes = { done: 0, total: 0 };
705
+ const received = { done: 0, total: 0 };
706
+ const steps = new Set();
707
+ let waiting = false;
708
+ for (const { room, state } of coverage) {
709
+ if (state === 'unadmitted')
710
+ continue;
711
+ rooms.total += 1;
712
+ if (state === 'complete')
713
+ rooms.done += 1;
714
+ const transfer = transfers.get(room);
715
+ const held = installs.get(room);
716
+ const inventory = transfer?.inventory ?? replicas.get(room)?.record.inventory ?? null;
717
+ if (inventory !== null) {
718
+ const done = transfer?.done ?? inventory;
719
+ const share = held?.share ?? null;
720
+ parts.total += inventory.parts;
721
+ bytes.total += inventory.bytes;
722
+ parts.done += done.parts;
723
+ bytes.done += done.bytes;
724
+ if (share !== null && share.entries > 0) {
725
+ bytes.done += Math.floor((share.bytes * share.written) / share.entries);
726
+ }
727
+ received.total += inventory.bytes;
728
+ received.done += done.bytes + (share?.bytes ?? 0);
729
+ for (const arrived of arrivals.get(room)?.values() ?? [])
730
+ received.done += arrived;
731
+ }
732
+ const installing = held !== undefined && state !== 'complete';
733
+ if (installing)
734
+ steps.add(held.step);
735
+ waiting ||= installing || state === 'acquiring';
736
+ }
737
+ return {
738
+ step: INSTALL_STEPS.find((step) => steps.has(step)) ?? null,
739
+ paused: waiting && !onScreen ? 'hidden' : null,
740
+ rooms,
741
+ parts,
742
+ bytes,
743
+ received,
744
+ };
745
+ }
628
746
  function readinessFor(port, reach) {
629
747
  const required = requiredRooms(reach);
630
748
  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))
749
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
750
+ const preparing = coverage
751
+ .filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
634
752
  .map(({ room }) => room);
635
- const preparing = building('preparing');
636
753
  if (preparing.length > 0)
637
754
  prepare(preparing);
638
- const indexing = building('indexing');
639
- if (indexing.length > 0)
640
- index(indexing, []);
641
755
  return readinessOf({
642
756
  code: port.code,
643
757
  coverage: {
644
758
  listed: required.listed,
645
759
  rooms: coverage,
646
- conversations: conversationCoverage(reach, search),
760
+ conversations: conversationCoverage(reach),
647
761
  search: search ? 'required' : 'unused',
648
762
  },
649
763
  persistence,
650
764
  connection: connectionOf(),
651
765
  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,
766
+ space: full ? 'full' : port.space,
767
+ installation: installationOf(coverage),
656
768
  refused: refusedIn(required.rooms),
657
769
  lost: storageLoss !== null,
658
770
  established: targets[port.target]?.established === true,
@@ -661,7 +773,8 @@ export function composeRuntime(options) {
661
773
  function establish(name) {
662
774
  targets = { ...targets, [name]: { established: true } };
663
775
  queueReadiness();
664
- void durably((local) => local.keepTargets(targets)).catch(() => {
776
+ probeEachMinute();
777
+ void durably((local) => local.fact('targets', targets)).catch(() => {
665
778
  journal.record(`target record failed ${name}`);
666
779
  });
667
780
  }
@@ -698,92 +811,51 @@ export function composeRuntime(options) {
698
811
  readinessWatches.delete(watch);
699
812
  };
700
813
  }
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 ?? '-'}`);
814
+ function filled() {
815
+ if (full || closed)
816
+ return;
817
+ full = true;
818
+ journal.record('space full');
726
819
  queueReadiness();
727
- if (rechecking !== null)
820
+ probeEachMinute();
821
+ }
822
+ function installedTarget() {
823
+ return Object.values(targets).some((target) => target.established);
824
+ }
825
+ function probeEachMinute() {
826
+ if (!full || probing !== null || closed || !installedTarget())
728
827
  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();
828
+ // A device whose storage filled after its install tries a write each minute, so space freed elsewhere ends its read-only mode without a Retry.
829
+ probing = setInterval(() => {
830
+ void probed();
732
831
  }, 60_000);
733
832
  }
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();
833
+ function probed() {
834
+ testing ??= database.ask({ kind: 'space.probe' }).then(() => {
835
+ testing = null;
836
+ spaceFreed();
837
+ return true;
838
+ }, () => {
839
+ testing = null;
840
+ return false;
841
+ });
842
+ return testing;
744
843
  }
745
- function resume() {
746
- if (rechecking !== null)
747
- clearInterval(rechecking);
748
- rechecking = null;
749
- if (paused.size === 0 && capacity === null)
844
+ function spaceFreed() {
845
+ if (!full)
750
846
  return;
751
- paused.clear();
752
- capacity = null;
753
- journal.record('capacity resumed');
847
+ full = false;
848
+ if (probing !== null)
849
+ clearInterval(probing);
850
+ probing = null;
851
+ unpreparable.clear();
852
+ journal.record('space freed');
754
853
  queueReadiness();
755
854
  if (!closed && authorization !== undefined)
756
855
  void sync('unstreamed');
757
856
  }
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
857
  function seedRoomHeld(room, version) {
786
- return prepared.has(room) && reachedBy(room, version);
858
+ return indexed(room) && reachedBy(room, version);
787
859
  }
788
860
  function seedHeld(at) {
789
861
  return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
@@ -817,6 +889,75 @@ export function composeRuntime(options) {
817
889
  shown();
818
890
  }
819
891
  }
892
+ function sameCover(left, right) {
893
+ return JSON.stringify(left) === JSON.stringify(right);
894
+ }
895
+ async function coveredRows(plan, cover) {
896
+ for (;;) {
897
+ const before = cover();
898
+ if ('pending' in before)
899
+ return { pending: before };
900
+ const local = await openLocal();
901
+ const keys = before.rooms.map(({ key }) => key);
902
+ const changes = local.changes(keys);
903
+ const current = () => local.changes(keys) === changes;
904
+ const rows = await windowRows(local, plan, before.rooms, current);
905
+ const after = cover();
906
+ if (rows !== null && sameCover(before, after))
907
+ return { rows, cover: before, current };
908
+ }
909
+ }
910
+ async function windowRows(local, plan, rooms, unchanged) {
911
+ const first = await local.query(plan, rooms, QUERY_BYTES);
912
+ if (first.more === undefined || !('documents' in first)) {
913
+ const { more: _more, ...rows } = first;
914
+ return rows;
915
+ }
916
+ const documents = [...first.documents];
917
+ for (let more = first.more; more !== undefined;) {
918
+ if (!unchanged())
919
+ return null;
920
+ const page = await local.query({
921
+ ...plan,
922
+ after: { text: more, values: JSON.parse(more) },
923
+ limit: plan.limit === null ? null : plan.limit - documents.length,
924
+ total: false,
925
+ }, rooms, QUERY_BYTES);
926
+ if (!('documents' in page))
927
+ return null;
928
+ documents.push(...page.documents);
929
+ more = page.more;
930
+ if (more === undefined) {
931
+ return {
932
+ documents,
933
+ ...(page.next === undefined ? {} : { next: page.next }),
934
+ ...(first.total === undefined ? {} : { total: first.total }),
935
+ };
936
+ }
937
+ }
938
+ return null;
939
+ }
940
+ function keyedOf(rows, coverage) {
941
+ if ('groups' in rows) {
942
+ return {
943
+ result: { ...rows, coverage },
944
+ keys: rows.groups.map((group) => JSON.stringify(group.key)),
945
+ };
946
+ }
947
+ return {
948
+ result: { ...rows, coverage },
949
+ keys: rows.documents.map((document) => documentKey(document.room, document.path)),
950
+ };
951
+ }
952
+ async function answeredPlan(plan, cover) {
953
+ const answered = await coveredRows(plan, cover);
954
+ if ('pending' in answered) {
955
+ const pending = answered.pending;
956
+ return { result: pending, keys: [], current: () => true };
957
+ }
958
+ const held = answered.cover;
959
+ return { ...keyedOf(answered.rows, held.coverage), current: answered.current };
960
+ }
820
961
  async function rowsQuery(plan) {
821
962
  const before = covered(plan);
822
963
  awaitPreparation(before);
@@ -825,59 +966,15 @@ export function composeRuntime(options) {
825
966
  return seededResult(seeded).result;
826
967
  if ('pending' in before)
827
968
  return before;
828
- const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
829
- awaitPreparation(cover);
830
- if ('pending' in cover)
969
+ const answered = await coveredRows(plan, () => {
970
+ const cover = covered(plan);
971
+ awaitPreparation(cover);
831
972
  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(() => { });
973
+ });
974
+ if ('pending' in answered)
975
+ return answered.pending;
976
+ const held = answered.cover;
977
+ return { ...answered.rows, coverage: held.coverage };
881
978
  }
882
979
  function publishChangeSet(set) {
883
980
  applyChangeSet(set);
@@ -887,7 +984,10 @@ export function composeRuntime(options) {
887
984
  for (const locator of set.queues)
888
985
  emitQueue(locator);
889
986
  if (set.drafts.length > 0 ||
890
- set.documents.some((change) => roomDrafts.get(change.locator)?.has(change.path) === true)) {
987
+ set.documents.some((change) => {
988
+ const held = roomDrafts.get(change.locator);
989
+ return held !== undefined && (change.path === null || held.has(change.path));
990
+ })) {
891
991
  publishRoomDrafts(false);
892
992
  }
893
993
  for (const app of set.presentations)
@@ -900,9 +1000,22 @@ export function composeRuntime(options) {
900
1000
  if (reason === 'revoked-authorization') {
901
1001
  await exclusive(async () => {
902
1002
  const revoked = { status: 'unavailable', reason: 'revoked' };
1003
+ const records = [...replicas].map(([locator, held]) => ({
1004
+ locator,
1005
+ held,
1006
+ record: { ...held.record, model: revoked, selectionModel: revoked },
1007
+ }));
1008
+ let answer = null;
903
1009
  try {
904
1010
  const local = await openLocal();
905
- await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
1011
+ answer = await local.commit({
1012
+ rooms: records.map(({ locator, record }) => ({
1013
+ locator,
1014
+ key: record.key,
1015
+ record: storedRecord(record),
1016
+ composition: compositionOf(record),
1017
+ })),
1018
+ });
906
1019
  }
907
1020
  catch {
908
1021
  journal.record('model revocation persistence failed');
@@ -918,16 +1031,16 @@ export function composeRuntime(options) {
918
1031
  liveModels.delete(key);
919
1032
  }
920
1033
  const changed = [];
921
- for (const [locator, room] of rooms) {
922
- const before = replicas.get(locator) ?? null;
923
- if (before !== null) {
924
- const record = {
925
- ...before.confirmed.record,
926
- model: revoked,
927
- selectionModel: revoked,
928
- };
929
- replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
930
- }
1034
+ for (const { locator, held, record } of records) {
1035
+ if (replicas.get(locator) !== held)
1036
+ continue;
1037
+ const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
1038
+ replicas.set(locator, { ...held, record, indexed });
1039
+ }
1040
+ for (const locator of [...rooms.keys()]) {
1041
+ const room = rooms.get(locator);
1042
+ if (room === undefined)
1043
+ continue;
931
1044
  rooms.set(locator, {
932
1045
  ...room,
933
1046
  model: modelTypes(revoked),
@@ -935,9 +1048,10 @@ export function composeRuntime(options) {
935
1048
  ? null
936
1049
  : { ...room.typedSelection, model: modelTypes(revoked) },
937
1050
  });
938
- changed.push(changeSet(locator, before, replicas.get(locator) ?? null, { room: true }));
1051
+ changed.push(changeSet(locator, { documents: null, room: true }));
939
1052
  }
940
1053
  publishChangeSet(mergeChangeSets(changed));
1054
+ prepare();
941
1055
  });
942
1056
  }
943
1057
  }
@@ -968,12 +1082,13 @@ export function composeRuntime(options) {
968
1082
  model: modelTypes(modelOf(room.locator, 'typed-selection')),
969
1083
  },
970
1084
  providerOwnedPaths: room.providerOwnedPaths,
971
- roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
1085
+ roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
972
1086
  synchronizationAvailable,
973
1087
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
974
1088
  });
975
1089
  }
976
1090
  async function applyDiscovery(found, started, covered) {
1091
+ const lockedBefore = lockedRooms();
977
1092
  for (const room of found) {
978
1093
  const fence = fences.get(room.locator);
979
1094
  if (fence !== undefined && fence.erased && fence.at < started)
@@ -999,15 +1114,17 @@ export function composeRuntime(options) {
999
1114
  else
1000
1115
  unlistedKept(unlisted);
1001
1116
  for (const room of listed) {
1002
- const held = replicas.get(room.locator)?.confirmed.record;
1117
+ const held = replicas.get(room.locator)?.record;
1003
1118
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
1004
1119
  list(held, false);
1005
1120
  continue;
1006
1121
  }
1007
1122
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
1008
1123
  }
1124
+ confirmLocks(listed);
1009
1125
  roomsListed = true;
1010
1126
  emitRooms();
1127
+ relocked(lockedBefore);
1011
1128
  return listed;
1012
1129
  }
1013
1130
  function heldLocally(locator) {
@@ -1028,7 +1145,7 @@ export function composeRuntime(options) {
1028
1145
  }
1029
1146
  function unlistedKept(unlisted) {
1030
1147
  for (const locator of unlisted) {
1031
- const held = replicas.get(locator)?.confirmed.record;
1148
+ const held = replicas.get(locator)?.record;
1032
1149
  if (held !== undefined)
1033
1150
  list(held, false);
1034
1151
  else
@@ -1037,16 +1154,16 @@ export function composeRuntime(options) {
1037
1154
  }
1038
1155
  function fence(locators) {
1039
1156
  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,
1157
+ const removed = fresh.map((locator) => ({
1158
+ locator,
1043
1159
  drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
1044
1160
  }));
1045
1161
  for (const locator of fresh) {
1046
1162
  fences.set(locator, { at: discoveries, erased: false });
1047
1163
  lastFenced.set(locator, ++fencings);
1164
+ confirmed.delete(locator);
1048
1165
  for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1049
- pendings.delete(batch.id);
1166
+ pendings.delete(pendingKey(locator, batch.id));
1050
1167
  replicas.delete(locator);
1051
1168
  queuesWithoutReplica.delete(locator);
1052
1169
  rooms.delete(locator);
@@ -1058,8 +1175,7 @@ export function composeRuntime(options) {
1058
1175
  unreadable.delete(locator);
1059
1176
  stale.delete(locator);
1060
1177
  repairable.delete(locator);
1061
- unindexable.delete(locator);
1062
- paused.delete(locator);
1178
+ unpreparable.delete(locator);
1063
1179
  scopedRefusals.delete(locator);
1064
1180
  sourceRefusals.delete(locator);
1065
1181
  declarationRefusals.delete(locator);
@@ -1073,26 +1189,48 @@ export function composeRuntime(options) {
1073
1189
  }
1074
1190
  if (fresh.length === 0)
1075
1191
  return fresh;
1076
- publishChangeSet(mergeChangeSets(removed));
1077
- for (const locator of fresh)
1078
- prepared.delete(locator);
1079
- searchIndex.forget(fresh);
1080
- readSide.recover();
1192
+ publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, { documents: null, room: true, queue: true, drafts }))));
1193
+ liveQueries.recover();
1081
1194
  moveTo({ ...status, ...attention() });
1082
1195
  return fresh;
1083
1196
  }
1084
- async function erased(local, locators) {
1085
- await local.erase(locators);
1197
+ async function eraseFenced(locators) {
1086
1198
  for (const locator of locators) {
1199
+ while (await durably((local) => local.erase(locator, ERASE_BATCH)))
1200
+ await yielded();
1087
1201
  const held = fences.get(locator);
1088
1202
  if (held !== undefined)
1089
1203
  held.erased = true;
1090
1204
  }
1091
1205
  }
1206
+ let sweeping = false;
1207
+ function sweep() {
1208
+ if (sweeping || closed)
1209
+ return;
1210
+ sweeping = true;
1211
+ void (async () => {
1212
+ try {
1213
+ while (!closed && (await durably((local) => local.erase(null, ERASE_BATCH)))) {
1214
+ await yielded();
1215
+ }
1216
+ }
1217
+ catch {
1218
+ journal.record('erasure failed');
1219
+ }
1220
+ finally {
1221
+ sweeping = false;
1222
+ }
1223
+ })();
1224
+ }
1092
1225
  async function loseRooms(locators) {
1093
1226
  const fresh = fence(locators);
1094
- if (fresh.length > 0)
1095
- await durably((local) => erased(local, fresh));
1227
+ if (fresh.length === 0)
1228
+ return;
1229
+ await durably(async (local) => {
1230
+ await local.fence(fresh);
1231
+ await unpair(local);
1232
+ });
1233
+ await eraseFenced(fresh);
1096
1234
  }
1097
1235
  function restored(key, persisted) {
1098
1236
  const prior = models.get(key);
@@ -1113,54 +1251,56 @@ export function composeRuntime(options) {
1113
1251
  return model;
1114
1252
  }
1115
1253
  async function hydrate(opened) {
1116
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
1117
- opened.all(),
1254
+ const [held, queues, kept, heldConversations, chatDrafts] = await Promise.all([
1255
+ opened.opened(),
1118
1256
  opened.queues(),
1119
1257
  opened.roomDrafts(),
1120
- opened.presentation(),
1121
1258
  opened.conversations(),
1122
1259
  opened.chatDrafts(),
1123
- opened.lastCompleted(),
1124
- opened.providerLimits(),
1125
- opened.capabilities(),
1126
- opened.member(),
1127
- opened.transfers(),
1128
- opened.targets(),
1129
1260
  ]);
1130
- targets = heldTargets;
1261
+ const records = [...held.rooms.values()];
1262
+ const counts = await opened.counts(records.map(({ key, typedSelection }) => ({ key, roots: typedSelection?.roots ?? [] })));
1263
+ const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1264
+ targets = held.targets;
1131
1265
  transfers.clear();
1132
- for (const record of staged)
1133
- transfers.set(record.locator, record);
1134
- limits = declared;
1135
- capabilities = named;
1136
- member = known;
1266
+ for (const [locator, record] of held.transfers)
1267
+ transfers.set(locator, record);
1268
+ limits = held.providerLimits;
1269
+ capabilities = held.capabilities;
1270
+ member = held.member;
1137
1271
  roomDrafts.clear();
1138
1272
  for (const record of kept)
1139
1273
  rememberRoomDraft(record);
1140
1274
  presentations.clear();
1141
- for (const record of presented)
1275
+ for (const record of held.presentation)
1142
1276
  presentations.set(record.app, record.value);
1143
- conversations.hydrate(held, chatDrafts);
1144
- const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1277
+ conversations.hydrate(heldConversations, chatDrafts);
1145
1278
  pendings.clear();
1146
1279
  for (const queue of queues) {
1147
- for (const { batch, state } of queue.pending)
1148
- pendings.set(batch.id, state);
1280
+ for (const { batch, state } of queue.pending) {
1281
+ pendings.set(pendingKey(queue.locator, batch.id), state);
1282
+ }
1149
1283
  }
1150
- const previousReplicas = new Map(replicas);
1151
1284
  replicas.clear();
1152
1285
  queuesWithoutReplica.clear();
1153
- const replicated = new Set(records.map((record) => record.locator));
1154
1286
  for (const [locator, queue] of queuedBatches) {
1155
- if (!replicated.has(locator))
1287
+ if (!held.rooms.has(locator))
1156
1288
  queuesWithoutReplica.set(locator, queue);
1157
1289
  }
1158
- for (const record of records) {
1159
- const model = restored(modelKey(record.locator, 'collection'), record.model);
1160
- const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
1161
- const prior = previousReplicas.get(record.locator);
1162
- const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
1163
- replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
1290
+ records.forEach((stored, index) => {
1291
+ if (fences.has(stored.locator))
1292
+ return;
1293
+ const model = restored(modelKey(stored.locator, 'collection'), stored.model);
1294
+ const selectionModel = restored(modelKey(stored.locator, 'typed-selection'), stored.selectionModel);
1295
+ const record = { ...stored, model, selectionModel };
1296
+ replicas.set(record.locator, {
1297
+ record,
1298
+ queue: queuedBatches.get(record.locator) ?? [],
1299
+ overlaid: held.overlaid.get(record.key) ?? new Set(),
1300
+ count: counts[index] ?? 0,
1301
+ confirmed: 0,
1302
+ indexed: !held.unindexed.has(record.key),
1303
+ });
1164
1304
  const existing = rooms.get(record.locator);
1165
1305
  if (existing === undefined)
1166
1306
  list(record, false);
@@ -1179,11 +1319,15 @@ export function composeRuntime(options) {
1179
1319
  },
1180
1320
  });
1181
1321
  }
1182
- }
1322
+ });
1323
+ for (const key of held.orphans)
1324
+ await opened.release(key);
1325
+ if (held.erasing || held.orphans.length > 0)
1326
+ queueMicrotask(sweep);
1183
1327
  return {
1184
- lastCompleted,
1328
+ lastCompleted: held.lastCompleted,
1185
1329
  locators: new Set([...queuedBatches.keys(), ...replicas.keys()]),
1186
- read: mergeChangeSets([...new Set([...previousReplicas.keys(), ...replicas.keys()])].map((locator) => changeSet(locator, previousReplicas.get(locator) ?? null, replicas.get(locator) ?? null))),
1330
+ read: mergeChangeSets([...replicas.keys()].map((locator) => changeSet(locator, { documents: null }))),
1187
1331
  };
1188
1332
  }
1189
1333
  async function readState(opened) {
@@ -1202,8 +1346,7 @@ export function composeRuntime(options) {
1202
1346
  async function open() {
1203
1347
  if (closed)
1204
1348
  throw new Error('runtime closed');
1205
- const opened = await openRuntimeStore(isFenced);
1206
- store = opened;
1349
+ const opened = openRuntimeStore(isFenced, database);
1207
1350
  const interrupted = await opened.fences();
1208
1351
  for (const locator of interrupted) {
1209
1352
  fences.set(locator, { at: discoveries, erased: false });
@@ -1222,9 +1365,17 @@ export function composeRuntime(options) {
1222
1365
  emitStorageLoss();
1223
1366
  }
1224
1367
  const lastCompleted = await readState(opened);
1225
- if (interrupted.length > 0)
1226
- await erased(opened, interrupted);
1368
+ if (interrupted.length > 0) {
1369
+ queueMicrotask(() => {
1370
+ void eraseFenced(interrupted).catch(() => {
1371
+ journal.record('erasure failed');
1372
+ });
1373
+ });
1374
+ }
1227
1375
  moveTo({ ...status, lastCompleted, ...attention() });
1376
+ void exclusive(() => unpair(opened)).catch(() => {
1377
+ journal.record('unpair failed');
1378
+ });
1228
1379
  return opened;
1229
1380
  }
1230
1381
  function durably(work) {
@@ -1245,86 +1396,262 @@ export function composeRuntime(options) {
1245
1396
  mutations = next.then(() => undefined, () => undefined);
1246
1397
  return next;
1247
1398
  }
1248
- function withdrawable(batch) {
1249
- const state = pendings.get(batch.id);
1250
- return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
1399
+ function withdrawable(locator) {
1400
+ return (batch) => {
1401
+ const state = pendings.get(pendingKey(locator, batch.id));
1402
+ return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
1403
+ };
1404
+ }
1405
+ const REWRITE_LINKS = ['field', 'body'];
1406
+ const CHECKED_LINKS = ['field', 'section'];
1407
+ async function sliceAt(record, view, queue, extra = [], moved = []) {
1408
+ const local = await openLocal();
1409
+ const needed = new Set([...touchedBy(queue), ...extra]);
1410
+ const movedPaths = [...new Set([...movedBy(queue, record.locator), ...moved])];
1411
+ if (movedPaths.length > 0) {
1412
+ for (const { path } of await local.referrers(record.key, view, movedPaths, REWRITE_LINKS)) {
1413
+ needed.add(path);
1414
+ }
1415
+ }
1416
+ const paths = [...needed];
1417
+ return stepped(slicing(await local.entries(record.key, view, paths), paths));
1418
+ }
1419
+ async function landedWhole(locator, batch, confirmedLayer, current) {
1420
+ if (!landed(batch, confirmedLayer, current))
1421
+ return false;
1422
+ const partner = partnerOf(batch, locator);
1423
+ if (partner === null)
1424
+ return true;
1425
+ const other = replicas.get(partner);
1426
+ const half = other?.queue.find((one) => one.id === batch.id);
1427
+ if (other === undefined || half === undefined)
1428
+ return false;
1429
+ return landed(half, await sliceAt(other.record, 'confirmed', [half]));
1430
+ }
1431
+ function carriedBack(locator, unlanded, written) {
1432
+ const back = new Map();
1433
+ const taken = [];
1434
+ for (const batch of unlanded) {
1435
+ const between = batch.between;
1436
+ if (between === undefined)
1437
+ continue;
1438
+ const source = between.source.room;
1439
+ const destination = between.destination.room;
1440
+ for (const { path: to } of pendings.get(pendingKey(destination, batch.id))?.drafts ?? []) {
1441
+ const record = roomDrafts.get(destination)?.get(to);
1442
+ const path = between.moves.find((move) => move.to === to)?.path;
1443
+ if (record === undefined || path === undefined)
1444
+ continue;
1445
+ const [into, key] = source === locator ? [written, path] : [back, documentKey(source, path)];
1446
+ const existing = into.get(key) ?? roomDrafts.get(source)?.get(path);
1447
+ taken.push({ locator: destination, path: to });
1448
+ into.set(key, {
1449
+ locator: source,
1450
+ path,
1451
+ revision: roomDraftRevision(),
1452
+ parts: returnedInto(existing?.parts, record.parts),
1453
+ returned: existing?.returned ?? null,
1454
+ });
1455
+ }
1456
+ }
1457
+ const records = [...back.values()];
1458
+ const elsewhere = taken.filter((one) => one.locator !== locator);
1459
+ const rooms = new Set([...records, ...elsewhere].map((one) => one.locator));
1460
+ return {
1461
+ taken: taken.filter((one) => one.locator === locator).map(({ path }) => path),
1462
+ writes: [...rooms].map((room) => ({
1463
+ locator: room,
1464
+ roomDrafts: records.filter((record) => record.locator === room),
1465
+ consumedRoomDrafts: elsewhere.filter((one) => one.locator === room).map(({ path }) => path),
1466
+ })),
1467
+ changes: [...rooms].map((room) => changeSet(room, {
1468
+ drafts: [...records, ...elsewhere]
1469
+ .filter((one) => one.locator === room)
1470
+ .map(({ path }) => path),
1471
+ })),
1472
+ remember() {
1473
+ for (const one of taken)
1474
+ forgetRoomDraft(one.locator, one.path);
1475
+ for (const record of records)
1476
+ rememberRoomDraft(record);
1477
+ },
1478
+ };
1479
+ }
1480
+ function committedRoom(locator, answer) {
1481
+ const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
1482
+ if (!indexed)
1483
+ prepare([locator]);
1484
+ return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
1485
+ }
1486
+ function replayed(record, confirmedLayer, queue) {
1487
+ return stepped(replaying(confirmedLayer, queue, record.locator, compositionOf(record)));
1251
1488
  }
1252
- async function settle(local, locator, before, confirmed, queue, settling = {}) {
1489
+ async function overlayDiff(record, before, visible, touched) {
1490
+ const kept = before !== undefined && before.record.key === record.key
1491
+ ? before.overlaid
1492
+ : new Set();
1493
+ return {
1494
+ overlay: {
1495
+ set: await stepped(overlayOf(visible, touched)),
1496
+ cleared: [...kept].filter((path) => !touched.has(path)),
1497
+ },
1498
+ touched,
1499
+ };
1500
+ }
1501
+ async function settle(local, locator, before, record, queue, settling = {}) {
1502
+ return together(local, [await settlement(local, locator, before, record, queue, settling)]);
1503
+ }
1504
+ function unpairedIn(locator, batch) {
1505
+ if (!queueIn(locator).some((held) => held.id === batch.id))
1506
+ return false;
1507
+ if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
1508
+ return false;
1509
+ return !leavingIn(batch, locator) || withdrawable(locator)(batch);
1510
+ }
1511
+ async function together(local, settlements) {
1512
+ const settled = new Set(settlements.map(({ locator }) => locator));
1513
+ const all = [...settlements];
1514
+ for (let wave = settlements; wave.length > 0;) {
1515
+ const partnered = new Map();
1516
+ for (const { locator, left } of wave) {
1517
+ for (const batch of left) {
1518
+ const partner = partnerOf(batch, locator);
1519
+ if (partner === null || settled.has(partner) || !unpairedIn(partner, batch))
1520
+ continue;
1521
+ partnered.set(partner, [...(partnered.get(partner) ?? []), batch]);
1522
+ }
1523
+ }
1524
+ const partners = [];
1525
+ for (const [partner, batches] of partnered) {
1526
+ settled.add(partner);
1527
+ partners.push(await unpairing(local, partner, batches));
1528
+ }
1529
+ all.push(...partners);
1530
+ wave = partners;
1531
+ }
1532
+ const answer = await local.commit({
1533
+ rooms: all.flatMap(({ commit }) => commit.rooms),
1534
+ writes: all.flatMap(({ commit }) => commit.writes),
1535
+ });
1536
+ return mergeChangeSets(all.map((one) => one.apply(answer)));
1537
+ }
1538
+ async function settlement(local, locator, before, record, queue, settling = {}) {
1539
+ const view = settling.view ?? 'confirmed';
1253
1540
  const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
1254
- const { withdrawn, kept } = await stepped(standing(confirmed, queue, withdrawable, localSaves));
1541
+ const overrides = settling.changes;
1542
+ const read = await sliceAt(record, view, queue, [
1543
+ ...(before !== undefined && before.record.key === record.key ? before.overlaid : []),
1544
+ ...(overrides?.set.map(({ path }) => path) ?? []),
1545
+ ...(overrides?.removed ?? []),
1546
+ ]);
1547
+ const confirmedLayer = overrides === undefined
1548
+ ? read
1549
+ : await stepped(slicingWith(read, overrides.set, overrides.removed));
1550
+ const composition = compositionOf(record);
1551
+ const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
1255
1552
  const founded = kept.filter((batch) => !queue.includes(batch));
1256
1553
  const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
1257
- const unqueued = withdrawn.map((batch) => ({
1258
- batch,
1259
- returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
1260
- }));
1554
+ const unqueued = [];
1555
+ for (const batch of withdrawn) {
1556
+ unqueued.push({ batch, landed: await landedWhole(locator, batch, confirmedLayer) });
1557
+ }
1261
1558
  const returning = [
1262
- ...[...unqueued].reverse(),
1263
- ...(settling.leaving === undefined ? [] : [settling.leaving]),
1559
+ ...[...unqueued].reverse().map(({ batch, landed: held }) => ({
1560
+ batch,
1561
+ returned: localSaves || held ? null : returnedBy('withdrawn'),
1562
+ })),
1563
+ ...(settling.leaving ?? []),
1264
1564
  ];
1265
1565
  const written = new Map();
1266
- for (const { batch, returned } of returning) {
1267
- if (returned === null)
1566
+ for (const { batch, returned: reason } of returning) {
1567
+ if (reason === null || batch.between?.destination.room === locator)
1268
1568
  continue;
1269
- for (const saved of pendings.get(batch.id)?.drafts ?? []) {
1569
+ for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
1270
1570
  const existing = written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path);
1271
1571
  written.set(saved.path, {
1272
1572
  locator,
1273
1573
  path: saved.path,
1274
1574
  revision: roomDraftRevision(),
1275
1575
  parts: returnedInto(existing?.parts, saved.parts),
1276
- returned,
1576
+ returned: reason,
1277
1577
  });
1278
1578
  }
1279
1579
  }
1280
- await local.keepAuthored({ locator, queue: kept, founded, roomDrafts: [...written.values()] });
1580
+ const carried = carriedBack(locator, [
1581
+ ...[...unqueued].reverse().flatMap(({ batch, landed: held }) => (held ? [] : [batch])),
1582
+ ...(settling.leaving ?? []).flatMap(({ batch, returned }) => returned === null ? [] : [batch]),
1583
+ ], written);
1584
+ const replayedLayer = await replayed(record, confirmedLayer, kept);
1585
+ const { overlay, touched } = await overlayDiff(record, before, replayedLayer.visible, replayedLayer.touched);
1586
+ const commit = {
1587
+ rooms: [
1588
+ {
1589
+ locator,
1590
+ key: record.key,
1591
+ record: storedRecord(record),
1592
+ roots: record.typedSelection?.roots ?? [],
1593
+ ...settling.commit,
1594
+ ...(overrides === undefined
1595
+ ? {}
1596
+ : { confirmed: { set: overrides.set, removed: overrides.removed } }),
1597
+ overlay,
1598
+ },
1599
+ ],
1600
+ writes: [
1601
+ {
1602
+ locator,
1603
+ queue: kept,
1604
+ founded,
1605
+ roomDrafts: [...written.values()],
1606
+ consumedRoomDrafts: carried.taken,
1607
+ },
1608
+ ...carried.writes,
1609
+ ],
1610
+ };
1281
1611
  const staying = new Set(kept.map((batch) => batch.id));
1282
- for (const batch of previousQueue) {
1283
- if (!staying.has(batch.id))
1284
- pendings.delete(batch.id);
1285
- }
1286
- for (const record of written.values())
1287
- rememberRoomDraft(record);
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
1612
+ return {
1613
+ locator,
1614
+ left: previousQueue.filter((batch) => !staying.has(batch.id)),
1615
+ commit,
1616
+ apply: (answer) => {
1617
+ for (const batch of previousQueue) {
1618
+ if (!staying.has(batch.id))
1619
+ pendings.delete(pendingKey(locator, batch.id));
1620
+ }
1621
+ for (const draft of written.values())
1622
+ rememberRoomDraft(draft);
1623
+ carried.remember();
1624
+ const drafts = [...written.keys(), ...carried.taken];
1306
1625
  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);
1626
+ for (const { batch, landed: held } of unqueued) {
1627
+ const outcome = localSaves ? 'dropped' : held ? 'landed' : 'withdrawn';
1628
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1629
+ }
1630
+ const changed = answer.changed[locator];
1631
+ const confirmedChanged = settling.commit?.published !== undefined ||
1632
+ settling.commit?.refreshed === true ||
1633
+ overrides !== undefined;
1634
+ const { count, indexed } = committedRoom(locator, answer);
1635
+ replicas.set(locator, {
1636
+ record,
1637
+ queue: kept,
1638
+ overlaid: touched,
1639
+ count,
1640
+ confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
1641
+ indexed,
1642
+ });
1643
+ return mergeChangeSets([
1644
+ changeSet(locator, {
1645
+ documents: changed === undefined ? [] : changed,
1646
+ room: settling.room,
1647
+ queue: queueChanged,
1648
+ drafts,
1649
+ }),
1650
+ ...carried.changes,
1651
+ ]);
1652
+ },
1322
1653
  };
1323
1654
  }
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);
1327
- }
1328
1655
  function adoptModels(locator, record) {
1329
1656
  for (const [attached, model] of [
1330
1657
  ['collection', record.model],
@@ -1343,7 +1670,7 @@ export function composeRuntime(options) {
1343
1670
  const taken = (answer, previous, attached) => {
1344
1671
  if (answer === undefined)
1345
1672
  return previous;
1346
- const { state, refused } = attachedModel(answer, previous);
1673
+ const { state, refused } = attachedModel(answer, previous, attached === 'collection' ? roomTypes : NO_ROOM_TYPES);
1347
1674
  if (refused !== null) {
1348
1675
  const where = refused.location === undefined ? '' : ` ${refused.location}`;
1349
1676
  journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
@@ -1356,65 +1683,76 @@ export function composeRuntime(options) {
1356
1683
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
1357
1684
  };
1358
1685
  }
1359
- function publish(local, room, confirmed, landing, live) {
1360
- const { base, stored } = landing;
1361
- return exclusive(async () => {
1362
- if (stored !== undefined) {
1363
- await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
1364
- }
1686
+ function publish(local, room, record, landing, live) {
1687
+ const landed = exclusive(async () => {
1365
1688
  if (!live()) {
1366
1689
  caughtUpTo(room.locator);
1367
1690
  return true;
1368
1691
  }
1369
1692
  const prior = replicas.get(room.locator);
1370
- if (base !== undefined &&
1371
- (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
1372
- prior.confirmed.record.revisions !== base.record.revisions)) {
1693
+ if (landing.kind === 'refreshed' &&
1694
+ (prior === undefined || prior.record.key !== record.key || prior.confirmed !== landing.base)) {
1695
+ await local.unrefresh(record.key);
1373
1696
  return false;
1374
1697
  }
1375
- adoptModels(room.locator, confirmed.record);
1698
+ adoptModels(room.locator, record);
1376
1699
  const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
1377
- const roomVersion = confirmed.record.roomVersion;
1700
+ const roomVersion = record.roomVersion;
1378
1701
  const accepted = queue.filter((batch) => {
1379
- const accepted = pendings.get(batch.id)?.accepted;
1380
- return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
1702
+ const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
1703
+ return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
1381
1704
  });
1382
- let settled;
1705
+ let set;
1383
1706
  try {
1384
- settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
1707
+ set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
1708
+ ? {
1709
+ room: true,
1710
+ view: 'refreshed',
1711
+ commit: { refreshed: true, composition: compositionOf(record) },
1712
+ }
1713
+ : {
1714
+ room: true,
1715
+ view: landing.refreshed ? 'refreshed' : 'confirmed',
1716
+ commit: {
1717
+ published: { from: prior?.record.key ?? null },
1718
+ composition: compositionOf(record),
1719
+ ...(landing.refreshed ? { refreshed: true } : {}),
1720
+ },
1721
+ });
1385
1722
  }
1386
1723
  catch (error) {
1387
- if (!exceeded(error))
1724
+ if (!refusedForSpace(error))
1388
1725
  throw error;
1389
- await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
1390
1726
  caughtUpTo(room.locator);
1391
1727
  return true;
1392
1728
  }
1393
- const set = settled();
1394
- if (stored !== undefined) {
1729
+ if (landing.kind === 'bootstrapped') {
1395
1730
  transfers.delete(room.locator);
1396
1731
  catching.delete(room.locator);
1732
+ sweep();
1397
1733
  }
1398
1734
  for (const batch of accepted)
1399
- pendings.delete(batch.id);
1735
+ pendings.delete(pendingKey(room.locator, batch.id));
1400
1736
  stale.delete(room.locator);
1401
- unindexable.delete(room.locator);
1737
+ unpreparable.delete(room.locator);
1402
1738
  if (unreadable.delete(room.locator))
1403
1739
  scopedRefusals.delete(room.locator);
1404
1740
  sourceRefusals.delete(room.locator);
1405
1741
  declarationRefusals.delete(room.locator);
1406
1742
  list(room, true);
1407
- if (prior === undefined)
1408
- prepared.add(room.locator);
1409
1743
  publishChangeSet(set);
1744
+ await unpair(local);
1410
1745
  moveTo({ ...status, ...attention() });
1411
1746
  options.adapter.roomStream?.published(room.locator, roomVersion);
1747
+ if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1748
+ submitLater(room.locator);
1749
+ }
1412
1750
  return true;
1413
1751
  });
1752
+ return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
1414
1753
  }
1415
1754
  async function savedRoom(locator) {
1416
1755
  const local = await openLocal();
1417
- await referenced(locator);
1418
1756
  const replica = replicas.get(locator);
1419
1757
  const room = rooms.get(locator);
1420
1758
  if (replica === undefined || room === undefined) {
@@ -1444,13 +1782,19 @@ export function composeRuntime(options) {
1444
1782
  }
1445
1783
  const target = await savedRoom(locator);
1446
1784
  const saved = [];
1785
+ const files = [];
1786
+ const filed = read.filter(({ parts }) => parts.file !== undefined).map(({ path }) => path);
1787
+ const visible = filed.length === 0 ? undefined : await visibleOf(locator, filed);
1447
1788
  for (const { path, parts } of read) {
1448
1789
  const file = parts.file === undefined
1449
1790
  ? undefined
1450
- : await heldFile(parts.file.blob, binaryBase(target.replica.visible, path));
1791
+ : await heldFile(parts.file.blob, binaryBase(visible, path));
1792
+ if (parts.file !== undefined && file !== undefined) {
1793
+ files.push({ key: file.localId, blob: parts.file.blob });
1794
+ }
1451
1795
  saved.push({ path, parts: held(undefined, parts, file) });
1452
1796
  }
1453
- return committed(locator, target, saved, new Set(), writtenBy);
1797
+ return committed(locator, target, saved, new Set(), writtenBy, files);
1454
1798
  }
1455
1799
  function carriedDrafts(locator, saved, consumed) {
1456
1800
  const holding = roomDrafts.get(locator);
@@ -1464,13 +1808,361 @@ export function composeRuntime(options) {
1464
1808
  return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
1465
1809
  }));
1466
1810
  }
1467
- async function committed(locator, { local, replica, room }, saved, consumed, writtenBy) {
1468
- const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
1811
+ async function savingIn(replica, changes, extra, leaving = []) {
1812
+ const record = replica.record;
1813
+ const local = await openLocal();
1814
+ const paths = [
1815
+ ...extra,
1816
+ ...changes.flatMap((change) => change.kind === 'move' ? [change.path, change.to] : [change.path]),
1817
+ ];
1818
+ const moved = [
1819
+ ...leaving,
1820
+ ...changes.flatMap((change) => (change.kind === 'move' ? [change.path] : [])),
1821
+ ];
1822
+ const removed = changes.flatMap((change) => (change.kind === 'remove' ? [change.path] : []));
1823
+ const referring = removed.length === 0
1824
+ ? []
1825
+ : (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
1826
+ const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
1827
+ const { visible } = await replayed(record, confirmedLayer, replica.queue);
1828
+ const composition = compositionOf(record);
1829
+ return {
1830
+ record,
1831
+ confirmed: confirmedLayer,
1832
+ visible,
1833
+ *visibleOver(queue) {
1834
+ return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
1835
+ },
1836
+ };
1837
+ }
1838
+ async function unmodeledIn(record) {
1839
+ const composition = compositionFor(record);
1840
+ if (available(composition))
1841
+ return false;
1842
+ const local = await openLocal();
1843
+ const roots = record.typedSelection?.roots ?? [];
1844
+ if (composition.model.status === 'unavailable' &&
1845
+ (await local.typedWithin(record.key, 'visible', roots, false))) {
1846
+ return true;
1847
+ }
1848
+ return (composition.selection !== null &&
1849
+ composition.selection.model.status === 'unavailable' &&
1850
+ (await local.typedWithin(record.key, 'visible', roots, true)));
1851
+ }
1852
+ function textDiffers(left, right) {
1853
+ return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
1854
+ !sameBinary(left?.binary ?? undefined, right?.binary ?? undefined));
1855
+ }
1856
+ function textOf(entry) {
1857
+ if (entry === null)
1858
+ return undefined;
1859
+ return entry.binary !== null ? null : entry.content;
1860
+ }
1861
+ function walkOf(local, key, view) {
1862
+ return async (after) => {
1863
+ const page = await local.walk(key, view, after, WALK_BYTES);
1864
+ return {
1865
+ entries: page.entries.map(({ path, content, binary }) => ({
1866
+ path,
1867
+ content: binary !== null ? null : content,
1868
+ })),
1869
+ next: page.next,
1870
+ };
1871
+ };
1872
+ }
1873
+ function lookupOf(local, key, view) {
1874
+ return async (paths) => {
1875
+ if (paths.length === 0)
1876
+ return new Map();
1877
+ return new Map((await local.typed(key, view, paths)).map(({ path, type, binary }) => [
1878
+ path,
1879
+ { type: type ?? undefined, binary },
1880
+ ]));
1881
+ };
1882
+ }
1883
+ function declaredOf(entry) {
1884
+ if (entry.binary !== null)
1885
+ return { type: undefined, binary: true };
1886
+ const read = parseFrontmatter(entry.content ?? '');
1887
+ return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
1888
+ }
1889
+ function overriddenWalk(walk, overrides) {
1890
+ const sorted = [...overrides.keys()].sort();
1891
+ const beyond = (path) => {
1892
+ if (path === null)
1893
+ return 0;
1894
+ let low = 0;
1895
+ let high = sorted.length;
1896
+ while (low < high) {
1897
+ const middle = (low + high) >>> 1;
1898
+ if (sorted[middle] <= path)
1899
+ low = middle + 1;
1900
+ else
1901
+ high = middle;
1902
+ }
1903
+ return low;
1904
+ };
1905
+ return async (after) => {
1906
+ const page = await walk(after);
1907
+ const end = page.next;
1908
+ const added = sorted
1909
+ .slice(beyond(after), end === null ? sorted.length : beyond(end))
1910
+ .flatMap((path) => {
1911
+ const entry = overrides.get(path);
1912
+ return entry === null || entry === undefined
1913
+ ? []
1914
+ : [{ path, content: entry.binary !== null ? null : entry.content }];
1915
+ });
1916
+ const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
1917
+ return { entries, next: end };
1918
+ };
1919
+ }
1920
+ function overriddenLookup(lookup, overrides) {
1921
+ return async (paths) => {
1922
+ const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
1923
+ for (const path of paths) {
1924
+ const entry = overrides.get(path);
1925
+ if (entry === undefined)
1926
+ continue;
1927
+ if (entry === null)
1928
+ known.delete(path);
1929
+ else
1930
+ known.set(path, declaredOf(entry));
1931
+ }
1932
+ return known;
1933
+ };
1934
+ }
1935
+ function validatorOf(record, confirmedLayer) {
1936
+ return async (resulting, touched) => {
1937
+ const composition = compositionFor(record);
1938
+ const local = await openLocal();
1939
+ const changed = [];
1940
+ const overrides = new Map();
1941
+ const pace = paced();
1942
+ for (const path of new Set([...confirmedLayer.held, ...touched])) {
1943
+ await pace(ROW_WEIGHT);
1944
+ const was = entryAt(confirmedLayer, path);
1945
+ const now = entryAt(resulting, path);
1946
+ if (!textDiffers(was, now))
1947
+ continue;
1948
+ overrides.set(path, now);
1949
+ if (textOf(was) !== textOf(now))
1950
+ changed.push({ path, was: textOf(was), now: textOf(now) });
1951
+ }
1952
+ const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
1953
+ const clean = available(composition) &&
1954
+ changed.length <= CLEAN_CHANGES &&
1955
+ (await changedCleanly(changed, compositionOf(record), composition, {
1956
+ lookup,
1957
+ texts: textsOf(local, record.key, 'confirmed'),
1958
+ referrers: referrersOf(local, record.key, 'confirmed'),
1959
+ }));
1960
+ if (clean)
1961
+ return { verdict: 'accept' };
1962
+ return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
1963
+ };
1964
+ }
1965
+ function textsOf(local, key, view) {
1966
+ return async (paths) => new Map((await local.entries(key, view, paths)).map(({ path, content, binary }) => [
1967
+ path,
1968
+ binary !== null ? null : content,
1969
+ ]));
1970
+ }
1971
+ function referrersOf(local, key, view) {
1972
+ return async (targets) => [
1973
+ ...new Set((await local.referrers(key, view, targets, CHECKED_LINKS)).map(({ path }) => path)),
1974
+ ];
1975
+ }
1976
+ async function sideOf(locator, replica, room, saving) {
1977
+ const record = replica.record;
1978
+ return {
1979
+ room: locator,
1980
+ handle: room.handle,
1981
+ baseRoomVersion: record.roomVersion,
1982
+ composition: compositionFor(record),
1983
+ operations: room.operations,
1984
+ providerOwnedPaths: room.providerOwnedPaths,
1985
+ visible: saving.visible,
1986
+ queued: queuedDocuments(replica.queue),
1987
+ unmodeled: await unmodeledIn(record),
1988
+ validate: validatorOf(record, saving.confirmed),
1989
+ };
1990
+ }
1991
+ function movedInto(locator, saved) {
1992
+ const into = saved.find(({ parts }) => parts.move?.room !== undefined && parts.move.room !== locator);
1993
+ return into?.parts.move?.room ?? null;
1994
+ }
1995
+ async function saveCommit(local, files, rooms) {
1996
+ const touched = new Map();
1997
+ const commits = [];
1998
+ for (const { locator, saving, before, queue } of rooms) {
1999
+ const replayedLayer = await replayed(saving.record, saving.confirmed, queue);
2000
+ const diff = await overlayDiff(saving.record, before, replayedLayer.visible, replayedLayer.touched);
2001
+ touched.set(locator, diff.touched);
2002
+ commits.push({
2003
+ locator,
2004
+ key: saving.record.key,
2005
+ roots: saving.record.typedSelection?.roots ?? [],
2006
+ overlay: diff.overlay,
2007
+ });
2008
+ }
2009
+ try {
2010
+ const answer = await local.commit({
2011
+ rooms: commits,
2012
+ writes: rooms.map(({ write }) => write),
2013
+ files,
2014
+ whole: true,
2015
+ });
2016
+ return { answer, touched };
2017
+ }
2018
+ catch {
2019
+ return null;
2020
+ }
2021
+ }
2022
+ function savedReplica(locator, before, queue, touched, answer) {
2023
+ replicas.set(locator, {
2024
+ ...before,
2025
+ queue,
2026
+ overlaid: touched,
2027
+ count: answer.counts[locator] ?? before.count,
2028
+ });
2029
+ }
2030
+ async function committedMove(locator, destination, { local, replica, room }, saved, consumed, writtenBy, files) {
2031
+ const mixed = saved.find(({ parts }) => Object.keys(parts).length !== 1 || parts.move?.room !== destination);
2032
+ if (mixed !== undefined) {
2033
+ return { outcome: 'refused', code: 'mixed-room-move', retryable: false, file: mixed.path };
2034
+ }
2035
+ const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
2036
+ const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
2037
+ const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, sourceSaving.visible)));
1469
2038
  if (changed.length > 0)
1470
2039
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
1471
- const parts = saved.map(({ path, parts }) => changesOf(path, parts));
1472
- const changes = parts.flatMap(({ changes }) => changes);
1473
- const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
2040
+ const target = replicas.get(destination);
2041
+ const listedRoom = rooms.get(destination);
2042
+ if (target === undefined || listedRoom === undefined) {
2043
+ throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
2044
+ }
2045
+ const arriving = await savingIn(target, [], moves.map(({ to }) => to));
2046
+ const built = await buildMove({
2047
+ source: await sideOf(locator, replica, room, sourceSaving),
2048
+ destination: await sideOf(destination, target, listedRoom, arriving),
2049
+ moves,
2050
+ providerLimits: limits,
2051
+ local: localSaves,
2052
+ ...(writtenBy === undefined ? {} : { writtenBy }),
2053
+ });
2054
+ if (built.kind === 'refused')
2055
+ return { outcome: 'refused', ...built.refusal };
2056
+ const id = built.leaving.id;
2057
+ const carrying = saved.flatMap(({ path, parts }) => {
2058
+ const record = roomDrafts.get(locator)?.get(path);
2059
+ const to = moveTarget(parts.move);
2060
+ if (record === undefined || consumed.has(path) || roomDrafts.get(destination)?.has(to)) {
2061
+ return [];
2062
+ }
2063
+ const moved = {
2064
+ ...record,
2065
+ locator: destination,
2066
+ path: to,
2067
+ revision: roomDraftRevision(),
2068
+ };
2069
+ return [{ from: path, record: moved }];
2070
+ });
2071
+ const carried = carrying.map(({ record }) => record);
2072
+ const marked = carried.map(({ path }) => ({ path, parts: {} }));
2073
+ const left = [...consumed, ...carrying.map(({ from }) => from)];
2074
+ const leavingQueue = [...replica.queue, built.leaving];
2075
+ const arrivingQueue = [...target.queue, built.arriving];
2076
+ const committedBoth = await saveCommit(local, files, [
2077
+ {
2078
+ locator,
2079
+ saving: sourceSaving,
2080
+ before: replica,
2081
+ queue: leavingQueue,
2082
+ write: {
2083
+ locator,
2084
+ queue: leavingQueue,
2085
+ saved: new Map([[id, saved]]),
2086
+ consumedRoomDrafts: left,
2087
+ },
2088
+ },
2089
+ {
2090
+ locator: destination,
2091
+ saving: arriving,
2092
+ before: target,
2093
+ queue: arrivingQueue,
2094
+ write: {
2095
+ locator: destination,
2096
+ queue: arrivingQueue,
2097
+ saved: new Map([[id, marked]]),
2098
+ roomDrafts: carried,
2099
+ },
2100
+ },
2101
+ ]);
2102
+ if (committedBoth === null)
2103
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
2104
+ const { answer, touched } = committedBoth;
2105
+ if (answer.fenced !== null) {
2106
+ throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2107
+ }
2108
+ if (fences.has(locator) || fences.has(destination)) {
2109
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2110
+ }
2111
+ pendings.set(pendingKey(locator, id), {
2112
+ drafts: saved,
2113
+ submission: { state: 'waiting' },
2114
+ lost: false,
2115
+ });
2116
+ pendings.set(pendingKey(destination, id), {
2117
+ drafts: marked,
2118
+ submission: { state: 'waiting' },
2119
+ lost: false,
2120
+ });
2121
+ for (const path of left)
2122
+ forgetRoomDraft(locator, path);
2123
+ for (const record of carried)
2124
+ rememberRoomDraft(record);
2125
+ savedReplica(locator, replica, leavingQueue, touched.get(locator), answer);
2126
+ savedReplica(destination, target, arrivingQueue, touched.get(destination), answer);
2127
+ publishChangeSet(mergeChangeSets([
2128
+ changeSet(locator, {
2129
+ documents: answer.changed[locator] ?? [],
2130
+ queue: true,
2131
+ drafts: left,
2132
+ }),
2133
+ changeSet(destination, {
2134
+ documents: answer.changed[destination] ?? [],
2135
+ queue: true,
2136
+ drafts: carried.map(({ path }) => path),
2137
+ }),
2138
+ ]));
2139
+ queuedSave(locator);
2140
+ return { outcome: 'saved', identifier: id };
2141
+ }
2142
+ function queuedSave(locator) {
2143
+ if (!localSaves)
2144
+ recorded = false;
2145
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
2146
+ submitLater(locator);
2147
+ }
2148
+ async function committed(locator, target, saved, consumed, writtenBy, files = []) {
2149
+ if (full)
2150
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
2151
+ const into = movedInto(locator, saved);
2152
+ if (into !== null) {
2153
+ return committedMove(locator, into, target, saved, consumed, writtenBy, files);
2154
+ }
2155
+ const { local, replica, room } = target;
2156
+ const parts = saved.map(({ path, parts: held }) => changesOf(path, held));
2157
+ const changes = parts.flatMap(({ changes: made }) => made);
2158
+ const saving = await savingIn(replica, changes, [
2159
+ ...saved.map(({ path }) => path),
2160
+ ...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
2161
+ ]);
2162
+ const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, underneath(held, path, saving.visible)));
2163
+ if (changed.length > 0)
2164
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2165
+ const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
1474
2166
  if (requeue.kind === 'refused')
1475
2167
  return { outcome: 'refused', ...requeue.refusal };
1476
2168
  const foreign = [...requeue.withdrawn.keys()].some((origin) => {
@@ -1488,22 +2180,19 @@ export function composeRuntime(options) {
1488
2180
  const withdrawing = requeue.withdrawn.size > 0;
1489
2181
  const input = {
1490
2182
  local: localSaves,
1491
- baseRoomVersion: replica.confirmed.record.roomVersion,
1492
- composition: compositionFor(replica.confirmed.record),
2183
+ baseRoomVersion: replica.record.roomVersion,
2184
+ composition: compositionFor(replica.record),
1493
2185
  operations: room.operations,
1494
2186
  providerOwnedPaths: room.providerOwnedPaths,
1495
2187
  providerLimits: limits,
1496
2188
  handle: room.handle,
1497
- visible: replica.visible,
2189
+ visible: saving.visible,
1498
2190
  changes,
1499
2191
  bases: parts.flatMap(({ bases }) => bases),
1500
2192
  queued: queuedDocuments(replica.queue),
1501
2193
  writtenBy,
1502
- clean: {
1503
- text: replica.confirmed.record.documents,
1504
- binaries: replica.confirmed.record.binaries,
1505
- composition: compositionOf(replica.confirmed.record),
1506
- },
2194
+ unmodeled: await unmodeledIn(replica.record),
2195
+ validate: validatorOf(replica.record, saving.confirmed),
1507
2196
  };
1508
2197
  const checked = withdrawing
1509
2198
  ? await buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
@@ -1528,8 +2217,7 @@ export function composeRuntime(options) {
1528
2217
  ? null
1529
2218
  : await buildBatch({
1530
2219
  ...input,
1531
- visible: (await stepped(replicating(replica.confirmed, requeue.queue, replica)))
1532
- .visible,
2220
+ visible: await stepped(saving.visibleOver(requeue.queue)),
1533
2221
  changes: requeue.changes,
1534
2222
  queued: queuedDocuments(requeue.queue),
1535
2223
  });
@@ -1548,7 +2236,7 @@ export function composeRuntime(options) {
1548
2236
  for (const [origin, replaced] of withdrawn) {
1549
2237
  if (replaced === null)
1550
2238
  continue;
1551
- const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
2239
+ const carried = withdrawnDrafts(pendings.get(pendingKey(locator, origin))?.drafts ?? [], changes);
1552
2240
  drafted.set(replaced.id, replaced === join?.batch
1553
2241
  ? [
1554
2242
  ...carried.map((held) => joinedDraft(held, saved)),
@@ -1562,66 +2250,77 @@ export function composeRuntime(options) {
1562
2250
  for (const replaced of withdrawn.values()) {
1563
2251
  if (replaced === null || !batches.includes(replaced))
1564
2252
  continue;
1565
- const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
2253
+ const limited = await stepped(limiting(batches, replaced, limits, saving.visibleOver));
1566
2254
  if (limited !== null)
1567
2255
  return { outcome: 'refused', ...limited };
1568
2256
  }
1569
2257
  const carried = carriedDrafts(locator, saved, consumed);
1570
2258
  const left = [...consumed, ...carried.keys()];
1571
2259
  const arrived = [...carried.values()];
1572
- try {
1573
- await local.keepAuthored({
2260
+ const done = await saveCommit(local, files, [
2261
+ {
1574
2262
  locator,
2263
+ saving,
2264
+ before: replica,
1575
2265
  queue: batches,
1576
- saved: drafted,
1577
- consumedRoomDrafts: left,
1578
- roomDrafts: arrived,
1579
- });
1580
- }
1581
- catch {
2266
+ write: {
2267
+ locator,
2268
+ queue: batches,
2269
+ saved: drafted,
2270
+ consumedRoomDrafts: left,
2271
+ roomDrafts: arrived,
2272
+ },
2273
+ },
2274
+ ]);
2275
+ if (done === null)
1582
2276
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2277
+ const { answer, touched } = done;
2278
+ if (answer.fenced !== null) {
2279
+ throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
1583
2280
  }
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
2281
  if (fences.has(locator))
1590
2282
  throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1591
2283
  for (const origin of withdrawn.keys())
1592
- pendings.delete(origin);
2284
+ pendings.delete(pendingKey(locator, origin));
1593
2285
  for (const [id, drafts] of drafted) {
1594
- pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
2286
+ pendings.set(pendingKey(locator, id), {
2287
+ drafts,
2288
+ submission: { state: 'waiting' },
2289
+ lost: false,
2290
+ });
1595
2291
  }
1596
- if (!localSaves)
1597
- recorded = false;
1598
2292
  for (const path of left)
1599
2293
  forgetRoomDraft(locator, path);
1600
2294
  for (const record of arrived)
1601
2295
  rememberRoomDraft(record);
1602
- replicas.set(locator, next);
1603
- publishChangeSet(set);
1604
- moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
1605
- submitLater(locator);
2296
+ savedReplica(locator, replica, batches, touched.get(locator), answer);
2297
+ publishChangeSet(changeSet(locator, {
2298
+ documents: answer.changed[locator] ?? [],
2299
+ queue: true,
2300
+ drafts: [...left, ...arrived.map(({ path }) => path)],
2301
+ }));
2302
+ queuedSave(locator);
1606
2303
  const identifier = join?.batch.id ??
1607
2304
  built?.batch.id ??
1608
2305
  [...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
1609
2306
  return { outcome: 'saved', identifier };
1610
2307
  }
1611
- async function mark(local, locator, identifier, submission, lost) {
1612
- const state = pendings.get(identifier);
1613
- if (!continuing() ||
1614
- state === undefined ||
1615
- (JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
1616
- return;
1617
- }
1618
- await local.submit(locator, identifier, submission, lost);
1619
- if (fences.has(locator))
1620
- return;
1621
- pendings.set(identifier, { ...state, submission, lost });
1622
- const replica = replicas.get(locator);
1623
- if (replica !== undefined)
1624
- publishChangeSet(changeSet(locator, replica, replica, { queue: true }));
2308
+ async function mark(local, locator, batch, submission, lost) {
2309
+ const partner = partnerOf(batch, locator);
2310
+ for (const held of partner === null ? [locator] : [locator, partner]) {
2311
+ const state = pendings.get(pendingKey(held, batch.id));
2312
+ if (!continuing() ||
2313
+ state === undefined ||
2314
+ (JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
2315
+ continue;
2316
+ }
2317
+ await local.submit(held, batch.id, submission, lost);
2318
+ if (fences.has(held))
2319
+ continue;
2320
+ pendings.set(pendingKey(held, batch.id), { ...state, submission, lost });
2321
+ if (replicas.has(held))
2322
+ publishChangeSet(changeSet(held, { queue: true }));
2323
+ }
1625
2324
  moveTo({ ...status, ...attention() });
1626
2325
  }
1627
2326
  async function keepUpload(local, locator, identifier, upload) {
@@ -1630,7 +2329,8 @@ export function composeRuntime(options) {
1630
2329
  await local.keepUpload(locator, identifier, upload);
1631
2330
  if (fences.has(locator))
1632
2331
  throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1633
- pendings.set(identifier, { ...pendings.get(identifier), upload });
2332
+ const key = pendingKey(locator, identifier);
2333
+ pendings.set(key, { ...pendings.get(key), upload });
1634
2334
  if (!continuing())
1635
2335
  throw new Error('connection refused');
1636
2336
  }
@@ -1663,7 +2363,7 @@ export function composeRuntime(options) {
1663
2363
  if (fencedSince(locator, since))
1664
2364
  return failure({ kind: 'room-not-readable', locator });
1665
2365
  if (result.kind === 'scoped-refusal') {
1666
- const room = replicas.get(result.locator)?.confirmed.record;
2366
+ const room = replicas.get(result.locator)?.record;
1667
2367
  if (room !== undefined)
1668
2368
  await scoped(result, room, await openLocal());
1669
2369
  }
@@ -1676,19 +2376,55 @@ export function composeRuntime(options) {
1676
2376
  return UNCERTAIN;
1677
2377
  }
1678
2378
  }
1679
- function queuedBlob(locator, path, localId) {
2379
+ function located(room, path, destination) {
2380
+ const crossing = roomTarget(destination);
2381
+ if (crossing === null) {
2382
+ const named = target(path, destination);
2383
+ return named === null || !rooms.has(room) ? null : { room, path: named };
2384
+ }
2385
+ const held = [...rooms.values()].find((listed) => listed.handle === crossing.handle);
2386
+ return held === undefined ? null : { room: held.locator, path: crossing.path };
2387
+ }
2388
+ async function resolveReference(room, path, destination) {
2389
+ if (roomTarget(destination) === null && target(path, destination) === null) {
2390
+ return ABSENT_TARGET;
2391
+ }
2392
+ const at = located(room, path, destination);
2393
+ if (at === null)
2394
+ return UNREADABLE;
2395
+ if (locked(at.room))
2396
+ return LOCKED_UNREADABLE;
2397
+ const held = replicas.get(at.room);
2398
+ if (held !== undefined &&
2399
+ (await (await openLocal()).entries(held.record.key, 'visible', [at.path])).length > 0) {
2400
+ return { kind: 'document', ...at };
2401
+ }
2402
+ const history = await readHistory(at.room, at.path);
2403
+ if (history.kind === 'no-history' || history.kind === 'revision')
2404
+ return ABSENT_TARGET;
2405
+ if (history.kind !== 'history')
2406
+ return history;
2407
+ const [last] = history.revisions;
2408
+ if (last === undefined)
2409
+ return ABSENT_TARGET;
2410
+ return history.deleted === true
2411
+ ? { kind: 'deleted', ...at, revision: last }
2412
+ : { kind: 'document', ...at };
2413
+ }
2414
+ function queuedFacts(locator, path, localId) {
1680
2415
  for (const batch of replicas.get(locator)?.queue ?? []) {
1681
2416
  const change = batch.changes[queuedFile(batch, path, localId)];
1682
2417
  if (change?.kind === 'binary')
1683
- return change.blob;
2418
+ return change;
1684
2419
  }
1685
- return null;
2420
+ const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2421
+ return drafted?.localId === localId ? drafted : null;
1686
2422
  }
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;
2423
+ function batchFile(batch) {
2424
+ return async (localId) => {
2425
+ const change = batch.changes.find((held) => held.kind === 'binary' && held.localId === localId);
2426
+ return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
2427
+ };
1692
2428
  }
1693
2429
  async function transferBinary(request, transfer, answer) {
1694
2430
  if (closed || refusal !== null)
@@ -1702,18 +2438,15 @@ export function composeRuntime(options) {
1702
2438
  if (closed || authorization === undefined || refusal !== null)
1703
2439
  return UNCERTAIN;
1704
2440
  const since = fencings;
1705
- journal.record(request.kind === 'path'
1706
- ? `transfer binary ${request.locator} ${request.path}`
1707
- : `transfer binary ${request.fileRef}`);
2441
+ journal.record(`transfer binary ${request.locator} ${request.path}`);
1708
2442
  try {
1709
2443
  const result = await transfer(authorization);
1710
2444
  const settled = result;
1711
2445
  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 });
2446
+ if (fencedSince(request.locator, since))
2447
+ return failure({ kind: 'room-not-readable', locator: request.locator });
1715
2448
  if (settled.kind === 'scoped-refusal') {
1716
- const room = replicas.get(settled.locator)?.confirmed.record;
2449
+ const room = replicas.get(settled.locator)?.record;
1717
2450
  if (room !== undefined)
1718
2451
  await scoped(settled, room, await openLocal());
1719
2452
  }
@@ -1726,68 +2459,88 @@ export function composeRuntime(options) {
1726
2459
  return UNCERTAIN;
1727
2460
  }
1728
2461
  }
1729
- function readBinary(request) {
1730
- return transferBinary(request, (held) => options.adapter.binary(held, request), binaryAnswer);
2462
+ async function readBinary(request, name) {
2463
+ if (closed)
2464
+ return UNCERTAIN;
2465
+ const held = await keptFiles.find(name);
2466
+ if (held !== null)
2467
+ return held;
2468
+ return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
1731
2469
  }
1732
- function binaryPath(locator, path, revision) {
2470
+ async function binaryPath(locator, path, revision) {
1733
2471
  if (revision !== undefined)
1734
2472
  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)
2473
+ const replica = replicas.get(locator);
2474
+ if (replica === undefined)
2475
+ return null;
2476
+ const local = await openLocal();
2477
+ const [held] = await local.entries(replica.record.key, 'visible', [path]);
2478
+ if (held?.binary === undefined || held.binary === null)
1738
2479
  return null;
1739
- if (held.state === 'pending')
1740
- return { kind: 'pending', localId: held.localId };
1741
- const record = binaryRecord(path, held, visible.revisions[path]);
2480
+ if (held.binary.state === 'pending')
2481
+ return { kind: 'pending', localId: held.binary.localId };
2482
+ const record = binaryRecord(path, held.binary, held.revision ?? undefined);
1742
2483
  if (record.state !== 'accepted')
1743
2484
  return null;
1744
- const replica = replicas.get(locator);
1745
- const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
2485
+ const queued = replica.queue.flatMap((batch) => batch.changes);
2486
+ const sources = queued.flatMap((change) => (change.kind === 'move' ? [change.path] : []));
2487
+ const confirmedLayer = await stepped(slicing(await local.entries(replica.record.key, 'confirmed', [path, ...sources]), [
2488
+ path,
2489
+ ...sources,
2490
+ ]));
1746
2491
  const holds = (at) => {
1747
- const kept = replica?.confirmed.record.binaries[at];
2492
+ const kept = confirmedLayer.binaries[at];
1748
2493
  return (kept?.state === 'accepted' &&
1749
- kept.fileRef === record.fileRef &&
1750
- replica?.confirmed.record.revisions[at] === record.revision);
2494
+ kept.sha256 === record.sha256 &&
2495
+ confirmedLayer.revisions[at] === record.revision);
1751
2496
  };
1752
- let confirmed = path;
1753
- for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
2497
+ let confirmedAt = path;
2498
+ for (let index = queued.length - 1; index >= 0 && !holds(confirmedAt); index -= 1) {
1754
2499
  const change = queued[index];
1755
- if (change.kind === 'move' && change.to === confirmed)
1756
- confirmed = change.path;
2500
+ if (change.kind === 'move' && change.to === confirmedAt)
2501
+ confirmedAt = change.path;
1757
2502
  }
1758
2503
  return {
1759
2504
  kind: 'path',
1760
2505
  locator,
1761
- path: confirmed,
2506
+ path: confirmedAt,
1762
2507
  revision: record.revision,
1763
- fileRef: record.fileRef,
1764
2508
  };
1765
2509
  }
1766
2510
  async function readBinaryPath(locator, path, revision) {
1767
2511
  await ready;
1768
- const asked = binaryPath(locator, path, revision);
2512
+ const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2513
+ const asked = drafted !== undefined && drafted.localId === revision
2514
+ ? { kind: 'pending', localId: revision }
2515
+ : await binaryPath(locator, path, revision);
1769
2516
  if (asked === null)
1770
2517
  return absentBinary(path);
1771
2518
  if (asked.kind === 'pending') {
1772
- const blob = queuedBlob(locator, path, asked.localId);
1773
- return blob === null ? absentBinary(path) : { kind: 'binary', blob };
2519
+ const facts = queuedFacts(locator, path, asked.localId);
2520
+ return facts === null
2521
+ ? absentBinary(path)
2522
+ : {
2523
+ kind: 'binary',
2524
+ file: asked.localId,
2525
+ byteLength: facts.byteLength,
2526
+ mediaType: facts.mediaType,
2527
+ };
1774
2528
  }
1775
- return readBinary(asked);
2529
+ return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
1776
2530
  }
1777
2531
  async function linkBinaryPath(locator, path, revision) {
1778
2532
  await ready;
1779
- const asked = binaryPath(locator, path, revision);
2533
+ const asked = await binaryPath(locator, path, revision);
1780
2534
  if (asked === null || asked.kind === 'pending')
1781
2535
  return absentBinary(path);
1782
2536
  return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
1783
2537
  }
1784
- async function readBinaryReference(reference) {
1785
- await ready;
1786
- return readBinary({
1787
- kind: 'reference',
1788
- fileRef: reference.fileRef,
1789
- referrer: reference.referrer,
1790
- });
2538
+ function readPart(file, part) {
2539
+ return database.ask({ kind: 'file.part', key: file, part });
2540
+ }
2541
+ async function cleanUp() {
2542
+ const freed = await database.ask({ kind: 'opened.clean' });
2543
+ return freed + ((await options.shell.cleanUp?.()) ?? 0);
1791
2544
  }
1792
2545
  function declaredAgents() {
1793
2546
  for (const capability of capabilities) {
@@ -1805,6 +2558,13 @@ export function composeRuntime(options) {
1805
2558
  }
1806
2559
  return undefined;
1807
2560
  }
2561
+ function calledRooms(name, input) {
2562
+ const member = declaredAction(name)?.room;
2563
+ if (member === undefined || input === null || typeof input !== 'object')
2564
+ return [];
2565
+ const named = input[member];
2566
+ return (Array.isArray(named) ? named : [named]).filter((room) => typeof room === 'string');
2567
+ }
1808
2568
  async function invokeAction(name, input, dispatching) {
1809
2569
  await ready;
1810
2570
  if (closed || refusal !== null)
@@ -1824,7 +2584,7 @@ export function composeRuntime(options) {
1824
2584
  try {
1825
2585
  const result = await options.adapter.action(authorization, { name, input });
1826
2586
  if (result.kind === 'scoped-refusal') {
1827
- const room = replicas.get(result.locator)?.confirmed.record;
2587
+ const room = replicas.get(result.locator)?.record;
1828
2588
  if (room !== undefined)
1829
2589
  await scoped(result, room, await openLocal());
1830
2590
  }
@@ -1899,93 +2659,426 @@ export function composeRuntime(options) {
1899
2659
  }
1900
2660
  function reachedBy(locator, roomVersion) {
1901
2661
  const held = replicas.get(locator);
1902
- return (held !== undefined &&
1903
- options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion));
2662
+ return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
1904
2663
  }
1905
- function writtenAfter(locator, roomVersion, path) {
1906
- const written = replicas.get(locator)?.confirmed.record.written[path];
2664
+ async function writtenAfter(locator, roomVersion, path) {
2665
+ const held = replicas.get(locator);
2666
+ if (held === undefined)
2667
+ return false;
2668
+ const [entry] = await (await openLocal()).entries(held.record.key, 'confirmed', [path]);
2669
+ const written = entry?.written ?? undefined;
1907
2670
  return (written !== undefined &&
1908
2671
  options.adapter.reached(locator, written, roomVersion) &&
1909
2672
  !options.adapter.reached(locator, roomVersion, written));
1910
2673
  }
1911
- async function pullRefresh(authorized, confirmed, live) {
1912
- if (!live())
2674
+ function overriddenTexts(texts, overrides) {
2675
+ return async (paths) => {
2676
+ const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
2677
+ for (const path of paths) {
2678
+ const entry = overrides.get(path);
2679
+ if (entry === undefined)
2680
+ continue;
2681
+ if (entry === null)
2682
+ known.delete(path);
2683
+ else
2684
+ known.set(path, entry.binary !== null ? null : entry.content);
2685
+ }
2686
+ return known;
2687
+ };
2688
+ }
2689
+ async function unreadableRoom(texts, walk, paths) {
2690
+ if (paths !== null) {
2691
+ const text = {};
2692
+ for (const [path, content] of await texts(paths))
2693
+ if (content !== null)
2694
+ text[path] = content;
2695
+ return stepped(unreadableIn(text, Object.keys(text)));
2696
+ }
2697
+ for (let after = null;;) {
2698
+ const page = await walk(after);
2699
+ const text = {};
2700
+ for (const { path, content } of page.entries)
2701
+ if (content !== null)
2702
+ text[path] = content;
2703
+ const refused = await stepped(unreadableIn(text, Object.keys(text)));
2704
+ if (refused !== null)
2705
+ return refused;
2706
+ if (page.next === null)
2707
+ return null;
2708
+ after = page.next;
2709
+ }
2710
+ }
2711
+ async function validatedRoom(local, record, view, previous, overrides = null) {
2712
+ const composition = compositionOf(record);
2713
+ const modeled = record.model.status !== 'unavailable' || record.selectionModel.status !== 'unavailable';
2714
+ const lookup = overrides === null
2715
+ ? lookupOf(local, record.key, view)
2716
+ : overriddenLookup(lookupOf(local, record.key, view), overrides);
2717
+ const texts = overrides === null
2718
+ ? textsOf(local, record.key, view)
2719
+ : overriddenTexts(textsOf(local, record.key, view), overrides);
2720
+ const walk = overrides === null
2721
+ ? walkOf(local, record.key, view)
2722
+ : overriddenWalk(walkOf(local, record.key, view), overrides);
2723
+ let changes = null;
2724
+ let reshaped = false;
2725
+ if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
2726
+ const [was, now] = await Promise.all([
2727
+ local.entries(record.key, 'confirmed', previous.changed),
2728
+ overrides === null
2729
+ ? local.entries(record.key, view, previous.changed)
2730
+ : Promise.resolve([]),
2731
+ ]);
2732
+ const before = new Map(was.map((entry) => [entry.path, entry]));
2733
+ const after = overrides === null
2734
+ ? new Map(now.map((entry) => [entry.path, entry]))
2735
+ : overrides;
2736
+ changes = previous.changed.flatMap((path) => {
2737
+ const one = before.get(path) ?? null;
2738
+ const other = after.get(path) ?? null;
2739
+ reshaped ||= textDiffers(one, other);
2740
+ const held = textOf(one);
2741
+ const next = textOf(other);
2742
+ return held === next ? [] : [{ path, was: held, now: next }];
2743
+ });
2744
+ }
2745
+ const named = changes === null ? null : changes.map(({ path }) => path);
2746
+ const textChanged = changes === null || changes.length > 0 || reshaped;
2747
+ const modelsChanged = previous === null || !sameComposition(compositionOf(previous.record), composition);
2748
+ if (modeled && (textChanged || modelsChanged)) {
2749
+ const clean = previous !== null &&
2750
+ changes !== null &&
2751
+ (await changedCleanly(changes, compositionOf(previous.record), composition, {
2752
+ lookup,
2753
+ texts,
2754
+ referrers: referrersOf(local, record.key, 'confirmed'),
2755
+ }));
2756
+ if (clean)
2757
+ return null;
2758
+ const verdict = await validatedCollection(walk, lookup, composition);
2759
+ if (verdict.verdict === 'accept')
2760
+ return null;
2761
+ if (verdict.rule !== 'model-unavailable')
2762
+ return { kind: 'refused', refusal: verdict };
2763
+ return unreadableRoom(texts, walk, named);
2764
+ }
2765
+ if (textChanged)
2766
+ return unreadableRoom(texts, walk, named);
2767
+ return null;
2768
+ }
2769
+ function refreshingOf(local, key, liveness, replaying = null) {
2770
+ const write = replaying === null ? exclusive : installWrite;
2771
+ return {
2772
+ async turn() {
2773
+ if (replaying === null)
2774
+ return;
2775
+ installing(replaying, 'replaying');
2776
+ await screened();
2777
+ },
2778
+ prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
2779
+ async stage(page) {
2780
+ for (let written = 0;;) {
2781
+ if (replaying !== null)
2782
+ await screened();
2783
+ const end = batchEnd(page.entries, written);
2784
+ const last = end === page.entries.length;
2785
+ const entries = page.entries.slice(written, end);
2786
+ const staged = await write(async () => {
2787
+ if (!liveness())
2788
+ return false;
2789
+ await timed(() => local.refresh(key, last
2790
+ ? { ...page, entries }
2791
+ : { entries, removals: [], folders: [], folderRemovals: [] }));
2792
+ return true;
2793
+ });
2794
+ if (!staged)
2795
+ return false;
2796
+ if (last)
2797
+ return true;
2798
+ written = end;
2799
+ }
2800
+ },
2801
+ };
2802
+ }
2803
+ function batchEnd(entries, from) {
2804
+ let end = from;
2805
+ let spent = 0;
2806
+ while (end < entries.length) {
2807
+ const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
2808
+ if (spent + size > stageBytes && end > from)
2809
+ break;
2810
+ spent += size;
2811
+ end += 1;
2812
+ }
2813
+ return end;
2814
+ }
2815
+ async function timed(work) {
2816
+ const started = performance.now();
2817
+ const done = await work();
2818
+ const spent = performance.now() - started;
2819
+ if (spent < STAGE_AIM_MS / 2)
2820
+ stageBytes = Math.min(STAGE_BYTES, Math.round(stageBytes * 1.5));
2821
+ else if (spent > STAGE_AIM_MS * 2) {
2822
+ stageBytes = Math.max(STAGE_FLOOR, Math.round(stageBytes / 1.5));
2823
+ }
2824
+ return done;
2825
+ }
2826
+ function installWrite(work) {
2827
+ const next = installWrites.then(() => exclusive(work), () => exclusive(work));
2828
+ installWrites = next.then(() => undefined, () => undefined);
2829
+ return next;
2830
+ }
2831
+ function screened() {
2832
+ if (onScreen || closed)
2833
+ return Promise.resolve();
2834
+ queueReadiness();
2835
+ return new Promise((resolve) => shownWaiters.push(resolve));
2836
+ }
2837
+ async function heldPart() {
2838
+ while (partsFree === 0)
2839
+ await new Promise((admit) => partWaiters.push(admit));
2840
+ partsFree -= 1;
2841
+ return freed();
2842
+ }
2843
+ function freed() {
2844
+ let released = false;
2845
+ return () => {
2846
+ if (released)
2847
+ return;
2848
+ released = true;
2849
+ partsFree += 1;
2850
+ partWaiters.shift()?.();
2851
+ };
2852
+ }
2853
+ function installing(locator, step, share = null) {
2854
+ installs.set(locator, { step, share });
2855
+ queueReadiness();
2856
+ }
2857
+ function installed(locator) {
2858
+ if (installs.delete(locator))
2859
+ queueReadiness();
2860
+ }
2861
+ function screen(value) {
2862
+ if (value === onScreen)
2863
+ return;
2864
+ onScreen = value;
2865
+ journal.record(`installation ${value ? 'shown' : 'hidden'}`);
2866
+ queueReadiness();
2867
+ if (!value)
2868
+ return;
2869
+ for (const resolve of shownWaiters.splice(0))
2870
+ resolve();
2871
+ }
2872
+ async function pullRefresh(authorized, held, liveness) {
2873
+ if (!liveness())
1913
2874
  return unconfirmed;
1914
2875
  const turn = generation;
2876
+ const { record } = held;
2877
+ const base = held.confirmed;
2878
+ const local = await openLocal();
1915
2879
  try {
1916
- const result = await refreshRoom(reads, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1917
- const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
1918
- journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
1919
- return result;
2880
+ await exclusive(() => local.unrefresh(record.key));
2881
+ const result = await advancement(reads, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, liveness));
2882
+ if (result.kind !== 'advanced') {
2883
+ journal.record(`transfer refresh ${record.locator} ${result.kind}`);
2884
+ return result;
2885
+ }
2886
+ const refused = await validatedRoom(local, result.record, 'refreshed', {
2887
+ record,
2888
+ changed: [...result.changed],
2889
+ });
2890
+ if (refused !== null) {
2891
+ journal.record(`transfer refresh ${record.locator} refused`);
2892
+ return refused;
2893
+ }
2894
+ journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
2895
+ return { kind: 'advanced', record: result.record, base };
1920
2896
  }
1921
2897
  catch (error) {
1922
- journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
2898
+ if (refusedForSpace(error))
2899
+ return unconfirmed;
2900
+ journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
1923
2901
  throw error;
1924
2902
  }
1925
2903
  finally {
1926
- reads.finished(confirmed.record.locator, turn);
1927
- }
2904
+ reads.finished(record.locator, turn);
2905
+ }
2906
+ }
2907
+ const heldContents = new Map();
2908
+ function heldContentOf(local, key) {
2909
+ const known = heldContents.get(key);
2910
+ if (known !== undefined)
2911
+ return known;
2912
+ if (heldContents.size > ROOM_READS)
2913
+ heldContents.clear();
2914
+ const made = {
2915
+ files: (after, limit) => local.files(key, 'confirmed', after, limit),
2916
+ async contents(paths) {
2917
+ return new Map((await local.entries(key, 'confirmed', paths)).flatMap(({ path, content, revision }) => content === null || revision === null ? [] : [[path, { content, revision }]]));
2918
+ },
2919
+ };
2920
+ heldContents.set(key, made);
2921
+ return made;
1928
2922
  }
1929
- function staging(local, live) {
2923
+ function staging(local, liveness) {
1930
2924
  return {
1931
2925
  held: (locator) => transfers.get(locator) ?? null,
1932
- staged: (record) => local.staged(record),
1933
- stage: (record, page) => exclusive(async () => {
1934
- if (!live())
1935
- return false;
1936
- const bytes = layerBytes(page);
1937
- if (!(await admitted(local, record.locator, bytes)))
1938
- return false;
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;
2926
+ async turn(locator, step) {
2927
+ installing(locator, step);
2928
+ for (;;) {
2929
+ await screened();
2930
+ const release = step === 'downloading' ? await heldPart() : () => { };
2931
+ if (onScreen || closed)
2932
+ return release;
2933
+ release();
1947
2934
  }
1948
- transfers.set(record.locator, record);
1949
- readSide.recover();
1950
- return true;
2935
+ },
2936
+ spare() {
2937
+ if (!onScreen || closed || partsFree === 0)
2938
+ return null;
2939
+ partsFree -= 1;
2940
+ return freed();
2941
+ },
2942
+ arriving(locator) {
2943
+ let ended = false;
2944
+ return {
2945
+ at(token, bytes) {
2946
+ if (ended)
2947
+ return;
2948
+ const held = arrivals.get(locator) ?? new Map();
2949
+ arrivals.set(locator, held.set(token, bytes));
2950
+ const now = performance.now();
2951
+ if (now - arrivalReported < ARRIVAL_REPORT_MS)
2952
+ return;
2953
+ arrivalReported = now;
2954
+ queueReadiness();
2955
+ },
2956
+ end() {
2957
+ ended = true;
2958
+ if (arrivals.delete(locator))
2959
+ queueReadiness();
2960
+ },
2961
+ };
2962
+ },
2963
+ received: (locator, token) => local.transferPart(locator, token),
2964
+ keep: (locator, token, received) => installWrite(async () => {
2965
+ if (liveness())
2966
+ await local.keepPart(locator, token, received);
1951
2967
  }),
2968
+ announced(locator) {
2969
+ const later = announced.get(locator);
2970
+ return later === undefined ? [] : [later];
2971
+ },
2972
+ async stage(record, page, composition, received) {
2973
+ const { locator } = record;
2974
+ const whole = received?.byteLength ?? 0;
2975
+ const from = record.writing?.written ?? 0;
2976
+ const token = record.writing?.token ?? null;
2977
+ const entries = page.entries.length;
2978
+ let held = record;
2979
+ if (token !== null)
2980
+ arrivals.get(locator)?.delete(token);
2981
+ installing(locator, 'writing', { written: from, entries, bytes: whole });
2982
+ for (let written = from;;) {
2983
+ await screened();
2984
+ if (!liveness())
2985
+ return null;
2986
+ const end = batchEnd(page.entries, written);
2987
+ const last = end === entries;
2988
+ const next = last
2989
+ ? {
2990
+ ...held,
2991
+ writing: null,
2992
+ done: {
2993
+ parts: held.done.parts + (received === null ? 0 : 1),
2994
+ bytes: held.done.bytes + whole,
2995
+ },
2996
+ }
2997
+ : { ...held, writing: { token, written: end } };
2998
+ const key = await installWrite(async () => {
2999
+ if (!liveness())
3000
+ return null;
3001
+ return timed(() => local.stage(next, composition, {
3002
+ entries: page.entries.slice(written, end),
3003
+ folders: last ? page.folders : [],
3004
+ }, last && received !== null && token !== null ? token : undefined));
3005
+ });
3006
+ if (key === null)
3007
+ return null;
3008
+ const arrived = !transfers.has(locator);
3009
+ held = { ...next, key };
3010
+ transfers.set(locator, held);
3011
+ if (arrived)
3012
+ liveQueries.recover();
3013
+ if (last) {
3014
+ installing(locator, held.pageToken === null ? 'replaying' : 'downloading');
3015
+ liveQueries.recover();
3016
+ return held;
3017
+ }
3018
+ installing(locator, 'writing', { written: end, entries, bytes: whole });
3019
+ written = end;
3020
+ }
3021
+ },
3022
+ async refreshing(locator, key) {
3023
+ await exclusive(() => local.unrefresh(key));
3024
+ return refreshingOf(local, key, liveness, locator);
3025
+ },
1952
3026
  abandon: (locator) => exclusive(async () => {
1953
- if (!live())
3027
+ if (!liveness())
1954
3028
  return;
1955
3029
  await local.abandon(locator);
1956
3030
  transfers.delete(locator);
1957
- readSide.recover();
3031
+ liveQueries.recover();
3032
+ sweep();
1958
3033
  }),
3034
+ setAside: (locator) => exclusive(async () => {
3035
+ if (!liveness())
3036
+ return;
3037
+ await local.abandon(locator, true);
3038
+ transfers.delete(locator);
3039
+ liveQueries.recover();
3040
+ }),
3041
+ release: (key) => exclusive(async () => {
3042
+ await local.release(key);
3043
+ sweep();
3044
+ }),
3045
+ content: (key) => heldContentOf(local, key),
3046
+ validate: (record, view) => validatedRoom(local, record, view, null),
1959
3047
  };
1960
3048
  }
1961
- function reusable(room) {
1962
- const record = replicas.get(room.locator)?.confirmed.record;
3049
+ function reusable(room, local) {
3050
+ const record = replicas.get(room.locator)?.record;
1963
3051
  if (record === undefined || record.replica.selection !== room.replica.selection)
1964
3052
  return null;
1965
- return { documents: record.documents, revisions: record.revisions };
3053
+ return heldContentOf(local, record.key);
1966
3054
  }
1967
- async function pullBootstrap(authorized, room, local, live) {
1968
- if (!live())
3055
+ async function pullBootstrap(authorized, room, local, liveness) {
3056
+ if (!liveness())
1969
3057
  return unconfirmed;
1970
3058
  const turn = generation;
1971
3059
  if (replicas.has(room.locator)) {
1972
3060
  catching.add(room.locator);
1973
- readSide.recover();
3061
+ liveQueries.recover();
1974
3062
  }
1975
3063
  try {
1976
3064
  const result = await bootstrapRoom(reads, authorized, room, {
1977
3065
  collection: modelOf(room.locator),
1978
3066
  selection: modelOf(room.locator, 'typed-selection'),
1979
- }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
1980
- const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
3067
+ }, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
3068
+ const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
1981
3069
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1982
- if (result.kind !== 'staged')
3070
+ if (result.kind !== 'staged') {
1983
3071
  caughtUpTo(room.locator);
3072
+ installed(room.locator);
3073
+ }
1984
3074
  return result;
1985
3075
  }
1986
3076
  catch (error) {
1987
- journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
1988
3077
  caughtUpTo(room.locator);
3078
+ installed(room.locator);
3079
+ if (refusedForSpace(error))
3080
+ return unconfirmed;
3081
+ journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
1989
3082
  throw error;
1990
3083
  }
1991
3084
  finally {
@@ -1994,10 +3087,10 @@ export function composeRuntime(options) {
1994
3087
  }
1995
3088
  function caughtUpTo(locator) {
1996
3089
  if (catching.delete(locator))
1997
- readSide.recover();
3090
+ liveQueries.recover();
1998
3091
  }
1999
3092
  function behind(room) {
2000
- const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
3093
+ const held = replicas.get(room.locator)?.record.roomVersion;
2001
3094
  return (held !== undefined &&
2002
3095
  [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
2003
3096
  }
@@ -2009,13 +3102,13 @@ export function composeRuntime(options) {
2009
3102
  }
2010
3103
  return advanceOnce(room, local, authorized, live);
2011
3104
  }
2012
- async function advanceOnce(room, local, authorized, live) {
2013
- await referenced(room.locator);
2014
- if (!live() || paused.has(room.locator))
3105
+ async function advanceOnce(room, local, authorized, running) {
3106
+ const live = () => running() && !full;
3107
+ if (!live())
2015
3108
  return unconfirmed;
2016
3109
  const held = replicas.get(room.locator);
2017
3110
  if (held !== undefined) {
2018
- const refreshed = await pullRefresh(authorized, held.confirmed, live);
3111
+ const refreshed = await pullRefresh(authorized, held, live);
2019
3112
  if (!live())
2020
3113
  return unconfirmed;
2021
3114
  if (refreshed.kind === 'still-current') {
@@ -2030,10 +3123,11 @@ export function composeRuntime(options) {
2030
3123
  return null;
2031
3124
  }
2032
3125
  if (refreshed.kind === 'advanced') {
2033
- if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
2034
- const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
3126
+ const landing = { kind: 'refreshed', base: refreshed.base };
3127
+ if (!(await publish(local, room, refreshed.record, landing, live))) {
3128
+ const now = replicas.get(room.locator)?.record.roomVersion;
2035
3129
  const reached = now !== undefined &&
2036
- options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
3130
+ options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
2037
3131
  return reached ? null : advance(room, local, authorized, live);
2038
3132
  }
2039
3133
  await retainModel(local, room, 'collection', modelOf(room.locator));
@@ -2043,19 +3137,18 @@ export function composeRuntime(options) {
2043
3137
  return refreshed;
2044
3138
  if (opened !== null) {
2045
3139
  owed.add(room.locator);
2046
- readSide.recover();
3140
+ liveQueries.recover();
2047
3141
  return unconfirmed;
2048
3142
  }
2049
3143
  if (reloadsDue.get(room.locator) === generation)
2050
3144
  return unconfirmed;
2051
3145
  }
2052
3146
  const bootstrapped = await pullBootstrap(authorized, room, local, live);
2053
- if (!live())
2054
- return unconfirmed;
2055
- if (bootstrapped.kind !== 'staged')
2056
- return bootstrapped;
2057
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
2058
- return null;
3147
+ if (bootstrapped.kind === 'staged') {
3148
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3149
+ return live() ? null : unconfirmed;
3150
+ }
3151
+ return live() ? bootstrapped : unconfirmed;
2059
3152
  }
2060
3153
  async function withhold(withheld, room, local, progress) {
2061
3154
  if (withheld.kind === 'unconfirmed')
@@ -2127,57 +3220,254 @@ export function composeRuntime(options) {
2127
3220
  (accepted.author === undefined ? '' : ` author ${accepted.author}`) +
2128
3221
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
2129
3222
  }
2130
- async function adoptAccepted(room, local, submitted, accepted) {
2131
- await referenced(room.locator);
3223
+ function settling(acceptance) {
3224
+ return acceptance !== null && 'apply' in acceptance;
3225
+ }
3226
+ function refusalOf(acceptance) {
3227
+ return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
3228
+ }
3229
+ async function acceptance(room, local, submitted, accepted) {
2132
3230
  if (!continuing())
2133
3231
  return unconfirmed;
2134
3232
  const current = replicas.get(room.locator);
2135
3233
  if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
2136
3234
  return null;
2137
3235
  }
2138
- const held = current.confirmed.record.roomVersion;
3236
+ const held = current.record.roomVersion;
2139
3237
  logAccepted(room.locator, submitted, accepted);
2140
- const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
2141
- ? { kind: 'staged', confirmed: current.confirmed }
2142
- : await applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
2143
- ? accepted.roomVersion
2144
- : held, accepted.roomVersion);
2145
- if (applied.kind !== 'staged') {
2146
- const state = pendings.get(submitted.id);
2147
- await local.accept(room.locator, submitted.id, accepted.roomVersion);
2148
- if (state !== undefined && !fences.has(room.locator))
2149
- pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
2150
- emitQueue(room.locator);
2151
- return applied;
2152
- }
2153
- const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
2154
- pendings.delete(submitted.id);
2155
- unreadable.delete(room.locator);
2156
- sourceRefusals.delete(room.locator);
2157
- declarationRefusals.delete(room.locator);
2158
- const set = settled();
2159
- list(room, true);
2160
- publishChangeSet(set);
2161
- moveTo({ ...status, ...attention() });
2162
- return null;
3238
+ let record = current.record;
3239
+ let changes;
3240
+ if (!options.adapter.reached(room.locator, held, accepted.roomVersion)) {
3241
+ const paths = [...returnedPaths(submitted, accepted)];
3242
+ const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
3243
+ const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
3244
+ record = {
3245
+ ...record,
3246
+ roomVersion: options.adapter.follows(room.locator, held, accepted.roomVersion)
3247
+ ? accepted.roomVersion
3248
+ : held,
3249
+ };
3250
+ const overrides = new Map([
3251
+ ...back.set.map((entry) => [entry.path, entry]),
3252
+ ...back.removed.map((path) => [path, null]),
3253
+ ]);
3254
+ const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
3255
+ if (refused !== null) {
3256
+ const key = pendingKey(room.locator, submitted.id);
3257
+ return {
3258
+ locator: room.locator,
3259
+ left: [],
3260
+ commit: {
3261
+ rooms: [],
3262
+ writes: [
3263
+ {
3264
+ locator: room.locator,
3265
+ accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
3266
+ },
3267
+ ],
3268
+ },
3269
+ refused,
3270
+ apply() {
3271
+ const state = pendings.get(key);
3272
+ if (state !== undefined && !fences.has(room.locator))
3273
+ pendings.set(key, { ...state, accepted: accepted.roomVersion });
3274
+ return changeSet(room.locator, { queue: true });
3275
+ },
3276
+ };
3277
+ }
3278
+ changes = back;
3279
+ }
3280
+ const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
3281
+ return {
3282
+ ...settled,
3283
+ apply(answer) {
3284
+ const set = settled.apply(answer);
3285
+ pendings.delete(pendingKey(room.locator, submitted.id));
3286
+ unreadable.delete(room.locator);
3287
+ sourceRefusals.delete(room.locator);
3288
+ declarationRefusals.delete(room.locator);
3289
+ list(room, true);
3290
+ return set;
3291
+ },
3292
+ };
3293
+ }
3294
+ async function adoptedTogether(local, acceptances) {
3295
+ const settlements = acceptances.filter(settling);
3296
+ if (settlements.length > 0) {
3297
+ publishChangeSet(await together(local, settlements));
3298
+ moveTo({ ...status, ...attention() });
3299
+ }
3300
+ await unpair(local);
2163
3301
  }
2164
3302
  async function adopted(room, local, submitted, accepted) {
2165
- const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
3303
+ const failed = await exclusive(async () => {
3304
+ const adopting = await acceptance(room, local, submitted, accepted);
3305
+ await adoptedTogether(local, [adopting]);
3306
+ return refusalOf(adopting);
3307
+ });
3308
+ if (failed === null)
3309
+ return true;
3310
+ if (failed.kind === 'refused' && continuing())
3311
+ refusedSource(room, failed);
3312
+ return false;
3313
+ }
3314
+ async function adoptedMove(room, partner, local, submitted, accepted) {
3315
+ const other = discovered.get(partner);
3316
+ const failed = await exclusive(async () => {
3317
+ const left = await acceptance(room, local, submitted, accepted);
3318
+ const arriving = replicas.get(partner)?.queue.find((batch) => batch.id === submitted.id);
3319
+ const arrived = other === undefined || arriving === undefined || accepted.destination === undefined
3320
+ ? null
3321
+ : await acceptance(other, local, arriving, {
3322
+ kind: 'accepted',
3323
+ ...accepted.destination,
3324
+ });
3325
+ await adoptedTogether(local, [left, arrived]);
3326
+ const refusedArrival = refusalOf(arrived);
3327
+ if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
3328
+ refusedSource(other, refusedArrival);
3329
+ }
3330
+ return refusalOf(left);
3331
+ });
3332
+ submitLater(partner);
2166
3333
  if (failed === null)
2167
3334
  return true;
2168
3335
  if (failed.kind === 'refused' && continuing())
2169
3336
  refusedSource(room, failed);
2170
3337
  return false;
2171
3338
  }
3339
+ function queueIn(locator) {
3340
+ return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
3341
+ }
3342
+ function behindIn(locator, batch) {
3343
+ const held = replicas.get(locator)?.queue;
3344
+ if (held === undefined)
3345
+ return queuesWithoutReplica.get(locator) !== undefined;
3346
+ return held.findIndex((one) => one.id === batch.id) > 0;
3347
+ }
3348
+ function unpaired(skipped) {
3349
+ for (const locator of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
3350
+ for (const batch of queueIn(locator)) {
3351
+ const partner = partnerOf(batch, locator);
3352
+ if (partner === null || skipped.has(pendingKey(locator, batch.id)))
3353
+ continue;
3354
+ if (queueIn(partner).some((held) => held.id === batch.id))
3355
+ continue;
3356
+ if (unpairedIn(locator, batch))
3357
+ return { locator, batch };
3358
+ }
3359
+ }
3360
+ return null;
3361
+ }
3362
+ async function unpairing(local, locator, batches) {
3363
+ const ids = new Set(batches.map(({ id }) => id));
3364
+ const leaving = batches.map((batch) => ({
3365
+ batch,
3366
+ returned: !localSaves && leavingIn(batch, locator) ? returnedBy('withdrawn') : null,
3367
+ }));
3368
+ const outcome = localSaves ? 'dropped' : 'unpaired';
3369
+ const current = replicas.get(locator);
3370
+ if (current !== undefined) {
3371
+ const settled = await settlement(local, locator, current, current.record, current.queue.filter((one) => !ids.has(one.id)), { leaving });
3372
+ return {
3373
+ ...settled,
3374
+ apply(answer) {
3375
+ for (const batch of batches)
3376
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
3377
+ const set = settled.apply(answer);
3378
+ submitLater(locator);
3379
+ return set;
3380
+ },
3381
+ };
3382
+ }
3383
+ const queue = queueIn(locator).filter((one) => !ids.has(one.id));
3384
+ const written = new Map();
3385
+ for (const { batch, returned } of leaving) {
3386
+ if (returned === null)
3387
+ continue;
3388
+ for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
3389
+ written.set(saved.path, {
3390
+ locator,
3391
+ path: saved.path,
3392
+ revision: roomDraftRevision(),
3393
+ parts: returnedInto((written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path))?.parts, saved.parts),
3394
+ returned,
3395
+ });
3396
+ }
3397
+ }
3398
+ const carried = carriedBack(locator, leaving.flatMap(({ batch, returned }) => (returned === null ? [] : [batch])), written);
3399
+ return {
3400
+ locator,
3401
+ left: batches,
3402
+ commit: {
3403
+ rooms: [],
3404
+ writes: [
3405
+ {
3406
+ locator,
3407
+ queue,
3408
+ roomDrafts: [...written.values()],
3409
+ consumedRoomDrafts: carried.taken,
3410
+ },
3411
+ ...carried.writes,
3412
+ ],
3413
+ },
3414
+ apply() {
3415
+ for (const batch of batches) {
3416
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
3417
+ pendings.delete(pendingKey(locator, batch.id));
3418
+ }
3419
+ if (queue.length > 0)
3420
+ queuesWithoutReplica.set(locator, queue);
3421
+ else
3422
+ queuesWithoutReplica.delete(locator);
3423
+ for (const record of written.values())
3424
+ rememberRoomDraft(record);
3425
+ carried.remember();
3426
+ submitLater(locator);
3427
+ return mergeChangeSets([
3428
+ changeSet(locator, {
3429
+ queue: true,
3430
+ drafts: [...written.keys(), ...carried.taken],
3431
+ }),
3432
+ ...carried.changes,
3433
+ ]);
3434
+ },
3435
+ };
3436
+ }
3437
+ async function unpair(local) {
3438
+ const handled = new Set();
3439
+ for (let held = unpaired(handled); held !== null && continuing(); held = unpaired(handled)) {
3440
+ const { locator, batch } = held;
3441
+ handled.add(pendingKey(locator, batch.id));
3442
+ publishChangeSet(await together(local, [await unpairing(local, locator, [batch])]));
3443
+ }
3444
+ if (handled.size === 0)
3445
+ return;
3446
+ moveTo({ ...status, ...attention() });
3447
+ }
3448
+ async function discardHeld(head, room, local, signal) {
3449
+ const partner = partnerOf(head, room.locator);
3450
+ const other = partner === null ? undefined : discovered.get(partner);
3451
+ await discardPending(other === undefined ? [room] : [room, other], local, signal);
3452
+ }
2172
3453
  async function submitRoom(room, local, authorized, progress, signal) {
2173
3454
  while (continuing() && authorization !== undefined && !signal.aborted) {
2174
3455
  const replica = replicas.get(room.locator);
2175
3456
  const head = replica?.queue[0];
2176
3457
  if (replica === undefined || head === undefined)
2177
3458
  return 'done';
2178
- if (pendings.get(head.id)?.accepted !== undefined)
3459
+ const state = pendings.get(pendingKey(room.locator, head.id));
3460
+ if (state?.accepted !== undefined)
3461
+ return 'done';
3462
+ const partner = partnerOf(head, room.locator);
3463
+ if (partner !== null && !leavingIn(head, room.locator)) {
3464
+ submitLater(partner);
2179
3465
  return 'done';
2180
- const state = pendings.get(head.id);
3466
+ }
3467
+ if (partner !== null && behindIn(partner, head)) {
3468
+ progress.complete = false;
3469
+ return 'done';
3470
+ }
2181
3471
  const { submission, lost: earlier } = state ?? {
2182
3472
  submission: { state: 'waiting' },
2183
3473
  lost: false,
@@ -2193,16 +3483,16 @@ export function composeRuntime(options) {
2193
3483
  }
2194
3484
  const lost = earlier || submission.state === 'submitted';
2195
3485
  if (lost && pastHorizon(head.id)) {
2196
- await discardPending(room, local, signal);
3486
+ await discardHeld(head, room, local, signal);
2197
3487
  return 'done';
2198
3488
  }
2199
3489
  let result;
2200
3490
  journal.record(`submission ${room.locator} ${head.id}`);
2201
- await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
3491
+ await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
2202
3492
  if (discoveryPaused || fences.has(room.locator))
2203
3493
  return 'done';
2204
3494
  if (signal.aborted || writesHeld()) {
2205
- await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
3495
+ await exclusive(() => mark(local, room.locator, head, submission, earlier));
2206
3496
  if (signal.aborted)
2207
3497
  return 'done';
2208
3498
  progress.complete = false;
@@ -2210,11 +3500,17 @@ export function composeRuntime(options) {
2210
3500
  return 'done';
2211
3501
  }
2212
3502
  const opensBefore = openSignals;
2213
- const answered = awaitingAnswer(room.locator);
3503
+ const answeredHere = awaitingAnswer(room.locator);
3504
+ const answeredThere = partner === null ? () => { } : awaitingAnswer(partner);
3505
+ const answered = () => {
3506
+ answeredHere();
3507
+ answeredThere();
3508
+ };
2214
3509
  try {
2215
3510
  result = await options.adapter.mutate(authorized, {
2216
3511
  locator: room.locator,
2217
3512
  changeBatch: head,
3513
+ file: batchFile(head),
2218
3514
  ...(state?.upload === undefined ? {} : { upload: state.upload }),
2219
3515
  }, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
2220
3516
  }
@@ -2239,9 +3535,10 @@ export function composeRuntime(options) {
2239
3535
  return 'again';
2240
3536
  }
2241
3537
  if (result.kind === 'room-not-readable') {
3538
+ const unread = discovered.get(result.locator) ?? room;
2242
3539
  progress.complete = false;
2243
- unreadable.add(room.locator);
2244
- list(room, false);
3540
+ unreadable.add(unread.locator);
3541
+ list(unread, false);
2245
3542
  emitRooms();
2246
3543
  return 'done';
2247
3544
  }
@@ -2256,24 +3553,30 @@ export function composeRuntime(options) {
2256
3553
  }
2257
3554
  if (result.kind === 'write-halted') {
2258
3555
  journal.record(`outcome ${room.locator} ${head.id} write-halted`);
2259
- await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, lost));
3556
+ await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
2260
3557
  if (serviceMode === 'open' && openSignals !== opensBefore)
2261
3558
  continue;
2262
3559
  haltedRefusal = true;
2263
3560
  progress.complete = false;
2264
3561
  return 'done';
2265
3562
  }
3563
+ const other = partner === null ? undefined : discovered.get(partner);
2266
3564
  if (result.kind === 'conflicted' ||
2267
3565
  result.kind === 'rejected' ||
2268
3566
  result.kind === 'expired' ||
2269
3567
  result.kind === 'retryable-refusal') {
2270
3568
  if (result.kind === 'expired' && lost && result.restored !== true) {
2271
3569
  journal.record(`outcome ${room.locator} ${head.id} expired`);
2272
- await discardPending(room, local, signal);
3570
+ await discardHeld(head, room, local, signal);
2273
3571
  return 'done';
2274
3572
  }
2275
3573
  if (result.kind === 'expired') {
2276
- await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
3574
+ await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
3575
+ const there = other === undefined ? null : await advance(other, local, authorized, continuing);
3576
+ if (other !== undefined && there !== null) {
3577
+ await withhold(there, other, local, progress);
3578
+ return 'done';
3579
+ }
2277
3580
  const withheld = await advance(room, local, authorized, continuing);
2278
3581
  if (withheld !== null) {
2279
3582
  await withhold(withheld, room, local, progress);
@@ -2290,11 +3593,25 @@ export function composeRuntime(options) {
2290
3593
  if (result.kind === 'retryable-refusal') {
2291
3594
  const reset = result.reset;
2292
3595
  progress.complete = false;
2293
- await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
3596
+ await exclusive(() => mark(local, room.locator, head, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
2294
3597
  return reset === undefined ? 'again' : { at: Date.parse(reset) };
2295
3598
  }
2296
3599
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
2297
3600
  const left = leaving(head, result);
3601
+ const refreshed = async () => ({
3602
+ there: other === undefined ? null : await advance(other, local, authorized, continuing),
3603
+ here: await advance(room, local, authorized, continuing),
3604
+ });
3605
+ const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
3606
+ if (judged !== null && (judged.there !== null || judged.here !== null)) {
3607
+ await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
3608
+ if (other !== undefined && judged.there !== null) {
3609
+ await withhold(judged.there, other, local, progress);
3610
+ }
3611
+ if (judged.here !== null)
3612
+ await withhold(judged.here, room, local, progress);
3613
+ return 'done';
3614
+ }
2298
3615
  await exclusive(async () => {
2299
3616
  if (!continuing())
2300
3617
  return;
@@ -2302,25 +3619,29 @@ export function composeRuntime(options) {
2302
3619
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
2303
3620
  return;
2304
3621
  }
2305
- const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
2306
- leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
2307
- (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
2308
- ? { ...left, returned: null }
2309
- : left,
2310
- });
2311
- publishChangeSet(settled());
3622
+ const confirmedLayer = await sliceAt(current.record, 'confirmed', [head]);
3623
+ const held = (result.kind === 'expired' &&
3624
+ (await landedWhole(room.locator, head, confirmedLayer))) ||
3625
+ (result.kind === 'conflicted' &&
3626
+ (await landedWhole(room.locator, head, confirmedLayer, result.affected)));
3627
+ publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
3628
+ await unpair(local);
2312
3629
  moveTo({ ...status, ...attention() });
2313
3630
  });
2314
3631
  if (result.kind === 'expired')
2315
3632
  continue;
2316
- const withheld = await advance(room, local, authorized, continuing);
2317
- if (withheld !== null) {
2318
- await withhold(withheld, room, local, progress);
3633
+ const { there, here } = judged ?? (await refreshed());
3634
+ if (other !== undefined && there !== null)
3635
+ await withhold(there, other, local, progress);
3636
+ if (here !== null) {
3637
+ await withhold(here, room, local, progress);
2319
3638
  return 'done';
2320
3639
  }
2321
3640
  continue;
2322
3641
  }
2323
- const kept = await adopted(room, local, head, result).finally(answered);
3642
+ const kept = await (partner === null
3643
+ ? adopted(room, local, head, result)
3644
+ : adoptedMove(room, partner, local, head, result)).finally(answered);
2324
3645
  if (!kept) {
2325
3646
  progress.complete = false;
2326
3647
  return 'done';
@@ -2331,44 +3652,67 @@ export function composeRuntime(options) {
2331
3652
  function pastHorizon(identity) {
2332
3653
  return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
2333
3654
  }
2334
- async function discardPending(room, local, signal) {
2335
- const { locator } = room;
3655
+ async function discardPending(rooms, local, signal) {
2336
3656
  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);
3657
+ const held = rooms
3658
+ .filter(({ locator }) => !fences.has(locator))
3659
+ .map((room) => ({
3660
+ room,
3661
+ before: replicas.get(room.locator),
3662
+ queue: queueIn(room.locator),
3663
+ }));
3664
+ if (held.length === 0)
3665
+ return [];
3666
+ const answer = await local.commit({
3667
+ rooms: held.flatMap(({ room, before }) => before === undefined
3668
+ ? []
3669
+ : [
3670
+ {
3671
+ locator: room.locator,
3672
+ key: before.record.key,
3673
+ roots: before.record.typedSelection?.roots ?? [],
3674
+ overlay: { set: [], cleared: [...before.overlaid] },
3675
+ },
3676
+ ]),
3677
+ discards: held.map(({ room }) => room.locator),
3678
+ });
3679
+ for (const { room, before, queue } of held) {
3680
+ const { locator } = room;
3681
+ lastFenced.set(locator, ++fencings);
3682
+ for (const batch of queue)
3683
+ pendings.delete(pendingKey(locator, batch.id));
3684
+ queuesWithoutReplica.delete(locator);
3685
+ if (before !== undefined) {
3686
+ replicas.set(locator, {
3687
+ ...before,
3688
+ queue: [],
3689
+ overlaid: new Set(),
3690
+ count: answer.counts[locator] ?? before.count,
3691
+ });
3692
+ }
3693
+ journal.record(`room discarded ${locator}`);
2348
3694
  }
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 }));
3695
+ publishChangeSet(mergeChangeSets(held.map(({ room }) => changeSet(room.locator, {
3696
+ documents: answer.changed[room.locator] ?? [],
3697
+ queue: true,
3698
+ }))));
2357
3699
  moveTo({ ...status, ...attention() });
2358
- return true;
3700
+ return held.map(({ room }) => room);
2359
3701
  });
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);
3702
+ for (const room of discarded) {
3703
+ if (signal.aborted)
3704
+ return;
3705
+ const progress = { complete: true, reachable: true };
3706
+ await roomTask(room.locator, async (live) => {
3707
+ await Promise.race([
3708
+ reload(room, local, progress, () => live() && !signal.aborted),
3709
+ abandonedOn(signal),
3710
+ ]);
3711
+ });
3712
+ if (signal.aborted)
3713
+ return;
3714
+ await rest(progress);
3715
+ }
2372
3716
  }
2373
3717
  function awaitingAnswer(locator) {
2374
3718
  let release = () => { };
@@ -2385,7 +3729,7 @@ export function composeRuntime(options) {
2385
3729
  async function retainModel(local, room, attached, next) {
2386
3730
  const key = modelKey(room.locator, attached);
2387
3731
  const previous = modelOf(room.locator, attached);
2388
- const heldBefore = replicas.get(room.locator)?.confirmed.record;
3732
+ const heldBefore = replicas.get(room.locator)?.record;
2389
3733
  if (JSON.stringify(previous) === JSON.stringify(next) &&
2390
3734
  (heldBefore === undefined ||
2391
3735
  (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
@@ -2404,32 +3748,47 @@ export function composeRuntime(options) {
2404
3748
  let changed = null;
2405
3749
  if (held !== undefined) {
2406
3750
  const declared = {
2407
- ...held.confirmed.record,
3751
+ ...held.record,
2408
3752
  operations: room.operations,
2409
3753
  replica: room.replica,
2410
3754
  typedSelection: room.typedSelection ?? null,
2411
3755
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
2412
3756
  };
2413
- const reclassified = sameMembership(declared, held.confirmed.record)
2414
- ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
2415
- : await readReclassified(declared, held.confirmed);
2416
- const record = reclassified.kind === 'staged'
3757
+ let read = 'staged';
3758
+ if (!sameMembership(declared, held.record)) {
3759
+ const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), compositionOf(declared));
3760
+ read =
3761
+ verdict.verdict !== 'refuse'
3762
+ ? 'staged'
3763
+ : verdict.rule === 'model-unavailable'
3764
+ ? 'deferred'
3765
+ : { kind: 'refused', refusal: verdict };
3766
+ }
3767
+ const record = read === 'staged'
2417
3768
  ? declared
2418
- : {
2419
- ...declared,
2420
- typedSelection: held.confirmed.record.typedSelection,
2421
- };
2422
- const read = reclassified.kind === 'deferred'
2423
- ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
2424
- : reclassified;
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 });
2432
- if (read.kind === 'staged') {
3769
+ : { ...declared, typedSelection: held.record.typedSelection };
3770
+ const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
3771
+ const replayedLayer = await replayed(record, confirmedLayer, held.queue);
3772
+ const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
3773
+ const answer = await local.commit({
3774
+ rooms: [
3775
+ {
3776
+ locator: room.locator,
3777
+ key: record.key,
3778
+ record: storedRecord(record),
3779
+ roots: record.typedSelection?.roots ?? [],
3780
+ composition: compositionOf(record),
3781
+ overlay: diff.overlay,
3782
+ },
3783
+ ],
3784
+ });
3785
+ const { count, indexed } = committedRoom(room.locator, answer);
3786
+ replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
3787
+ changed = changeSet(room.locator, {
3788
+ documents: answer.changed[room.locator] ?? [],
3789
+ room: true,
3790
+ });
3791
+ if (read === 'staged' || read === 'deferred') {
2433
3792
  if (declarationRefusals.delete(room.locator)) {
2434
3793
  sourceRefusals.delete(room.locator);
2435
3794
  unreadable.delete(room.locator);
@@ -2453,7 +3812,7 @@ export function composeRuntime(options) {
2453
3812
  });
2454
3813
  }
2455
3814
  async function scoped(result, room, local) {
2456
- if ((result.scope === 'content' || result.scope === 'sync') && !result.retryable) {
3815
+ if (result.scope === 'content' && !result.retryable) {
2457
3816
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
2458
3817
  await loseRooms([room.locator]);
2459
3818
  return;
@@ -2471,15 +3830,7 @@ export function composeRuntime(options) {
2471
3830
  });
2472
3831
  else if (result.scope !== 'operation') {
2473
3832
  unreadable.add(room.locator);
2474
- scopedAway = {
2475
- ...room,
2476
- replica: { ...room.replica, admission: 'unavailable' },
2477
- operations: {
2478
- ...room.operations,
2479
- ...(result.scope === 'content' ? { read: [] } : {}),
2480
- ...(result.scope === 'sync' ? { sync: [] } : {}),
2481
- },
2482
- };
3833
+ scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
2483
3834
  await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
2484
3835
  }
2485
3836
  if (!continuing())
@@ -2490,17 +3841,10 @@ export function composeRuntime(options) {
2490
3841
  }
2491
3842
  function heldRooms(listed) {
2492
3843
  return listed.flatMap((room) => {
2493
- const record = replicas.get(room.locator)?.confirmed.record;
3844
+ const record = replicas.get(room.locator)?.record;
2494
3845
  if (record === undefined || record.replica.selection !== room.replica.selection)
2495
3846
  return [];
2496
- return [
2497
- {
2498
- locator: room.locator,
2499
- roomVersion: record.roomVersion,
2500
- documents: record.documents,
2501
- revisions: record.revisions,
2502
- },
2503
- ];
3847
+ return [{ locator: room.locator, roomVersion: record.roomVersion }];
2504
3848
  });
2505
3849
  }
2506
3850
  async function synchronize(listed, local, progress, attempt, live) {
@@ -2528,7 +3872,7 @@ export function composeRuntime(options) {
2528
3872
  progress.complete = false;
2529
3873
  return false;
2530
3874
  }
2531
- const prior = replicas.get(room.locator)?.confirmed.record;
3875
+ const prior = replicas.get(room.locator)?.record;
2532
3876
  if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
2533
3877
  list(prior, false);
2534
3878
  emitRooms();
@@ -2576,13 +3920,12 @@ export function composeRuntime(options) {
2576
3920
  if (!live() || authorization === undefined)
2577
3921
  return;
2578
3922
  const bootstrapped = await pullBootstrap(authorization, room, local, live);
2579
- if (!live())
2580
- return;
2581
3923
  if (bootstrapped.kind !== 'staged') {
2582
- await withhold(bootstrapped, room, local, progress);
3924
+ if (live())
3925
+ await withhold(bootstrapped, room, local, progress);
2583
3926
  return;
2584
3927
  }
2585
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
3928
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
2586
3929
  await caughtUp(room, local, progress, live);
2587
3930
  }
2588
3931
  finally {
@@ -2639,12 +3982,13 @@ export function composeRuntime(options) {
2639
3982
  }
2640
3983
  for (const room of result.rooms) {
2641
3984
  if (room.operations === undefined ||
2642
- !['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
3985
+ !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
2643
3986
  room.operations[operation].every((scope) => typeof scope === 'string')) ||
2644
3987
  room.replica === undefined ||
2645
3988
  typeof room.replica.selection !== 'string' ||
2646
3989
  room.replica.selection.length === 0 ||
2647
- !['admitted', 'unavailable'].includes(room.replica.admission)) {
3990
+ !['admitted', 'unavailable'].includes(room.replica.admission) ||
3991
+ !['locked', 'open'].includes(room.replica.offline)) {
2648
3992
  journal.record(`invalid model or replica declaration ${room.locator}`);
2649
3993
  await refuse('missing-required-guarantee');
2650
3994
  notifyReady();
@@ -2668,11 +4012,12 @@ export function composeRuntime(options) {
2668
4012
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
2669
4013
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
2670
4014
  limits = result.providerLimits;
2671
- await local.declare(limits, capabilities);
4015
+ await local.fact('providerLimits', limits);
4016
+ await local.fact('capabilities', capabilities);
2672
4017
  }
2673
4018
  const signedIn = result.member ?? null;
2674
4019
  if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
2675
- await local.keepMember(signedIn);
4020
+ await local.fact('member', signedIn);
2676
4021
  member = signedIn;
2677
4022
  emitMember();
2678
4023
  }
@@ -2806,6 +4151,7 @@ export function composeRuntime(options) {
2806
4151
  function closedSocket(revoked, lost) {
2807
4152
  const opened = socketOpen;
2808
4153
  socketOpen = false;
4154
+ connectionLost();
2809
4155
  if (lost) {
2810
4156
  reads.lost();
2811
4157
  conversations.lost();
@@ -2875,7 +4221,7 @@ export function composeRuntime(options) {
2875
4221
  owed.clear();
2876
4222
  for (const locator of reloading)
2877
4223
  catching.add(locator);
2878
- readSide.recover();
4224
+ liveQueries.recover();
2879
4225
  return reloading;
2880
4226
  }
2881
4227
  async function reloadNow(replaced, progress) {
@@ -3042,7 +4388,7 @@ export function composeRuntime(options) {
3042
4388
  if (synced && !recorded) {
3043
4389
  const completedAt = new Date().toISOString();
3044
4390
  try {
3045
- await (await openLocal()).complete(completedAt);
4391
+ await (await openLocal()).fact('lastCompleted', completedAt);
3046
4392
  if (!live())
3047
4393
  return;
3048
4394
  recorded = true;
@@ -3082,8 +4428,8 @@ export function composeRuntime(options) {
3082
4428
  if (room === undefined || room.replica.admission !== 'admitted')
3083
4429
  return;
3084
4430
  if (held !== undefined &&
3085
- (held.confirmed.record.replica.selection !== room.replica.selection ||
3086
- options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
4431
+ (held.record.replica.selection !== room.replica.selection ||
4432
+ options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
3087
4433
  return;
3088
4434
  }
3089
4435
  const during = underway?.generation === generation ? underway.progress : null;
@@ -3130,6 +4476,7 @@ export function composeRuntime(options) {
3130
4476
  return;
3131
4477
  if (event.kind === 'opened') {
3132
4478
  socketOpen = true;
4479
+ connectionLive();
3133
4480
  generation += 1;
3134
4481
  reads.opened();
3135
4482
  conversations.reopened();
@@ -3144,7 +4491,14 @@ export function composeRuntime(options) {
3144
4491
  void rediscover(event.scope);
3145
4492
  else
3146
4493
  void applyRoom(event.locator, event.roomVersion);
3147
- }, receipts);
4494
+ }, receipts, heldRevisions);
4495
+ }
4496
+ async function heldRevisions(locator, paths) {
4497
+ const held = replicas.get(locator);
4498
+ if (held === undefined || paths.length === 0)
4499
+ return new Map();
4500
+ const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
4501
+ return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
3148
4502
  }
3149
4503
  function writesHeld() {
3150
4504
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -3214,8 +4568,12 @@ export function composeRuntime(options) {
3214
4568
  if (closed)
3215
4569
  return;
3216
4570
  closed = true;
3217
- if (rechecking !== null)
3218
- clearInterval(rechecking);
4571
+ if (probing !== null)
4572
+ clearInterval(probing);
4573
+ for (const resolve of shownWaiters.splice(0))
4574
+ resolve();
4575
+ connectionLive();
4576
+ lockListeners.clear();
3219
4577
  readinessWatches.clear();
3220
4578
  withdrawStream?.();
3221
4579
  withdrawStream = undefined;
@@ -3224,17 +4582,13 @@ export function composeRuntime(options) {
3224
4582
  withdrawServiceMode?.();
3225
4583
  conversations.close();
3226
4584
  void recovery.retire();
3227
- store?.close();
3228
- void opening?.then((opened) => {
3229
- opened.close();
3230
- }, () => { });
3231
4585
  roomListeners.clear();
3232
4586
  refusalListeners.clear();
3233
4587
  storageLossListeners.clear();
3234
4588
  statusListeners.clear();
3235
4589
  serviceModeListeners.clear();
3236
4590
  memberListeners.clear();
3237
- readSide.close();
4591
+ liveQueries.close();
3238
4592
  queueSubscriptions.clear();
3239
4593
  roomDraftListeners.clear();
3240
4594
  presentationListeners.clear();
@@ -3280,7 +4634,7 @@ export function composeRuntime(options) {
3280
4634
  const watchers = seedWatchers.get(plan.key) ?? new Set();
3281
4635
  seedWatchers.set(plan.key, watchers);
3282
4636
  watchers.add(shown);
3283
- const stop = readSide.watch(plan, identityOf(plan), () => {
4637
+ const stop = liveQueries.watch(plan, identityOf(plan), () => {
3284
4638
  const cover = covered(plan);
3285
4639
  awaitPreparation(cover);
3286
4640
  return seedBehind(plan, cover);
@@ -3301,66 +4655,53 @@ export function composeRuntime(options) {
3301
4655
  stop();
3302
4656
  };
3303
4657
  }
3304
- async function searched(input, reach) {
4658
+ async function searched(input, reach, locked) {
3305
4659
  const asked = searchPlanOf(input);
3306
- const plan = { ...asked, limit: capped(asked.limit) };
3307
- const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
4660
+ const filter = {
4661
+ ...filterOf(asked.documents ?? {}, roomFacts(reach), planReach(reach)),
4662
+ locked,
4663
+ };
4664
+ const unlocking = (filter.rooms ?? [])
4665
+ .filter((room) => locked.includes(room))
4666
+ .map((room) => ({ room, awaits: 'unlock' }));
3308
4667
  for (;;) {
3309
4668
  const cover = covered(filter);
3310
4669
  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);
4670
+ const pending = [...unlocking, ...('pending' in cover ? cover.pending : [])];
3332
4671
  const complete = pending.length === 0 && !('pending' in cover);
4672
+ const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3333
4673
  const answering = complete
3334
4674
  ? filter
3335
- : {
3336
- ...filter,
3337
- rooms: covering
3338
- .filter(({ room }) => searchIndex.rooms().has(room))
3339
- .map(({ room }) => room),
3340
- };
4675
+ : { ...filter, rooms: reachedRooms(filter).filter((room) => !awaited.has(room)) };
3341
4676
  const answered = covered(answering);
3342
4677
  if ('pending' in answered)
3343
4678
  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))
4679
+ const listed = conversations.searchable();
4680
+ const named = reach !== null && !reach.conversations
4681
+ ? []
4682
+ : (asked.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
4683
+ const hits = await (await openLocal()).search({
4684
+ text: asked.text,
4685
+ limit: capped(asked.limit),
4686
+ after: asked.after,
4687
+ total: asked.total,
4688
+ documents: {
4689
+ plan: answering,
4690
+ rooms: answered.rooms,
4691
+ beneath: asked.collection
4692
+ ? answered.rooms.map(({ locator, key }) => ({
4693
+ key,
4694
+ roots: replicas.get(locator)?.record.typedSelection?.roots ?? [],
4695
+ }))
4696
+ : [],
4697
+ },
4698
+ conversations: named,
4699
+ });
4700
+ if (JSON.stringify(covered(answering)) !== JSON.stringify(answered))
3353
4701
  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
4702
  const coverage = {
3361
4703
  rooms: answered.coverage.rooms,
3362
- conversations: searchedConversations,
3363
- index: searchIndex.generation(),
4704
+ conversations: named,
3364
4705
  overlay: answered.coverage.overlay,
3365
4706
  };
3366
4707
  return complete
@@ -3374,13 +4715,17 @@ export function composeRuntime(options) {
3374
4715
  roomDraftListeners.add(listener);
3375
4716
  void exclusive(async () => {
3376
4717
  await openLocal();
3377
- if (closed || !roomDraftListeners.has(listener))
3378
- return;
3379
- listener.ready = true;
3380
4718
  }).then(() => {
3381
- if (closed || !listener.ready || !roomDraftListeners.has(listener))
4719
+ if (closed || !roomDraftListeners.has(listener))
3382
4720
  return;
3383
- listener.notify(snapshotRoomDrafts());
4721
+ const snapshot = snapshotRoomDrafts();
4722
+ draftsShown = draftsShown.then(async () => {
4723
+ const value = await snapshot.catch(() => null);
4724
+ if (value === null || closed || !roomDraftListeners.has(listener))
4725
+ return;
4726
+ listener.ready = true;
4727
+ listener.notify(value);
4728
+ });
3384
4729
  }, () => { });
3385
4730
  return () => {
3386
4731
  roomDraftListeners.delete(listener);
@@ -3413,7 +4758,7 @@ export function composeRuntime(options) {
3413
4758
  };
3414
4759
  }
3415
4760
  if (topic === 'readiness') {
3416
- return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
4761
+ return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
3417
4762
  }
3418
4763
  if (topic === 'rooms') {
3419
4764
  roomListeners.add(deliver);
@@ -3475,7 +4820,7 @@ export function composeRuntime(options) {
3475
4820
  return listed();
3476
4821
  },
3477
4822
  query: ((input) => rowsQuery(planned(input, null))),
3478
- search: (input) => searched(input, null),
4823
+ search: (input) => searched(input, null, []),
3479
4824
  types(locator) {
3480
4825
  return {
3481
4826
  collection: modelTypes(modelOf(locator)),
@@ -3495,9 +4840,9 @@ export function composeRuntime(options) {
3495
4840
  return listed;
3496
4841
  });
3497
4842
  },
3498
- read(room, path) {
4843
+ async read(room, path) {
3499
4844
  const record = roomDrafts.get(room)?.get(path);
3500
- return record === undefined ? null : roomDraftOf(record);
4845
+ return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
3501
4846
  },
3502
4847
  write(room, path, revision, input) {
3503
4848
  return exclusive(async () => {
@@ -3507,11 +4852,11 @@ export function composeRuntime(options) {
3507
4852
  const local = await openLocal();
3508
4853
  const current = roomDrafts.get(room)?.get(path);
3509
4854
  if ((current?.revision ?? null) !== revision) {
3510
- return staleWrite(current === undefined ? null : roomDraftOf(current));
4855
+ return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
3511
4856
  }
3512
4857
  const file = parts.file === undefined
3513
4858
  ? undefined
3514
- : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
4859
+ : await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
3515
4860
  const record = {
3516
4861
  locator: room,
3517
4862
  path,
@@ -3519,10 +4864,12 @@ export function composeRuntime(options) {
3519
4864
  parts: held(current?.parts, parts, file),
3520
4865
  returned: current?.returned ?? null,
3521
4866
  };
3522
- await local.keepRoomDraft(record);
4867
+ await local.keepRoomDraft(record, parts.file === undefined || file === undefined
4868
+ ? []
4869
+ : [{ key: file.localId, blob: parts.file.blob }]);
3523
4870
  rememberRoomDraft(record);
3524
- publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
3525
- return { outcome: 'written', roomDraft: roomDraftOf(record) };
4871
+ publishChangeSet(changeSet(room, { drafts: [path] }));
4872
+ return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
3526
4873
  });
3527
4874
  },
3528
4875
  discard(room, path, revision) {
@@ -3530,11 +4877,11 @@ export function composeRuntime(options) {
3530
4877
  const local = await openLocal();
3531
4878
  const current = roomDrafts.get(room)?.get(path);
3532
4879
  if (current === undefined || current.revision !== revision) {
3533
- return staleWrite(current === undefined ? null : roomDraftOf(current));
4880
+ return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
3534
4881
  }
3535
4882
  await local.discardRoomDraft(room, path);
3536
4883
  forgetRoomDraft(room, path);
3537
- publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
4884
+ publishChangeSet(changeSet(room, { drafts: [path] }));
3538
4885
  moveTo({ ...status, ...attention() });
3539
4886
  return { outcome: 'written', roomDraft: null };
3540
4887
  });
@@ -3568,15 +4915,20 @@ export function composeRuntime(options) {
3568
4915
  history(locator, path, query) {
3569
4916
  return readHistory(locator, path, query);
3570
4917
  },
4918
+ resolve(room, path, destination) {
4919
+ return resolveReference(room, path, destination);
4920
+ },
3571
4921
  binaryFile: {
4922
+ part: readPart,
3572
4923
  read: readBinaryPath,
3573
4924
  link: linkBinaryPath,
3574
- readReference: readBinaryReference,
3575
4925
  },
3576
- call(name, input) {
3577
- return recoveredAction(name, input);
4926
+ async call(name, input) {
4927
+ await ready;
4928
+ const room = calledRooms(name, input).find((one) => locked(one));
4929
+ return room === undefined ? recoveredAction(name, input) : lockedRoom(room);
3578
4930
  },
3579
- reached(locator, roomVersion, path) {
4931
+ async reached(locator, roomVersion, path) {
3580
4932
  return path === undefined
3581
4933
  ? reachedBy(locator, roomVersion)
3582
4934
  : writtenAfter(locator, roomVersion, path);
@@ -3584,14 +4936,16 @@ export function composeRuntime(options) {
3584
4936
  log() {
3585
4937
  return journal.export();
3586
4938
  },
4939
+ cleanUp,
3587
4940
  persistence() {
3588
4941
  return persistence === 'granted';
3589
4942
  },
3590
4943
  async retry() {
3591
4944
  journal.record('retry');
3592
- unindexable.clear();
4945
+ unpreparable.clear();
3593
4946
  queueReadiness();
3594
- resume();
4947
+ if (full)
4948
+ await probed();
3595
4949
  },
3596
4950
  observe: observing(watch),
3597
4951
  async reauthorize() {
@@ -3613,12 +4967,15 @@ export function composeRuntime(options) {
3613
4967
  const reachable = {
3614
4968
  md,
3615
4969
  watch,
3616
- referrerScope: (referrer) => options.adapter.referrerScope(referrer),
3617
4970
  plan: planned,
3618
4971
  query: (plan) => rowsQuery(plan),
3619
4972
  watchPlan,
3620
4973
  search: searched,
3621
4974
  watchReadiness,
4975
+ locked,
4976
+ lockedRooms,
4977
+ watchLocks,
4978
+ located,
3622
4979
  };
3623
4980
  return {
3624
4981
  md,
@@ -3633,6 +4990,11 @@ export function composeRuntime(options) {
3633
4990
  journal.record(`persistence ${answer}`);
3634
4991
  queueReadiness();
3635
4992
  },
3636
- within: (reach, port) => narrowed(reachable, reach, port),
4993
+ shown: screen,
4994
+ within: (reach, port) => {
4995
+ if (full)
4996
+ void probed();
4997
+ return narrowed(reachable, reach, port);
4998
+ },
3637
4999
  };
3638
5000
  }