@agentshouse/mdruntime 0.1.0-cedb03eaa48b6 → 0.1.0-cee9c8aa4cd86

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 (192) hide show
  1. package/AGENTS.md +86 -71
  2. package/README.md +9 -8
  3. package/dist/adapter.d.ts +44 -18
  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/bootstrap.d.ts +13 -6
  9. package/dist/bootstrap.d.ts.map +1 -1
  10. package/dist/bootstrap.js +90 -61
  11. package/dist/chain.d.ts +11 -0
  12. package/dist/chain.d.ts.map +1 -0
  13. package/dist/chain.js +64 -0
  14. package/dist/change-set.d.ts +9 -16
  15. package/dist/change-set.d.ts.map +1 -1
  16. package/dist/change-set.js +11 -89
  17. package/dist/change.d.ts +31 -1
  18. package/dist/change.d.ts.map +1 -1
  19. package/dist/change.js +10 -1
  20. package/dist/chat/house.d.ts +25 -0
  21. package/dist/chat/house.d.ts.map +1 -0
  22. package/dist/chat/house.js +38 -0
  23. package/dist/chat/input.d.ts +3 -1
  24. package/dist/chat/input.d.ts.map +1 -1
  25. package/dist/chat/input.js +11 -18
  26. package/dist/chat/list.d.ts +2 -1
  27. package/dist/chat/list.d.ts.map +1 -1
  28. package/dist/chat/list.js +7 -3
  29. package/dist/chat/markdown.d.ts +1 -0
  30. package/dist/chat/markdown.d.ts.map +1 -1
  31. package/dist/chat/markdown.js +78 -20
  32. package/dist/chat/message.d.ts +1 -0
  33. package/dist/chat/message.d.ts.map +1 -1
  34. package/dist/chat/message.js +73 -11
  35. package/dist/chat/messages.d.ts +14 -1
  36. package/dist/chat/messages.d.ts.map +1 -1
  37. package/dist/chat/messages.js +79 -24
  38. package/dist/chat/name.d.ts +1 -0
  39. package/dist/chat/name.d.ts.map +1 -1
  40. package/dist/chat/name.js +1 -1
  41. package/dist/chat/opened.d.ts +1 -0
  42. package/dist/chat/opened.d.ts.map +1 -1
  43. package/dist/chat/opened.js +2 -0
  44. package/dist/chat/overlay.d.ts +1 -0
  45. package/dist/chat/overlay.d.ts.map +1 -1
  46. package/dist/chat/overlay.js +11 -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/pieces.d.ts +62 -0
  51. package/dist/chat/pieces.d.ts.map +1 -0
  52. package/dist/chat/pieces.js +1110 -0
  53. package/dist/chat/provider.d.ts +8 -1
  54. package/dist/chat/provider.d.ts.map +1 -1
  55. package/dist/chat/provider.js +88 -81
  56. package/dist/chat/ready.d.ts +5 -2
  57. package/dist/chat/ready.d.ts.map +1 -1
  58. package/dist/chat/ready.js +7 -5
  59. package/dist/chat/room-file.d.ts +8 -0
  60. package/dist/chat/room-file.d.ts.map +1 -0
  61. package/dist/chat/room-file.js +72 -0
  62. package/dist/chat/steady.d.ts +3 -0
  63. package/dist/chat/steady.d.ts.map +1 -0
  64. package/dist/chat/steady.js +26 -0
  65. package/dist/chat/style.d.ts.map +1 -1
  66. package/dist/chat/style.js +5 -0
  67. package/dist/chat.d.ts +3 -2
  68. package/dist/chat.d.ts.map +1 -1
  69. package/dist/chat.js +2 -2
  70. package/dist/client.d.ts +6 -1
  71. package/dist/client.d.ts.map +1 -1
  72. package/dist/client.js +29 -2
  73. package/dist/collection.d.ts +6 -19
  74. package/dist/collection.d.ts.map +1 -1
  75. package/dist/collection.js +30 -317
  76. package/dist/compile.d.ts +16 -0
  77. package/dist/compile.d.ts.map +1 -0
  78. package/dist/compile.js +718 -0
  79. package/dist/connection.d.ts +11 -5
  80. package/dist/connection.d.ts.map +1 -1
  81. package/dist/connection.js +1931 -706
  82. package/dist/conversation.d.ts +9 -3
  83. package/dist/conversation.d.ts.map +1 -1
  84. package/dist/conversation.js +85 -61
  85. package/dist/digest.d.ts +7 -0
  86. package/dist/digest.d.ts.map +1 -0
  87. package/dist/digest.js +113 -0
  88. package/dist/durability.d.ts +2 -1
  89. package/dist/durability.d.ts.map +1 -1
  90. package/dist/entry.d.ts +3 -1
  91. package/dist/entry.d.ts.map +1 -1
  92. package/dist/entry.js +18 -24
  93. package/dist/file.d.ts +2 -10
  94. package/dist/file.d.ts.map +1 -1
  95. package/dist/file.js +3 -120
  96. package/dist/fulltext.d.ts +27 -0
  97. package/dist/fulltext.d.ts.map +1 -0
  98. package/dist/fulltext.js +368 -0
  99. package/dist/host.d.ts +0 -1
  100. package/dist/host.d.ts.map +1 -1
  101. package/dist/host.js +10 -3
  102. package/dist/identity.d.ts +0 -1
  103. package/dist/identity.d.ts.map +1 -1
  104. package/dist/identity.js +0 -3
  105. package/dist/index.d.ts +8 -5
  106. package/dist/index.d.ts.map +1 -1
  107. package/dist/index.js +3 -0
  108. package/dist/live.d.ts +22 -0
  109. package/dist/live.d.ts.map +1 -0
  110. package/dist/live.js +123 -0
  111. package/dist/memory.d.ts +1 -0
  112. package/dist/memory.d.ts.map +1 -1
  113. package/dist/memory.js +22 -6
  114. package/dist/messages.d.ts +28 -0
  115. package/dist/messages.d.ts.map +1 -0
  116. package/dist/messages.js +90 -0
  117. package/dist/name.d.ts +4 -0
  118. package/dist/name.d.ts.map +1 -0
  119. package/dist/name.js +122 -0
  120. package/dist/observation.d.ts +12 -0
  121. package/dist/observation.d.ts.map +1 -1
  122. package/dist/observation.js +84 -0
  123. package/dist/opened.d.ts +22 -0
  124. package/dist/opened.d.ts.map +1 -0
  125. package/dist/opened.js +107 -0
  126. package/dist/outcome.d.ts +29 -4
  127. package/dist/outcome.d.ts.map +1 -1
  128. package/dist/outcome.js +25 -0
  129. package/dist/overlay.d.ts +2 -5
  130. package/dist/overlay.d.ts.map +1 -1
  131. package/dist/overlay.js +86 -171
  132. package/dist/query.d.ts +2 -1
  133. package/dist/query.d.ts.map +1 -1
  134. package/dist/query.js +1 -0
  135. package/dist/reach.d.ts +5 -1
  136. package/dist/reach.d.ts.map +1 -1
  137. package/dist/reach.js +158 -52
  138. package/dist/readiness.d.ts +3 -3
  139. package/dist/readiness.d.ts.map +1 -1
  140. package/dist/reads.js +1 -1
  141. package/dist/recovery.d.ts +2 -2
  142. package/dist/recovery.d.ts.map +1 -1
  143. package/dist/recovery.js +8 -7
  144. package/dist/refresh.d.ts +10 -13
  145. package/dist/refresh.d.ts.map +1 -1
  146. package/dist/refresh.js +74 -51
  147. package/dist/replica.d.ts +54 -36
  148. package/dist/replica.d.ts.map +1 -1
  149. package/dist/replica.js +277 -184
  150. package/dist/room-draft.d.ts +3 -1
  151. package/dist/room-draft.d.ts.map +1 -1
  152. package/dist/room-draft.js +29 -22
  153. package/dist/rows.d.ts +29 -31
  154. package/dist/rows.d.ts.map +1 -1
  155. package/dist/rows.js +113 -69
  156. package/dist/save.d.ts +50 -14
  157. package/dist/save.d.ts.map +1 -1
  158. package/dist/save.js +502 -178
  159. package/dist/search.d.ts +11 -43
  160. package/dist/search.d.ts.map +1 -1
  161. package/dist/search.js +0 -213
  162. package/dist/shared.d.ts +8 -3
  163. package/dist/shared.d.ts.map +1 -1
  164. package/dist/shared.js +129 -66
  165. package/dist/store.d.ts +79 -93
  166. package/dist/store.d.ts.map +1 -1
  167. package/dist/store.js +318 -687
  168. package/dist/stored.d.ts +138 -0
  169. package/dist/stored.d.ts.map +1 -0
  170. package/dist/stored.js +544 -0
  171. package/dist/tables.d.ts +332 -0
  172. package/dist/tables.d.ts.map +1 -0
  173. package/dist/tables.js +570 -0
  174. package/dist/turn.d.ts +4 -2
  175. package/dist/turn.d.ts.map +1 -1
  176. package/dist/turn.js +46 -22
  177. package/dist/validation.d.ts +30 -0
  178. package/dist/validation.d.ts.map +1 -0
  179. package/dist/validation.js +162 -0
  180. package/dist/windows.d.ts +2 -6
  181. package/dist/windows.d.ts.map +1 -1
  182. package/dist/windows.js +30 -34
  183. package/dist/wire.d.ts +16 -0
  184. package/dist/wire.d.ts.map +1 -1
  185. package/dist/wire.js +4 -0
  186. package/dist/words.d.ts +19 -0
  187. package/dist/words.d.ts.map +1 -0
  188. package/dist/words.js +150 -0
  189. package/package.json +10 -4
  190. package/dist/read-side.d.ts +0 -27
  191. package/dist/read-side.d.ts.map +0 -1
  192. package/dist/read-side.js +0 -824
@@ -1,47 +1,42 @@
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, 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, absentReference, 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, 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
28
  import { detectStorageLoss, nextGeneration } from './durability.js';
27
29
  import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
28
30
  import { FencedMap } from './fence.js';
29
- import { tally } from './shared.js';
30
- import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
31
+ import { fingerprint } from './stored.js';
32
+ import { sameBinary } from './overlay.js';
33
+ import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
31
34
  import { identityTime, roomDraftRevision } from './identity.js';
32
35
  import { beneath, normalize } from './paths.js';
33
36
  import { openRuntimeStore, } from './store.js';
37
+ import { changedCleanly, sameComposition, validatedCollection, } from './validation.js';
34
38
  import { unconfirmed } from './transfer.js';
35
39
  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
40
  function within(root, path) {
46
41
  return root === '' || beneath(root, path);
47
42
  }
@@ -59,24 +54,18 @@ const OPENING_LANE = 'opening';
59
54
  const CONVERSATION_LANE = 'conversations';
60
55
  const ROOM_READS = 256;
61
56
  const ROOM_ATTEMPTS = 16;
57
+ const ERASE_BATCH = 512;
58
+ const REDERIVE_BATCH = 256;
59
+ const WALK_BYTES = 1 << 20;
60
+ const QUERY_BYTES = 1 << 20;
61
+ const STAGE_BYTES = 1 << 20;
62
+ const CLEAN_CHANGES = 2_000;
63
+ // 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.
64
+ const LOCK_GRACE_MS = 120_000;
62
65
  function operationOf(input) {
63
66
  const operation = input?.operation_id;
64
67
  return typeof operation === 'string' ? operation : null;
65
68
  }
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
69
  function abandonedOn(signal) {
81
70
  return new Promise((resolve) => {
82
71
  if (signal.aborted)
@@ -88,11 +77,14 @@ function abandonedOn(signal) {
88
77
  function roomLane(locator) {
89
78
  return `room ${locator}`;
90
79
  }
80
+ function pendingKey(locator, id) {
81
+ return `${locator}\u0000${id}`;
82
+ }
91
83
  export function composeRuntime(options) {
92
84
  const localSaves = options.adapter.localSaves === true;
85
+ const roomTypes = roomTypesOf(options.adapter.roomTypes);
93
86
  let authorization = options.authorization;
94
87
  let opening;
95
- let store;
96
88
  const fences = new Map();
97
89
  const isFenced = (locator) => fences.has(locator);
98
90
  let fencings = 0;
@@ -164,6 +156,7 @@ export function composeRuntime(options) {
164
156
  let discovering = null;
165
157
  let opened = null;
166
158
  const owed = new Set();
159
+ const reloadsDue = new Map();
167
160
  const lanes = new Map();
168
161
  let roomReads = { generation, free: ROOM_READS, waiting: [] };
169
162
  const working = new Set();
@@ -175,10 +168,13 @@ export function composeRuntime(options) {
175
168
  const queueSubscriptions = new Set();
176
169
  const roomDraftListeners = new Set();
177
170
  const presentationListeners = new Set();
178
- const readSide = createReadSide();
171
+ const liveQueries = createLiveQueries((plan, cover) => answeredPlan(plan, cover));
179
172
  let withdrawConversations;
180
173
  let currencyQueued = null;
181
174
  let socketOpen = false;
175
+ const confirmed = new Set();
176
+ let lockGrace = null;
177
+ const lockListeners = new Set();
182
178
  let serviceMode = 'open';
183
179
  let haltedRefusal = false;
184
180
  let openSignals = 0;
@@ -186,9 +182,8 @@ export function composeRuntime(options) {
186
182
  let member = null;
187
183
  const memberListeners = new Set();
188
184
  const prepared = new Set();
189
- const projecting = new Set();
190
185
  const unpreparedDemand = new Set();
191
- const unindexable = new Set();
186
+ const unpreparable = new Set();
192
187
  let preparing = false;
193
188
  let targets = {};
194
189
  let persistence = options.shell.persistence;
@@ -201,7 +196,19 @@ export function composeRuntime(options) {
201
196
  const transfers = new FencedMap(isFenced);
202
197
  const conversations = createConversationStore({
203
198
  agents: declaredAgents,
204
- documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
199
+ documents: (declaration, after) => rowsQuery(planOf({
200
+ rooms: [declaration.locator],
201
+ types: [declaration.type],
202
+ select: {
203
+ fields: [
204
+ declaration.identity,
205
+ declaration.name,
206
+ ...(declaration.retired === undefined ? [] : [declaration.retired]),
207
+ ].map((field) => `/${escapePointer(field)}`),
208
+ },
209
+ limit: MAXIMUM_WINDOW,
210
+ ...(after === undefined ? {} : { after }),
211
+ }, roomFacts())),
205
212
  call: (name, input) => invokeAction(name, input),
206
213
  durably,
207
214
  stream: () => options.adapter.conversations,
@@ -211,14 +218,8 @@ export function composeRuntime(options) {
211
218
  unanswered: resendLater,
212
219
  expired: (operation) => pastHorizon(operation),
213
220
  });
214
- const searchIndex = createSearchIndex({
215
- room: (room) => readSide.room(room),
216
- document: (room, path) => readSide.document(room, path),
217
- });
218
- const roomsToIndex = new Set();
219
- const conversationsToIndex = new Set();
220
- let indexing = false;
221
221
  const journal = createLog();
222
+ const keptFiles = openedFiles(options.database, isFenced);
222
223
  let notifyReady = () => { };
223
224
  const ready = new Promise((resolve) => {
224
225
  notifyReady = resolve;
@@ -236,16 +237,36 @@ export function composeRuntime(options) {
236
237
  if (room.size === 0)
237
238
  roomDrafts.delete(locator);
238
239
  }
239
- function roomDraftOf(record) {
240
+ async function visibleOf(locator, paths) {
241
+ const held = replicas.get(locator);
242
+ if (held === undefined)
243
+ return undefined;
244
+ const local = await openLocal();
245
+ const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
246
+ return { ...slice, collection: collectionOf(slice.documents, compositionOf(held.record)) };
247
+ }
248
+ function roomDraftOf(record, visible) {
240
249
  return structuredClone({
241
250
  room: record.locator,
242
251
  path: record.path,
243
252
  revision: record.revision,
244
253
  parts: record.parts,
245
- changedUnderneath: underneath(record.parts, record.path, replicas.get(record.locator)?.visible),
254
+ changedUnderneath: underneath(record.parts, record.path, visible),
246
255
  returned: record.returned,
247
256
  });
248
257
  }
258
+ async function roomDraftsOf(records) {
259
+ const rooms = new Map();
260
+ for (const record of records) {
261
+ const paths = rooms.get(record.locator) ?? [];
262
+ paths.push(record.path);
263
+ rooms.set(record.locator, paths);
264
+ }
265
+ const layers = new Map();
266
+ for (const [locator, paths] of rooms)
267
+ layers.set(locator, await visibleOf(locator, paths));
268
+ return records.map((record) => roomDraftOf(record, layers.get(record.locator)));
269
+ }
249
270
  function snapshotRoomDrafts() {
250
271
  const records = [...roomDrafts.values()].flatMap((room) => [...room.values()]);
251
272
  records.sort((left, right) => {
@@ -255,18 +276,24 @@ export function composeRuntime(options) {
255
276
  return left.path < right.path ? -1 : 1;
256
277
  return 0;
257
278
  });
258
- return records.map(roomDraftOf);
279
+ return roomDraftsOf(records);
259
280
  }
281
+ let draftsShown = Promise.resolve();
260
282
  function publishRoomDrafts(waitingOnly) {
261
283
  if (closed)
262
284
  return;
263
- const value = snapshotRoomDrafts();
264
- for (const listener of roomDraftListeners) {
265
- if (waitingOnly && listener.ready)
266
- continue;
267
- listener.ready = true;
268
- listener.notify(value);
269
- }
285
+ const snapshot = snapshotRoomDrafts();
286
+ draftsShown = draftsShown.then(async () => {
287
+ const value = await snapshot.catch(() => null);
288
+ if (value === null || closed)
289
+ return;
290
+ for (const listener of roomDraftListeners) {
291
+ if (waitingOnly && listener.ready)
292
+ continue;
293
+ listener.ready = true;
294
+ listener.notify(value);
295
+ }
296
+ });
270
297
  }
271
298
  function presentationOf(app) {
272
299
  const value = presentations.get(app);
@@ -278,7 +305,7 @@ export function composeRuntime(options) {
278
305
  const stored = next(presentations.get(app));
279
306
  await local.present({ app, value: stored });
280
307
  presentations.set(app, stored);
281
- publishChangeSet(changeSet('', null, null, { presentation: app }));
308
+ publishChangeSet(changeSet('', { presentation: app }));
282
309
  });
283
310
  }
284
311
  function emitPresentation(app) {
@@ -329,21 +356,69 @@ export function composeRuntime(options) {
329
356
  return { attentionRequired, reset };
330
357
  }
331
358
  function documentsIn(locator) {
332
- const replica = replicas.get(locator);
333
- if (replica === undefined)
334
- return 0;
335
- const roots = replica.confirmed.record.typedSelection?.roots ?? [];
336
- const weighed = outsideOf(roots);
337
- return (tally(replica.visible.documents, weighed.documents) +
338
- tally(replica.visible.binaries, weighed.binaries));
359
+ return replicas.get(locator)?.count ?? 0;
339
360
  }
340
361
  function listed() {
341
362
  return structuredClone([...rooms.values()].map((room) => {
342
363
  const documents = documentsIn(room.locator);
343
364
  counted.set(room.locator, documents);
344
- return { ...room, documents };
365
+ return locked(room.locator)
366
+ ? {
367
+ ...room,
368
+ model: modelTypes(NO_MODEL),
369
+ typedSelection: null,
370
+ documents: 0,
371
+ locked: true,
372
+ }
373
+ : { ...room, documents, locked: false };
345
374
  }));
346
375
  }
376
+ function locked(locator) {
377
+ return rooms.get(locator)?.replica.offline === 'locked' && !confirmed.has(locator);
378
+ }
379
+ function lockedRooms() {
380
+ return [...rooms.keys()].filter(locked).sort();
381
+ }
382
+ function relocked(before) {
383
+ const after = lockedRooms();
384
+ if (JSON.stringify(after) === JSON.stringify(before))
385
+ return;
386
+ journal.record(`locked ${after.length}`);
387
+ emitRooms();
388
+ for (const notify of [...lockListeners])
389
+ notify();
390
+ }
391
+ function confirmLocks(listed) {
392
+ if (!socketOpen)
393
+ return;
394
+ for (const room of listed) {
395
+ if (room.replica.admission === 'admitted')
396
+ confirmed.add(room.locator);
397
+ }
398
+ }
399
+ function connectionLost() {
400
+ if (lockGrace !== null || closed)
401
+ return;
402
+ lockGrace = setTimeout(() => {
403
+ lockGrace = null;
404
+ if (socketOpen || closed)
405
+ return;
406
+ const before = lockedRooms();
407
+ confirmed.clear();
408
+ relocked(before);
409
+ }, LOCK_GRACE_MS);
410
+ }
411
+ function connectionLive() {
412
+ if (lockGrace !== null)
413
+ clearTimeout(lockGrace);
414
+ lockGrace = null;
415
+ }
416
+ function watchLocks(notify) {
417
+ lockListeners.add(notify);
418
+ return () => {
419
+ lockListeners.delete(notify);
420
+ };
421
+ }
347
422
  function roomFacts(reach = null) {
348
423
  const held = [...rooms.values()]
349
424
  .filter((room) => holds(reach, room.locator))
@@ -362,7 +437,7 @@ export function composeRuntime(options) {
362
437
  return { listed: roomsListed, rooms: held };
363
438
  }
364
439
  function emitRooms() {
365
- readSide.recover();
440
+ liveQueries.recover();
366
441
  queueReadiness();
367
442
  const value = listed();
368
443
  for (const listener of roomListeners)
@@ -401,17 +476,17 @@ export function composeRuntime(options) {
401
476
  for (const listener of statusListeners)
402
477
  listener(shown);
403
478
  }
404
- function entryOf(batch) {
479
+ function entryOf(locator, batch) {
405
480
  if (localSaves)
406
481
  return Object.freeze({ batch, state: 'local' });
407
- const state = pendings.get(batch.id);
482
+ const state = pendings.get(pendingKey(locator, batch.id));
408
483
  if (state?.accepted !== undefined) {
409
484
  return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
410
485
  }
411
486
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
412
487
  }
413
488
  function entriesOf(locator) {
414
- return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
489
+ return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map((batch) => entryOf(locator, batch));
415
490
  }
416
491
  function emitQueue(locator) {
417
492
  const entries = entriesOf(locator);
@@ -421,66 +496,35 @@ export function composeRuntime(options) {
421
496
  }
422
497
  }
423
498
  function applyChangeSet(set) {
424
- const held = {
425
- ...set,
426
- documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
427
- };
428
- const applied = readSide.apply(held, () => searchIndex.changed(held));
429
- if (applied instanceof Promise)
430
- void applied.catch(unpublished);
499
+ liveQueries.touched(set);
431
500
  }
432
501
  function unpublished(error) {
433
502
  journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
434
503
  }
435
- async function referenced(locator) {
436
- for (;;) {
437
- const record = replicas.get(locator)?.confirmed.record;
438
- if (record === undefined || closed)
439
- return;
440
- const held = { text: record.documents, binaries: record.binaries };
441
- const composition = compositionOf(record);
442
- if (referencesIndexed(held, composition))
443
- return;
444
- await indexReferrers(held, composition);
445
- }
446
- }
447
504
  async function prepareReplica(locator) {
448
- await referenced(locator);
449
- const replica = replicas.get(locator);
450
- if (replica === undefined || closed || prepared.has(locator))
451
- return;
452
- const visible = replica.visible;
453
- const paths = [
454
- ...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
455
- ];
456
- const entered = [];
457
- projecting.add(locator);
458
- try {
459
- await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
460
- const current = replicas.get(locator);
461
- if (current === undefined || closed)
462
- return;
463
- const set = entering(locator, current, chunk);
464
- entered.push(set);
465
- return readSide.apply(set);
466
- });
467
- if (!replicas.has(locator) || closed)
505
+ for (;;) {
506
+ const held = replicas.get(locator);
507
+ if (held === undefined || closed || prepared.has(locator))
468
508
  return;
509
+ const key = held.record.key;
510
+ const done = await exclusive(async () => (await openLocal()).rederive(key, REDERIVE_BATCH));
511
+ if (replicas.get(locator)?.record.key !== key)
512
+ continue;
513
+ if (!done)
514
+ continue;
469
515
  prepared.add(locator);
516
+ publishChangeSet(changeSet(locator, { documents: null }));
517
+ liveQueries.recover();
518
+ queueReadiness();
519
+ return;
470
520
  }
471
- finally {
472
- projecting.delete(locator);
473
- }
474
- conversations.changed(mergeChangeSets(entered));
475
- readSide.recover();
476
- queueReadiness();
477
521
  }
478
522
  function unprepared() {
479
- return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
523
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unpreparable.has(locator));
480
524
  }
481
- function unbuilt(locator, error) {
482
- unindexable.add(locator);
483
- journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
525
+ function unpreparedBy(locator, error) {
526
+ unpreparable.add(locator);
527
+ journal.record(`preparation failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
484
528
  queueReadiness();
485
529
  }
486
530
  function prepare(locators = []) {
@@ -496,7 +540,7 @@ export function composeRuntime(options) {
496
540
  if (closed)
497
541
  return;
498
542
  unpreparedDemand.delete(waiting);
499
- await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
543
+ await prepareReplica(waiting).catch((error) => unpreparedBy(waiting, error));
500
544
  }
501
545
  }
502
546
  finally {
@@ -504,14 +548,18 @@ export function composeRuntime(options) {
504
548
  }
505
549
  })();
506
550
  }
551
+ function roomsOfReach(reach) {
552
+ return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
553
+ }
507
554
  function reachedRooms(plan) {
508
- const reach = plan.reach;
509
- const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
555
+ const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
510
556
  const named = plan.rooms ??
511
557
  (reached === null
512
558
  ? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
513
559
  : [...reached]);
514
- return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
560
+ return [...new Set(named)]
561
+ .filter((locator) => (reached === null || reached.has(locator)) && !plan.locked.includes(locator))
562
+ .sort();
515
563
  }
516
564
  function awaited(locator) {
517
565
  return (!roomsListed ||
@@ -525,6 +573,7 @@ export function composeRuntime(options) {
525
573
  const pending = [];
526
574
  const coverage = [];
527
575
  const overlay = [];
576
+ const read = [];
528
577
  for (const locator of locators) {
529
578
  const replica = replicas.get(locator);
530
579
  if (replica === undefined) {
@@ -538,15 +587,16 @@ export function composeRuntime(options) {
538
587
  }
539
588
  coverage.push({
540
589
  room: locator,
541
- generation: replica.confirmed.record.transfer,
590
+ generation: replica.record.transfer,
542
591
  current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
543
592
  });
593
+ read.push({ locator, key: replica.record.key });
544
594
  for (const batch of replica.queue)
545
595
  overlay.push(batch.id);
546
596
  }
547
597
  if (pending.length > 0)
548
598
  return { pending };
549
- return { coverage: { rooms: coverage, indexes: [], overlay } };
599
+ return { coverage: { rooms: coverage, indexes: [], overlay }, rooms: read };
550
600
  }
551
601
  function awaitPreparation(cover) {
552
602
  if (!('pending' in cover))
@@ -555,15 +605,16 @@ export function composeRuntime(options) {
555
605
  if (waiting.length > 0)
556
606
  prepare(waiting.map(({ room }) => room));
557
607
  }
558
- function requiredRooms(reach) {
608
+ function requiredRooms(given) {
609
+ const reach = planReach(given);
559
610
  if (reach === null)
560
611
  return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
561
- const named = [...new Set(reach.rooms)].sort();
612
+ const named = [...roomsOfReach(reach)].sort();
562
613
  if (roomsListed)
563
614
  return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
564
615
  return { listed: named.every((room) => replicas.has(room)), rooms: named };
565
616
  }
566
- function roomState(locator, search) {
617
+ function roomState(locator) {
567
618
  if (sourceRefusals.has(locator))
568
619
  return 'refused';
569
620
  const listedRoom = discovered.get(locator);
@@ -576,25 +627,15 @@ export function composeRuntime(options) {
576
627
  return 'acquiring';
577
628
  if (!prepared.has(locator))
578
629
  return 'preparing';
579
- if (search && !searchIndex.rooms().has(locator))
580
- return 'indexing';
581
630
  return 'complete';
582
631
  }
583
- function conversationCoverage(reach, search) {
632
+ function conversationCoverage(reach) {
584
633
  const declared = reach === null || reach.conversations;
585
634
  if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
586
635
  return 'undeclared';
587
636
  if (!conversations.known() || !conversations.current())
588
637
  return 'acquiring';
589
- if (!search)
590
- return 'complete';
591
- const unindexed = conversations
592
- .searchable()
593
- .filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
594
- if (unindexed.length === 0)
595
- return 'complete';
596
- index([], unindexed);
597
- return 'indexing';
638
+ return 'complete';
598
639
  }
599
640
  function connectionOf() {
600
641
  if (socketOpen)
@@ -624,22 +665,18 @@ export function composeRuntime(options) {
624
665
  function readinessFor(port, reach) {
625
666
  const required = requiredRooms(reach);
626
667
  const search = port.search === 'required';
627
- const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
628
- const building = (state) => coverage
629
- .filter((room) => room.state === state && !unindexable.has(room.room))
668
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
669
+ const preparing = coverage
670
+ .filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
630
671
  .map(({ room }) => room);
631
- const preparing = building('preparing');
632
672
  if (preparing.length > 0)
633
673
  prepare(preparing);
634
- const indexing = building('indexing');
635
- if (indexing.length > 0)
636
- index(indexing, []);
637
674
  return readinessOf({
638
675
  code: port.code,
639
676
  coverage: {
640
677
  listed: required.listed,
641
678
  rooms: coverage,
642
- conversations: conversationCoverage(reach, search),
679
+ conversations: conversationCoverage(reach),
643
680
  search: search ? 'required' : 'unused',
644
681
  },
645
682
  persistence,
@@ -657,7 +694,7 @@ export function composeRuntime(options) {
657
694
  function establish(name) {
658
695
  targets = { ...targets, [name]: { established: true } };
659
696
  queueReadiness();
660
- void durably((local) => local.keepTargets(targets)).catch(() => {
697
+ void durably((local) => local.fact('targets', targets)).catch(() => {
661
698
  journal.record(`target record failed ${name}`);
662
699
  });
663
700
  }
@@ -694,18 +731,12 @@ export function composeRuntime(options) {
694
731
  readinessWatches.delete(watch);
695
732
  };
696
733
  }
697
- function layerBytes(layer) {
734
+ function pageBytes(page) {
698
735
  let bytes = 0;
699
- for (const [path, text] of Object.entries(layer.documents))
700
- bytes += (path.length + text.length) * 2;
736
+ for (const { path, content } of page.entries)
737
+ bytes += (path.length + (content?.length ?? 0)) * 2;
701
738
  return bytes;
702
739
  }
703
- function grownBy(next, previous) {
704
- if (previous === undefined)
705
- return layerBytes(next);
706
- const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
707
- return layerBytes({ ...next, documents });
708
- }
709
740
  function exceeded(error) {
710
741
  return (typeof error === 'object' &&
711
742
  error !== null &&
@@ -713,7 +744,7 @@ export function composeRuntime(options) {
713
744
  }
714
745
  async function estimated() {
715
746
  const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
716
- return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
747
+ return estimate === null ? null : Math.max(0, estimate.total - estimate.usage);
717
748
  }
718
749
  function pause(locator, cause) {
719
750
  paused.add(locator);
@@ -734,7 +765,8 @@ export function composeRuntime(options) {
734
765
  const available = await estimated().catch(() => null);
735
766
  if (available === null || available < (held.needed ?? 0))
736
767
  return;
737
- if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
768
+ if (held.cause === 'storage-exceeded' &&
769
+ (held.available === null || available <= held.available))
738
770
  return;
739
771
  resume();
740
772
  }
@@ -776,7 +808,7 @@ export function composeRuntime(options) {
776
808
  }
777
809
  async function exceededOn(locator, bytes) {
778
810
  const available = await estimated().catch(() => null);
779
- pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
811
+ pause(locator, { cause: 'storage-exceeded', needed: bytes, available });
780
812
  }
781
813
  function seedRoomHeld(room, version) {
782
814
  return prepared.has(room) && reachedBy(room, version);
@@ -813,6 +845,75 @@ export function composeRuntime(options) {
813
845
  shown();
814
846
  }
815
847
  }
848
+ function sameCover(left, right) {
849
+ return JSON.stringify(left) === JSON.stringify(right);
850
+ }
851
+ async function coveredRows(plan, cover) {
852
+ for (;;) {
853
+ const before = cover();
854
+ if ('pending' in before)
855
+ return { pending: before };
856
+ const local = await openLocal();
857
+ const keys = before.rooms.map(({ key }) => key);
858
+ const changes = local.changes(keys);
859
+ const current = () => local.changes(keys) === changes;
860
+ const rows = await windowRows(local, plan, before.rooms, current);
861
+ const after = cover();
862
+ if (rows !== null && sameCover(before, after))
863
+ return { rows, cover: before, current };
864
+ }
865
+ }
866
+ async function windowRows(local, plan, rooms, unchanged) {
867
+ const first = await local.query(plan, rooms, QUERY_BYTES);
868
+ if (first.more === undefined || !('documents' in first)) {
869
+ const { more: _more, ...rows } = first;
870
+ return rows;
871
+ }
872
+ const documents = [...first.documents];
873
+ for (let more = first.more; more !== undefined;) {
874
+ if (!unchanged())
875
+ return null;
876
+ const page = await local.query({
877
+ ...plan,
878
+ after: { text: more, values: JSON.parse(more) },
879
+ limit: plan.limit === null ? null : plan.limit - documents.length,
880
+ total: false,
881
+ }, rooms, QUERY_BYTES);
882
+ if (!('documents' in page))
883
+ return null;
884
+ documents.push(...page.documents);
885
+ more = page.more;
886
+ if (more === undefined) {
887
+ return {
888
+ documents,
889
+ ...(page.next === undefined ? {} : { next: page.next }),
890
+ ...(first.total === undefined ? {} : { total: first.total }),
891
+ };
892
+ }
893
+ }
894
+ return null;
895
+ }
896
+ function keyedOf(rows, coverage) {
897
+ if ('groups' in rows) {
898
+ return {
899
+ result: { ...rows, coverage },
900
+ keys: rows.groups.map((group) => JSON.stringify(group.key)),
901
+ };
902
+ }
903
+ return {
904
+ result: { ...rows, coverage },
905
+ keys: rows.documents.map((document) => documentKey(document.room, document.path)),
906
+ };
907
+ }
908
+ async function answeredPlan(plan, cover) {
909
+ const answered = await coveredRows(plan, cover);
910
+ if ('pending' in answered) {
911
+ const pending = answered.pending;
912
+ return { result: pending, keys: [], current: () => true };
913
+ }
914
+ const held = answered.cover;
915
+ return { ...keyedOf(answered.rows, held.coverage), current: answered.current };
916
+ }
816
917
  async function rowsQuery(plan) {
817
918
  const before = covered(plan);
818
919
  awaitPreparation(before);
@@ -821,59 +922,15 @@ export function composeRuntime(options) {
821
922
  return seededResult(seeded).result;
822
923
  if ('pending' in before)
823
924
  return before;
824
- const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
825
- awaitPreparation(cover);
826
- if ('pending' in cover)
925
+ const answered = await coveredRows(plan, () => {
926
+ const cover = covered(plan);
927
+ awaitPreparation(cover);
827
928
  return cover;
828
- return { ...rows, coverage: cover.coverage };
829
- }
830
- async function indexConversation(conversation) {
831
- searchIndex.indexMessages(conversation, [], false);
832
- let after = null;
833
- for (;;) {
834
- const batch = await durably((local) => local.messagesAfter(conversation, after));
835
- if (closed)
836
- return;
837
- searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
838
- if (batch.length === 0)
839
- return;
840
- after = batch[batch.length - 1].identity;
841
- }
842
- }
843
- function indexNext() {
844
- const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
845
- if (room !== undefined) {
846
- roomsToIndex.delete(room);
847
- return () => prepared.has(room)
848
- ? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
849
- : undefined;
850
- }
851
- const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
852
- if (conversation === undefined)
853
- return null;
854
- conversationsToIndex.delete(conversation);
855
- return () => indexConversation(conversation);
856
- }
857
- function index(rooms, named) {
858
- for (const room of rooms)
859
- roomsToIndex.add(room);
860
- for (const conversation of named)
861
- conversationsToIndex.add(conversation);
862
- if (indexing)
863
- return;
864
- indexing = true;
865
- void (async () => {
866
- try {
867
- for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
868
- await yielded();
869
- await unit();
870
- queueReadiness();
871
- }
872
- }
873
- finally {
874
- indexing = false;
875
- }
876
- })().catch(() => { });
929
+ });
930
+ if ('pending' in answered)
931
+ return answered.pending;
932
+ const held = answered.cover;
933
+ return { ...answered.rows, coverage: held.coverage };
877
934
  }
878
935
  function publishChangeSet(set) {
879
936
  applyChangeSet(set);
@@ -883,7 +940,10 @@ export function composeRuntime(options) {
883
940
  for (const locator of set.queues)
884
941
  emitQueue(locator);
885
942
  if (set.drafts.length > 0 ||
886
- set.documents.some((change) => roomDrafts.get(change.locator)?.has(change.path) === true)) {
943
+ set.documents.some((change) => {
944
+ const held = roomDrafts.get(change.locator);
945
+ return held !== undefined && (change.path === null || held.has(change.path));
946
+ })) {
887
947
  publishRoomDrafts(false);
888
948
  }
889
949
  for (const app of set.presentations)
@@ -896,9 +956,21 @@ export function composeRuntime(options) {
896
956
  if (reason === 'revoked-authorization') {
897
957
  await exclusive(async () => {
898
958
  const revoked = { status: 'unavailable', reason: 'revoked' };
959
+ const records = [...replicas].map(([locator, held]) => ({
960
+ locator,
961
+ held,
962
+ record: { ...held.record, model: revoked, selectionModel: revoked },
963
+ }));
899
964
  try {
900
965
  const local = await openLocal();
901
- await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
966
+ await local.commit({
967
+ rooms: records.map(({ locator, record }) => ({
968
+ locator,
969
+ key: record.key,
970
+ record: storedRecord(record),
971
+ composition: compositionOf(record),
972
+ })),
973
+ });
902
974
  }
903
975
  catch {
904
976
  journal.record('model revocation persistence failed');
@@ -914,16 +986,16 @@ export function composeRuntime(options) {
914
986
  liveModels.delete(key);
915
987
  }
916
988
  const changed = [];
917
- for (const [locator, room] of rooms) {
918
- const before = replicas.get(locator) ?? null;
919
- if (before !== null) {
920
- const record = {
921
- ...before.confirmed.record,
922
- model: revoked,
923
- selectionModel: revoked,
924
- };
925
- replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
926
- }
989
+ for (const { locator, held, record } of records) {
990
+ if (replicas.get(locator) !== held)
991
+ continue;
992
+ recomposed(locator, held.record, record);
993
+ replicas.set(locator, { ...held, record });
994
+ }
995
+ for (const locator of [...rooms.keys()]) {
996
+ const room = rooms.get(locator);
997
+ if (room === undefined)
998
+ continue;
927
999
  rooms.set(locator, {
928
1000
  ...room,
929
1001
  model: modelTypes(revoked),
@@ -931,9 +1003,10 @@ export function composeRuntime(options) {
931
1003
  ? null
932
1004
  : { ...room.typedSelection, model: modelTypes(revoked) },
933
1005
  });
934
- changed.push(changeSet(locator, before, replicas.get(locator) ?? null, { room: true }));
1006
+ changed.push(changeSet(locator, { documents: null, room: true }));
935
1007
  }
936
1008
  publishChangeSet(mergeChangeSets(changed));
1009
+ prepare();
937
1010
  });
938
1011
  }
939
1012
  }
@@ -964,12 +1037,13 @@ export function composeRuntime(options) {
964
1037
  model: modelTypes(modelOf(room.locator, 'typed-selection')),
965
1038
  },
966
1039
  providerOwnedPaths: room.providerOwnedPaths,
967
- roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
1040
+ roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
968
1041
  synchronizationAvailable,
969
1042
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
970
1043
  });
971
1044
  }
972
1045
  async function applyDiscovery(found, started, covered) {
1046
+ const lockedBefore = lockedRooms();
973
1047
  for (const room of found) {
974
1048
  const fence = fences.get(room.locator);
975
1049
  if (fence !== undefined && fence.erased && fence.at < started)
@@ -995,15 +1069,17 @@ export function composeRuntime(options) {
995
1069
  else
996
1070
  unlistedKept(unlisted);
997
1071
  for (const room of listed) {
998
- const held = replicas.get(room.locator)?.confirmed.record;
1072
+ const held = replicas.get(room.locator)?.record;
999
1073
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
1000
1074
  list(held, false);
1001
1075
  continue;
1002
1076
  }
1003
1077
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
1004
1078
  }
1079
+ confirmLocks(listed);
1005
1080
  roomsListed = true;
1006
1081
  emitRooms();
1082
+ relocked(lockedBefore);
1007
1083
  return listed;
1008
1084
  }
1009
1085
  function heldLocally(locator) {
@@ -1024,7 +1100,7 @@ export function composeRuntime(options) {
1024
1100
  }
1025
1101
  function unlistedKept(unlisted) {
1026
1102
  for (const locator of unlisted) {
1027
- const held = replicas.get(locator)?.confirmed.record;
1103
+ const held = replicas.get(locator)?.record;
1028
1104
  if (held !== undefined)
1029
1105
  list(held, false);
1030
1106
  else
@@ -1033,16 +1109,16 @@ export function composeRuntime(options) {
1033
1109
  }
1034
1110
  function fence(locators) {
1035
1111
  const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1036
- const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
1037
- room: true,
1038
- queue: true,
1112
+ const removed = fresh.map((locator) => ({
1113
+ locator,
1039
1114
  drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
1040
1115
  }));
1041
1116
  for (const locator of fresh) {
1042
1117
  fences.set(locator, { at: discoveries, erased: false });
1043
1118
  lastFenced.set(locator, ++fencings);
1119
+ confirmed.delete(locator);
1044
1120
  for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1045
- pendings.delete(batch.id);
1121
+ pendings.delete(pendingKey(locator, batch.id));
1046
1122
  replicas.delete(locator);
1047
1123
  queuesWithoutReplica.delete(locator);
1048
1124
  rooms.delete(locator);
@@ -1054,8 +1130,9 @@ export function composeRuntime(options) {
1054
1130
  unreadable.delete(locator);
1055
1131
  stale.delete(locator);
1056
1132
  repairable.delete(locator);
1057
- unindexable.delete(locator);
1133
+ unpreparable.delete(locator);
1058
1134
  paused.delete(locator);
1135
+ prepared.delete(locator);
1059
1136
  scopedRefusals.delete(locator);
1060
1137
  sourceRefusals.delete(locator);
1061
1138
  declarationRefusals.delete(locator);
@@ -1069,26 +1146,48 @@ export function composeRuntime(options) {
1069
1146
  }
1070
1147
  if (fresh.length === 0)
1071
1148
  return fresh;
1072
- publishChangeSet(mergeChangeSets(removed));
1073
- for (const locator of fresh)
1074
- prepared.delete(locator);
1075
- searchIndex.forget(fresh);
1076
- readSide.recover();
1149
+ publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, { documents: null, room: true, queue: true, drafts }))));
1150
+ liveQueries.recover();
1077
1151
  moveTo({ ...status, ...attention() });
1078
1152
  return fresh;
1079
1153
  }
1080
- async function erased(local, locators) {
1081
- await local.erase(locators);
1154
+ async function eraseFenced(locators) {
1082
1155
  for (const locator of locators) {
1156
+ while (await durably((local) => local.erase(locator, ERASE_BATCH)))
1157
+ await yielded();
1083
1158
  const held = fences.get(locator);
1084
1159
  if (held !== undefined)
1085
1160
  held.erased = true;
1086
1161
  }
1087
1162
  }
1163
+ let sweeping = false;
1164
+ function sweep() {
1165
+ if (sweeping || closed)
1166
+ return;
1167
+ sweeping = true;
1168
+ void (async () => {
1169
+ try {
1170
+ while (!closed && (await durably((local) => local.erase(null, ERASE_BATCH)))) {
1171
+ await yielded();
1172
+ }
1173
+ }
1174
+ catch {
1175
+ journal.record('erasure failed');
1176
+ }
1177
+ finally {
1178
+ sweeping = false;
1179
+ }
1180
+ })();
1181
+ }
1088
1182
  async function loseRooms(locators) {
1089
1183
  const fresh = fence(locators);
1090
- if (fresh.length > 0)
1091
- await durably((local) => erased(local, fresh));
1184
+ if (fresh.length === 0)
1185
+ return;
1186
+ await durably(async (local) => {
1187
+ await local.fence(fresh);
1188
+ await unpair(local);
1189
+ });
1190
+ await eraseFenced(fresh);
1092
1191
  }
1093
1192
  function restored(key, persisted) {
1094
1193
  const prior = models.get(key);
@@ -1109,54 +1208,58 @@ export function composeRuntime(options) {
1109
1208
  return model;
1110
1209
  }
1111
1210
  async function hydrate(opened) {
1112
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
1113
- opened.all(),
1211
+ const [held, queues, kept, heldConversations, chatDrafts] = await Promise.all([
1212
+ opened.opened(),
1114
1213
  opened.queues(),
1115
1214
  opened.roomDrafts(),
1116
- opened.presentation(),
1117
1215
  opened.conversations(),
1118
1216
  opened.chatDrafts(),
1119
- opened.lastCompleted(),
1120
- opened.providerLimits(),
1121
- opened.capabilities(),
1122
- opened.member(),
1123
- opened.transfers(),
1124
- opened.targets(),
1125
1217
  ]);
1126
- targets = heldTargets;
1218
+ const records = [...held.rooms.values()];
1219
+ const counts = await opened.counts(records.map(({ key, typedSelection }) => ({ key, roots: typedSelection?.roots ?? [] })));
1220
+ const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1221
+ targets = held.targets;
1127
1222
  transfers.clear();
1128
- for (const record of staged)
1129
- transfers.set(record.locator, record);
1130
- limits = declared;
1131
- capabilities = named;
1132
- member = known;
1223
+ for (const [locator, record] of held.transfers)
1224
+ transfers.set(locator, record);
1225
+ limits = held.providerLimits;
1226
+ capabilities = held.capabilities;
1227
+ member = held.member;
1133
1228
  roomDrafts.clear();
1134
1229
  for (const record of kept)
1135
1230
  rememberRoomDraft(record);
1136
1231
  presentations.clear();
1137
- for (const record of presented)
1232
+ for (const record of held.presentation)
1138
1233
  presentations.set(record.app, record.value);
1139
- conversations.hydrate(held, chatDrafts);
1140
- const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1234
+ conversations.hydrate(heldConversations, chatDrafts);
1141
1235
  pendings.clear();
1142
1236
  for (const queue of queues) {
1143
- for (const { batch, state } of queue.pending)
1144
- pendings.set(batch.id, state);
1237
+ for (const { batch, state } of queue.pending) {
1238
+ pendings.set(pendingKey(queue.locator, batch.id), state);
1239
+ }
1145
1240
  }
1146
- const previousReplicas = new Map(replicas);
1147
1241
  replicas.clear();
1242
+ prepared.clear();
1148
1243
  queuesWithoutReplica.clear();
1149
- const replicated = new Set(records.map((record) => record.locator));
1150
1244
  for (const [locator, queue] of queuedBatches) {
1151
- if (!replicated.has(locator))
1245
+ if (!held.rooms.has(locator))
1152
1246
  queuesWithoutReplica.set(locator, queue);
1153
1247
  }
1154
- for (const record of records) {
1155
- const model = restored(modelKey(record.locator, 'collection'), record.model);
1156
- const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
1157
- const prior = previousReplicas.get(record.locator);
1158
- const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
1159
- replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
1248
+ records.forEach((stored, index) => {
1249
+ if (fences.has(stored.locator))
1250
+ return;
1251
+ const model = restored(modelKey(stored.locator, 'collection'), stored.model);
1252
+ const selectionModel = restored(modelKey(stored.locator, 'typed-selection'), stored.selectionModel);
1253
+ const record = { ...stored, model, selectionModel };
1254
+ replicas.set(record.locator, {
1255
+ record,
1256
+ queue: queuedBatches.get(record.locator) ?? [],
1257
+ overlaid: held.overlaid.get(record.key) ?? new Set(),
1258
+ count: counts[index] ?? 0,
1259
+ confirmed: 0,
1260
+ });
1261
+ if (!held.unprepared.has(record.key))
1262
+ prepared.add(record.locator);
1160
1263
  const existing = rooms.get(record.locator);
1161
1264
  if (existing === undefined)
1162
1265
  list(record, false);
@@ -1175,11 +1278,15 @@ export function composeRuntime(options) {
1175
1278
  },
1176
1279
  });
1177
1280
  }
1178
- }
1281
+ });
1282
+ for (const key of held.orphans)
1283
+ await opened.release(key);
1284
+ if (held.erasing || held.orphans.length > 0)
1285
+ queueMicrotask(sweep);
1179
1286
  return {
1180
- lastCompleted,
1287
+ lastCompleted: held.lastCompleted,
1181
1288
  locators: new Set([...queuedBatches.keys(), ...replicas.keys()]),
1182
- read: mergeChangeSets([...new Set([...previousReplicas.keys(), ...replicas.keys()])].map((locator) => changeSet(locator, previousReplicas.get(locator) ?? null, replicas.get(locator) ?? null))),
1289
+ read: mergeChangeSets([...replicas.keys()].map((locator) => changeSet(locator, { documents: null }))),
1183
1290
  };
1184
1291
  }
1185
1292
  async function readState(opened) {
@@ -1198,8 +1305,7 @@ export function composeRuntime(options) {
1198
1305
  async function open() {
1199
1306
  if (closed)
1200
1307
  throw new Error('runtime closed');
1201
- const opened = await openRuntimeStore(isFenced);
1202
- store = opened;
1308
+ const opened = openRuntimeStore(isFenced, options.database);
1203
1309
  const interrupted = await opened.fences();
1204
1310
  for (const locator of interrupted) {
1205
1311
  fences.set(locator, { at: discoveries, erased: false });
@@ -1218,9 +1324,17 @@ export function composeRuntime(options) {
1218
1324
  emitStorageLoss();
1219
1325
  }
1220
1326
  const lastCompleted = await readState(opened);
1221
- if (interrupted.length > 0)
1222
- await erased(opened, interrupted);
1327
+ if (interrupted.length > 0) {
1328
+ queueMicrotask(() => {
1329
+ void eraseFenced(interrupted).catch(() => {
1330
+ journal.record('erasure failed');
1331
+ });
1332
+ });
1333
+ }
1223
1334
  moveTo({ ...status, lastCompleted, ...attention() });
1335
+ void exclusive(() => unpair(opened)).catch(() => {
1336
+ journal.record('unpair failed');
1337
+ });
1224
1338
  return opened;
1225
1339
  }
1226
1340
  function durably(work) {
@@ -1241,80 +1355,272 @@ export function composeRuntime(options) {
1241
1355
  mutations = next.then(() => undefined, () => undefined);
1242
1356
  return next;
1243
1357
  }
1244
- function withdrawable(batch) {
1245
- const state = pendings.get(batch.id);
1246
- return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
1358
+ function withdrawable(locator) {
1359
+ return (batch) => {
1360
+ const state = pendings.get(pendingKey(locator, batch.id));
1361
+ return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
1362
+ };
1363
+ }
1364
+ const REWRITE_LINKS = ['field', 'body'];
1365
+ const CHECKED_LINKS = ['field', 'section'];
1366
+ async function sliceAt(record, view, queue, extra = [], moved = []) {
1367
+ const local = await openLocal();
1368
+ const needed = new Set([...touchedBy(queue), ...extra]);
1369
+ const movedPaths = [...new Set([...movedBy(queue, record.locator), ...moved])];
1370
+ if (movedPaths.length > 0) {
1371
+ for (const { path } of await local.referrers(record.key, view, movedPaths, REWRITE_LINKS)) {
1372
+ needed.add(path);
1373
+ }
1374
+ }
1375
+ const paths = [...needed];
1376
+ return stepped(slicing(await local.entries(record.key, view, paths), paths));
1247
1377
  }
1248
- async function settle(local, locator, before, confirmed, queue, settling = {}) {
1378
+ async function landedWhole(locator, batch, confirmedLayer, current) {
1379
+ if (!landed(batch, confirmedLayer, current))
1380
+ return false;
1381
+ const partner = partnerOf(batch, locator);
1382
+ if (partner === null)
1383
+ return true;
1384
+ const other = replicas.get(partner);
1385
+ const half = other?.queue.find((one) => one.id === batch.id);
1386
+ if (other === undefined || half === undefined)
1387
+ return false;
1388
+ return landed(half, await sliceAt(other.record, 'confirmed', [half]));
1389
+ }
1390
+ function carriedBack(locator, unlanded, written) {
1391
+ const back = new Map();
1392
+ const taken = [];
1393
+ for (const batch of unlanded) {
1394
+ const between = batch.between;
1395
+ if (between === undefined)
1396
+ continue;
1397
+ const source = between.source.room;
1398
+ const destination = between.destination.room;
1399
+ for (const { path: to } of pendings.get(pendingKey(destination, batch.id))?.drafts ?? []) {
1400
+ const record = roomDrafts.get(destination)?.get(to);
1401
+ const path = between.moves.find((move) => move.to === to)?.path;
1402
+ if (record === undefined || path === undefined)
1403
+ continue;
1404
+ const [into, key] = source === locator ? [written, path] : [back, documentKey(source, path)];
1405
+ const existing = into.get(key) ?? roomDrafts.get(source)?.get(path);
1406
+ taken.push({ locator: destination, path: to });
1407
+ into.set(key, {
1408
+ locator: source,
1409
+ path,
1410
+ revision: roomDraftRevision(),
1411
+ parts: returnedInto(existing?.parts, record.parts),
1412
+ returned: existing?.returned ?? null,
1413
+ });
1414
+ }
1415
+ }
1416
+ const records = [...back.values()];
1417
+ const elsewhere = taken.filter((one) => one.locator !== locator);
1418
+ const rooms = new Set([...records, ...elsewhere].map((one) => one.locator));
1419
+ return {
1420
+ taken: taken.filter((one) => one.locator === locator).map(({ path }) => path),
1421
+ writes: [...rooms].map((room) => ({
1422
+ locator: room,
1423
+ roomDrafts: records.filter((record) => record.locator === room),
1424
+ consumedRoomDrafts: elsewhere.filter((one) => one.locator === room).map(({ path }) => path),
1425
+ })),
1426
+ changes: [...rooms].map((room) => changeSet(room, {
1427
+ drafts: [...records, ...elsewhere]
1428
+ .filter((one) => one.locator === room)
1429
+ .map(({ path }) => path),
1430
+ })),
1431
+ remember() {
1432
+ for (const one of taken)
1433
+ forgetRoomDraft(one.locator, one.path);
1434
+ for (const record of records)
1435
+ rememberRoomDraft(record);
1436
+ },
1437
+ };
1438
+ }
1439
+ function recomposed(locator, before, after) {
1440
+ if (before !== undefined &&
1441
+ before.key === after.key &&
1442
+ fingerprint(compositionOf(before)) === fingerprint(compositionOf(after))) {
1443
+ return;
1444
+ }
1445
+ if (before !== undefined && before.key === after.key)
1446
+ prepared.delete(locator);
1447
+ }
1448
+ function committedRoom(locator, record, answer) {
1449
+ if (answer.prepared[locator] === false)
1450
+ prepared.delete(locator);
1451
+ else if (answer.prepared[locator] === true)
1452
+ prepared.add(locator);
1453
+ if (!prepared.has(locator))
1454
+ prepare([locator]);
1455
+ return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0 };
1456
+ }
1457
+ function replayed(record, confirmedLayer, queue) {
1458
+ return stepped(replaying(confirmedLayer, queue, record.locator, compositionOf(record)));
1459
+ }
1460
+ async function overlayDiff(record, before, visible, touched) {
1461
+ const kept = before !== undefined && before.record.key === record.key
1462
+ ? before.overlaid
1463
+ : new Set();
1464
+ return {
1465
+ overlay: {
1466
+ set: await stepped(overlayOf(visible, touched)),
1467
+ cleared: [...kept].filter((path) => !touched.has(path)),
1468
+ },
1469
+ touched,
1470
+ };
1471
+ }
1472
+ async function settle(local, locator, before, record, queue, settling = {}) {
1473
+ return together(local, [await settlement(local, locator, before, record, queue, settling)]);
1474
+ }
1475
+ function unpairedIn(locator, batch) {
1476
+ if (!queueIn(locator).some((held) => held.id === batch.id))
1477
+ return false;
1478
+ if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
1479
+ return false;
1480
+ return !leavingIn(batch, locator) || withdrawable(locator)(batch);
1481
+ }
1482
+ async function together(local, settlements) {
1483
+ const settled = new Set(settlements.map(({ locator }) => locator));
1484
+ const all = [...settlements];
1485
+ for (let wave = settlements; wave.length > 0;) {
1486
+ const partnered = new Map();
1487
+ for (const { locator, left } of wave) {
1488
+ for (const batch of left) {
1489
+ const partner = partnerOf(batch, locator);
1490
+ if (partner === null || settled.has(partner) || !unpairedIn(partner, batch))
1491
+ continue;
1492
+ partnered.set(partner, [...(partnered.get(partner) ?? []), batch]);
1493
+ }
1494
+ }
1495
+ const partners = [];
1496
+ for (const [partner, batches] of partnered) {
1497
+ settled.add(partner);
1498
+ partners.push(await unpairing(local, partner, batches));
1499
+ }
1500
+ all.push(...partners);
1501
+ wave = partners;
1502
+ }
1503
+ const answer = await local.commit({
1504
+ rooms: all.flatMap(({ commit }) => commit.rooms),
1505
+ writes: all.flatMap(({ commit }) => commit.writes),
1506
+ });
1507
+ return mergeChangeSets(all.map((one) => one.apply(answer)));
1508
+ }
1509
+ async function settlement(local, locator, before, record, queue, settling = {}) {
1510
+ const view = settling.view ?? 'confirmed';
1249
1511
  const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
1250
- const { withdrawn, kept } = standing(confirmed, queue, withdrawable, localSaves);
1512
+ const overrides = settling.changes;
1513
+ const read = await sliceAt(record, view, queue, [
1514
+ ...(before !== undefined && before.record.key === record.key ? before.overlaid : []),
1515
+ ...(overrides?.set.map(({ path }) => path) ?? []),
1516
+ ...(overrides?.removed ?? []),
1517
+ ]);
1518
+ const confirmedLayer = overrides === undefined
1519
+ ? read
1520
+ : await stepped(slicingWith(read, overrides.set, overrides.removed));
1521
+ const composition = compositionOf(record);
1522
+ const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
1251
1523
  const founded = kept.filter((batch) => !queue.includes(batch));
1252
1524
  const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
1253
- const unqueued = withdrawn.map((batch) => ({
1254
- batch,
1255
- returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
1256
- }));
1525
+ const unqueued = [];
1526
+ for (const batch of withdrawn) {
1527
+ unqueued.push({ batch, landed: await landedWhole(locator, batch, confirmedLayer) });
1528
+ }
1257
1529
  const returning = [
1258
- ...[...unqueued].reverse(),
1259
- ...(settling.leaving === undefined ? [] : [settling.leaving]),
1530
+ ...[...unqueued].reverse().map(({ batch, landed: held }) => ({
1531
+ batch,
1532
+ returned: localSaves || held ? null : returnedBy('withdrawn'),
1533
+ })),
1534
+ ...(settling.leaving ?? []),
1260
1535
  ];
1261
1536
  const written = new Map();
1262
- for (const { batch, returned } of returning) {
1263
- if (returned === null)
1537
+ for (const { batch, returned: reason } of returning) {
1538
+ if (reason === null || batch.between?.destination.room === locator)
1264
1539
  continue;
1265
- for (const saved of pendings.get(batch.id)?.drafts ?? []) {
1540
+ for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
1266
1541
  const existing = written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path);
1267
1542
  written.set(saved.path, {
1268
1543
  locator,
1269
1544
  path: saved.path,
1270
1545
  revision: roomDraftRevision(),
1271
1546
  parts: returnedInto(existing?.parts, saved.parts),
1272
- returned,
1547
+ returned: reason,
1273
1548
  });
1274
1549
  }
1275
1550
  }
1276
- await local.keepAuthored({ locator, queue: kept, founded, roomDrafts: [...written.values()] });
1551
+ const carried = carriedBack(locator, [
1552
+ ...[...unqueued].reverse().flatMap(({ batch, landed: held }) => (held ? [] : [batch])),
1553
+ ...(settling.leaving ?? []).flatMap(({ batch, returned }) => returned === null ? [] : [batch]),
1554
+ ], written);
1555
+ const replayedLayer = await replayed(record, confirmedLayer, kept);
1556
+ const { overlay, touched } = await overlayDiff(record, before, replayedLayer.visible, replayedLayer.touched);
1557
+ const commit = {
1558
+ rooms: [
1559
+ {
1560
+ locator,
1561
+ key: record.key,
1562
+ record: storedRecord(record),
1563
+ roots: record.typedSelection?.roots ?? [],
1564
+ ...settling.commit,
1565
+ ...(overrides === undefined
1566
+ ? {}
1567
+ : { confirmed: { set: overrides.set, removed: overrides.removed } }),
1568
+ overlay,
1569
+ },
1570
+ ],
1571
+ writes: [
1572
+ {
1573
+ locator,
1574
+ queue: kept,
1575
+ founded,
1576
+ roomDrafts: [...written.values()],
1577
+ consumedRoomDrafts: carried.taken,
1578
+ },
1579
+ ...carried.writes,
1580
+ ],
1581
+ };
1277
1582
  const staying = new Set(kept.map((batch) => batch.id));
1278
- for (const batch of previousQueue) {
1279
- if (!staying.has(batch.id))
1280
- pendings.delete(batch.id);
1281
- }
1282
- for (const record of written.values())
1283
- rememberRoomDraft(record);
1284
- try {
1285
- await local.write({
1286
- next: confirmed.record,
1287
- previous: before?.confirmed.record ?? null,
1288
- ...(settling.stored === undefined ? {} : { stored: settling.stored }),
1289
- });
1290
- }
1291
- catch (error) {
1292
- const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
1293
- if (held !== null)
1294
- replicas.set(locator, held);
1295
- else if (kept.length > 0)
1296
- queuesWithoutReplica.set(locator, kept);
1297
- else
1583
+ return {
1584
+ locator,
1585
+ left: previousQueue.filter((batch) => !staying.has(batch.id)),
1586
+ commit,
1587
+ apply: (answer) => {
1588
+ for (const batch of previousQueue) {
1589
+ if (!staying.has(batch.id))
1590
+ pendings.delete(pendingKey(locator, batch.id));
1591
+ }
1592
+ for (const draft of written.values())
1593
+ rememberRoomDraft(draft);
1594
+ carried.remember();
1595
+ const drafts = [...written.keys(), ...carried.taken];
1298
1596
  queuesWithoutReplica.delete(locator);
1299
- publishChangeSet(changeSet(locator, before ?? null, held, {
1300
- queue: queueChanged,
1301
- drafts: [...written.keys()],
1302
- }));
1303
- moveTo({ ...status, ...attention() });
1304
- throw error;
1305
- }
1306
- return () => {
1307
- queuesWithoutReplica.delete(locator);
1308
- for (const { batch, returned } of unqueued) {
1309
- const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
1310
- journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1311
- }
1312
- replicas.set(locator, replicaOf(confirmed, kept, before));
1313
- return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
1314
- room: settling.room,
1315
- queue: queueChanged,
1316
- drafts: [...written.keys()],
1317
- });
1597
+ for (const { batch, landed: held } of unqueued) {
1598
+ const outcome = localSaves ? 'dropped' : held ? 'landed' : 'withdrawn';
1599
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1600
+ }
1601
+ const changed = answer.changed[locator];
1602
+ const confirmedChanged = settling.commit?.published !== undefined ||
1603
+ settling.commit?.refreshed === true ||
1604
+ overrides !== undefined;
1605
+ recomposed(locator, before?.record, record);
1606
+ const { count } = committedRoom(locator, record, answer);
1607
+ replicas.set(locator, {
1608
+ record,
1609
+ queue: kept,
1610
+ overlaid: touched,
1611
+ count,
1612
+ confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
1613
+ });
1614
+ return mergeChangeSets([
1615
+ changeSet(locator, {
1616
+ documents: changed === undefined ? [] : changed,
1617
+ room: settling.room,
1618
+ queue: queueChanged,
1619
+ drafts,
1620
+ }),
1621
+ ...carried.changes,
1622
+ ]);
1623
+ },
1318
1624
  };
1319
1625
  }
1320
1626
  function adoptModels(locator, record) {
@@ -1335,7 +1641,7 @@ export function composeRuntime(options) {
1335
1641
  const taken = (answer, previous, attached) => {
1336
1642
  if (answer === undefined)
1337
1643
  return previous;
1338
- const { state, refused } = attachedModel(answer, previous);
1644
+ const { state, refused } = attachedModel(answer, previous, attached === 'collection' ? roomTypes : NO_ROOM_TYPES);
1339
1645
  if (refused !== null) {
1340
1646
  const where = refused.location === undefined ? '' : ` ${refused.location}`;
1341
1647
  journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
@@ -1348,65 +1654,76 @@ export function composeRuntime(options) {
1348
1654
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
1349
1655
  };
1350
1656
  }
1351
- function publish(local, room, confirmed, landing, live) {
1352
- const { base, stored } = landing;
1657
+ function publish(local, room, record, landing, live) {
1353
1658
  return exclusive(async () => {
1354
- if (stored !== undefined) {
1355
- await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
1356
- }
1357
1659
  if (!live()) {
1358
1660
  caughtUpTo(room.locator);
1359
1661
  return true;
1360
1662
  }
1361
1663
  const prior = replicas.get(room.locator);
1362
- if (base !== undefined &&
1363
- (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
1364
- prior.confirmed.record.revisions !== base.record.revisions)) {
1664
+ if (landing.kind === 'refreshed' &&
1665
+ (prior === undefined || prior.record.key !== record.key || prior.confirmed !== landing.base)) {
1666
+ await local.unrefresh(record.key);
1365
1667
  return false;
1366
1668
  }
1367
- adoptModels(room.locator, confirmed.record);
1669
+ adoptModels(room.locator, record);
1368
1670
  const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
1369
- const roomVersion = confirmed.record.roomVersion;
1671
+ const roomVersion = record.roomVersion;
1370
1672
  const accepted = queue.filter((batch) => {
1371
- const accepted = pendings.get(batch.id)?.accepted;
1372
- return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
1673
+ const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
1674
+ return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
1373
1675
  });
1374
- let settled;
1676
+ let set;
1375
1677
  try {
1376
- settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
1678
+ set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
1679
+ ? {
1680
+ room: true,
1681
+ view: 'refreshed',
1682
+ commit: { refreshed: true, composition: compositionOf(record) },
1683
+ }
1684
+ : {
1685
+ room: true,
1686
+ view: landing.refreshed ? 'refreshed' : 'confirmed',
1687
+ commit: {
1688
+ published: { from: prior?.record.key ?? null },
1689
+ composition: compositionOf(record),
1690
+ ...(landing.refreshed ? { refreshed: true } : {}),
1691
+ },
1692
+ });
1377
1693
  }
1378
1694
  catch (error) {
1379
1695
  if (!exceeded(error))
1380
1696
  throw error;
1381
- await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
1697
+ await exceededOn(room.locator, null);
1382
1698
  caughtUpTo(room.locator);
1383
1699
  return true;
1384
1700
  }
1385
- const set = settled();
1386
- if (stored !== undefined) {
1701
+ if (landing.kind === 'bootstrapped') {
1387
1702
  transfers.delete(room.locator);
1388
1703
  catching.delete(room.locator);
1704
+ sweep();
1389
1705
  }
1390
1706
  for (const batch of accepted)
1391
- pendings.delete(batch.id);
1707
+ pendings.delete(pendingKey(room.locator, batch.id));
1392
1708
  stale.delete(room.locator);
1393
- unindexable.delete(room.locator);
1709
+ unpreparable.delete(room.locator);
1394
1710
  if (unreadable.delete(room.locator))
1395
1711
  scopedRefusals.delete(room.locator);
1396
1712
  sourceRefusals.delete(room.locator);
1397
1713
  declarationRefusals.delete(room.locator);
1398
1714
  list(room, true);
1399
- if (prior === undefined)
1400
- prepared.add(room.locator);
1401
1715
  publishChangeSet(set);
1716
+ await unpair(local);
1402
1717
  moveTo({ ...status, ...attention() });
1403
1718
  options.adapter.roomStream?.published(room.locator, roomVersion);
1719
+ if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1720
+ submitLater(room.locator);
1721
+ }
1404
1722
  return true;
1405
1723
  });
1406
1724
  }
1407
1725
  async function savedRoom(locator) {
1408
1726
  const local = await openLocal();
1409
- await referenced(locator);
1410
1727
  const replica = replicas.get(locator);
1411
1728
  const room = rooms.get(locator);
1412
1729
  if (replica === undefined || room === undefined) {
@@ -1436,13 +1753,19 @@ export function composeRuntime(options) {
1436
1753
  }
1437
1754
  const target = await savedRoom(locator);
1438
1755
  const saved = [];
1756
+ const files = [];
1757
+ const filed = read.filter(({ parts }) => parts.file !== undefined).map(({ path }) => path);
1758
+ const visible = filed.length === 0 ? undefined : await visibleOf(locator, filed);
1439
1759
  for (const { path, parts } of read) {
1440
1760
  const file = parts.file === undefined
1441
1761
  ? undefined
1442
- : await heldFile(parts.file.blob, binaryBase(target.replica.visible, path));
1762
+ : await heldFile(parts.file.blob, binaryBase(visible, path));
1763
+ if (parts.file !== undefined && file !== undefined) {
1764
+ files.push({ key: file.localId, blob: parts.file.blob });
1765
+ }
1443
1766
  saved.push({ path, parts: held(undefined, parts, file) });
1444
1767
  }
1445
- return committed(locator, target, saved, new Set(), writtenBy);
1768
+ return committed(locator, target, saved, new Set(), writtenBy, files);
1446
1769
  }
1447
1770
  function carriedDrafts(locator, saved, consumed) {
1448
1771
  const holding = roomDrafts.get(locator);
@@ -1456,13 +1779,359 @@ export function composeRuntime(options) {
1456
1779
  return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
1457
1780
  }));
1458
1781
  }
1459
- async function committed(locator, { local, replica, room }, saved, consumed, writtenBy) {
1460
- const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
1782
+ async function savingIn(replica, changes, extra, leaving = []) {
1783
+ const record = replica.record;
1784
+ const local = await openLocal();
1785
+ const paths = [
1786
+ ...extra,
1787
+ ...changes.flatMap((change) => change.kind === 'move' ? [change.path, change.to] : [change.path]),
1788
+ ];
1789
+ const moved = [
1790
+ ...leaving,
1791
+ ...changes.flatMap((change) => (change.kind === 'move' ? [change.path] : [])),
1792
+ ];
1793
+ const removed = changes.flatMap((change) => (change.kind === 'remove' ? [change.path] : []));
1794
+ const referring = removed.length === 0
1795
+ ? []
1796
+ : (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
1797
+ const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
1798
+ const { visible } = await replayed(record, confirmedLayer, replica.queue);
1799
+ const composition = compositionOf(record);
1800
+ return {
1801
+ record,
1802
+ confirmed: confirmedLayer,
1803
+ visible,
1804
+ *visibleOver(queue) {
1805
+ return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
1806
+ },
1807
+ };
1808
+ }
1809
+ async function unmodeledIn(record) {
1810
+ const composition = compositionFor(record);
1811
+ if (available(composition))
1812
+ return false;
1813
+ const local = await openLocal();
1814
+ const roots = record.typedSelection?.roots ?? [];
1815
+ if (composition.model.status === 'unavailable' &&
1816
+ (await local.typedWithin(record.key, 'visible', roots, false))) {
1817
+ return true;
1818
+ }
1819
+ return (composition.selection !== null &&
1820
+ composition.selection.model.status === 'unavailable' &&
1821
+ (await local.typedWithin(record.key, 'visible', roots, true)));
1822
+ }
1823
+ function textDiffers(left, right) {
1824
+ return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
1825
+ !sameBinary(left?.binary ?? undefined, right?.binary ?? undefined));
1826
+ }
1827
+ function textOf(entry) {
1828
+ if (entry === null)
1829
+ return undefined;
1830
+ return entry.binary !== null ? null : entry.content;
1831
+ }
1832
+ function walkOf(local, key, view) {
1833
+ return async (after) => {
1834
+ const page = await local.walk(key, view, after, WALK_BYTES);
1835
+ return {
1836
+ entries: page.entries.map(({ path, content, binary }) => ({
1837
+ path,
1838
+ content: binary !== null ? null : content,
1839
+ })),
1840
+ next: page.next,
1841
+ };
1842
+ };
1843
+ }
1844
+ function lookupOf(local, key, view) {
1845
+ return async (paths) => {
1846
+ if (paths.length === 0)
1847
+ return new Map();
1848
+ return new Map((await local.typed(key, view, paths)).map(({ path, type, binary }) => [
1849
+ path,
1850
+ { type: type ?? undefined, binary },
1851
+ ]));
1852
+ };
1853
+ }
1854
+ function declaredOf(entry) {
1855
+ if (entry.binary !== null)
1856
+ return { type: undefined, binary: true };
1857
+ const read = parseFrontmatter(entry.content ?? '');
1858
+ return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
1859
+ }
1860
+ function overriddenWalk(walk, overrides) {
1861
+ const sorted = [...overrides.keys()].sort();
1862
+ const beyond = (path) => {
1863
+ if (path === null)
1864
+ return 0;
1865
+ let low = 0;
1866
+ let high = sorted.length;
1867
+ while (low < high) {
1868
+ const middle = (low + high) >>> 1;
1869
+ if (sorted[middle] <= path)
1870
+ low = middle + 1;
1871
+ else
1872
+ high = middle;
1873
+ }
1874
+ return low;
1875
+ };
1876
+ return async (after) => {
1877
+ const page = await walk(after);
1878
+ const end = page.next;
1879
+ const added = sorted
1880
+ .slice(beyond(after), end === null ? sorted.length : beyond(end))
1881
+ .flatMap((path) => {
1882
+ const entry = overrides.get(path);
1883
+ return entry === null || entry === undefined
1884
+ ? []
1885
+ : [{ path, content: entry.binary !== null ? null : entry.content }];
1886
+ });
1887
+ const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
1888
+ return { entries, next: end };
1889
+ };
1890
+ }
1891
+ function overriddenLookup(lookup, overrides) {
1892
+ return async (paths) => {
1893
+ const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
1894
+ for (const path of paths) {
1895
+ const entry = overrides.get(path);
1896
+ if (entry === undefined)
1897
+ continue;
1898
+ if (entry === null)
1899
+ known.delete(path);
1900
+ else
1901
+ known.set(path, declaredOf(entry));
1902
+ }
1903
+ return known;
1904
+ };
1905
+ }
1906
+ function validatorOf(record, confirmedLayer) {
1907
+ return async (resulting, touched) => {
1908
+ const composition = compositionFor(record);
1909
+ const local = await openLocal();
1910
+ const changed = [];
1911
+ const overrides = new Map();
1912
+ const pace = paced();
1913
+ for (const path of new Set([...confirmedLayer.held, ...touched])) {
1914
+ await pace(ROW_WEIGHT);
1915
+ const was = entryAt(confirmedLayer, path);
1916
+ const now = entryAt(resulting, path);
1917
+ if (!textDiffers(was, now))
1918
+ continue;
1919
+ overrides.set(path, now);
1920
+ if (textOf(was) !== textOf(now))
1921
+ changed.push({ path, was: textOf(was), now: textOf(now) });
1922
+ }
1923
+ const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
1924
+ const clean = available(composition) &&
1925
+ changed.length <= CLEAN_CHANGES &&
1926
+ (await changedCleanly(changed, compositionOf(record), composition, {
1927
+ lookup,
1928
+ texts: textsOf(local, record.key, 'confirmed'),
1929
+ referrers: referrersOf(local, record.key, 'confirmed'),
1930
+ }));
1931
+ if (clean)
1932
+ return { verdict: 'accept' };
1933
+ return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
1934
+ };
1935
+ }
1936
+ function textsOf(local, key, view) {
1937
+ return async (paths) => new Map((await local.entries(key, view, paths)).map(({ path, content, binary }) => [
1938
+ path,
1939
+ binary !== null ? null : content,
1940
+ ]));
1941
+ }
1942
+ function referrersOf(local, key, view) {
1943
+ return async (targets) => [
1944
+ ...new Set((await local.referrers(key, view, targets, CHECKED_LINKS)).map(({ path }) => path)),
1945
+ ];
1946
+ }
1947
+ async function sideOf(locator, replica, room, saving) {
1948
+ const record = replica.record;
1949
+ return {
1950
+ room: locator,
1951
+ handle: room.handle,
1952
+ baseRoomVersion: record.roomVersion,
1953
+ composition: compositionFor(record),
1954
+ operations: room.operations,
1955
+ providerOwnedPaths: room.providerOwnedPaths,
1956
+ visible: saving.visible,
1957
+ queued: queuedDocuments(replica.queue),
1958
+ unmodeled: await unmodeledIn(record),
1959
+ validate: validatorOf(record, saving.confirmed),
1960
+ };
1961
+ }
1962
+ function movedInto(locator, saved) {
1963
+ const into = saved.find(({ parts }) => parts.move?.room !== undefined && parts.move.room !== locator);
1964
+ return into?.parts.move?.room ?? null;
1965
+ }
1966
+ async function saveCommit(local, files, rooms) {
1967
+ const touched = new Map();
1968
+ const commits = [];
1969
+ for (const { locator, saving, before, queue } of rooms) {
1970
+ const replayedLayer = await replayed(saving.record, saving.confirmed, queue);
1971
+ const diff = await overlayDiff(saving.record, before, replayedLayer.visible, replayedLayer.touched);
1972
+ touched.set(locator, diff.touched);
1973
+ commits.push({
1974
+ locator,
1975
+ key: saving.record.key,
1976
+ roots: saving.record.typedSelection?.roots ?? [],
1977
+ overlay: diff.overlay,
1978
+ });
1979
+ }
1980
+ try {
1981
+ const answer = await local.commit({
1982
+ rooms: commits,
1983
+ writes: rooms.map(({ write }) => write),
1984
+ files,
1985
+ whole: true,
1986
+ });
1987
+ return { answer, touched };
1988
+ }
1989
+ catch {
1990
+ return null;
1991
+ }
1992
+ }
1993
+ function savedReplica(locator, before, queue, touched, answer) {
1994
+ replicas.set(locator, {
1995
+ ...before,
1996
+ queue,
1997
+ overlaid: touched,
1998
+ count: answer.counts[locator] ?? before.count,
1999
+ });
2000
+ }
2001
+ async function committedMove(locator, destination, { local, replica, room }, saved, consumed, writtenBy, files) {
2002
+ const mixed = saved.find(({ parts }) => Object.keys(parts).length !== 1 || parts.move?.room !== destination);
2003
+ if (mixed !== undefined) {
2004
+ return { outcome: 'refused', code: 'mixed-room-move', retryable: false, file: mixed.path };
2005
+ }
2006
+ const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
2007
+ const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
2008
+ const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, sourceSaving.visible)));
2009
+ if (changed.length > 0)
2010
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2011
+ const target = replicas.get(destination);
2012
+ const listedRoom = rooms.get(destination);
2013
+ if (target === undefined || listedRoom === undefined) {
2014
+ throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
2015
+ }
2016
+ const arriving = await savingIn(target, [], moves.map(({ to }) => to));
2017
+ const built = await buildMove({
2018
+ source: await sideOf(locator, replica, room, sourceSaving),
2019
+ destination: await sideOf(destination, target, listedRoom, arriving),
2020
+ moves,
2021
+ providerLimits: limits,
2022
+ local: localSaves,
2023
+ ...(writtenBy === undefined ? {} : { writtenBy }),
2024
+ });
2025
+ if (built.kind === 'refused')
2026
+ return { outcome: 'refused', ...built.refusal };
2027
+ const id = built.leaving.id;
2028
+ const carrying = saved.flatMap(({ path, parts }) => {
2029
+ const record = roomDrafts.get(locator)?.get(path);
2030
+ const to = moveTarget(parts.move);
2031
+ if (record === undefined || consumed.has(path) || roomDrafts.get(destination)?.has(to)) {
2032
+ return [];
2033
+ }
2034
+ const moved = {
2035
+ ...record,
2036
+ locator: destination,
2037
+ path: to,
2038
+ revision: roomDraftRevision(),
2039
+ };
2040
+ return [{ from: path, record: moved }];
2041
+ });
2042
+ const carried = carrying.map(({ record }) => record);
2043
+ const marked = carried.map(({ path }) => ({ path, parts: {} }));
2044
+ const left = [...consumed, ...carrying.map(({ from }) => from)];
2045
+ const leavingQueue = [...replica.queue, built.leaving];
2046
+ const arrivingQueue = [...target.queue, built.arriving];
2047
+ const committedBoth = await saveCommit(local, files, [
2048
+ {
2049
+ locator,
2050
+ saving: sourceSaving,
2051
+ before: replica,
2052
+ queue: leavingQueue,
2053
+ write: {
2054
+ locator,
2055
+ queue: leavingQueue,
2056
+ saved: new Map([[id, saved]]),
2057
+ consumedRoomDrafts: left,
2058
+ },
2059
+ },
2060
+ {
2061
+ locator: destination,
2062
+ saving: arriving,
2063
+ before: target,
2064
+ queue: arrivingQueue,
2065
+ write: {
2066
+ locator: destination,
2067
+ queue: arrivingQueue,
2068
+ saved: new Map([[id, marked]]),
2069
+ roomDrafts: carried,
2070
+ },
2071
+ },
2072
+ ]);
2073
+ if (committedBoth === null)
2074
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
2075
+ const { answer, touched } = committedBoth;
2076
+ if (answer.fenced !== null) {
2077
+ throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2078
+ }
2079
+ if (fences.has(locator) || fences.has(destination)) {
2080
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2081
+ }
2082
+ pendings.set(pendingKey(locator, id), {
2083
+ drafts: saved,
2084
+ submission: { state: 'waiting' },
2085
+ lost: false,
2086
+ });
2087
+ pendings.set(pendingKey(destination, id), {
2088
+ drafts: marked,
2089
+ submission: { state: 'waiting' },
2090
+ lost: false,
2091
+ });
2092
+ for (const path of left)
2093
+ forgetRoomDraft(locator, path);
2094
+ for (const record of carried)
2095
+ rememberRoomDraft(record);
2096
+ savedReplica(locator, replica, leavingQueue, touched.get(locator), answer);
2097
+ savedReplica(destination, target, arrivingQueue, touched.get(destination), answer);
2098
+ publishChangeSet(mergeChangeSets([
2099
+ changeSet(locator, {
2100
+ documents: answer.changed[locator] ?? [],
2101
+ queue: true,
2102
+ drafts: left,
2103
+ }),
2104
+ changeSet(destination, {
2105
+ documents: answer.changed[destination] ?? [],
2106
+ queue: true,
2107
+ drafts: carried.map(({ path }) => path),
2108
+ }),
2109
+ ]));
2110
+ queuedSave(locator);
2111
+ return { outcome: 'saved', identifier: id };
2112
+ }
2113
+ function queuedSave(locator) {
2114
+ if (!localSaves)
2115
+ recorded = false;
2116
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
2117
+ submitLater(locator);
2118
+ }
2119
+ async function committed(locator, target, saved, consumed, writtenBy, files = []) {
2120
+ const into = movedInto(locator, saved);
2121
+ if (into !== null) {
2122
+ return committedMove(locator, into, target, saved, consumed, writtenBy, files);
2123
+ }
2124
+ const { local, replica, room } = target;
2125
+ const parts = saved.map(({ path, parts: held }) => changesOf(path, held));
2126
+ const changes = parts.flatMap(({ changes: made }) => made);
2127
+ const saving = await savingIn(replica, changes, [
2128
+ ...saved.map(({ path }) => path),
2129
+ ...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
2130
+ ]);
2131
+ const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, underneath(held, path, saving.visible)));
1461
2132
  if (changed.length > 0)
1462
2133
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
1463
- const parts = saved.map(({ path, parts }) => changesOf(path, parts));
1464
- const changes = parts.flatMap(({ changes }) => changes);
1465
- const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
2134
+ const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
1466
2135
  if (requeue.kind === 'refused')
1467
2136
  return { outcome: 'refused', ...requeue.refusal };
1468
2137
  const foreign = [...requeue.withdrawn.keys()].some((origin) => {
@@ -1480,25 +2149,22 @@ export function composeRuntime(options) {
1480
2149
  const withdrawing = requeue.withdrawn.size > 0;
1481
2150
  const input = {
1482
2151
  local: localSaves,
1483
- baseRoomVersion: replica.confirmed.record.roomVersion,
1484
- composition: compositionFor(replica.confirmed.record),
2152
+ baseRoomVersion: replica.record.roomVersion,
2153
+ composition: compositionFor(replica.record),
1485
2154
  operations: room.operations,
1486
2155
  providerOwnedPaths: room.providerOwnedPaths,
1487
2156
  providerLimits: limits,
1488
2157
  handle: room.handle,
1489
- visible: replica.visible,
2158
+ visible: saving.visible,
1490
2159
  changes,
1491
2160
  bases: parts.flatMap(({ bases }) => bases),
1492
2161
  queued: queuedDocuments(replica.queue),
1493
2162
  writtenBy,
1494
- clean: {
1495
- text: replica.confirmed.record.documents,
1496
- binaries: replica.confirmed.record.binaries,
1497
- composition: compositionOf(replica.confirmed.record),
1498
- },
2163
+ unmodeled: await unmodeledIn(replica.record),
2164
+ validate: validatorOf(replica.record, saving.confirmed),
1499
2165
  };
1500
2166
  const checked = withdrawing
1501
- ? buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
2167
+ ? await buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
1502
2168
  : null;
1503
2169
  if (checked?.kind === 'refused')
1504
2170
  return { outcome: 'refused', ...checked.refusal };
@@ -1515,12 +2181,12 @@ export function composeRuntime(options) {
1515
2181
  };
1516
2182
  }
1517
2183
  const built = !withdrawing
1518
- ? buildBatch(input)
2184
+ ? await buildBatch(input)
1519
2185
  : requeue.changes.length === 0
1520
2186
  ? null
1521
- : buildBatch({
2187
+ : await buildBatch({
1522
2188
  ...input,
1523
- visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
2189
+ visible: await stepped(saving.visibleOver(requeue.queue)),
1524
2190
  changes: requeue.changes,
1525
2191
  queued: queuedDocuments(requeue.queue),
1526
2192
  });
@@ -1539,7 +2205,7 @@ export function composeRuntime(options) {
1539
2205
  for (const [origin, replaced] of withdrawn) {
1540
2206
  if (replaced === null)
1541
2207
  continue;
1542
- const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
2208
+ const carried = withdrawnDrafts(pendings.get(pendingKey(locator, origin))?.drafts ?? [], changes);
1543
2209
  drafted.set(replaced.id, replaced === join?.batch
1544
2210
  ? [
1545
2211
  ...carried.map((held) => joinedDraft(held, saved)),
@@ -1553,64 +2219,77 @@ export function composeRuntime(options) {
1553
2219
  for (const replaced of withdrawn.values()) {
1554
2220
  if (replaced === null || !batches.includes(replaced))
1555
2221
  continue;
1556
- const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
2222
+ const limited = await stepped(limiting(batches, replaced, limits, saving.visibleOver));
1557
2223
  if (limited !== null)
1558
2224
  return { outcome: 'refused', ...limited };
1559
2225
  }
1560
2226
  const carried = carriedDrafts(locator, saved, consumed);
1561
2227
  const left = [...consumed, ...carried.keys()];
1562
2228
  const arrived = [...carried.values()];
1563
- try {
1564
- await local.keepAuthored({
2229
+ const done = await saveCommit(local, files, [
2230
+ {
1565
2231
  locator,
2232
+ saving,
2233
+ before: replica,
1566
2234
  queue: batches,
1567
- saved: drafted,
1568
- consumedRoomDrafts: left,
1569
- roomDrafts: arrived,
1570
- });
1571
- }
1572
- catch {
2235
+ write: {
2236
+ locator,
2237
+ queue: batches,
2238
+ saved: drafted,
2239
+ consumedRoomDrafts: left,
2240
+ roomDrafts: arrived,
2241
+ },
2242
+ },
2243
+ ]);
2244
+ if (done === null)
1573
2245
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2246
+ const { answer, touched } = done;
2247
+ if (answer.fenced !== null) {
2248
+ throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
1574
2249
  }
1575
2250
  if (fences.has(locator))
1576
2251
  throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1577
2252
  for (const origin of withdrawn.keys())
1578
- pendings.delete(origin);
2253
+ pendings.delete(pendingKey(locator, origin));
1579
2254
  for (const [id, drafts] of drafted) {
1580
- pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
2255
+ pendings.set(pendingKey(locator, id), {
2256
+ drafts,
2257
+ submission: { state: 'waiting' },
2258
+ lost: false,
2259
+ });
1581
2260
  }
1582
- if (!localSaves)
1583
- recorded = false;
1584
2261
  for (const path of left)
1585
2262
  forgetRoomDraft(locator, path);
1586
2263
  for (const record of arrived)
1587
2264
  rememberRoomDraft(record);
1588
- replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
1589
- publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
2265
+ savedReplica(locator, replica, batches, touched.get(locator), answer);
2266
+ publishChangeSet(changeSet(locator, {
2267
+ documents: answer.changed[locator] ?? [],
1590
2268
  queue: true,
1591
2269
  drafts: [...left, ...arrived.map(({ path }) => path)],
1592
2270
  }));
1593
- moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
1594
- submitLater(locator);
2271
+ queuedSave(locator);
1595
2272
  const identifier = join?.batch.id ??
1596
2273
  built?.batch.id ??
1597
2274
  [...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
1598
2275
  return { outcome: 'saved', identifier };
1599
2276
  }
1600
- async function mark(local, locator, identifier, submission, lost) {
1601
- const state = pendings.get(identifier);
1602
- if (!continuing() ||
1603
- state === undefined ||
1604
- (JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
1605
- return;
2277
+ async function mark(local, locator, batch, submission, lost) {
2278
+ const partner = partnerOf(batch, locator);
2279
+ for (const held of partner === null ? [locator] : [locator, partner]) {
2280
+ const state = pendings.get(pendingKey(held, batch.id));
2281
+ if (!continuing() ||
2282
+ state === undefined ||
2283
+ (JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
2284
+ continue;
2285
+ }
2286
+ await local.submit(held, batch.id, submission, lost);
2287
+ if (fences.has(held))
2288
+ continue;
2289
+ pendings.set(pendingKey(held, batch.id), { ...state, submission, lost });
2290
+ if (replicas.has(held))
2291
+ publishChangeSet(changeSet(held, { queue: true }));
1606
2292
  }
1607
- await local.submit(locator, identifier, submission, lost);
1608
- if (fences.has(locator))
1609
- return;
1610
- pendings.set(identifier, { ...state, submission, lost });
1611
- const replica = replicas.get(locator);
1612
- if (replica !== undefined)
1613
- publishChangeSet(changeSet(locator, replica, replica, { queue: true }));
1614
2293
  moveTo({ ...status, ...attention() });
1615
2294
  }
1616
2295
  async function keepUpload(local, locator, identifier, upload) {
@@ -1619,7 +2298,8 @@ export function composeRuntime(options) {
1619
2298
  await local.keepUpload(locator, identifier, upload);
1620
2299
  if (fences.has(locator))
1621
2300
  throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1622
- pendings.set(identifier, { ...pendings.get(identifier), upload });
2301
+ const key = pendingKey(locator, identifier);
2302
+ pendings.set(key, { ...pendings.get(key), upload });
1623
2303
  if (!continuing())
1624
2304
  throw new Error('connection refused');
1625
2305
  }
@@ -1652,7 +2332,7 @@ export function composeRuntime(options) {
1652
2332
  if (fencedSince(locator, since))
1653
2333
  return failure({ kind: 'room-not-readable', locator });
1654
2334
  if (result.kind === 'scoped-refusal') {
1655
- const room = replicas.get(result.locator)?.confirmed.record;
2335
+ const room = replicas.get(result.locator)?.record;
1656
2336
  if (room !== undefined)
1657
2337
  await scoped(result, room, await openLocal());
1658
2338
  }
@@ -1665,19 +2345,60 @@ export function composeRuntime(options) {
1665
2345
  return UNCERTAIN;
1666
2346
  }
1667
2347
  }
1668
- function queuedBlob(locator, path, localId) {
2348
+ function located(room, path, destination) {
2349
+ const crossing = roomTarget(destination);
2350
+ if (crossing === null) {
2351
+ const named = target(path, destination);
2352
+ return named === null || !rooms.has(room) ? null : { room, path: named };
2353
+ }
2354
+ const held = [...rooms.values()].find((listed) => listed.handle === crossing.handle);
2355
+ return held === undefined ? null : { room: held.locator, path: crossing.path };
2356
+ }
2357
+ async function resolveReference(room, path, destination) {
2358
+ if (roomTarget(destination) === null && target(path, destination) === null) {
2359
+ return ABSENT_TARGET;
2360
+ }
2361
+ const at = located(room, path, destination);
2362
+ if (at === null)
2363
+ return UNREADABLE;
2364
+ if (locked(at.room))
2365
+ return LOCKED_UNREADABLE;
2366
+ const held = replicas.get(at.room);
2367
+ if (held !== undefined &&
2368
+ (await (await openLocal()).entries(held.record.key, 'visible', [at.path])).length > 0) {
2369
+ return { kind: 'document', ...at };
2370
+ }
2371
+ const history = await readHistory(at.room, at.path);
2372
+ if (history.kind === 'no-history' || history.kind === 'revision')
2373
+ return ABSENT_TARGET;
2374
+ if (history.kind !== 'history')
2375
+ return history;
2376
+ const [last] = history.revisions;
2377
+ if (last === undefined)
2378
+ return ABSENT_TARGET;
2379
+ return history.deleted === true
2380
+ ? { kind: 'deleted', ...at, revision: last }
2381
+ : { kind: 'document', ...at };
2382
+ }
2383
+ function queuedFacts(locator, path, localId) {
1669
2384
  for (const batch of replicas.get(locator)?.queue ?? []) {
1670
2385
  const change = batch.changes[queuedFile(batch, path, localId)];
1671
2386
  if (change?.kind === 'binary')
1672
- return change.blob;
2387
+ return change;
1673
2388
  }
1674
- return null;
2389
+ const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2390
+ return drafted?.localId === localId ? drafted : null;
2391
+ }
2392
+ function batchFile(batch) {
2393
+ return async (localId) => {
2394
+ const change = batch.changes.find((held) => held.kind === 'binary' && held.localId === localId);
2395
+ return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
2396
+ };
1675
2397
  }
1676
2398
  function binaryRoom(request) {
1677
2399
  if (request.kind === 'path')
1678
2400
  return request.locator;
1679
- const scope = options.adapter.referrerScope(request.referrer);
1680
- return scope?.kind === 'room' ? scope.locator : null;
2401
+ return options.adapter.referrerScope(request.referrer)?.locator ?? null;
1681
2402
  }
1682
2403
  async function transferBinary(request, transfer, answer) {
1683
2404
  if (closed || refusal !== null)
@@ -1702,7 +2423,7 @@ export function composeRuntime(options) {
1702
2423
  if (locator !== null && fencedSince(locator, since))
1703
2424
  return failure({ kind: 'room-not-readable', locator });
1704
2425
  if (settled.kind === 'scoped-refusal') {
1705
- const room = replicas.get(settled.locator)?.confirmed.record;
2426
+ const room = replicas.get(settled.locator)?.record;
1706
2427
  if (room !== undefined)
1707
2428
  await scoped(settled, room, await openLocal());
1708
2429
  }
@@ -1715,68 +2436,96 @@ export function composeRuntime(options) {
1715
2436
  return UNCERTAIN;
1716
2437
  }
1717
2438
  }
1718
- function readBinary(request) {
1719
- return transferBinary(request, (held) => options.adapter.binary(held, request), binaryAnswer);
2439
+ async function readBinary(request, name) {
2440
+ if (closed)
2441
+ return UNCERTAIN;
2442
+ const held = await keptFiles.find(name);
2443
+ if (held !== null)
2444
+ return held;
2445
+ return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
1720
2446
  }
1721
- function binaryPath(locator, path, revision) {
2447
+ async function binaryPath(locator, path, revision) {
1722
2448
  if (revision !== undefined)
1723
2449
  return { kind: 'path', locator, path, revision };
1724
- const visible = replicas.get(locator)?.visible;
1725
- const held = visible?.binaries[path];
1726
- if (visible === undefined || held === undefined)
2450
+ const replica = replicas.get(locator);
2451
+ if (replica === undefined)
2452
+ return null;
2453
+ const local = await openLocal();
2454
+ const [held] = await local.entries(replica.record.key, 'visible', [path]);
2455
+ if (held?.binary === undefined || held.binary === null)
1727
2456
  return null;
1728
- if (held.state === 'pending')
1729
- return { kind: 'pending', localId: held.localId };
1730
- const record = binaryRecord(path, held, visible.revisions[path]);
2457
+ if (held.binary.state === 'pending')
2458
+ return { kind: 'pending', localId: held.binary.localId };
2459
+ const record = binaryRecord(path, held.binary, held.revision ?? undefined);
1731
2460
  if (record.state !== 'accepted')
1732
2461
  return null;
1733
- const replica = replicas.get(locator);
1734
- const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
2462
+ const queued = replica.queue.flatMap((batch) => batch.changes);
2463
+ const sources = queued.flatMap((change) => (change.kind === 'move' ? [change.path] : []));
2464
+ const confirmedLayer = await stepped(slicing(await local.entries(replica.record.key, 'confirmed', [path, ...sources]), [
2465
+ path,
2466
+ ...sources,
2467
+ ]));
1735
2468
  const holds = (at) => {
1736
- const kept = replica?.confirmed.record.binaries[at];
2469
+ const kept = confirmedLayer.binaries[at];
1737
2470
  return (kept?.state === 'accepted' &&
1738
2471
  kept.fileRef === record.fileRef &&
1739
- replica?.confirmed.record.revisions[at] === record.revision);
2472
+ confirmedLayer.revisions[at] === record.revision);
1740
2473
  };
1741
- let confirmed = path;
1742
- for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
2474
+ let confirmedAt = path;
2475
+ for (let index = queued.length - 1; index >= 0 && !holds(confirmedAt); index -= 1) {
1743
2476
  const change = queued[index];
1744
- if (change.kind === 'move' && change.to === confirmed)
1745
- confirmed = change.path;
2477
+ if (change.kind === 'move' && change.to === confirmedAt)
2478
+ confirmedAt = change.path;
1746
2479
  }
1747
2480
  return {
1748
2481
  kind: 'path',
1749
2482
  locator,
1750
- path: confirmed,
2483
+ path: confirmedAt,
1751
2484
  revision: record.revision,
1752
2485
  fileRef: record.fileRef,
1753
2486
  };
1754
2487
  }
1755
2488
  async function readBinaryPath(locator, path, revision) {
1756
2489
  await ready;
1757
- const asked = binaryPath(locator, path, revision);
2490
+ const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2491
+ const asked = drafted !== undefined && drafted.localId === revision
2492
+ ? { kind: 'pending', localId: revision }
2493
+ : await binaryPath(locator, path, revision);
1758
2494
  if (asked === null)
1759
2495
  return absentBinary(path);
1760
2496
  if (asked.kind === 'pending') {
1761
- const blob = queuedBlob(locator, path, asked.localId);
1762
- return blob === null ? absentBinary(path) : { kind: 'binary', blob };
2497
+ const facts = queuedFacts(locator, path, asked.localId);
2498
+ return facts === null
2499
+ ? absentBinary(path)
2500
+ : {
2501
+ kind: 'binary',
2502
+ file: asked.localId,
2503
+ byteLength: facts.byteLength,
2504
+ mediaType: facts.mediaType,
2505
+ };
1763
2506
  }
1764
- return readBinary(asked);
2507
+ return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
1765
2508
  }
1766
2509
  async function linkBinaryPath(locator, path, revision) {
1767
2510
  await ready;
1768
- const asked = binaryPath(locator, path, revision);
2511
+ const asked = await binaryPath(locator, path, revision);
1769
2512
  if (asked === null || asked.kind === 'pending')
1770
2513
  return absentBinary(path);
1771
2514
  return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
1772
2515
  }
1773
2516
  async function readBinaryReference(reference) {
1774
2517
  await ready;
1775
- return readBinary({
1776
- kind: 'reference',
1777
- fileRef: reference.fileRef,
1778
- referrer: reference.referrer,
1779
- });
2518
+ const scope = options.adapter.referrerScope(reference.referrer);
2519
+ if (scope === null)
2520
+ return absentReference(reference.fileRef);
2521
+ return readBinary({ kind: 'reference', fileRef: reference.fileRef, referrer: reference.referrer }, { locator: scope.locator, path: scope.path ?? '', revision: reference.fileRef });
2522
+ }
2523
+ function readPart(file, part) {
2524
+ return options.database.ask({ kind: 'file.part', key: file, part });
2525
+ }
2526
+ async function cleanUp() {
2527
+ const freed = await options.database.ask({ kind: 'opened.clean' });
2528
+ return freed + ((await options.shell.cleanUp?.()) ?? 0);
1780
2529
  }
1781
2530
  function declaredAgents() {
1782
2531
  for (const capability of capabilities) {
@@ -1794,6 +2543,13 @@ export function composeRuntime(options) {
1794
2543
  }
1795
2544
  return undefined;
1796
2545
  }
2546
+ function calledRooms(name, input) {
2547
+ const member = declaredAction(name)?.room;
2548
+ if (member === undefined || input === null || typeof input !== 'object')
2549
+ return [];
2550
+ const named = input[member];
2551
+ return (Array.isArray(named) ? named : [named]).filter((room) => typeof room === 'string');
2552
+ }
1797
2553
  async function invokeAction(name, input, dispatching) {
1798
2554
  await ready;
1799
2555
  if (closed || refusal !== null)
@@ -1813,7 +2569,7 @@ export function composeRuntime(options) {
1813
2569
  try {
1814
2570
  const result = await options.adapter.action(authorization, { name, input });
1815
2571
  if (result.kind === 'scoped-refusal') {
1816
- const room = replicas.get(result.locator)?.confirmed.record;
2572
+ const room = replicas.get(result.locator)?.record;
1817
2573
  if (room !== undefined)
1818
2574
  await scoped(result, room, await openLocal());
1819
2575
  }
@@ -1888,85 +2644,277 @@ export function composeRuntime(options) {
1888
2644
  }
1889
2645
  function reachedBy(locator, roomVersion) {
1890
2646
  const held = replicas.get(locator);
1891
- return (held !== undefined &&
1892
- options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion));
2647
+ return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
1893
2648
  }
1894
- function writtenAfter(locator, roomVersion, path) {
1895
- const written = replicas.get(locator)?.confirmed.record.written[path];
2649
+ async function writtenAfter(locator, roomVersion, path) {
2650
+ const held = replicas.get(locator);
2651
+ if (held === undefined)
2652
+ return false;
2653
+ const [entry] = await (await openLocal()).entries(held.record.key, 'confirmed', [path]);
2654
+ const written = entry?.written ?? undefined;
1896
2655
  return (written !== undefined &&
1897
2656
  options.adapter.reached(locator, written, roomVersion) &&
1898
2657
  !options.adapter.reached(locator, roomVersion, written));
1899
2658
  }
1900
- async function pullRefresh(authorized, confirmed, live) {
1901
- if (!live())
2659
+ function overriddenTexts(texts, overrides) {
2660
+ return async (paths) => {
2661
+ const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
2662
+ for (const path of paths) {
2663
+ const entry = overrides.get(path);
2664
+ if (entry === undefined)
2665
+ continue;
2666
+ if (entry === null)
2667
+ known.delete(path);
2668
+ else
2669
+ known.set(path, entry.binary !== null ? null : entry.content);
2670
+ }
2671
+ return known;
2672
+ };
2673
+ }
2674
+ async function unreadableRoom(texts, walk, paths) {
2675
+ if (paths !== null) {
2676
+ const text = {};
2677
+ for (const [path, content] of await texts(paths))
2678
+ if (content !== null)
2679
+ text[path] = content;
2680
+ return stepped(unreadableIn(text, Object.keys(text)));
2681
+ }
2682
+ for (let after = null;;) {
2683
+ const page = await walk(after);
2684
+ const text = {};
2685
+ for (const { path, content } of page.entries)
2686
+ if (content !== null)
2687
+ text[path] = content;
2688
+ const refused = await stepped(unreadableIn(text, Object.keys(text)));
2689
+ if (refused !== null)
2690
+ return refused;
2691
+ if (page.next === null)
2692
+ return null;
2693
+ after = page.next;
2694
+ }
2695
+ }
2696
+ async function validatedRoom(local, record, view, previous, overrides = null) {
2697
+ const composition = compositionOf(record);
2698
+ const modeled = record.model.status !== 'unavailable' || record.selectionModel.status !== 'unavailable';
2699
+ const lookup = overrides === null
2700
+ ? lookupOf(local, record.key, view)
2701
+ : overriddenLookup(lookupOf(local, record.key, view), overrides);
2702
+ const texts = overrides === null
2703
+ ? textsOf(local, record.key, view)
2704
+ : overriddenTexts(textsOf(local, record.key, view), overrides);
2705
+ const walk = overrides === null
2706
+ ? walkOf(local, record.key, view)
2707
+ : overriddenWalk(walkOf(local, record.key, view), overrides);
2708
+ let changes = null;
2709
+ let reshaped = false;
2710
+ if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
2711
+ const [was, now] = await Promise.all([
2712
+ local.entries(record.key, 'confirmed', previous.changed),
2713
+ overrides === null
2714
+ ? local.entries(record.key, view, previous.changed)
2715
+ : Promise.resolve([]),
2716
+ ]);
2717
+ const before = new Map(was.map((entry) => [entry.path, entry]));
2718
+ const after = overrides === null
2719
+ ? new Map(now.map((entry) => [entry.path, entry]))
2720
+ : overrides;
2721
+ changes = previous.changed.flatMap((path) => {
2722
+ const one = before.get(path) ?? null;
2723
+ const other = after.get(path) ?? null;
2724
+ reshaped ||= textDiffers(one, other);
2725
+ const held = textOf(one);
2726
+ const next = textOf(other);
2727
+ return held === next ? [] : [{ path, was: held, now: next }];
2728
+ });
2729
+ }
2730
+ const named = changes === null ? null : changes.map(({ path }) => path);
2731
+ const textChanged = changes === null || changes.length > 0 || reshaped;
2732
+ const modelsChanged = previous === null || !sameComposition(compositionOf(previous.record), composition);
2733
+ if (modeled && (textChanged || modelsChanged)) {
2734
+ const clean = previous !== null &&
2735
+ changes !== null &&
2736
+ (await changedCleanly(changes, compositionOf(previous.record), composition, {
2737
+ lookup,
2738
+ texts,
2739
+ referrers: referrersOf(local, record.key, 'confirmed'),
2740
+ }));
2741
+ if (clean)
2742
+ return null;
2743
+ const verdict = await validatedCollection(walk, lookup, composition);
2744
+ if (verdict.verdict === 'accept')
2745
+ return null;
2746
+ if (verdict.rule !== 'model-unavailable')
2747
+ return { kind: 'refused', refusal: verdict };
2748
+ return unreadableRoom(texts, walk, named);
2749
+ }
2750
+ if (textChanged)
2751
+ return unreadableRoom(texts, walk, named);
2752
+ return null;
2753
+ }
2754
+ function refreshingOf(local, key, liveness) {
2755
+ return {
2756
+ prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
2757
+ async stage(page) {
2758
+ const batches = batched(page.entries);
2759
+ for (const [index, entries] of batches.entries()) {
2760
+ const last = index === batches.length - 1;
2761
+ const staged = await exclusive(async () => {
2762
+ if (!liveness())
2763
+ return false;
2764
+ await local.refresh(key, last
2765
+ ? { ...page, entries }
2766
+ : { entries, removals: [], folders: [], folderRemovals: [] });
2767
+ return true;
2768
+ });
2769
+ if (!staged)
2770
+ return false;
2771
+ }
2772
+ return true;
2773
+ },
2774
+ };
2775
+ }
2776
+ function batched(entries) {
2777
+ const batches = [[]];
2778
+ let spent = 0;
2779
+ for (const entry of entries) {
2780
+ const size = (entry.content?.length ?? 0) + ROW_WEIGHT;
2781
+ if (spent + size > STAGE_BYTES && batches.at(-1).length > 0) {
2782
+ batches.push([]);
2783
+ spent = 0;
2784
+ }
2785
+ batches.at(-1).push(entry);
2786
+ spent += size;
2787
+ }
2788
+ return batches;
2789
+ }
2790
+ async function pullRefresh(authorized, held, liveness) {
2791
+ if (!liveness())
1902
2792
  return unconfirmed;
1903
2793
  const turn = generation;
2794
+ const { record } = held;
2795
+ const base = held.confirmed;
2796
+ const local = await openLocal();
1904
2797
  try {
1905
- const result = await refreshRoom(reads, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1906
- const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
1907
- journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
1908
- return result;
2798
+ await exclusive(() => local.unrefresh(record.key));
2799
+ const result = await advancement(reads, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, liveness));
2800
+ if (result.kind !== 'advanced') {
2801
+ journal.record(`transfer refresh ${record.locator} ${result.kind}`);
2802
+ return result;
2803
+ }
2804
+ const refused = await validatedRoom(local, result.record, 'refreshed', {
2805
+ record,
2806
+ changed: [...result.changed],
2807
+ });
2808
+ if (refused !== null) {
2809
+ journal.record(`transfer refresh ${record.locator} refused`);
2810
+ return refused;
2811
+ }
2812
+ journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
2813
+ return { kind: 'advanced', record: result.record, base };
1909
2814
  }
1910
2815
  catch (error) {
1911
- journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
2816
+ journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
1912
2817
  throw error;
1913
2818
  }
1914
2819
  finally {
1915
- reads.finished(confirmed.record.locator, turn);
1916
- }
2820
+ reads.finished(record.locator, turn);
2821
+ }
2822
+ }
2823
+ const heldContents = new Map();
2824
+ function heldContentOf(local, key) {
2825
+ const known = heldContents.get(key);
2826
+ if (known !== undefined)
2827
+ return known;
2828
+ if (heldContents.size > ROOM_READS)
2829
+ heldContents.clear();
2830
+ const made = {
2831
+ files: (after, limit) => local.files(key, 'confirmed', after, limit),
2832
+ async contents(paths) {
2833
+ return new Map((await local.entries(key, 'confirmed', paths)).flatMap(({ path, content, revision }) => content === null || revision === null ? [] : [[path, { content, revision }]]));
2834
+ },
2835
+ };
2836
+ heldContents.set(key, made);
2837
+ return made;
1917
2838
  }
1918
- function staging(local, live) {
2839
+ function staging(local, liveness) {
1919
2840
  return {
1920
2841
  held: (locator) => transfers.get(locator) ?? null,
1921
- staged: (record) => local.staged(record),
1922
- stage: (record, page) => exclusive(async () => {
1923
- if (!live())
1924
- return false;
1925
- const bytes = layerBytes(page);
1926
- if (!(await admitted(local, record.locator, bytes)))
1927
- return false;
1928
- try {
1929
- await local.stage(record, page);
1930
- }
1931
- catch (error) {
1932
- if (!exceeded(error))
1933
- throw error;
1934
- await exceededOn(record.locator, bytes);
1935
- return false;
2842
+ async stage(record, page, composition) {
2843
+ const bytes = pageBytes(page);
2844
+ const batches = batched(page.entries);
2845
+ let key = record.key;
2846
+ for (const [index, entries] of batches.entries()) {
2847
+ const last = index === batches.length - 1;
2848
+ const staged = await exclusive(async () => {
2849
+ if (!liveness())
2850
+ return null;
2851
+ if (index === 0 && !(await admitted(local, record.locator, bytes)))
2852
+ return null;
2853
+ try {
2854
+ return await local.stage(record, composition, { entries, folders: last ? page.folders : [] }, key, last);
2855
+ }
2856
+ catch (error) {
2857
+ if (!exceeded(error))
2858
+ throw error;
2859
+ await exceededOn(record.locator, bytes);
2860
+ return null;
2861
+ }
2862
+ });
2863
+ if (staged === null)
2864
+ return null;
2865
+ key = staged;
1936
2866
  }
1937
- transfers.set(record.locator, record);
1938
- readSide.recover();
1939
- return true;
1940
- }),
2867
+ transfers.set(record.locator, { ...record, key });
2868
+ liveQueries.recover();
2869
+ return key;
2870
+ },
2871
+ async refreshing(key) {
2872
+ await exclusive(() => local.unrefresh(key));
2873
+ return refreshingOf(local, key, liveness);
2874
+ },
1941
2875
  abandon: (locator) => exclusive(async () => {
1942
- if (!live())
2876
+ if (!liveness())
1943
2877
  return;
1944
2878
  await local.abandon(locator);
1945
2879
  transfers.delete(locator);
1946
- readSide.recover();
2880
+ liveQueries.recover();
2881
+ sweep();
2882
+ }),
2883
+ setAside: (locator) => exclusive(async () => {
2884
+ if (!liveness())
2885
+ return;
2886
+ await local.abandon(locator, true);
2887
+ transfers.delete(locator);
2888
+ liveQueries.recover();
1947
2889
  }),
2890
+ release: (key) => exclusive(async () => {
2891
+ await local.release(key);
2892
+ sweep();
2893
+ }),
2894
+ content: (key) => heldContentOf(local, key),
2895
+ validate: (record, view) => validatedRoom(local, record, view, null),
1948
2896
  };
1949
2897
  }
1950
- function reusable(room) {
1951
- const record = replicas.get(room.locator)?.confirmed.record;
2898
+ function reusable(room, local) {
2899
+ const record = replicas.get(room.locator)?.record;
1952
2900
  if (record === undefined || record.replica.selection !== room.replica.selection)
1953
2901
  return null;
1954
- return { documents: record.documents, revisions: record.revisions };
2902
+ return heldContentOf(local, record.key);
1955
2903
  }
1956
- async function pullBootstrap(authorized, room, local, live) {
1957
- if (!live())
2904
+ async function pullBootstrap(authorized, room, local, liveness) {
2905
+ if (!liveness())
1958
2906
  return unconfirmed;
1959
2907
  const turn = generation;
1960
2908
  if (replicas.has(room.locator)) {
1961
2909
  catching.add(room.locator);
1962
- readSide.recover();
2910
+ liveQueries.recover();
1963
2911
  }
1964
2912
  try {
1965
2913
  const result = await bootstrapRoom(reads, authorized, room, {
1966
2914
  collection: modelOf(room.locator),
1967
2915
  selection: modelOf(room.locator, 'typed-selection'),
1968
- }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
1969
- const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
2916
+ }, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
2917
+ const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
1970
2918
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1971
2919
  if (result.kind !== 'staged')
1972
2920
  caughtUpTo(room.locator);
@@ -1983,10 +2931,10 @@ export function composeRuntime(options) {
1983
2931
  }
1984
2932
  function caughtUpTo(locator) {
1985
2933
  if (catching.delete(locator))
1986
- readSide.recover();
2934
+ liveQueries.recover();
1987
2935
  }
1988
2936
  function behind(room) {
1989
- const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
2937
+ const held = replicas.get(room.locator)?.record.roomVersion;
1990
2938
  return (held !== undefined &&
1991
2939
  [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
1992
2940
  }
@@ -1999,12 +2947,11 @@ export function composeRuntime(options) {
1999
2947
  return advanceOnce(room, local, authorized, live);
2000
2948
  }
2001
2949
  async function advanceOnce(room, local, authorized, live) {
2002
- await referenced(room.locator);
2003
2950
  if (!live() || paused.has(room.locator))
2004
2951
  return unconfirmed;
2005
2952
  const held = replicas.get(room.locator);
2006
2953
  if (held !== undefined) {
2007
- const refreshed = await pullRefresh(authorized, held.confirmed, live);
2954
+ const refreshed = await pullRefresh(authorized, held, live);
2008
2955
  if (!live())
2009
2956
  return unconfirmed;
2010
2957
  if (refreshed.kind === 'still-current') {
@@ -2019,10 +2966,11 @@ export function composeRuntime(options) {
2019
2966
  return null;
2020
2967
  }
2021
2968
  if (refreshed.kind === 'advanced') {
2022
- if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
2023
- const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
2969
+ const landing = { kind: 'refreshed', base: refreshed.base };
2970
+ if (!(await publish(local, room, refreshed.record, landing, live))) {
2971
+ const now = replicas.get(room.locator)?.record.roomVersion;
2024
2972
  const reached = now !== undefined &&
2025
- options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
2973
+ options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
2026
2974
  return reached ? null : advance(room, local, authorized, live);
2027
2975
  }
2028
2976
  await retainModel(local, room, 'collection', modelOf(room.locator));
@@ -2032,16 +2980,18 @@ export function composeRuntime(options) {
2032
2980
  return refreshed;
2033
2981
  if (opened !== null) {
2034
2982
  owed.add(room.locator);
2035
- readSide.recover();
2983
+ liveQueries.recover();
2036
2984
  return unconfirmed;
2037
2985
  }
2986
+ if (reloadsDue.get(room.locator) === generation)
2987
+ return unconfirmed;
2038
2988
  }
2039
2989
  const bootstrapped = await pullBootstrap(authorized, room, local, live);
2040
2990
  if (!live())
2041
2991
  return unconfirmed;
2042
2992
  if (bootstrapped.kind !== 'staged')
2043
2993
  return bootstrapped;
2044
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
2994
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
2045
2995
  return null;
2046
2996
  }
2047
2997
  async function withhold(withheld, room, local, progress) {
@@ -2114,57 +3064,254 @@ export function composeRuntime(options) {
2114
3064
  (accepted.author === undefined ? '' : ` author ${accepted.author}`) +
2115
3065
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
2116
3066
  }
2117
- async function adoptAccepted(room, local, submitted, accepted) {
2118
- await referenced(room.locator);
3067
+ function settling(acceptance) {
3068
+ return acceptance !== null && 'apply' in acceptance;
3069
+ }
3070
+ function refusalOf(acceptance) {
3071
+ return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
3072
+ }
3073
+ async function acceptance(room, local, submitted, accepted) {
2119
3074
  if (!continuing())
2120
3075
  return unconfirmed;
2121
3076
  const current = replicas.get(room.locator);
2122
3077
  if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
2123
3078
  return null;
2124
3079
  }
2125
- const held = current.confirmed.record.roomVersion;
3080
+ const held = current.record.roomVersion;
2126
3081
  logAccepted(room.locator, submitted, accepted);
2127
- const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
2128
- ? { kind: 'staged', confirmed: current.confirmed }
2129
- : applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
2130
- ? accepted.roomVersion
2131
- : held, accepted.roomVersion);
2132
- if (applied.kind !== 'staged') {
2133
- const state = pendings.get(submitted.id);
2134
- await local.accept(room.locator, submitted.id, accepted.roomVersion);
2135
- if (state !== undefined && !fences.has(room.locator))
2136
- pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
2137
- emitQueue(room.locator);
2138
- return applied;
2139
- }
2140
- const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
2141
- pendings.delete(submitted.id);
2142
- unreadable.delete(room.locator);
2143
- sourceRefusals.delete(room.locator);
2144
- declarationRefusals.delete(room.locator);
2145
- const set = settled();
2146
- list(room, true);
2147
- publishChangeSet(set);
2148
- moveTo({ ...status, ...attention() });
2149
- return null;
3082
+ let record = current.record;
3083
+ let changes;
3084
+ if (!options.adapter.reached(room.locator, held, accepted.roomVersion)) {
3085
+ const paths = [...returnedPaths(submitted, accepted)];
3086
+ const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
3087
+ const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
3088
+ record = {
3089
+ ...record,
3090
+ roomVersion: options.adapter.follows(room.locator, held, accepted.roomVersion)
3091
+ ? accepted.roomVersion
3092
+ : held,
3093
+ };
3094
+ const overrides = new Map([
3095
+ ...back.set.map((entry) => [entry.path, entry]),
3096
+ ...back.removed.map((path) => [path, null]),
3097
+ ]);
3098
+ const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
3099
+ if (refused !== null) {
3100
+ const key = pendingKey(room.locator, submitted.id);
3101
+ return {
3102
+ locator: room.locator,
3103
+ left: [],
3104
+ commit: {
3105
+ rooms: [],
3106
+ writes: [
3107
+ {
3108
+ locator: room.locator,
3109
+ accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
3110
+ },
3111
+ ],
3112
+ },
3113
+ refused,
3114
+ apply() {
3115
+ const state = pendings.get(key);
3116
+ if (state !== undefined && !fences.has(room.locator))
3117
+ pendings.set(key, { ...state, accepted: accepted.roomVersion });
3118
+ return changeSet(room.locator, { queue: true });
3119
+ },
3120
+ };
3121
+ }
3122
+ changes = back;
3123
+ }
3124
+ const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
3125
+ return {
3126
+ ...settled,
3127
+ apply(answer) {
3128
+ const set = settled.apply(answer);
3129
+ pendings.delete(pendingKey(room.locator, submitted.id));
3130
+ unreadable.delete(room.locator);
3131
+ sourceRefusals.delete(room.locator);
3132
+ declarationRefusals.delete(room.locator);
3133
+ list(room, true);
3134
+ return set;
3135
+ },
3136
+ };
3137
+ }
3138
+ async function adoptedTogether(local, acceptances) {
3139
+ const settlements = acceptances.filter(settling);
3140
+ if (settlements.length > 0) {
3141
+ publishChangeSet(await together(local, settlements));
3142
+ moveTo({ ...status, ...attention() });
3143
+ }
3144
+ await unpair(local);
2150
3145
  }
2151
3146
  async function adopted(room, local, submitted, accepted) {
2152
- const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
3147
+ const failed = await exclusive(async () => {
3148
+ const adopting = await acceptance(room, local, submitted, accepted);
3149
+ await adoptedTogether(local, [adopting]);
3150
+ return refusalOf(adopting);
3151
+ });
3152
+ if (failed === null)
3153
+ return true;
3154
+ if (failed.kind === 'refused' && continuing())
3155
+ refusedSource(room, failed);
3156
+ return false;
3157
+ }
3158
+ async function adoptedMove(room, partner, local, submitted, accepted) {
3159
+ const other = discovered.get(partner);
3160
+ const failed = await exclusive(async () => {
3161
+ const left = await acceptance(room, local, submitted, accepted);
3162
+ const arriving = replicas.get(partner)?.queue.find((batch) => batch.id === submitted.id);
3163
+ const arrived = other === undefined || arriving === undefined || accepted.destination === undefined
3164
+ ? null
3165
+ : await acceptance(other, local, arriving, {
3166
+ kind: 'accepted',
3167
+ ...accepted.destination,
3168
+ });
3169
+ await adoptedTogether(local, [left, arrived]);
3170
+ const refusedArrival = refusalOf(arrived);
3171
+ if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
3172
+ refusedSource(other, refusedArrival);
3173
+ }
3174
+ return refusalOf(left);
3175
+ });
3176
+ submitLater(partner);
2153
3177
  if (failed === null)
2154
3178
  return true;
2155
3179
  if (failed.kind === 'refused' && continuing())
2156
3180
  refusedSource(room, failed);
2157
3181
  return false;
2158
3182
  }
3183
+ function queueIn(locator) {
3184
+ return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
3185
+ }
3186
+ function behindIn(locator, batch) {
3187
+ const held = replicas.get(locator)?.queue;
3188
+ if (held === undefined)
3189
+ return queuesWithoutReplica.get(locator) !== undefined;
3190
+ return held.findIndex((one) => one.id === batch.id) > 0;
3191
+ }
3192
+ function unpaired(skipped) {
3193
+ for (const locator of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
3194
+ for (const batch of queueIn(locator)) {
3195
+ const partner = partnerOf(batch, locator);
3196
+ if (partner === null || skipped.has(pendingKey(locator, batch.id)))
3197
+ continue;
3198
+ if (queueIn(partner).some((held) => held.id === batch.id))
3199
+ continue;
3200
+ if (unpairedIn(locator, batch))
3201
+ return { locator, batch };
3202
+ }
3203
+ }
3204
+ return null;
3205
+ }
3206
+ async function unpairing(local, locator, batches) {
3207
+ const ids = new Set(batches.map(({ id }) => id));
3208
+ const leaving = batches.map((batch) => ({
3209
+ batch,
3210
+ returned: !localSaves && leavingIn(batch, locator) ? returnedBy('withdrawn') : null,
3211
+ }));
3212
+ const outcome = localSaves ? 'dropped' : 'unpaired';
3213
+ const current = replicas.get(locator);
3214
+ if (current !== undefined) {
3215
+ const settled = await settlement(local, locator, current, current.record, current.queue.filter((one) => !ids.has(one.id)), { leaving });
3216
+ return {
3217
+ ...settled,
3218
+ apply(answer) {
3219
+ for (const batch of batches)
3220
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
3221
+ const set = settled.apply(answer);
3222
+ submitLater(locator);
3223
+ return set;
3224
+ },
3225
+ };
3226
+ }
3227
+ const queue = queueIn(locator).filter((one) => !ids.has(one.id));
3228
+ const written = new Map();
3229
+ for (const { batch, returned } of leaving) {
3230
+ if (returned === null)
3231
+ continue;
3232
+ for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
3233
+ written.set(saved.path, {
3234
+ locator,
3235
+ path: saved.path,
3236
+ revision: roomDraftRevision(),
3237
+ parts: returnedInto((written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path))?.parts, saved.parts),
3238
+ returned,
3239
+ });
3240
+ }
3241
+ }
3242
+ const carried = carriedBack(locator, leaving.flatMap(({ batch, returned }) => (returned === null ? [] : [batch])), written);
3243
+ return {
3244
+ locator,
3245
+ left: batches,
3246
+ commit: {
3247
+ rooms: [],
3248
+ writes: [
3249
+ {
3250
+ locator,
3251
+ queue,
3252
+ roomDrafts: [...written.values()],
3253
+ consumedRoomDrafts: carried.taken,
3254
+ },
3255
+ ...carried.writes,
3256
+ ],
3257
+ },
3258
+ apply() {
3259
+ for (const batch of batches) {
3260
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
3261
+ pendings.delete(pendingKey(locator, batch.id));
3262
+ }
3263
+ if (queue.length > 0)
3264
+ queuesWithoutReplica.set(locator, queue);
3265
+ else
3266
+ queuesWithoutReplica.delete(locator);
3267
+ for (const record of written.values())
3268
+ rememberRoomDraft(record);
3269
+ carried.remember();
3270
+ submitLater(locator);
3271
+ return mergeChangeSets([
3272
+ changeSet(locator, {
3273
+ queue: true,
3274
+ drafts: [...written.keys(), ...carried.taken],
3275
+ }),
3276
+ ...carried.changes,
3277
+ ]);
3278
+ },
3279
+ };
3280
+ }
3281
+ async function unpair(local) {
3282
+ const handled = new Set();
3283
+ for (let held = unpaired(handled); held !== null && continuing(); held = unpaired(handled)) {
3284
+ const { locator, batch } = held;
3285
+ handled.add(pendingKey(locator, batch.id));
3286
+ publishChangeSet(await together(local, [await unpairing(local, locator, [batch])]));
3287
+ }
3288
+ if (handled.size === 0)
3289
+ return;
3290
+ moveTo({ ...status, ...attention() });
3291
+ }
3292
+ async function discardHeld(head, room, local, signal) {
3293
+ const partner = partnerOf(head, room.locator);
3294
+ const other = partner === null ? undefined : discovered.get(partner);
3295
+ await discardPending(other === undefined ? [room] : [room, other], local, signal);
3296
+ }
2159
3297
  async function submitRoom(room, local, authorized, progress, signal) {
2160
3298
  while (continuing() && authorization !== undefined && !signal.aborted) {
2161
3299
  const replica = replicas.get(room.locator);
2162
3300
  const head = replica?.queue[0];
2163
3301
  if (replica === undefined || head === undefined)
2164
3302
  return 'done';
2165
- if (pendings.get(head.id)?.accepted !== undefined)
3303
+ const state = pendings.get(pendingKey(room.locator, head.id));
3304
+ if (state?.accepted !== undefined)
3305
+ return 'done';
3306
+ const partner = partnerOf(head, room.locator);
3307
+ if (partner !== null && !leavingIn(head, room.locator)) {
3308
+ submitLater(partner);
3309
+ return 'done';
3310
+ }
3311
+ if (partner !== null && behindIn(partner, head)) {
3312
+ progress.complete = false;
2166
3313
  return 'done';
2167
- const state = pendings.get(head.id);
3314
+ }
2168
3315
  const { submission, lost: earlier } = state ?? {
2169
3316
  submission: { state: 'waiting' },
2170
3317
  lost: false,
@@ -2180,16 +3327,16 @@ export function composeRuntime(options) {
2180
3327
  }
2181
3328
  const lost = earlier || submission.state === 'submitted';
2182
3329
  if (lost && pastHorizon(head.id)) {
2183
- await discardPending(room, local, signal);
3330
+ await discardHeld(head, room, local, signal);
2184
3331
  return 'done';
2185
3332
  }
2186
3333
  let result;
2187
3334
  journal.record(`submission ${room.locator} ${head.id}`);
2188
- await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
3335
+ await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
2189
3336
  if (discoveryPaused || fences.has(room.locator))
2190
3337
  return 'done';
2191
3338
  if (signal.aborted || writesHeld()) {
2192
- await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
3339
+ await exclusive(() => mark(local, room.locator, head, submission, earlier));
2193
3340
  if (signal.aborted)
2194
3341
  return 'done';
2195
3342
  progress.complete = false;
@@ -2197,11 +3344,17 @@ export function composeRuntime(options) {
2197
3344
  return 'done';
2198
3345
  }
2199
3346
  const opensBefore = openSignals;
2200
- const answered = awaitingAnswer(room.locator);
3347
+ const answeredHere = awaitingAnswer(room.locator);
3348
+ const answeredThere = partner === null ? () => { } : awaitingAnswer(partner);
3349
+ const answered = () => {
3350
+ answeredHere();
3351
+ answeredThere();
3352
+ };
2201
3353
  try {
2202
3354
  result = await options.adapter.mutate(authorized, {
2203
3355
  locator: room.locator,
2204
3356
  changeBatch: head,
3357
+ file: batchFile(head),
2205
3358
  ...(state?.upload === undefined ? {} : { upload: state.upload }),
2206
3359
  }, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
2207
3360
  }
@@ -2226,9 +3379,10 @@ export function composeRuntime(options) {
2226
3379
  return 'again';
2227
3380
  }
2228
3381
  if (result.kind === 'room-not-readable') {
3382
+ const unread = discovered.get(result.locator) ?? room;
2229
3383
  progress.complete = false;
2230
- unreadable.add(room.locator);
2231
- list(room, false);
3384
+ unreadable.add(unread.locator);
3385
+ list(unread, false);
2232
3386
  emitRooms();
2233
3387
  return 'done';
2234
3388
  }
@@ -2243,24 +3397,30 @@ export function composeRuntime(options) {
2243
3397
  }
2244
3398
  if (result.kind === 'write-halted') {
2245
3399
  journal.record(`outcome ${room.locator} ${head.id} write-halted`);
2246
- await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, lost));
3400
+ await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
2247
3401
  if (serviceMode === 'open' && openSignals !== opensBefore)
2248
3402
  continue;
2249
3403
  haltedRefusal = true;
2250
3404
  progress.complete = false;
2251
3405
  return 'done';
2252
3406
  }
3407
+ const other = partner === null ? undefined : discovered.get(partner);
2253
3408
  if (result.kind === 'conflicted' ||
2254
3409
  result.kind === 'rejected' ||
2255
3410
  result.kind === 'expired' ||
2256
3411
  result.kind === 'retryable-refusal') {
2257
3412
  if (result.kind === 'expired' && lost && result.restored !== true) {
2258
3413
  journal.record(`outcome ${room.locator} ${head.id} expired`);
2259
- await discardPending(room, local, signal);
3414
+ await discardHeld(head, room, local, signal);
2260
3415
  return 'done';
2261
3416
  }
2262
3417
  if (result.kind === 'expired') {
2263
- await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
3418
+ await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
3419
+ const there = other === undefined ? null : await advance(other, local, authorized, continuing);
3420
+ if (other !== undefined && there !== null) {
3421
+ await withhold(there, other, local, progress);
3422
+ return 'done';
3423
+ }
2264
3424
  const withheld = await advance(room, local, authorized, continuing);
2265
3425
  if (withheld !== null) {
2266
3426
  await withhold(withheld, room, local, progress);
@@ -2277,11 +3437,25 @@ export function composeRuntime(options) {
2277
3437
  if (result.kind === 'retryable-refusal') {
2278
3438
  const reset = result.reset;
2279
3439
  progress.complete = false;
2280
- await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
3440
+ await exclusive(() => mark(local, room.locator, head, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
2281
3441
  return reset === undefined ? 'again' : { at: Date.parse(reset) };
2282
3442
  }
2283
3443
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
2284
3444
  const left = leaving(head, result);
3445
+ const refreshed = async () => ({
3446
+ there: other === undefined ? null : await advance(other, local, authorized, continuing),
3447
+ here: await advance(room, local, authorized, continuing),
3448
+ });
3449
+ const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
3450
+ if (judged !== null && (judged.there !== null || judged.here !== null)) {
3451
+ await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
3452
+ if (other !== undefined && judged.there !== null) {
3453
+ await withhold(judged.there, other, local, progress);
3454
+ }
3455
+ if (judged.here !== null)
3456
+ await withhold(judged.here, room, local, progress);
3457
+ return 'done';
3458
+ }
2285
3459
  await exclusive(async () => {
2286
3460
  if (!continuing())
2287
3461
  return;
@@ -2289,25 +3463,29 @@ export function composeRuntime(options) {
2289
3463
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
2290
3464
  return;
2291
3465
  }
2292
- const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
2293
- leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
2294
- (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
2295
- ? { ...left, returned: null }
2296
- : left,
2297
- });
2298
- publishChangeSet(settled());
3466
+ const confirmedLayer = await sliceAt(current.record, 'confirmed', [head]);
3467
+ const held = (result.kind === 'expired' &&
3468
+ (await landedWhole(room.locator, head, confirmedLayer))) ||
3469
+ (result.kind === 'conflicted' &&
3470
+ (await landedWhole(room.locator, head, confirmedLayer, result.affected)));
3471
+ publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
3472
+ await unpair(local);
2299
3473
  moveTo({ ...status, ...attention() });
2300
3474
  });
2301
3475
  if (result.kind === 'expired')
2302
3476
  continue;
2303
- const withheld = await advance(room, local, authorized, continuing);
2304
- if (withheld !== null) {
2305
- await withhold(withheld, room, local, progress);
3477
+ const { there, here } = judged ?? (await refreshed());
3478
+ if (other !== undefined && there !== null)
3479
+ await withhold(there, other, local, progress);
3480
+ if (here !== null) {
3481
+ await withhold(here, room, local, progress);
2306
3482
  return 'done';
2307
3483
  }
2308
3484
  continue;
2309
3485
  }
2310
- const kept = await adopted(room, local, head, result).finally(answered);
3486
+ const kept = await (partner === null
3487
+ ? adopted(room, local, head, result)
3488
+ : adoptedMove(room, partner, local, head, result)).finally(answered);
2311
3489
  if (!kept) {
2312
3490
  progress.complete = false;
2313
3491
  return 'done';
@@ -2318,44 +3496,67 @@ export function composeRuntime(options) {
2318
3496
  function pastHorizon(identity) {
2319
3497
  return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
2320
3498
  }
2321
- async function discardPending(room, local, signal) {
2322
- const { locator } = room;
3499
+ async function discardPending(rooms, local, signal) {
2323
3500
  const discarded = await exclusive(async () => {
2324
- if (fences.has(locator))
2325
- return false;
2326
- const before = replicas.get(locator);
2327
- const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
2328
- fences.set(locator, { at: discoveries, erased: false });
2329
- lastFenced.set(locator, ++fencings);
2330
- try {
2331
- await local.discard(locator);
2332
- }
2333
- finally {
2334
- fences.delete(locator);
3501
+ const held = rooms
3502
+ .filter(({ locator }) => !fences.has(locator))
3503
+ .map((room) => ({
3504
+ room,
3505
+ before: replicas.get(room.locator),
3506
+ queue: queueIn(room.locator),
3507
+ }));
3508
+ if (held.length === 0)
3509
+ return [];
3510
+ const answer = await local.commit({
3511
+ rooms: held.flatMap(({ room, before }) => before === undefined
3512
+ ? []
3513
+ : [
3514
+ {
3515
+ locator: room.locator,
3516
+ key: before.record.key,
3517
+ roots: before.record.typedSelection?.roots ?? [],
3518
+ overlay: { set: [], cleared: [...before.overlaid] },
3519
+ },
3520
+ ]),
3521
+ discards: held.map(({ room }) => room.locator),
3522
+ });
3523
+ for (const { room, before, queue } of held) {
3524
+ const { locator } = room;
3525
+ lastFenced.set(locator, ++fencings);
3526
+ for (const batch of queue)
3527
+ pendings.delete(pendingKey(locator, batch.id));
3528
+ queuesWithoutReplica.delete(locator);
3529
+ if (before !== undefined) {
3530
+ replicas.set(locator, {
3531
+ ...before,
3532
+ queue: [],
3533
+ overlaid: new Set(),
3534
+ count: answer.counts[locator] ?? before.count,
3535
+ });
3536
+ }
3537
+ journal.record(`room discarded ${locator}`);
2335
3538
  }
2336
- for (const batch of queue)
2337
- pendings.delete(batch.id);
2338
- queuesWithoutReplica.delete(locator);
2339
- const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
2340
- if (after !== null)
2341
- replicas.set(locator, after);
2342
- journal.record(`room discarded ${locator}`);
2343
- publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
3539
+ publishChangeSet(mergeChangeSets(held.map(({ room }) => changeSet(room.locator, {
3540
+ documents: answer.changed[room.locator] ?? [],
3541
+ queue: true,
3542
+ }))));
2344
3543
  moveTo({ ...status, ...attention() });
2345
- return true;
2346
- });
2347
- if (!discarded)
2348
- return;
2349
- const progress = { complete: true, reachable: true };
2350
- await roomTask(locator, async (live) => {
2351
- await Promise.race([
2352
- reload(room, local, progress, () => live() && !signal.aborted),
2353
- abandonedOn(signal),
2354
- ]);
3544
+ return held.map(({ room }) => room);
2355
3545
  });
2356
- if (signal.aborted)
2357
- return;
2358
- await rest(progress);
3546
+ for (const room of discarded) {
3547
+ if (signal.aborted)
3548
+ return;
3549
+ const progress = { complete: true, reachable: true };
3550
+ await roomTask(room.locator, async (live) => {
3551
+ await Promise.race([
3552
+ reload(room, local, progress, () => live() && !signal.aborted),
3553
+ abandonedOn(signal),
3554
+ ]);
3555
+ });
3556
+ if (signal.aborted)
3557
+ return;
3558
+ await rest(progress);
3559
+ }
2359
3560
  }
2360
3561
  function awaitingAnswer(locator) {
2361
3562
  let release = () => { };
@@ -2372,7 +3573,7 @@ export function composeRuntime(options) {
2372
3573
  async function retainModel(local, room, attached, next) {
2373
3574
  const key = modelKey(room.locator, attached);
2374
3575
  const previous = modelOf(room.locator, attached);
2375
- const heldBefore = replicas.get(room.locator)?.confirmed.record;
3576
+ const heldBefore = replicas.get(room.locator)?.record;
2376
3577
  if (JSON.stringify(previous) === JSON.stringify(next) &&
2377
3578
  (heldBefore === undefined ||
2378
3579
  (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
@@ -2388,36 +3589,57 @@ export function composeRuntime(options) {
2388
3589
  if (!continuing())
2389
3590
  return;
2390
3591
  const held = replicas.get(room.locator);
3592
+ let changed = null;
2391
3593
  if (held !== undefined) {
2392
3594
  const declared = {
2393
- ...held.confirmed.record,
3595
+ ...held.record,
2394
3596
  operations: room.operations,
2395
3597
  replica: room.replica,
2396
3598
  typedSelection: room.typedSelection ?? null,
2397
3599
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
2398
3600
  };
2399
- const reclassified = sameMembership(declared, held.confirmed.record)
2400
- ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
2401
- : readReclassified(declared, held.confirmed);
2402
- const record = reclassified.kind === 'staged'
3601
+ let read = 'staged';
3602
+ if (!sameMembership(declared, held.record)) {
3603
+ const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), compositionOf(declared));
3604
+ read =
3605
+ verdict.verdict !== 'refuse'
3606
+ ? 'staged'
3607
+ : verdict.rule === 'model-unavailable'
3608
+ ? 'deferred'
3609
+ : { kind: 'refused', refusal: verdict };
3610
+ }
3611
+ const record = read === 'staged'
2403
3612
  ? declared
2404
- : {
2405
- ...declared,
2406
- typedSelection: held.confirmed.record.typedSelection,
2407
- };
2408
- const read = reclassified.kind === 'deferred'
2409
- ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
2410
- : reclassified;
2411
- await local.write({ next: record, previous: held.confirmed.record });
2412
- if (read.kind === 'staged') {
2413
- replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
3613
+ : { ...declared, typedSelection: held.record.typedSelection };
3614
+ const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
3615
+ const replayedLayer = await replayed(record, confirmedLayer, held.queue);
3616
+ const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
3617
+ const answer = await local.commit({
3618
+ rooms: [
3619
+ {
3620
+ locator: room.locator,
3621
+ key: record.key,
3622
+ record: storedRecord(record),
3623
+ roots: record.typedSelection?.roots ?? [],
3624
+ composition: compositionOf(record),
3625
+ overlay: diff.overlay,
3626
+ },
3627
+ ],
3628
+ });
3629
+ recomposed(room.locator, held.record, record);
3630
+ const { count } = committedRoom(room.locator, record, answer);
3631
+ replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count });
3632
+ changed = changeSet(room.locator, {
3633
+ documents: answer.changed[room.locator] ?? [],
3634
+ room: true,
3635
+ });
3636
+ if (read === 'staged' || read === 'deferred') {
2414
3637
  if (declarationRefusals.delete(room.locator)) {
2415
3638
  sourceRefusals.delete(room.locator);
2416
3639
  unreadable.delete(room.locator);
2417
3640
  }
2418
3641
  }
2419
- else if (read.kind === 'refused') {
2420
- replicas.set(room.locator, { ...held, confirmed: { ...held.confirmed, record } });
3642
+ else {
2421
3643
  refusedSource(room, read);
2422
3644
  declarationRefusals.add(room.locator);
2423
3645
  }
@@ -2428,14 +3650,14 @@ export function composeRuntime(options) {
2428
3650
  invalidModels.delete(key);
2429
3651
  }
2430
3652
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
2431
- if (held === undefined)
3653
+ if (changed === null)
2432
3654
  emitRooms();
2433
3655
  else
2434
- publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
3656
+ publishChangeSet(changed);
2435
3657
  });
2436
3658
  }
2437
3659
  async function scoped(result, room, local) {
2438
- if ((result.scope === 'content' || result.scope === 'sync') && !result.retryable) {
3660
+ if (result.scope === 'content' && !result.retryable) {
2439
3661
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
2440
3662
  await loseRooms([room.locator]);
2441
3663
  return;
@@ -2453,15 +3675,7 @@ export function composeRuntime(options) {
2453
3675
  });
2454
3676
  else if (result.scope !== 'operation') {
2455
3677
  unreadable.add(room.locator);
2456
- scopedAway = {
2457
- ...room,
2458
- replica: { ...room.replica, admission: 'unavailable' },
2459
- operations: {
2460
- ...room.operations,
2461
- ...(result.scope === 'content' ? { read: [] } : {}),
2462
- ...(result.scope === 'sync' ? { sync: [] } : {}),
2463
- },
2464
- };
3678
+ scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
2465
3679
  await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
2466
3680
  }
2467
3681
  if (!continuing())
@@ -2472,17 +3686,10 @@ export function composeRuntime(options) {
2472
3686
  }
2473
3687
  function heldRooms(listed) {
2474
3688
  return listed.flatMap((room) => {
2475
- const record = replicas.get(room.locator)?.confirmed.record;
3689
+ const record = replicas.get(room.locator)?.record;
2476
3690
  if (record === undefined || record.replica.selection !== room.replica.selection)
2477
3691
  return [];
2478
- return [
2479
- {
2480
- locator: room.locator,
2481
- roomVersion: record.roomVersion,
2482
- documents: record.documents,
2483
- revisions: record.revisions,
2484
- },
2485
- ];
3692
+ return [{ locator: room.locator, roomVersion: record.roomVersion }];
2486
3693
  });
2487
3694
  }
2488
3695
  async function synchronize(listed, local, progress, attempt, live) {
@@ -2510,7 +3717,7 @@ export function composeRuntime(options) {
2510
3717
  progress.complete = false;
2511
3718
  return false;
2512
3719
  }
2513
- const prior = replicas.get(room.locator)?.confirmed.record;
3720
+ const prior = replicas.get(room.locator)?.record;
2514
3721
  if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
2515
3722
  list(prior, false);
2516
3723
  emitRooms();
@@ -2553,6 +3760,8 @@ export function composeRuntime(options) {
2553
3760
  }
2554
3761
  async function reload(room, local, progress, live) {
2555
3762
  try {
3763
+ if (live())
3764
+ reloadsDue.delete(room.locator);
2556
3765
  if (!live() || authorization === undefined)
2557
3766
  return;
2558
3767
  const bootstrapped = await pullBootstrap(authorization, room, local, live);
@@ -2562,7 +3771,7 @@ export function composeRuntime(options) {
2562
3771
  await withhold(bootstrapped, room, local, progress);
2563
3772
  return;
2564
3773
  }
2565
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
3774
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
2566
3775
  await caughtUp(room, local, progress, live);
2567
3776
  }
2568
3777
  finally {
@@ -2619,12 +3828,13 @@ export function composeRuntime(options) {
2619
3828
  }
2620
3829
  for (const room of result.rooms) {
2621
3830
  if (room.operations === undefined ||
2622
- !['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
3831
+ !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
2623
3832
  room.operations[operation].every((scope) => typeof scope === 'string')) ||
2624
3833
  room.replica === undefined ||
2625
3834
  typeof room.replica.selection !== 'string' ||
2626
3835
  room.replica.selection.length === 0 ||
2627
- !['admitted', 'unavailable'].includes(room.replica.admission)) {
3836
+ !['admitted', 'unavailable'].includes(room.replica.admission) ||
3837
+ !['locked', 'open'].includes(room.replica.offline)) {
2628
3838
  journal.record(`invalid model or replica declaration ${room.locator}`);
2629
3839
  await refuse('missing-required-guarantee');
2630
3840
  notifyReady();
@@ -2648,11 +3858,12 @@ export function composeRuntime(options) {
2648
3858
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
2649
3859
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
2650
3860
  limits = result.providerLimits;
2651
- await local.declare(limits, capabilities);
3861
+ await local.fact('providerLimits', limits);
3862
+ await local.fact('capabilities', capabilities);
2652
3863
  }
2653
3864
  const signedIn = result.member ?? null;
2654
3865
  if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
2655
- await local.keepMember(signedIn);
3866
+ await local.fact('member', signedIn);
2656
3867
  member = signedIn;
2657
3868
  emitMember();
2658
3869
  }
@@ -2786,6 +3997,7 @@ export function composeRuntime(options) {
2786
3997
  function closedSocket(revoked, lost) {
2787
3998
  const opened = socketOpen;
2788
3999
  socketOpen = false;
4000
+ connectionLost();
2789
4001
  if (lost) {
2790
4002
  reads.lost();
2791
4003
  conversations.lost();
@@ -2855,17 +4067,20 @@ export function composeRuntime(options) {
2855
4067
  owed.clear();
2856
4068
  for (const locator of reloading)
2857
4069
  catching.add(locator);
2858
- readSide.recover();
4070
+ liveQueries.recover();
2859
4071
  return reloading;
2860
4072
  }
2861
4073
  async function reloadNow(replaced, progress) {
4074
+ const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
4075
+ for (const room of reloaded)
4076
+ reloadsDue.set(room.locator, generation);
2862
4077
  try {
2863
4078
  const local = await openLocal();
2864
- return await Promise.allSettled([...discovered.values()]
2865
- .filter((room) => replicas.has(room.locator) && replaced(room))
2866
- .map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
4079
+ return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
2867
4080
  }
2868
4081
  catch {
4082
+ for (const room of reloaded)
4083
+ reloadsDue.delete(room.locator);
2869
4084
  progress.reachable = false;
2870
4085
  return [];
2871
4086
  }
@@ -3019,7 +4234,7 @@ export function composeRuntime(options) {
3019
4234
  if (synced && !recorded) {
3020
4235
  const completedAt = new Date().toISOString();
3021
4236
  try {
3022
- await (await openLocal()).complete(completedAt);
4237
+ await (await openLocal()).fact('lastCompleted', completedAt);
3023
4238
  if (!live())
3024
4239
  return;
3025
4240
  recorded = true;
@@ -3059,8 +4274,8 @@ export function composeRuntime(options) {
3059
4274
  if (room === undefined || room.replica.admission !== 'admitted')
3060
4275
  return;
3061
4276
  if (held !== undefined &&
3062
- (held.confirmed.record.replica.selection !== room.replica.selection ||
3063
- options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
4277
+ (held.record.replica.selection !== room.replica.selection ||
4278
+ options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
3064
4279
  return;
3065
4280
  }
3066
4281
  const during = underway?.generation === generation ? underway.progress : null;
@@ -3107,6 +4322,7 @@ export function composeRuntime(options) {
3107
4322
  return;
3108
4323
  if (event.kind === 'opened') {
3109
4324
  socketOpen = true;
4325
+ connectionLive();
3110
4326
  generation += 1;
3111
4327
  reads.opened();
3112
4328
  conversations.reopened();
@@ -3121,7 +4337,14 @@ export function composeRuntime(options) {
3121
4337
  void rediscover(event.scope);
3122
4338
  else
3123
4339
  void applyRoom(event.locator, event.roomVersion);
3124
- }, receipts);
4340
+ }, receipts, heldRevisions);
4341
+ }
4342
+ async function heldRevisions(locator, paths) {
4343
+ const held = replicas.get(locator);
4344
+ if (held === undefined || paths.length === 0)
4345
+ return new Map();
4346
+ const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
4347
+ return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
3125
4348
  }
3126
4349
  function writesHeld() {
3127
4350
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -3193,6 +4416,8 @@ export function composeRuntime(options) {
3193
4416
  closed = true;
3194
4417
  if (rechecking !== null)
3195
4418
  clearInterval(rechecking);
4419
+ connectionLive();
4420
+ lockListeners.clear();
3196
4421
  readinessWatches.clear();
3197
4422
  withdrawStream?.();
3198
4423
  withdrawStream = undefined;
@@ -3201,17 +4426,13 @@ export function composeRuntime(options) {
3201
4426
  withdrawServiceMode?.();
3202
4427
  conversations.close();
3203
4428
  void recovery.retire();
3204
- store?.close();
3205
- void opening?.then((opened) => {
3206
- opened.close();
3207
- }, () => { });
3208
4429
  roomListeners.clear();
3209
4430
  refusalListeners.clear();
3210
4431
  storageLossListeners.clear();
3211
4432
  statusListeners.clear();
3212
4433
  serviceModeListeners.clear();
3213
4434
  memberListeners.clear();
3214
- readSide.close();
4435
+ liveQueries.close();
3215
4436
  queueSubscriptions.clear();
3216
4437
  roomDraftListeners.clear();
3217
4438
  presentationListeners.clear();
@@ -3257,7 +4478,7 @@ export function composeRuntime(options) {
3257
4478
  const watchers = seedWatchers.get(plan.key) ?? new Set();
3258
4479
  seedWatchers.set(plan.key, watchers);
3259
4480
  watchers.add(shown);
3260
- const stop = readSide.watch(plan, identityOf(plan), () => {
4481
+ const stop = liveQueries.watch(plan, identityOf(plan), () => {
3261
4482
  const cover = covered(plan);
3262
4483
  awaitPreparation(cover);
3263
4484
  return seedBehind(plan, cover);
@@ -3278,66 +4499,53 @@ export function composeRuntime(options) {
3278
4499
  stop();
3279
4500
  };
3280
4501
  }
3281
- async function searched(input, reach) {
4502
+ async function searched(input, reach, locked) {
3282
4503
  const asked = searchPlanOf(input);
3283
- const plan = { ...asked, limit: capped(asked.limit) };
3284
- const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
4504
+ const filter = {
4505
+ ...filterOf(asked.documents ?? {}, roomFacts(reach), planReach(reach)),
4506
+ locked,
4507
+ };
4508
+ const unlocking = (filter.rooms ?? [])
4509
+ .filter((room) => locked.includes(room))
4510
+ .map((room) => ({ room, awaits: 'unlock' }));
3285
4511
  for (;;) {
3286
4512
  const cover = covered(filter);
3287
4513
  awaitPreparation(cover);
3288
- const pending = 'pending' in cover ? [...cover.pending] : [];
3289
- const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3290
- const held = covered({
3291
- ...filter,
3292
- rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
3293
- });
3294
- const covering = 'pending' in held ? [] : held.coverage.rooms;
3295
- const unindexed = covering
3296
- .filter(({ room }) => !searchIndex.rooms().has(room))
3297
- .map(({ room }) => room);
3298
- for (const room of unindexed)
3299
- pending.push({ room, awaits: 'index' });
3300
- const listed = conversations.searchable();
3301
- const named = reach !== null && !reach.conversations
3302
- ? []
3303
- : (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
3304
- const waiting = named.filter((one) => !searchIndex.conversations().has(one));
3305
- for (const conversation of waiting)
3306
- pending.push({ conversation, awaits: 'index' });
3307
- if (unindexed.length > 0 || waiting.length > 0)
3308
- index(unindexed, waiting);
4514
+ const pending = [...unlocking, ...('pending' in cover ? cover.pending : [])];
3309
4515
  const complete = pending.length === 0 && !('pending' in cover);
4516
+ const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3310
4517
  const answering = complete
3311
4518
  ? filter
3312
- : {
3313
- ...filter,
3314
- rooms: covering
3315
- .filter(({ room }) => searchIndex.rooms().has(room))
3316
- .map(({ room }) => room),
3317
- };
4519
+ : { ...filter, rooms: reachedRooms(filter).filter((room) => !awaited.has(room)) };
3318
4520
  const answered = covered(answering);
3319
4521
  if ('pending' in answered)
3320
4522
  continue;
3321
- const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
3322
- await readSide.settled();
3323
- const seen = searchIndex.changes();
3324
- await searchIndex.caughtUp();
3325
- const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
3326
- if (selected !== null && 'pending' in selected)
3327
- continue;
3328
- if (searchIndex.changes() !== seen ||
3329
- JSON.stringify(covered(answering)) !== JSON.stringify(answered))
4523
+ const listed = conversations.searchable();
4524
+ const named = reach !== null && !reach.conversations
4525
+ ? []
4526
+ : (asked.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
4527
+ const hits = await (await openLocal()).search({
4528
+ text: asked.text,
4529
+ limit: capped(asked.limit),
4530
+ after: asked.after,
4531
+ total: asked.total,
4532
+ documents: {
4533
+ plan: answering,
4534
+ rooms: answered.rooms,
4535
+ beneath: asked.collection
4536
+ ? answered.rooms.map(({ locator, key }) => ({
4537
+ key,
4538
+ roots: replicas.get(locator)?.record.typedSelection?.roots ?? [],
4539
+ }))
4540
+ : [],
4541
+ },
4542
+ conversations: named,
4543
+ });
4544
+ if (JSON.stringify(covered(answering)) !== JSON.stringify(answered))
3330
4545
  continue;
3331
- const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms === null ? null : new Set(answering.rooms), plan.collection
3332
- ? new Map(answered.coverage.rooms.map(({ room }) => [
3333
- room,
3334
- replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
3335
- ]))
3336
- : null);
3337
4546
  const coverage = {
3338
4547
  rooms: answered.coverage.rooms,
3339
- conversations: searchedConversations,
3340
- index: searchIndex.generation(),
4548
+ conversations: named,
3341
4549
  overlay: answered.coverage.overlay,
3342
4550
  };
3343
4551
  return complete
@@ -3351,13 +4559,17 @@ export function composeRuntime(options) {
3351
4559
  roomDraftListeners.add(listener);
3352
4560
  void exclusive(async () => {
3353
4561
  await openLocal();
3354
- if (closed || !roomDraftListeners.has(listener))
3355
- return;
3356
- listener.ready = true;
3357
4562
  }).then(() => {
3358
- if (closed || !listener.ready || !roomDraftListeners.has(listener))
4563
+ if (closed || !roomDraftListeners.has(listener))
3359
4564
  return;
3360
- listener.notify(snapshotRoomDrafts());
4565
+ const snapshot = snapshotRoomDrafts();
4566
+ draftsShown = draftsShown.then(async () => {
4567
+ const value = await snapshot.catch(() => null);
4568
+ if (value === null || closed || !roomDraftListeners.has(listener))
4569
+ return;
4570
+ listener.ready = true;
4571
+ listener.notify(value);
4572
+ });
3361
4573
  }, () => { });
3362
4574
  return () => {
3363
4575
  roomDraftListeners.delete(listener);
@@ -3452,7 +4664,7 @@ export function composeRuntime(options) {
3452
4664
  return listed();
3453
4665
  },
3454
4666
  query: ((input) => rowsQuery(planned(input, null))),
3455
- search: (input) => searched(input, null),
4667
+ search: (input) => searched(input, null, []),
3456
4668
  types(locator) {
3457
4669
  return {
3458
4670
  collection: modelTypes(modelOf(locator)),
@@ -3472,9 +4684,9 @@ export function composeRuntime(options) {
3472
4684
  return listed;
3473
4685
  });
3474
4686
  },
3475
- read(room, path) {
4687
+ async read(room, path) {
3476
4688
  const record = roomDrafts.get(room)?.get(path);
3477
- return record === undefined ? null : roomDraftOf(record);
4689
+ return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
3478
4690
  },
3479
4691
  write(room, path, revision, input) {
3480
4692
  return exclusive(async () => {
@@ -3484,11 +4696,11 @@ export function composeRuntime(options) {
3484
4696
  const local = await openLocal();
3485
4697
  const current = roomDrafts.get(room)?.get(path);
3486
4698
  if ((current?.revision ?? null) !== revision) {
3487
- return staleWrite(current === undefined ? null : roomDraftOf(current));
4699
+ return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
3488
4700
  }
3489
4701
  const file = parts.file === undefined
3490
4702
  ? undefined
3491
- : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
4703
+ : await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
3492
4704
  const record = {
3493
4705
  locator: room,
3494
4706
  path,
@@ -3496,10 +4708,12 @@ export function composeRuntime(options) {
3496
4708
  parts: held(current?.parts, parts, file),
3497
4709
  returned: current?.returned ?? null,
3498
4710
  };
3499
- await local.keepRoomDraft(record);
4711
+ await local.keepRoomDraft(record, parts.file === undefined || file === undefined
4712
+ ? []
4713
+ : [{ key: file.localId, blob: parts.file.blob }]);
3500
4714
  rememberRoomDraft(record);
3501
- publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
3502
- return { outcome: 'written', roomDraft: roomDraftOf(record) };
4715
+ publishChangeSet(changeSet(room, { drafts: [path] }));
4716
+ return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
3503
4717
  });
3504
4718
  },
3505
4719
  discard(room, path, revision) {
@@ -3507,11 +4721,11 @@ export function composeRuntime(options) {
3507
4721
  const local = await openLocal();
3508
4722
  const current = roomDrafts.get(room)?.get(path);
3509
4723
  if (current === undefined || current.revision !== revision) {
3510
- return staleWrite(current === undefined ? null : roomDraftOf(current));
4724
+ return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
3511
4725
  }
3512
4726
  await local.discardRoomDraft(room, path);
3513
4727
  forgetRoomDraft(room, path);
3514
- publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
4728
+ publishChangeSet(changeSet(room, { drafts: [path] }));
3515
4729
  moveTo({ ...status, ...attention() });
3516
4730
  return { outcome: 'written', roomDraft: null };
3517
4731
  });
@@ -3545,15 +4759,21 @@ export function composeRuntime(options) {
3545
4759
  history(locator, path, query) {
3546
4760
  return readHistory(locator, path, query);
3547
4761
  },
4762
+ resolve(room, path, destination) {
4763
+ return resolveReference(room, path, destination);
4764
+ },
3548
4765
  binaryFile: {
4766
+ part: readPart,
3549
4767
  read: readBinaryPath,
3550
4768
  link: linkBinaryPath,
3551
4769
  readReference: readBinaryReference,
3552
4770
  },
3553
- call(name, input) {
3554
- return recoveredAction(name, input);
4771
+ async call(name, input) {
4772
+ await ready;
4773
+ const room = calledRooms(name, input).find((one) => locked(one));
4774
+ return room === undefined ? recoveredAction(name, input) : lockedRoom(room);
3555
4775
  },
3556
- reached(locator, roomVersion, path) {
4776
+ async reached(locator, roomVersion, path) {
3557
4777
  return path === undefined
3558
4778
  ? reachedBy(locator, roomVersion)
3559
4779
  : writtenAfter(locator, roomVersion, path);
@@ -3561,12 +4781,13 @@ export function composeRuntime(options) {
3561
4781
  log() {
3562
4782
  return journal.export();
3563
4783
  },
4784
+ cleanUp,
3564
4785
  persistence() {
3565
4786
  return persistence === 'granted';
3566
4787
  },
3567
4788
  async retry() {
3568
4789
  journal.record('retry');
3569
- unindexable.clear();
4790
+ unpreparable.clear();
3570
4791
  queueReadiness();
3571
4792
  resume();
3572
4793
  },
@@ -3596,6 +4817,10 @@ export function composeRuntime(options) {
3596
4817
  watchPlan,
3597
4818
  search: searched,
3598
4819
  watchReadiness,
4820
+ locked,
4821
+ lockedRooms,
4822
+ watchLocks,
4823
+ located,
3599
4824
  };
3600
4825
  return {
3601
4826
  md,