@agentshouse/mdruntime 0.1.0-c5d3fa6fb67bc → 0.1.0-c5f333dfa0583

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 (198) hide show
  1. package/AGENTS.md +225 -212
  2. package/README.md +9 -8
  3. package/dist/adapter.d.ts +64 -24
  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 +15 -7
  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 +8 -9
  15. package/dist/change-set.d.ts.map +1 -1
  16. package/dist/change-set.js +10 -85
  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/earlier.d.ts +10 -0
  21. package/dist/chat/earlier.d.ts.map +1 -0
  22. package/dist/chat/earlier.js +54 -0
  23. package/dist/chat/house.d.ts +25 -0
  24. package/dist/chat/house.d.ts.map +1 -0
  25. package/dist/chat/house.js +38 -0
  26. package/dist/chat/input.d.ts +3 -1
  27. package/dist/chat/input.d.ts.map +1 -1
  28. package/dist/chat/input.js +11 -18
  29. package/dist/chat/kit.js +1 -1
  30. package/dist/chat/list.d.ts +2 -1
  31. package/dist/chat/list.d.ts.map +1 -1
  32. package/dist/chat/list.js +45 -5
  33. package/dist/chat/markdown.d.ts +1 -0
  34. package/dist/chat/markdown.d.ts.map +1 -1
  35. package/dist/chat/markdown.js +78 -20
  36. package/dist/chat/message.d.ts +1 -0
  37. package/dist/chat/message.d.ts.map +1 -1
  38. package/dist/chat/message.js +73 -11
  39. package/dist/chat/messages.d.ts +14 -1
  40. package/dist/chat/messages.d.ts.map +1 -1
  41. package/dist/chat/messages.js +79 -24
  42. package/dist/chat/name.d.ts +1 -0
  43. package/dist/chat/name.d.ts.map +1 -1
  44. package/dist/chat/name.js +1 -1
  45. package/dist/chat/opened.d.ts +1 -0
  46. package/dist/chat/opened.d.ts.map +1 -1
  47. package/dist/chat/opened.js +2 -0
  48. package/dist/chat/overlay.d.ts +1 -0
  49. package/dist/chat/overlay.d.ts.map +1 -1
  50. package/dist/chat/overlay.js +11 -5
  51. package/dist/chat/parts.d.ts +1 -0
  52. package/dist/chat/parts.d.ts.map +1 -1
  53. package/dist/chat/parts.js +3 -2
  54. package/dist/chat/pieces.d.ts +62 -0
  55. package/dist/chat/pieces.d.ts.map +1 -0
  56. package/dist/chat/pieces.js +1110 -0
  57. package/dist/chat/provider.d.ts +8 -1
  58. package/dist/chat/provider.d.ts.map +1 -1
  59. package/dist/chat/provider.js +98 -92
  60. package/dist/chat/question.d.ts +1 -1
  61. package/dist/chat/question.d.ts.map +1 -1
  62. package/dist/chat/question.js +7 -18
  63. package/dist/chat/ready.d.ts +5 -2
  64. package/dist/chat/ready.d.ts.map +1 -1
  65. package/dist/chat/ready.js +7 -5
  66. package/dist/chat/room-file.d.ts +8 -0
  67. package/dist/chat/room-file.d.ts.map +1 -0
  68. package/dist/chat/room-file.js +72 -0
  69. package/dist/chat/steady.d.ts +3 -0
  70. package/dist/chat/steady.d.ts.map +1 -0
  71. package/dist/chat/steady.js +26 -0
  72. package/dist/chat/style.d.ts.map +1 -1
  73. package/dist/chat/style.js +6 -1
  74. package/dist/chat.d.ts +3 -2
  75. package/dist/chat.d.ts.map +1 -1
  76. package/dist/chat.js +2 -2
  77. package/dist/client.d.ts +11 -2
  78. package/dist/client.d.ts.map +1 -1
  79. package/dist/client.js +45 -7
  80. package/dist/collection.d.ts +6 -19
  81. package/dist/collection.d.ts.map +1 -1
  82. package/dist/collection.js +30 -287
  83. package/dist/compile.d.ts +11 -0
  84. package/dist/compile.d.ts.map +1 -0
  85. package/dist/compile.js +712 -0
  86. package/dist/connection.d.ts +21 -8
  87. package/dist/connection.d.ts.map +1 -1
  88. package/dist/connection.js +2319 -688
  89. package/dist/conversation.d.ts +15 -3
  90. package/dist/conversation.d.ts.map +1 -1
  91. package/dist/conversation.js +233 -80
  92. package/dist/digest.d.ts +7 -0
  93. package/dist/digest.d.ts.map +1 -0
  94. package/dist/digest.js +113 -0
  95. package/dist/durability.d.ts +2 -1
  96. package/dist/durability.d.ts.map +1 -1
  97. package/dist/entry.d.ts +3 -1
  98. package/dist/entry.d.ts.map +1 -1
  99. package/dist/entry.js +18 -23
  100. package/dist/file.d.ts +2 -10
  101. package/dist/file.d.ts.map +1 -1
  102. package/dist/file.js +3 -120
  103. package/dist/host.d.ts +5 -4
  104. package/dist/host.d.ts.map +1 -1
  105. package/dist/host.js +55 -7
  106. package/dist/index.d.ts +10 -6
  107. package/dist/index.d.ts.map +1 -1
  108. package/dist/index.js +4 -0
  109. package/dist/live.d.ts +22 -0
  110. package/dist/live.d.ts.map +1 -0
  111. package/dist/live.js +123 -0
  112. package/dist/memory.d.ts +7 -3
  113. package/dist/memory.d.ts.map +1 -1
  114. package/dist/memory.js +39 -17
  115. package/dist/name.d.ts +4 -0
  116. package/dist/name.d.ts.map +1 -0
  117. package/dist/name.js +122 -0
  118. package/dist/observation.d.ts +14 -0
  119. package/dist/observation.d.ts.map +1 -1
  120. package/dist/observation.js +93 -3
  121. package/dist/opened.d.ts +22 -0
  122. package/dist/opened.d.ts.map +1 -0
  123. package/dist/opened.js +107 -0
  124. package/dist/outcome.d.ts +35 -4
  125. package/dist/outcome.d.ts.map +1 -1
  126. package/dist/outcome.js +28 -0
  127. package/dist/overlay.d.ts +2 -5
  128. package/dist/overlay.d.ts.map +1 -1
  129. package/dist/overlay.js +86 -171
  130. package/dist/query.d.ts +26 -2
  131. package/dist/query.d.ts.map +1 -1
  132. package/dist/query.js +34 -6
  133. package/dist/reach.d.ts +5 -1
  134. package/dist/reach.d.ts.map +1 -1
  135. package/dist/reach.js +167 -48
  136. package/dist/react.d.ts +3 -3
  137. package/dist/react.d.ts.map +1 -1
  138. package/dist/react.js +7 -17
  139. package/dist/readiness.d.ts +1 -1
  140. package/dist/readiness.d.ts.map +1 -1
  141. package/dist/reads.d.ts +11 -0
  142. package/dist/reads.d.ts.map +1 -0
  143. package/dist/reads.js +98 -0
  144. package/dist/recovery.d.ts +2 -2
  145. package/dist/recovery.d.ts.map +1 -1
  146. package/dist/recovery.js +8 -7
  147. package/dist/refresh.d.ts +12 -15
  148. package/dist/refresh.d.ts.map +1 -1
  149. package/dist/refresh.js +74 -51
  150. package/dist/replica.d.ts +55 -33
  151. package/dist/replica.d.ts.map +1 -1
  152. package/dist/replica.js +295 -186
  153. package/dist/room-draft.d.ts +15 -1
  154. package/dist/room-draft.d.ts.map +1 -1
  155. package/dist/room-draft.js +32 -25
  156. package/dist/rows.d.ts +29 -30
  157. package/dist/rows.d.ts.map +1 -1
  158. package/dist/rows.js +113 -64
  159. package/dist/save.d.ts +51 -14
  160. package/dist/save.d.ts.map +1 -1
  161. package/dist/save.js +518 -180
  162. package/dist/search.d.ts +19 -4
  163. package/dist/search.d.ts.map +1 -1
  164. package/dist/search.js +192 -35
  165. package/dist/seed.d.ts +16 -0
  166. package/dist/seed.d.ts.map +1 -0
  167. package/dist/seed.js +75 -0
  168. package/dist/shared.d.ts +8 -3
  169. package/dist/shared.d.ts.map +1 -1
  170. package/dist/shared.js +129 -66
  171. package/dist/store.d.ts +99 -71
  172. package/dist/store.d.ts.map +1 -1
  173. package/dist/store.js +461 -586
  174. package/dist/stored.d.ts +143 -0
  175. package/dist/stored.d.ts.map +1 -0
  176. package/dist/stored.js +561 -0
  177. package/dist/tables.d.ts +320 -0
  178. package/dist/tables.d.ts.map +1 -0
  179. package/dist/tables.js +537 -0
  180. package/dist/turn.d.ts +5 -3
  181. package/dist/turn.d.ts.map +1 -1
  182. package/dist/turn.js +51 -21
  183. package/dist/validation.d.ts +30 -0
  184. package/dist/validation.d.ts.map +1 -0
  185. package/dist/validation.js +162 -0
  186. package/dist/windows.d.ts +22 -0
  187. package/dist/windows.d.ts.map +1 -0
  188. package/dist/windows.js +89 -0
  189. package/dist/wire.d.ts +32 -0
  190. package/dist/wire.d.ts.map +1 -1
  191. package/dist/wire.js +8 -0
  192. package/package.json +10 -3
  193. package/dist/acquisition.d.ts +0 -14
  194. package/dist/acquisition.d.ts.map +0 -1
  195. package/dist/acquisition.js +0 -24
  196. package/dist/read-side.d.ts +0 -25
  197. package/dist/read-side.d.ts.map +0 -1
  198. package/dist/read-side.js +0 -642
@@ -1,36 +1,51 @@
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
- import { observing } from './observation.js';
4
- import { filterOf, identityOf, planOf, } from './query.js';
5
- import { createReadSide } from './read-side.js';
6
- import { markdownPath, roomOfKey } from './rows.js';
4
+ import { observing, whole } from './observation.js';
5
+ import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
6
+ import { createLiveQueries } from './live.js';
7
+ import { seededResult } from './seed.js';
8
+ import { documentKey, roomOfKey } from './rows.js';
7
9
  import { createSearchIndex, searchPlanOf, } from './search.js';
10
+ import { capped, MAXIMUM_WINDOW } from './windows.js';
8
11
  import { bootstrapRoom } from './bootstrap.js';
9
- import { changeSet, entering, mergeChangeSets } from './change-set.js';
12
+ import { changeSet, mergeChangeSets } from './change-set.js';
10
13
  import { createConversationStore, } from './conversation.js';
11
- import { queuedFile } from './change.js';
12
- 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';
13
16
  import { runtimeError } from './failure.js';
14
17
  import { createLog } from './log.js';
15
18
  import { binaryRecord } from './binary.js';
16
- import { absentBinary, binaryAnswer, 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';
17
21
  import { createRecovery } from './recovery.js';
18
- import { refreshRoom } from './refresh.js';
19
- import { buildBatch, joined, limitedIn, requeued } from './save.js';
20
- import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
21
- import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
22
+ import { doorReads } from './reads.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';
26
+ import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
22
27
  import { same, saving, STARTING, transferring, } from './status.js';
23
28
  import { detectStorageLoss, nextGeneration } from './durability.js';
24
- import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
25
29
  import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
26
30
  import { FencedMap } from './fence.js';
27
- import { tally } from './shared.js';
28
- 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';
29
34
  import { identityTime, roomDraftRevision } from './identity.js';
30
35
  import { beneath, normalize } from './paths.js';
31
36
  import { openRuntimeStore, } from './store.js';
37
+ import { changedCleanly, sameComposition, validatedCollection, } from './validation.js';
32
38
  import { unconfirmed } from './transfer.js';
33
39
  import { governs, holds, narrowed, planReach, } from './reach.js';
40
+ function roomsAlone(plan) {
41
+ return (plan.reach === null &&
42
+ plan.root.types === null &&
43
+ plan.root.where === null &&
44
+ plan.root.pathPrefix === null &&
45
+ plan.paths === null &&
46
+ plan.hop === null &&
47
+ plan.backlinks === null);
48
+ }
34
49
  function within(root, path) {
35
50
  return root === '' || beneath(root, path);
36
51
  }
@@ -48,19 +63,18 @@ const OPENING_LANE = 'opening';
48
63
  const CONVERSATION_LANE = 'conversations';
49
64
  const ROOM_READS = 256;
50
65
  const ROOM_ATTEMPTS = 16;
51
- const outside = new Map();
52
- function outsideOf(roots) {
53
- const key = JSON.stringify(roots);
54
- const held = outside.get(key);
55
- if (held !== undefined)
56
- return held;
57
- const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
58
- const made = {
59
- documents: (path) => (markdownPath(path) ? counted(path) : 0),
60
- binaries: counted,
61
- };
62
- outside.set(key, made);
63
- return made;
66
+ const SEARCH_BATCH = 256;
67
+ const ERASE_BATCH = 512;
68
+ const REDERIVE_BATCH = 256;
69
+ const WALK_BYTES = 1 << 20;
70
+ const QUERY_BYTES = 1 << 20;
71
+ const STAGE_BYTES = 1 << 20;
72
+ const CLEAN_CHANGES = 2_000;
73
+ // 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.
74
+ const LOCK_GRACE_MS = 120_000;
75
+ function operationOf(input) {
76
+ const operation = input?.operation_id;
77
+ return typeof operation === 'string' ? operation : null;
64
78
  }
65
79
  function abandonedOn(signal) {
66
80
  return new Promise((resolve) => {
@@ -73,8 +87,12 @@ function abandonedOn(signal) {
73
87
  function roomLane(locator) {
74
88
  return `room ${locator}`;
75
89
  }
90
+ function pendingKey(locator, id) {
91
+ return `${locator}\u0000${id}`;
92
+ }
76
93
  export function composeRuntime(options) {
77
94
  const localSaves = options.adapter.localSaves === true;
95
+ const roomTypes = roomTypesOf(options.adapter.roomTypes);
78
96
  let authorization = options.authorization;
79
97
  let opening;
80
98
  let store;
@@ -87,6 +105,8 @@ export function composeRuntime(options) {
87
105
  const replicas = new FencedMap(isFenced);
88
106
  const queuesWithoutReplica = new FencedMap(isFenced);
89
107
  const pendings = new Map();
108
+ const seeds = new Map();
109
+ const seedWatchers = new Map();
90
110
  const roomDrafts = new FencedMap(isFenced);
91
111
  const presentations = new Map();
92
112
  let limits = [];
@@ -99,6 +119,7 @@ export function composeRuntime(options) {
99
119
  const liveModels = new FencedMap(isModelFenced);
100
120
  const invalidModels = new Set();
101
121
  const discovered = new Map();
122
+ const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
102
123
  let roomsListed = false;
103
124
  const scopedRefusals = new Map();
104
125
  const sourceRefusals = new Map();
@@ -135,6 +156,7 @@ export function composeRuntime(options) {
135
156
  let handed = false;
136
157
  const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
137
158
  const repairing = new Set();
159
+ const calling = new Set();
138
160
  const repairable = new Map();
139
161
  let outcomeRetention = null;
140
162
  const answering = new Map();
@@ -145,6 +167,7 @@ export function composeRuntime(options) {
145
167
  let discovering = null;
146
168
  let opened = null;
147
169
  const owed = new Set();
170
+ const reloadsDue = new Map();
148
171
  const lanes = new Map();
149
172
  let roomReads = { generation, free: ROOM_READS, waiting: [] };
150
173
  const working = new Set();
@@ -156,10 +179,13 @@ export function composeRuntime(options) {
156
179
  const queueSubscriptions = new Set();
157
180
  const roomDraftListeners = new Set();
158
181
  const presentationListeners = new Set();
159
- const readSide = createReadSide();
182
+ const liveQueries = createLiveQueries((plan, cover) => answeredPlan(plan, cover));
160
183
  let withdrawConversations;
161
184
  let currencyQueued = null;
162
185
  let socketOpen = false;
186
+ const confirmed = new Set();
187
+ let lockGrace = null;
188
+ const lockListeners = new Set();
163
189
  let serviceMode = 'open';
164
190
  let haltedRefusal = false;
165
191
  let openSignals = 0;
@@ -167,12 +193,9 @@ export function composeRuntime(options) {
167
193
  let member = null;
168
194
  const memberListeners = new Set();
169
195
  const prepared = new Set();
170
- const projecting = new Set();
171
196
  const unpreparedDemand = new Set();
172
197
  const unindexable = new Set();
173
198
  let preparing = false;
174
- let demand = WHOLE_DEMAND;
175
- let narrowing = null;
176
199
  let targets = {};
177
200
  let persistence = options.shell.persistence;
178
201
  let capacity = null;
@@ -184,24 +207,54 @@ export function composeRuntime(options) {
184
207
  const transfers = new FencedMap(isFenced);
185
208
  const conversations = createConversationStore({
186
209
  agents: declaredAgents,
187
- documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
210
+ documents: (declaration, after) => rowsQuery(planOf({
211
+ rooms: [declaration.locator],
212
+ types: [declaration.type],
213
+ select: {
214
+ fields: [
215
+ declaration.identity,
216
+ declaration.name,
217
+ ...(declaration.retired === undefined ? [] : [declaration.retired]),
218
+ ].map((field) => `/${escapePointer(field)}`),
219
+ },
220
+ limit: MAXIMUM_WINDOW,
221
+ ...(after === undefined ? {} : { after }),
222
+ }, roomFacts())),
188
223
  call: (name, input) => invokeAction(name, input),
189
224
  durably,
190
225
  stream: () => options.adapter.conversations,
191
226
  publish: publishChangeSet,
192
- online: () => !status.providerUnreachable && refusal === null,
227
+ online: () => connectionOf() !== 'unreachable' && refusal === null,
193
228
  closed: () => closed,
194
229
  unanswered: resendLater,
195
230
  expired: (operation) => pastHorizon(operation),
196
231
  });
197
232
  const searchIndex = createSearchIndex({
198
- room: (room) => readSide.room(room),
199
- document: (room, path) => readSide.document(room, path),
233
+ async *room(room) {
234
+ const local = await openLocal();
235
+ for (let after = null;;) {
236
+ const key = replicas.get(room)?.record.key;
237
+ if (key === undefined)
238
+ return;
239
+ const page = await local.searched(key, room, after, SEARCH_BATCH, null);
240
+ yield page.documents;
241
+ if (page.next === null)
242
+ return;
243
+ after = page.next;
244
+ }
245
+ },
246
+ async documents(room, paths) {
247
+ const key = replicas.get(room)?.record.key;
248
+ if (key === undefined || paths.length === 0)
249
+ return [];
250
+ return (await (await openLocal()).searched(key, room, null, paths.length, paths)).documents;
251
+ },
200
252
  });
201
253
  const roomsToIndex = new Set();
202
254
  const conversationsToIndex = new Set();
203
255
  let indexing = false;
204
256
  const journal = createLog();
257
+ const keptFiles = openedFiles(options.database, isFenced);
205
258
  let notifyReady = () => { };
206
259
  const ready = new Promise((resolve) => {
207
260
  notifyReady = resolve;
@@ -219,16 +272,36 @@ export function composeRuntime(options) {
219
272
  if (room.size === 0)
220
273
  roomDrafts.delete(locator);
221
274
  }
222
- function roomDraftOf(record) {
275
+ async function visibleOf(locator, paths) {
276
+ const held = replicas.get(locator);
277
+ if (held === undefined)
278
+ return undefined;
279
+ const local = await openLocal();
280
+ const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
281
+ return { ...slice, collection: collectionOf(slice.documents, compositionOf(held.record)) };
282
+ }
283
+ function roomDraftOf(record, visible) {
223
284
  return structuredClone({
224
285
  room: record.locator,
225
286
  path: record.path,
226
287
  revision: record.revision,
227
288
  parts: record.parts,
228
- changedUnderneath: underneath(record.parts, record.path, replicas.get(record.locator)?.visible),
289
+ changedUnderneath: underneath(record.parts, record.path, visible),
229
290
  returned: record.returned,
230
291
  });
231
292
  }
293
+ async function roomDraftsOf(records) {
294
+ const rooms = new Map();
295
+ for (const record of records) {
296
+ const paths = rooms.get(record.locator) ?? [];
297
+ paths.push(record.path);
298
+ rooms.set(record.locator, paths);
299
+ }
300
+ const layers = new Map();
301
+ for (const [locator, paths] of rooms)
302
+ layers.set(locator, await visibleOf(locator, paths));
303
+ return records.map((record) => roomDraftOf(record, layers.get(record.locator)));
304
+ }
232
305
  function snapshotRoomDrafts() {
233
306
  const records = [...roomDrafts.values()].flatMap((room) => [...room.values()]);
234
307
  records.sort((left, right) => {
@@ -238,23 +311,38 @@ export function composeRuntime(options) {
238
311
  return left.path < right.path ? -1 : 1;
239
312
  return 0;
240
313
  });
241
- return records.map(roomDraftOf);
314
+ return roomDraftsOf(records);
242
315
  }
316
+ let draftsShown = Promise.resolve();
243
317
  function publishRoomDrafts(waitingOnly) {
244
318
  if (closed)
245
319
  return;
246
- const value = snapshotRoomDrafts();
247
- for (const listener of roomDraftListeners) {
248
- if (waitingOnly && listener.ready)
249
- continue;
250
- listener.ready = true;
251
- listener.notify(value);
252
- }
320
+ const snapshot = snapshotRoomDrafts();
321
+ draftsShown = draftsShown.then(async () => {
322
+ const value = await snapshot.catch(() => null);
323
+ if (value === null || closed)
324
+ return;
325
+ for (const listener of roomDraftListeners) {
326
+ if (waitingOnly && listener.ready)
327
+ continue;
328
+ listener.ready = true;
329
+ listener.notify(value);
330
+ }
331
+ });
253
332
  }
254
333
  function presentationOf(app) {
255
334
  const value = presentations.get(app);
256
335
  return value === undefined ? null : structuredClone(value);
257
336
  }
337
+ function keepPresentation(app, next) {
338
+ return exclusive(async () => {
339
+ const local = await openLocal();
340
+ const stored = next(presentations.get(app));
341
+ await local.present({ app, value: stored });
342
+ presentations.set(app, stored);
343
+ publishChangeSet(changeSet('', { presentation: app }));
344
+ });
345
+ }
258
346
  function emitPresentation(app) {
259
347
  if (closed)
260
348
  return;
@@ -303,21 +391,69 @@ export function composeRuntime(options) {
303
391
  return { attentionRequired, reset };
304
392
  }
305
393
  function documentsIn(locator) {
306
- const replica = replicas.get(locator);
307
- if (replica === undefined)
308
- return 0;
309
- const roots = replica.confirmed.record.typedSelection?.roots ?? [];
310
- const weighed = outsideOf(roots);
311
- return (tally(replica.visible.documents, weighed.documents) +
312
- tally(replica.visible.binaries, weighed.binaries));
394
+ return replicas.get(locator)?.count ?? 0;
313
395
  }
314
396
  function listed() {
315
397
  return structuredClone([...rooms.values()].map((room) => {
316
398
  const documents = documentsIn(room.locator);
317
399
  counted.set(room.locator, documents);
318
- return { ...room, documents };
400
+ return locked(room.locator)
401
+ ? {
402
+ ...room,
403
+ model: modelTypes(NO_MODEL),
404
+ typedSelection: null,
405
+ documents: 0,
406
+ locked: true,
407
+ }
408
+ : { ...room, documents, locked: false };
319
409
  }));
320
410
  }
411
+ function locked(locator) {
412
+ return rooms.get(locator)?.replica.offline === 'locked' && !confirmed.has(locator);
413
+ }
414
+ function lockedRooms() {
415
+ return [...rooms.keys()].filter(locked).sort();
416
+ }
417
+ function relocked(before) {
418
+ const after = lockedRooms();
419
+ if (JSON.stringify(after) === JSON.stringify(before))
420
+ return;
421
+ journal.record(`locked ${after.length}`);
422
+ emitRooms();
423
+ for (const notify of [...lockListeners])
424
+ notify();
425
+ }
426
+ function confirmLocks(listed) {
427
+ if (!socketOpen)
428
+ return;
429
+ for (const room of listed) {
430
+ if (room.replica.admission === 'admitted')
431
+ confirmed.add(room.locator);
432
+ }
433
+ }
434
+ function connectionLost() {
435
+ if (lockGrace !== null || closed)
436
+ return;
437
+ lockGrace = setTimeout(() => {
438
+ lockGrace = null;
439
+ if (socketOpen || closed)
440
+ return;
441
+ const before = lockedRooms();
442
+ confirmed.clear();
443
+ relocked(before);
444
+ }, LOCK_GRACE_MS);
445
+ }
446
+ function connectionLive() {
447
+ if (lockGrace !== null)
448
+ clearTimeout(lockGrace);
449
+ lockGrace = null;
450
+ }
451
+ function watchLocks(notify) {
452
+ lockListeners.add(notify);
453
+ return () => {
454
+ lockListeners.delete(notify);
455
+ };
456
+ }
321
457
  function roomFacts(reach = null) {
322
458
  const held = [...rooms.values()]
323
459
  .filter((room) => holds(reach, room.locator))
@@ -336,7 +472,7 @@ export function composeRuntime(options) {
336
472
  return { listed: roomsListed, rooms: held };
337
473
  }
338
474
  function emitRooms() {
339
- readSide.recover();
475
+ liveQueries.recover();
340
476
  queueReadiness();
341
477
  const value = listed();
342
478
  for (const listener of roomListeners)
@@ -366,7 +502,7 @@ export function composeRuntime(options) {
366
502
  if (closed)
367
503
  return;
368
504
  queueReadiness();
369
- const next = status.state === 'synced' && demand.conversations && !conversations.current()
505
+ const next = status.state === 'synced' && !conversations.current()
370
506
  ? { ...status, state: 'syncing' }
371
507
  : status;
372
508
  if (same(shown, next))
@@ -375,17 +511,17 @@ export function composeRuntime(options) {
375
511
  for (const listener of statusListeners)
376
512
  listener(shown);
377
513
  }
378
- function entryOf(batch) {
514
+ function entryOf(locator, batch) {
379
515
  if (localSaves)
380
516
  return Object.freeze({ batch, state: 'local' });
381
- const state = pendings.get(batch.id);
517
+ const state = pendings.get(pendingKey(locator, batch.id));
382
518
  if (state?.accepted !== undefined) {
383
519
  return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
384
520
  }
385
521
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
386
522
  }
387
523
  function entriesOf(locator) {
388
- return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
524
+ return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map((batch) => entryOf(locator, batch));
389
525
  }
390
526
  function emitQueue(locator) {
391
527
  const entries = entriesOf(locator);
@@ -395,55 +531,29 @@ export function composeRuntime(options) {
395
531
  }
396
532
  }
397
533
  function applyChangeSet(set) {
398
- const held = {
399
- ...set,
400
- documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
401
- };
402
- readSide.apply(held);
403
- searchIndex.changed(held);
534
+ liveQueries.touched(set);
535
+ searchIndex.changed(set);
404
536
  }
405
- async function referenced(locator) {
406
- for (;;) {
407
- const record = replicas.get(locator)?.confirmed.record;
408
- if (record === undefined || closed)
409
- return;
410
- const held = { text: record.documents, binaries: record.binaries };
411
- const composition = compositionOf(record);
412
- if (referencesIndexed(held, composition))
413
- return;
414
- await indexReferrers(held, composition);
415
- }
537
+ function unpublished(error) {
538
+ journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
416
539
  }
417
540
  async function prepareReplica(locator) {
418
- await referenced(locator);
419
- const replica = replicas.get(locator);
420
- if (replica === undefined || closed || prepared.has(locator))
421
- return;
422
- const visible = replica.visible;
423
- const paths = [
424
- ...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
425
- ];
426
- const entered = [];
427
- projecting.add(locator);
428
- try {
429
- await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
430
- const current = replicas.get(locator);
431
- if (current === undefined || closed)
432
- return;
433
- const set = entering(locator, current, chunk);
434
- readSide.apply(set);
435
- entered.push(set);
436
- });
437
- if (!replicas.has(locator) || closed)
541
+ for (;;) {
542
+ const held = replicas.get(locator);
543
+ if (held === undefined || closed || prepared.has(locator))
438
544
  return;
545
+ const key = held.record.key;
546
+ const done = await exclusive(async () => (await openLocal()).rederive(key, REDERIVE_BATCH));
547
+ if (replicas.get(locator)?.record.key !== key)
548
+ continue;
549
+ if (!done)
550
+ continue;
439
551
  prepared.add(locator);
552
+ publishChangeSet(changeSet(locator, { documents: null }));
553
+ liveQueries.recover();
554
+ queueReadiness();
555
+ return;
440
556
  }
441
- finally {
442
- projecting.delete(locator);
443
- }
444
- conversations.changed(mergeChangeSets(entered));
445
- readSide.recover();
446
- queueReadiness();
447
557
  }
448
558
  function unprepared() {
449
559
  return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
@@ -474,17 +584,22 @@ export function composeRuntime(options) {
474
584
  }
475
585
  })();
476
586
  }
587
+ function roomsOfReach(reach) {
588
+ return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
589
+ }
477
590
  function reachedRooms(plan) {
478
- const reach = plan.reach;
479
- const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
591
+ const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
480
592
  const named = plan.rooms ??
481
593
  (reached === null
482
594
  ? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
483
595
  : [...reached]);
484
- return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
596
+ return [...new Set(named)]
597
+ .filter((locator) => (reached === null || reached.has(locator)) && !plan.locked.includes(locator))
598
+ .sort();
485
599
  }
486
600
  function awaited(locator) {
487
- return rooms.has(locator) || !roomsListed;
601
+ return (!roomsListed ||
602
+ (rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
488
603
  }
489
604
  function covered(plan) {
490
605
  const locators = reachedRooms(plan);
@@ -494,6 +609,7 @@ export function composeRuntime(options) {
494
609
  const pending = [];
495
610
  const coverage = [];
496
611
  const overlay = [];
612
+ const read = [];
497
613
  for (const locator of locators) {
498
614
  const replica = replicas.get(locator);
499
615
  if (replica === undefined) {
@@ -507,15 +623,16 @@ export function composeRuntime(options) {
507
623
  }
508
624
  coverage.push({
509
625
  room: locator,
510
- generation: replica.confirmed.record.transfer,
626
+ generation: replica.record.transfer,
511
627
  current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
512
628
  });
629
+ read.push({ locator, key: replica.record.key });
513
630
  for (const batch of replica.queue)
514
631
  overlay.push(batch.id);
515
632
  }
516
633
  if (pending.length > 0)
517
634
  return { pending };
518
- return { coverage: { rooms: coverage, indexes: [], overlay } };
635
+ return { coverage: { rooms: coverage, indexes: [], overlay }, rooms: read };
519
636
  }
520
637
  function awaitPreparation(cover) {
521
638
  if (!('pending' in cover))
@@ -524,61 +641,11 @@ export function composeRuntime(options) {
524
641
  if (waiting.length > 0)
525
642
  prepare(waiting.map(({ room }) => room));
526
643
  }
527
- function acquires(room) {
528
- return (demanded(demand, room) ||
529
- (replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
530
- queuesWithoutReplica.has(room.locator));
531
- }
532
- function acquiredLocators() {
533
- return [...discovered.values()]
534
- .filter(acquires)
535
- .map((room) => room.locator)
536
- .sort();
537
- }
538
- function heldDemand(acquired) {
539
- if (demand.whole)
540
- return null;
541
- return { rooms: acquired.map((room) => room.locator), conversations: demand.conversations };
542
- }
543
- function applyDemand(next) {
544
- const before = acquiredLocators();
545
- const conversed = demand.conversations;
546
- demand = next;
547
- const after = acquiredLocators();
548
- queueReadiness();
549
- if (!conversed && next.conversations)
550
- scheduleCurrency();
551
- if (JSON.stringify(before) === JSON.stringify(after) && conversed === next.conversations)
552
- return;
553
- journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
554
- if (options.adapter.roomStream === undefined)
555
- void sync('unstreamed');
556
- else if (socketOpen)
557
- options.adapter.roomStream.reopen();
558
- }
559
- function acquire(next) {
560
- if (closed)
561
- return;
562
- if (narrowing !== null)
563
- clearTimeout(narrowing);
564
- narrowing = null;
565
- const widening = wider(next, demand);
566
- if (!widening && !narrower(next, demand))
567
- return;
568
- if (widening || !roomsListed) {
569
- applyDemand(next);
570
- return;
571
- }
572
- // A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
573
- narrowing = setTimeout(() => {
574
- narrowing = null;
575
- applyDemand(next);
576
- }, 5_000);
577
- }
578
- function requiredRooms(reach) {
644
+ function requiredRooms(given) {
645
+ const reach = planReach(given);
579
646
  if (reach === null)
580
647
  return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
581
- const named = [...new Set(reach.rooms)].sort();
648
+ const named = [...roomsOfReach(reach)].sort();
582
649
  if (roomsListed)
583
650
  return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
584
651
  return { listed: named.every((room) => replicas.has(room)), rooms: named };
@@ -677,7 +744,7 @@ export function composeRuntime(options) {
677
744
  function establish(name) {
678
745
  targets = { ...targets, [name]: { established: true } };
679
746
  queueReadiness();
680
- void durably((local) => local.keepTargets(targets)).catch(() => {
747
+ void durably((local) => local.fact('targets', targets)).catch(() => {
681
748
  journal.record(`target record failed ${name}`);
682
749
  });
683
750
  }
@@ -714,18 +781,12 @@ export function composeRuntime(options) {
714
781
  readinessWatches.delete(watch);
715
782
  };
716
783
  }
717
- function layerBytes(layer) {
784
+ function pageBytes(page) {
718
785
  let bytes = 0;
719
- for (const [path, text] of Object.entries(layer.documents))
720
- bytes += (path.length + text.length) * 2;
786
+ for (const { path, content } of page.entries)
787
+ bytes += (path.length + (content?.length ?? 0)) * 2;
721
788
  return bytes;
722
789
  }
723
- function grownBy(next, previous) {
724
- if (previous === undefined)
725
- return layerBytes(next);
726
- const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
727
- return layerBytes({ ...next, documents });
728
- }
729
790
  function exceeded(error) {
730
791
  return (typeof error === 'object' &&
731
792
  error !== null &&
@@ -733,7 +794,7 @@ export function composeRuntime(options) {
733
794
  }
734
795
  async function estimated() {
735
796
  const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
736
- return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
797
+ return estimate === null ? null : Math.max(0, estimate.total - estimate.usage);
737
798
  }
738
799
  function pause(locator, cause) {
739
800
  paused.add(locator);
@@ -754,7 +815,8 @@ export function composeRuntime(options) {
754
815
  const available = await estimated().catch(() => null);
755
816
  if (available === null || available < (held.needed ?? 0))
756
817
  return;
757
- if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
818
+ if (held.cause === 'storage-exceeded' &&
819
+ (held.available === null || available <= held.available))
758
820
  return;
759
821
  resume();
760
822
  }
@@ -772,10 +834,7 @@ export function composeRuntime(options) {
772
834
  void sync('unstreamed');
773
835
  }
774
836
  async function reclaimed(local) {
775
- const unreferenced = [...transfers.keys()].filter((locator) => {
776
- const room = discovered.get(locator);
777
- return room === undefined || !acquires(room);
778
- });
837
+ const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
779
838
  for (const locator of unreferenced) {
780
839
  await local.abandon(locator);
781
840
  transfers.delete(locator);
@@ -799,20 +858,156 @@ export function composeRuntime(options) {
799
858
  }
800
859
  async function exceededOn(locator, bytes) {
801
860
  const available = await estimated().catch(() => null);
802
- pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
861
+ pause(locator, { cause: 'storage-exceeded', needed: bytes, available });
862
+ }
863
+ function seedRoomHeld(room, version) {
864
+ return prepared.has(room) && reachedBy(room, version);
865
+ }
866
+ function seedHeld(at) {
867
+ return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
868
+ }
869
+ function seedApplies(plan) {
870
+ const seed = seeds.get(plan.key);
871
+ if (seed === undefined)
872
+ return null;
873
+ if (!seedHeld(seed.at))
874
+ return seed;
875
+ seeds.delete(plan.key);
876
+ return null;
877
+ }
878
+ function seedBehind(plan, cover) {
879
+ const seed = seedApplies(plan);
880
+ if (seed === null || 'pending' in cover)
881
+ return cover;
882
+ return {
883
+ pending: Object.entries(seed.at)
884
+ .filter(([room, version]) => !seedRoomHeld(room, version))
885
+ .map(([room]) => ({ room, awaits: 'acquisition' })),
886
+ };
887
+ }
888
+ function seed(entry) {
889
+ if (seedHeld(entry.at))
890
+ return;
891
+ for (const { query, rows } of entry.queries) {
892
+ const key = queryKey(query);
893
+ seeds.set(key, { rows, at: entry.at });
894
+ for (const shown of seedWatchers.get(key) ?? [])
895
+ shown();
896
+ }
897
+ }
898
+ function sameCover(left, right) {
899
+ return JSON.stringify(left) === JSON.stringify(right);
900
+ }
901
+ async function coveredRows(plan, cover) {
902
+ for (;;) {
903
+ const before = cover();
904
+ if ('pending' in before)
905
+ return { pending: before };
906
+ const local = await openLocal();
907
+ const keys = before.rooms.map(({ key }) => key);
908
+ const changes = local.changes(keys);
909
+ const current = () => local.changes(keys) === changes;
910
+ const rows = await windowRows(local, plan, before.rooms, current);
911
+ const after = cover();
912
+ if (rows !== null && sameCover(before, after))
913
+ return { rows, cover: before, current };
914
+ }
915
+ }
916
+ async function windowRows(local, plan, rooms, unchanged) {
917
+ const first = await local.query(plan, rooms, QUERY_BYTES);
918
+ if (first.more === undefined || !('documents' in first)) {
919
+ const { more: _more, ...rows } = first;
920
+ return rows;
921
+ }
922
+ const documents = [...first.documents];
923
+ for (let more = first.more; more !== undefined;) {
924
+ if (!unchanged())
925
+ return null;
926
+ const page = await local.query({
927
+ ...plan,
928
+ after: { text: more, values: JSON.parse(more) },
929
+ limit: plan.limit === null ? null : plan.limit - documents.length,
930
+ total: false,
931
+ }, rooms, QUERY_BYTES);
932
+ if (!('documents' in page))
933
+ return null;
934
+ documents.push(...page.documents);
935
+ more = page.more;
936
+ if (more === undefined) {
937
+ return {
938
+ documents,
939
+ ...(page.next === undefined ? {} : { next: page.next }),
940
+ ...(first.total === undefined ? {} : { total: first.total }),
941
+ };
942
+ }
943
+ }
944
+ return null;
945
+ }
946
+ function keyedOf(rows, coverage) {
947
+ if ('groups' in rows) {
948
+ return {
949
+ result: { ...rows, coverage },
950
+ keys: rows.groups.map((group) => JSON.stringify(group.key)),
951
+ };
952
+ }
953
+ return {
954
+ result: { ...rows, coverage },
955
+ keys: rows.documents.map((document) => documentKey(document.room, document.path)),
956
+ };
957
+ }
958
+ async function answeredPlan(plan, cover) {
959
+ const answered = await coveredRows(plan, cover);
960
+ if ('pending' in answered) {
961
+ const pending = answered.pending;
962
+ return { result: pending, keys: [], current: () => true };
963
+ }
964
+ const held = answered.cover;
965
+ return { ...keyedOf(answered.rows, held.coverage), current: answered.current };
803
966
  }
804
967
  async function rowsQuery(plan) {
805
968
  const before = covered(plan);
806
969
  awaitPreparation(before);
970
+ const seeded = seedApplies(plan);
971
+ if (seeded !== null)
972
+ return seededResult(seeded).result;
807
973
  if ('pending' in before)
808
974
  return before;
809
- const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
810
- awaitPreparation(cover);
811
- if ('pending' in cover)
975
+ const answered = await coveredRows(plan, () => {
976
+ const cover = covered(plan);
977
+ awaitPreparation(cover);
812
978
  return cover;
813
- return 'groups' in rows
814
- ? { groups: rows.groups, coverage: cover.coverage }
815
- : { documents: rows.documents, coverage: cover.coverage };
979
+ });
980
+ if ('pending' in answered)
981
+ return answered.pending;
982
+ const held = answered.cover;
983
+ return { ...answered.rows, coverage: held.coverage };
984
+ }
985
+ async function everyRow(plan) {
986
+ const window = { ...plan, limit: MAXIMUM_WINDOW, after: null, total: false };
987
+ const first = await rowsQuery(window);
988
+ if ('pending' in first || first.next === undefined)
989
+ return first;
990
+ const documents = 'documents' in first ? [...first.documents] : [];
991
+ const groups = 'groups' in first ? [...first.groups] : [];
992
+ for (let next = first.next; next !== undefined;) {
993
+ const values = JSON.parse(next);
994
+ const page = await rowsQuery({
995
+ ...window,
996
+ after: { text: next, values: values.length === 0 ? null : values },
997
+ });
998
+ if ('pending' in page)
999
+ return page;
1000
+ if ('documents' in page)
1001
+ documents.push(...page.documents);
1002
+ else
1003
+ groups.push(...page.groups);
1004
+ if (JSON.stringify(page.coverage) !== JSON.stringify(first.coverage))
1005
+ return everyRow(plan);
1006
+ next = page.next;
1007
+ }
1008
+ return 'documents' in first
1009
+ ? { documents, coverage: first.coverage }
1010
+ : { groups, coverage: first.coverage };
816
1011
  }
817
1012
  async function indexConversation(conversation) {
818
1013
  searchIndex.indexMessages(conversation, [], false);
@@ -870,7 +1065,10 @@ export function composeRuntime(options) {
870
1065
  for (const locator of set.queues)
871
1066
  emitQueue(locator);
872
1067
  if (set.drafts.length > 0 ||
873
- set.documents.some((change) => roomDrafts.get(change.locator)?.has(change.path) === true)) {
1068
+ set.documents.some((change) => {
1069
+ const held = roomDrafts.get(change.locator);
1070
+ return held !== undefined && (change.path === null || held.has(change.path));
1071
+ })) {
874
1072
  publishRoomDrafts(false);
875
1073
  }
876
1074
  for (const app of set.presentations)
@@ -883,9 +1081,21 @@ export function composeRuntime(options) {
883
1081
  if (reason === 'revoked-authorization') {
884
1082
  await exclusive(async () => {
885
1083
  const revoked = { status: 'unavailable', reason: 'revoked' };
1084
+ const records = [...replicas].map(([locator, held]) => ({
1085
+ locator,
1086
+ held,
1087
+ record: { ...held.record, model: revoked, selectionModel: revoked },
1088
+ }));
886
1089
  try {
887
1090
  const local = await openLocal();
888
- await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
1091
+ await local.commit({
1092
+ rooms: records.map(({ locator, record }) => ({
1093
+ locator,
1094
+ key: record.key,
1095
+ record: storedRecord(record),
1096
+ composition: compositionOf(record),
1097
+ })),
1098
+ });
889
1099
  }
890
1100
  catch {
891
1101
  journal.record('model revocation persistence failed');
@@ -901,16 +1111,16 @@ export function composeRuntime(options) {
901
1111
  liveModels.delete(key);
902
1112
  }
903
1113
  const changed = [];
904
- for (const [locator, room] of rooms) {
905
- const before = replicas.get(locator) ?? null;
906
- if (before !== null) {
907
- const record = {
908
- ...before.confirmed.record,
909
- model: revoked,
910
- selectionModel: revoked,
911
- };
912
- replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
913
- }
1114
+ for (const { locator, held, record } of records) {
1115
+ if (replicas.get(locator) !== held)
1116
+ continue;
1117
+ recomposed(locator, held.record, record);
1118
+ replicas.set(locator, { ...held, record });
1119
+ }
1120
+ for (const locator of [...rooms.keys()]) {
1121
+ const room = rooms.get(locator);
1122
+ if (room === undefined)
1123
+ continue;
914
1124
  rooms.set(locator, {
915
1125
  ...room,
916
1126
  model: modelTypes(revoked),
@@ -918,9 +1128,10 @@ export function composeRuntime(options) {
918
1128
  ? null
919
1129
  : { ...room.typedSelection, model: modelTypes(revoked) },
920
1130
  });
921
- changed.push(changeSet(locator, before, replicas.get(locator) ?? null, { room: true }));
1131
+ changed.push(changeSet(locator, { documents: null, room: true }));
922
1132
  }
923
1133
  publishChangeSet(mergeChangeSets(changed));
1134
+ prepare();
924
1135
  });
925
1136
  }
926
1137
  }
@@ -951,41 +1162,57 @@ export function composeRuntime(options) {
951
1162
  model: modelTypes(modelOf(room.locator, 'typed-selection')),
952
1163
  },
953
1164
  providerOwnedPaths: room.providerOwnedPaths,
954
- roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
1165
+ roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
955
1166
  synchronizationAvailable,
956
1167
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
957
1168
  });
958
1169
  }
959
- async function applyDiscovery(found, started) {
1170
+ async function applyDiscovery(found, started, covered) {
1171
+ const lockedBefore = lockedRooms();
960
1172
  for (const room of found) {
961
1173
  const fence = fences.get(room.locator);
962
1174
  if (fence !== undefined && fence.erased && fence.at < started)
963
1175
  fences.delete(room.locator);
964
1176
  }
1177
+ await loseRooms(found
1178
+ .filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
1179
+ .map((room) => room.locator));
965
1180
  const listed = found.filter((room) => !fences.has(room.locator));
966
1181
  const listedLocators = new Set(listed.map((room) => room.locator));
967
1182
  for (const locator of listedLocators)
968
1183
  scopedRefusals.delete(locator);
969
- discovered.clear();
1184
+ if (covered === undefined)
1185
+ discovered.clear();
1186
+ else
1187
+ for (const locator of covered)
1188
+ discovered.delete(locator);
970
1189
  for (const room of listed)
971
1190
  discovered.set(room.locator, room);
972
- const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
1191
+ const unlisted = unlistedHeld(new Set(found.map((room) => room.locator))).filter((locator) => covered === undefined || covered.includes(locator));
973
1192
  if (options.adapter.exhaustiveDiscovery === true)
974
1193
  await loseRooms(unlisted);
975
1194
  else
976
1195
  unlistedKept(unlisted);
977
1196
  for (const room of listed) {
978
- const held = replicas.get(room.locator)?.confirmed.record;
1197
+ const held = replicas.get(room.locator)?.record;
979
1198
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
980
1199
  list(held, false);
981
1200
  continue;
982
1201
  }
983
1202
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
984
1203
  }
1204
+ confirmLocks(listed);
985
1205
  roomsListed = true;
986
1206
  emitRooms();
1207
+ relocked(lockedBefore);
987
1208
  return listed;
988
1209
  }
1210
+ function heldLocally(locator) {
1211
+ return (replicas.has(locator) ||
1212
+ roomDrafts.has(locator) ||
1213
+ transfers.has(locator) ||
1214
+ queuesWithoutReplica.has(locator));
1215
+ }
989
1216
  function unlistedHeld(listed) {
990
1217
  return [
991
1218
  ...new Set([
@@ -998,7 +1225,7 @@ export function composeRuntime(options) {
998
1225
  }
999
1226
  function unlistedKept(unlisted) {
1000
1227
  for (const locator of unlisted) {
1001
- const held = replicas.get(locator)?.confirmed.record;
1228
+ const held = replicas.get(locator)?.record;
1002
1229
  if (held !== undefined)
1003
1230
  list(held, false);
1004
1231
  else
@@ -1007,16 +1234,16 @@ export function composeRuntime(options) {
1007
1234
  }
1008
1235
  function fence(locators) {
1009
1236
  const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1010
- const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
1011
- room: true,
1012
- queue: true,
1237
+ const removed = fresh.map((locator) => ({
1238
+ locator,
1013
1239
  drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
1014
1240
  }));
1015
1241
  for (const locator of fresh) {
1016
1242
  fences.set(locator, { at: discoveries, erased: false });
1017
1243
  lastFenced.set(locator, ++fencings);
1244
+ confirmed.delete(locator);
1018
1245
  for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1019
- pendings.delete(batch.id);
1246
+ pendings.delete(pendingKey(locator, batch.id));
1020
1247
  replicas.delete(locator);
1021
1248
  queuesWithoutReplica.delete(locator);
1022
1249
  rooms.delete(locator);
@@ -1030,6 +1257,7 @@ export function composeRuntime(options) {
1030
1257
  repairable.delete(locator);
1031
1258
  unindexable.delete(locator);
1032
1259
  paused.delete(locator);
1260
+ prepared.delete(locator);
1033
1261
  scopedRefusals.delete(locator);
1034
1262
  sourceRefusals.delete(locator);
1035
1263
  declarationRefusals.delete(locator);
@@ -1043,26 +1271,49 @@ export function composeRuntime(options) {
1043
1271
  }
1044
1272
  if (fresh.length === 0)
1045
1273
  return fresh;
1046
- publishChangeSet(mergeChangeSets(removed));
1047
- for (const locator of fresh)
1048
- prepared.delete(locator);
1049
1274
  searchIndex.forget(fresh);
1050
- readSide.recover();
1275
+ publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, { documents: null, room: true, queue: true, drafts }))));
1276
+ liveQueries.recover();
1051
1277
  moveTo({ ...status, ...attention() });
1052
1278
  return fresh;
1053
1279
  }
1054
- async function erased(local, locators) {
1055
- await local.erase(locators);
1280
+ async function eraseFenced(locators) {
1056
1281
  for (const locator of locators) {
1282
+ while (await durably((local) => local.erase(locator, ERASE_BATCH)))
1283
+ await yielded();
1057
1284
  const held = fences.get(locator);
1058
1285
  if (held !== undefined)
1059
1286
  held.erased = true;
1060
1287
  }
1061
1288
  }
1289
+ let sweeping = false;
1290
+ function sweep() {
1291
+ if (sweeping || closed)
1292
+ return;
1293
+ sweeping = true;
1294
+ void (async () => {
1295
+ try {
1296
+ while (!closed && (await durably((local) => local.erase(null, ERASE_BATCH)))) {
1297
+ await yielded();
1298
+ }
1299
+ }
1300
+ catch {
1301
+ journal.record('erasure failed');
1302
+ }
1303
+ finally {
1304
+ sweeping = false;
1305
+ }
1306
+ })();
1307
+ }
1062
1308
  async function loseRooms(locators) {
1063
1309
  const fresh = fence(locators);
1064
- if (fresh.length > 0)
1065
- await durably((local) => erased(local, fresh));
1310
+ if (fresh.length === 0)
1311
+ return;
1312
+ await durably(async (local) => {
1313
+ await local.fence(fresh);
1314
+ await unpair(local);
1315
+ });
1316
+ await eraseFenced(fresh);
1066
1317
  }
1067
1318
  function restored(key, persisted) {
1068
1319
  const prior = models.get(key);
@@ -1083,54 +1334,58 @@ export function composeRuntime(options) {
1083
1334
  return model;
1084
1335
  }
1085
1336
  async function hydrate(opened) {
1086
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
1087
- opened.all(),
1337
+ const [held, queues, kept, heldConversations, chatDrafts] = await Promise.all([
1338
+ opened.opened(),
1088
1339
  opened.queues(),
1089
1340
  opened.roomDrafts(),
1090
- opened.presentation(),
1091
1341
  opened.conversations(),
1092
1342
  opened.chatDrafts(),
1093
- opened.lastCompleted(),
1094
- opened.providerLimits(),
1095
- opened.capabilities(),
1096
- opened.member(),
1097
- opened.transfers(),
1098
- opened.targets(),
1099
1343
  ]);
1100
- targets = heldTargets;
1344
+ const records = [...held.rooms.values()];
1345
+ const counts = await opened.counts(records.map(({ key, typedSelection }) => ({ key, roots: typedSelection?.roots ?? [] })));
1346
+ const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1347
+ targets = held.targets;
1101
1348
  transfers.clear();
1102
- for (const record of staged)
1103
- transfers.set(record.locator, record);
1104
- limits = declared;
1105
- capabilities = named;
1106
- member = known;
1349
+ for (const [locator, record] of held.transfers)
1350
+ transfers.set(locator, record);
1351
+ limits = held.providerLimits;
1352
+ capabilities = held.capabilities;
1353
+ member = held.member;
1107
1354
  roomDrafts.clear();
1108
1355
  for (const record of kept)
1109
1356
  rememberRoomDraft(record);
1110
1357
  presentations.clear();
1111
- for (const record of presented)
1358
+ for (const record of held.presentation)
1112
1359
  presentations.set(record.app, record.value);
1113
- conversations.hydrate(held, chatDrafts);
1114
- const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1360
+ conversations.hydrate(heldConversations, chatDrafts);
1115
1361
  pendings.clear();
1116
1362
  for (const queue of queues) {
1117
- for (const { batch, state } of queue.pending)
1118
- pendings.set(batch.id, state);
1363
+ for (const { batch, state } of queue.pending) {
1364
+ pendings.set(pendingKey(queue.locator, batch.id), state);
1365
+ }
1119
1366
  }
1120
- const previousReplicas = new Map(replicas);
1121
1367
  replicas.clear();
1368
+ prepared.clear();
1122
1369
  queuesWithoutReplica.clear();
1123
- const replicated = new Set(records.map((record) => record.locator));
1124
1370
  for (const [locator, queue] of queuedBatches) {
1125
- if (!replicated.has(locator))
1371
+ if (!held.rooms.has(locator))
1126
1372
  queuesWithoutReplica.set(locator, queue);
1127
1373
  }
1128
- for (const record of records) {
1129
- const model = restored(modelKey(record.locator, 'collection'), record.model);
1130
- const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
1131
- const prior = previousReplicas.get(record.locator);
1132
- const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
1133
- replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
1374
+ records.forEach((stored, index) => {
1375
+ if (fences.has(stored.locator))
1376
+ return;
1377
+ const model = restored(modelKey(stored.locator, 'collection'), stored.model);
1378
+ const selectionModel = restored(modelKey(stored.locator, 'typed-selection'), stored.selectionModel);
1379
+ const record = { ...stored, model, selectionModel };
1380
+ replicas.set(record.locator, {
1381
+ record,
1382
+ queue: queuedBatches.get(record.locator) ?? [],
1383
+ overlaid: held.overlaid.get(record.key) ?? new Set(),
1384
+ count: counts[index] ?? 0,
1385
+ confirmed: 0,
1386
+ });
1387
+ if (!held.unprepared.has(record.key))
1388
+ prepared.add(record.locator);
1134
1389
  const existing = rooms.get(record.locator);
1135
1390
  if (existing === undefined)
1136
1391
  list(record, false);
@@ -1149,11 +1404,15 @@ export function composeRuntime(options) {
1149
1404
  },
1150
1405
  });
1151
1406
  }
1152
- }
1407
+ });
1408
+ for (const key of held.orphans)
1409
+ await opened.release(key);
1410
+ if (held.erasing || held.orphans.length > 0)
1411
+ queueMicrotask(sweep);
1153
1412
  return {
1154
- lastCompleted,
1413
+ lastCompleted: held.lastCompleted,
1155
1414
  locators: new Set([...queuedBatches.keys(), ...replicas.keys()]),
1156
- read: mergeChangeSets([...new Set([...previousReplicas.keys(), ...replicas.keys()])].map((locator) => changeSet(locator, previousReplicas.get(locator) ?? null, replicas.get(locator) ?? null))),
1415
+ read: mergeChangeSets([...replicas.keys()].map((locator) => changeSet(locator, { documents: null }))),
1157
1416
  };
1158
1417
  }
1159
1418
  async function readState(opened) {
@@ -1172,7 +1431,7 @@ export function composeRuntime(options) {
1172
1431
  async function open() {
1173
1432
  if (closed)
1174
1433
  throw new Error('runtime closed');
1175
- const opened = await openRuntimeStore(isFenced);
1434
+ const opened = await openRuntimeStore(isFenced, options.database);
1176
1435
  store = opened;
1177
1436
  const interrupted = await opened.fences();
1178
1437
  for (const locator of interrupted) {
@@ -1192,9 +1451,17 @@ export function composeRuntime(options) {
1192
1451
  emitStorageLoss();
1193
1452
  }
1194
1453
  const lastCompleted = await readState(opened);
1195
- if (interrupted.length > 0)
1196
- await erased(opened, interrupted);
1454
+ if (interrupted.length > 0) {
1455
+ queueMicrotask(() => {
1456
+ void eraseFenced(interrupted).catch(() => {
1457
+ journal.record('erasure failed');
1458
+ });
1459
+ });
1460
+ }
1197
1461
  moveTo({ ...status, lastCompleted, ...attention() });
1462
+ void exclusive(() => unpair(opened)).catch(() => {
1463
+ journal.record('unpair failed');
1464
+ });
1198
1465
  return opened;
1199
1466
  }
1200
1467
  function durably(work) {
@@ -1215,78 +1482,272 @@ export function composeRuntime(options) {
1215
1482
  mutations = next.then(() => undefined, () => undefined);
1216
1483
  return next;
1217
1484
  }
1218
- function withdrawable(batch) {
1219
- const state = pendings.get(batch.id);
1220
- return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
1485
+ function withdrawable(locator) {
1486
+ return (batch) => {
1487
+ const state = pendings.get(pendingKey(locator, batch.id));
1488
+ return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
1489
+ };
1490
+ }
1491
+ const REWRITE_LINKS = ['field', 'body'];
1492
+ const CHECKED_LINKS = ['field', 'section'];
1493
+ async function sliceAt(record, view, queue, extra = [], moved = []) {
1494
+ const local = await openLocal();
1495
+ const needed = new Set([...touchedBy(queue), ...extra]);
1496
+ const movedPaths = [...new Set([...movedBy(queue, record.locator), ...moved])];
1497
+ if (movedPaths.length > 0) {
1498
+ for (const { path } of await local.referrers(record.key, view, movedPaths, REWRITE_LINKS)) {
1499
+ needed.add(path);
1500
+ }
1501
+ }
1502
+ const paths = [...needed];
1503
+ return stepped(slicing(await local.entries(record.key, view, paths), paths));
1221
1504
  }
1222
- async function settle(local, locator, before, confirmed, queue, settling = {}) {
1505
+ async function landedWhole(locator, batch, confirmedLayer, current) {
1506
+ if (!landed(batch, confirmedLayer, current))
1507
+ return false;
1508
+ const partner = partnerOf(batch, locator);
1509
+ if (partner === null)
1510
+ return true;
1511
+ const other = replicas.get(partner);
1512
+ const half = other?.queue.find((one) => one.id === batch.id);
1513
+ if (other === undefined || half === undefined)
1514
+ return false;
1515
+ return landed(half, await sliceAt(other.record, 'confirmed', [half]));
1516
+ }
1517
+ function carriedBack(locator, unlanded, written) {
1518
+ const back = new Map();
1519
+ const taken = [];
1520
+ for (const batch of unlanded) {
1521
+ const between = batch.between;
1522
+ if (between === undefined)
1523
+ continue;
1524
+ const source = between.source.room;
1525
+ const destination = between.destination.room;
1526
+ for (const { path: to } of pendings.get(pendingKey(destination, batch.id))?.drafts ?? []) {
1527
+ const record = roomDrafts.get(destination)?.get(to);
1528
+ const path = between.moves.find((move) => move.to === to)?.path;
1529
+ if (record === undefined || path === undefined)
1530
+ continue;
1531
+ const [into, key] = source === locator ? [written, path] : [back, documentKey(source, path)];
1532
+ const existing = into.get(key) ?? roomDrafts.get(source)?.get(path);
1533
+ taken.push({ locator: destination, path: to });
1534
+ into.set(key, {
1535
+ locator: source,
1536
+ path,
1537
+ revision: roomDraftRevision(),
1538
+ parts: returnedInto(existing?.parts, record.parts),
1539
+ returned: existing?.returned ?? null,
1540
+ });
1541
+ }
1542
+ }
1543
+ const records = [...back.values()];
1544
+ const elsewhere = taken.filter((one) => one.locator !== locator);
1545
+ const rooms = new Set([...records, ...elsewhere].map((one) => one.locator));
1546
+ return {
1547
+ taken: taken.filter((one) => one.locator === locator).map(({ path }) => path),
1548
+ writes: [...rooms].map((room) => ({
1549
+ locator: room,
1550
+ roomDrafts: records.filter((record) => record.locator === room),
1551
+ consumedRoomDrafts: elsewhere.filter((one) => one.locator === room).map(({ path }) => path),
1552
+ })),
1553
+ changes: [...rooms].map((room) => changeSet(room, {
1554
+ drafts: [...records, ...elsewhere]
1555
+ .filter((one) => one.locator === room)
1556
+ .map(({ path }) => path),
1557
+ })),
1558
+ remember() {
1559
+ for (const one of taken)
1560
+ forgetRoomDraft(one.locator, one.path);
1561
+ for (const record of records)
1562
+ rememberRoomDraft(record);
1563
+ },
1564
+ };
1565
+ }
1566
+ function recomposed(locator, before, after) {
1567
+ if (before !== undefined &&
1568
+ before.key === after.key &&
1569
+ fingerprint(compositionOf(before)) === fingerprint(compositionOf(after))) {
1570
+ return;
1571
+ }
1572
+ if (before !== undefined && before.key === after.key)
1573
+ prepared.delete(locator);
1574
+ }
1575
+ function committedRoom(locator, record, answer) {
1576
+ if (answer.prepared[locator] === false)
1577
+ prepared.delete(locator);
1578
+ else if (answer.prepared[locator] === true)
1579
+ prepared.add(locator);
1580
+ if (!prepared.has(locator))
1581
+ prepare([locator]);
1582
+ return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0 };
1583
+ }
1584
+ function replayed(record, confirmedLayer, queue) {
1585
+ return stepped(replaying(confirmedLayer, queue, record.locator, compositionOf(record)));
1586
+ }
1587
+ async function overlayDiff(record, before, visible, touched) {
1588
+ const kept = before !== undefined && before.record.key === record.key
1589
+ ? before.overlaid
1590
+ : new Set();
1591
+ return {
1592
+ overlay: {
1593
+ set: await stepped(overlayOf(visible, touched)),
1594
+ cleared: [...kept].filter((path) => !touched.has(path)),
1595
+ },
1596
+ touched,
1597
+ };
1598
+ }
1599
+ async function settle(local, locator, before, record, queue, settling = {}) {
1600
+ return together(local, [await settlement(local, locator, before, record, queue, settling)]);
1601
+ }
1602
+ function unpairedIn(locator, batch) {
1603
+ if (!queueIn(locator).some((held) => held.id === batch.id))
1604
+ return false;
1605
+ if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
1606
+ return false;
1607
+ return !leavingIn(batch, locator) || withdrawable(locator)(batch);
1608
+ }
1609
+ async function together(local, settlements) {
1610
+ const settled = new Set(settlements.map(({ locator }) => locator));
1611
+ const all = [...settlements];
1612
+ for (let wave = settlements; wave.length > 0;) {
1613
+ const partnered = new Map();
1614
+ for (const { locator, left } of wave) {
1615
+ for (const batch of left) {
1616
+ const partner = partnerOf(batch, locator);
1617
+ if (partner === null || settled.has(partner) || !unpairedIn(partner, batch))
1618
+ continue;
1619
+ partnered.set(partner, [...(partnered.get(partner) ?? []), batch]);
1620
+ }
1621
+ }
1622
+ const partners = [];
1623
+ for (const [partner, batches] of partnered) {
1624
+ settled.add(partner);
1625
+ partners.push(await unpairing(local, partner, batches));
1626
+ }
1627
+ all.push(...partners);
1628
+ wave = partners;
1629
+ }
1630
+ const answer = await local.commit({
1631
+ rooms: all.flatMap(({ commit }) => commit.rooms),
1632
+ writes: all.flatMap(({ commit }) => commit.writes),
1633
+ });
1634
+ return mergeChangeSets(all.map((one) => one.apply(answer)));
1635
+ }
1636
+ async function settlement(local, locator, before, record, queue, settling = {}) {
1637
+ const view = settling.view ?? 'confirmed';
1223
1638
  const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
1224
- const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
1225
- const kept = queue.filter((batch) => !withdrawn.includes(batch));
1226
- const unqueued = withdrawn.map((batch) => ({
1227
- batch,
1228
- returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
1229
- }));
1639
+ const overrides = settling.changes;
1640
+ const read = await sliceAt(record, view, queue, [
1641
+ ...(before !== undefined && before.record.key === record.key ? before.overlaid : []),
1642
+ ...(overrides?.set.map(({ path }) => path) ?? []),
1643
+ ...(overrides?.removed ?? []),
1644
+ ]);
1645
+ const confirmedLayer = overrides === undefined
1646
+ ? read
1647
+ : await stepped(slicingWith(read, overrides.set, overrides.removed));
1648
+ const composition = compositionOf(record);
1649
+ const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
1650
+ const founded = kept.filter((batch) => !queue.includes(batch));
1651
+ const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
1652
+ const unqueued = [];
1653
+ for (const batch of withdrawn) {
1654
+ unqueued.push({ batch, landed: await landedWhole(locator, batch, confirmedLayer) });
1655
+ }
1230
1656
  const returning = [
1231
- ...[...unqueued].reverse(),
1232
- ...(settling.leaving === undefined ? [] : [settling.leaving]),
1657
+ ...[...unqueued].reverse().map(({ batch, landed: held }) => ({
1658
+ batch,
1659
+ returned: localSaves || held ? null : returnedBy('withdrawn'),
1660
+ })),
1661
+ ...(settling.leaving ?? []),
1233
1662
  ];
1234
1663
  const written = new Map();
1235
- for (const { batch, returned } of returning) {
1236
- if (returned === null)
1664
+ for (const { batch, returned: reason } of returning) {
1665
+ if (reason === null || batch.between?.destination.room === locator)
1237
1666
  continue;
1238
- for (const saved of pendings.get(batch.id)?.drafts ?? []) {
1667
+ for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
1239
1668
  const existing = written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path);
1240
1669
  written.set(saved.path, {
1241
1670
  locator,
1242
1671
  path: saved.path,
1243
1672
  revision: roomDraftRevision(),
1244
1673
  parts: returnedInto(existing?.parts, saved.parts),
1245
- returned,
1674
+ returned: reason,
1246
1675
  });
1247
1676
  }
1248
1677
  }
1249
- await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
1250
- for (const batch of previousQueue) {
1251
- if (!kept.includes(batch))
1252
- pendings.delete(batch.id);
1253
- }
1254
- for (const record of written.values())
1255
- rememberRoomDraft(record);
1256
- try {
1257
- await local.write({
1258
- next: confirmed.record,
1259
- previous: before?.confirmed.record ?? null,
1260
- ...(settling.stored === undefined ? {} : { stored: settling.stored }),
1261
- });
1262
- }
1263
- catch (error) {
1264
- const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
1265
- if (held !== null)
1266
- replicas.set(locator, held);
1267
- else if (kept.length > 0)
1268
- queuesWithoutReplica.set(locator, kept);
1269
- else
1678
+ const carried = carriedBack(locator, [
1679
+ ...[...unqueued].reverse().flatMap(({ batch, landed: held }) => (held ? [] : [batch])),
1680
+ ...(settling.leaving ?? []).flatMap(({ batch, returned }) => returned === null ? [] : [batch]),
1681
+ ], written);
1682
+ const replayedLayer = await replayed(record, confirmedLayer, kept);
1683
+ const { overlay, touched } = await overlayDiff(record, before, replayedLayer.visible, replayedLayer.touched);
1684
+ const commit = {
1685
+ rooms: [
1686
+ {
1687
+ locator,
1688
+ key: record.key,
1689
+ record: storedRecord(record),
1690
+ roots: record.typedSelection?.roots ?? [],
1691
+ ...settling.commit,
1692
+ ...(overrides === undefined
1693
+ ? {}
1694
+ : { confirmed: { set: overrides.set, removed: overrides.removed } }),
1695
+ overlay,
1696
+ },
1697
+ ],
1698
+ writes: [
1699
+ {
1700
+ locator,
1701
+ queue: kept,
1702
+ founded,
1703
+ roomDrafts: [...written.values()],
1704
+ consumedRoomDrafts: carried.taken,
1705
+ },
1706
+ ...carried.writes,
1707
+ ],
1708
+ };
1709
+ const staying = new Set(kept.map((batch) => batch.id));
1710
+ return {
1711
+ locator,
1712
+ left: previousQueue.filter((batch) => !staying.has(batch.id)),
1713
+ commit,
1714
+ apply: (answer) => {
1715
+ for (const batch of previousQueue) {
1716
+ if (!staying.has(batch.id))
1717
+ pendings.delete(pendingKey(locator, batch.id));
1718
+ }
1719
+ for (const draft of written.values())
1720
+ rememberRoomDraft(draft);
1721
+ carried.remember();
1722
+ const drafts = [...written.keys(), ...carried.taken];
1270
1723
  queuesWithoutReplica.delete(locator);
1271
- publishChangeSet(changeSet(locator, before ?? null, held, {
1272
- queue: kept.length !== previousQueue.length,
1273
- drafts: [...written.keys()],
1274
- }));
1275
- moveTo({ ...status, ...attention() });
1276
- throw error;
1277
- }
1278
- return () => {
1279
- queuesWithoutReplica.delete(locator);
1280
- for (const { batch, returned } of unqueued) {
1281
- const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
1282
- journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1283
- }
1284
- replicas.set(locator, replicaOf(confirmed, kept, before));
1285
- return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
1286
- room: settling.room,
1287
- queue: kept.length !== previousQueue.length,
1288
- drafts: [...written.keys()],
1289
- });
1724
+ for (const { batch, landed: held } of unqueued) {
1725
+ const outcome = localSaves ? 'dropped' : held ? 'landed' : 'withdrawn';
1726
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1727
+ }
1728
+ const changed = answer.changed[locator];
1729
+ const confirmedChanged = settling.commit?.published !== undefined ||
1730
+ settling.commit?.refreshed === true ||
1731
+ overrides !== undefined;
1732
+ recomposed(locator, before?.record, record);
1733
+ const { count } = committedRoom(locator, record, answer);
1734
+ replicas.set(locator, {
1735
+ record,
1736
+ queue: kept,
1737
+ overlaid: touched,
1738
+ count,
1739
+ confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
1740
+ });
1741
+ return mergeChangeSets([
1742
+ changeSet(locator, {
1743
+ documents: changed === undefined ? [] : changed,
1744
+ room: settling.room,
1745
+ queue: queueChanged,
1746
+ drafts,
1747
+ }),
1748
+ ...carried.changes,
1749
+ ]);
1750
+ },
1290
1751
  };
1291
1752
  }
1292
1753
  function adoptModels(locator, record) {
@@ -1307,7 +1768,7 @@ export function composeRuntime(options) {
1307
1768
  const taken = (answer, previous, attached) => {
1308
1769
  if (answer === undefined)
1309
1770
  return previous;
1310
- const { state, refused } = attachedModel(answer, previous);
1771
+ const { state, refused } = attachedModel(answer, previous, attached === 'collection' ? roomTypes : NO_ROOM_TYPES);
1311
1772
  if (refused !== null) {
1312
1773
  const where = refused.location === undefined ? '' : ` ${refused.location}`;
1313
1774
  journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
@@ -1320,47 +1781,57 @@ export function composeRuntime(options) {
1320
1781
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
1321
1782
  };
1322
1783
  }
1323
- function publish(local, room, confirmed, landing, live) {
1324
- const { base, stored } = landing;
1784
+ function publish(local, room, record, landing, live) {
1325
1785
  return exclusive(async () => {
1326
- if (stored !== undefined) {
1327
- await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
1328
- }
1329
1786
  if (!live()) {
1330
1787
  caughtUpTo(room.locator);
1331
1788
  return true;
1332
1789
  }
1333
1790
  const prior = replicas.get(room.locator);
1334
- if (base !== undefined &&
1335
- (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
1336
- prior.confirmed.record.revisions !== base.record.revisions)) {
1791
+ if (landing.kind === 'refreshed' &&
1792
+ (prior === undefined || prior.record.key !== record.key || prior.confirmed !== landing.base)) {
1793
+ await local.unrefresh(record.key);
1337
1794
  return false;
1338
1795
  }
1339
- adoptModels(room.locator, confirmed.record);
1796
+ adoptModels(room.locator, record);
1340
1797
  const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
1341
- const roomVersion = confirmed.record.roomVersion;
1798
+ const roomVersion = record.roomVersion;
1342
1799
  const accepted = queue.filter((batch) => {
1343
- const accepted = pendings.get(batch.id)?.accepted;
1344
- return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
1800
+ const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
1801
+ return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
1345
1802
  });
1346
- let settled;
1803
+ let set;
1347
1804
  try {
1348
- settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
1805
+ set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
1806
+ ? {
1807
+ room: true,
1808
+ view: 'refreshed',
1809
+ commit: { refreshed: true, composition: compositionOf(record) },
1810
+ }
1811
+ : {
1812
+ room: true,
1813
+ view: landing.refreshed ? 'refreshed' : 'confirmed',
1814
+ commit: {
1815
+ published: { from: prior?.record.key ?? null },
1816
+ composition: compositionOf(record),
1817
+ ...(landing.refreshed ? { refreshed: true } : {}),
1818
+ },
1819
+ });
1349
1820
  }
1350
1821
  catch (error) {
1351
1822
  if (!exceeded(error))
1352
1823
  throw error;
1353
- await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
1824
+ await exceededOn(room.locator, null);
1354
1825
  caughtUpTo(room.locator);
1355
1826
  return true;
1356
1827
  }
1357
- const set = settled();
1358
- if (stored !== undefined) {
1828
+ if (landing.kind === 'bootstrapped') {
1359
1829
  transfers.delete(room.locator);
1360
1830
  catching.delete(room.locator);
1831
+ sweep();
1361
1832
  }
1362
1833
  for (const batch of accepted)
1363
- pendings.delete(batch.id);
1834
+ pendings.delete(pendingKey(room.locator, batch.id));
1364
1835
  stale.delete(room.locator);
1365
1836
  unindexable.delete(room.locator);
1366
1837
  if (unreadable.delete(room.locator))
@@ -1368,22 +1839,27 @@ export function composeRuntime(options) {
1368
1839
  sourceRefusals.delete(room.locator);
1369
1840
  declarationRefusals.delete(room.locator);
1370
1841
  list(room, true);
1371
- if (prior === undefined)
1372
- prepared.add(room.locator);
1373
1842
  publishChangeSet(set);
1843
+ await unpair(local);
1374
1844
  moveTo({ ...status, ...attention() });
1375
1845
  options.adapter.roomStream?.published(room.locator, roomVersion);
1846
+ if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1847
+ submitLater(room.locator);
1848
+ }
1376
1849
  return true;
1377
1850
  });
1378
1851
  }
1379
- async function saveRoomDrafts(locator, named, writtenBy) {
1852
+ async function savedRoom(locator) {
1380
1853
  const local = await openLocal();
1381
- await referenced(locator);
1382
1854
  const replica = replicas.get(locator);
1383
1855
  const room = rooms.get(locator);
1384
1856
  if (replica === undefined || room === undefined) {
1385
1857
  throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1386
1858
  }
1859
+ return { local, replica, room };
1860
+ }
1861
+ async function saveRoomDrafts(locator, named, writtenBy) {
1862
+ const target = await savedRoom(locator);
1387
1863
  const saved = [];
1388
1864
  for (const { path, revision } of named) {
1389
1865
  const record = roomDrafts.get(locator)?.get(path);
@@ -1393,12 +1869,396 @@ export function composeRuntime(options) {
1393
1869
  if (!saved.some((draft) => draft.path === path))
1394
1870
  saved.push({ path, parts: record.parts });
1395
1871
  }
1396
- const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
1872
+ return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
1873
+ }
1874
+ async function saveParts(locator, named, writtenBy) {
1875
+ const read = named.map(({ path, parts }) => ({ path, parts: readParts(parts) }));
1876
+ if (read.some(({ path }, at) => path === '' ||
1877
+ normalize(path) !== path ||
1878
+ read.findIndex((one) => one.path === path) !== at)) {
1879
+ throw runtimeError('room-draft-malformed');
1880
+ }
1881
+ const target = await savedRoom(locator);
1882
+ const saved = [];
1883
+ const files = [];
1884
+ const filed = read.filter(({ parts }) => parts.file !== undefined).map(({ path }) => path);
1885
+ const visible = filed.length === 0 ? undefined : await visibleOf(locator, filed);
1886
+ for (const { path, parts } of read) {
1887
+ const file = parts.file === undefined
1888
+ ? undefined
1889
+ : await heldFile(parts.file.blob, binaryBase(visible, path));
1890
+ if (parts.file !== undefined && file !== undefined) {
1891
+ files.push({ key: file.localId, blob: parts.file.blob });
1892
+ }
1893
+ saved.push({ path, parts: held(undefined, parts, file) });
1894
+ }
1895
+ return committed(locator, target, saved, new Set(), writtenBy, files);
1896
+ }
1897
+ function carriedDrafts(locator, saved, consumed) {
1898
+ const holding = roomDrafts.get(locator);
1899
+ return new Map(saved.flatMap(({ path, parts }) => {
1900
+ const record = holding?.get(path);
1901
+ if (parts.move === undefined || record === undefined || consumed.has(path))
1902
+ return [];
1903
+ const to = moveTarget(parts.move);
1904
+ if (to === path || holding?.has(to) === true)
1905
+ return [];
1906
+ return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
1907
+ }));
1908
+ }
1909
+ async function savingIn(replica, changes, extra, leaving = []) {
1910
+ const record = replica.record;
1911
+ const local = await openLocal();
1912
+ const paths = [
1913
+ ...extra,
1914
+ ...changes.flatMap((change) => change.kind === 'move' ? [change.path, change.to] : [change.path]),
1915
+ ];
1916
+ const moved = [
1917
+ ...leaving,
1918
+ ...changes.flatMap((change) => (change.kind === 'move' ? [change.path] : [])),
1919
+ ];
1920
+ const removed = changes.flatMap((change) => (change.kind === 'remove' ? [change.path] : []));
1921
+ const referring = removed.length === 0
1922
+ ? []
1923
+ : (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
1924
+ const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
1925
+ const { visible } = await replayed(record, confirmedLayer, replica.queue);
1926
+ const composition = compositionOf(record);
1927
+ return {
1928
+ record,
1929
+ confirmed: confirmedLayer,
1930
+ visible,
1931
+ *visibleOver(queue) {
1932
+ return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
1933
+ },
1934
+ };
1935
+ }
1936
+ async function unmodeledIn(record) {
1937
+ const composition = compositionFor(record);
1938
+ if (available(composition))
1939
+ return false;
1940
+ const local = await openLocal();
1941
+ const roots = record.typedSelection?.roots ?? [];
1942
+ if (composition.model.status === 'unavailable' &&
1943
+ (await local.typedWithin(record.key, 'visible', roots, false))) {
1944
+ return true;
1945
+ }
1946
+ return (composition.selection !== null &&
1947
+ composition.selection.model.status === 'unavailable' &&
1948
+ (await local.typedWithin(record.key, 'visible', roots, true)));
1949
+ }
1950
+ function textDiffers(left, right) {
1951
+ return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
1952
+ !sameBinary(left?.binary ?? undefined, right?.binary ?? undefined));
1953
+ }
1954
+ function textOf(entry) {
1955
+ if (entry === null)
1956
+ return undefined;
1957
+ return entry.binary !== null ? null : entry.content;
1958
+ }
1959
+ function walkOf(local, key, view) {
1960
+ return async (after) => {
1961
+ const page = await local.walk(key, view, after, WALK_BYTES);
1962
+ return {
1963
+ entries: page.entries.map(({ path, content, binary }) => ({
1964
+ path,
1965
+ content: binary !== null ? null : content,
1966
+ })),
1967
+ next: page.next,
1968
+ };
1969
+ };
1970
+ }
1971
+ function lookupOf(local, key, view) {
1972
+ return async (paths) => {
1973
+ if (paths.length === 0)
1974
+ return new Map();
1975
+ return new Map((await local.typed(key, view, paths)).map(({ path, type, binary }) => [
1976
+ path,
1977
+ { type: type ?? undefined, binary },
1978
+ ]));
1979
+ };
1980
+ }
1981
+ function declaredOf(entry) {
1982
+ if (entry.binary !== null)
1983
+ return { type: undefined, binary: true };
1984
+ const read = parseFrontmatter(entry.content ?? '');
1985
+ return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
1986
+ }
1987
+ function overriddenWalk(walk, overrides) {
1988
+ const sorted = [...overrides.keys()].sort();
1989
+ const beyond = (path) => {
1990
+ if (path === null)
1991
+ return 0;
1992
+ let low = 0;
1993
+ let high = sorted.length;
1994
+ while (low < high) {
1995
+ const middle = (low + high) >>> 1;
1996
+ if (sorted[middle] <= path)
1997
+ low = middle + 1;
1998
+ else
1999
+ high = middle;
2000
+ }
2001
+ return low;
2002
+ };
2003
+ return async (after) => {
2004
+ const page = await walk(after);
2005
+ const end = page.next;
2006
+ const added = sorted
2007
+ .slice(beyond(after), end === null ? sorted.length : beyond(end))
2008
+ .flatMap((path) => {
2009
+ const entry = overrides.get(path);
2010
+ return entry === null || entry === undefined
2011
+ ? []
2012
+ : [{ path, content: entry.binary !== null ? null : entry.content }];
2013
+ });
2014
+ const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
2015
+ return { entries, next: end };
2016
+ };
2017
+ }
2018
+ function overriddenLookup(lookup, overrides) {
2019
+ return async (paths) => {
2020
+ const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
2021
+ for (const path of paths) {
2022
+ const entry = overrides.get(path);
2023
+ if (entry === undefined)
2024
+ continue;
2025
+ if (entry === null)
2026
+ known.delete(path);
2027
+ else
2028
+ known.set(path, declaredOf(entry));
2029
+ }
2030
+ return known;
2031
+ };
2032
+ }
2033
+ function validatorOf(record, confirmedLayer) {
2034
+ return async (resulting, touched) => {
2035
+ const composition = compositionFor(record);
2036
+ const local = await openLocal();
2037
+ const changed = [];
2038
+ const overrides = new Map();
2039
+ const pace = paced();
2040
+ for (const path of new Set([...confirmedLayer.held, ...touched])) {
2041
+ await pace(ROW_WEIGHT);
2042
+ const was = entryAt(confirmedLayer, path);
2043
+ const now = entryAt(resulting, path);
2044
+ if (!textDiffers(was, now))
2045
+ continue;
2046
+ overrides.set(path, now);
2047
+ if (textOf(was) !== textOf(now))
2048
+ changed.push({ path, was: textOf(was), now: textOf(now) });
2049
+ }
2050
+ const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
2051
+ const clean = available(composition) &&
2052
+ changed.length <= CLEAN_CHANGES &&
2053
+ (await changedCleanly(changed, compositionOf(record), composition, {
2054
+ lookup,
2055
+ texts: textsOf(local, record.key, 'confirmed'),
2056
+ referrers: referrersOf(local, record.key, 'confirmed'),
2057
+ }));
2058
+ if (clean)
2059
+ return { verdict: 'accept' };
2060
+ return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
2061
+ };
2062
+ }
2063
+ function textsOf(local, key, view) {
2064
+ return async (paths) => new Map((await local.entries(key, view, paths)).map(({ path, content, binary }) => [
2065
+ path,
2066
+ binary !== null ? null : content,
2067
+ ]));
2068
+ }
2069
+ function referrersOf(local, key, view) {
2070
+ return async (targets) => [
2071
+ ...new Set((await local.referrers(key, view, targets, CHECKED_LINKS)).map(({ path }) => path)),
2072
+ ];
2073
+ }
2074
+ async function sideOf(locator, replica, room, saving) {
2075
+ const record = replica.record;
2076
+ return {
2077
+ room: locator,
2078
+ handle: room.handle,
2079
+ baseRoomVersion: record.roomVersion,
2080
+ composition: compositionFor(record),
2081
+ operations: room.operations,
2082
+ providerOwnedPaths: room.providerOwnedPaths,
2083
+ visible: saving.visible,
2084
+ queued: queuedDocuments(replica.queue),
2085
+ unmodeled: await unmodeledIn(record),
2086
+ validate: validatorOf(record, saving.confirmed),
2087
+ };
2088
+ }
2089
+ function movedInto(locator, saved) {
2090
+ const into = saved.find(({ parts }) => parts.move?.room !== undefined && parts.move.room !== locator);
2091
+ return into?.parts.move?.room ?? null;
2092
+ }
2093
+ async function saveCommit(local, files, rooms) {
2094
+ const touched = new Map();
2095
+ const commits = [];
2096
+ for (const { locator, saving, before, queue } of rooms) {
2097
+ const replayedLayer = await replayed(saving.record, saving.confirmed, queue);
2098
+ const diff = await overlayDiff(saving.record, before, replayedLayer.visible, replayedLayer.touched);
2099
+ touched.set(locator, diff.touched);
2100
+ commits.push({
2101
+ locator,
2102
+ key: saving.record.key,
2103
+ roots: saving.record.typedSelection?.roots ?? [],
2104
+ overlay: diff.overlay,
2105
+ });
2106
+ }
2107
+ try {
2108
+ const answer = await local.commit({
2109
+ rooms: commits,
2110
+ writes: rooms.map(({ write }) => write),
2111
+ files,
2112
+ whole: true,
2113
+ });
2114
+ return { answer, touched };
2115
+ }
2116
+ catch {
2117
+ return null;
2118
+ }
2119
+ }
2120
+ function savedReplica(locator, before, queue, touched, answer) {
2121
+ replicas.set(locator, {
2122
+ ...before,
2123
+ queue,
2124
+ overlaid: touched,
2125
+ count: answer.counts[locator] ?? before.count,
2126
+ });
2127
+ }
2128
+ async function committedMove(locator, destination, { local, replica, room }, saved, consumed, writtenBy, files) {
2129
+ const mixed = saved.find(({ parts }) => Object.keys(parts).length !== 1 || parts.move?.room !== destination);
2130
+ if (mixed !== undefined) {
2131
+ return { outcome: 'refused', code: 'mixed-room-move', retryable: false, file: mixed.path };
2132
+ }
2133
+ const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
2134
+ const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
2135
+ const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, sourceSaving.visible)));
1397
2136
  if (changed.length > 0)
1398
2137
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
1399
- const parts = saved.map(({ path, parts }) => changesOf(path, parts));
1400
- const changes = parts.flatMap(({ changes }) => changes);
1401
- const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
2138
+ const target = replicas.get(destination);
2139
+ const listedRoom = rooms.get(destination);
2140
+ if (target === undefined || listedRoom === undefined) {
2141
+ throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
2142
+ }
2143
+ const arriving = await savingIn(target, [], moves.map(({ to }) => to));
2144
+ const built = await buildMove({
2145
+ source: await sideOf(locator, replica, room, sourceSaving),
2146
+ destination: await sideOf(destination, target, listedRoom, arriving),
2147
+ moves,
2148
+ providerLimits: limits,
2149
+ local: localSaves,
2150
+ ...(writtenBy === undefined ? {} : { writtenBy }),
2151
+ });
2152
+ if (built.kind === 'refused')
2153
+ return { outcome: 'refused', ...built.refusal };
2154
+ const id = built.leaving.id;
2155
+ const carrying = saved.flatMap(({ path, parts }) => {
2156
+ const record = roomDrafts.get(locator)?.get(path);
2157
+ const to = moveTarget(parts.move);
2158
+ if (record === undefined || consumed.has(path) || roomDrafts.get(destination)?.has(to)) {
2159
+ return [];
2160
+ }
2161
+ const moved = {
2162
+ ...record,
2163
+ locator: destination,
2164
+ path: to,
2165
+ revision: roomDraftRevision(),
2166
+ };
2167
+ return [{ from: path, record: moved }];
2168
+ });
2169
+ const carried = carrying.map(({ record }) => record);
2170
+ const marked = carried.map(({ path }) => ({ path, parts: {} }));
2171
+ const left = [...consumed, ...carrying.map(({ from }) => from)];
2172
+ const leavingQueue = [...replica.queue, built.leaving];
2173
+ const arrivingQueue = [...target.queue, built.arriving];
2174
+ const committedBoth = await saveCommit(local, files, [
2175
+ {
2176
+ locator,
2177
+ saving: sourceSaving,
2178
+ before: replica,
2179
+ queue: leavingQueue,
2180
+ write: {
2181
+ locator,
2182
+ queue: leavingQueue,
2183
+ saved: new Map([[id, saved]]),
2184
+ consumedRoomDrafts: left,
2185
+ },
2186
+ },
2187
+ {
2188
+ locator: destination,
2189
+ saving: arriving,
2190
+ before: target,
2191
+ queue: arrivingQueue,
2192
+ write: {
2193
+ locator: destination,
2194
+ queue: arrivingQueue,
2195
+ saved: new Map([[id, marked]]),
2196
+ roomDrafts: carried,
2197
+ },
2198
+ },
2199
+ ]);
2200
+ if (committedBoth === null)
2201
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
2202
+ const { answer, touched } = committedBoth;
2203
+ if (answer.fenced !== null) {
2204
+ throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2205
+ }
2206
+ if (fences.has(locator) || fences.has(destination)) {
2207
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2208
+ }
2209
+ pendings.set(pendingKey(locator, id), {
2210
+ drafts: saved,
2211
+ submission: { state: 'waiting' },
2212
+ lost: false,
2213
+ });
2214
+ pendings.set(pendingKey(destination, id), {
2215
+ drafts: marked,
2216
+ submission: { state: 'waiting' },
2217
+ lost: false,
2218
+ });
2219
+ for (const path of left)
2220
+ forgetRoomDraft(locator, path);
2221
+ for (const record of carried)
2222
+ rememberRoomDraft(record);
2223
+ savedReplica(locator, replica, leavingQueue, touched.get(locator), answer);
2224
+ savedReplica(destination, target, arrivingQueue, touched.get(destination), answer);
2225
+ publishChangeSet(mergeChangeSets([
2226
+ changeSet(locator, {
2227
+ documents: answer.changed[locator] ?? [],
2228
+ queue: true,
2229
+ drafts: left,
2230
+ }),
2231
+ changeSet(destination, {
2232
+ documents: answer.changed[destination] ?? [],
2233
+ queue: true,
2234
+ drafts: carried.map(({ path }) => path),
2235
+ }),
2236
+ ]));
2237
+ queuedSave(locator);
2238
+ return { outcome: 'saved', identifier: id };
2239
+ }
2240
+ function queuedSave(locator) {
2241
+ if (!localSaves)
2242
+ recorded = false;
2243
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
2244
+ submitLater(locator);
2245
+ }
2246
+ async function committed(locator, target, saved, consumed, writtenBy, files = []) {
2247
+ const into = movedInto(locator, saved);
2248
+ if (into !== null) {
2249
+ return committedMove(locator, into, target, saved, consumed, writtenBy, files);
2250
+ }
2251
+ const { local, replica, room } = target;
2252
+ const parts = saved.map(({ path, parts: held }) => changesOf(path, held));
2253
+ const changes = parts.flatMap(({ changes: made }) => made);
2254
+ const saving = await savingIn(replica, changes, [
2255
+ ...saved.map(({ path }) => path),
2256
+ ...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
2257
+ ]);
2258
+ const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, underneath(held, path, saving.visible)));
2259
+ if (changed.length > 0)
2260
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2261
+ const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
1402
2262
  if (requeue.kind === 'refused')
1403
2263
  return { outcome: 'refused', ...requeue.refusal };
1404
2264
  const foreign = [...requeue.withdrawn.keys()].some((origin) => {
@@ -1416,23 +2276,23 @@ export function composeRuntime(options) {
1416
2276
  const withdrawing = requeue.withdrawn.size > 0;
1417
2277
  const input = {
1418
2278
  local: localSaves,
1419
- baseRoomVersion: replica.confirmed.record.roomVersion,
1420
- composition: compositionFor(replica.confirmed.record),
2279
+ baseRoomVersion: replica.record.roomVersion,
2280
+ composition: compositionFor(replica.record),
1421
2281
  operations: room.operations,
1422
2282
  providerOwnedPaths: room.providerOwnedPaths,
1423
2283
  providerLimits: limits,
1424
2284
  handle: room.handle,
1425
- visible: replica.visible,
2285
+ visible: saving.visible,
1426
2286
  changes,
1427
2287
  bases: parts.flatMap(({ bases }) => bases),
2288
+ queued: queuedDocuments(replica.queue),
1428
2289
  writtenBy,
1429
- clean: {
1430
- text: replica.confirmed.record.documents,
1431
- binaries: replica.confirmed.record.binaries,
1432
- composition: compositionOf(replica.confirmed.record),
1433
- },
2290
+ unmodeled: await unmodeledIn(replica.record),
2291
+ validate: validatorOf(replica.record, saving.confirmed),
1434
2292
  };
1435
- const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
2293
+ const checked = withdrawing
2294
+ ? await buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
2295
+ : null;
1436
2296
  if (checked?.kind === 'refused')
1437
2297
  return { outcome: 'refused', ...checked.refusal };
1438
2298
  const owners = [...requeue.owners].flatMap((origin) => {
@@ -1448,13 +2308,14 @@ export function composeRuntime(options) {
1448
2308
  };
1449
2309
  }
1450
2310
  const built = !withdrawing
1451
- ? buildBatch(input)
2311
+ ? await buildBatch(input)
1452
2312
  : requeue.changes.length === 0
1453
2313
  ? null
1454
- : buildBatch({
2314
+ : await buildBatch({
1455
2315
  ...input,
1456
- visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
2316
+ visible: await stepped(saving.visibleOver(requeue.queue)),
1457
2317
  changes: requeue.changes,
2318
+ queued: queuedDocuments(requeue.queue),
1458
2319
  });
1459
2320
  if (built?.kind === 'refused')
1460
2321
  return { outcome: 'refused', ...built.refusal };
@@ -1471,7 +2332,7 @@ export function composeRuntime(options) {
1471
2332
  for (const [origin, replaced] of withdrawn) {
1472
2333
  if (replaced === null)
1473
2334
  continue;
1474
- const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
2335
+ const carried = withdrawnDrafts(pendings.get(pendingKey(locator, origin))?.drafts ?? [], changes);
1475
2336
  drafted.set(replaced.id, replaced === join?.batch
1476
2337
  ? [
1477
2338
  ...carried.map((held) => joinedDraft(held, saved)),
@@ -1485,59 +2346,77 @@ export function composeRuntime(options) {
1485
2346
  for (const replaced of withdrawn.values()) {
1486
2347
  if (replaced === null || !batches.includes(replaced))
1487
2348
  continue;
1488
- const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
2349
+ const limited = await stepped(limiting(batches, replaced, limits, saving.visibleOver));
1489
2350
  if (limited !== null)
1490
2351
  return { outcome: 'refused', ...limited };
1491
2352
  }
1492
- const consumed = saved.map(({ path }) => path);
1493
- try {
1494
- await local.keepAuthored({
2353
+ const carried = carriedDrafts(locator, saved, consumed);
2354
+ const left = [...consumed, ...carried.keys()];
2355
+ const arrived = [...carried.values()];
2356
+ const done = await saveCommit(local, files, [
2357
+ {
1495
2358
  locator,
2359
+ saving,
2360
+ before: replica,
1496
2361
  queue: batches,
1497
- saved: drafted,
1498
- consumedRoomDrafts: consumed,
1499
- });
1500
- }
1501
- catch {
2362
+ write: {
2363
+ locator,
2364
+ queue: batches,
2365
+ saved: drafted,
2366
+ consumedRoomDrafts: left,
2367
+ roomDrafts: arrived,
2368
+ },
2369
+ },
2370
+ ]);
2371
+ if (done === null)
1502
2372
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2373
+ const { answer, touched } = done;
2374
+ if (answer.fenced !== null) {
2375
+ throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
1503
2376
  }
1504
2377
  if (fences.has(locator))
1505
2378
  throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1506
2379
  for (const origin of withdrawn.keys())
1507
- pendings.delete(origin);
2380
+ pendings.delete(pendingKey(locator, origin));
1508
2381
  for (const [id, drafts] of drafted) {
1509
- pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
2382
+ pendings.set(pendingKey(locator, id), {
2383
+ drafts,
2384
+ submission: { state: 'waiting' },
2385
+ lost: false,
2386
+ });
1510
2387
  }
1511
- if (!localSaves)
1512
- recorded = false;
1513
- for (const path of consumed)
2388
+ for (const path of left)
1514
2389
  forgetRoomDraft(locator, path);
1515
- replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
1516
- publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
2390
+ for (const record of arrived)
2391
+ rememberRoomDraft(record);
2392
+ savedReplica(locator, replica, batches, touched.get(locator), answer);
2393
+ publishChangeSet(changeSet(locator, {
2394
+ documents: answer.changed[locator] ?? [],
1517
2395
  queue: true,
1518
- drafts: consumed,
2396
+ drafts: [...left, ...arrived.map(({ path }) => path)],
1519
2397
  }));
1520
- moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
1521
- submitLater(locator);
2398
+ queuedSave(locator);
1522
2399
  const identifier = join?.batch.id ??
1523
2400
  built?.batch.id ??
1524
2401
  [...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
1525
2402
  return { outcome: 'saved', identifier };
1526
2403
  }
1527
- async function mark(local, locator, identifier, submission, lost) {
1528
- const state = pendings.get(identifier);
1529
- if (!continuing() ||
1530
- state === undefined ||
1531
- (JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
1532
- return;
2404
+ async function mark(local, locator, batch, submission, lost) {
2405
+ const partner = partnerOf(batch, locator);
2406
+ for (const held of partner === null ? [locator] : [locator, partner]) {
2407
+ const state = pendings.get(pendingKey(held, batch.id));
2408
+ if (!continuing() ||
2409
+ state === undefined ||
2410
+ (JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
2411
+ continue;
2412
+ }
2413
+ await local.submit(held, batch.id, submission, lost);
2414
+ if (fences.has(held))
2415
+ continue;
2416
+ pendings.set(pendingKey(held, batch.id), { ...state, submission, lost });
2417
+ if (replicas.has(held))
2418
+ publishChangeSet(changeSet(held, { queue: true }));
1533
2419
  }
1534
- await local.submit(locator, identifier, submission, lost);
1535
- if (fences.has(locator))
1536
- return;
1537
- pendings.set(identifier, { ...state, submission, lost });
1538
- const replica = replicas.get(locator);
1539
- if (replica !== undefined)
1540
- publishChangeSet(changeSet(locator, replica, replica, { queue: true }));
1541
2420
  moveTo({ ...status, ...attention() });
1542
2421
  }
1543
2422
  async function keepUpload(local, locator, identifier, upload) {
@@ -1546,7 +2425,8 @@ export function composeRuntime(options) {
1546
2425
  await local.keepUpload(locator, identifier, upload);
1547
2426
  if (fences.has(locator))
1548
2427
  throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1549
- pendings.set(identifier, { ...pendings.get(identifier), upload });
2428
+ const key = pendingKey(locator, identifier);
2429
+ pendings.set(key, { ...pendings.get(key), upload });
1550
2430
  if (!continuing())
1551
2431
  throw new Error('connection refused');
1552
2432
  }
@@ -1579,7 +2459,7 @@ export function composeRuntime(options) {
1579
2459
  if (fencedSince(locator, since))
1580
2460
  return failure({ kind: 'room-not-readable', locator });
1581
2461
  if (result.kind === 'scoped-refusal') {
1582
- const room = replicas.get(result.locator)?.confirmed.record;
2462
+ const room = replicas.get(result.locator)?.record;
1583
2463
  if (room !== undefined)
1584
2464
  await scoped(result, room, await openLocal());
1585
2465
  }
@@ -1592,21 +2472,62 @@ export function composeRuntime(options) {
1592
2472
  return UNCERTAIN;
1593
2473
  }
1594
2474
  }
1595
- function queuedBlob(locator, path, localId) {
2475
+ function located(room, path, destination) {
2476
+ const crossing = roomTarget(destination);
2477
+ if (crossing === null) {
2478
+ const named = target(path, destination);
2479
+ return named === null || !rooms.has(room) ? null : { room, path: named };
2480
+ }
2481
+ const held = [...rooms.values()].find((listed) => listed.handle === crossing.handle);
2482
+ return held === undefined ? null : { room: held.locator, path: crossing.path };
2483
+ }
2484
+ async function resolveReference(room, path, destination) {
2485
+ if (roomTarget(destination) === null && target(path, destination) === null) {
2486
+ return ABSENT_TARGET;
2487
+ }
2488
+ const at = located(room, path, destination);
2489
+ if (at === null)
2490
+ return UNREADABLE;
2491
+ if (locked(at.room))
2492
+ return LOCKED_UNREADABLE;
2493
+ const held = replicas.get(at.room);
2494
+ if (held !== undefined &&
2495
+ (await (await openLocal()).entries(held.record.key, 'visible', [at.path])).length > 0) {
2496
+ return { kind: 'document', ...at };
2497
+ }
2498
+ const history = await readHistory(at.room, at.path);
2499
+ if (history.kind === 'no-history' || history.kind === 'revision')
2500
+ return ABSENT_TARGET;
2501
+ if (history.kind !== 'history')
2502
+ return history;
2503
+ const [last] = history.revisions;
2504
+ if (last === undefined)
2505
+ return ABSENT_TARGET;
2506
+ return history.deleted === true
2507
+ ? { kind: 'deleted', ...at, revision: last }
2508
+ : { kind: 'document', ...at };
2509
+ }
2510
+ function queuedFacts(locator, path, localId) {
1596
2511
  for (const batch of replicas.get(locator)?.queue ?? []) {
1597
2512
  const change = batch.changes[queuedFile(batch, path, localId)];
1598
2513
  if (change?.kind === 'binary')
1599
- return change.blob;
2514
+ return change;
1600
2515
  }
1601
- return null;
2516
+ const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2517
+ return drafted?.localId === localId ? drafted : null;
2518
+ }
2519
+ function batchFile(batch) {
2520
+ return async (localId) => {
2521
+ const change = batch.changes.find((held) => held.kind === 'binary' && held.localId === localId);
2522
+ return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
2523
+ };
1602
2524
  }
1603
2525
  function binaryRoom(request) {
1604
2526
  if (request.kind === 'path')
1605
2527
  return request.locator;
1606
- const scope = options.adapter.referrerScope(request.referrer);
1607
- return scope?.kind === 'room' ? scope.locator : null;
2528
+ return options.adapter.referrerScope(request.referrer)?.locator ?? null;
1608
2529
  }
1609
- async function readBinary(request) {
2530
+ async function transferBinary(request, transfer, answer) {
1610
2531
  if (closed || refusal !== null)
1611
2532
  return UNCERTAIN;
1612
2533
  try {
@@ -1622,69 +2543,116 @@ export function composeRuntime(options) {
1622
2543
  ? `transfer binary ${request.locator} ${request.path}`
1623
2544
  : `transfer binary ${request.fileRef}`);
1624
2545
  try {
1625
- const result = await options.adapter.binary(authorization, request);
1626
- journal.record(`outcome binary ${result.kind}`);
2546
+ const result = await transfer(authorization);
2547
+ const settled = result;
2548
+ journal.record(`outcome binary ${settled.kind}`);
1627
2549
  const locator = binaryRoom(request);
1628
2550
  if (locator !== null && fencedSince(locator, since))
1629
2551
  return failure({ kind: 'room-not-readable', locator });
1630
- if (result.kind === 'scoped-refusal') {
1631
- const room = replicas.get(result.locator)?.confirmed.record;
2552
+ if (settled.kind === 'scoped-refusal') {
2553
+ const room = replicas.get(settled.locator)?.record;
1632
2554
  if (room !== undefined)
1633
- await scoped(result, room, await openLocal());
2555
+ await scoped(settled, room, await openLocal());
1634
2556
  }
1635
- if (result.kind === 'revoked-authorization')
2557
+ if (settled.kind === 'revoked-authorization')
1636
2558
  await refuse('revoked-authorization');
1637
- return binaryAnswer(result);
2559
+ return answer(result);
1638
2560
  }
1639
2561
  catch {
1640
2562
  journal.record('outcome binary transport-uncertainty');
1641
2563
  return UNCERTAIN;
1642
2564
  }
1643
2565
  }
1644
- async function readBinaryPath(locator, path, revision) {
1645
- await ready;
2566
+ async function readBinary(request, name) {
2567
+ if (closed)
2568
+ return UNCERTAIN;
2569
+ const held = await keptFiles.find(name);
2570
+ if (held !== null)
2571
+ return held;
2572
+ return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2573
+ }
2574
+ async function binaryPath(locator, path, revision) {
1646
2575
  if (revision !== undefined)
1647
- return readBinary({ kind: 'path', locator, path, revision });
1648
- const visible = replicas.get(locator)?.visible;
1649
- const held = visible?.binaries[path];
1650
- if (visible === undefined || held === undefined)
1651
- return absentBinary(path);
1652
- if (held.state === 'pending') {
1653
- const blob = queuedBlob(locator, path, held.localId);
1654
- return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1655
- }
1656
- const record = binaryRecord(path, held, visible.revisions[path]);
1657
- if (record.state !== 'accepted')
1658
- return absentBinary(path);
2576
+ return { kind: 'path', locator, path, revision };
1659
2577
  const replica = replicas.get(locator);
1660
- const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
2578
+ if (replica === undefined)
2579
+ return null;
2580
+ const local = await openLocal();
2581
+ const [held] = await local.entries(replica.record.key, 'visible', [path]);
2582
+ if (held?.binary === undefined || held.binary === null)
2583
+ return null;
2584
+ if (held.binary.state === 'pending')
2585
+ return { kind: 'pending', localId: held.binary.localId };
2586
+ const record = binaryRecord(path, held.binary, held.revision ?? undefined);
2587
+ if (record.state !== 'accepted')
2588
+ return null;
2589
+ const queued = replica.queue.flatMap((batch) => batch.changes);
2590
+ const sources = queued.flatMap((change) => (change.kind === 'move' ? [change.path] : []));
2591
+ const confirmedLayer = await stepped(slicing(await local.entries(replica.record.key, 'confirmed', [path, ...sources]), [
2592
+ path,
2593
+ ...sources,
2594
+ ]));
1661
2595
  const holds = (at) => {
1662
- const kept = replica?.confirmed.record.binaries[at];
2596
+ const kept = confirmedLayer.binaries[at];
1663
2597
  return (kept?.state === 'accepted' &&
1664
2598
  kept.fileRef === record.fileRef &&
1665
- replica?.confirmed.record.revisions[at] === record.revision);
2599
+ confirmedLayer.revisions[at] === record.revision);
1666
2600
  };
1667
- let confirmed = path;
1668
- for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
2601
+ let confirmedAt = path;
2602
+ for (let index = queued.length - 1; index >= 0 && !holds(confirmedAt); index -= 1) {
1669
2603
  const change = queued[index];
1670
- if (change.kind === 'move' && change.to === confirmed)
1671
- confirmed = change.path;
2604
+ if (change.kind === 'move' && change.to === confirmedAt)
2605
+ confirmedAt = change.path;
1672
2606
  }
1673
- return readBinary({
2607
+ return {
1674
2608
  kind: 'path',
1675
2609
  locator,
1676
- path: confirmed,
2610
+ path: confirmedAt,
1677
2611
  revision: record.revision,
1678
2612
  fileRef: record.fileRef,
1679
- });
2613
+ };
2614
+ }
2615
+ async function readBinaryPath(locator, path, revision) {
2616
+ await ready;
2617
+ const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2618
+ const asked = drafted !== undefined && drafted.localId === revision
2619
+ ? { kind: 'pending', localId: revision }
2620
+ : await binaryPath(locator, path, revision);
2621
+ if (asked === null)
2622
+ return absentBinary(path);
2623
+ if (asked.kind === 'pending') {
2624
+ const facts = queuedFacts(locator, path, asked.localId);
2625
+ return facts === null
2626
+ ? absentBinary(path)
2627
+ : {
2628
+ kind: 'binary',
2629
+ file: asked.localId,
2630
+ byteLength: facts.byteLength,
2631
+ mediaType: facts.mediaType,
2632
+ };
2633
+ }
2634
+ return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
2635
+ }
2636
+ async function linkBinaryPath(locator, path, revision) {
2637
+ await ready;
2638
+ const asked = await binaryPath(locator, path, revision);
2639
+ if (asked === null || asked.kind === 'pending')
2640
+ return absentBinary(path);
2641
+ return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
1680
2642
  }
1681
2643
  async function readBinaryReference(reference) {
1682
2644
  await ready;
1683
- return readBinary({
1684
- kind: 'reference',
1685
- fileRef: reference.fileRef,
1686
- referrer: reference.referrer,
1687
- });
2645
+ const scope = options.adapter.referrerScope(reference.referrer);
2646
+ if (scope === null)
2647
+ return absentReference(reference.fileRef);
2648
+ return readBinary({ kind: 'reference', fileRef: reference.fileRef, referrer: reference.referrer }, { locator: scope.locator, path: scope.path ?? '', revision: reference.fileRef });
2649
+ }
2650
+ function readPart(file, part) {
2651
+ return options.database.ask({ kind: 'file.part', key: file, part });
2652
+ }
2653
+ async function cleanUp() {
2654
+ const freed = await options.database.ask({ kind: 'opened.clean' });
2655
+ return freed + ((await options.shell.cleanUp?.()) ?? 0);
1688
2656
  }
1689
2657
  function declaredAgents() {
1690
2658
  for (const capability of capabilities) {
@@ -1702,7 +2670,14 @@ export function composeRuntime(options) {
1702
2670
  }
1703
2671
  return undefined;
1704
2672
  }
1705
- async function invokeAction(name, input) {
2673
+ function calledRooms(name, input) {
2674
+ const member = declaredAction(name)?.room;
2675
+ if (member === undefined || input === null || typeof input !== 'object')
2676
+ return [];
2677
+ const named = input[member];
2678
+ return (Array.isArray(named) ? named : [named]).filter((room) => typeof room === 'string');
2679
+ }
2680
+ async function invokeAction(name, input, dispatching) {
1706
2681
  await ready;
1707
2682
  if (closed || refusal !== null)
1708
2683
  return UNCERTAIN;
@@ -1716,11 +2691,12 @@ export function composeRuntime(options) {
1716
2691
  }
1717
2692
  if (closed || authorization === undefined || refusal !== null)
1718
2693
  return UNCERTAIN;
2694
+ await dispatching?.();
1719
2695
  journal.record(`transfer action ${name}`);
1720
2696
  try {
1721
2697
  const result = await options.adapter.action(authorization, { name, input });
1722
2698
  if (result.kind === 'scoped-refusal') {
1723
- const room = replicas.get(result.locator)?.confirmed.record;
2699
+ const room = replicas.get(result.locator)?.record;
1724
2700
  if (room !== undefined)
1725
2701
  await scoped(result, room, await openLocal());
1726
2702
  }
@@ -1736,85 +2712,336 @@ export function composeRuntime(options) {
1736
2712
  return UNCERTAIN;
1737
2713
  }
1738
2714
  }
2715
+ function unsettled(result) {
2716
+ return result.kind !== 'action' && result.retryable;
2717
+ }
2718
+ async function recoveredAction(name, input) {
2719
+ const operation = operationOf(input);
2720
+ if (operation === null)
2721
+ return invokeAction(name, input);
2722
+ const kept = async () => (await openLocal()).keepCall({ operation, name, input });
2723
+ calling.add(operation);
2724
+ try {
2725
+ let result = await invokeAction(name, input, kept);
2726
+ if (unsettled(result)) {
2727
+ await recovery.attempt(`action ${operation}`, async () => {
2728
+ if (closed || refusal !== null)
2729
+ return 'done';
2730
+ if (!socketOpen)
2731
+ return 'again';
2732
+ result = await invokeAction(name, input, kept);
2733
+ return unsettled(result) ? 'again' : 'done';
2734
+ }, { again: true });
2735
+ }
2736
+ if (!unsettled(result))
2737
+ await (await openLocal()).dropCall(operation);
2738
+ return result;
2739
+ }
2740
+ finally {
2741
+ calling.delete(operation);
2742
+ }
2743
+ }
2744
+ async function resendCalls(signal) {
2745
+ if (!socketOpen || closed || stopping)
2746
+ return 'done';
2747
+ const local = await openLocal();
2748
+ for (const held of await local.calls()) {
2749
+ if (calling.has(held.operation))
2750
+ continue;
2751
+ if (closed || signal.aborted)
2752
+ return 'done';
2753
+ if (pastHorizon(held.operation)) {
2754
+ await local.dropCall(held.operation);
2755
+ continue;
2756
+ }
2757
+ calling.add(held.operation);
2758
+ try {
2759
+ if (unsettled(await invokeAction(held.name, held.input)))
2760
+ return 'again';
2761
+ await local.dropCall(held.operation);
2762
+ }
2763
+ finally {
2764
+ calling.delete(held.operation);
2765
+ }
2766
+ }
2767
+ return 'done';
2768
+ }
1739
2769
  function continuing() {
1740
2770
  return !closed && !discoveryPaused;
1741
2771
  }
1742
2772
  function reachedBy(locator, roomVersion) {
1743
2773
  const held = replicas.get(locator);
1744
- return (held !== undefined &&
1745
- options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion));
2774
+ return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
1746
2775
  }
1747
- function writtenAfter(locator, roomVersion, path) {
1748
- const written = replicas.get(locator)?.confirmed.record.written[path];
2776
+ async function writtenAfter(locator, roomVersion, path) {
2777
+ const held = replicas.get(locator);
2778
+ if (held === undefined)
2779
+ return false;
2780
+ const [entry] = await (await openLocal()).entries(held.record.key, 'confirmed', [path]);
2781
+ const written = entry?.written ?? undefined;
1749
2782
  return (written !== undefined &&
1750
2783
  options.adapter.reached(locator, written, roomVersion) &&
1751
2784
  !options.adapter.reached(locator, roomVersion, written));
1752
2785
  }
1753
- async function pullRefresh(authorized, confirmed, live) {
1754
- if (!live())
2786
+ function overriddenTexts(texts, overrides) {
2787
+ return async (paths) => {
2788
+ const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
2789
+ for (const path of paths) {
2790
+ const entry = overrides.get(path);
2791
+ if (entry === undefined)
2792
+ continue;
2793
+ if (entry === null)
2794
+ known.delete(path);
2795
+ else
2796
+ known.set(path, entry.binary !== null ? null : entry.content);
2797
+ }
2798
+ return known;
2799
+ };
2800
+ }
2801
+ async function unreadableRoom(texts, walk, paths) {
2802
+ if (paths !== null) {
2803
+ const text = {};
2804
+ for (const [path, content] of await texts(paths))
2805
+ if (content !== null)
2806
+ text[path] = content;
2807
+ return stepped(unreadableIn(text, Object.keys(text)));
2808
+ }
2809
+ for (let after = null;;) {
2810
+ const page = await walk(after);
2811
+ const text = {};
2812
+ for (const { path, content } of page.entries)
2813
+ if (content !== null)
2814
+ text[path] = content;
2815
+ const refused = await stepped(unreadableIn(text, Object.keys(text)));
2816
+ if (refused !== null)
2817
+ return refused;
2818
+ if (page.next === null)
2819
+ return null;
2820
+ after = page.next;
2821
+ }
2822
+ }
2823
+ async function validatedRoom(local, record, view, previous, overrides = null) {
2824
+ const composition = compositionOf(record);
2825
+ const modeled = record.model.status !== 'unavailable' || record.selectionModel.status !== 'unavailable';
2826
+ const lookup = overrides === null
2827
+ ? lookupOf(local, record.key, view)
2828
+ : overriddenLookup(lookupOf(local, record.key, view), overrides);
2829
+ const texts = overrides === null
2830
+ ? textsOf(local, record.key, view)
2831
+ : overriddenTexts(textsOf(local, record.key, view), overrides);
2832
+ const walk = overrides === null
2833
+ ? walkOf(local, record.key, view)
2834
+ : overriddenWalk(walkOf(local, record.key, view), overrides);
2835
+ let changes = null;
2836
+ let reshaped = false;
2837
+ if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
2838
+ const [was, now] = await Promise.all([
2839
+ local.entries(record.key, 'confirmed', previous.changed),
2840
+ overrides === null
2841
+ ? local.entries(record.key, view, previous.changed)
2842
+ : Promise.resolve([]),
2843
+ ]);
2844
+ const before = new Map(was.map((entry) => [entry.path, entry]));
2845
+ const after = overrides === null
2846
+ ? new Map(now.map((entry) => [entry.path, entry]))
2847
+ : overrides;
2848
+ changes = previous.changed.flatMap((path) => {
2849
+ const one = before.get(path) ?? null;
2850
+ const other = after.get(path) ?? null;
2851
+ reshaped ||= textDiffers(one, other);
2852
+ const held = textOf(one);
2853
+ const next = textOf(other);
2854
+ return held === next ? [] : [{ path, was: held, now: next }];
2855
+ });
2856
+ }
2857
+ const named = changes === null ? null : changes.map(({ path }) => path);
2858
+ const textChanged = changes === null || changes.length > 0 || reshaped;
2859
+ const modelsChanged = previous === null || !sameComposition(compositionOf(previous.record), composition);
2860
+ if (modeled && (textChanged || modelsChanged)) {
2861
+ const clean = previous !== null &&
2862
+ changes !== null &&
2863
+ (await changedCleanly(changes, compositionOf(previous.record), composition, {
2864
+ lookup,
2865
+ texts,
2866
+ referrers: referrersOf(local, record.key, 'confirmed'),
2867
+ }));
2868
+ if (clean)
2869
+ return null;
2870
+ const verdict = await validatedCollection(walk, lookup, composition);
2871
+ if (verdict.verdict === 'accept')
2872
+ return null;
2873
+ if (verdict.rule !== 'model-unavailable')
2874
+ return { kind: 'refused', refusal: verdict };
2875
+ return unreadableRoom(texts, walk, named);
2876
+ }
2877
+ if (textChanged)
2878
+ return unreadableRoom(texts, walk, named);
2879
+ return null;
2880
+ }
2881
+ function refreshingOf(local, key, liveness) {
2882
+ return {
2883
+ prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
2884
+ async stage(page) {
2885
+ const batches = batched(page.entries);
2886
+ for (const [index, entries] of batches.entries()) {
2887
+ const last = index === batches.length - 1;
2888
+ const staged = await exclusive(async () => {
2889
+ if (!liveness())
2890
+ return false;
2891
+ await local.refresh(key, last
2892
+ ? { ...page, entries }
2893
+ : { entries, removals: [], folders: [], folderRemovals: [] });
2894
+ return true;
2895
+ });
2896
+ if (!staged)
2897
+ return false;
2898
+ }
2899
+ return true;
2900
+ },
2901
+ };
2902
+ }
2903
+ function batched(entries) {
2904
+ const batches = [[]];
2905
+ let spent = 0;
2906
+ for (const entry of entries) {
2907
+ const size = (entry.content?.length ?? 0) + ROW_WEIGHT;
2908
+ if (spent + size > STAGE_BYTES && batches.at(-1).length > 0) {
2909
+ batches.push([]);
2910
+ spent = 0;
2911
+ }
2912
+ batches.at(-1).push(entry);
2913
+ spent += size;
2914
+ }
2915
+ return batches;
2916
+ }
2917
+ async function pullRefresh(authorized, held, liveness) {
2918
+ if (!liveness())
1755
2919
  return unconfirmed;
2920
+ const turn = generation;
2921
+ const { record } = held;
2922
+ const base = held.confirmed;
2923
+ const local = await openLocal();
1756
2924
  try {
1757
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1758
- const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
1759
- journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
1760
- return result;
2925
+ await exclusive(() => local.unrefresh(record.key));
2926
+ const result = await advancement(reads, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, liveness));
2927
+ if (result.kind !== 'advanced') {
2928
+ journal.record(`transfer refresh ${record.locator} ${result.kind}`);
2929
+ return result;
2930
+ }
2931
+ const refused = await validatedRoom(local, result.record, 'refreshed', {
2932
+ record,
2933
+ changed: [...result.changed],
2934
+ });
2935
+ if (refused !== null) {
2936
+ journal.record(`transfer refresh ${record.locator} refused`);
2937
+ return refused;
2938
+ }
2939
+ journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
2940
+ return { kind: 'advanced', record: result.record, base };
1761
2941
  }
1762
2942
  catch (error) {
1763
- journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
2943
+ journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
1764
2944
  throw error;
1765
2945
  }
2946
+ finally {
2947
+ reads.finished(record.locator, turn);
2948
+ }
2949
+ }
2950
+ const heldContents = new Map();
2951
+ function heldContentOf(local, key) {
2952
+ const known = heldContents.get(key);
2953
+ if (known !== undefined)
2954
+ return known;
2955
+ if (heldContents.size > ROOM_READS)
2956
+ heldContents.clear();
2957
+ const made = {
2958
+ files: (after, limit) => local.files(key, 'confirmed', after, limit),
2959
+ async contents(paths) {
2960
+ return new Map((await local.entries(key, 'confirmed', paths)).flatMap(({ path, content, revision }) => content === null || revision === null ? [] : [[path, { content, revision }]]));
2961
+ },
2962
+ };
2963
+ heldContents.set(key, made);
2964
+ return made;
1766
2965
  }
1767
- function staging(local, live) {
2966
+ function staging(local, liveness) {
1768
2967
  return {
1769
2968
  held: (locator) => transfers.get(locator) ?? null,
1770
- staged: (record) => local.staged(record),
1771
- stage: (record, page) => exclusive(async () => {
1772
- if (!live())
1773
- return false;
1774
- const bytes = layerBytes(page);
1775
- if (!(await admitted(local, record.locator, bytes)))
1776
- return false;
1777
- try {
1778
- await local.stage(record, page);
1779
- }
1780
- catch (error) {
1781
- if (!exceeded(error))
1782
- throw error;
1783
- await exceededOn(record.locator, bytes);
1784
- return false;
2969
+ async stage(record, page, composition) {
2970
+ const bytes = pageBytes(page);
2971
+ const batches = batched(page.entries);
2972
+ let key = record.key;
2973
+ for (const [index, entries] of batches.entries()) {
2974
+ const last = index === batches.length - 1;
2975
+ const staged = await exclusive(async () => {
2976
+ if (!liveness())
2977
+ return null;
2978
+ if (index === 0 && !(await admitted(local, record.locator, bytes)))
2979
+ return null;
2980
+ try {
2981
+ return await local.stage(record, composition, { entries, folders: last ? page.folders : [] }, key, last);
2982
+ }
2983
+ catch (error) {
2984
+ if (!exceeded(error))
2985
+ throw error;
2986
+ await exceededOn(record.locator, bytes);
2987
+ return null;
2988
+ }
2989
+ });
2990
+ if (staged === null)
2991
+ return null;
2992
+ key = staged;
1785
2993
  }
1786
- transfers.set(record.locator, record);
1787
- readSide.recover();
1788
- return true;
1789
- }),
2994
+ transfers.set(record.locator, { ...record, key });
2995
+ liveQueries.recover();
2996
+ return key;
2997
+ },
2998
+ async refreshing(key) {
2999
+ await exclusive(() => local.unrefresh(key));
3000
+ return refreshingOf(local, key, liveness);
3001
+ },
1790
3002
  abandon: (locator) => exclusive(async () => {
1791
- if (!live())
3003
+ if (!liveness())
1792
3004
  return;
1793
3005
  await local.abandon(locator);
1794
3006
  transfers.delete(locator);
1795
- readSide.recover();
3007
+ liveQueries.recover();
3008
+ sweep();
3009
+ }),
3010
+ setAside: (locator) => exclusive(async () => {
3011
+ if (!liveness())
3012
+ return;
3013
+ await local.abandon(locator, true);
3014
+ transfers.delete(locator);
3015
+ liveQueries.recover();
1796
3016
  }),
3017
+ release: (key) => exclusive(async () => {
3018
+ await local.release(key);
3019
+ sweep();
3020
+ }),
3021
+ content: (key) => heldContentOf(local, key),
3022
+ validate: (record, view) => validatedRoom(local, record, view, null),
1797
3023
  };
1798
3024
  }
1799
- function reusable(room) {
1800
- const record = replicas.get(room.locator)?.confirmed.record;
3025
+ function reusable(room, local) {
3026
+ const record = replicas.get(room.locator)?.record;
1801
3027
  if (record === undefined || record.replica.selection !== room.replica.selection)
1802
3028
  return null;
1803
- return { documents: record.documents, revisions: record.revisions };
3029
+ return heldContentOf(local, record.key);
1804
3030
  }
1805
- async function pullBootstrap(authorized, room, local, live) {
1806
- if (!live())
3031
+ async function pullBootstrap(authorized, room, local, liveness) {
3032
+ if (!liveness())
1807
3033
  return unconfirmed;
3034
+ const turn = generation;
1808
3035
  if (replicas.has(room.locator)) {
1809
3036
  catching.add(room.locator);
1810
- readSide.recover();
3037
+ liveQueries.recover();
1811
3038
  }
1812
3039
  try {
1813
- const result = await bootstrapRoom(options.adapter, authorized, room, {
3040
+ const result = await bootstrapRoom(reads, authorized, room, {
1814
3041
  collection: modelOf(room.locator),
1815
3042
  selection: modelOf(room.locator, 'typed-selection'),
1816
- }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
1817
- const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
3043
+ }, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
3044
+ const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
1818
3045
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1819
3046
  if (result.kind !== 'staged')
1820
3047
  caughtUpTo(room.locator);
@@ -1825,18 +3052,33 @@ export function composeRuntime(options) {
1825
3052
  caughtUpTo(room.locator);
1826
3053
  throw error;
1827
3054
  }
3055
+ finally {
3056
+ reads.finished(room.locator, turn);
3057
+ }
1828
3058
  }
1829
3059
  function caughtUpTo(locator) {
1830
3060
  if (catching.delete(locator))
1831
- readSide.recover();
3061
+ liveQueries.recover();
3062
+ }
3063
+ function behind(room) {
3064
+ const held = replicas.get(room.locator)?.record.roomVersion;
3065
+ return (held !== undefined &&
3066
+ [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
1832
3067
  }
1833
3068
  async function advance(room, local, authorized, live) {
1834
- await referenced(room.locator);
3069
+ const carried = reads.carried(room.locator);
3070
+ const withheld = await advanceOnce(room, local, authorized, live);
3071
+ if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
3072
+ return withheld;
3073
+ }
3074
+ return advanceOnce(room, local, authorized, live);
3075
+ }
3076
+ async function advanceOnce(room, local, authorized, live) {
1835
3077
  if (!live() || paused.has(room.locator))
1836
3078
  return unconfirmed;
1837
3079
  const held = replicas.get(room.locator);
1838
3080
  if (held !== undefined) {
1839
- const refreshed = await pullRefresh(authorized, held.confirmed, live);
3081
+ const refreshed = await pullRefresh(authorized, held, live);
1840
3082
  if (!live())
1841
3083
  return unconfirmed;
1842
3084
  if (refreshed.kind === 'still-current') {
@@ -1851,10 +3093,11 @@ export function composeRuntime(options) {
1851
3093
  return null;
1852
3094
  }
1853
3095
  if (refreshed.kind === 'advanced') {
1854
- if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
1855
- const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
3096
+ const landing = { kind: 'refreshed', base: refreshed.base };
3097
+ if (!(await publish(local, room, refreshed.record, landing, live))) {
3098
+ const now = replicas.get(room.locator)?.record.roomVersion;
1856
3099
  const reached = now !== undefined &&
1857
- options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
3100
+ options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
1858
3101
  return reached ? null : advance(room, local, authorized, live);
1859
3102
  }
1860
3103
  await retainModel(local, room, 'collection', modelOf(room.locator));
@@ -1864,16 +3107,18 @@ export function composeRuntime(options) {
1864
3107
  return refreshed;
1865
3108
  if (opened !== null) {
1866
3109
  owed.add(room.locator);
1867
- readSide.recover();
3110
+ liveQueries.recover();
1868
3111
  return unconfirmed;
1869
3112
  }
3113
+ if (reloadsDue.get(room.locator) === generation)
3114
+ return unconfirmed;
1870
3115
  }
1871
3116
  const bootstrapped = await pullBootstrap(authorized, room, local, live);
1872
3117
  if (!live())
1873
3118
  return unconfirmed;
1874
3119
  if (bootstrapped.kind !== 'staged')
1875
3120
  return bootstrapped;
1876
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
3121
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
1877
3122
  return null;
1878
3123
  }
1879
3124
  async function withhold(withheld, room, local, progress) {
@@ -1946,57 +3191,254 @@ export function composeRuntime(options) {
1946
3191
  (accepted.author === undefined ? '' : ` author ${accepted.author}`) +
1947
3192
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1948
3193
  }
1949
- async function adoptAccepted(room, local, submitted, accepted) {
1950
- await referenced(room.locator);
3194
+ function settling(acceptance) {
3195
+ return acceptance !== null && 'apply' in acceptance;
3196
+ }
3197
+ function refusalOf(acceptance) {
3198
+ return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
3199
+ }
3200
+ async function acceptance(room, local, submitted, accepted) {
1951
3201
  if (!continuing())
1952
3202
  return unconfirmed;
1953
3203
  const current = replicas.get(room.locator);
1954
3204
  if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1955
3205
  return null;
1956
3206
  }
1957
- const held = current.confirmed.record.roomVersion;
3207
+ const held = current.record.roomVersion;
1958
3208
  logAccepted(room.locator, submitted, accepted);
1959
- const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1960
- ? { kind: 'staged', confirmed: current.confirmed }
1961
- : applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
1962
- ? accepted.roomVersion
1963
- : held, accepted.roomVersion);
1964
- if (applied.kind !== 'staged') {
1965
- const state = pendings.get(submitted.id);
1966
- await local.accept(room.locator, submitted.id, accepted.roomVersion);
1967
- if (state !== undefined && !fences.has(room.locator))
1968
- pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1969
- emitQueue(room.locator);
1970
- return applied;
1971
- }
1972
- const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1973
- pendings.delete(submitted.id);
1974
- unreadable.delete(room.locator);
1975
- sourceRefusals.delete(room.locator);
1976
- declarationRefusals.delete(room.locator);
1977
- const set = settled();
1978
- list(room, true);
1979
- publishChangeSet(set);
1980
- moveTo({ ...status, ...attention() });
1981
- return null;
3209
+ let record = current.record;
3210
+ let changes;
3211
+ if (!options.adapter.reached(room.locator, held, accepted.roomVersion)) {
3212
+ const paths = [...returnedPaths(submitted, accepted)];
3213
+ const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
3214
+ const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
3215
+ record = {
3216
+ ...record,
3217
+ roomVersion: options.adapter.follows(room.locator, held, accepted.roomVersion)
3218
+ ? accepted.roomVersion
3219
+ : held,
3220
+ };
3221
+ const overrides = new Map([
3222
+ ...back.set.map((entry) => [entry.path, entry]),
3223
+ ...back.removed.map((path) => [path, null]),
3224
+ ]);
3225
+ const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
3226
+ if (refused !== null) {
3227
+ const key = pendingKey(room.locator, submitted.id);
3228
+ return {
3229
+ locator: room.locator,
3230
+ left: [],
3231
+ commit: {
3232
+ rooms: [],
3233
+ writes: [
3234
+ {
3235
+ locator: room.locator,
3236
+ accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
3237
+ },
3238
+ ],
3239
+ },
3240
+ refused,
3241
+ apply() {
3242
+ const state = pendings.get(key);
3243
+ if (state !== undefined && !fences.has(room.locator))
3244
+ pendings.set(key, { ...state, accepted: accepted.roomVersion });
3245
+ return changeSet(room.locator, { queue: true });
3246
+ },
3247
+ };
3248
+ }
3249
+ changes = back;
3250
+ }
3251
+ const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
3252
+ return {
3253
+ ...settled,
3254
+ apply(answer) {
3255
+ const set = settled.apply(answer);
3256
+ pendings.delete(pendingKey(room.locator, submitted.id));
3257
+ unreadable.delete(room.locator);
3258
+ sourceRefusals.delete(room.locator);
3259
+ declarationRefusals.delete(room.locator);
3260
+ list(room, true);
3261
+ return set;
3262
+ },
3263
+ };
3264
+ }
3265
+ async function adoptedTogether(local, acceptances) {
3266
+ const settlements = acceptances.filter(settling);
3267
+ if (settlements.length > 0) {
3268
+ publishChangeSet(await together(local, settlements));
3269
+ moveTo({ ...status, ...attention() });
3270
+ }
3271
+ await unpair(local);
1982
3272
  }
1983
3273
  async function adopted(room, local, submitted, accepted) {
1984
- const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
3274
+ const failed = await exclusive(async () => {
3275
+ const adopting = await acceptance(room, local, submitted, accepted);
3276
+ await adoptedTogether(local, [adopting]);
3277
+ return refusalOf(adopting);
3278
+ });
3279
+ if (failed === null)
3280
+ return true;
3281
+ if (failed.kind === 'refused' && continuing())
3282
+ refusedSource(room, failed);
3283
+ return false;
3284
+ }
3285
+ async function adoptedMove(room, partner, local, submitted, accepted) {
3286
+ const other = discovered.get(partner);
3287
+ const failed = await exclusive(async () => {
3288
+ const left = await acceptance(room, local, submitted, accepted);
3289
+ const arriving = replicas.get(partner)?.queue.find((batch) => batch.id === submitted.id);
3290
+ const arrived = other === undefined || arriving === undefined || accepted.destination === undefined
3291
+ ? null
3292
+ : await acceptance(other, local, arriving, {
3293
+ kind: 'accepted',
3294
+ ...accepted.destination,
3295
+ });
3296
+ await adoptedTogether(local, [left, arrived]);
3297
+ const refusedArrival = refusalOf(arrived);
3298
+ if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
3299
+ refusedSource(other, refusedArrival);
3300
+ }
3301
+ return refusalOf(left);
3302
+ });
3303
+ submitLater(partner);
1985
3304
  if (failed === null)
1986
3305
  return true;
1987
3306
  if (failed.kind === 'refused' && continuing())
1988
3307
  refusedSource(room, failed);
1989
3308
  return false;
1990
3309
  }
3310
+ function queueIn(locator) {
3311
+ return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
3312
+ }
3313
+ function behindIn(locator, batch) {
3314
+ const held = replicas.get(locator)?.queue;
3315
+ if (held === undefined)
3316
+ return queuesWithoutReplica.get(locator) !== undefined;
3317
+ return held.findIndex((one) => one.id === batch.id) > 0;
3318
+ }
3319
+ function unpaired(skipped) {
3320
+ for (const locator of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
3321
+ for (const batch of queueIn(locator)) {
3322
+ const partner = partnerOf(batch, locator);
3323
+ if (partner === null || skipped.has(pendingKey(locator, batch.id)))
3324
+ continue;
3325
+ if (queueIn(partner).some((held) => held.id === batch.id))
3326
+ continue;
3327
+ if (unpairedIn(locator, batch))
3328
+ return { locator, batch };
3329
+ }
3330
+ }
3331
+ return null;
3332
+ }
3333
+ async function unpairing(local, locator, batches) {
3334
+ const ids = new Set(batches.map(({ id }) => id));
3335
+ const leaving = batches.map((batch) => ({
3336
+ batch,
3337
+ returned: !localSaves && leavingIn(batch, locator) ? returnedBy('withdrawn') : null,
3338
+ }));
3339
+ const outcome = localSaves ? 'dropped' : 'unpaired';
3340
+ const current = replicas.get(locator);
3341
+ if (current !== undefined) {
3342
+ const settled = await settlement(local, locator, current, current.record, current.queue.filter((one) => !ids.has(one.id)), { leaving });
3343
+ return {
3344
+ ...settled,
3345
+ apply(answer) {
3346
+ for (const batch of batches)
3347
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
3348
+ const set = settled.apply(answer);
3349
+ submitLater(locator);
3350
+ return set;
3351
+ },
3352
+ };
3353
+ }
3354
+ const queue = queueIn(locator).filter((one) => !ids.has(one.id));
3355
+ const written = new Map();
3356
+ for (const { batch, returned } of leaving) {
3357
+ if (returned === null)
3358
+ continue;
3359
+ for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
3360
+ written.set(saved.path, {
3361
+ locator,
3362
+ path: saved.path,
3363
+ revision: roomDraftRevision(),
3364
+ parts: returnedInto((written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path))?.parts, saved.parts),
3365
+ returned,
3366
+ });
3367
+ }
3368
+ }
3369
+ const carried = carriedBack(locator, leaving.flatMap(({ batch, returned }) => (returned === null ? [] : [batch])), written);
3370
+ return {
3371
+ locator,
3372
+ left: batches,
3373
+ commit: {
3374
+ rooms: [],
3375
+ writes: [
3376
+ {
3377
+ locator,
3378
+ queue,
3379
+ roomDrafts: [...written.values()],
3380
+ consumedRoomDrafts: carried.taken,
3381
+ },
3382
+ ...carried.writes,
3383
+ ],
3384
+ },
3385
+ apply() {
3386
+ for (const batch of batches) {
3387
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
3388
+ pendings.delete(pendingKey(locator, batch.id));
3389
+ }
3390
+ if (queue.length > 0)
3391
+ queuesWithoutReplica.set(locator, queue);
3392
+ else
3393
+ queuesWithoutReplica.delete(locator);
3394
+ for (const record of written.values())
3395
+ rememberRoomDraft(record);
3396
+ carried.remember();
3397
+ submitLater(locator);
3398
+ return mergeChangeSets([
3399
+ changeSet(locator, {
3400
+ queue: true,
3401
+ drafts: [...written.keys(), ...carried.taken],
3402
+ }),
3403
+ ...carried.changes,
3404
+ ]);
3405
+ },
3406
+ };
3407
+ }
3408
+ async function unpair(local) {
3409
+ const handled = new Set();
3410
+ for (let held = unpaired(handled); held !== null && continuing(); held = unpaired(handled)) {
3411
+ const { locator, batch } = held;
3412
+ handled.add(pendingKey(locator, batch.id));
3413
+ publishChangeSet(await together(local, [await unpairing(local, locator, [batch])]));
3414
+ }
3415
+ if (handled.size === 0)
3416
+ return;
3417
+ moveTo({ ...status, ...attention() });
3418
+ }
3419
+ async function discardHeld(head, room, local, signal) {
3420
+ const partner = partnerOf(head, room.locator);
3421
+ const other = partner === null ? undefined : discovered.get(partner);
3422
+ await discardPending(other === undefined ? [room] : [room, other], local, signal);
3423
+ }
1991
3424
  async function submitRoom(room, local, authorized, progress, signal) {
1992
3425
  while (continuing() && authorization !== undefined && !signal.aborted) {
1993
3426
  const replica = replicas.get(room.locator);
1994
3427
  const head = replica?.queue[0];
1995
3428
  if (replica === undefined || head === undefined)
1996
3429
  return 'done';
1997
- if (pendings.get(head.id)?.accepted !== undefined)
3430
+ const state = pendings.get(pendingKey(room.locator, head.id));
3431
+ if (state?.accepted !== undefined)
1998
3432
  return 'done';
1999
- const state = pendings.get(head.id);
3433
+ const partner = partnerOf(head, room.locator);
3434
+ if (partner !== null && !leavingIn(head, room.locator)) {
3435
+ submitLater(partner);
3436
+ return 'done';
3437
+ }
3438
+ if (partner !== null && behindIn(partner, head)) {
3439
+ progress.complete = false;
3440
+ return 'done';
3441
+ }
2000
3442
  const { submission, lost: earlier } = state ?? {
2001
3443
  submission: { state: 'waiting' },
2002
3444
  lost: false,
@@ -2012,16 +3454,16 @@ export function composeRuntime(options) {
2012
3454
  }
2013
3455
  const lost = earlier || submission.state === 'submitted';
2014
3456
  if (lost && pastHorizon(head.id)) {
2015
- await discardPending(room, local, signal);
3457
+ await discardHeld(head, room, local, signal);
2016
3458
  return 'done';
2017
3459
  }
2018
3460
  let result;
2019
3461
  journal.record(`submission ${room.locator} ${head.id}`);
2020
- await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
3462
+ await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
2021
3463
  if (discoveryPaused || fences.has(room.locator))
2022
3464
  return 'done';
2023
3465
  if (signal.aborted || writesHeld()) {
2024
- await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
3466
+ await exclusive(() => mark(local, room.locator, head, submission, earlier));
2025
3467
  if (signal.aborted)
2026
3468
  return 'done';
2027
3469
  progress.complete = false;
@@ -2029,11 +3471,17 @@ export function composeRuntime(options) {
2029
3471
  return 'done';
2030
3472
  }
2031
3473
  const opensBefore = openSignals;
2032
- const answered = awaitingAnswer(room.locator);
3474
+ const answeredHere = awaitingAnswer(room.locator);
3475
+ const answeredThere = partner === null ? () => { } : awaitingAnswer(partner);
3476
+ const answered = () => {
3477
+ answeredHere();
3478
+ answeredThere();
3479
+ };
2033
3480
  try {
2034
3481
  result = await options.adapter.mutate(authorized, {
2035
3482
  locator: room.locator,
2036
3483
  changeBatch: head,
3484
+ file: batchFile(head),
2037
3485
  ...(state?.upload === undefined ? {} : { upload: state.upload }),
2038
3486
  }, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
2039
3487
  }
@@ -2058,9 +3506,10 @@ export function composeRuntime(options) {
2058
3506
  return 'again';
2059
3507
  }
2060
3508
  if (result.kind === 'room-not-readable') {
3509
+ const unread = discovered.get(result.locator) ?? room;
2061
3510
  progress.complete = false;
2062
- unreadable.add(room.locator);
2063
- list(room, false);
3511
+ unreadable.add(unread.locator);
3512
+ list(unread, false);
2064
3513
  emitRooms();
2065
3514
  return 'done';
2066
3515
  }
@@ -2075,24 +3524,30 @@ export function composeRuntime(options) {
2075
3524
  }
2076
3525
  if (result.kind === 'write-halted') {
2077
3526
  journal.record(`outcome ${room.locator} ${head.id} write-halted`);
2078
- await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, lost));
3527
+ await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
2079
3528
  if (serviceMode === 'open' && openSignals !== opensBefore)
2080
3529
  continue;
2081
3530
  haltedRefusal = true;
2082
3531
  progress.complete = false;
2083
3532
  return 'done';
2084
3533
  }
3534
+ const other = partner === null ? undefined : discovered.get(partner);
2085
3535
  if (result.kind === 'conflicted' ||
2086
3536
  result.kind === 'rejected' ||
2087
3537
  result.kind === 'expired' ||
2088
3538
  result.kind === 'retryable-refusal') {
2089
3539
  if (result.kind === 'expired' && lost && result.restored !== true) {
2090
3540
  journal.record(`outcome ${room.locator} ${head.id} expired`);
2091
- await discardPending(room, local, signal);
3541
+ await discardHeld(head, room, local, signal);
2092
3542
  return 'done';
2093
3543
  }
2094
3544
  if (result.kind === 'expired') {
2095
- await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
3545
+ await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
3546
+ const there = other === undefined ? null : await advance(other, local, authorized, continuing);
3547
+ if (other !== undefined && there !== null) {
3548
+ await withhold(there, other, local, progress);
3549
+ return 'done';
3550
+ }
2096
3551
  const withheld = await advance(room, local, authorized, continuing);
2097
3552
  if (withheld !== null) {
2098
3553
  await withhold(withheld, room, local, progress);
@@ -2109,11 +3564,25 @@ export function composeRuntime(options) {
2109
3564
  if (result.kind === 'retryable-refusal') {
2110
3565
  const reset = result.reset;
2111
3566
  progress.complete = false;
2112
- await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
3567
+ await exclusive(() => mark(local, room.locator, head, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
2113
3568
  return reset === undefined ? 'again' : { at: Date.parse(reset) };
2114
3569
  }
2115
3570
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
2116
3571
  const left = leaving(head, result);
3572
+ const refreshed = async () => ({
3573
+ there: other === undefined ? null : await advance(other, local, authorized, continuing),
3574
+ here: await advance(room, local, authorized, continuing),
3575
+ });
3576
+ const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
3577
+ if (judged !== null && (judged.there !== null || judged.here !== null)) {
3578
+ await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
3579
+ if (other !== undefined && judged.there !== null) {
3580
+ await withhold(judged.there, other, local, progress);
3581
+ }
3582
+ if (judged.here !== null)
3583
+ await withhold(judged.here, room, local, progress);
3584
+ return 'done';
3585
+ }
2117
3586
  await exclusive(async () => {
2118
3587
  if (!continuing())
2119
3588
  return;
@@ -2121,25 +3590,29 @@ export function composeRuntime(options) {
2121
3590
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
2122
3591
  return;
2123
3592
  }
2124
- const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
2125
- leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
2126
- (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
2127
- ? { ...left, returned: null }
2128
- : left,
2129
- });
2130
- publishChangeSet(settled());
3593
+ const confirmedLayer = await sliceAt(current.record, 'confirmed', [head]);
3594
+ const held = (result.kind === 'expired' &&
3595
+ (await landedWhole(room.locator, head, confirmedLayer))) ||
3596
+ (result.kind === 'conflicted' &&
3597
+ (await landedWhole(room.locator, head, confirmedLayer, result.affected)));
3598
+ publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
3599
+ await unpair(local);
2131
3600
  moveTo({ ...status, ...attention() });
2132
3601
  });
2133
3602
  if (result.kind === 'expired')
2134
3603
  continue;
2135
- const withheld = await advance(room, local, authorized, continuing);
2136
- if (withheld !== null) {
2137
- await withhold(withheld, room, local, progress);
3604
+ const { there, here } = judged ?? (await refreshed());
3605
+ if (other !== undefined && there !== null)
3606
+ await withhold(there, other, local, progress);
3607
+ if (here !== null) {
3608
+ await withhold(here, room, local, progress);
2138
3609
  return 'done';
2139
3610
  }
2140
3611
  continue;
2141
3612
  }
2142
- const kept = await adopted(room, local, head, result).finally(answered);
3613
+ const kept = await (partner === null
3614
+ ? adopted(room, local, head, result)
3615
+ : adoptedMove(room, partner, local, head, result)).finally(answered);
2143
3616
  if (!kept) {
2144
3617
  progress.complete = false;
2145
3618
  return 'done';
@@ -2150,44 +3623,67 @@ export function composeRuntime(options) {
2150
3623
  function pastHorizon(identity) {
2151
3624
  return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
2152
3625
  }
2153
- async function discardPending(room, local, signal) {
2154
- const { locator } = room;
3626
+ async function discardPending(rooms, local, signal) {
2155
3627
  const discarded = await exclusive(async () => {
2156
- if (fences.has(locator))
2157
- return false;
2158
- const before = replicas.get(locator);
2159
- const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
2160
- fences.set(locator, { at: discoveries, erased: false });
2161
- lastFenced.set(locator, ++fencings);
2162
- try {
2163
- await local.discard(locator);
2164
- }
2165
- finally {
2166
- fences.delete(locator);
3628
+ const held = rooms
3629
+ .filter(({ locator }) => !fences.has(locator))
3630
+ .map((room) => ({
3631
+ room,
3632
+ before: replicas.get(room.locator),
3633
+ queue: queueIn(room.locator),
3634
+ }));
3635
+ if (held.length === 0)
3636
+ return [];
3637
+ const answer = await local.commit({
3638
+ rooms: held.flatMap(({ room, before }) => before === undefined
3639
+ ? []
3640
+ : [
3641
+ {
3642
+ locator: room.locator,
3643
+ key: before.record.key,
3644
+ roots: before.record.typedSelection?.roots ?? [],
3645
+ overlay: { set: [], cleared: [...before.overlaid] },
3646
+ },
3647
+ ]),
3648
+ discards: held.map(({ room }) => room.locator),
3649
+ });
3650
+ for (const { room, before, queue } of held) {
3651
+ const { locator } = room;
3652
+ lastFenced.set(locator, ++fencings);
3653
+ for (const batch of queue)
3654
+ pendings.delete(pendingKey(locator, batch.id));
3655
+ queuesWithoutReplica.delete(locator);
3656
+ if (before !== undefined) {
3657
+ replicas.set(locator, {
3658
+ ...before,
3659
+ queue: [],
3660
+ overlaid: new Set(),
3661
+ count: answer.counts[locator] ?? before.count,
3662
+ });
3663
+ }
3664
+ journal.record(`room discarded ${locator}`);
2167
3665
  }
2168
- for (const batch of queue)
2169
- pendings.delete(batch.id);
2170
- queuesWithoutReplica.delete(locator);
2171
- const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
2172
- if (after !== null)
2173
- replicas.set(locator, after);
2174
- journal.record(`room discarded ${locator}`);
2175
- publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
3666
+ publishChangeSet(mergeChangeSets(held.map(({ room }) => changeSet(room.locator, {
3667
+ documents: answer.changed[room.locator] ?? [],
3668
+ queue: true,
3669
+ }))));
2176
3670
  moveTo({ ...status, ...attention() });
2177
- return true;
2178
- });
2179
- if (!discarded)
2180
- return;
2181
- const progress = { complete: true, reachable: true };
2182
- await roomTask(locator, async (live) => {
2183
- await Promise.race([
2184
- reload(room, local, progress, () => live() && !signal.aborted),
2185
- abandonedOn(signal),
2186
- ]);
3671
+ return held.map(({ room }) => room);
2187
3672
  });
2188
- if (signal.aborted)
2189
- return;
2190
- await rest(progress);
3673
+ for (const room of discarded) {
3674
+ if (signal.aborted)
3675
+ return;
3676
+ const progress = { complete: true, reachable: true };
3677
+ await roomTask(room.locator, async (live) => {
3678
+ await Promise.race([
3679
+ reload(room, local, progress, () => live() && !signal.aborted),
3680
+ abandonedOn(signal),
3681
+ ]);
3682
+ });
3683
+ if (signal.aborted)
3684
+ return;
3685
+ await rest(progress);
3686
+ }
2191
3687
  }
2192
3688
  function awaitingAnswer(locator) {
2193
3689
  let release = () => { };
@@ -2204,7 +3700,7 @@ export function composeRuntime(options) {
2204
3700
  async function retainModel(local, room, attached, next) {
2205
3701
  const key = modelKey(room.locator, attached);
2206
3702
  const previous = modelOf(room.locator, attached);
2207
- const heldBefore = replicas.get(room.locator)?.confirmed.record;
3703
+ const heldBefore = replicas.get(room.locator)?.record;
2208
3704
  if (JSON.stringify(previous) === JSON.stringify(next) &&
2209
3705
  (heldBefore === undefined ||
2210
3706
  (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
@@ -2220,36 +3716,57 @@ export function composeRuntime(options) {
2220
3716
  if (!continuing())
2221
3717
  return;
2222
3718
  const held = replicas.get(room.locator);
3719
+ let changed = null;
2223
3720
  if (held !== undefined) {
2224
3721
  const declared = {
2225
- ...held.confirmed.record,
3722
+ ...held.record,
2226
3723
  operations: room.operations,
2227
3724
  replica: room.replica,
2228
3725
  typedSelection: room.typedSelection ?? null,
2229
3726
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
2230
3727
  };
2231
- const reclassified = sameMembership(declared, held.confirmed.record)
2232
- ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
2233
- : readReclassified(declared, held.confirmed);
2234
- const record = reclassified.kind === 'staged'
3728
+ let read = 'staged';
3729
+ if (!sameMembership(declared, held.record)) {
3730
+ const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), compositionOf(declared));
3731
+ read =
3732
+ verdict.verdict !== 'refuse'
3733
+ ? 'staged'
3734
+ : verdict.rule === 'model-unavailable'
3735
+ ? 'deferred'
3736
+ : { kind: 'refused', refusal: verdict };
3737
+ }
3738
+ const record = read === 'staged'
2235
3739
  ? declared
2236
- : {
2237
- ...declared,
2238
- typedSelection: held.confirmed.record.typedSelection,
2239
- };
2240
- const read = reclassified.kind === 'deferred'
2241
- ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
2242
- : reclassified;
2243
- await local.write({ next: record, previous: held.confirmed.record });
2244
- if (read.kind === 'staged') {
2245
- replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
3740
+ : { ...declared, typedSelection: held.record.typedSelection };
3741
+ const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
3742
+ const replayedLayer = await replayed(record, confirmedLayer, held.queue);
3743
+ const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
3744
+ const answer = await local.commit({
3745
+ rooms: [
3746
+ {
3747
+ locator: room.locator,
3748
+ key: record.key,
3749
+ record: storedRecord(record),
3750
+ roots: record.typedSelection?.roots ?? [],
3751
+ composition: compositionOf(record),
3752
+ overlay: diff.overlay,
3753
+ },
3754
+ ],
3755
+ });
3756
+ recomposed(room.locator, held.record, record);
3757
+ const { count } = committedRoom(room.locator, record, answer);
3758
+ replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count });
3759
+ changed = changeSet(room.locator, {
3760
+ documents: answer.changed[room.locator] ?? [],
3761
+ room: true,
3762
+ });
3763
+ if (read === 'staged' || read === 'deferred') {
2246
3764
  if (declarationRefusals.delete(room.locator)) {
2247
3765
  sourceRefusals.delete(room.locator);
2248
3766
  unreadable.delete(room.locator);
2249
3767
  }
2250
3768
  }
2251
- else if (read.kind === 'refused') {
2252
- replicas.set(room.locator, { ...held, confirmed: { ...held.confirmed, record } });
3769
+ else {
2253
3770
  refusedSource(room, read);
2254
3771
  declarationRefusals.add(room.locator);
2255
3772
  }
@@ -2260,13 +3777,18 @@ export function composeRuntime(options) {
2260
3777
  invalidModels.delete(key);
2261
3778
  }
2262
3779
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
2263
- if (held === undefined)
3780
+ if (changed === null)
2264
3781
  emitRooms();
2265
3782
  else
2266
- publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
3783
+ publishChangeSet(changed);
2267
3784
  });
2268
3785
  }
2269
3786
  async function scoped(result, room, local) {
3787
+ if (result.scope === 'content' && !result.retryable) {
3788
+ journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
3789
+ await loseRooms([room.locator]);
3790
+ return;
3791
+ }
2270
3792
  scopedRefusals.set(room.locator, {
2271
3793
  code: result.code,
2272
3794
  fields: result.fields,
@@ -2280,15 +3802,7 @@ export function composeRuntime(options) {
2280
3802
  });
2281
3803
  else if (result.scope !== 'operation') {
2282
3804
  unreadable.add(room.locator);
2283
- scopedAway = {
2284
- ...room,
2285
- replica: { ...room.replica, admission: 'unavailable' },
2286
- operations: {
2287
- ...room.operations,
2288
- ...(result.scope === 'content' ? { read: [] } : {}),
2289
- ...(result.scope === 'sync' ? { sync: [] } : {}),
2290
- },
2291
- };
3805
+ scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
2292
3806
  await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
2293
3807
  }
2294
3808
  if (!continuing())
@@ -2299,30 +3813,22 @@ export function composeRuntime(options) {
2299
3813
  }
2300
3814
  function heldRooms(listed) {
2301
3815
  return listed.flatMap((room) => {
2302
- const record = replicas.get(room.locator)?.confirmed.record;
3816
+ const record = replicas.get(room.locator)?.record;
2303
3817
  if (record === undefined || record.replica.selection !== room.replica.selection)
2304
3818
  return [];
2305
- return [
2306
- {
2307
- locator: room.locator,
2308
- roomVersion: record.roomVersion,
2309
- documents: record.documents,
2310
- revisions: record.revisions,
2311
- },
2312
- ];
3819
+ return [{ locator: room.locator, roomVersion: record.roomVersion }];
2313
3820
  });
2314
3821
  }
2315
3822
  async function synchronize(listed, local, progress, attempt, live) {
2316
- const acquired = listed.filter(acquires);
2317
- const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
3823
+ const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
2318
3824
  if (!live())
2319
3825
  return;
2320
- const ready = acquired.filter((_, index) => {
3826
+ const ready = listed.filter((_, index) => {
2321
3827
  const outcome = prepared[index];
2322
3828
  return outcome?.status === 'fulfilled' && outcome.value;
2323
3829
  });
2324
3830
  if (attempt === 'opened') {
2325
- options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
3831
+ options.adapter.roomStream?.hold(heldRooms(ready));
2326
3832
  handed = true;
2327
3833
  }
2328
3834
  discovering?.release();
@@ -2338,7 +3844,7 @@ export function composeRuntime(options) {
2338
3844
  progress.complete = false;
2339
3845
  return false;
2340
3846
  }
2341
- const prior = replicas.get(room.locator)?.confirmed.record;
3847
+ const prior = replicas.get(room.locator)?.record;
2342
3848
  if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
2343
3849
  list(prior, false);
2344
3850
  emitRooms();
@@ -2350,19 +3856,10 @@ export function composeRuntime(options) {
2350
3856
  await retainModel(local, room, 'collection', modelOf(room.locator));
2351
3857
  if (discoveryPaused || authorization === undefined)
2352
3858
  return false;
2353
- if (room.replica.admission !== 'admitted') {
2354
- if (replicas.has(room.locator))
2355
- progress.complete = false;
2356
- return false;
2357
- }
2358
- return true;
3859
+ return room.replica.admission === 'admitted';
2359
3860
  }
2360
3861
  async function caughtUp(room, local, progress, live) {
2361
- const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
2362
- if (held === undefined || authorization === undefined || !live())
2363
- return;
2364
- const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
2365
- if (!behind)
3862
+ if (authorization === undefined || !live() || !behind(room))
2366
3863
  return;
2367
3864
  const withheld = await advance(room, local, authorization, live);
2368
3865
  if (withheld !== null)
@@ -2390,6 +3887,8 @@ export function composeRuntime(options) {
2390
3887
  }
2391
3888
  async function reload(room, local, progress, live) {
2392
3889
  try {
3890
+ if (live())
3891
+ reloadsDue.delete(room.locator);
2393
3892
  if (!live() || authorization === undefined)
2394
3893
  return;
2395
3894
  const bootstrapped = await pullBootstrap(authorization, room, local, live);
@@ -2399,14 +3898,14 @@ export function composeRuntime(options) {
2399
3898
  await withhold(bootstrapped, room, local, progress);
2400
3899
  return;
2401
3900
  }
2402
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
3901
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
2403
3902
  await caughtUp(room, local, progress, live);
2404
3903
  }
2405
3904
  finally {
2406
3905
  caughtUpTo(room.locator);
2407
3906
  }
2408
3907
  }
2409
- async function discoverRooms(local, progress, attempt, live) {
3908
+ async function discoverRooms(local, progress, attempt, live, scope) {
2410
3909
  if (closed || authorization === undefined) {
2411
3910
  notifyReady();
2412
3911
  return;
@@ -2416,7 +3915,7 @@ export function composeRuntime(options) {
2416
3915
  const started = discoveries;
2417
3916
  let result;
2418
3917
  try {
2419
- result = await options.adapter.discover(authorization);
3918
+ result = await options.adapter.discover(authorization, scope);
2420
3919
  }
2421
3920
  catch {
2422
3921
  result = { kind: 'transport-uncertainty' };
@@ -2456,12 +3955,13 @@ export function composeRuntime(options) {
2456
3955
  }
2457
3956
  for (const room of result.rooms) {
2458
3957
  if (room.operations === undefined ||
2459
- !['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
3958
+ !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
2460
3959
  room.operations[operation].every((scope) => typeof scope === 'string')) ||
2461
3960
  room.replica === undefined ||
2462
3961
  typeof room.replica.selection !== 'string' ||
2463
3962
  room.replica.selection.length === 0 ||
2464
- !['admitted', 'unavailable'].includes(room.replica.admission)) {
3963
+ !['admitted', 'unavailable'].includes(room.replica.admission) ||
3964
+ !['locked', 'open'].includes(room.replica.offline)) {
2465
3965
  journal.record(`invalid model or replica declaration ${room.locator}`);
2466
3966
  await refuse('missing-required-guarantee');
2467
3967
  notifyReady();
@@ -2485,15 +3985,16 @@ export function composeRuntime(options) {
2485
3985
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
2486
3986
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
2487
3987
  limits = result.providerLimits;
2488
- await local.declare(limits, capabilities);
3988
+ await local.fact('providerLimits', limits);
3989
+ await local.fact('capabilities', capabilities);
2489
3990
  }
2490
3991
  const signedIn = result.member ?? null;
2491
3992
  if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
2492
- await local.keepMember(signedIn);
3993
+ await local.fact('member', signedIn);
2493
3994
  member = signedIn;
2494
3995
  emitMember();
2495
3996
  }
2496
- await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
3997
+ await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
2497
3998
  }
2498
3999
  async function ensureAuthorization() {
2499
4000
  if (authorization !== undefined)
@@ -2590,9 +4091,44 @@ export function composeRuntime(options) {
2590
4091
  options.adapter.roomStream?.reopen();
2591
4092
  });
2592
4093
  }
2593
- function closedSocket(revoked) {
4094
+ function rediscover(scope) {
4095
+ return task(OPENING_LANE, async (live) => {
4096
+ if (!live() || !socketOpen)
4097
+ return;
4098
+ const progress = { complete: true, reachable: true };
4099
+ const gate = discover();
4100
+ moveTo(transferring(status));
4101
+ try {
4102
+ const local = await openLocal();
4103
+ if (!live())
4104
+ return;
4105
+ await ensureAuthorization();
4106
+ if (!live())
4107
+ return;
4108
+ await discoverRooms(local, progress, 'authorized', live, scope);
4109
+ }
4110
+ catch {
4111
+ progress.complete = false;
4112
+ }
4113
+ finally {
4114
+ gate.release();
4115
+ notifyReady();
4116
+ }
4117
+ if (!live())
4118
+ return;
4119
+ if (!progress.complete)
4120
+ complete = false;
4121
+ await rest(progress, live);
4122
+ });
4123
+ }
4124
+ function closedSocket(revoked, lost) {
2594
4125
  const opened = socketOpen;
2595
4126
  socketOpen = false;
4127
+ connectionLost();
4128
+ if (lost) {
4129
+ reads.lost();
4130
+ conversations.lost();
4131
+ }
2596
4132
  if (revoked) {
2597
4133
  void refuse('revoked-authorization');
2598
4134
  return;
@@ -2609,9 +4145,10 @@ export function composeRuntime(options) {
2609
4145
  function applyOpening(expired) {
2610
4146
  const progress = underway?.generation === generation ? underway.progress : null;
2611
4147
  const cycle = progress ?? { complete: true, reachable: true };
2612
- const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
2613
4148
  if (opened === generation)
2614
4149
  opened = null;
4150
+ const reloading = progress === null ? [] : owedReloads(expired);
4151
+ const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
2615
4152
  return task(OPENING_LANE, async (live) => {
2616
4153
  underway = null;
2617
4154
  if (!live())
@@ -2621,24 +4158,17 @@ export function composeRuntime(options) {
2621
4158
  await rest(cycle, live);
2622
4159
  return;
2623
4160
  }
2624
- await roomsCarried(expired);
4161
+ await roomsCarried([...expired, ...reloading]);
2625
4162
  if (!live())
2626
4163
  return;
2627
- const replaced = new Set(owed);
2628
- owed.clear();
2629
4164
  try {
2630
4165
  const local = await openLocal();
2631
- const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
2632
- const reloading = held.filter((room) => !expired.includes(room.locator) &&
2633
- (replaced.has(room.locator) || revoked(room.locator)));
2634
- for (const room of reloading)
2635
- catching.add(room.locator);
2636
- readSide.recover();
2637
- const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
2638
- const reloaded = [...(await reloads), ...(await replacing)];
4166
+ const reloaded = await replacing;
2639
4167
  if (progress.reachable) {
2640
- for (const room of held) {
2641
- if (!expired.includes(room.locator) && !reloading.includes(room)) {
4168
+ for (const room of discovered.values()) {
4169
+ if (replicas.has(room.locator) &&
4170
+ !expired.includes(room.locator) &&
4171
+ !reloading.includes(room.locator)) {
2642
4172
  await confirmModels(room, local);
2643
4173
  }
2644
4174
  }
@@ -2655,14 +4185,29 @@ export function composeRuntime(options) {
2655
4185
  await finishOpening(progress, live);
2656
4186
  });
2657
4187
  }
4188
+ function owedReloads(expired) {
4189
+ const reloading = [...discovered.values()]
4190
+ .filter((room) => replicas.has(room.locator) &&
4191
+ !expired.includes(room.locator) &&
4192
+ (owed.has(room.locator) || revoked(room.locator)))
4193
+ .map((room) => room.locator);
4194
+ owed.clear();
4195
+ for (const locator of reloading)
4196
+ catching.add(locator);
4197
+ liveQueries.recover();
4198
+ return reloading;
4199
+ }
2658
4200
  async function reloadNow(replaced, progress) {
4201
+ const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
4202
+ for (const room of reloaded)
4203
+ reloadsDue.set(room.locator, generation);
2659
4204
  try {
2660
4205
  const local = await openLocal();
2661
- return await Promise.allSettled([...discovered.values()]
2662
- .filter((room) => replicas.has(room.locator) && replaced(room))
2663
- .map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
4206
+ return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
2664
4207
  }
2665
4208
  catch {
4209
+ for (const room of reloaded)
4210
+ reloadsDue.delete(room.locator);
2666
4211
  progress.reachable = false;
2667
4212
  return [];
2668
4213
  }
@@ -2779,6 +4324,7 @@ export function composeRuntime(options) {
2779
4324
  progress.complete = false;
2780
4325
  return;
2781
4326
  }
4327
+ reads.applied();
2782
4328
  submitAll();
2783
4329
  for (const locator of stale)
2784
4330
  if (repairable.has(locator))
@@ -2787,10 +4333,11 @@ export function composeRuntime(options) {
2787
4333
  if (status.providerUnreachable)
2788
4334
  conversations.reconnected();
2789
4335
  const reading = { complete: true, reachable: true };
2790
- if (demand.conversations)
2791
- await conversations.catchUp(reading);
4336
+ await conversations.catchUp(reading);
2792
4337
  if (progress.reachable && reading.reachable)
2793
4338
  await conversations.resend(reading);
4339
+ if (progress.reachable)
4340
+ void recovery.attempt('calls', resendCalls);
2794
4341
  if (!reading.reachable) {
2795
4342
  progress.reachable = false;
2796
4343
  resendLater();
@@ -2814,7 +4361,7 @@ export function composeRuntime(options) {
2814
4361
  if (synced && !recorded) {
2815
4362
  const completedAt = new Date().toISOString();
2816
4363
  try {
2817
- await (await openLocal()).complete(completedAt);
4364
+ await (await openLocal()).fact('lastCompleted', completedAt);
2818
4365
  if (!live())
2819
4366
  return;
2820
4367
  recorded = true;
@@ -2851,11 +4398,11 @@ export function composeRuntime(options) {
2851
4398
  return;
2852
4399
  const room = discovered.get(locator);
2853
4400
  const held = replicas.get(locator);
2854
- if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
4401
+ if (room === undefined || room.replica.admission !== 'admitted')
2855
4402
  return;
2856
4403
  if (held !== undefined &&
2857
- (held.confirmed.record.replica.selection !== room.replica.selection ||
2858
- options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
4404
+ (held.record.replica.selection !== room.replica.selection ||
4405
+ options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
2859
4406
  return;
2860
4407
  }
2861
4408
  const during = underway?.generation === generation ? underway.progress : null;
@@ -2902,18 +4449,29 @@ export function composeRuntime(options) {
2902
4449
  return;
2903
4450
  if (event.kind === 'opened') {
2904
4451
  socketOpen = true;
4452
+ connectionLive();
2905
4453
  generation += 1;
4454
+ reads.opened();
2906
4455
  conversations.reopened();
2907
4456
  recovery.resume();
2908
4457
  void sync('opened');
2909
4458
  }
2910
4459
  else if (event.kind === 'closed')
2911
- closedSocket(event.revoked);
4460
+ closedSocket(event.revoked, event.lost);
2912
4461
  else if (event.kind === 'opening')
2913
4462
  void applyOpening(event.expired);
4463
+ else if (event.kind === 'authority-changed')
4464
+ void rediscover(event.scope);
2914
4465
  else
2915
4466
  void applyRoom(event.locator, event.roomVersion);
2916
- }, receipts);
4467
+ }, receipts, heldRevisions);
4468
+ }
4469
+ async function heldRevisions(locator, paths) {
4470
+ const held = replicas.get(locator);
4471
+ if (held === undefined || paths.length === 0)
4472
+ return new Map();
4473
+ const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
4474
+ return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
2917
4475
  }
2918
4476
  function writesHeld() {
2919
4477
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -2965,7 +4523,7 @@ export function composeRuntime(options) {
2965
4523
  if (!live())
2966
4524
  return;
2967
4525
  await conversations.reconcile();
2968
- if (conversations.current() || !demand.conversations) {
4526
+ if (conversations.current()) {
2969
4527
  show();
2970
4528
  return;
2971
4529
  }
@@ -2983,10 +4541,10 @@ export function composeRuntime(options) {
2983
4541
  if (closed)
2984
4542
  return;
2985
4543
  closed = true;
2986
- if (narrowing !== null)
2987
- clearTimeout(narrowing);
2988
4544
  if (rechecking !== null)
2989
4545
  clearInterval(rechecking);
4546
+ connectionLive();
4547
+ lockListeners.clear();
2990
4548
  readinessWatches.clear();
2991
4549
  withdrawStream?.();
2992
4550
  withdrawStream = undefined;
@@ -3005,7 +4563,7 @@ export function composeRuntime(options) {
3005
4563
  statusListeners.clear();
3006
4564
  serviceModeListeners.clear();
3007
4565
  memberListeners.clear();
3008
- readSide.close();
4566
+ liveQueries.close();
3009
4567
  queueSubscriptions.clear();
3010
4568
  roomDraftListeners.clear();
3011
4569
  presentationListeners.clear();
@@ -3032,22 +4590,63 @@ export function composeRuntime(options) {
3032
4590
  if (options.adapter.roomStream === undefined)
3033
4591
  void sync('unstreamed');
3034
4592
  function planned(input, reach) {
3035
- return planOf(input, roomFacts(reach), planReach(reach));
4593
+ const plan = planOf(input, roomFacts(reach), planReach(reach));
4594
+ const named = plan.paths !== null &&
4595
+ plan.rooms !== null &&
4596
+ plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
4597
+ return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
3036
4598
  }
3037
4599
  function watchPlan(plan, deliver) {
3038
- return readSide.watch(plan, identityOf(plan), () => {
4600
+ let seeded = false;
4601
+ let pending = true;
4602
+ const shown = () => {
4603
+ const seed = seeds.get(plan.key);
4604
+ if (seed === undefined || seeded || !pending)
4605
+ return;
4606
+ seeded = true;
4607
+ deliver(seededResult(seed));
4608
+ };
4609
+ const watchers = seedWatchers.get(plan.key) ?? new Set();
4610
+ seedWatchers.set(plan.key, watchers);
4611
+ watchers.add(shown);
4612
+ const stop = liveQueries.watch(plan, identityOf(plan), () => {
3039
4613
  const cover = covered(plan);
3040
4614
  awaitPreparation(cover);
3041
- return cover;
3042
- }, deliver);
4615
+ return seedBehind(plan, cover);
4616
+ }, (message) => {
4617
+ const answered = message;
4618
+ pending = whole(answered) && 'pending' in answered.result;
4619
+ if (pending && seeds.has(plan.key)) {
4620
+ shown();
4621
+ return;
4622
+ }
4623
+ seeded = false;
4624
+ deliver(message);
4625
+ });
4626
+ return () => {
4627
+ watchers.delete(shown);
4628
+ if (watchers.size === 0)
4629
+ seedWatchers.delete(plan.key);
4630
+ stop();
4631
+ };
3043
4632
  }
3044
- async function searched(input, reach) {
3045
- const plan = searchPlanOf(input);
3046
- const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
4633
+ async function searched(input, reach, locked) {
4634
+ const asked = searchPlanOf(input);
4635
+ const plan = { ...asked, limit: capped(asked.limit) };
4636
+ const filter = {
4637
+ ...filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)),
4638
+ locked,
4639
+ };
4640
+ const unlocking = (filter.rooms ?? [])
4641
+ .filter((room) => locked.includes(room))
4642
+ .map((room) => ({ room, awaits: 'unlock' }));
3047
4643
  for (;;) {
3048
4644
  const cover = covered(filter);
3049
4645
  awaitPreparation(cover);
3050
- const pending = 'pending' in cover ? [...cover.pending] : [];
4646
+ const pending = [
4647
+ ...unlocking,
4648
+ ...('pending' in cover ? cover.pending : []),
4649
+ ];
3051
4650
  const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3052
4651
  const held = covered({
3053
4652
  ...filter,
@@ -3083,20 +4682,31 @@ export function composeRuntime(options) {
3083
4682
  const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
3084
4683
  const seen = searchIndex.changes();
3085
4684
  await searchIndex.caughtUp();
3086
- const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(answering);
4685
+ const selected = roomsAlone(answering) ? null : await everyRow(answering);
3087
4686
  if (selected !== null && 'pending' in selected)
3088
4687
  continue;
3089
4688
  if (searchIndex.changes() !== seen ||
3090
4689
  JSON.stringify(covered(answering)) !== JSON.stringify(answered))
3091
4690
  continue;
3092
- const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, complete ? null : new Set(answering.rooms));
4691
+ const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms !== null
4692
+ ? new Set(answering.rooms)
4693
+ : locked.length === 0
4694
+ ? null
4695
+ : new Set(reachedRooms(answering)), plan.collection
4696
+ ? new Map(answered.coverage.rooms.map(({ room }) => [
4697
+ room,
4698
+ replicas.get(room)?.record.typedSelection?.roots ?? [],
4699
+ ]))
4700
+ : null);
3093
4701
  const coverage = {
3094
4702
  rooms: answered.coverage.rooms,
3095
4703
  conversations: searchedConversations,
3096
4704
  index: searchIndex.generation(),
3097
4705
  overlay: answered.coverage.overlay,
3098
4706
  };
3099
- return complete ? { ...hits, coverage } : { pending, ...hits, coverage };
4707
+ return complete
4708
+ ? { ...hits, coverage }
4709
+ : { pending, documents: hits.documents, messages: hits.messages, coverage };
3100
4710
  }
3101
4711
  }
3102
4712
  const watch = (topic, input, deliver) => {
@@ -3105,13 +4715,17 @@ export function composeRuntime(options) {
3105
4715
  roomDraftListeners.add(listener);
3106
4716
  void exclusive(async () => {
3107
4717
  await openLocal();
3108
- if (closed || !roomDraftListeners.has(listener))
3109
- return;
3110
- listener.ready = true;
3111
4718
  }).then(() => {
3112
- if (closed || !listener.ready || !roomDraftListeners.has(listener))
4719
+ if (closed || !roomDraftListeners.has(listener))
3113
4720
  return;
3114
- listener.notify(snapshotRoomDrafts());
4721
+ const snapshot = snapshotRoomDrafts();
4722
+ draftsShown = draftsShown.then(async () => {
4723
+ const value = await snapshot.catch(() => null);
4724
+ if (value === null || closed || !roomDraftListeners.has(listener))
4725
+ return;
4726
+ listener.ready = true;
4727
+ listener.notify(value);
4728
+ });
3115
4729
  }, () => { });
3116
4730
  return () => {
3117
4731
  roomDraftListeners.delete(listener);
@@ -3206,7 +4820,7 @@ export function composeRuntime(options) {
3206
4820
  return listed();
3207
4821
  },
3208
4822
  query: ((input) => rowsQuery(planned(input, null))),
3209
- search: (input) => searched(input, null),
4823
+ search: (input) => searched(input, null, []),
3210
4824
  types(locator) {
3211
4825
  return {
3212
4826
  collection: modelTypes(modelOf(locator)),
@@ -3226,9 +4840,9 @@ export function composeRuntime(options) {
3226
4840
  return listed;
3227
4841
  });
3228
4842
  },
3229
- read(room, path) {
4843
+ async read(room, path) {
3230
4844
  const record = roomDrafts.get(room)?.get(path);
3231
- return record === undefined ? null : roomDraftOf(record);
4845
+ return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
3232
4846
  },
3233
4847
  write(room, path, revision, input) {
3234
4848
  return exclusive(async () => {
@@ -3238,11 +4852,11 @@ export function composeRuntime(options) {
3238
4852
  const local = await openLocal();
3239
4853
  const current = roomDrafts.get(room)?.get(path);
3240
4854
  if ((current?.revision ?? null) !== revision) {
3241
- return staleWrite(current === undefined ? null : roomDraftOf(current));
4855
+ return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
3242
4856
  }
3243
4857
  const file = parts.file === undefined
3244
4858
  ? undefined
3245
- : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
4859
+ : await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
3246
4860
  const record = {
3247
4861
  locator: room,
3248
4862
  path,
@@ -3250,10 +4864,12 @@ export function composeRuntime(options) {
3250
4864
  parts: held(current?.parts, parts, file),
3251
4865
  returned: current?.returned ?? null,
3252
4866
  };
3253
- await local.keepRoomDraft(record);
4867
+ await local.keepRoomDraft(record, parts.file === undefined || file === undefined
4868
+ ? []
4869
+ : [{ key: file.localId, blob: parts.file.blob }]);
3254
4870
  rememberRoomDraft(record);
3255
- publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
3256
- return { outcome: 'written', roomDraft: roomDraftOf(record) };
4871
+ publishChangeSet(changeSet(room, { drafts: [path] }));
4872
+ return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
3257
4873
  });
3258
4874
  },
3259
4875
  discard(room, path, revision) {
@@ -3261,11 +4877,11 @@ export function composeRuntime(options) {
3261
4877
  const local = await openLocal();
3262
4878
  const current = roomDrafts.get(room)?.get(path);
3263
4879
  if (current === undefined || current.revision !== revision) {
3264
- return staleWrite(current === undefined ? null : roomDraftOf(current));
4880
+ return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
3265
4881
  }
3266
4882
  await local.discardRoomDraft(room, path);
3267
4883
  forgetRoomDraft(room, path);
3268
- publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
4884
+ publishChangeSet(changeSet(room, { drafts: [path] }));
3269
4885
  moveTo({ ...status, ...attention() });
3270
4886
  return { outcome: 'written', roomDraft: null };
3271
4887
  });
@@ -3281,29 +4897,39 @@ export function composeRuntime(options) {
3281
4897
  });
3282
4898
  },
3283
4899
  write(app, value) {
3284
- return exclusive(async () => {
3285
- const local = await openLocal();
3286
- const stored = structuredClone(value);
3287
- await local.present({ app, value: stored });
3288
- presentations.set(app, stored);
3289
- publishChangeSet(changeSet('', null, null, { presentation: app }));
3290
- });
4900
+ return keepPresentation(app, () => structuredClone(value));
4901
+ },
4902
+ edit(app, members) {
4903
+ return keepPresentation(app, (held) => Object.fromEntries([
4904
+ ...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(members, member)),
4905
+ ...Object.entries(structuredClone(members)).filter(([, value]) => value !== null),
4906
+ ]));
3291
4907
  },
3292
4908
  },
3293
4909
  save(room, named, writtenBy) {
3294
4910
  return exclusive(() => saveRoomDrafts(room, named, writtenBy));
3295
4911
  },
4912
+ saveParts(room, parts, writtenBy) {
4913
+ return exclusive(() => saveParts(room, parts, writtenBy));
4914
+ },
3296
4915
  history(locator, path, query) {
3297
4916
  return readHistory(locator, path, query);
3298
4917
  },
4918
+ resolve(room, path, destination) {
4919
+ return resolveReference(room, path, destination);
4920
+ },
3299
4921
  binaryFile: {
4922
+ part: readPart,
3300
4923
  read: readBinaryPath,
4924
+ link: linkBinaryPath,
3301
4925
  readReference: readBinaryReference,
3302
4926
  },
3303
- call(name, input) {
3304
- return invokeAction(name, input);
4927
+ async call(name, input) {
4928
+ await ready;
4929
+ const room = calledRooms(name, input).find((one) => locked(one));
4930
+ return room === undefined ? recoveredAction(name, input) : lockedRoom(room);
3305
4931
  },
3306
- reached(locator, roomVersion, path) {
4932
+ async reached(locator, roomVersion, path) {
3307
4933
  return path === undefined
3308
4934
  ? reachedBy(locator, roomVersion)
3309
4935
  : writtenAfter(locator, roomVersion, path);
@@ -3311,6 +4937,7 @@ export function composeRuntime(options) {
3311
4937
  log() {
3312
4938
  return journal.export();
3313
4939
  },
4940
+ cleanUp,
3314
4941
  persistence() {
3315
4942
  return persistence === 'granted';
3316
4943
  },
@@ -3346,13 +4973,17 @@ export function composeRuntime(options) {
3346
4973
  watchPlan,
3347
4974
  search: searched,
3348
4975
  watchReadiness,
4976
+ locked,
4977
+ lockedRooms,
4978
+ watchLocks,
4979
+ located,
3349
4980
  };
3350
4981
  return {
3351
4982
  md,
4983
+ seed,
3352
4984
  stop,
3353
4985
  lose: loseRooms,
3354
4986
  watch,
3355
- acquire,
3356
4987
  persisted(answer) {
3357
4988
  if (answer === persistence)
3358
4989
  return;