@agentshouse/mdruntime 0.1.0-cfcbb238a5fa0 → 0.1.0-cfe148b32e3d8

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 (211) hide show
  1. package/AGENTS.md +229 -211
  2. package/README.md +9 -8
  3. package/dist/adapter.d.ts +69 -35
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/attached-model.d.ts +4 -1
  6. package/dist/attached-model.d.ts.map +1 -1
  7. package/dist/attached-model.js +13 -3
  8. package/dist/binary.d.ts +0 -5
  9. package/dist/binary.d.ts.map +1 -1
  10. package/dist/binary.js +1 -1
  11. package/dist/bootstrap.d.ts +24 -6
  12. package/dist/bootstrap.d.ts.map +1 -1
  13. package/dist/bootstrap.js +167 -82
  14. package/dist/chain.d.ts +11 -0
  15. package/dist/chain.d.ts.map +1 -0
  16. package/dist/chain.js +64 -0
  17. package/dist/change-set.d.ts +9 -16
  18. package/dist/change-set.d.ts.map +1 -1
  19. package/dist/change-set.js +11 -89
  20. package/dist/change.d.ts +31 -1
  21. package/dist/change.d.ts.map +1 -1
  22. package/dist/change.js +10 -1
  23. package/dist/chat/earlier.d.ts +10 -0
  24. package/dist/chat/earlier.d.ts.map +1 -0
  25. package/dist/chat/earlier.js +54 -0
  26. package/dist/chat/house.d.ts +25 -0
  27. package/dist/chat/house.d.ts.map +1 -0
  28. package/dist/chat/house.js +38 -0
  29. package/dist/chat/input.d.ts +3 -1
  30. package/dist/chat/input.d.ts.map +1 -1
  31. package/dist/chat/input.js +11 -18
  32. package/dist/chat/list.d.ts +2 -1
  33. package/dist/chat/list.d.ts.map +1 -1
  34. package/dist/chat/list.js +45 -5
  35. package/dist/chat/markdown.d.ts +2 -2
  36. package/dist/chat/markdown.d.ts.map +1 -1
  37. package/dist/chat/markdown.js +80 -39
  38. package/dist/chat/message.d.ts +1 -0
  39. package/dist/chat/message.d.ts.map +1 -1
  40. package/dist/chat/message.js +73 -11
  41. package/dist/chat/messages.d.ts +14 -1
  42. package/dist/chat/messages.d.ts.map +1 -1
  43. package/dist/chat/messages.js +79 -24
  44. package/dist/chat/name.d.ts +1 -0
  45. package/dist/chat/name.d.ts.map +1 -1
  46. package/dist/chat/name.js +1 -1
  47. package/dist/chat/opened.d.ts +1 -0
  48. package/dist/chat/opened.d.ts.map +1 -1
  49. package/dist/chat/opened.js +2 -0
  50. package/dist/chat/overlay.d.ts +1 -0
  51. package/dist/chat/overlay.d.ts.map +1 -1
  52. package/dist/chat/overlay.js +11 -5
  53. package/dist/chat/parts.d.ts +1 -0
  54. package/dist/chat/parts.d.ts.map +1 -1
  55. package/dist/chat/parts.js +3 -2
  56. package/dist/chat/pieces.d.ts +62 -0
  57. package/dist/chat/pieces.d.ts.map +1 -0
  58. package/dist/chat/pieces.js +1110 -0
  59. package/dist/chat/provider.d.ts +8 -1
  60. package/dist/chat/provider.d.ts.map +1 -1
  61. package/dist/chat/provider.js +98 -92
  62. package/dist/chat/question.d.ts +1 -1
  63. package/dist/chat/question.d.ts.map +1 -1
  64. package/dist/chat/question.js +7 -18
  65. package/dist/chat/ready.d.ts +5 -2
  66. package/dist/chat/ready.d.ts.map +1 -1
  67. package/dist/chat/ready.js +7 -5
  68. package/dist/chat/room-file.d.ts +8 -0
  69. package/dist/chat/room-file.d.ts.map +1 -0
  70. package/dist/chat/room-file.js +72 -0
  71. package/dist/chat/steady.d.ts +3 -0
  72. package/dist/chat/steady.d.ts.map +1 -0
  73. package/dist/chat/steady.js +26 -0
  74. package/dist/chat/style.d.ts.map +1 -1
  75. package/dist/chat/style.js +6 -1
  76. package/dist/chat.d.ts +3 -2
  77. package/dist/chat.d.ts.map +1 -1
  78. package/dist/chat.js +2 -2
  79. package/dist/client.d.ts +11 -4
  80. package/dist/client.d.ts.map +1 -1
  81. package/dist/client.js +35 -5
  82. package/dist/collection.d.ts +6 -19
  83. package/dist/collection.d.ts.map +1 -1
  84. package/dist/collection.js +30 -287
  85. package/dist/compile.d.ts +16 -0
  86. package/dist/compile.d.ts.map +1 -0
  87. package/dist/compile.js +721 -0
  88. package/dist/connection.d.ts +19 -9
  89. package/dist/connection.d.ts.map +1 -1
  90. package/dist/connection.js +2392 -848
  91. package/dist/conversation.d.ts +14 -3
  92. package/dist/conversation.d.ts.map +1 -1
  93. package/dist/conversation.js +198 -89
  94. package/dist/digest.d.ts +7 -0
  95. package/dist/digest.d.ts.map +1 -0
  96. package/dist/digest.js +113 -0
  97. package/dist/durability.d.ts +3 -1
  98. package/dist/durability.d.ts.map +1 -1
  99. package/dist/entry.d.ts +3 -1
  100. package/dist/entry.d.ts.map +1 -1
  101. package/dist/entry.js +18 -23
  102. package/dist/file.d.ts +2 -10
  103. package/dist/file.d.ts.map +1 -1
  104. package/dist/file.js +3 -120
  105. package/dist/fulltext.d.ts +27 -0
  106. package/dist/fulltext.d.ts.map +1 -0
  107. package/dist/fulltext.js +368 -0
  108. package/dist/host.d.ts +1 -4
  109. package/dist/host.d.ts.map +1 -1
  110. package/dist/host.js +21 -8
  111. package/dist/identity.d.ts +0 -1
  112. package/dist/identity.d.ts.map +1 -1
  113. package/dist/identity.js +0 -3
  114. package/dist/index.d.ts +11 -8
  115. package/dist/index.d.ts.map +1 -1
  116. package/dist/index.js +3 -0
  117. package/dist/live.d.ts +22 -0
  118. package/dist/live.d.ts.map +1 -0
  119. package/dist/live.js +123 -0
  120. package/dist/memory.d.ts +3 -2
  121. package/dist/memory.d.ts.map +1 -1
  122. package/dist/memory.js +30 -20
  123. package/dist/messages.d.ts +28 -0
  124. package/dist/messages.d.ts.map +1 -0
  125. package/dist/messages.js +90 -0
  126. package/dist/name.d.ts +4 -0
  127. package/dist/name.d.ts.map +1 -0
  128. package/dist/name.js +122 -0
  129. package/dist/observation.d.ts +14 -0
  130. package/dist/observation.d.ts.map +1 -1
  131. package/dist/observation.js +93 -3
  132. package/dist/opened.d.ts +22 -0
  133. package/dist/opened.d.ts.map +1 -0
  134. package/dist/opened.js +107 -0
  135. package/dist/outcome.d.ts +34 -4
  136. package/dist/outcome.d.ts.map +1 -1
  137. package/dist/outcome.js +25 -0
  138. package/dist/overlay.d.ts +2 -5
  139. package/dist/overlay.d.ts.map +1 -1
  140. package/dist/overlay.js +87 -172
  141. package/dist/query.d.ts +26 -2
  142. package/dist/query.d.ts.map +1 -1
  143. package/dist/query.js +41 -6
  144. package/dist/reach.d.ts +5 -3
  145. package/dist/reach.d.ts.map +1 -1
  146. package/dist/reach.js +158 -57
  147. package/dist/react.d.ts +1 -2
  148. package/dist/react.d.ts.map +1 -1
  149. package/dist/react.js +2 -14
  150. package/dist/readiness.d.ts +18 -3
  151. package/dist/readiness.d.ts.map +1 -1
  152. package/dist/readiness.js +1 -1
  153. package/dist/reads.d.ts.map +1 -1
  154. package/dist/reads.js +4 -2
  155. package/dist/recovery.d.ts +2 -2
  156. package/dist/recovery.d.ts.map +1 -1
  157. package/dist/recovery.js +8 -7
  158. package/dist/refresh.d.ts +11 -13
  159. package/dist/refresh.d.ts.map +1 -1
  160. package/dist/refresh.js +78 -51
  161. package/dist/replica.d.ts +57 -33
  162. package/dist/replica.d.ts.map +1 -1
  163. package/dist/replica.js +295 -186
  164. package/dist/room-draft.d.ts +3 -1
  165. package/dist/room-draft.d.ts.map +1 -1
  166. package/dist/room-draft.js +29 -22
  167. package/dist/rows.d.ts +29 -30
  168. package/dist/rows.d.ts.map +1 -1
  169. package/dist/rows.js +113 -64
  170. package/dist/save.d.ts +51 -14
  171. package/dist/save.d.ts.map +1 -1
  172. package/dist/save.js +518 -180
  173. package/dist/search.d.ts +23 -42
  174. package/dist/search.d.ts.map +1 -1
  175. package/dist/search.js +28 -182
  176. package/dist/seed.d.ts.map +1 -1
  177. package/dist/seed.js +18 -4
  178. package/dist/shared.d.ts +8 -3
  179. package/dist/shared.d.ts.map +1 -1
  180. package/dist/shared.js +129 -66
  181. package/dist/store.d.ts +97 -88
  182. package/dist/store.d.ts.map +1 -1
  183. package/dist/store.js +335 -659
  184. package/dist/stored.d.ts +145 -0
  185. package/dist/stored.d.ts.map +1 -0
  186. package/dist/stored.js +567 -0
  187. package/dist/tables.d.ts +342 -0
  188. package/dist/tables.d.ts.map +1 -0
  189. package/dist/tables.js +576 -0
  190. package/dist/turn.d.ts +5 -3
  191. package/dist/turn.d.ts.map +1 -1
  192. package/dist/turn.js +51 -21
  193. package/dist/validation.d.ts +30 -0
  194. package/dist/validation.d.ts.map +1 -0
  195. package/dist/validation.js +162 -0
  196. package/dist/windows.d.ts +22 -0
  197. package/dist/windows.d.ts.map +1 -0
  198. package/dist/windows.js +89 -0
  199. package/dist/wire.d.ts +29 -1
  200. package/dist/wire.d.ts.map +1 -1
  201. package/dist/wire.js +8 -1
  202. package/dist/words.d.ts +19 -0
  203. package/dist/words.d.ts.map +1 -0
  204. package/dist/words.js +150 -0
  205. package/package.json +10 -4
  206. package/dist/acquisition.d.ts +0 -14
  207. package/dist/acquisition.d.ts.map +0 -1
  208. package/dist/acquisition.js +0 -24
  209. package/dist/read-side.d.ts +0 -25
  210. package/dist/read-side.d.ts.map +0 -1
  211. package/dist/read-side.js +0 -642
@@ -1,46 +1,41 @@
1
- import { attachedModel } from './attached-model.js';
1
+ import { escapePointer, parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
2
+ import { attachedModel, NO_ROOM_TYPES, roomTypesOf } from './attached-model.js';
2
3
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
3
4
  import { observing, whole } from './observation.js';
4
- import { filterOf, identityOf, planOf, queryKey, } from './query.js';
5
- import { createReadSide } from './read-side.js';
5
+ import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
6
+ import { createLiveQueries } from './live.js';
6
7
  import { seededResult } from './seed.js';
7
- import { markdownPath, roomOfKey } from './rows.js';
8
- import { createSearchIndex, searchPlanOf, } from './search.js';
8
+ import { documentKey, roomOfKey } from './rows.js';
9
+ import { searchPlanOf } from './search.js';
10
+ import { capped, MAXIMUM_WINDOW } from './windows.js';
9
11
  import { bootstrapRoom } from './bootstrap.js';
10
- import { changeSet, entering, mergeChangeSets } from './change-set.js';
12
+ import { changeSet, mergeChangeSets } from './change-set.js';
11
13
  import { createConversationStore, } from './conversation.js';
12
- import { queuedFile } from './change.js';
13
- 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';
14
16
  import { runtimeError } from './failure.js';
15
17
  import { createLog } from './log.js';
16
18
  import { binaryRecord } from './binary.js';
17
- import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
19
+ import { openedFiles } from './opened.js';
20
+ import { ABSENT_TARGET, absentBinary, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
18
21
  import { createRecovery } from './recovery.js';
19
22
  import { doorReads } from './reads.js';
20
- import { refreshRoom } from './refresh.js';
21
- import { buildBatch, joined, limitedIn, requeued } from './save.js';
22
- import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
23
- import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
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';
24
27
  import { same, saving, STARTING, transferring, } from './status.js';
25
28
  import { detectStorageLoss, nextGeneration } from './durability.js';
26
- import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
27
29
  import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
28
30
  import { FencedMap } from './fence.js';
29
- import { tally } from './shared.js';
30
- import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
31
+ import { sameBinary } from './overlay.js';
32
+ import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
31
33
  import { identityTime, roomDraftRevision } from './identity.js';
32
34
  import { beneath, normalize } from './paths.js';
33
35
  import { openRuntimeStore, } from './store.js';
36
+ import { changedCleanly, sameComposition, validatedCollection, } from './validation.js';
34
37
  import { unconfirmed } from './transfer.js';
35
38
  import { governs, holds, narrowed, planReach, } from './reach.js';
36
- function roomsAlone(plan) {
37
- return (plan.reach === null &&
38
- plan.root.types === null &&
39
- plan.root.where === null &&
40
- plan.root.pathPrefix === null &&
41
- plan.hop === null &&
42
- plan.backlinks === null);
43
- }
44
39
  function within(root, path) {
45
40
  return root === '' || beneath(root, path);
46
41
  }
@@ -58,19 +53,29 @@ const OPENING_LANE = 'opening';
58
53
  const CONVERSATION_LANE = 'conversations';
59
54
  const ROOM_READS = 256;
60
55
  const ROOM_ATTEMPTS = 16;
61
- const outside = new Map();
62
- function outsideOf(roots) {
63
- const key = JSON.stringify(roots);
64
- const held = outside.get(key);
65
- if (held !== undefined)
66
- return held;
67
- const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
68
- const made = {
69
- documents: (path) => (markdownPath(path) ? counted(path) : 0),
70
- binaries: counted,
71
- };
72
- outside.set(key, made);
73
- return made;
56
+ const ERASE_BATCH = 512;
57
+ const REDERIVE_BATCH = 256;
58
+ const WALK_BYTES = 1 << 20;
59
+ const QUERY_BYTES = 1 << 20;
60
+ const STAGE_BYTES = 1 << 20;
61
+ const STAGE_FLOOR = 1 << 16;
62
+ // A batch aims at this much database work, so a Save or a query waits behind at most one short write.
63
+ const STAGE_AIM_MS = 50;
64
+ const PARTS_HELD = 4;
65
+ // Bytes still arriving are reported at most this often, well inside the 2 s progress bound, so readiness is not rebuilt for every chunk the network hands over.
66
+ const ARRIVAL_REPORT_MS = 250;
67
+ const INSTALL_STEPS = [
68
+ 'writing',
69
+ 'downloading',
70
+ 'replaying',
71
+ 'listing',
72
+ ];
73
+ const CLEAN_CHANGES = 2_000;
74
+ // A Room that locks offline stays readable this long after the connection drops, so a laptop waking or changing network keeps working while a disconnected device locks soon after.
75
+ const LOCK_GRACE_MS = 120_000;
76
+ function operationOf(input) {
77
+ const operation = input?.operation_id;
78
+ return typeof operation === 'string' ? operation : null;
74
79
  }
75
80
  function abandonedOn(signal) {
76
81
  return new Promise((resolve) => {
@@ -83,11 +88,14 @@ function abandonedOn(signal) {
83
88
  function roomLane(locator) {
84
89
  return `room ${locator}`;
85
90
  }
91
+ function pendingKey(locator, id) {
92
+ return `${locator}\u0000${id}`;
93
+ }
86
94
  export function composeRuntime(options) {
87
95
  const localSaves = options.adapter.localSaves === true;
96
+ const roomTypes = roomTypesOf(options.adapter.roomTypes);
88
97
  let authorization = options.authorization;
89
98
  let opening;
90
- let store;
91
99
  const fences = new Map();
92
100
  const isFenced = (locator) => fences.has(locator);
93
101
  let fencings = 0;
@@ -148,6 +156,7 @@ export function composeRuntime(options) {
148
156
  let handed = false;
149
157
  const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
150
158
  const repairing = new Set();
159
+ const calling = new Set();
151
160
  const repairable = new Map();
152
161
  let outcomeRetention = null;
153
162
  const answering = new Map();
@@ -158,6 +167,7 @@ export function composeRuntime(options) {
158
167
  let discovering = null;
159
168
  let opened = null;
160
169
  const owed = new Set();
170
+ const reloadsDue = new Map();
161
171
  const lanes = new Map();
162
172
  let roomReads = { generation, free: ROOM_READS, waiting: [] };
163
173
  const working = new Set();
@@ -169,23 +179,22 @@ export function composeRuntime(options) {
169
179
  const queueSubscriptions = new Set();
170
180
  const roomDraftListeners = new Set();
171
181
  const presentationListeners = new Set();
172
- const readSide = createReadSide();
182
+ const liveQueries = createLiveQueries((plan, cover) => answeredPlan(plan, cover));
173
183
  let withdrawConversations;
174
184
  let currencyQueued = null;
175
185
  let socketOpen = false;
186
+ const confirmed = new Set();
187
+ let lockGrace = null;
188
+ const lockListeners = new Set();
176
189
  let serviceMode = 'open';
177
190
  let haltedRefusal = false;
178
191
  let openSignals = 0;
179
192
  const serviceModeListeners = new Set();
180
193
  let member = null;
181
194
  const memberListeners = new Set();
182
- const prepared = new Set();
183
- const projecting = new Set();
184
195
  const unpreparedDemand = new Set();
185
- const unindexable = new Set();
196
+ const unpreparable = new Set();
186
197
  let preparing = false;
187
- let demand = WHOLE_DEMAND;
188
- let narrowing = null;
189
198
  let targets = {};
190
199
  let persistence = options.shell.persistence;
191
200
  let capacity = null;
@@ -195,26 +204,41 @@ export function composeRuntime(options) {
195
204
  let readinessQueued = false;
196
205
  const catching = new Set();
197
206
  const transfers = new FencedMap(isFenced);
207
+ const installs = new Map();
208
+ const arrivals = new Map();
209
+ let arrivalReported = 0;
210
+ let onScreen = options.shell.onScreen;
211
+ const shownWaiters = [];
212
+ let partsFree = PARTS_HELD;
213
+ const partWaiters = [];
214
+ let stageBytes = STAGE_FLOOR * 4;
215
+ let installWrites = Promise.resolve();
198
216
  const conversations = createConversationStore({
199
217
  agents: declaredAgents,
200
- documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
218
+ documents: (declaration, after) => rowsQuery(planOf({
219
+ rooms: [declaration.locator],
220
+ types: [declaration.type],
221
+ select: {
222
+ fields: [
223
+ declaration.identity,
224
+ declaration.name,
225
+ ...(declaration.retired === undefined ? [] : [declaration.retired]),
226
+ ].map((field) => `/${escapePointer(field)}`),
227
+ },
228
+ limit: MAXIMUM_WINDOW,
229
+ ...(after === undefined ? {} : { after }),
230
+ }, roomFacts())),
201
231
  call: (name, input) => invokeAction(name, input),
202
232
  durably,
203
233
  stream: () => options.adapter.conversations,
204
234
  publish: publishChangeSet,
205
- online: () => !status.providerUnreachable && refusal === null,
235
+ online: () => connectionOf() !== 'unreachable' && refusal === null,
206
236
  closed: () => closed,
207
237
  unanswered: resendLater,
208
238
  expired: (operation) => pastHorizon(operation),
209
239
  });
210
- const searchIndex = createSearchIndex({
211
- room: (room) => readSide.room(room),
212
- document: (room, path) => readSide.document(room, path),
213
- });
214
- const roomsToIndex = new Set();
215
- const conversationsToIndex = new Set();
216
- let indexing = false;
217
240
  const journal = createLog();
241
+ const keptFiles = openedFiles(options.database, isFenced);
218
242
  let notifyReady = () => { };
219
243
  const ready = new Promise((resolve) => {
220
244
  notifyReady = resolve;
@@ -232,16 +256,36 @@ export function composeRuntime(options) {
232
256
  if (room.size === 0)
233
257
  roomDrafts.delete(locator);
234
258
  }
235
- function roomDraftOf(record) {
259
+ async function visibleOf(locator, paths) {
260
+ const held = replicas.get(locator);
261
+ if (held === undefined)
262
+ return undefined;
263
+ const local = await openLocal();
264
+ const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
265
+ return { ...slice, collection: collectionOf(slice.documents, compositionOf(held.record)) };
266
+ }
267
+ function roomDraftOf(record, visible) {
236
268
  return structuredClone({
237
269
  room: record.locator,
238
270
  path: record.path,
239
271
  revision: record.revision,
240
272
  parts: record.parts,
241
- changedUnderneath: underneath(record.parts, record.path, replicas.get(record.locator)?.visible),
273
+ changedUnderneath: underneath(record.parts, record.path, visible),
242
274
  returned: record.returned,
243
275
  });
244
276
  }
277
+ async function roomDraftsOf(records) {
278
+ const rooms = new Map();
279
+ for (const record of records) {
280
+ const paths = rooms.get(record.locator) ?? [];
281
+ paths.push(record.path);
282
+ rooms.set(record.locator, paths);
283
+ }
284
+ const layers = new Map();
285
+ for (const [locator, paths] of rooms)
286
+ layers.set(locator, await visibleOf(locator, paths));
287
+ return records.map((record) => roomDraftOf(record, layers.get(record.locator)));
288
+ }
245
289
  function snapshotRoomDrafts() {
246
290
  const records = [...roomDrafts.values()].flatMap((room) => [...room.values()]);
247
291
  records.sort((left, right) => {
@@ -251,23 +295,38 @@ export function composeRuntime(options) {
251
295
  return left.path < right.path ? -1 : 1;
252
296
  return 0;
253
297
  });
254
- return records.map(roomDraftOf);
298
+ return roomDraftsOf(records);
255
299
  }
300
+ let draftsShown = Promise.resolve();
256
301
  function publishRoomDrafts(waitingOnly) {
257
302
  if (closed)
258
303
  return;
259
- const value = snapshotRoomDrafts();
260
- for (const listener of roomDraftListeners) {
261
- if (waitingOnly && listener.ready)
262
- continue;
263
- listener.ready = true;
264
- listener.notify(value);
265
- }
304
+ const snapshot = snapshotRoomDrafts();
305
+ draftsShown = draftsShown.then(async () => {
306
+ const value = await snapshot.catch(() => null);
307
+ if (value === null || closed)
308
+ return;
309
+ for (const listener of roomDraftListeners) {
310
+ if (waitingOnly && listener.ready)
311
+ continue;
312
+ listener.ready = true;
313
+ listener.notify(value);
314
+ }
315
+ });
266
316
  }
267
317
  function presentationOf(app) {
268
318
  const value = presentations.get(app);
269
319
  return value === undefined ? null : structuredClone(value);
270
320
  }
321
+ function keepPresentation(app, next) {
322
+ return exclusive(async () => {
323
+ const local = await openLocal();
324
+ const stored = next(presentations.get(app));
325
+ await local.present({ app, value: stored });
326
+ presentations.set(app, stored);
327
+ publishChangeSet(changeSet('', { presentation: app }));
328
+ });
329
+ }
271
330
  function emitPresentation(app) {
272
331
  if (closed)
273
332
  return;
@@ -316,21 +375,69 @@ export function composeRuntime(options) {
316
375
  return { attentionRequired, reset };
317
376
  }
318
377
  function documentsIn(locator) {
319
- const replica = replicas.get(locator);
320
- if (replica === undefined)
321
- return 0;
322
- const roots = replica.confirmed.record.typedSelection?.roots ?? [];
323
- const weighed = outsideOf(roots);
324
- return (tally(replica.visible.documents, weighed.documents) +
325
- tally(replica.visible.binaries, weighed.binaries));
378
+ return replicas.get(locator)?.count ?? 0;
326
379
  }
327
380
  function listed() {
328
381
  return structuredClone([...rooms.values()].map((room) => {
329
382
  const documents = documentsIn(room.locator);
330
383
  counted.set(room.locator, documents);
331
- return { ...room, documents };
384
+ return locked(room.locator)
385
+ ? {
386
+ ...room,
387
+ model: modelTypes(NO_MODEL),
388
+ typedSelection: null,
389
+ documents: 0,
390
+ locked: true,
391
+ }
392
+ : { ...room, documents, locked: false };
332
393
  }));
333
394
  }
395
+ function locked(locator) {
396
+ return rooms.get(locator)?.replica.offline === 'locked' && !confirmed.has(locator);
397
+ }
398
+ function lockedRooms() {
399
+ return [...rooms.keys()].filter(locked).sort();
400
+ }
401
+ function relocked(before) {
402
+ const after = lockedRooms();
403
+ if (JSON.stringify(after) === JSON.stringify(before))
404
+ return;
405
+ journal.record(`locked ${after.length}`);
406
+ emitRooms();
407
+ for (const notify of [...lockListeners])
408
+ notify();
409
+ }
410
+ function confirmLocks(listed) {
411
+ if (!socketOpen)
412
+ return;
413
+ for (const room of listed) {
414
+ if (room.replica.admission === 'admitted')
415
+ confirmed.add(room.locator);
416
+ }
417
+ }
418
+ function connectionLost() {
419
+ if (lockGrace !== null || closed)
420
+ return;
421
+ lockGrace = setTimeout(() => {
422
+ lockGrace = null;
423
+ if (socketOpen || closed)
424
+ return;
425
+ const before = lockedRooms();
426
+ confirmed.clear();
427
+ relocked(before);
428
+ }, LOCK_GRACE_MS);
429
+ }
430
+ function connectionLive() {
431
+ if (lockGrace !== null)
432
+ clearTimeout(lockGrace);
433
+ lockGrace = null;
434
+ }
435
+ function watchLocks(notify) {
436
+ lockListeners.add(notify);
437
+ return () => {
438
+ lockListeners.delete(notify);
439
+ };
440
+ }
334
441
  function roomFacts(reach = null) {
335
442
  const held = [...rooms.values()]
336
443
  .filter((room) => holds(reach, room.locator))
@@ -349,7 +456,7 @@ export function composeRuntime(options) {
349
456
  return { listed: roomsListed, rooms: held };
350
457
  }
351
458
  function emitRooms() {
352
- readSide.recover();
459
+ liveQueries.recover();
353
460
  queueReadiness();
354
461
  const value = listed();
355
462
  for (const listener of roomListeners)
@@ -379,7 +486,7 @@ export function composeRuntime(options) {
379
486
  if (closed)
380
487
  return;
381
488
  queueReadiness();
382
- const next = status.state === 'synced' && demand.conversations && !conversations.current()
489
+ const next = status.state === 'synced' && !conversations.current()
383
490
  ? { ...status, state: 'syncing' }
384
491
  : status;
385
492
  if (same(shown, next))
@@ -388,17 +495,17 @@ export function composeRuntime(options) {
388
495
  for (const listener of statusListeners)
389
496
  listener(shown);
390
497
  }
391
- function entryOf(batch) {
498
+ function entryOf(locator, batch) {
392
499
  if (localSaves)
393
500
  return Object.freeze({ batch, state: 'local' });
394
- const state = pendings.get(batch.id);
501
+ const state = pendings.get(pendingKey(locator, batch.id));
395
502
  if (state?.accepted !== undefined) {
396
503
  return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
397
504
  }
398
505
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
399
506
  }
400
507
  function entriesOf(locator) {
401
- return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
508
+ return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map((batch) => entryOf(locator, batch));
402
509
  }
403
510
  function emitQueue(locator) {
404
511
  const entries = entriesOf(locator);
@@ -408,62 +515,39 @@ export function composeRuntime(options) {
408
515
  }
409
516
  }
410
517
  function applyChangeSet(set) {
411
- const held = {
412
- ...set,
413
- documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
414
- };
415
- readSide.apply(held);
416
- searchIndex.changed(held);
518
+ liveQueries.touched(set);
417
519
  }
418
- async function referenced(locator) {
419
- for (;;) {
420
- const record = replicas.get(locator)?.confirmed.record;
421
- if (record === undefined || closed)
422
- return;
423
- const held = { text: record.documents, binaries: record.binaries };
424
- const composition = compositionOf(record);
425
- if (referencesIndexed(held, composition))
426
- return;
427
- await indexReferrers(held, composition);
428
- }
520
+ function unpublished(error) {
521
+ journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
522
+ }
523
+ function indexed(locator) {
524
+ return replicas.get(locator)?.indexed === true;
429
525
  }
430
526
  async function prepareReplica(locator) {
431
- await referenced(locator);
432
- const replica = replicas.get(locator);
433
- if (replica === undefined || closed || prepared.has(locator))
434
- return;
435
- const visible = replica.visible;
436
- const paths = [
437
- ...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
438
- ];
439
- const entered = [];
440
- projecting.add(locator);
441
- try {
442
- await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
443
- const current = replicas.get(locator);
444
- if (current === undefined || closed)
445
- return;
446
- const set = entering(locator, current, chunk);
447
- readSide.apply(set);
448
- entered.push(set);
449
- });
450
- if (!replicas.has(locator) || closed)
527
+ for (;;) {
528
+ const held = replicas.get(locator);
529
+ if (held === undefined || closed || held.indexed)
451
530
  return;
452
- prepared.add(locator);
453
- }
454
- finally {
455
- projecting.delete(locator);
531
+ const key = held.record.key;
532
+ const done = await exclusive(async () => (await openLocal()).rederive(key, REDERIVE_BATCH));
533
+ const now = replicas.get(locator);
534
+ if (now?.record.key !== key)
535
+ continue;
536
+ if (!done)
537
+ continue;
538
+ replicas.set(locator, { ...now, indexed: true });
539
+ publishChangeSet(changeSet(locator, { documents: null }));
540
+ liveQueries.recover();
541
+ queueReadiness();
542
+ return;
456
543
  }
457
- conversations.changed(mergeChangeSets(entered));
458
- readSide.recover();
459
- queueReadiness();
460
544
  }
461
545
  function unprepared() {
462
- return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
546
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
463
547
  }
464
- function unbuilt(locator, error) {
465
- unindexable.add(locator);
466
- journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
548
+ function unpreparedBy(locator, error) {
549
+ unpreparable.add(locator);
550
+ journal.record(`preparation failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
467
551
  queueReadiness();
468
552
  }
469
553
  function prepare(locators = []) {
@@ -479,7 +563,7 @@ export function composeRuntime(options) {
479
563
  if (closed)
480
564
  return;
481
565
  unpreparedDemand.delete(waiting);
482
- await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
566
+ await prepareReplica(waiting).catch((error) => unpreparedBy(waiting, error));
483
567
  }
484
568
  }
485
569
  finally {
@@ -487,14 +571,18 @@ export function composeRuntime(options) {
487
571
  }
488
572
  })();
489
573
  }
574
+ function roomsOfReach(reach) {
575
+ return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
576
+ }
490
577
  function reachedRooms(plan) {
491
- const reach = plan.reach;
492
- const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
578
+ const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
493
579
  const named = plan.rooms ??
494
580
  (reached === null
495
581
  ? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
496
582
  : [...reached]);
497
- return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
583
+ return [...new Set(named)]
584
+ .filter((locator) => (reached === null || reached.has(locator)) && !plan.locked.includes(locator))
585
+ .sort();
498
586
  }
499
587
  function awaited(locator) {
500
588
  return (!roomsListed ||
@@ -508,6 +596,7 @@ export function composeRuntime(options) {
508
596
  const pending = [];
509
597
  const coverage = [];
510
598
  const overlay = [];
599
+ const read = [];
511
600
  for (const locator of locators) {
512
601
  const replica = replicas.get(locator);
513
602
  if (replica === undefined) {
@@ -515,21 +604,22 @@ export function composeRuntime(options) {
515
604
  pending.push({ room: locator, awaits: 'acquisition' });
516
605
  continue;
517
606
  }
518
- if (!prepared.has(locator)) {
607
+ if (!replica.indexed) {
519
608
  pending.push({ room: locator, awaits: 'preparation' });
520
609
  continue;
521
610
  }
522
611
  coverage.push({
523
612
  room: locator,
524
- generation: replica.confirmed.record.transfer,
613
+ generation: replica.record.transfer,
525
614
  current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
526
615
  });
616
+ read.push({ locator, key: replica.record.key });
527
617
  for (const batch of replica.queue)
528
618
  overlay.push(batch.id);
529
619
  }
530
620
  if (pending.length > 0)
531
621
  return { pending };
532
- return { coverage: { rooms: coverage, indexes: [], overlay } };
622
+ return { coverage: { rooms: coverage, indexes: [], overlay }, rooms: read };
533
623
  }
534
624
  function awaitPreparation(cover) {
535
625
  if (!('pending' in cover))
@@ -538,66 +628,16 @@ export function composeRuntime(options) {
538
628
  if (waiting.length > 0)
539
629
  prepare(waiting.map(({ room }) => room));
540
630
  }
541
- function acquires(room) {
542
- return (demanded(demand, room) ||
543
- (replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
544
- queuesWithoutReplica.has(room.locator));
545
- }
546
- function acquiredLocators() {
547
- return [...discovered.values()]
548
- .filter(acquires)
549
- .map((room) => room.locator)
550
- .sort();
551
- }
552
- function heldDemand(acquired) {
553
- if (demand.whole)
554
- return null;
555
- return { rooms: acquired.map((room) => room.locator), conversations: demand.conversations };
556
- }
557
- function applyDemand(next) {
558
- const before = acquiredLocators();
559
- const conversed = demand.conversations;
560
- demand = next;
561
- const after = acquiredLocators();
562
- queueReadiness();
563
- if (!conversed && next.conversations)
564
- scheduleCurrency();
565
- if (JSON.stringify(before) === JSON.stringify(after) && conversed === next.conversations)
566
- return;
567
- journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
568
- if (options.adapter.roomStream === undefined)
569
- void sync('unstreamed');
570
- else if (socketOpen)
571
- options.adapter.roomStream.reopen();
572
- }
573
- function acquire(next) {
574
- if (closed)
575
- return;
576
- if (narrowing !== null)
577
- clearTimeout(narrowing);
578
- narrowing = null;
579
- const widening = wider(next, demand);
580
- if (!widening && !narrower(next, demand))
581
- return;
582
- if (widening || !roomsListed) {
583
- applyDemand(next);
584
- return;
585
- }
586
- // A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
587
- narrowing = setTimeout(() => {
588
- narrowing = null;
589
- applyDemand(next);
590
- }, 5_000);
591
- }
592
- function requiredRooms(reach) {
631
+ function requiredRooms(given) {
632
+ const reach = planReach(given);
593
633
  if (reach === null)
594
634
  return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
595
- const named = [...new Set(reach.rooms)].sort();
635
+ const named = [...roomsOfReach(reach)].sort();
596
636
  if (roomsListed)
597
637
  return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
598
638
  return { listed: named.every((room) => replicas.has(room)), rooms: named };
599
639
  }
600
- function roomState(locator, search) {
640
+ function roomState(locator) {
601
641
  if (sourceRefusals.has(locator))
602
642
  return 'refused';
603
643
  const listedRoom = discovered.get(locator);
@@ -608,27 +648,17 @@ export function composeRuntime(options) {
608
648
  return 'unadmitted';
609
649
  if (!replicas.has(locator))
610
650
  return 'acquiring';
611
- if (!prepared.has(locator))
651
+ if (!indexed(locator))
612
652
  return 'preparing';
613
- if (search && !searchIndex.rooms().has(locator))
614
- return 'indexing';
615
653
  return 'complete';
616
654
  }
617
- function conversationCoverage(reach, search) {
655
+ function conversationCoverage(reach) {
618
656
  const declared = reach === null || reach.conversations;
619
657
  if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
620
658
  return 'undeclared';
621
659
  if (!conversations.known() || !conversations.current())
622
660
  return 'acquiring';
623
- if (!search)
624
- return 'complete';
625
- const unindexed = conversations
626
- .searchable()
627
- .filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
628
- if (unindexed.length === 0)
629
- return 'complete';
630
- index([], unindexed);
631
- return 'indexing';
661
+ return 'complete';
632
662
  }
633
663
  function connectionOf() {
634
664
  if (socketOpen)
@@ -655,25 +685,66 @@ export function composeRuntime(options) {
655
685
  ];
656
686
  });
657
687
  }
688
+ function installationOf(coverage) {
689
+ const rooms = { done: 0, total: 0 };
690
+ const parts = { done: 0, total: 0 };
691
+ const bytes = { done: 0, total: 0 };
692
+ const received = { done: 0, total: 0 };
693
+ const steps = new Set();
694
+ let waiting = false;
695
+ for (const { room, state } of coverage) {
696
+ if (state === 'unadmitted')
697
+ continue;
698
+ rooms.total += 1;
699
+ if (state === 'complete')
700
+ rooms.done += 1;
701
+ const transfer = transfers.get(room);
702
+ const held = installs.get(room);
703
+ const inventory = transfer?.inventory ?? replicas.get(room)?.record.inventory ?? null;
704
+ if (inventory !== null) {
705
+ const done = transfer?.done ?? inventory;
706
+ const share = held?.share ?? null;
707
+ parts.total += inventory.parts;
708
+ bytes.total += inventory.bytes;
709
+ parts.done += done.parts;
710
+ bytes.done += done.bytes;
711
+ if (share !== null && share.entries > 0) {
712
+ bytes.done += Math.floor((share.bytes * share.written) / share.entries);
713
+ }
714
+ received.total += inventory.bytes;
715
+ received.done += done.bytes + (share?.bytes ?? 0);
716
+ for (const arrived of arrivals.get(room)?.values() ?? [])
717
+ received.done += arrived;
718
+ }
719
+ const installing = held !== undefined && state !== 'complete';
720
+ if (installing)
721
+ steps.add(held.step);
722
+ waiting ||= installing || state === 'acquiring';
723
+ }
724
+ return {
725
+ step: INSTALL_STEPS.find((step) => steps.has(step)) ?? null,
726
+ paused: waiting && !onScreen ? 'hidden' : null,
727
+ rooms,
728
+ parts,
729
+ bytes,
730
+ received,
731
+ };
732
+ }
658
733
  function readinessFor(port, reach) {
659
734
  const required = requiredRooms(reach);
660
735
  const search = port.search === 'required';
661
- const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
662
- const building = (state) => coverage
663
- .filter((room) => room.state === state && !unindexable.has(room.room))
736
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
737
+ const preparing = coverage
738
+ .filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
664
739
  .map(({ room }) => room);
665
- const preparing = building('preparing');
666
740
  if (preparing.length > 0)
667
741
  prepare(preparing);
668
- const indexing = building('indexing');
669
- if (indexing.length > 0)
670
- index(indexing, []);
671
742
  return readinessOf({
672
743
  code: port.code,
673
744
  coverage: {
674
745
  listed: required.listed,
675
746
  rooms: coverage,
676
- conversations: conversationCoverage(reach, search),
747
+ conversations: conversationCoverage(reach),
677
748
  search: search ? 'required' : 'unused',
678
749
  },
679
750
  persistence,
@@ -683,6 +754,7 @@ export function composeRuntime(options) {
683
754
  coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
684
755
  ? capacity
685
756
  : port.capacity,
757
+ installation: installationOf(coverage),
686
758
  refused: refusedIn(required.rooms),
687
759
  lost: storageLoss !== null,
688
760
  established: targets[port.target]?.established === true,
@@ -691,7 +763,7 @@ export function composeRuntime(options) {
691
763
  function establish(name) {
692
764
  targets = { ...targets, [name]: { established: true } };
693
765
  queueReadiness();
694
- void durably((local) => local.keepTargets(targets)).catch(() => {
766
+ void durably((local) => local.fact('targets', targets)).catch(() => {
695
767
  journal.record(`target record failed ${name}`);
696
768
  });
697
769
  }
@@ -728,18 +800,12 @@ export function composeRuntime(options) {
728
800
  readinessWatches.delete(watch);
729
801
  };
730
802
  }
731
- function layerBytes(layer) {
803
+ function pageBytes(page) {
732
804
  let bytes = 0;
733
- for (const [path, text] of Object.entries(layer.documents))
734
- bytes += (path.length + text.length) * 2;
805
+ for (const { path, content } of page.entries)
806
+ bytes += (path.length + (content?.length ?? 0)) * 2;
735
807
  return bytes;
736
808
  }
737
- function grownBy(next, previous) {
738
- if (previous === undefined)
739
- return layerBytes(next);
740
- const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
741
- return layerBytes({ ...next, documents });
742
- }
743
809
  function exceeded(error) {
744
810
  return (typeof error === 'object' &&
745
811
  error !== null &&
@@ -747,7 +813,7 @@ export function composeRuntime(options) {
747
813
  }
748
814
  async function estimated() {
749
815
  const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
750
- return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
816
+ return estimate === null ? null : Math.max(0, estimate.total - estimate.usage);
751
817
  }
752
818
  function pause(locator, cause) {
753
819
  paused.add(locator);
@@ -768,7 +834,8 @@ export function composeRuntime(options) {
768
834
  const available = await estimated().catch(() => null);
769
835
  if (available === null || available < (held.needed ?? 0))
770
836
  return;
771
- if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
837
+ if (held.cause === 'storage-exceeded' &&
838
+ (held.available === null || available <= held.available))
772
839
  return;
773
840
  resume();
774
841
  }
@@ -786,10 +853,7 @@ export function composeRuntime(options) {
786
853
  void sync('unstreamed');
787
854
  }
788
855
  async function reclaimed(local) {
789
- const unreferenced = [...transfers.keys()].filter((locator) => {
790
- const room = discovered.get(locator);
791
- return room === undefined || !acquires(room);
792
- });
856
+ const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
793
857
  for (const locator of unreferenced) {
794
858
  await local.abandon(locator);
795
859
  transfers.delete(locator);
@@ -813,10 +877,10 @@ export function composeRuntime(options) {
813
877
  }
814
878
  async function exceededOn(locator, bytes) {
815
879
  const available = await estimated().catch(() => null);
816
- pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
880
+ pause(locator, { cause: 'storage-exceeded', needed: bytes, available });
817
881
  }
818
882
  function seedRoomHeld(room, version) {
819
- return prepared.has(room) && reachedBy(room, version);
883
+ return indexed(room) && reachedBy(room, version);
820
884
  }
821
885
  function seedHeld(at) {
822
886
  return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
@@ -850,6 +914,75 @@ export function composeRuntime(options) {
850
914
  shown();
851
915
  }
852
916
  }
917
+ function sameCover(left, right) {
918
+ return JSON.stringify(left) === JSON.stringify(right);
919
+ }
920
+ async function coveredRows(plan, cover) {
921
+ for (;;) {
922
+ const before = cover();
923
+ if ('pending' in before)
924
+ return { pending: before };
925
+ const local = await openLocal();
926
+ const keys = before.rooms.map(({ key }) => key);
927
+ const changes = local.changes(keys);
928
+ const current = () => local.changes(keys) === changes;
929
+ const rows = await windowRows(local, plan, before.rooms, current);
930
+ const after = cover();
931
+ if (rows !== null && sameCover(before, after))
932
+ return { rows, cover: before, current };
933
+ }
934
+ }
935
+ async function windowRows(local, plan, rooms, unchanged) {
936
+ const first = await local.query(plan, rooms, QUERY_BYTES);
937
+ if (first.more === undefined || !('documents' in first)) {
938
+ const { more: _more, ...rows } = first;
939
+ return rows;
940
+ }
941
+ const documents = [...first.documents];
942
+ for (let more = first.more; more !== undefined;) {
943
+ if (!unchanged())
944
+ return null;
945
+ const page = await local.query({
946
+ ...plan,
947
+ after: { text: more, values: JSON.parse(more) },
948
+ limit: plan.limit === null ? null : plan.limit - documents.length,
949
+ total: false,
950
+ }, rooms, QUERY_BYTES);
951
+ if (!('documents' in page))
952
+ return null;
953
+ documents.push(...page.documents);
954
+ more = page.more;
955
+ if (more === undefined) {
956
+ return {
957
+ documents,
958
+ ...(page.next === undefined ? {} : { next: page.next }),
959
+ ...(first.total === undefined ? {} : { total: first.total }),
960
+ };
961
+ }
962
+ }
963
+ return null;
964
+ }
965
+ function keyedOf(rows, coverage) {
966
+ if ('groups' in rows) {
967
+ return {
968
+ result: { ...rows, coverage },
969
+ keys: rows.groups.map((group) => JSON.stringify(group.key)),
970
+ };
971
+ }
972
+ return {
973
+ result: { ...rows, coverage },
974
+ keys: rows.documents.map((document) => documentKey(document.room, document.path)),
975
+ };
976
+ }
977
+ async function answeredPlan(plan, cover) {
978
+ const answered = await coveredRows(plan, cover);
979
+ if ('pending' in answered) {
980
+ const pending = answered.pending;
981
+ return { result: pending, keys: [], current: () => true };
982
+ }
983
+ const held = answered.cover;
984
+ return { ...keyedOf(answered.rows, held.coverage), current: answered.current };
985
+ }
853
986
  async function rowsQuery(plan) {
854
987
  const before = covered(plan);
855
988
  awaitPreparation(before);
@@ -858,61 +991,15 @@ export function composeRuntime(options) {
858
991
  return seededResult(seeded).result;
859
992
  if ('pending' in before)
860
993
  return before;
861
- const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
862
- awaitPreparation(cover);
863
- if ('pending' in cover)
994
+ const answered = await coveredRows(plan, () => {
995
+ const cover = covered(plan);
996
+ awaitPreparation(cover);
864
997
  return cover;
865
- return 'groups' in rows
866
- ? { groups: rows.groups, coverage: cover.coverage }
867
- : { documents: rows.documents, coverage: cover.coverage };
868
- }
869
- async function indexConversation(conversation) {
870
- searchIndex.indexMessages(conversation, [], false);
871
- let after = null;
872
- for (;;) {
873
- const batch = await durably((local) => local.messagesAfter(conversation, after));
874
- if (closed)
875
- return;
876
- searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
877
- if (batch.length === 0)
878
- return;
879
- after = batch[batch.length - 1].identity;
880
- }
881
- }
882
- function indexNext() {
883
- const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
884
- if (room !== undefined) {
885
- roomsToIndex.delete(room);
886
- return () => prepared.has(room)
887
- ? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
888
- : undefined;
889
- }
890
- const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
891
- if (conversation === undefined)
892
- return null;
893
- conversationsToIndex.delete(conversation);
894
- return () => indexConversation(conversation);
895
- }
896
- function index(rooms, named) {
897
- for (const room of rooms)
898
- roomsToIndex.add(room);
899
- for (const conversation of named)
900
- conversationsToIndex.add(conversation);
901
- if (indexing)
902
- return;
903
- indexing = true;
904
- void (async () => {
905
- try {
906
- for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
907
- await yielded();
908
- await unit();
909
- queueReadiness();
910
- }
911
- }
912
- finally {
913
- indexing = false;
914
- }
915
- })().catch(() => { });
998
+ });
999
+ if ('pending' in answered)
1000
+ return answered.pending;
1001
+ const held = answered.cover;
1002
+ return { ...answered.rows, coverage: held.coverage };
916
1003
  }
917
1004
  function publishChangeSet(set) {
918
1005
  applyChangeSet(set);
@@ -922,7 +1009,10 @@ export function composeRuntime(options) {
922
1009
  for (const locator of set.queues)
923
1010
  emitQueue(locator);
924
1011
  if (set.drafts.length > 0 ||
925
- set.documents.some((change) => roomDrafts.get(change.locator)?.has(change.path) === true)) {
1012
+ set.documents.some((change) => {
1013
+ const held = roomDrafts.get(change.locator);
1014
+ return held !== undefined && (change.path === null || held.has(change.path));
1015
+ })) {
926
1016
  publishRoomDrafts(false);
927
1017
  }
928
1018
  for (const app of set.presentations)
@@ -935,9 +1025,22 @@ export function composeRuntime(options) {
935
1025
  if (reason === 'revoked-authorization') {
936
1026
  await exclusive(async () => {
937
1027
  const revoked = { status: 'unavailable', reason: 'revoked' };
1028
+ const records = [...replicas].map(([locator, held]) => ({
1029
+ locator,
1030
+ held,
1031
+ record: { ...held.record, model: revoked, selectionModel: revoked },
1032
+ }));
1033
+ let answer = null;
938
1034
  try {
939
1035
  const local = await openLocal();
940
- await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
1036
+ answer = await local.commit({
1037
+ rooms: records.map(({ locator, record }) => ({
1038
+ locator,
1039
+ key: record.key,
1040
+ record: storedRecord(record),
1041
+ composition: compositionOf(record),
1042
+ })),
1043
+ });
941
1044
  }
942
1045
  catch {
943
1046
  journal.record('model revocation persistence failed');
@@ -953,16 +1056,16 @@ export function composeRuntime(options) {
953
1056
  liveModels.delete(key);
954
1057
  }
955
1058
  const changed = [];
956
- for (const [locator, room] of rooms) {
957
- const before = replicas.get(locator) ?? null;
958
- if (before !== null) {
959
- const record = {
960
- ...before.confirmed.record,
961
- model: revoked,
962
- selectionModel: revoked,
963
- };
964
- replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
965
- }
1059
+ for (const { locator, held, record } of records) {
1060
+ if (replicas.get(locator) !== held)
1061
+ continue;
1062
+ const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
1063
+ replicas.set(locator, { ...held, record, indexed });
1064
+ }
1065
+ for (const locator of [...rooms.keys()]) {
1066
+ const room = rooms.get(locator);
1067
+ if (room === undefined)
1068
+ continue;
966
1069
  rooms.set(locator, {
967
1070
  ...room,
968
1071
  model: modelTypes(revoked),
@@ -970,9 +1073,10 @@ export function composeRuntime(options) {
970
1073
  ? null
971
1074
  : { ...room.typedSelection, model: modelTypes(revoked) },
972
1075
  });
973
- changed.push(changeSet(locator, before, replicas.get(locator) ?? null, { room: true }));
1076
+ changed.push(changeSet(locator, { documents: null, room: true }));
974
1077
  }
975
1078
  publishChangeSet(mergeChangeSets(changed));
1079
+ prepare();
976
1080
  });
977
1081
  }
978
1082
  }
@@ -1003,12 +1107,13 @@ export function composeRuntime(options) {
1003
1107
  model: modelTypes(modelOf(room.locator, 'typed-selection')),
1004
1108
  },
1005
1109
  providerOwnedPaths: room.providerOwnedPaths,
1006
- roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
1110
+ roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
1007
1111
  synchronizationAvailable,
1008
1112
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
1009
1113
  });
1010
1114
  }
1011
- async function applyDiscovery(found, started) {
1115
+ async function applyDiscovery(found, started, covered) {
1116
+ const lockedBefore = lockedRooms();
1012
1117
  for (const room of found) {
1013
1118
  const fence = fences.get(room.locator);
1014
1119
  if (fence !== undefined && fence.erased && fence.at < started)
@@ -1021,24 +1126,30 @@ export function composeRuntime(options) {
1021
1126
  const listedLocators = new Set(listed.map((room) => room.locator));
1022
1127
  for (const locator of listedLocators)
1023
1128
  scopedRefusals.delete(locator);
1024
- discovered.clear();
1129
+ if (covered === undefined)
1130
+ discovered.clear();
1131
+ else
1132
+ for (const locator of covered)
1133
+ discovered.delete(locator);
1025
1134
  for (const room of listed)
1026
1135
  discovered.set(room.locator, room);
1027
- const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
1136
+ const unlisted = unlistedHeld(new Set(found.map((room) => room.locator))).filter((locator) => covered === undefined || covered.includes(locator));
1028
1137
  if (options.adapter.exhaustiveDiscovery === true)
1029
1138
  await loseRooms(unlisted);
1030
1139
  else
1031
1140
  unlistedKept(unlisted);
1032
1141
  for (const room of listed) {
1033
- const held = replicas.get(room.locator)?.confirmed.record;
1142
+ const held = replicas.get(room.locator)?.record;
1034
1143
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
1035
1144
  list(held, false);
1036
1145
  continue;
1037
1146
  }
1038
1147
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
1039
1148
  }
1149
+ confirmLocks(listed);
1040
1150
  roomsListed = true;
1041
1151
  emitRooms();
1152
+ relocked(lockedBefore);
1042
1153
  return listed;
1043
1154
  }
1044
1155
  function heldLocally(locator) {
@@ -1059,7 +1170,7 @@ export function composeRuntime(options) {
1059
1170
  }
1060
1171
  function unlistedKept(unlisted) {
1061
1172
  for (const locator of unlisted) {
1062
- const held = replicas.get(locator)?.confirmed.record;
1173
+ const held = replicas.get(locator)?.record;
1063
1174
  if (held !== undefined)
1064
1175
  list(held, false);
1065
1176
  else
@@ -1068,16 +1179,16 @@ export function composeRuntime(options) {
1068
1179
  }
1069
1180
  function fence(locators) {
1070
1181
  const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1071
- const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
1072
- room: true,
1073
- queue: true,
1182
+ const removed = fresh.map((locator) => ({
1183
+ locator,
1074
1184
  drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
1075
1185
  }));
1076
1186
  for (const locator of fresh) {
1077
1187
  fences.set(locator, { at: discoveries, erased: false });
1078
1188
  lastFenced.set(locator, ++fencings);
1189
+ confirmed.delete(locator);
1079
1190
  for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1080
- pendings.delete(batch.id);
1191
+ pendings.delete(pendingKey(locator, batch.id));
1081
1192
  replicas.delete(locator);
1082
1193
  queuesWithoutReplica.delete(locator);
1083
1194
  rooms.delete(locator);
@@ -1089,7 +1200,7 @@ export function composeRuntime(options) {
1089
1200
  unreadable.delete(locator);
1090
1201
  stale.delete(locator);
1091
1202
  repairable.delete(locator);
1092
- unindexable.delete(locator);
1203
+ unpreparable.delete(locator);
1093
1204
  paused.delete(locator);
1094
1205
  scopedRefusals.delete(locator);
1095
1206
  sourceRefusals.delete(locator);
@@ -1104,26 +1215,48 @@ export function composeRuntime(options) {
1104
1215
  }
1105
1216
  if (fresh.length === 0)
1106
1217
  return fresh;
1107
- publishChangeSet(mergeChangeSets(removed));
1108
- for (const locator of fresh)
1109
- prepared.delete(locator);
1110
- searchIndex.forget(fresh);
1111
- readSide.recover();
1218
+ publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, { documents: null, room: true, queue: true, drafts }))));
1219
+ liveQueries.recover();
1112
1220
  moveTo({ ...status, ...attention() });
1113
1221
  return fresh;
1114
1222
  }
1115
- async function erased(local, locators) {
1116
- await local.erase(locators);
1223
+ async function eraseFenced(locators) {
1117
1224
  for (const locator of locators) {
1225
+ while (await durably((local) => local.erase(locator, ERASE_BATCH)))
1226
+ await yielded();
1118
1227
  const held = fences.get(locator);
1119
1228
  if (held !== undefined)
1120
1229
  held.erased = true;
1121
1230
  }
1122
1231
  }
1232
+ let sweeping = false;
1233
+ function sweep() {
1234
+ if (sweeping || closed)
1235
+ return;
1236
+ sweeping = true;
1237
+ void (async () => {
1238
+ try {
1239
+ while (!closed && (await durably((local) => local.erase(null, ERASE_BATCH)))) {
1240
+ await yielded();
1241
+ }
1242
+ }
1243
+ catch {
1244
+ journal.record('erasure failed');
1245
+ }
1246
+ finally {
1247
+ sweeping = false;
1248
+ }
1249
+ })();
1250
+ }
1123
1251
  async function loseRooms(locators) {
1124
1252
  const fresh = fence(locators);
1125
- if (fresh.length > 0)
1126
- await durably((local) => erased(local, fresh));
1253
+ if (fresh.length === 0)
1254
+ return;
1255
+ await durably(async (local) => {
1256
+ await local.fence(fresh);
1257
+ await unpair(local);
1258
+ });
1259
+ await eraseFenced(fresh);
1127
1260
  }
1128
1261
  function restored(key, persisted) {
1129
1262
  const prior = models.get(key);
@@ -1144,54 +1277,56 @@ export function composeRuntime(options) {
1144
1277
  return model;
1145
1278
  }
1146
1279
  async function hydrate(opened) {
1147
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
1148
- opened.all(),
1280
+ const [held, queues, kept, heldConversations, chatDrafts] = await Promise.all([
1281
+ opened.opened(),
1149
1282
  opened.queues(),
1150
1283
  opened.roomDrafts(),
1151
- opened.presentation(),
1152
1284
  opened.conversations(),
1153
1285
  opened.chatDrafts(),
1154
- opened.lastCompleted(),
1155
- opened.providerLimits(),
1156
- opened.capabilities(),
1157
- opened.member(),
1158
- opened.transfers(),
1159
- opened.targets(),
1160
1286
  ]);
1161
- targets = heldTargets;
1287
+ const records = [...held.rooms.values()];
1288
+ const counts = await opened.counts(records.map(({ key, typedSelection }) => ({ key, roots: typedSelection?.roots ?? [] })));
1289
+ const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1290
+ targets = held.targets;
1162
1291
  transfers.clear();
1163
- for (const record of staged)
1164
- transfers.set(record.locator, record);
1165
- limits = declared;
1166
- capabilities = named;
1167
- member = known;
1292
+ for (const [locator, record] of held.transfers)
1293
+ transfers.set(locator, record);
1294
+ limits = held.providerLimits;
1295
+ capabilities = held.capabilities;
1296
+ member = held.member;
1168
1297
  roomDrafts.clear();
1169
1298
  for (const record of kept)
1170
1299
  rememberRoomDraft(record);
1171
1300
  presentations.clear();
1172
- for (const record of presented)
1301
+ for (const record of held.presentation)
1173
1302
  presentations.set(record.app, record.value);
1174
- conversations.hydrate(held, chatDrafts);
1175
- const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1303
+ conversations.hydrate(heldConversations, chatDrafts);
1176
1304
  pendings.clear();
1177
1305
  for (const queue of queues) {
1178
- for (const { batch, state } of queue.pending)
1179
- pendings.set(batch.id, state);
1306
+ for (const { batch, state } of queue.pending) {
1307
+ pendings.set(pendingKey(queue.locator, batch.id), state);
1308
+ }
1180
1309
  }
1181
- const previousReplicas = new Map(replicas);
1182
1310
  replicas.clear();
1183
1311
  queuesWithoutReplica.clear();
1184
- const replicated = new Set(records.map((record) => record.locator));
1185
1312
  for (const [locator, queue] of queuedBatches) {
1186
- if (!replicated.has(locator))
1313
+ if (!held.rooms.has(locator))
1187
1314
  queuesWithoutReplica.set(locator, queue);
1188
1315
  }
1189
- for (const record of records) {
1190
- const model = restored(modelKey(record.locator, 'collection'), record.model);
1191
- const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
1192
- const prior = previousReplicas.get(record.locator);
1193
- const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
1194
- replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
1316
+ records.forEach((stored, index) => {
1317
+ if (fences.has(stored.locator))
1318
+ return;
1319
+ const model = restored(modelKey(stored.locator, 'collection'), stored.model);
1320
+ const selectionModel = restored(modelKey(stored.locator, 'typed-selection'), stored.selectionModel);
1321
+ const record = { ...stored, model, selectionModel };
1322
+ replicas.set(record.locator, {
1323
+ record,
1324
+ queue: queuedBatches.get(record.locator) ?? [],
1325
+ overlaid: held.overlaid.get(record.key) ?? new Set(),
1326
+ count: counts[index] ?? 0,
1327
+ confirmed: 0,
1328
+ indexed: !held.unindexed.has(record.key),
1329
+ });
1195
1330
  const existing = rooms.get(record.locator);
1196
1331
  if (existing === undefined)
1197
1332
  list(record, false);
@@ -1210,11 +1345,15 @@ export function composeRuntime(options) {
1210
1345
  },
1211
1346
  });
1212
1347
  }
1213
- }
1348
+ });
1349
+ for (const key of held.orphans)
1350
+ await opened.release(key);
1351
+ if (held.erasing || held.orphans.length > 0)
1352
+ queueMicrotask(sweep);
1214
1353
  return {
1215
- lastCompleted,
1354
+ lastCompleted: held.lastCompleted,
1216
1355
  locators: new Set([...queuedBatches.keys(), ...replicas.keys()]),
1217
- read: mergeChangeSets([...new Set([...previousReplicas.keys(), ...replicas.keys()])].map((locator) => changeSet(locator, previousReplicas.get(locator) ?? null, replicas.get(locator) ?? null))),
1356
+ read: mergeChangeSets([...replicas.keys()].map((locator) => changeSet(locator, { documents: null }))),
1218
1357
  };
1219
1358
  }
1220
1359
  async function readState(opened) {
@@ -1233,8 +1372,7 @@ export function composeRuntime(options) {
1233
1372
  async function open() {
1234
1373
  if (closed)
1235
1374
  throw new Error('runtime closed');
1236
- const opened = await openRuntimeStore(isFenced);
1237
- store = opened;
1375
+ const opened = openRuntimeStore(isFenced, options.database);
1238
1376
  const interrupted = await opened.fences();
1239
1377
  for (const locator of interrupted) {
1240
1378
  fences.set(locator, { at: discoveries, erased: false });
@@ -1253,9 +1391,17 @@ export function composeRuntime(options) {
1253
1391
  emitStorageLoss();
1254
1392
  }
1255
1393
  const lastCompleted = await readState(opened);
1256
- if (interrupted.length > 0)
1257
- await erased(opened, interrupted);
1394
+ if (interrupted.length > 0) {
1395
+ queueMicrotask(() => {
1396
+ void eraseFenced(interrupted).catch(() => {
1397
+ journal.record('erasure failed');
1398
+ });
1399
+ });
1400
+ }
1258
1401
  moveTo({ ...status, lastCompleted, ...attention() });
1402
+ void exclusive(() => unpair(opened)).catch(() => {
1403
+ journal.record('unpair failed');
1404
+ });
1259
1405
  return opened;
1260
1406
  }
1261
1407
  function durably(work) {
@@ -1276,78 +1422,260 @@ export function composeRuntime(options) {
1276
1422
  mutations = next.then(() => undefined, () => undefined);
1277
1423
  return next;
1278
1424
  }
1279
- function withdrawable(batch) {
1280
- const state = pendings.get(batch.id);
1281
- return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
1425
+ function withdrawable(locator) {
1426
+ return (batch) => {
1427
+ const state = pendings.get(pendingKey(locator, batch.id));
1428
+ return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
1429
+ };
1430
+ }
1431
+ const REWRITE_LINKS = ['field', 'body'];
1432
+ const CHECKED_LINKS = ['field', 'section'];
1433
+ async function sliceAt(record, view, queue, extra = [], moved = []) {
1434
+ const local = await openLocal();
1435
+ const needed = new Set([...touchedBy(queue), ...extra]);
1436
+ const movedPaths = [...new Set([...movedBy(queue, record.locator), ...moved])];
1437
+ if (movedPaths.length > 0) {
1438
+ for (const { path } of await local.referrers(record.key, view, movedPaths, REWRITE_LINKS)) {
1439
+ needed.add(path);
1440
+ }
1441
+ }
1442
+ const paths = [...needed];
1443
+ return stepped(slicing(await local.entries(record.key, view, paths), paths));
1444
+ }
1445
+ async function landedWhole(locator, batch, confirmedLayer, current) {
1446
+ if (!landed(batch, confirmedLayer, current))
1447
+ return false;
1448
+ const partner = partnerOf(batch, locator);
1449
+ if (partner === null)
1450
+ return true;
1451
+ const other = replicas.get(partner);
1452
+ const half = other?.queue.find((one) => one.id === batch.id);
1453
+ if (other === undefined || half === undefined)
1454
+ return false;
1455
+ return landed(half, await sliceAt(other.record, 'confirmed', [half]));
1456
+ }
1457
+ function carriedBack(locator, unlanded, written) {
1458
+ const back = new Map();
1459
+ const taken = [];
1460
+ for (const batch of unlanded) {
1461
+ const between = batch.between;
1462
+ if (between === undefined)
1463
+ continue;
1464
+ const source = between.source.room;
1465
+ const destination = between.destination.room;
1466
+ for (const { path: to } of pendings.get(pendingKey(destination, batch.id))?.drafts ?? []) {
1467
+ const record = roomDrafts.get(destination)?.get(to);
1468
+ const path = between.moves.find((move) => move.to === to)?.path;
1469
+ if (record === undefined || path === undefined)
1470
+ continue;
1471
+ const [into, key] = source === locator ? [written, path] : [back, documentKey(source, path)];
1472
+ const existing = into.get(key) ?? roomDrafts.get(source)?.get(path);
1473
+ taken.push({ locator: destination, path: to });
1474
+ into.set(key, {
1475
+ locator: source,
1476
+ path,
1477
+ revision: roomDraftRevision(),
1478
+ parts: returnedInto(existing?.parts, record.parts),
1479
+ returned: existing?.returned ?? null,
1480
+ });
1481
+ }
1482
+ }
1483
+ const records = [...back.values()];
1484
+ const elsewhere = taken.filter((one) => one.locator !== locator);
1485
+ const rooms = new Set([...records, ...elsewhere].map((one) => one.locator));
1486
+ return {
1487
+ taken: taken.filter((one) => one.locator === locator).map(({ path }) => path),
1488
+ writes: [...rooms].map((room) => ({
1489
+ locator: room,
1490
+ roomDrafts: records.filter((record) => record.locator === room),
1491
+ consumedRoomDrafts: elsewhere.filter((one) => one.locator === room).map(({ path }) => path),
1492
+ })),
1493
+ changes: [...rooms].map((room) => changeSet(room, {
1494
+ drafts: [...records, ...elsewhere]
1495
+ .filter((one) => one.locator === room)
1496
+ .map(({ path }) => path),
1497
+ })),
1498
+ remember() {
1499
+ for (const one of taken)
1500
+ forgetRoomDraft(one.locator, one.path);
1501
+ for (const record of records)
1502
+ rememberRoomDraft(record);
1503
+ },
1504
+ };
1505
+ }
1506
+ function committedRoom(locator, answer) {
1507
+ const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
1508
+ if (!indexed)
1509
+ prepare([locator]);
1510
+ return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
1511
+ }
1512
+ function replayed(record, confirmedLayer, queue) {
1513
+ return stepped(replaying(confirmedLayer, queue, record.locator, compositionOf(record)));
1282
1514
  }
1283
- async function settle(local, locator, before, confirmed, queue, settling = {}) {
1515
+ async function overlayDiff(record, before, visible, touched) {
1516
+ const kept = before !== undefined && before.record.key === record.key
1517
+ ? before.overlaid
1518
+ : new Set();
1519
+ return {
1520
+ overlay: {
1521
+ set: await stepped(overlayOf(visible, touched)),
1522
+ cleared: [...kept].filter((path) => !touched.has(path)),
1523
+ },
1524
+ touched,
1525
+ };
1526
+ }
1527
+ async function settle(local, locator, before, record, queue, settling = {}) {
1528
+ return together(local, [await settlement(local, locator, before, record, queue, settling)]);
1529
+ }
1530
+ function unpairedIn(locator, batch) {
1531
+ if (!queueIn(locator).some((held) => held.id === batch.id))
1532
+ return false;
1533
+ if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
1534
+ return false;
1535
+ return !leavingIn(batch, locator) || withdrawable(locator)(batch);
1536
+ }
1537
+ async function together(local, settlements) {
1538
+ const settled = new Set(settlements.map(({ locator }) => locator));
1539
+ const all = [...settlements];
1540
+ for (let wave = settlements; wave.length > 0;) {
1541
+ const partnered = new Map();
1542
+ for (const { locator, left } of wave) {
1543
+ for (const batch of left) {
1544
+ const partner = partnerOf(batch, locator);
1545
+ if (partner === null || settled.has(partner) || !unpairedIn(partner, batch))
1546
+ continue;
1547
+ partnered.set(partner, [...(partnered.get(partner) ?? []), batch]);
1548
+ }
1549
+ }
1550
+ const partners = [];
1551
+ for (const [partner, batches] of partnered) {
1552
+ settled.add(partner);
1553
+ partners.push(await unpairing(local, partner, batches));
1554
+ }
1555
+ all.push(...partners);
1556
+ wave = partners;
1557
+ }
1558
+ const answer = await local.commit({
1559
+ rooms: all.flatMap(({ commit }) => commit.rooms),
1560
+ writes: all.flatMap(({ commit }) => commit.writes),
1561
+ });
1562
+ return mergeChangeSets(all.map((one) => one.apply(answer)));
1563
+ }
1564
+ async function settlement(local, locator, before, record, queue, settling = {}) {
1565
+ const view = settling.view ?? 'confirmed';
1284
1566
  const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
1285
- const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
1286
- const kept = queue.filter((batch) => !withdrawn.includes(batch));
1287
- const unqueued = withdrawn.map((batch) => ({
1288
- batch,
1289
- returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
1290
- }));
1567
+ const overrides = settling.changes;
1568
+ const read = await sliceAt(record, view, queue, [
1569
+ ...(before !== undefined && before.record.key === record.key ? before.overlaid : []),
1570
+ ...(overrides?.set.map(({ path }) => path) ?? []),
1571
+ ...(overrides?.removed ?? []),
1572
+ ]);
1573
+ const confirmedLayer = overrides === undefined
1574
+ ? read
1575
+ : await stepped(slicingWith(read, overrides.set, overrides.removed));
1576
+ const composition = compositionOf(record);
1577
+ const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
1578
+ const founded = kept.filter((batch) => !queue.includes(batch));
1579
+ const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
1580
+ const unqueued = [];
1581
+ for (const batch of withdrawn) {
1582
+ unqueued.push({ batch, landed: await landedWhole(locator, batch, confirmedLayer) });
1583
+ }
1291
1584
  const returning = [
1292
- ...[...unqueued].reverse(),
1293
- ...(settling.leaving === undefined ? [] : [settling.leaving]),
1585
+ ...[...unqueued].reverse().map(({ batch, landed: held }) => ({
1586
+ batch,
1587
+ returned: localSaves || held ? null : returnedBy('withdrawn'),
1588
+ })),
1589
+ ...(settling.leaving ?? []),
1294
1590
  ];
1295
1591
  const written = new Map();
1296
- for (const { batch, returned } of returning) {
1297
- if (returned === null)
1592
+ for (const { batch, returned: reason } of returning) {
1593
+ if (reason === null || batch.between?.destination.room === locator)
1298
1594
  continue;
1299
- for (const saved of pendings.get(batch.id)?.drafts ?? []) {
1595
+ for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
1300
1596
  const existing = written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path);
1301
1597
  written.set(saved.path, {
1302
1598
  locator,
1303
1599
  path: saved.path,
1304
1600
  revision: roomDraftRevision(),
1305
1601
  parts: returnedInto(existing?.parts, saved.parts),
1306
- returned,
1602
+ returned: reason,
1307
1603
  });
1308
1604
  }
1309
1605
  }
1310
- await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
1311
- for (const batch of previousQueue) {
1312
- if (!kept.includes(batch))
1313
- pendings.delete(batch.id);
1314
- }
1315
- for (const record of written.values())
1316
- rememberRoomDraft(record);
1317
- try {
1318
- await local.write({
1319
- next: confirmed.record,
1320
- previous: before?.confirmed.record ?? null,
1321
- ...(settling.stored === undefined ? {} : { stored: settling.stored }),
1322
- });
1323
- }
1324
- catch (error) {
1325
- const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
1326
- if (held !== null)
1327
- replicas.set(locator, held);
1328
- else if (kept.length > 0)
1329
- queuesWithoutReplica.set(locator, kept);
1330
- else
1606
+ const carried = carriedBack(locator, [
1607
+ ...[...unqueued].reverse().flatMap(({ batch, landed: held }) => (held ? [] : [batch])),
1608
+ ...(settling.leaving ?? []).flatMap(({ batch, returned }) => returned === null ? [] : [batch]),
1609
+ ], written);
1610
+ const replayedLayer = await replayed(record, confirmedLayer, kept);
1611
+ const { overlay, touched } = await overlayDiff(record, before, replayedLayer.visible, replayedLayer.touched);
1612
+ const commit = {
1613
+ rooms: [
1614
+ {
1615
+ locator,
1616
+ key: record.key,
1617
+ record: storedRecord(record),
1618
+ roots: record.typedSelection?.roots ?? [],
1619
+ ...settling.commit,
1620
+ ...(overrides === undefined
1621
+ ? {}
1622
+ : { confirmed: { set: overrides.set, removed: overrides.removed } }),
1623
+ overlay,
1624
+ },
1625
+ ],
1626
+ writes: [
1627
+ {
1628
+ locator,
1629
+ queue: kept,
1630
+ founded,
1631
+ roomDrafts: [...written.values()],
1632
+ consumedRoomDrafts: carried.taken,
1633
+ },
1634
+ ...carried.writes,
1635
+ ],
1636
+ };
1637
+ const staying = new Set(kept.map((batch) => batch.id));
1638
+ return {
1639
+ locator,
1640
+ left: previousQueue.filter((batch) => !staying.has(batch.id)),
1641
+ commit,
1642
+ apply: (answer) => {
1643
+ for (const batch of previousQueue) {
1644
+ if (!staying.has(batch.id))
1645
+ pendings.delete(pendingKey(locator, batch.id));
1646
+ }
1647
+ for (const draft of written.values())
1648
+ rememberRoomDraft(draft);
1649
+ carried.remember();
1650
+ const drafts = [...written.keys(), ...carried.taken];
1331
1651
  queuesWithoutReplica.delete(locator);
1332
- publishChangeSet(changeSet(locator, before ?? null, held, {
1333
- queue: kept.length !== previousQueue.length,
1334
- drafts: [...written.keys()],
1335
- }));
1336
- moveTo({ ...status, ...attention() });
1337
- throw error;
1338
- }
1339
- return () => {
1340
- queuesWithoutReplica.delete(locator);
1341
- for (const { batch, returned } of unqueued) {
1342
- const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
1343
- journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1344
- }
1345
- replicas.set(locator, replicaOf(confirmed, kept, before));
1346
- return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
1347
- room: settling.room,
1348
- queue: kept.length !== previousQueue.length,
1349
- drafts: [...written.keys()],
1350
- });
1652
+ for (const { batch, landed: held } of unqueued) {
1653
+ const outcome = localSaves ? 'dropped' : held ? 'landed' : 'withdrawn';
1654
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1655
+ }
1656
+ const changed = answer.changed[locator];
1657
+ const confirmedChanged = settling.commit?.published !== undefined ||
1658
+ settling.commit?.refreshed === true ||
1659
+ overrides !== undefined;
1660
+ const { count, indexed } = committedRoom(locator, answer);
1661
+ replicas.set(locator, {
1662
+ record,
1663
+ queue: kept,
1664
+ overlaid: touched,
1665
+ count,
1666
+ confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
1667
+ indexed,
1668
+ });
1669
+ return mergeChangeSets([
1670
+ changeSet(locator, {
1671
+ documents: changed === undefined ? [] : changed,
1672
+ room: settling.room,
1673
+ queue: queueChanged,
1674
+ drafts,
1675
+ }),
1676
+ ...carried.changes,
1677
+ ]);
1678
+ },
1351
1679
  };
1352
1680
  }
1353
1681
  function adoptModels(locator, record) {
@@ -1368,7 +1696,7 @@ export function composeRuntime(options) {
1368
1696
  const taken = (answer, previous, attached) => {
1369
1697
  if (answer === undefined)
1370
1698
  return previous;
1371
- const { state, refused } = attachedModel(answer, previous);
1699
+ const { state, refused } = attachedModel(answer, previous, attached === 'collection' ? roomTypes : NO_ROOM_TYPES);
1372
1700
  if (refused !== null) {
1373
1701
  const where = refused.location === undefined ? '' : ` ${refused.location}`;
1374
1702
  journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
@@ -1381,70 +1709,86 @@ export function composeRuntime(options) {
1381
1709
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
1382
1710
  };
1383
1711
  }
1384
- function publish(local, room, confirmed, landing, live) {
1385
- const { base, stored } = landing;
1386
- return exclusive(async () => {
1387
- if (stored !== undefined) {
1388
- await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
1389
- }
1712
+ function publish(local, room, record, landing, live) {
1713
+ const landed = exclusive(async () => {
1390
1714
  if (!live()) {
1391
1715
  caughtUpTo(room.locator);
1392
1716
  return true;
1393
1717
  }
1394
1718
  const prior = replicas.get(room.locator);
1395
- if (base !== undefined &&
1396
- (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
1397
- prior.confirmed.record.revisions !== base.record.revisions)) {
1719
+ if (landing.kind === 'refreshed' &&
1720
+ (prior === undefined || prior.record.key !== record.key || prior.confirmed !== landing.base)) {
1721
+ await local.unrefresh(record.key);
1398
1722
  return false;
1399
1723
  }
1400
- adoptModels(room.locator, confirmed.record);
1724
+ adoptModels(room.locator, record);
1401
1725
  const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
1402
- const roomVersion = confirmed.record.roomVersion;
1726
+ const roomVersion = record.roomVersion;
1403
1727
  const accepted = queue.filter((batch) => {
1404
- const accepted = pendings.get(batch.id)?.accepted;
1405
- return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
1728
+ const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
1729
+ return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
1406
1730
  });
1407
- let settled;
1731
+ let set;
1408
1732
  try {
1409
- settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
1733
+ set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
1734
+ ? {
1735
+ room: true,
1736
+ view: 'refreshed',
1737
+ commit: { refreshed: true, composition: compositionOf(record) },
1738
+ }
1739
+ : {
1740
+ room: true,
1741
+ view: landing.refreshed ? 'refreshed' : 'confirmed',
1742
+ commit: {
1743
+ published: { from: prior?.record.key ?? null },
1744
+ composition: compositionOf(record),
1745
+ ...(landing.refreshed ? { refreshed: true } : {}),
1746
+ },
1747
+ });
1410
1748
  }
1411
1749
  catch (error) {
1412
1750
  if (!exceeded(error))
1413
1751
  throw error;
1414
- await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
1752
+ await exceededOn(room.locator, null);
1415
1753
  caughtUpTo(room.locator);
1416
1754
  return true;
1417
1755
  }
1418
- const set = settled();
1419
- if (stored !== undefined) {
1756
+ if (landing.kind === 'bootstrapped') {
1420
1757
  transfers.delete(room.locator);
1421
1758
  catching.delete(room.locator);
1759
+ sweep();
1422
1760
  }
1423
1761
  for (const batch of accepted)
1424
- pendings.delete(batch.id);
1762
+ pendings.delete(pendingKey(room.locator, batch.id));
1425
1763
  stale.delete(room.locator);
1426
- unindexable.delete(room.locator);
1764
+ unpreparable.delete(room.locator);
1427
1765
  if (unreadable.delete(room.locator))
1428
1766
  scopedRefusals.delete(room.locator);
1429
1767
  sourceRefusals.delete(room.locator);
1430
1768
  declarationRefusals.delete(room.locator);
1431
1769
  list(room, true);
1432
- if (prior === undefined)
1433
- prepared.add(room.locator);
1434
1770
  publishChangeSet(set);
1771
+ await unpair(local);
1435
1772
  moveTo({ ...status, ...attention() });
1436
1773
  options.adapter.roomStream?.published(room.locator, roomVersion);
1774
+ if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1775
+ submitLater(room.locator);
1776
+ }
1437
1777
  return true;
1438
1778
  });
1779
+ return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
1439
1780
  }
1440
- async function saveRoomDrafts(locator, named, writtenBy) {
1781
+ async function savedRoom(locator) {
1441
1782
  const local = await openLocal();
1442
- await referenced(locator);
1443
1783
  const replica = replicas.get(locator);
1444
1784
  const room = rooms.get(locator);
1445
1785
  if (replica === undefined || room === undefined) {
1446
1786
  throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1447
1787
  }
1788
+ return { local, replica, room };
1789
+ }
1790
+ async function saveRoomDrafts(locator, named, writtenBy) {
1791
+ const target = await savedRoom(locator);
1448
1792
  const saved = [];
1449
1793
  for (const { path, revision } of named) {
1450
1794
  const record = roomDrafts.get(locator)?.get(path);
@@ -1454,12 +1798,396 @@ export function composeRuntime(options) {
1454
1798
  if (!saved.some((draft) => draft.path === path))
1455
1799
  saved.push({ path, parts: record.parts });
1456
1800
  }
1457
- const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
1801
+ return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
1802
+ }
1803
+ async function saveParts(locator, named, writtenBy) {
1804
+ const read = named.map(({ path, parts }) => ({ path, parts: readParts(parts) }));
1805
+ if (read.some(({ path }, at) => path === '' ||
1806
+ normalize(path) !== path ||
1807
+ read.findIndex((one) => one.path === path) !== at)) {
1808
+ throw runtimeError('room-draft-malformed');
1809
+ }
1810
+ const target = await savedRoom(locator);
1811
+ const saved = [];
1812
+ const files = [];
1813
+ const filed = read.filter(({ parts }) => parts.file !== undefined).map(({ path }) => path);
1814
+ const visible = filed.length === 0 ? undefined : await visibleOf(locator, filed);
1815
+ for (const { path, parts } of read) {
1816
+ const file = parts.file === undefined
1817
+ ? undefined
1818
+ : await heldFile(parts.file.blob, binaryBase(visible, path));
1819
+ if (parts.file !== undefined && file !== undefined) {
1820
+ files.push({ key: file.localId, blob: parts.file.blob });
1821
+ }
1822
+ saved.push({ path, parts: held(undefined, parts, file) });
1823
+ }
1824
+ return committed(locator, target, saved, new Set(), writtenBy, files);
1825
+ }
1826
+ function carriedDrafts(locator, saved, consumed) {
1827
+ const holding = roomDrafts.get(locator);
1828
+ return new Map(saved.flatMap(({ path, parts }) => {
1829
+ const record = holding?.get(path);
1830
+ if (parts.move === undefined || record === undefined || consumed.has(path))
1831
+ return [];
1832
+ const to = moveTarget(parts.move);
1833
+ if (to === path || holding?.has(to) === true)
1834
+ return [];
1835
+ return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
1836
+ }));
1837
+ }
1838
+ async function savingIn(replica, changes, extra, leaving = []) {
1839
+ const record = replica.record;
1840
+ const local = await openLocal();
1841
+ const paths = [
1842
+ ...extra,
1843
+ ...changes.flatMap((change) => change.kind === 'move' ? [change.path, change.to] : [change.path]),
1844
+ ];
1845
+ const moved = [
1846
+ ...leaving,
1847
+ ...changes.flatMap((change) => (change.kind === 'move' ? [change.path] : [])),
1848
+ ];
1849
+ const removed = changes.flatMap((change) => (change.kind === 'remove' ? [change.path] : []));
1850
+ const referring = removed.length === 0
1851
+ ? []
1852
+ : (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
1853
+ const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
1854
+ const { visible } = await replayed(record, confirmedLayer, replica.queue);
1855
+ const composition = compositionOf(record);
1856
+ return {
1857
+ record,
1858
+ confirmed: confirmedLayer,
1859
+ visible,
1860
+ *visibleOver(queue) {
1861
+ return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
1862
+ },
1863
+ };
1864
+ }
1865
+ async function unmodeledIn(record) {
1866
+ const composition = compositionFor(record);
1867
+ if (available(composition))
1868
+ return false;
1869
+ const local = await openLocal();
1870
+ const roots = record.typedSelection?.roots ?? [];
1871
+ if (composition.model.status === 'unavailable' &&
1872
+ (await local.typedWithin(record.key, 'visible', roots, false))) {
1873
+ return true;
1874
+ }
1875
+ return (composition.selection !== null &&
1876
+ composition.selection.model.status === 'unavailable' &&
1877
+ (await local.typedWithin(record.key, 'visible', roots, true)));
1878
+ }
1879
+ function textDiffers(left, right) {
1880
+ return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
1881
+ !sameBinary(left?.binary ?? undefined, right?.binary ?? undefined));
1882
+ }
1883
+ function textOf(entry) {
1884
+ if (entry === null)
1885
+ return undefined;
1886
+ return entry.binary !== null ? null : entry.content;
1887
+ }
1888
+ function walkOf(local, key, view) {
1889
+ return async (after) => {
1890
+ const page = await local.walk(key, view, after, WALK_BYTES);
1891
+ return {
1892
+ entries: page.entries.map(({ path, content, binary }) => ({
1893
+ path,
1894
+ content: binary !== null ? null : content,
1895
+ })),
1896
+ next: page.next,
1897
+ };
1898
+ };
1899
+ }
1900
+ function lookupOf(local, key, view) {
1901
+ return async (paths) => {
1902
+ if (paths.length === 0)
1903
+ return new Map();
1904
+ return new Map((await local.typed(key, view, paths)).map(({ path, type, binary }) => [
1905
+ path,
1906
+ { type: type ?? undefined, binary },
1907
+ ]));
1908
+ };
1909
+ }
1910
+ function declaredOf(entry) {
1911
+ if (entry.binary !== null)
1912
+ return { type: undefined, binary: true };
1913
+ const read = parseFrontmatter(entry.content ?? '');
1914
+ return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
1915
+ }
1916
+ function overriddenWalk(walk, overrides) {
1917
+ const sorted = [...overrides.keys()].sort();
1918
+ const beyond = (path) => {
1919
+ if (path === null)
1920
+ return 0;
1921
+ let low = 0;
1922
+ let high = sorted.length;
1923
+ while (low < high) {
1924
+ const middle = (low + high) >>> 1;
1925
+ if (sorted[middle] <= path)
1926
+ low = middle + 1;
1927
+ else
1928
+ high = middle;
1929
+ }
1930
+ return low;
1931
+ };
1932
+ return async (after) => {
1933
+ const page = await walk(after);
1934
+ const end = page.next;
1935
+ const added = sorted
1936
+ .slice(beyond(after), end === null ? sorted.length : beyond(end))
1937
+ .flatMap((path) => {
1938
+ const entry = overrides.get(path);
1939
+ return entry === null || entry === undefined
1940
+ ? []
1941
+ : [{ path, content: entry.binary !== null ? null : entry.content }];
1942
+ });
1943
+ const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
1944
+ return { entries, next: end };
1945
+ };
1946
+ }
1947
+ function overriddenLookup(lookup, overrides) {
1948
+ return async (paths) => {
1949
+ const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
1950
+ for (const path of paths) {
1951
+ const entry = overrides.get(path);
1952
+ if (entry === undefined)
1953
+ continue;
1954
+ if (entry === null)
1955
+ known.delete(path);
1956
+ else
1957
+ known.set(path, declaredOf(entry));
1958
+ }
1959
+ return known;
1960
+ };
1961
+ }
1962
+ function validatorOf(record, confirmedLayer) {
1963
+ return async (resulting, touched) => {
1964
+ const composition = compositionFor(record);
1965
+ const local = await openLocal();
1966
+ const changed = [];
1967
+ const overrides = new Map();
1968
+ const pace = paced();
1969
+ for (const path of new Set([...confirmedLayer.held, ...touched])) {
1970
+ await pace(ROW_WEIGHT);
1971
+ const was = entryAt(confirmedLayer, path);
1972
+ const now = entryAt(resulting, path);
1973
+ if (!textDiffers(was, now))
1974
+ continue;
1975
+ overrides.set(path, now);
1976
+ if (textOf(was) !== textOf(now))
1977
+ changed.push({ path, was: textOf(was), now: textOf(now) });
1978
+ }
1979
+ const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
1980
+ const clean = available(composition) &&
1981
+ changed.length <= CLEAN_CHANGES &&
1982
+ (await changedCleanly(changed, compositionOf(record), composition, {
1983
+ lookup,
1984
+ texts: textsOf(local, record.key, 'confirmed'),
1985
+ referrers: referrersOf(local, record.key, 'confirmed'),
1986
+ }));
1987
+ if (clean)
1988
+ return { verdict: 'accept' };
1989
+ return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
1990
+ };
1991
+ }
1992
+ function textsOf(local, key, view) {
1993
+ return async (paths) => new Map((await local.entries(key, view, paths)).map(({ path, content, binary }) => [
1994
+ path,
1995
+ binary !== null ? null : content,
1996
+ ]));
1997
+ }
1998
+ function referrersOf(local, key, view) {
1999
+ return async (targets) => [
2000
+ ...new Set((await local.referrers(key, view, targets, CHECKED_LINKS)).map(({ path }) => path)),
2001
+ ];
2002
+ }
2003
+ async function sideOf(locator, replica, room, saving) {
2004
+ const record = replica.record;
2005
+ return {
2006
+ room: locator,
2007
+ handle: room.handle,
2008
+ baseRoomVersion: record.roomVersion,
2009
+ composition: compositionFor(record),
2010
+ operations: room.operations,
2011
+ providerOwnedPaths: room.providerOwnedPaths,
2012
+ visible: saving.visible,
2013
+ queued: queuedDocuments(replica.queue),
2014
+ unmodeled: await unmodeledIn(record),
2015
+ validate: validatorOf(record, saving.confirmed),
2016
+ };
2017
+ }
2018
+ function movedInto(locator, saved) {
2019
+ const into = saved.find(({ parts }) => parts.move?.room !== undefined && parts.move.room !== locator);
2020
+ return into?.parts.move?.room ?? null;
2021
+ }
2022
+ async function saveCommit(local, files, rooms) {
2023
+ const touched = new Map();
2024
+ const commits = [];
2025
+ for (const { locator, saving, before, queue } of rooms) {
2026
+ const replayedLayer = await replayed(saving.record, saving.confirmed, queue);
2027
+ const diff = await overlayDiff(saving.record, before, replayedLayer.visible, replayedLayer.touched);
2028
+ touched.set(locator, diff.touched);
2029
+ commits.push({
2030
+ locator,
2031
+ key: saving.record.key,
2032
+ roots: saving.record.typedSelection?.roots ?? [],
2033
+ overlay: diff.overlay,
2034
+ });
2035
+ }
2036
+ try {
2037
+ const answer = await local.commit({
2038
+ rooms: commits,
2039
+ writes: rooms.map(({ write }) => write),
2040
+ files,
2041
+ whole: true,
2042
+ });
2043
+ return { answer, touched };
2044
+ }
2045
+ catch {
2046
+ return null;
2047
+ }
2048
+ }
2049
+ function savedReplica(locator, before, queue, touched, answer) {
2050
+ replicas.set(locator, {
2051
+ ...before,
2052
+ queue,
2053
+ overlaid: touched,
2054
+ count: answer.counts[locator] ?? before.count,
2055
+ });
2056
+ }
2057
+ async function committedMove(locator, destination, { local, replica, room }, saved, consumed, writtenBy, files) {
2058
+ const mixed = saved.find(({ parts }) => Object.keys(parts).length !== 1 || parts.move?.room !== destination);
2059
+ if (mixed !== undefined) {
2060
+ return { outcome: 'refused', code: 'mixed-room-move', retryable: false, file: mixed.path };
2061
+ }
2062
+ const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
2063
+ const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
2064
+ const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, sourceSaving.visible)));
1458
2065
  if (changed.length > 0)
1459
2066
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
1460
- const parts = saved.map(({ path, parts }) => changesOf(path, parts));
1461
- const changes = parts.flatMap(({ changes }) => changes);
1462
- const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
2067
+ const target = replicas.get(destination);
2068
+ const listedRoom = rooms.get(destination);
2069
+ if (target === undefined || listedRoom === undefined) {
2070
+ throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
2071
+ }
2072
+ const arriving = await savingIn(target, [], moves.map(({ to }) => to));
2073
+ const built = await buildMove({
2074
+ source: await sideOf(locator, replica, room, sourceSaving),
2075
+ destination: await sideOf(destination, target, listedRoom, arriving),
2076
+ moves,
2077
+ providerLimits: limits,
2078
+ local: localSaves,
2079
+ ...(writtenBy === undefined ? {} : { writtenBy }),
2080
+ });
2081
+ if (built.kind === 'refused')
2082
+ return { outcome: 'refused', ...built.refusal };
2083
+ const id = built.leaving.id;
2084
+ const carrying = saved.flatMap(({ path, parts }) => {
2085
+ const record = roomDrafts.get(locator)?.get(path);
2086
+ const to = moveTarget(parts.move);
2087
+ if (record === undefined || consumed.has(path) || roomDrafts.get(destination)?.has(to)) {
2088
+ return [];
2089
+ }
2090
+ const moved = {
2091
+ ...record,
2092
+ locator: destination,
2093
+ path: to,
2094
+ revision: roomDraftRevision(),
2095
+ };
2096
+ return [{ from: path, record: moved }];
2097
+ });
2098
+ const carried = carrying.map(({ record }) => record);
2099
+ const marked = carried.map(({ path }) => ({ path, parts: {} }));
2100
+ const left = [...consumed, ...carrying.map(({ from }) => from)];
2101
+ const leavingQueue = [...replica.queue, built.leaving];
2102
+ const arrivingQueue = [...target.queue, built.arriving];
2103
+ const committedBoth = await saveCommit(local, files, [
2104
+ {
2105
+ locator,
2106
+ saving: sourceSaving,
2107
+ before: replica,
2108
+ queue: leavingQueue,
2109
+ write: {
2110
+ locator,
2111
+ queue: leavingQueue,
2112
+ saved: new Map([[id, saved]]),
2113
+ consumedRoomDrafts: left,
2114
+ },
2115
+ },
2116
+ {
2117
+ locator: destination,
2118
+ saving: arriving,
2119
+ before: target,
2120
+ queue: arrivingQueue,
2121
+ write: {
2122
+ locator: destination,
2123
+ queue: arrivingQueue,
2124
+ saved: new Map([[id, marked]]),
2125
+ roomDrafts: carried,
2126
+ },
2127
+ },
2128
+ ]);
2129
+ if (committedBoth === null)
2130
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
2131
+ const { answer, touched } = committedBoth;
2132
+ if (answer.fenced !== null) {
2133
+ throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2134
+ }
2135
+ if (fences.has(locator) || fences.has(destination)) {
2136
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2137
+ }
2138
+ pendings.set(pendingKey(locator, id), {
2139
+ drafts: saved,
2140
+ submission: { state: 'waiting' },
2141
+ lost: false,
2142
+ });
2143
+ pendings.set(pendingKey(destination, id), {
2144
+ drafts: marked,
2145
+ submission: { state: 'waiting' },
2146
+ lost: false,
2147
+ });
2148
+ for (const path of left)
2149
+ forgetRoomDraft(locator, path);
2150
+ for (const record of carried)
2151
+ rememberRoomDraft(record);
2152
+ savedReplica(locator, replica, leavingQueue, touched.get(locator), answer);
2153
+ savedReplica(destination, target, arrivingQueue, touched.get(destination), answer);
2154
+ publishChangeSet(mergeChangeSets([
2155
+ changeSet(locator, {
2156
+ documents: answer.changed[locator] ?? [],
2157
+ queue: true,
2158
+ drafts: left,
2159
+ }),
2160
+ changeSet(destination, {
2161
+ documents: answer.changed[destination] ?? [],
2162
+ queue: true,
2163
+ drafts: carried.map(({ path }) => path),
2164
+ }),
2165
+ ]));
2166
+ queuedSave(locator);
2167
+ return { outcome: 'saved', identifier: id };
2168
+ }
2169
+ function queuedSave(locator) {
2170
+ if (!localSaves)
2171
+ recorded = false;
2172
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
2173
+ submitLater(locator);
2174
+ }
2175
+ async function committed(locator, target, saved, consumed, writtenBy, files = []) {
2176
+ const into = movedInto(locator, saved);
2177
+ if (into !== null) {
2178
+ return committedMove(locator, into, target, saved, consumed, writtenBy, files);
2179
+ }
2180
+ const { local, replica, room } = target;
2181
+ const parts = saved.map(({ path, parts: held }) => changesOf(path, held));
2182
+ const changes = parts.flatMap(({ changes: made }) => made);
2183
+ const saving = await savingIn(replica, changes, [
2184
+ ...saved.map(({ path }) => path),
2185
+ ...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
2186
+ ]);
2187
+ const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, underneath(held, path, saving.visible)));
2188
+ if (changed.length > 0)
2189
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2190
+ const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
1463
2191
  if (requeue.kind === 'refused')
1464
2192
  return { outcome: 'refused', ...requeue.refusal };
1465
2193
  const foreign = [...requeue.withdrawn.keys()].some((origin) => {
@@ -1477,23 +2205,23 @@ export function composeRuntime(options) {
1477
2205
  const withdrawing = requeue.withdrawn.size > 0;
1478
2206
  const input = {
1479
2207
  local: localSaves,
1480
- baseRoomVersion: replica.confirmed.record.roomVersion,
1481
- composition: compositionFor(replica.confirmed.record),
2208
+ baseRoomVersion: replica.record.roomVersion,
2209
+ composition: compositionFor(replica.record),
1482
2210
  operations: room.operations,
1483
2211
  providerOwnedPaths: room.providerOwnedPaths,
1484
2212
  providerLimits: limits,
1485
2213
  handle: room.handle,
1486
- visible: replica.visible,
2214
+ visible: saving.visible,
1487
2215
  changes,
1488
2216
  bases: parts.flatMap(({ bases }) => bases),
2217
+ queued: queuedDocuments(replica.queue),
1489
2218
  writtenBy,
1490
- clean: {
1491
- text: replica.confirmed.record.documents,
1492
- binaries: replica.confirmed.record.binaries,
1493
- composition: compositionOf(replica.confirmed.record),
1494
- },
2219
+ unmodeled: await unmodeledIn(replica.record),
2220
+ validate: validatorOf(replica.record, saving.confirmed),
1495
2221
  };
1496
- const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
2222
+ const checked = withdrawing
2223
+ ? await buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
2224
+ : null;
1497
2225
  if (checked?.kind === 'refused')
1498
2226
  return { outcome: 'refused', ...checked.refusal };
1499
2227
  const owners = [...requeue.owners].flatMap((origin) => {
@@ -1509,13 +2237,14 @@ export function composeRuntime(options) {
1509
2237
  };
1510
2238
  }
1511
2239
  const built = !withdrawing
1512
- ? buildBatch(input)
2240
+ ? await buildBatch(input)
1513
2241
  : requeue.changes.length === 0
1514
2242
  ? null
1515
- : buildBatch({
2243
+ : await buildBatch({
1516
2244
  ...input,
1517
- visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
2245
+ visible: await stepped(saving.visibleOver(requeue.queue)),
1518
2246
  changes: requeue.changes,
2247
+ queued: queuedDocuments(requeue.queue),
1519
2248
  });
1520
2249
  if (built?.kind === 'refused')
1521
2250
  return { outcome: 'refused', ...built.refusal };
@@ -1532,7 +2261,7 @@ export function composeRuntime(options) {
1532
2261
  for (const [origin, replaced] of withdrawn) {
1533
2262
  if (replaced === null)
1534
2263
  continue;
1535
- const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
2264
+ const carried = withdrawnDrafts(pendings.get(pendingKey(locator, origin))?.drafts ?? [], changes);
1536
2265
  drafted.set(replaced.id, replaced === join?.batch
1537
2266
  ? [
1538
2267
  ...carried.map((held) => joinedDraft(held, saved)),
@@ -1546,59 +2275,77 @@ export function composeRuntime(options) {
1546
2275
  for (const replaced of withdrawn.values()) {
1547
2276
  if (replaced === null || !batches.includes(replaced))
1548
2277
  continue;
1549
- const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
2278
+ const limited = await stepped(limiting(batches, replaced, limits, saving.visibleOver));
1550
2279
  if (limited !== null)
1551
2280
  return { outcome: 'refused', ...limited };
1552
2281
  }
1553
- const consumed = saved.map(({ path }) => path);
1554
- try {
1555
- await local.keepAuthored({
2282
+ const carried = carriedDrafts(locator, saved, consumed);
2283
+ const left = [...consumed, ...carried.keys()];
2284
+ const arrived = [...carried.values()];
2285
+ const done = await saveCommit(local, files, [
2286
+ {
1556
2287
  locator,
2288
+ saving,
2289
+ before: replica,
1557
2290
  queue: batches,
1558
- saved: drafted,
1559
- consumedRoomDrafts: consumed,
1560
- });
1561
- }
1562
- catch {
2291
+ write: {
2292
+ locator,
2293
+ queue: batches,
2294
+ saved: drafted,
2295
+ consumedRoomDrafts: left,
2296
+ roomDrafts: arrived,
2297
+ },
2298
+ },
2299
+ ]);
2300
+ if (done === null)
1563
2301
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2302
+ const { answer, touched } = done;
2303
+ if (answer.fenced !== null) {
2304
+ throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
1564
2305
  }
1565
2306
  if (fences.has(locator))
1566
2307
  throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1567
2308
  for (const origin of withdrawn.keys())
1568
- pendings.delete(origin);
2309
+ pendings.delete(pendingKey(locator, origin));
1569
2310
  for (const [id, drafts] of drafted) {
1570
- pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
2311
+ pendings.set(pendingKey(locator, id), {
2312
+ drafts,
2313
+ submission: { state: 'waiting' },
2314
+ lost: false,
2315
+ });
1571
2316
  }
1572
- if (!localSaves)
1573
- recorded = false;
1574
- for (const path of consumed)
2317
+ for (const path of left)
1575
2318
  forgetRoomDraft(locator, path);
1576
- replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
1577
- publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
2319
+ for (const record of arrived)
2320
+ rememberRoomDraft(record);
2321
+ savedReplica(locator, replica, batches, touched.get(locator), answer);
2322
+ publishChangeSet(changeSet(locator, {
2323
+ documents: answer.changed[locator] ?? [],
1578
2324
  queue: true,
1579
- drafts: consumed,
2325
+ drafts: [...left, ...arrived.map(({ path }) => path)],
1580
2326
  }));
1581
- moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
1582
- submitLater(locator);
2327
+ queuedSave(locator);
1583
2328
  const identifier = join?.batch.id ??
1584
2329
  built?.batch.id ??
1585
2330
  [...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
1586
2331
  return { outcome: 'saved', identifier };
1587
2332
  }
1588
- async function mark(local, locator, identifier, submission, lost) {
1589
- const state = pendings.get(identifier);
1590
- if (!continuing() ||
1591
- state === undefined ||
1592
- (JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
1593
- return;
2333
+ async function mark(local, locator, batch, submission, lost) {
2334
+ const partner = partnerOf(batch, locator);
2335
+ for (const held of partner === null ? [locator] : [locator, partner]) {
2336
+ const state = pendings.get(pendingKey(held, batch.id));
2337
+ if (!continuing() ||
2338
+ state === undefined ||
2339
+ (JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
2340
+ continue;
2341
+ }
2342
+ await local.submit(held, batch.id, submission, lost);
2343
+ if (fences.has(held))
2344
+ continue;
2345
+ pendings.set(pendingKey(held, batch.id), { ...state, submission, lost });
2346
+ if (replicas.has(held))
2347
+ publishChangeSet(changeSet(held, { queue: true }));
1594
2348
  }
1595
- await local.submit(locator, identifier, submission, lost);
1596
- if (fences.has(locator))
1597
- return;
1598
- pendings.set(identifier, { ...state, submission, lost });
1599
- const replica = replicas.get(locator);
1600
- if (replica !== undefined)
1601
- publishChangeSet(changeSet(locator, replica, replica, { queue: true }));
1602
2349
  moveTo({ ...status, ...attention() });
1603
2350
  }
1604
2351
  async function keepUpload(local, locator, identifier, upload) {
@@ -1607,7 +2354,8 @@ export function composeRuntime(options) {
1607
2354
  await local.keepUpload(locator, identifier, upload);
1608
2355
  if (fences.has(locator))
1609
2356
  throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1610
- pendings.set(identifier, { ...pendings.get(identifier), upload });
2357
+ const key = pendingKey(locator, identifier);
2358
+ pendings.set(key, { ...pendings.get(key), upload });
1611
2359
  if (!continuing())
1612
2360
  throw new Error('connection refused');
1613
2361
  }
@@ -1640,7 +2388,7 @@ export function composeRuntime(options) {
1640
2388
  if (fencedSince(locator, since))
1641
2389
  return failure({ kind: 'room-not-readable', locator });
1642
2390
  if (result.kind === 'scoped-refusal') {
1643
- const room = replicas.get(result.locator)?.confirmed.record;
2391
+ const room = replicas.get(result.locator)?.record;
1644
2392
  if (room !== undefined)
1645
2393
  await scoped(result, room, await openLocal());
1646
2394
  }
@@ -1653,21 +2401,57 @@ export function composeRuntime(options) {
1653
2401
  return UNCERTAIN;
1654
2402
  }
1655
2403
  }
1656
- function queuedBlob(locator, path, localId) {
2404
+ function located(room, path, destination) {
2405
+ const crossing = roomTarget(destination);
2406
+ if (crossing === null) {
2407
+ const named = target(path, destination);
2408
+ return named === null || !rooms.has(room) ? null : { room, path: named };
2409
+ }
2410
+ const held = [...rooms.values()].find((listed) => listed.handle === crossing.handle);
2411
+ return held === undefined ? null : { room: held.locator, path: crossing.path };
2412
+ }
2413
+ async function resolveReference(room, path, destination) {
2414
+ if (roomTarget(destination) === null && target(path, destination) === null) {
2415
+ return ABSENT_TARGET;
2416
+ }
2417
+ const at = located(room, path, destination);
2418
+ if (at === null)
2419
+ return UNREADABLE;
2420
+ if (locked(at.room))
2421
+ return LOCKED_UNREADABLE;
2422
+ const held = replicas.get(at.room);
2423
+ if (held !== undefined &&
2424
+ (await (await openLocal()).entries(held.record.key, 'visible', [at.path])).length > 0) {
2425
+ return { kind: 'document', ...at };
2426
+ }
2427
+ const history = await readHistory(at.room, at.path);
2428
+ if (history.kind === 'no-history' || history.kind === 'revision')
2429
+ return ABSENT_TARGET;
2430
+ if (history.kind !== 'history')
2431
+ return history;
2432
+ const [last] = history.revisions;
2433
+ if (last === undefined)
2434
+ return ABSENT_TARGET;
2435
+ return history.deleted === true
2436
+ ? { kind: 'deleted', ...at, revision: last }
2437
+ : { kind: 'document', ...at };
2438
+ }
2439
+ function queuedFacts(locator, path, localId) {
1657
2440
  for (const batch of replicas.get(locator)?.queue ?? []) {
1658
2441
  const change = batch.changes[queuedFile(batch, path, localId)];
1659
2442
  if (change?.kind === 'binary')
1660
- return change.blob;
2443
+ return change;
1661
2444
  }
1662
- return null;
2445
+ const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2446
+ return drafted?.localId === localId ? drafted : null;
1663
2447
  }
1664
- function binaryRoom(request) {
1665
- if (request.kind === 'path')
1666
- return request.locator;
1667
- const scope = options.adapter.referrerScope(request.referrer);
1668
- return scope?.kind === 'room' ? scope.locator : null;
2448
+ function batchFile(batch) {
2449
+ return async (localId) => {
2450
+ const change = batch.changes.find((held) => held.kind === 'binary' && held.localId === localId);
2451
+ return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
2452
+ };
1669
2453
  }
1670
- async function readBinary(request) {
2454
+ async function transferBinary(request, transfer, answer) {
1671
2455
  if (closed || refusal !== null)
1672
2456
  return UNCERTAIN;
1673
2457
  try {
@@ -1679,73 +2463,109 @@ export function composeRuntime(options) {
1679
2463
  if (closed || authorization === undefined || refusal !== null)
1680
2464
  return UNCERTAIN;
1681
2465
  const since = fencings;
1682
- journal.record(request.kind === 'path'
1683
- ? `transfer binary ${request.locator} ${request.path}`
1684
- : `transfer binary ${request.fileRef}`);
2466
+ journal.record(`transfer binary ${request.locator} ${request.path}`);
1685
2467
  try {
1686
- const result = await options.adapter.binary(authorization, request);
1687
- journal.record(`outcome binary ${result.kind}`);
1688
- const locator = binaryRoom(request);
1689
- if (locator !== null && fencedSince(locator, since))
1690
- return failure({ kind: 'room-not-readable', locator });
1691
- if (result.kind === 'scoped-refusal') {
1692
- const room = replicas.get(result.locator)?.confirmed.record;
2468
+ const result = await transfer(authorization);
2469
+ const settled = result;
2470
+ journal.record(`outcome binary ${settled.kind}`);
2471
+ if (fencedSince(request.locator, since))
2472
+ return failure({ kind: 'room-not-readable', locator: request.locator });
2473
+ if (settled.kind === 'scoped-refusal') {
2474
+ const room = replicas.get(settled.locator)?.record;
1693
2475
  if (room !== undefined)
1694
- await scoped(result, room, await openLocal());
2476
+ await scoped(settled, room, await openLocal());
1695
2477
  }
1696
- if (result.kind === 'revoked-authorization')
2478
+ if (settled.kind === 'revoked-authorization')
1697
2479
  await refuse('revoked-authorization');
1698
- return binaryAnswer(result);
2480
+ return answer(result);
1699
2481
  }
1700
2482
  catch {
1701
2483
  journal.record('outcome binary transport-uncertainty');
1702
2484
  return UNCERTAIN;
1703
2485
  }
1704
2486
  }
1705
- async function readBinaryPath(locator, path, revision) {
1706
- await ready;
2487
+ async function readBinary(request, name) {
2488
+ if (closed)
2489
+ return UNCERTAIN;
2490
+ const held = await keptFiles.find(name);
2491
+ if (held !== null)
2492
+ return held;
2493
+ return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2494
+ }
2495
+ async function binaryPath(locator, path, revision) {
1707
2496
  if (revision !== undefined)
1708
- return readBinary({ kind: 'path', locator, path, revision });
1709
- const visible = replicas.get(locator)?.visible;
1710
- const held = visible?.binaries[path];
1711
- if (visible === undefined || held === undefined)
1712
- return absentBinary(path);
1713
- if (held.state === 'pending') {
1714
- const blob = queuedBlob(locator, path, held.localId);
1715
- return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1716
- }
1717
- const record = binaryRecord(path, held, visible.revisions[path]);
1718
- if (record.state !== 'accepted')
1719
- return absentBinary(path);
2497
+ return { kind: 'path', locator, path, revision };
1720
2498
  const replica = replicas.get(locator);
1721
- const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
2499
+ if (replica === undefined)
2500
+ return null;
2501
+ const local = await openLocal();
2502
+ const [held] = await local.entries(replica.record.key, 'visible', [path]);
2503
+ if (held?.binary === undefined || held.binary === null)
2504
+ return null;
2505
+ if (held.binary.state === 'pending')
2506
+ return { kind: 'pending', localId: held.binary.localId };
2507
+ const record = binaryRecord(path, held.binary, held.revision ?? undefined);
2508
+ if (record.state !== 'accepted')
2509
+ return null;
2510
+ const queued = replica.queue.flatMap((batch) => batch.changes);
2511
+ const sources = queued.flatMap((change) => (change.kind === 'move' ? [change.path] : []));
2512
+ const confirmedLayer = await stepped(slicing(await local.entries(replica.record.key, 'confirmed', [path, ...sources]), [
2513
+ path,
2514
+ ...sources,
2515
+ ]));
1722
2516
  const holds = (at) => {
1723
- const kept = replica?.confirmed.record.binaries[at];
2517
+ const kept = confirmedLayer.binaries[at];
1724
2518
  return (kept?.state === 'accepted' &&
1725
- kept.fileRef === record.fileRef &&
1726
- replica?.confirmed.record.revisions[at] === record.revision);
2519
+ kept.sha256 === record.sha256 &&
2520
+ confirmedLayer.revisions[at] === record.revision);
1727
2521
  };
1728
- let confirmed = path;
1729
- for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
2522
+ let confirmedAt = path;
2523
+ for (let index = queued.length - 1; index >= 0 && !holds(confirmedAt); index -= 1) {
1730
2524
  const change = queued[index];
1731
- if (change.kind === 'move' && change.to === confirmed)
1732
- confirmed = change.path;
2525
+ if (change.kind === 'move' && change.to === confirmedAt)
2526
+ confirmedAt = change.path;
1733
2527
  }
1734
- return readBinary({
2528
+ return {
1735
2529
  kind: 'path',
1736
2530
  locator,
1737
- path: confirmed,
2531
+ path: confirmedAt,
1738
2532
  revision: record.revision,
1739
- fileRef: record.fileRef,
1740
- });
2533
+ };
1741
2534
  }
1742
- async function readBinaryReference(reference) {
2535
+ async function readBinaryPath(locator, path, revision) {
1743
2536
  await ready;
1744
- return readBinary({
1745
- kind: 'reference',
1746
- fileRef: reference.fileRef,
1747
- referrer: reference.referrer,
1748
- });
2537
+ const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2538
+ const asked = drafted !== undefined && drafted.localId === revision
2539
+ ? { kind: 'pending', localId: revision }
2540
+ : await binaryPath(locator, path, revision);
2541
+ if (asked === null)
2542
+ return absentBinary(path);
2543
+ if (asked.kind === 'pending') {
2544
+ const facts = queuedFacts(locator, path, asked.localId);
2545
+ return facts === null
2546
+ ? absentBinary(path)
2547
+ : {
2548
+ kind: 'binary',
2549
+ file: asked.localId,
2550
+ byteLength: facts.byteLength,
2551
+ mediaType: facts.mediaType,
2552
+ };
2553
+ }
2554
+ return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
2555
+ }
2556
+ async function linkBinaryPath(locator, path, revision) {
2557
+ await ready;
2558
+ const asked = await binaryPath(locator, path, revision);
2559
+ if (asked === null || asked.kind === 'pending')
2560
+ return absentBinary(path);
2561
+ return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
2562
+ }
2563
+ function readPart(file, part) {
2564
+ return options.database.ask({ kind: 'file.part', key: file, part });
2565
+ }
2566
+ async function cleanUp() {
2567
+ const freed = await options.database.ask({ kind: 'opened.clean' });
2568
+ return freed + ((await options.shell.cleanUp?.()) ?? 0);
1749
2569
  }
1750
2570
  function declaredAgents() {
1751
2571
  for (const capability of capabilities) {
@@ -1763,7 +2583,14 @@ export function composeRuntime(options) {
1763
2583
  }
1764
2584
  return undefined;
1765
2585
  }
1766
- async function invokeAction(name, input) {
2586
+ function calledRooms(name, input) {
2587
+ const member = declaredAction(name)?.room;
2588
+ if (member === undefined || input === null || typeof input !== 'object')
2589
+ return [];
2590
+ const named = input[member];
2591
+ return (Array.isArray(named) ? named : [named]).filter((room) => typeof room === 'string');
2592
+ }
2593
+ async function invokeAction(name, input, dispatching) {
1767
2594
  await ready;
1768
2595
  if (closed || refusal !== null)
1769
2596
  return UNCERTAIN;
@@ -1777,11 +2604,12 @@ export function composeRuntime(options) {
1777
2604
  }
1778
2605
  if (closed || authorization === undefined || refusal !== null)
1779
2606
  return UNCERTAIN;
2607
+ await dispatching?.();
1780
2608
  journal.record(`transfer action ${name}`);
1781
2609
  try {
1782
2610
  const result = await options.adapter.action(authorization, { name, input });
1783
2611
  if (result.kind === 'scoped-refusal') {
1784
- const room = replicas.get(result.locator)?.confirmed.record;
2612
+ const room = replicas.get(result.locator)?.record;
1785
2613
  if (room !== undefined)
1786
2614
  await scoped(result, room, await openLocal());
1787
2615
  }
@@ -1797,98 +2625,492 @@ export function composeRuntime(options) {
1797
2625
  return UNCERTAIN;
1798
2626
  }
1799
2627
  }
1800
- function continuing() {
1801
- return !closed && !discoveryPaused;
2628
+ function unsettled(result) {
2629
+ return result.kind !== 'action' && result.retryable;
2630
+ }
2631
+ async function recoveredAction(name, input) {
2632
+ const operation = operationOf(input);
2633
+ if (operation === null)
2634
+ return invokeAction(name, input);
2635
+ const kept = async () => (await openLocal()).keepCall({ operation, name, input });
2636
+ calling.add(operation);
2637
+ try {
2638
+ let result = await invokeAction(name, input, kept);
2639
+ if (unsettled(result)) {
2640
+ await recovery.attempt(`action ${operation}`, async () => {
2641
+ if (closed || refusal !== null)
2642
+ return 'done';
2643
+ if (!socketOpen)
2644
+ return 'again';
2645
+ result = await invokeAction(name, input, kept);
2646
+ return unsettled(result) ? 'again' : 'done';
2647
+ }, { again: true });
2648
+ }
2649
+ if (!unsettled(result))
2650
+ await (await openLocal()).dropCall(operation);
2651
+ return result;
2652
+ }
2653
+ finally {
2654
+ calling.delete(operation);
2655
+ }
2656
+ }
2657
+ async function resendCalls(signal) {
2658
+ if (!socketOpen || closed || stopping)
2659
+ return 'done';
2660
+ const local = await openLocal();
2661
+ for (const held of await local.calls()) {
2662
+ if (calling.has(held.operation))
2663
+ continue;
2664
+ if (closed || signal.aborted)
2665
+ return 'done';
2666
+ if (pastHorizon(held.operation)) {
2667
+ await local.dropCall(held.operation);
2668
+ continue;
2669
+ }
2670
+ calling.add(held.operation);
2671
+ try {
2672
+ if (unsettled(await invokeAction(held.name, held.input)))
2673
+ return 'again';
2674
+ await local.dropCall(held.operation);
2675
+ }
2676
+ finally {
2677
+ calling.delete(held.operation);
2678
+ }
2679
+ }
2680
+ return 'done';
2681
+ }
2682
+ function continuing() {
2683
+ return !closed && !discoveryPaused;
2684
+ }
2685
+ function reachedBy(locator, roomVersion) {
2686
+ const held = replicas.get(locator);
2687
+ return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
2688
+ }
2689
+ async function writtenAfter(locator, roomVersion, path) {
2690
+ const held = replicas.get(locator);
2691
+ if (held === undefined)
2692
+ return false;
2693
+ const [entry] = await (await openLocal()).entries(held.record.key, 'confirmed', [path]);
2694
+ const written = entry?.written ?? undefined;
2695
+ return (written !== undefined &&
2696
+ options.adapter.reached(locator, written, roomVersion) &&
2697
+ !options.adapter.reached(locator, roomVersion, written));
2698
+ }
2699
+ function overriddenTexts(texts, overrides) {
2700
+ return async (paths) => {
2701
+ const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
2702
+ for (const path of paths) {
2703
+ const entry = overrides.get(path);
2704
+ if (entry === undefined)
2705
+ continue;
2706
+ if (entry === null)
2707
+ known.delete(path);
2708
+ else
2709
+ known.set(path, entry.binary !== null ? null : entry.content);
2710
+ }
2711
+ return known;
2712
+ };
2713
+ }
2714
+ async function unreadableRoom(texts, walk, paths) {
2715
+ if (paths !== null) {
2716
+ const text = {};
2717
+ for (const [path, content] of await texts(paths))
2718
+ if (content !== null)
2719
+ text[path] = content;
2720
+ return stepped(unreadableIn(text, Object.keys(text)));
2721
+ }
2722
+ for (let after = null;;) {
2723
+ const page = await walk(after);
2724
+ const text = {};
2725
+ for (const { path, content } of page.entries)
2726
+ if (content !== null)
2727
+ text[path] = content;
2728
+ const refused = await stepped(unreadableIn(text, Object.keys(text)));
2729
+ if (refused !== null)
2730
+ return refused;
2731
+ if (page.next === null)
2732
+ return null;
2733
+ after = page.next;
2734
+ }
2735
+ }
2736
+ async function validatedRoom(local, record, view, previous, overrides = null) {
2737
+ const composition = compositionOf(record);
2738
+ const modeled = record.model.status !== 'unavailable' || record.selectionModel.status !== 'unavailable';
2739
+ const lookup = overrides === null
2740
+ ? lookupOf(local, record.key, view)
2741
+ : overriddenLookup(lookupOf(local, record.key, view), overrides);
2742
+ const texts = overrides === null
2743
+ ? textsOf(local, record.key, view)
2744
+ : overriddenTexts(textsOf(local, record.key, view), overrides);
2745
+ const walk = overrides === null
2746
+ ? walkOf(local, record.key, view)
2747
+ : overriddenWalk(walkOf(local, record.key, view), overrides);
2748
+ let changes = null;
2749
+ let reshaped = false;
2750
+ if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
2751
+ const [was, now] = await Promise.all([
2752
+ local.entries(record.key, 'confirmed', previous.changed),
2753
+ overrides === null
2754
+ ? local.entries(record.key, view, previous.changed)
2755
+ : Promise.resolve([]),
2756
+ ]);
2757
+ const before = new Map(was.map((entry) => [entry.path, entry]));
2758
+ const after = overrides === null
2759
+ ? new Map(now.map((entry) => [entry.path, entry]))
2760
+ : overrides;
2761
+ changes = previous.changed.flatMap((path) => {
2762
+ const one = before.get(path) ?? null;
2763
+ const other = after.get(path) ?? null;
2764
+ reshaped ||= textDiffers(one, other);
2765
+ const held = textOf(one);
2766
+ const next = textOf(other);
2767
+ return held === next ? [] : [{ path, was: held, now: next }];
2768
+ });
2769
+ }
2770
+ const named = changes === null ? null : changes.map(({ path }) => path);
2771
+ const textChanged = changes === null || changes.length > 0 || reshaped;
2772
+ const modelsChanged = previous === null || !sameComposition(compositionOf(previous.record), composition);
2773
+ if (modeled && (textChanged || modelsChanged)) {
2774
+ const clean = previous !== null &&
2775
+ changes !== null &&
2776
+ (await changedCleanly(changes, compositionOf(previous.record), composition, {
2777
+ lookup,
2778
+ texts,
2779
+ referrers: referrersOf(local, record.key, 'confirmed'),
2780
+ }));
2781
+ if (clean)
2782
+ return null;
2783
+ const verdict = await validatedCollection(walk, lookup, composition);
2784
+ if (verdict.verdict === 'accept')
2785
+ return null;
2786
+ if (verdict.rule !== 'model-unavailable')
2787
+ return { kind: 'refused', refusal: verdict };
2788
+ return unreadableRoom(texts, walk, named);
2789
+ }
2790
+ if (textChanged)
2791
+ return unreadableRoom(texts, walk, named);
2792
+ return null;
2793
+ }
2794
+ function refreshingOf(local, key, liveness, replaying = null) {
2795
+ const write = replaying === null ? exclusive : installWrite;
2796
+ return {
2797
+ async turn() {
2798
+ if (replaying === null)
2799
+ return;
2800
+ installing(replaying, 'replaying');
2801
+ await screened();
2802
+ },
2803
+ prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
2804
+ async stage(page) {
2805
+ for (let written = 0;;) {
2806
+ if (replaying !== null)
2807
+ await screened();
2808
+ const end = batchEnd(page.entries, written);
2809
+ const last = end === page.entries.length;
2810
+ const entries = page.entries.slice(written, end);
2811
+ const staged = await write(async () => {
2812
+ if (!liveness())
2813
+ return false;
2814
+ await timed(() => local.refresh(key, last
2815
+ ? { ...page, entries }
2816
+ : { entries, removals: [], folders: [], folderRemovals: [] }));
2817
+ return true;
2818
+ });
2819
+ if (!staged)
2820
+ return false;
2821
+ if (last)
2822
+ return true;
2823
+ written = end;
2824
+ }
2825
+ },
2826
+ };
2827
+ }
2828
+ function batchEnd(entries, from) {
2829
+ let end = from;
2830
+ let spent = 0;
2831
+ while (end < entries.length) {
2832
+ const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
2833
+ if (spent + size > stageBytes && end > from)
2834
+ break;
2835
+ spent += size;
2836
+ end += 1;
2837
+ }
2838
+ return end;
2839
+ }
2840
+ async function timed(work) {
2841
+ const started = performance.now();
2842
+ const done = await work();
2843
+ const spent = performance.now() - started;
2844
+ if (spent < STAGE_AIM_MS / 2)
2845
+ stageBytes = Math.min(STAGE_BYTES, Math.round(stageBytes * 1.5));
2846
+ else if (spent > STAGE_AIM_MS * 2) {
2847
+ stageBytes = Math.max(STAGE_FLOOR, Math.round(stageBytes / 1.5));
2848
+ }
2849
+ return done;
2850
+ }
2851
+ function installWrite(work) {
2852
+ const next = installWrites.then(() => exclusive(work), () => exclusive(work));
2853
+ installWrites = next.then(() => undefined, () => undefined);
2854
+ return next;
1802
2855
  }
1803
- function reachedBy(locator, roomVersion) {
1804
- const held = replicas.get(locator);
1805
- return (held !== undefined &&
1806
- options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion));
2856
+ function screened() {
2857
+ if (onScreen || closed)
2858
+ return Promise.resolve();
2859
+ queueReadiness();
2860
+ return new Promise((resolve) => shownWaiters.push(resolve));
1807
2861
  }
1808
- function writtenAfter(locator, roomVersion, path) {
1809
- const written = replicas.get(locator)?.confirmed.record.written[path];
1810
- return (written !== undefined &&
1811
- options.adapter.reached(locator, written, roomVersion) &&
1812
- !options.adapter.reached(locator, roomVersion, written));
2862
+ async function heldPart() {
2863
+ while (partsFree === 0)
2864
+ await new Promise((admit) => partWaiters.push(admit));
2865
+ partsFree -= 1;
2866
+ return freed();
1813
2867
  }
1814
- async function pullRefresh(authorized, confirmed, live) {
1815
- if (!live())
2868
+ function freed() {
2869
+ let released = false;
2870
+ return () => {
2871
+ if (released)
2872
+ return;
2873
+ released = true;
2874
+ partsFree += 1;
2875
+ partWaiters.shift()?.();
2876
+ };
2877
+ }
2878
+ function installing(locator, step, share = null) {
2879
+ installs.set(locator, { step, share });
2880
+ queueReadiness();
2881
+ }
2882
+ function installed(locator) {
2883
+ if (installs.delete(locator))
2884
+ queueReadiness();
2885
+ }
2886
+ function screen(value) {
2887
+ if (value === onScreen)
2888
+ return;
2889
+ onScreen = value;
2890
+ journal.record(`installation ${value ? 'shown' : 'hidden'}`);
2891
+ queueReadiness();
2892
+ if (!value)
2893
+ return;
2894
+ for (const resolve of shownWaiters.splice(0))
2895
+ resolve();
2896
+ }
2897
+ async function pullRefresh(authorized, held, liveness) {
2898
+ if (!liveness())
1816
2899
  return unconfirmed;
1817
2900
  const turn = generation;
2901
+ const { record } = held;
2902
+ const base = held.confirmed;
2903
+ const local = await openLocal();
1818
2904
  try {
1819
- const result = await refreshRoom(reads, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1820
- const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
1821
- journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
1822
- return result;
2905
+ await exclusive(() => local.unrefresh(record.key));
2906
+ const result = await advancement(reads, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, liveness));
2907
+ if (result.kind !== 'advanced') {
2908
+ journal.record(`transfer refresh ${record.locator} ${result.kind}`);
2909
+ return result;
2910
+ }
2911
+ const refused = await validatedRoom(local, result.record, 'refreshed', {
2912
+ record,
2913
+ changed: [...result.changed],
2914
+ });
2915
+ if (refused !== null) {
2916
+ journal.record(`transfer refresh ${record.locator} refused`);
2917
+ return refused;
2918
+ }
2919
+ journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
2920
+ return { kind: 'advanced', record: result.record, base };
1823
2921
  }
1824
2922
  catch (error) {
1825
- journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
2923
+ journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
1826
2924
  throw error;
1827
2925
  }
1828
2926
  finally {
1829
- reads.finished(confirmed.record.locator, turn);
1830
- }
2927
+ reads.finished(record.locator, turn);
2928
+ }
2929
+ }
2930
+ const heldContents = new Map();
2931
+ function heldContentOf(local, key) {
2932
+ const known = heldContents.get(key);
2933
+ if (known !== undefined)
2934
+ return known;
2935
+ if (heldContents.size > ROOM_READS)
2936
+ heldContents.clear();
2937
+ const made = {
2938
+ files: (after, limit) => local.files(key, 'confirmed', after, limit),
2939
+ async contents(paths) {
2940
+ return new Map((await local.entries(key, 'confirmed', paths)).flatMap(({ path, content, revision }) => content === null || revision === null ? [] : [[path, { content, revision }]]));
2941
+ },
2942
+ };
2943
+ heldContents.set(key, made);
2944
+ return made;
1831
2945
  }
1832
- function staging(local, live) {
2946
+ function staging(local, liveness) {
1833
2947
  return {
1834
2948
  held: (locator) => transfers.get(locator) ?? null,
1835
- staged: (record) => local.staged(record),
1836
- stage: (record, page) => exclusive(async () => {
1837
- if (!live())
1838
- return false;
1839
- const bytes = layerBytes(page);
1840
- if (!(await admitted(local, record.locator, bytes)))
1841
- return false;
1842
- try {
1843
- await local.stage(record, page);
1844
- }
1845
- catch (error) {
1846
- if (!exceeded(error))
1847
- throw error;
1848
- await exceededOn(record.locator, bytes);
1849
- return false;
2949
+ async turn(locator, step) {
2950
+ installing(locator, step);
2951
+ for (;;) {
2952
+ await screened();
2953
+ const release = step === 'downloading' ? await heldPart() : () => { };
2954
+ if (onScreen || closed)
2955
+ return release;
2956
+ release();
1850
2957
  }
1851
- transfers.set(record.locator, record);
1852
- readSide.recover();
1853
- return true;
2958
+ },
2959
+ spare() {
2960
+ if (!onScreen || closed || partsFree === 0)
2961
+ return null;
2962
+ partsFree -= 1;
2963
+ return freed();
2964
+ },
2965
+ arriving(locator) {
2966
+ let ended = false;
2967
+ return {
2968
+ at(token, bytes) {
2969
+ if (ended)
2970
+ return;
2971
+ const held = arrivals.get(locator) ?? new Map();
2972
+ arrivals.set(locator, held.set(token, bytes));
2973
+ const now = performance.now();
2974
+ if (now - arrivalReported < ARRIVAL_REPORT_MS)
2975
+ return;
2976
+ arrivalReported = now;
2977
+ queueReadiness();
2978
+ },
2979
+ end() {
2980
+ ended = true;
2981
+ if (arrivals.delete(locator))
2982
+ queueReadiness();
2983
+ },
2984
+ };
2985
+ },
2986
+ received: (locator, token) => local.transferPart(locator, token),
2987
+ keep: (locator, token, received) => installWrite(async () => {
2988
+ if (liveness())
2989
+ await local.keepPart(locator, token, received);
1854
2990
  }),
2991
+ announced(locator) {
2992
+ const later = announced.get(locator);
2993
+ return later === undefined ? [] : [later];
2994
+ },
2995
+ async stage(record, page, composition, received) {
2996
+ const { locator } = record;
2997
+ const bytes = pageBytes(page);
2998
+ const whole = received?.byteLength ?? 0;
2999
+ const from = record.writing?.written ?? 0;
3000
+ const token = record.writing?.token ?? null;
3001
+ const entries = page.entries.length;
3002
+ let held = record;
3003
+ if (token !== null)
3004
+ arrivals.get(locator)?.delete(token);
3005
+ installing(locator, 'writing', { written: from, entries, bytes: whole });
3006
+ for (let written = from;;) {
3007
+ await screened();
3008
+ if (!liveness())
3009
+ return null;
3010
+ const end = batchEnd(page.entries, written);
3011
+ const last = end === entries;
3012
+ const next = last
3013
+ ? {
3014
+ ...held,
3015
+ writing: null,
3016
+ done: {
3017
+ parts: held.done.parts + (received === null ? 0 : 1),
3018
+ bytes: held.done.bytes + whole,
3019
+ },
3020
+ }
3021
+ : { ...held, writing: { token, written: end } };
3022
+ const key = await installWrite(async () => {
3023
+ if (!liveness())
3024
+ return null;
3025
+ if (written === from && !(await admitted(local, locator, bytes)))
3026
+ return null;
3027
+ try {
3028
+ return await timed(() => local.stage(next, composition, {
3029
+ entries: page.entries.slice(written, end),
3030
+ folders: last ? page.folders : [],
3031
+ }, last && received !== null && token !== null ? token : undefined));
3032
+ }
3033
+ catch (error) {
3034
+ if (!exceeded(error))
3035
+ throw error;
3036
+ await exceededOn(locator, bytes);
3037
+ return null;
3038
+ }
3039
+ });
3040
+ if (key === null)
3041
+ return null;
3042
+ const arrived = !transfers.has(locator);
3043
+ held = { ...next, key };
3044
+ transfers.set(locator, held);
3045
+ if (arrived)
3046
+ liveQueries.recover();
3047
+ if (last) {
3048
+ installing(locator, held.pageToken === null ? 'replaying' : 'downloading');
3049
+ liveQueries.recover();
3050
+ return held;
3051
+ }
3052
+ installing(locator, 'writing', { written: end, entries, bytes: whole });
3053
+ written = end;
3054
+ }
3055
+ },
3056
+ async refreshing(locator, key) {
3057
+ await exclusive(() => local.unrefresh(key));
3058
+ return refreshingOf(local, key, liveness, locator);
3059
+ },
1855
3060
  abandon: (locator) => exclusive(async () => {
1856
- if (!live())
3061
+ if (!liveness())
1857
3062
  return;
1858
3063
  await local.abandon(locator);
1859
3064
  transfers.delete(locator);
1860
- readSide.recover();
3065
+ liveQueries.recover();
3066
+ sweep();
3067
+ }),
3068
+ setAside: (locator) => exclusive(async () => {
3069
+ if (!liveness())
3070
+ return;
3071
+ await local.abandon(locator, true);
3072
+ transfers.delete(locator);
3073
+ liveQueries.recover();
3074
+ }),
3075
+ release: (key) => exclusive(async () => {
3076
+ await local.release(key);
3077
+ sweep();
1861
3078
  }),
3079
+ content: (key) => heldContentOf(local, key),
3080
+ validate: (record, view) => validatedRoom(local, record, view, null),
1862
3081
  };
1863
3082
  }
1864
- function reusable(room) {
1865
- const record = replicas.get(room.locator)?.confirmed.record;
3083
+ function reusable(room, local) {
3084
+ const record = replicas.get(room.locator)?.record;
1866
3085
  if (record === undefined || record.replica.selection !== room.replica.selection)
1867
3086
  return null;
1868
- return { documents: record.documents, revisions: record.revisions };
3087
+ return heldContentOf(local, record.key);
1869
3088
  }
1870
- async function pullBootstrap(authorized, room, local, live) {
1871
- if (!live())
3089
+ async function pullBootstrap(authorized, room, local, liveness) {
3090
+ if (!liveness())
1872
3091
  return unconfirmed;
1873
3092
  const turn = generation;
1874
3093
  if (replicas.has(room.locator)) {
1875
3094
  catching.add(room.locator);
1876
- readSide.recover();
3095
+ liveQueries.recover();
1877
3096
  }
1878
3097
  try {
1879
3098
  const result = await bootstrapRoom(reads, authorized, room, {
1880
3099
  collection: modelOf(room.locator),
1881
3100
  selection: modelOf(room.locator, 'typed-selection'),
1882
- }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
1883
- const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
3101
+ }, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
3102
+ const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
1884
3103
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1885
- if (result.kind !== 'staged')
3104
+ if (result.kind !== 'staged') {
1886
3105
  caughtUpTo(room.locator);
3106
+ installed(room.locator);
3107
+ }
1887
3108
  return result;
1888
3109
  }
1889
3110
  catch (error) {
1890
3111
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
1891
3112
  caughtUpTo(room.locator);
3113
+ installed(room.locator);
1892
3114
  throw error;
1893
3115
  }
1894
3116
  finally {
@@ -1897,10 +3119,10 @@ export function composeRuntime(options) {
1897
3119
  }
1898
3120
  function caughtUpTo(locator) {
1899
3121
  if (catching.delete(locator))
1900
- readSide.recover();
3122
+ liveQueries.recover();
1901
3123
  }
1902
3124
  function behind(room) {
1903
- const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
3125
+ const held = replicas.get(room.locator)?.record.roomVersion;
1904
3126
  return (held !== undefined &&
1905
3127
  [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
1906
3128
  }
@@ -1913,12 +3135,11 @@ export function composeRuntime(options) {
1913
3135
  return advanceOnce(room, local, authorized, live);
1914
3136
  }
1915
3137
  async function advanceOnce(room, local, authorized, live) {
1916
- await referenced(room.locator);
1917
3138
  if (!live() || paused.has(room.locator))
1918
3139
  return unconfirmed;
1919
3140
  const held = replicas.get(room.locator);
1920
3141
  if (held !== undefined) {
1921
- const refreshed = await pullRefresh(authorized, held.confirmed, live);
3142
+ const refreshed = await pullRefresh(authorized, held, live);
1922
3143
  if (!live())
1923
3144
  return unconfirmed;
1924
3145
  if (refreshed.kind === 'still-current') {
@@ -1933,10 +3154,11 @@ export function composeRuntime(options) {
1933
3154
  return null;
1934
3155
  }
1935
3156
  if (refreshed.kind === 'advanced') {
1936
- if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
1937
- const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
3157
+ const landing = { kind: 'refreshed', base: refreshed.base };
3158
+ if (!(await publish(local, room, refreshed.record, landing, live))) {
3159
+ const now = replicas.get(room.locator)?.record.roomVersion;
1938
3160
  const reached = now !== undefined &&
1939
- options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
3161
+ options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
1940
3162
  return reached ? null : advance(room, local, authorized, live);
1941
3163
  }
1942
3164
  await retainModel(local, room, 'collection', modelOf(room.locator));
@@ -1946,17 +3168,18 @@ export function composeRuntime(options) {
1946
3168
  return refreshed;
1947
3169
  if (opened !== null) {
1948
3170
  owed.add(room.locator);
1949
- readSide.recover();
3171
+ liveQueries.recover();
1950
3172
  return unconfirmed;
1951
3173
  }
3174
+ if (reloadsDue.get(room.locator) === generation)
3175
+ return unconfirmed;
1952
3176
  }
1953
3177
  const bootstrapped = await pullBootstrap(authorized, room, local, live);
1954
- if (!live())
1955
- return unconfirmed;
1956
- if (bootstrapped.kind !== 'staged')
1957
- return bootstrapped;
1958
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
1959
- return null;
3178
+ if (bootstrapped.kind === 'staged') {
3179
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3180
+ return live() ? null : unconfirmed;
3181
+ }
3182
+ return live() ? bootstrapped : unconfirmed;
1960
3183
  }
1961
3184
  async function withhold(withheld, room, local, progress) {
1962
3185
  if (withheld.kind === 'unconfirmed')
@@ -2028,57 +3251,254 @@ export function composeRuntime(options) {
2028
3251
  (accepted.author === undefined ? '' : ` author ${accepted.author}`) +
2029
3252
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
2030
3253
  }
2031
- async function adoptAccepted(room, local, submitted, accepted) {
2032
- await referenced(room.locator);
3254
+ function settling(acceptance) {
3255
+ return acceptance !== null && 'apply' in acceptance;
3256
+ }
3257
+ function refusalOf(acceptance) {
3258
+ return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
3259
+ }
3260
+ async function acceptance(room, local, submitted, accepted) {
2033
3261
  if (!continuing())
2034
3262
  return unconfirmed;
2035
3263
  const current = replicas.get(room.locator);
2036
3264
  if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
2037
3265
  return null;
2038
3266
  }
2039
- const held = current.confirmed.record.roomVersion;
3267
+ const held = current.record.roomVersion;
2040
3268
  logAccepted(room.locator, submitted, accepted);
2041
- const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
2042
- ? { kind: 'staged', confirmed: current.confirmed }
2043
- : applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
2044
- ? accepted.roomVersion
2045
- : held, accepted.roomVersion);
2046
- if (applied.kind !== 'staged') {
2047
- const state = pendings.get(submitted.id);
2048
- await local.accept(room.locator, submitted.id, accepted.roomVersion);
2049
- if (state !== undefined && !fences.has(room.locator))
2050
- pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
2051
- emitQueue(room.locator);
2052
- return applied;
2053
- }
2054
- const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
2055
- pendings.delete(submitted.id);
2056
- unreadable.delete(room.locator);
2057
- sourceRefusals.delete(room.locator);
2058
- declarationRefusals.delete(room.locator);
2059
- const set = settled();
2060
- list(room, true);
2061
- publishChangeSet(set);
2062
- moveTo({ ...status, ...attention() });
2063
- return null;
3269
+ let record = current.record;
3270
+ let changes;
3271
+ if (!options.adapter.reached(room.locator, held, accepted.roomVersion)) {
3272
+ const paths = [...returnedPaths(submitted, accepted)];
3273
+ const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
3274
+ const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
3275
+ record = {
3276
+ ...record,
3277
+ roomVersion: options.adapter.follows(room.locator, held, accepted.roomVersion)
3278
+ ? accepted.roomVersion
3279
+ : held,
3280
+ };
3281
+ const overrides = new Map([
3282
+ ...back.set.map((entry) => [entry.path, entry]),
3283
+ ...back.removed.map((path) => [path, null]),
3284
+ ]);
3285
+ const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
3286
+ if (refused !== null) {
3287
+ const key = pendingKey(room.locator, submitted.id);
3288
+ return {
3289
+ locator: room.locator,
3290
+ left: [],
3291
+ commit: {
3292
+ rooms: [],
3293
+ writes: [
3294
+ {
3295
+ locator: room.locator,
3296
+ accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
3297
+ },
3298
+ ],
3299
+ },
3300
+ refused,
3301
+ apply() {
3302
+ const state = pendings.get(key);
3303
+ if (state !== undefined && !fences.has(room.locator))
3304
+ pendings.set(key, { ...state, accepted: accepted.roomVersion });
3305
+ return changeSet(room.locator, { queue: true });
3306
+ },
3307
+ };
3308
+ }
3309
+ changes = back;
3310
+ }
3311
+ const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
3312
+ return {
3313
+ ...settled,
3314
+ apply(answer) {
3315
+ const set = settled.apply(answer);
3316
+ pendings.delete(pendingKey(room.locator, submitted.id));
3317
+ unreadable.delete(room.locator);
3318
+ sourceRefusals.delete(room.locator);
3319
+ declarationRefusals.delete(room.locator);
3320
+ list(room, true);
3321
+ return set;
3322
+ },
3323
+ };
3324
+ }
3325
+ async function adoptedTogether(local, acceptances) {
3326
+ const settlements = acceptances.filter(settling);
3327
+ if (settlements.length > 0) {
3328
+ publishChangeSet(await together(local, settlements));
3329
+ moveTo({ ...status, ...attention() });
3330
+ }
3331
+ await unpair(local);
2064
3332
  }
2065
3333
  async function adopted(room, local, submitted, accepted) {
2066
- const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
3334
+ const failed = await exclusive(async () => {
3335
+ const adopting = await acceptance(room, local, submitted, accepted);
3336
+ await adoptedTogether(local, [adopting]);
3337
+ return refusalOf(adopting);
3338
+ });
3339
+ if (failed === null)
3340
+ return true;
3341
+ if (failed.kind === 'refused' && continuing())
3342
+ refusedSource(room, failed);
3343
+ return false;
3344
+ }
3345
+ async function adoptedMove(room, partner, local, submitted, accepted) {
3346
+ const other = discovered.get(partner);
3347
+ const failed = await exclusive(async () => {
3348
+ const left = await acceptance(room, local, submitted, accepted);
3349
+ const arriving = replicas.get(partner)?.queue.find((batch) => batch.id === submitted.id);
3350
+ const arrived = other === undefined || arriving === undefined || accepted.destination === undefined
3351
+ ? null
3352
+ : await acceptance(other, local, arriving, {
3353
+ kind: 'accepted',
3354
+ ...accepted.destination,
3355
+ });
3356
+ await adoptedTogether(local, [left, arrived]);
3357
+ const refusedArrival = refusalOf(arrived);
3358
+ if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
3359
+ refusedSource(other, refusedArrival);
3360
+ }
3361
+ return refusalOf(left);
3362
+ });
3363
+ submitLater(partner);
2067
3364
  if (failed === null)
2068
3365
  return true;
2069
3366
  if (failed.kind === 'refused' && continuing())
2070
3367
  refusedSource(room, failed);
2071
3368
  return false;
2072
3369
  }
3370
+ function queueIn(locator) {
3371
+ return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
3372
+ }
3373
+ function behindIn(locator, batch) {
3374
+ const held = replicas.get(locator)?.queue;
3375
+ if (held === undefined)
3376
+ return queuesWithoutReplica.get(locator) !== undefined;
3377
+ return held.findIndex((one) => one.id === batch.id) > 0;
3378
+ }
3379
+ function unpaired(skipped) {
3380
+ for (const locator of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
3381
+ for (const batch of queueIn(locator)) {
3382
+ const partner = partnerOf(batch, locator);
3383
+ if (partner === null || skipped.has(pendingKey(locator, batch.id)))
3384
+ continue;
3385
+ if (queueIn(partner).some((held) => held.id === batch.id))
3386
+ continue;
3387
+ if (unpairedIn(locator, batch))
3388
+ return { locator, batch };
3389
+ }
3390
+ }
3391
+ return null;
3392
+ }
3393
+ async function unpairing(local, locator, batches) {
3394
+ const ids = new Set(batches.map(({ id }) => id));
3395
+ const leaving = batches.map((batch) => ({
3396
+ batch,
3397
+ returned: !localSaves && leavingIn(batch, locator) ? returnedBy('withdrawn') : null,
3398
+ }));
3399
+ const outcome = localSaves ? 'dropped' : 'unpaired';
3400
+ const current = replicas.get(locator);
3401
+ if (current !== undefined) {
3402
+ const settled = await settlement(local, locator, current, current.record, current.queue.filter((one) => !ids.has(one.id)), { leaving });
3403
+ return {
3404
+ ...settled,
3405
+ apply(answer) {
3406
+ for (const batch of batches)
3407
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
3408
+ const set = settled.apply(answer);
3409
+ submitLater(locator);
3410
+ return set;
3411
+ },
3412
+ };
3413
+ }
3414
+ const queue = queueIn(locator).filter((one) => !ids.has(one.id));
3415
+ const written = new Map();
3416
+ for (const { batch, returned } of leaving) {
3417
+ if (returned === null)
3418
+ continue;
3419
+ for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
3420
+ written.set(saved.path, {
3421
+ locator,
3422
+ path: saved.path,
3423
+ revision: roomDraftRevision(),
3424
+ parts: returnedInto((written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path))?.parts, saved.parts),
3425
+ returned,
3426
+ });
3427
+ }
3428
+ }
3429
+ const carried = carriedBack(locator, leaving.flatMap(({ batch, returned }) => (returned === null ? [] : [batch])), written);
3430
+ return {
3431
+ locator,
3432
+ left: batches,
3433
+ commit: {
3434
+ rooms: [],
3435
+ writes: [
3436
+ {
3437
+ locator,
3438
+ queue,
3439
+ roomDrafts: [...written.values()],
3440
+ consumedRoomDrafts: carried.taken,
3441
+ },
3442
+ ...carried.writes,
3443
+ ],
3444
+ },
3445
+ apply() {
3446
+ for (const batch of batches) {
3447
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
3448
+ pendings.delete(pendingKey(locator, batch.id));
3449
+ }
3450
+ if (queue.length > 0)
3451
+ queuesWithoutReplica.set(locator, queue);
3452
+ else
3453
+ queuesWithoutReplica.delete(locator);
3454
+ for (const record of written.values())
3455
+ rememberRoomDraft(record);
3456
+ carried.remember();
3457
+ submitLater(locator);
3458
+ return mergeChangeSets([
3459
+ changeSet(locator, {
3460
+ queue: true,
3461
+ drafts: [...written.keys(), ...carried.taken],
3462
+ }),
3463
+ ...carried.changes,
3464
+ ]);
3465
+ },
3466
+ };
3467
+ }
3468
+ async function unpair(local) {
3469
+ const handled = new Set();
3470
+ for (let held = unpaired(handled); held !== null && continuing(); held = unpaired(handled)) {
3471
+ const { locator, batch } = held;
3472
+ handled.add(pendingKey(locator, batch.id));
3473
+ publishChangeSet(await together(local, [await unpairing(local, locator, [batch])]));
3474
+ }
3475
+ if (handled.size === 0)
3476
+ return;
3477
+ moveTo({ ...status, ...attention() });
3478
+ }
3479
+ async function discardHeld(head, room, local, signal) {
3480
+ const partner = partnerOf(head, room.locator);
3481
+ const other = partner === null ? undefined : discovered.get(partner);
3482
+ await discardPending(other === undefined ? [room] : [room, other], local, signal);
3483
+ }
2073
3484
  async function submitRoom(room, local, authorized, progress, signal) {
2074
3485
  while (continuing() && authorization !== undefined && !signal.aborted) {
2075
3486
  const replica = replicas.get(room.locator);
2076
3487
  const head = replica?.queue[0];
2077
3488
  if (replica === undefined || head === undefined)
2078
3489
  return 'done';
2079
- if (pendings.get(head.id)?.accepted !== undefined)
3490
+ const state = pendings.get(pendingKey(room.locator, head.id));
3491
+ if (state?.accepted !== undefined)
3492
+ return 'done';
3493
+ const partner = partnerOf(head, room.locator);
3494
+ if (partner !== null && !leavingIn(head, room.locator)) {
3495
+ submitLater(partner);
3496
+ return 'done';
3497
+ }
3498
+ if (partner !== null && behindIn(partner, head)) {
3499
+ progress.complete = false;
2080
3500
  return 'done';
2081
- const state = pendings.get(head.id);
3501
+ }
2082
3502
  const { submission, lost: earlier } = state ?? {
2083
3503
  submission: { state: 'waiting' },
2084
3504
  lost: false,
@@ -2094,16 +3514,16 @@ export function composeRuntime(options) {
2094
3514
  }
2095
3515
  const lost = earlier || submission.state === 'submitted';
2096
3516
  if (lost && pastHorizon(head.id)) {
2097
- await discardPending(room, local, signal);
3517
+ await discardHeld(head, room, local, signal);
2098
3518
  return 'done';
2099
3519
  }
2100
3520
  let result;
2101
3521
  journal.record(`submission ${room.locator} ${head.id}`);
2102
- await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
3522
+ await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
2103
3523
  if (discoveryPaused || fences.has(room.locator))
2104
3524
  return 'done';
2105
3525
  if (signal.aborted || writesHeld()) {
2106
- await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
3526
+ await exclusive(() => mark(local, room.locator, head, submission, earlier));
2107
3527
  if (signal.aborted)
2108
3528
  return 'done';
2109
3529
  progress.complete = false;
@@ -2111,11 +3531,17 @@ export function composeRuntime(options) {
2111
3531
  return 'done';
2112
3532
  }
2113
3533
  const opensBefore = openSignals;
2114
- const answered = awaitingAnswer(room.locator);
3534
+ const answeredHere = awaitingAnswer(room.locator);
3535
+ const answeredThere = partner === null ? () => { } : awaitingAnswer(partner);
3536
+ const answered = () => {
3537
+ answeredHere();
3538
+ answeredThere();
3539
+ };
2115
3540
  try {
2116
3541
  result = await options.adapter.mutate(authorized, {
2117
3542
  locator: room.locator,
2118
3543
  changeBatch: head,
3544
+ file: batchFile(head),
2119
3545
  ...(state?.upload === undefined ? {} : { upload: state.upload }),
2120
3546
  }, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
2121
3547
  }
@@ -2140,9 +3566,10 @@ export function composeRuntime(options) {
2140
3566
  return 'again';
2141
3567
  }
2142
3568
  if (result.kind === 'room-not-readable') {
3569
+ const unread = discovered.get(result.locator) ?? room;
2143
3570
  progress.complete = false;
2144
- unreadable.add(room.locator);
2145
- list(room, false);
3571
+ unreadable.add(unread.locator);
3572
+ list(unread, false);
2146
3573
  emitRooms();
2147
3574
  return 'done';
2148
3575
  }
@@ -2157,24 +3584,30 @@ export function composeRuntime(options) {
2157
3584
  }
2158
3585
  if (result.kind === 'write-halted') {
2159
3586
  journal.record(`outcome ${room.locator} ${head.id} write-halted`);
2160
- await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, lost));
3587
+ await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
2161
3588
  if (serviceMode === 'open' && openSignals !== opensBefore)
2162
3589
  continue;
2163
3590
  haltedRefusal = true;
2164
3591
  progress.complete = false;
2165
3592
  return 'done';
2166
3593
  }
3594
+ const other = partner === null ? undefined : discovered.get(partner);
2167
3595
  if (result.kind === 'conflicted' ||
2168
3596
  result.kind === 'rejected' ||
2169
3597
  result.kind === 'expired' ||
2170
3598
  result.kind === 'retryable-refusal') {
2171
3599
  if (result.kind === 'expired' && lost && result.restored !== true) {
2172
3600
  journal.record(`outcome ${room.locator} ${head.id} expired`);
2173
- await discardPending(room, local, signal);
3601
+ await discardHeld(head, room, local, signal);
2174
3602
  return 'done';
2175
3603
  }
2176
3604
  if (result.kind === 'expired') {
2177
- await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
3605
+ await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
3606
+ const there = other === undefined ? null : await advance(other, local, authorized, continuing);
3607
+ if (other !== undefined && there !== null) {
3608
+ await withhold(there, other, local, progress);
3609
+ return 'done';
3610
+ }
2178
3611
  const withheld = await advance(room, local, authorized, continuing);
2179
3612
  if (withheld !== null) {
2180
3613
  await withhold(withheld, room, local, progress);
@@ -2191,11 +3624,25 @@ export function composeRuntime(options) {
2191
3624
  if (result.kind === 'retryable-refusal') {
2192
3625
  const reset = result.reset;
2193
3626
  progress.complete = false;
2194
- await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
3627
+ await exclusive(() => mark(local, room.locator, head, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
2195
3628
  return reset === undefined ? 'again' : { at: Date.parse(reset) };
2196
3629
  }
2197
3630
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
2198
3631
  const left = leaving(head, result);
3632
+ const refreshed = async () => ({
3633
+ there: other === undefined ? null : await advance(other, local, authorized, continuing),
3634
+ here: await advance(room, local, authorized, continuing),
3635
+ });
3636
+ const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
3637
+ if (judged !== null && (judged.there !== null || judged.here !== null)) {
3638
+ await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
3639
+ if (other !== undefined && judged.there !== null) {
3640
+ await withhold(judged.there, other, local, progress);
3641
+ }
3642
+ if (judged.here !== null)
3643
+ await withhold(judged.here, room, local, progress);
3644
+ return 'done';
3645
+ }
2199
3646
  await exclusive(async () => {
2200
3647
  if (!continuing())
2201
3648
  return;
@@ -2203,25 +3650,29 @@ export function composeRuntime(options) {
2203
3650
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
2204
3651
  return;
2205
3652
  }
2206
- const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
2207
- leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
2208
- (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
2209
- ? { ...left, returned: null }
2210
- : left,
2211
- });
2212
- publishChangeSet(settled());
3653
+ const confirmedLayer = await sliceAt(current.record, 'confirmed', [head]);
3654
+ const held = (result.kind === 'expired' &&
3655
+ (await landedWhole(room.locator, head, confirmedLayer))) ||
3656
+ (result.kind === 'conflicted' &&
3657
+ (await landedWhole(room.locator, head, confirmedLayer, result.affected)));
3658
+ publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
3659
+ await unpair(local);
2213
3660
  moveTo({ ...status, ...attention() });
2214
3661
  });
2215
3662
  if (result.kind === 'expired')
2216
3663
  continue;
2217
- const withheld = await advance(room, local, authorized, continuing);
2218
- if (withheld !== null) {
2219
- await withhold(withheld, room, local, progress);
3664
+ const { there, here } = judged ?? (await refreshed());
3665
+ if (other !== undefined && there !== null)
3666
+ await withhold(there, other, local, progress);
3667
+ if (here !== null) {
3668
+ await withhold(here, room, local, progress);
2220
3669
  return 'done';
2221
3670
  }
2222
3671
  continue;
2223
3672
  }
2224
- const kept = await adopted(room, local, head, result).finally(answered);
3673
+ const kept = await (partner === null
3674
+ ? adopted(room, local, head, result)
3675
+ : adoptedMove(room, partner, local, head, result)).finally(answered);
2225
3676
  if (!kept) {
2226
3677
  progress.complete = false;
2227
3678
  return 'done';
@@ -2232,44 +3683,67 @@ export function composeRuntime(options) {
2232
3683
  function pastHorizon(identity) {
2233
3684
  return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
2234
3685
  }
2235
- async function discardPending(room, local, signal) {
2236
- const { locator } = room;
3686
+ async function discardPending(rooms, local, signal) {
2237
3687
  const discarded = await exclusive(async () => {
2238
- if (fences.has(locator))
2239
- return false;
2240
- const before = replicas.get(locator);
2241
- const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
2242
- fences.set(locator, { at: discoveries, erased: false });
2243
- lastFenced.set(locator, ++fencings);
2244
- try {
2245
- await local.discard(locator);
2246
- }
2247
- finally {
2248
- fences.delete(locator);
3688
+ const held = rooms
3689
+ .filter(({ locator }) => !fences.has(locator))
3690
+ .map((room) => ({
3691
+ room,
3692
+ before: replicas.get(room.locator),
3693
+ queue: queueIn(room.locator),
3694
+ }));
3695
+ if (held.length === 0)
3696
+ return [];
3697
+ const answer = await local.commit({
3698
+ rooms: held.flatMap(({ room, before }) => before === undefined
3699
+ ? []
3700
+ : [
3701
+ {
3702
+ locator: room.locator,
3703
+ key: before.record.key,
3704
+ roots: before.record.typedSelection?.roots ?? [],
3705
+ overlay: { set: [], cleared: [...before.overlaid] },
3706
+ },
3707
+ ]),
3708
+ discards: held.map(({ room }) => room.locator),
3709
+ });
3710
+ for (const { room, before, queue } of held) {
3711
+ const { locator } = room;
3712
+ lastFenced.set(locator, ++fencings);
3713
+ for (const batch of queue)
3714
+ pendings.delete(pendingKey(locator, batch.id));
3715
+ queuesWithoutReplica.delete(locator);
3716
+ if (before !== undefined) {
3717
+ replicas.set(locator, {
3718
+ ...before,
3719
+ queue: [],
3720
+ overlaid: new Set(),
3721
+ count: answer.counts[locator] ?? before.count,
3722
+ });
3723
+ }
3724
+ journal.record(`room discarded ${locator}`);
2249
3725
  }
2250
- for (const batch of queue)
2251
- pendings.delete(batch.id);
2252
- queuesWithoutReplica.delete(locator);
2253
- const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
2254
- if (after !== null)
2255
- replicas.set(locator, after);
2256
- journal.record(`room discarded ${locator}`);
2257
- publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
3726
+ publishChangeSet(mergeChangeSets(held.map(({ room }) => changeSet(room.locator, {
3727
+ documents: answer.changed[room.locator] ?? [],
3728
+ queue: true,
3729
+ }))));
2258
3730
  moveTo({ ...status, ...attention() });
2259
- return true;
2260
- });
2261
- if (!discarded)
2262
- return;
2263
- const progress = { complete: true, reachable: true };
2264
- await roomTask(locator, async (live) => {
2265
- await Promise.race([
2266
- reload(room, local, progress, () => live() && !signal.aborted),
2267
- abandonedOn(signal),
2268
- ]);
3731
+ return held.map(({ room }) => room);
2269
3732
  });
2270
- if (signal.aborted)
2271
- return;
2272
- await rest(progress);
3733
+ for (const room of discarded) {
3734
+ if (signal.aborted)
3735
+ return;
3736
+ const progress = { complete: true, reachable: true };
3737
+ await roomTask(room.locator, async (live) => {
3738
+ await Promise.race([
3739
+ reload(room, local, progress, () => live() && !signal.aborted),
3740
+ abandonedOn(signal),
3741
+ ]);
3742
+ });
3743
+ if (signal.aborted)
3744
+ return;
3745
+ await rest(progress);
3746
+ }
2273
3747
  }
2274
3748
  function awaitingAnswer(locator) {
2275
3749
  let release = () => { };
@@ -2286,7 +3760,7 @@ export function composeRuntime(options) {
2286
3760
  async function retainModel(local, room, attached, next) {
2287
3761
  const key = modelKey(room.locator, attached);
2288
3762
  const previous = modelOf(room.locator, attached);
2289
- const heldBefore = replicas.get(room.locator)?.confirmed.record;
3763
+ const heldBefore = replicas.get(room.locator)?.record;
2290
3764
  if (JSON.stringify(previous) === JSON.stringify(next) &&
2291
3765
  (heldBefore === undefined ||
2292
3766
  (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
@@ -2302,36 +3776,56 @@ export function composeRuntime(options) {
2302
3776
  if (!continuing())
2303
3777
  return;
2304
3778
  const held = replicas.get(room.locator);
3779
+ let changed = null;
2305
3780
  if (held !== undefined) {
2306
3781
  const declared = {
2307
- ...held.confirmed.record,
3782
+ ...held.record,
2308
3783
  operations: room.operations,
2309
3784
  replica: room.replica,
2310
3785
  typedSelection: room.typedSelection ?? null,
2311
3786
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
2312
3787
  };
2313
- const reclassified = sameMembership(declared, held.confirmed.record)
2314
- ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
2315
- : readReclassified(declared, held.confirmed);
2316
- const record = reclassified.kind === 'staged'
3788
+ let read = 'staged';
3789
+ if (!sameMembership(declared, held.record)) {
3790
+ const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), compositionOf(declared));
3791
+ read =
3792
+ verdict.verdict !== 'refuse'
3793
+ ? 'staged'
3794
+ : verdict.rule === 'model-unavailable'
3795
+ ? 'deferred'
3796
+ : { kind: 'refused', refusal: verdict };
3797
+ }
3798
+ const record = read === 'staged'
2317
3799
  ? declared
2318
- : {
2319
- ...declared,
2320
- typedSelection: held.confirmed.record.typedSelection,
2321
- };
2322
- const read = reclassified.kind === 'deferred'
2323
- ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
2324
- : reclassified;
2325
- await local.write({ next: record, previous: held.confirmed.record });
2326
- if (read.kind === 'staged') {
2327
- replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
3800
+ : { ...declared, typedSelection: held.record.typedSelection };
3801
+ const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
3802
+ const replayedLayer = await replayed(record, confirmedLayer, held.queue);
3803
+ const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
3804
+ const answer = await local.commit({
3805
+ rooms: [
3806
+ {
3807
+ locator: room.locator,
3808
+ key: record.key,
3809
+ record: storedRecord(record),
3810
+ roots: record.typedSelection?.roots ?? [],
3811
+ composition: compositionOf(record),
3812
+ overlay: diff.overlay,
3813
+ },
3814
+ ],
3815
+ });
3816
+ const { count, indexed } = committedRoom(room.locator, answer);
3817
+ replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
3818
+ changed = changeSet(room.locator, {
3819
+ documents: answer.changed[room.locator] ?? [],
3820
+ room: true,
3821
+ });
3822
+ if (read === 'staged' || read === 'deferred') {
2328
3823
  if (declarationRefusals.delete(room.locator)) {
2329
3824
  sourceRefusals.delete(room.locator);
2330
3825
  unreadable.delete(room.locator);
2331
3826
  }
2332
3827
  }
2333
- else if (read.kind === 'refused') {
2334
- replicas.set(room.locator, { ...held, confirmed: { ...held.confirmed, record } });
3828
+ else {
2335
3829
  refusedSource(room, read);
2336
3830
  declarationRefusals.add(room.locator);
2337
3831
  }
@@ -2342,14 +3836,14 @@ export function composeRuntime(options) {
2342
3836
  invalidModels.delete(key);
2343
3837
  }
2344
3838
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
2345
- if (held === undefined)
3839
+ if (changed === null)
2346
3840
  emitRooms();
2347
3841
  else
2348
- publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
3842
+ publishChangeSet(changed);
2349
3843
  });
2350
3844
  }
2351
3845
  async function scoped(result, room, local) {
2352
- if ((result.scope === 'content' || result.scope === 'sync') && !result.retryable) {
3846
+ if (result.scope === 'content' && !result.retryable) {
2353
3847
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
2354
3848
  await loseRooms([room.locator]);
2355
3849
  return;
@@ -2367,15 +3861,7 @@ export function composeRuntime(options) {
2367
3861
  });
2368
3862
  else if (result.scope !== 'operation') {
2369
3863
  unreadable.add(room.locator);
2370
- scopedAway = {
2371
- ...room,
2372
- replica: { ...room.replica, admission: 'unavailable' },
2373
- operations: {
2374
- ...room.operations,
2375
- ...(result.scope === 'content' ? { read: [] } : {}),
2376
- ...(result.scope === 'sync' ? { sync: [] } : {}),
2377
- },
2378
- };
3864
+ scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
2379
3865
  await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
2380
3866
  }
2381
3867
  if (!continuing())
@@ -2386,30 +3872,22 @@ export function composeRuntime(options) {
2386
3872
  }
2387
3873
  function heldRooms(listed) {
2388
3874
  return listed.flatMap((room) => {
2389
- const record = replicas.get(room.locator)?.confirmed.record;
3875
+ const record = replicas.get(room.locator)?.record;
2390
3876
  if (record === undefined || record.replica.selection !== room.replica.selection)
2391
3877
  return [];
2392
- return [
2393
- {
2394
- locator: room.locator,
2395
- roomVersion: record.roomVersion,
2396
- documents: record.documents,
2397
- revisions: record.revisions,
2398
- },
2399
- ];
3878
+ return [{ locator: room.locator, roomVersion: record.roomVersion }];
2400
3879
  });
2401
3880
  }
2402
3881
  async function synchronize(listed, local, progress, attempt, live) {
2403
- const acquired = listed.filter(acquires);
2404
- const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
3882
+ const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
2405
3883
  if (!live())
2406
3884
  return;
2407
- const ready = acquired.filter((_, index) => {
3885
+ const ready = listed.filter((_, index) => {
2408
3886
  const outcome = prepared[index];
2409
3887
  return outcome?.status === 'fulfilled' && outcome.value;
2410
3888
  });
2411
3889
  if (attempt === 'opened') {
2412
- options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
3890
+ options.adapter.roomStream?.hold(heldRooms(ready));
2413
3891
  handed = true;
2414
3892
  }
2415
3893
  discovering?.release();
@@ -2425,7 +3903,7 @@ export function composeRuntime(options) {
2425
3903
  progress.complete = false;
2426
3904
  return false;
2427
3905
  }
2428
- const prior = replicas.get(room.locator)?.confirmed.record;
3906
+ const prior = replicas.get(room.locator)?.record;
2429
3907
  if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
2430
3908
  list(prior, false);
2431
3909
  emitRooms();
@@ -2468,23 +3946,24 @@ export function composeRuntime(options) {
2468
3946
  }
2469
3947
  async function reload(room, local, progress, live) {
2470
3948
  try {
3949
+ if (live())
3950
+ reloadsDue.delete(room.locator);
2471
3951
  if (!live() || authorization === undefined)
2472
3952
  return;
2473
3953
  const bootstrapped = await pullBootstrap(authorization, room, local, live);
2474
- if (!live())
2475
- return;
2476
3954
  if (bootstrapped.kind !== 'staged') {
2477
- await withhold(bootstrapped, room, local, progress);
3955
+ if (live())
3956
+ await withhold(bootstrapped, room, local, progress);
2478
3957
  return;
2479
3958
  }
2480
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
3959
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
2481
3960
  await caughtUp(room, local, progress, live);
2482
3961
  }
2483
3962
  finally {
2484
3963
  caughtUpTo(room.locator);
2485
3964
  }
2486
3965
  }
2487
- async function discoverRooms(local, progress, attempt, live) {
3966
+ async function discoverRooms(local, progress, attempt, live, scope) {
2488
3967
  if (closed || authorization === undefined) {
2489
3968
  notifyReady();
2490
3969
  return;
@@ -2494,7 +3973,7 @@ export function composeRuntime(options) {
2494
3973
  const started = discoveries;
2495
3974
  let result;
2496
3975
  try {
2497
- result = await options.adapter.discover(authorization);
3976
+ result = await options.adapter.discover(authorization, scope);
2498
3977
  }
2499
3978
  catch {
2500
3979
  result = { kind: 'transport-uncertainty' };
@@ -2534,12 +4013,13 @@ export function composeRuntime(options) {
2534
4013
  }
2535
4014
  for (const room of result.rooms) {
2536
4015
  if (room.operations === undefined ||
2537
- !['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
4016
+ !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
2538
4017
  room.operations[operation].every((scope) => typeof scope === 'string')) ||
2539
4018
  room.replica === undefined ||
2540
4019
  typeof room.replica.selection !== 'string' ||
2541
4020
  room.replica.selection.length === 0 ||
2542
- !['admitted', 'unavailable'].includes(room.replica.admission)) {
4021
+ !['admitted', 'unavailable'].includes(room.replica.admission) ||
4022
+ !['locked', 'open'].includes(room.replica.offline)) {
2543
4023
  journal.record(`invalid model or replica declaration ${room.locator}`);
2544
4024
  await refuse('missing-required-guarantee');
2545
4025
  notifyReady();
@@ -2563,15 +4043,16 @@ export function composeRuntime(options) {
2563
4043
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
2564
4044
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
2565
4045
  limits = result.providerLimits;
2566
- await local.declare(limits, capabilities);
4046
+ await local.fact('providerLimits', limits);
4047
+ await local.fact('capabilities', capabilities);
2567
4048
  }
2568
4049
  const signedIn = result.member ?? null;
2569
4050
  if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
2570
- await local.keepMember(signedIn);
4051
+ await local.fact('member', signedIn);
2571
4052
  member = signedIn;
2572
4053
  emitMember();
2573
4054
  }
2574
- await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
4055
+ await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
2575
4056
  }
2576
4057
  async function ensureAuthorization() {
2577
4058
  if (authorization !== undefined)
@@ -2668,9 +4149,40 @@ export function composeRuntime(options) {
2668
4149
  options.adapter.roomStream?.reopen();
2669
4150
  });
2670
4151
  }
4152
+ function rediscover(scope) {
4153
+ return task(OPENING_LANE, async (live) => {
4154
+ if (!live() || !socketOpen)
4155
+ return;
4156
+ const progress = { complete: true, reachable: true };
4157
+ const gate = discover();
4158
+ moveTo(transferring(status));
4159
+ try {
4160
+ const local = await openLocal();
4161
+ if (!live())
4162
+ return;
4163
+ await ensureAuthorization();
4164
+ if (!live())
4165
+ return;
4166
+ await discoverRooms(local, progress, 'authorized', live, scope);
4167
+ }
4168
+ catch {
4169
+ progress.complete = false;
4170
+ }
4171
+ finally {
4172
+ gate.release();
4173
+ notifyReady();
4174
+ }
4175
+ if (!live())
4176
+ return;
4177
+ if (!progress.complete)
4178
+ complete = false;
4179
+ await rest(progress, live);
4180
+ });
4181
+ }
2671
4182
  function closedSocket(revoked, lost) {
2672
4183
  const opened = socketOpen;
2673
4184
  socketOpen = false;
4185
+ connectionLost();
2674
4186
  if (lost) {
2675
4187
  reads.lost();
2676
4188
  conversations.lost();
@@ -2691,9 +4203,10 @@ export function composeRuntime(options) {
2691
4203
  function applyOpening(expired) {
2692
4204
  const progress = underway?.generation === generation ? underway.progress : null;
2693
4205
  const cycle = progress ?? { complete: true, reachable: true };
2694
- const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
2695
4206
  if (opened === generation)
2696
4207
  opened = null;
4208
+ const reloading = progress === null ? [] : owedReloads(expired);
4209
+ const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
2697
4210
  return task(OPENING_LANE, async (live) => {
2698
4211
  underway = null;
2699
4212
  if (!live())
@@ -2703,24 +4216,17 @@ export function composeRuntime(options) {
2703
4216
  await rest(cycle, live);
2704
4217
  return;
2705
4218
  }
2706
- await roomsCarried(expired);
4219
+ await roomsCarried([...expired, ...reloading]);
2707
4220
  if (!live())
2708
4221
  return;
2709
- const replaced = new Set(owed);
2710
- owed.clear();
2711
4222
  try {
2712
4223
  const local = await openLocal();
2713
- const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
2714
- const reloading = held.filter((room) => !expired.includes(room.locator) &&
2715
- (replaced.has(room.locator) || revoked(room.locator)));
2716
- for (const room of reloading)
2717
- catching.add(room.locator);
2718
- readSide.recover();
2719
- const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
2720
- const reloaded = [...(await reloads), ...(await replacing)];
4224
+ const reloaded = await replacing;
2721
4225
  if (progress.reachable) {
2722
- for (const room of held) {
2723
- if (!expired.includes(room.locator) && !reloading.includes(room)) {
4226
+ for (const room of discovered.values()) {
4227
+ if (replicas.has(room.locator) &&
4228
+ !expired.includes(room.locator) &&
4229
+ !reloading.includes(room.locator)) {
2724
4230
  await confirmModels(room, local);
2725
4231
  }
2726
4232
  }
@@ -2737,14 +4243,29 @@ export function composeRuntime(options) {
2737
4243
  await finishOpening(progress, live);
2738
4244
  });
2739
4245
  }
4246
+ function owedReloads(expired) {
4247
+ const reloading = [...discovered.values()]
4248
+ .filter((room) => replicas.has(room.locator) &&
4249
+ !expired.includes(room.locator) &&
4250
+ (owed.has(room.locator) || revoked(room.locator)))
4251
+ .map((room) => room.locator);
4252
+ owed.clear();
4253
+ for (const locator of reloading)
4254
+ catching.add(locator);
4255
+ liveQueries.recover();
4256
+ return reloading;
4257
+ }
2740
4258
  async function reloadNow(replaced, progress) {
4259
+ const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
4260
+ for (const room of reloaded)
4261
+ reloadsDue.set(room.locator, generation);
2741
4262
  try {
2742
4263
  const local = await openLocal();
2743
- return await Promise.allSettled([...discovered.values()]
2744
- .filter((room) => replicas.has(room.locator) && replaced(room))
2745
- .map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
4264
+ return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
2746
4265
  }
2747
4266
  catch {
4267
+ for (const room of reloaded)
4268
+ reloadsDue.delete(room.locator);
2748
4269
  progress.reachable = false;
2749
4270
  return [];
2750
4271
  }
@@ -2870,10 +4391,11 @@ export function composeRuntime(options) {
2870
4391
  if (status.providerUnreachable)
2871
4392
  conversations.reconnected();
2872
4393
  const reading = { complete: true, reachable: true };
2873
- if (demand.conversations)
2874
- await conversations.catchUp(reading);
4394
+ await conversations.catchUp(reading);
2875
4395
  if (progress.reachable && reading.reachable)
2876
4396
  await conversations.resend(reading);
4397
+ if (progress.reachable)
4398
+ void recovery.attempt('calls', resendCalls);
2877
4399
  if (!reading.reachable) {
2878
4400
  progress.reachable = false;
2879
4401
  resendLater();
@@ -2897,7 +4419,7 @@ export function composeRuntime(options) {
2897
4419
  if (synced && !recorded) {
2898
4420
  const completedAt = new Date().toISOString();
2899
4421
  try {
2900
- await (await openLocal()).complete(completedAt);
4422
+ await (await openLocal()).fact('lastCompleted', completedAt);
2901
4423
  if (!live())
2902
4424
  return;
2903
4425
  recorded = true;
@@ -2934,11 +4456,11 @@ export function composeRuntime(options) {
2934
4456
  return;
2935
4457
  const room = discovered.get(locator);
2936
4458
  const held = replicas.get(locator);
2937
- if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
4459
+ if (room === undefined || room.replica.admission !== 'admitted')
2938
4460
  return;
2939
4461
  if (held !== undefined &&
2940
- (held.confirmed.record.replica.selection !== room.replica.selection ||
2941
- options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
4462
+ (held.record.replica.selection !== room.replica.selection ||
4463
+ options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
2942
4464
  return;
2943
4465
  }
2944
4466
  const during = underway?.generation === generation ? underway.progress : null;
@@ -2985,6 +4507,7 @@ export function composeRuntime(options) {
2985
4507
  return;
2986
4508
  if (event.kind === 'opened') {
2987
4509
  socketOpen = true;
4510
+ connectionLive();
2988
4511
  generation += 1;
2989
4512
  reads.opened();
2990
4513
  conversations.reopened();
@@ -2995,9 +4518,18 @@ export function composeRuntime(options) {
2995
4518
  closedSocket(event.revoked, event.lost);
2996
4519
  else if (event.kind === 'opening')
2997
4520
  void applyOpening(event.expired);
4521
+ else if (event.kind === 'authority-changed')
4522
+ void rediscover(event.scope);
2998
4523
  else
2999
4524
  void applyRoom(event.locator, event.roomVersion);
3000
- }, receipts);
4525
+ }, receipts, heldRevisions);
4526
+ }
4527
+ async function heldRevisions(locator, paths) {
4528
+ const held = replicas.get(locator);
4529
+ if (held === undefined || paths.length === 0)
4530
+ return new Map();
4531
+ const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
4532
+ return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
3001
4533
  }
3002
4534
  function writesHeld() {
3003
4535
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -3049,7 +4581,7 @@ export function composeRuntime(options) {
3049
4581
  if (!live())
3050
4582
  return;
3051
4583
  await conversations.reconcile();
3052
- if (conversations.current() || !demand.conversations) {
4584
+ if (conversations.current()) {
3053
4585
  show();
3054
4586
  return;
3055
4587
  }
@@ -3067,10 +4599,12 @@ export function composeRuntime(options) {
3067
4599
  if (closed)
3068
4600
  return;
3069
4601
  closed = true;
3070
- if (narrowing !== null)
3071
- clearTimeout(narrowing);
3072
4602
  if (rechecking !== null)
3073
4603
  clearInterval(rechecking);
4604
+ for (const resolve of shownWaiters.splice(0))
4605
+ resolve();
4606
+ connectionLive();
4607
+ lockListeners.clear();
3074
4608
  readinessWatches.clear();
3075
4609
  withdrawStream?.();
3076
4610
  withdrawStream = undefined;
@@ -3079,17 +4613,13 @@ export function composeRuntime(options) {
3079
4613
  withdrawServiceMode?.();
3080
4614
  conversations.close();
3081
4615
  void recovery.retire();
3082
- store?.close();
3083
- void opening?.then((opened) => {
3084
- opened.close();
3085
- }, () => { });
3086
4616
  roomListeners.clear();
3087
4617
  refusalListeners.clear();
3088
4618
  storageLossListeners.clear();
3089
4619
  statusListeners.clear();
3090
4620
  serviceModeListeners.clear();
3091
4621
  memberListeners.clear();
3092
- readSide.close();
4622
+ liveQueries.close();
3093
4623
  queueSubscriptions.clear();
3094
4624
  roomDraftListeners.clear();
3095
4625
  presentationListeners.clear();
@@ -3116,7 +4646,11 @@ export function composeRuntime(options) {
3116
4646
  if (options.adapter.roomStream === undefined)
3117
4647
  void sync('unstreamed');
3118
4648
  function planned(input, reach) {
3119
- return planOf(input, roomFacts(reach), planReach(reach));
4649
+ const plan = planOf(input, roomFacts(reach), planReach(reach));
4650
+ const named = plan.paths !== null &&
4651
+ plan.rooms !== null &&
4652
+ plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
4653
+ return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
3120
4654
  }
3121
4655
  function watchPlan(plan, deliver) {
3122
4656
  let seeded = false;
@@ -3131,7 +4665,7 @@ export function composeRuntime(options) {
3131
4665
  const watchers = seedWatchers.get(plan.key) ?? new Set();
3132
4666
  seedWatchers.set(plan.key, watchers);
3133
4667
  watchers.add(shown);
3134
- const stop = readSide.watch(plan, identityOf(plan), () => {
4668
+ const stop = liveQueries.watch(plan, identityOf(plan), () => {
3135
4669
  const cover = covered(plan);
3136
4670
  awaitPreparation(cover);
3137
4671
  return seedBehind(plan, cover);
@@ -3152,67 +4686,58 @@ export function composeRuntime(options) {
3152
4686
  stop();
3153
4687
  };
3154
4688
  }
3155
- async function searched(input, reach) {
3156
- const plan = searchPlanOf(input);
3157
- const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
4689
+ async function searched(input, reach, locked) {
4690
+ const asked = searchPlanOf(input);
4691
+ const filter = {
4692
+ ...filterOf(asked.documents ?? {}, roomFacts(reach), planReach(reach)),
4693
+ locked,
4694
+ };
4695
+ const unlocking = (filter.rooms ?? [])
4696
+ .filter((room) => locked.includes(room))
4697
+ .map((room) => ({ room, awaits: 'unlock' }));
3158
4698
  for (;;) {
3159
4699
  const cover = covered(filter);
3160
4700
  awaitPreparation(cover);
3161
- const pending = 'pending' in cover ? [...cover.pending] : [];
3162
- const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3163
- const held = covered({
3164
- ...filter,
3165
- rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
3166
- });
3167
- const covering = 'pending' in held ? [] : held.coverage.rooms;
3168
- const unindexed = covering
3169
- .filter(({ room }) => !searchIndex.rooms().has(room))
3170
- .map(({ room }) => room);
3171
- for (const room of unindexed)
3172
- pending.push({ room, awaits: 'index' });
3173
- const listed = conversations.searchable();
3174
- const named = reach !== null && !reach.conversations
3175
- ? []
3176
- : (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
3177
- const waiting = named.filter((one) => !searchIndex.conversations().has(one));
3178
- for (const conversation of waiting)
3179
- pending.push({ conversation, awaits: 'index' });
3180
- if (unindexed.length > 0 || waiting.length > 0)
3181
- index(unindexed, waiting);
4701
+ const pending = [...unlocking, ...('pending' in cover ? cover.pending : [])];
3182
4702
  const complete = pending.length === 0 && !('pending' in cover);
4703
+ const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3183
4704
  const answering = complete
3184
4705
  ? filter
3185
- : {
3186
- ...filter,
3187
- rooms: covering
3188
- .filter(({ room }) => searchIndex.rooms().has(room))
3189
- .map(({ room }) => room),
3190
- };
4706
+ : { ...filter, rooms: reachedRooms(filter).filter((room) => !awaited.has(room)) };
3191
4707
  const answered = covered(answering);
3192
4708
  if ('pending' in answered)
3193
4709
  continue;
3194
- const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
3195
- const seen = searchIndex.changes();
3196
- await searchIndex.caughtUp();
3197
- const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
3198
- if (selected !== null && 'pending' in selected)
3199
- continue;
3200
- if (searchIndex.changes() !== seen ||
3201
- JSON.stringify(covered(answering)) !== JSON.stringify(answered))
4710
+ const listed = conversations.searchable();
4711
+ const named = reach !== null && !reach.conversations
4712
+ ? []
4713
+ : (asked.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
4714
+ const hits = await (await openLocal()).search({
4715
+ text: asked.text,
4716
+ limit: capped(asked.limit),
4717
+ after: asked.after,
4718
+ total: asked.total,
4719
+ documents: {
4720
+ plan: answering,
4721
+ rooms: answered.rooms,
4722
+ beneath: asked.collection
4723
+ ? answered.rooms.map(({ locator, key }) => ({
4724
+ key,
4725
+ roots: replicas.get(locator)?.record.typedSelection?.roots ?? [],
4726
+ }))
4727
+ : [],
4728
+ },
4729
+ conversations: named,
4730
+ });
4731
+ if (JSON.stringify(covered(answering)) !== JSON.stringify(answered))
3202
4732
  continue;
3203
- const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms === null ? null : new Set(answering.rooms), plan.collection
3204
- ? new Map(answered.coverage.rooms.map(({ room }) => [
3205
- room,
3206
- replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
3207
- ]))
3208
- : null);
3209
4733
  const coverage = {
3210
4734
  rooms: answered.coverage.rooms,
3211
- conversations: searchedConversations,
3212
- index: searchIndex.generation(),
4735
+ conversations: named,
3213
4736
  overlay: answered.coverage.overlay,
3214
4737
  };
3215
- return complete ? { ...hits, coverage } : { pending, ...hits, coverage };
4738
+ return complete
4739
+ ? { ...hits, coverage }
4740
+ : { pending, documents: hits.documents, messages: hits.messages, coverage };
3216
4741
  }
3217
4742
  }
3218
4743
  const watch = (topic, input, deliver) => {
@@ -3221,13 +4746,17 @@ export function composeRuntime(options) {
3221
4746
  roomDraftListeners.add(listener);
3222
4747
  void exclusive(async () => {
3223
4748
  await openLocal();
3224
- if (closed || !roomDraftListeners.has(listener))
3225
- return;
3226
- listener.ready = true;
3227
4749
  }).then(() => {
3228
- if (closed || !listener.ready || !roomDraftListeners.has(listener))
4750
+ if (closed || !roomDraftListeners.has(listener))
3229
4751
  return;
3230
- listener.notify(snapshotRoomDrafts());
4752
+ const snapshot = snapshotRoomDrafts();
4753
+ draftsShown = draftsShown.then(async () => {
4754
+ const value = await snapshot.catch(() => null);
4755
+ if (value === null || closed || !roomDraftListeners.has(listener))
4756
+ return;
4757
+ listener.ready = true;
4758
+ listener.notify(value);
4759
+ });
3231
4760
  }, () => { });
3232
4761
  return () => {
3233
4762
  roomDraftListeners.delete(listener);
@@ -3322,7 +4851,7 @@ export function composeRuntime(options) {
3322
4851
  return listed();
3323
4852
  },
3324
4853
  query: ((input) => rowsQuery(planned(input, null))),
3325
- search: (input) => searched(input, null),
4854
+ search: (input) => searched(input, null, []),
3326
4855
  types(locator) {
3327
4856
  return {
3328
4857
  collection: modelTypes(modelOf(locator)),
@@ -3342,9 +4871,9 @@ export function composeRuntime(options) {
3342
4871
  return listed;
3343
4872
  });
3344
4873
  },
3345
- read(room, path) {
4874
+ async read(room, path) {
3346
4875
  const record = roomDrafts.get(room)?.get(path);
3347
- return record === undefined ? null : roomDraftOf(record);
4876
+ return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
3348
4877
  },
3349
4878
  write(room, path, revision, input) {
3350
4879
  return exclusive(async () => {
@@ -3354,11 +4883,11 @@ export function composeRuntime(options) {
3354
4883
  const local = await openLocal();
3355
4884
  const current = roomDrafts.get(room)?.get(path);
3356
4885
  if ((current?.revision ?? null) !== revision) {
3357
- return staleWrite(current === undefined ? null : roomDraftOf(current));
4886
+ return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
3358
4887
  }
3359
4888
  const file = parts.file === undefined
3360
4889
  ? undefined
3361
- : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
4890
+ : await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
3362
4891
  const record = {
3363
4892
  locator: room,
3364
4893
  path,
@@ -3366,10 +4895,12 @@ export function composeRuntime(options) {
3366
4895
  parts: held(current?.parts, parts, file),
3367
4896
  returned: current?.returned ?? null,
3368
4897
  };
3369
- await local.keepRoomDraft(record);
4898
+ await local.keepRoomDraft(record, parts.file === undefined || file === undefined
4899
+ ? []
4900
+ : [{ key: file.localId, blob: parts.file.blob }]);
3370
4901
  rememberRoomDraft(record);
3371
- publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
3372
- return { outcome: 'written', roomDraft: roomDraftOf(record) };
4902
+ publishChangeSet(changeSet(room, { drafts: [path] }));
4903
+ return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
3373
4904
  });
3374
4905
  },
3375
4906
  discard(room, path, revision) {
@@ -3377,11 +4908,11 @@ export function composeRuntime(options) {
3377
4908
  const local = await openLocal();
3378
4909
  const current = roomDrafts.get(room)?.get(path);
3379
4910
  if (current === undefined || current.revision !== revision) {
3380
- return staleWrite(current === undefined ? null : roomDraftOf(current));
4911
+ return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
3381
4912
  }
3382
4913
  await local.discardRoomDraft(room, path);
3383
4914
  forgetRoomDraft(room, path);
3384
- publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
4915
+ publishChangeSet(changeSet(room, { drafts: [path] }));
3385
4916
  moveTo({ ...status, ...attention() });
3386
4917
  return { outcome: 'written', roomDraft: null };
3387
4918
  });
@@ -3397,29 +4928,38 @@ export function composeRuntime(options) {
3397
4928
  });
3398
4929
  },
3399
4930
  write(app, value) {
3400
- return exclusive(async () => {
3401
- const local = await openLocal();
3402
- const stored = structuredClone(value);
3403
- await local.present({ app, value: stored });
3404
- presentations.set(app, stored);
3405
- publishChangeSet(changeSet('', null, null, { presentation: app }));
3406
- });
4931
+ return keepPresentation(app, () => structuredClone(value));
4932
+ },
4933
+ edit(app, members) {
4934
+ return keepPresentation(app, (held) => Object.fromEntries([
4935
+ ...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(members, member)),
4936
+ ...Object.entries(structuredClone(members)).filter(([, value]) => value !== null),
4937
+ ]));
3407
4938
  },
3408
4939
  },
3409
4940
  save(room, named, writtenBy) {
3410
4941
  return exclusive(() => saveRoomDrafts(room, named, writtenBy));
3411
4942
  },
4943
+ saveParts(room, parts, writtenBy) {
4944
+ return exclusive(() => saveParts(room, parts, writtenBy));
4945
+ },
3412
4946
  history(locator, path, query) {
3413
4947
  return readHistory(locator, path, query);
3414
4948
  },
4949
+ resolve(room, path, destination) {
4950
+ return resolveReference(room, path, destination);
4951
+ },
3415
4952
  binaryFile: {
4953
+ part: readPart,
3416
4954
  read: readBinaryPath,
3417
- readReference: readBinaryReference,
4955
+ link: linkBinaryPath,
3418
4956
  },
3419
- call(name, input) {
3420
- return invokeAction(name, input);
4957
+ async call(name, input) {
4958
+ await ready;
4959
+ const room = calledRooms(name, input).find((one) => locked(one));
4960
+ return room === undefined ? recoveredAction(name, input) : lockedRoom(room);
3421
4961
  },
3422
- reached(locator, roomVersion, path) {
4962
+ async reached(locator, roomVersion, path) {
3423
4963
  return path === undefined
3424
4964
  ? reachedBy(locator, roomVersion)
3425
4965
  : writtenAfter(locator, roomVersion, path);
@@ -3427,12 +4967,13 @@ export function composeRuntime(options) {
3427
4967
  log() {
3428
4968
  return journal.export();
3429
4969
  },
4970
+ cleanUp,
3430
4971
  persistence() {
3431
4972
  return persistence === 'granted';
3432
4973
  },
3433
4974
  async retry() {
3434
4975
  journal.record('retry');
3435
- unindexable.clear();
4976
+ unpreparable.clear();
3436
4977
  queueReadiness();
3437
4978
  resume();
3438
4979
  },
@@ -3456,12 +4997,15 @@ export function composeRuntime(options) {
3456
4997
  const reachable = {
3457
4998
  md,
3458
4999
  watch,
3459
- referrerScope: (referrer) => options.adapter.referrerScope(referrer),
3460
5000
  plan: planned,
3461
5001
  query: (plan) => rowsQuery(plan),
3462
5002
  watchPlan,
3463
5003
  search: searched,
3464
5004
  watchReadiness,
5005
+ locked,
5006
+ lockedRooms,
5007
+ watchLocks,
5008
+ located,
3465
5009
  };
3466
5010
  return {
3467
5011
  md,
@@ -3469,7 +5013,6 @@ export function composeRuntime(options) {
3469
5013
  stop,
3470
5014
  lose: loseRooms,
3471
5015
  watch,
3472
- acquire,
3473
5016
  persisted(answer) {
3474
5017
  if (answer === persistence)
3475
5018
  return;
@@ -3477,6 +5020,7 @@ export function composeRuntime(options) {
3477
5020
  journal.record(`persistence ${answer}`);
3478
5021
  queueReadiness();
3479
5022
  },
5023
+ shown: screen,
3480
5024
  within: (reach, port) => narrowed(reachable, reach, port),
3481
5025
  };
3482
5026
  }