@agentshouse/mdruntime 0.1.0-c5d3fa6fb67bc → 0.1.0-c63ad08fad0c3

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 +240 -220
  2. package/README.md +9 -8
  3. package/dist/adapter.d.ts +72 -24
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/attached-model.d.ts +4 -1
  6. package/dist/attached-model.d.ts.map +1 -1
  7. package/dist/attached-model.js +13 -3
  8. package/dist/bootstrap.d.ts +26 -7
  9. package/dist/bootstrap.d.ts.map +1 -1
  10. package/dist/bootstrap.js +167 -82
  11. package/dist/chain.d.ts +11 -0
  12. package/dist/chain.d.ts.map +1 -0
  13. package/dist/chain.js +64 -0
  14. package/dist/change-set.d.ts +9 -16
  15. package/dist/change-set.d.ts.map +1 -1
  16. package/dist/change-set.js +11 -89
  17. package/dist/change.d.ts +31 -1
  18. package/dist/change.d.ts.map +1 -1
  19. package/dist/change.js +10 -1
  20. package/dist/chat/earlier.d.ts +10 -0
  21. package/dist/chat/earlier.d.ts.map +1 -0
  22. package/dist/chat/earlier.js +54 -0
  23. package/dist/chat/house.d.ts +25 -0
  24. package/dist/chat/house.d.ts.map +1 -0
  25. package/dist/chat/house.js +38 -0
  26. package/dist/chat/input.d.ts +3 -1
  27. package/dist/chat/input.d.ts.map +1 -1
  28. package/dist/chat/input.js +11 -18
  29. package/dist/chat/kit.js +1 -1
  30. package/dist/chat/list.d.ts +2 -1
  31. package/dist/chat/list.d.ts.map +1 -1
  32. package/dist/chat/list.js +45 -5
  33. package/dist/chat/markdown.d.ts +1 -0
  34. package/dist/chat/markdown.d.ts.map +1 -1
  35. package/dist/chat/markdown.js +78 -20
  36. package/dist/chat/message.d.ts +1 -0
  37. package/dist/chat/message.d.ts.map +1 -1
  38. package/dist/chat/message.js +73 -11
  39. package/dist/chat/messages.d.ts +14 -1
  40. package/dist/chat/messages.d.ts.map +1 -1
  41. package/dist/chat/messages.js +79 -24
  42. package/dist/chat/name.d.ts +1 -0
  43. package/dist/chat/name.d.ts.map +1 -1
  44. package/dist/chat/name.js +1 -1
  45. package/dist/chat/opened.d.ts +1 -0
  46. package/dist/chat/opened.d.ts.map +1 -1
  47. package/dist/chat/opened.js +2 -0
  48. package/dist/chat/overlay.d.ts +1 -0
  49. package/dist/chat/overlay.d.ts.map +1 -1
  50. package/dist/chat/overlay.js +11 -5
  51. package/dist/chat/parts.d.ts +1 -0
  52. package/dist/chat/parts.d.ts.map +1 -1
  53. package/dist/chat/parts.js +3 -2
  54. package/dist/chat/pieces.d.ts +62 -0
  55. package/dist/chat/pieces.d.ts.map +1 -0
  56. package/dist/chat/pieces.js +1110 -0
  57. package/dist/chat/provider.d.ts +8 -1
  58. package/dist/chat/provider.d.ts.map +1 -1
  59. package/dist/chat/provider.js +98 -92
  60. package/dist/chat/question.d.ts +1 -1
  61. package/dist/chat/question.d.ts.map +1 -1
  62. package/dist/chat/question.js +7 -18
  63. package/dist/chat/ready.d.ts +5 -2
  64. package/dist/chat/ready.d.ts.map +1 -1
  65. package/dist/chat/ready.js +7 -5
  66. package/dist/chat/room-file.d.ts +8 -0
  67. package/dist/chat/room-file.d.ts.map +1 -0
  68. package/dist/chat/room-file.js +72 -0
  69. package/dist/chat/steady.d.ts +3 -0
  70. package/dist/chat/steady.d.ts.map +1 -0
  71. package/dist/chat/steady.js +26 -0
  72. package/dist/chat/style.d.ts.map +1 -1
  73. package/dist/chat/style.js +6 -1
  74. package/dist/chat.d.ts +3 -2
  75. package/dist/chat.d.ts.map +1 -1
  76. package/dist/chat.js +2 -2
  77. package/dist/client.d.ts +11 -2
  78. package/dist/client.d.ts.map +1 -1
  79. package/dist/client.js +45 -7
  80. package/dist/collection.d.ts +6 -19
  81. package/dist/collection.d.ts.map +1 -1
  82. package/dist/collection.js +30 -287
  83. package/dist/compile.d.ts +16 -0
  84. package/dist/compile.d.ts.map +1 -0
  85. package/dist/compile.js +721 -0
  86. package/dist/connection.d.ts +22 -8
  87. package/dist/connection.d.ts.map +1 -1
  88. package/dist/connection.js +2509 -830
  89. package/dist/conversation.d.ts +15 -3
  90. package/dist/conversation.d.ts.map +1 -1
  91. package/dist/conversation.js +238 -94
  92. package/dist/digest.d.ts +7 -0
  93. package/dist/digest.d.ts.map +1 -0
  94. package/dist/digest.js +113 -0
  95. package/dist/durability.d.ts +3 -1
  96. package/dist/durability.d.ts.map +1 -1
  97. package/dist/entry.d.ts +3 -1
  98. package/dist/entry.d.ts.map +1 -1
  99. package/dist/entry.js +18 -23
  100. package/dist/file.d.ts +2 -10
  101. package/dist/file.d.ts.map +1 -1
  102. package/dist/file.js +3 -120
  103. package/dist/fulltext.d.ts +27 -0
  104. package/dist/fulltext.d.ts.map +1 -0
  105. package/dist/fulltext.js +368 -0
  106. package/dist/host.d.ts +5 -4
  107. package/dist/host.d.ts.map +1 -1
  108. package/dist/host.js +57 -7
  109. package/dist/identity.d.ts +0 -1
  110. package/dist/identity.d.ts.map +1 -1
  111. package/dist/identity.js +0 -3
  112. package/dist/index.d.ts +12 -8
  113. package/dist/index.d.ts.map +1 -1
  114. package/dist/index.js +4 -0
  115. package/dist/live.d.ts +22 -0
  116. package/dist/live.d.ts.map +1 -0
  117. package/dist/live.js +123 -0
  118. package/dist/memory.d.ts +7 -3
  119. package/dist/memory.d.ts.map +1 -1
  120. package/dist/memory.js +39 -17
  121. package/dist/messages.d.ts +28 -0
  122. package/dist/messages.d.ts.map +1 -0
  123. package/dist/messages.js +90 -0
  124. package/dist/name.d.ts +4 -0
  125. package/dist/name.d.ts.map +1 -0
  126. package/dist/name.js +122 -0
  127. package/dist/observation.d.ts +14 -0
  128. package/dist/observation.d.ts.map +1 -1
  129. package/dist/observation.js +93 -3
  130. package/dist/opened.d.ts +22 -0
  131. package/dist/opened.d.ts.map +1 -0
  132. package/dist/opened.js +107 -0
  133. package/dist/outcome.d.ts +35 -4
  134. package/dist/outcome.d.ts.map +1 -1
  135. package/dist/outcome.js +28 -0
  136. package/dist/overlay.d.ts +2 -5
  137. package/dist/overlay.d.ts.map +1 -1
  138. package/dist/overlay.js +86 -171
  139. package/dist/query.d.ts +29 -2
  140. package/dist/query.d.ts.map +1 -1
  141. package/dist/query.js +46 -7
  142. package/dist/reach.d.ts +5 -1
  143. package/dist/reach.d.ts.map +1 -1
  144. package/dist/reach.js +167 -48
  145. package/dist/react.d.ts +3 -3
  146. package/dist/react.d.ts.map +1 -1
  147. package/dist/react.js +7 -17
  148. package/dist/readiness.d.ts +18 -3
  149. package/dist/readiness.d.ts.map +1 -1
  150. package/dist/readiness.js +1 -1
  151. package/dist/reads.d.ts +11 -0
  152. package/dist/reads.d.ts.map +1 -0
  153. package/dist/reads.js +100 -0
  154. package/dist/recovery.d.ts +2 -2
  155. package/dist/recovery.d.ts.map +1 -1
  156. package/dist/recovery.js +8 -7
  157. package/dist/refresh.d.ts +13 -15
  158. package/dist/refresh.d.ts.map +1 -1
  159. package/dist/refresh.js +78 -51
  160. package/dist/replica.d.ts +57 -33
  161. package/dist/replica.d.ts.map +1 -1
  162. package/dist/replica.js +295 -186
  163. package/dist/room-draft.d.ts +15 -1
  164. package/dist/room-draft.d.ts.map +1 -1
  165. package/dist/room-draft.js +32 -25
  166. package/dist/rows.d.ts +29 -30
  167. package/dist/rows.d.ts.map +1 -1
  168. package/dist/rows.js +113 -64
  169. package/dist/save.d.ts +51 -14
  170. package/dist/save.d.ts.map +1 -1
  171. package/dist/save.js +518 -180
  172. package/dist/search.d.ts +25 -42
  173. package/dist/search.d.ts.map +1 -1
  174. package/dist/search.js +30 -169
  175. package/dist/seed.d.ts +16 -0
  176. package/dist/seed.d.ts.map +1 -0
  177. package/dist/seed.js +75 -0
  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 +32 -0
  200. package/dist/wire.d.ts.map +1 -1
  201. package/dist/wire.js +8 -0
  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,34 +1,39 @@
1
- import { attachedModel } from './attached-model.js';
1
+ import { escapePointer, parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
2
+ import { attachedModel, NO_ROOM_TYPES, roomTypesOf } from './attached-model.js';
2
3
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
3
- import { observing } from './observation.js';
4
- import { filterOf, identityOf, planOf, } from './query.js';
5
- import { createReadSide } from './read-side.js';
6
- import { markdownPath, roomOfKey } from './rows.js';
7
- import { createSearchIndex, searchPlanOf, } from './search.js';
4
+ import { observing, whole } from './observation.js';
5
+ import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
6
+ import { createLiveQueries } from './live.js';
7
+ import { seededResult } from './seed.js';
8
+ import { documentKey, roomOfKey } from './rows.js';
9
+ import { searchPlanOf } from './search.js';
10
+ import { capped, MAXIMUM_WINDOW } from './windows.js';
8
11
  import { bootstrapRoom } from './bootstrap.js';
9
- import { changeSet, entering, mergeChangeSets } from './change-set.js';
12
+ import { changeSet, mergeChangeSets } from './change-set.js';
10
13
  import { createConversationStore, } from './conversation.js';
11
- import { queuedFile } from './change.js';
12
- import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
14
+ import { leaving as leavingIn, partnerOf, queuedFile, } from './change.js';
15
+ import { available, collectionOf, unreadable as unreadableIn, validRoot, } from './collection.js';
13
16
  import { runtimeError } from './failure.js';
14
17
  import { createLog } from './log.js';
15
18
  import { binaryRecord } from './binary.js';
16
- import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
19
+ import { openedFiles } from './opened.js';
20
+ import { ABSENT_TARGET, absentBinary, absentReference, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
17
21
  import { createRecovery } from './recovery.js';
18
- import { refreshRoom } from './refresh.js';
19
- import { buildBatch, joined, limitedIn, requeued } from './save.js';
20
- import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
21
- import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
22
+ import { doorReads } from './reads.js';
23
+ import { advancement } from './refresh.js';
24
+ import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
25
+ import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, sameMembership, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
26
+ import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
22
27
  import { same, saving, STARTING, transferring, } from './status.js';
23
28
  import { detectStorageLoss, nextGeneration } from './durability.js';
24
- import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
25
29
  import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
26
30
  import { FencedMap } from './fence.js';
27
- import { tally } from './shared.js';
28
- import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
31
+ import { sameBinary } from './overlay.js';
32
+ import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
29
33
  import { identityTime, roomDraftRevision } from './identity.js';
30
34
  import { beneath, normalize } from './paths.js';
31
35
  import { openRuntimeStore, } from './store.js';
36
+ import { changedCleanly, sameComposition, validatedCollection, } from './validation.js';
32
37
  import { unconfirmed } from './transfer.js';
33
38
  import { governs, holds, narrowed, planReach, } from './reach.js';
34
39
  function within(root, path) {
@@ -48,19 +53,29 @@ const OPENING_LANE = 'opening';
48
53
  const CONVERSATION_LANE = 'conversations';
49
54
  const ROOM_READS = 256;
50
55
  const ROOM_ATTEMPTS = 16;
51
- const outside = new Map();
52
- function outsideOf(roots) {
53
- const key = JSON.stringify(roots);
54
- const held = outside.get(key);
55
- if (held !== undefined)
56
- return held;
57
- const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
58
- const made = {
59
- documents: (path) => (markdownPath(path) ? counted(path) : 0),
60
- binaries: counted,
61
- };
62
- outside.set(key, made);
63
- return made;
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;
64
79
  }
65
80
  function abandonedOn(signal) {
66
81
  return new Promise((resolve) => {
@@ -73,11 +88,14 @@ function abandonedOn(signal) {
73
88
  function roomLane(locator) {
74
89
  return `room ${locator}`;
75
90
  }
91
+ function pendingKey(locator, id) {
92
+ return `${locator}\u0000${id}`;
93
+ }
76
94
  export function composeRuntime(options) {
77
95
  const localSaves = options.adapter.localSaves === true;
96
+ const roomTypes = roomTypesOf(options.adapter.roomTypes);
78
97
  let authorization = options.authorization;
79
98
  let opening;
80
- let store;
81
99
  const fences = new Map();
82
100
  const isFenced = (locator) => fences.has(locator);
83
101
  let fencings = 0;
@@ -87,6 +105,8 @@ export function composeRuntime(options) {
87
105
  const replicas = new FencedMap(isFenced);
88
106
  const queuesWithoutReplica = new FencedMap(isFenced);
89
107
  const pendings = new Map();
108
+ const seeds = new Map();
109
+ const seedWatchers = new Map();
90
110
  const roomDrafts = new FencedMap(isFenced);
91
111
  const presentations = new Map();
92
112
  let limits = [];
@@ -99,6 +119,7 @@ export function composeRuntime(options) {
99
119
  const liveModels = new FencedMap(isModelFenced);
100
120
  const invalidModels = new Set();
101
121
  const discovered = new Map();
122
+ const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
102
123
  let roomsListed = false;
103
124
  const scopedRefusals = new Map();
104
125
  const sourceRefusals = new Map();
@@ -135,6 +156,7 @@ export function composeRuntime(options) {
135
156
  let handed = false;
136
157
  const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
137
158
  const repairing = new Set();
159
+ const calling = new Set();
138
160
  const repairable = new Map();
139
161
  let outcomeRetention = null;
140
162
  const answering = new Map();
@@ -145,6 +167,7 @@ export function composeRuntime(options) {
145
167
  let discovering = null;
146
168
  let opened = null;
147
169
  const owed = new Set();
170
+ const reloadsDue = new Map();
148
171
  const lanes = new Map();
149
172
  let roomReads = { generation, free: ROOM_READS, waiting: [] };
150
173
  const working = new Set();
@@ -156,23 +179,22 @@ export function composeRuntime(options) {
156
179
  const queueSubscriptions = new Set();
157
180
  const roomDraftListeners = new Set();
158
181
  const presentationListeners = new Set();
159
- const readSide = createReadSide();
182
+ const liveQueries = createLiveQueries((plan, cover) => answeredPlan(plan, cover));
160
183
  let withdrawConversations;
161
184
  let currencyQueued = null;
162
185
  let socketOpen = false;
186
+ const confirmed = new Set();
187
+ let lockGrace = null;
188
+ const lockListeners = new Set();
163
189
  let serviceMode = 'open';
164
190
  let haltedRefusal = false;
165
191
  let openSignals = 0;
166
192
  const serviceModeListeners = new Set();
167
193
  let member = null;
168
194
  const memberListeners = new Set();
169
- const prepared = new Set();
170
- const projecting = new Set();
171
195
  const unpreparedDemand = new Set();
172
- const unindexable = new Set();
196
+ const unpreparable = new Set();
173
197
  let preparing = false;
174
- let demand = WHOLE_DEMAND;
175
- let narrowing = null;
176
198
  let targets = {};
177
199
  let persistence = options.shell.persistence;
178
200
  let capacity = null;
@@ -182,26 +204,41 @@ export function composeRuntime(options) {
182
204
  let readinessQueued = false;
183
205
  const catching = new Set();
184
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();
185
216
  const conversations = createConversationStore({
186
217
  agents: declaredAgents,
187
- 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())),
188
231
  call: (name, input) => invokeAction(name, input),
189
232
  durably,
190
233
  stream: () => options.adapter.conversations,
191
234
  publish: publishChangeSet,
192
- online: () => !status.providerUnreachable && refusal === null,
235
+ online: () => connectionOf() !== 'unreachable' && refusal === null,
193
236
  closed: () => closed,
194
237
  unanswered: resendLater,
195
238
  expired: (operation) => pastHorizon(operation),
196
239
  });
197
- const searchIndex = createSearchIndex({
198
- room: (room) => readSide.room(room),
199
- document: (room, path) => readSide.document(room, path),
200
- });
201
- const roomsToIndex = new Set();
202
- const conversationsToIndex = new Set();
203
- let indexing = false;
204
240
  const journal = createLog();
241
+ const keptFiles = openedFiles(options.database, isFenced);
205
242
  let notifyReady = () => { };
206
243
  const ready = new Promise((resolve) => {
207
244
  notifyReady = resolve;
@@ -219,16 +256,36 @@ export function composeRuntime(options) {
219
256
  if (room.size === 0)
220
257
  roomDrafts.delete(locator);
221
258
  }
222
- 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) {
223
268
  return structuredClone({
224
269
  room: record.locator,
225
270
  path: record.path,
226
271
  revision: record.revision,
227
272
  parts: record.parts,
228
- changedUnderneath: underneath(record.parts, record.path, replicas.get(record.locator)?.visible),
273
+ changedUnderneath: underneath(record.parts, record.path, visible),
229
274
  returned: record.returned,
230
275
  });
231
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
+ }
232
289
  function snapshotRoomDrafts() {
233
290
  const records = [...roomDrafts.values()].flatMap((room) => [...room.values()]);
234
291
  records.sort((left, right) => {
@@ -238,23 +295,38 @@ export function composeRuntime(options) {
238
295
  return left.path < right.path ? -1 : 1;
239
296
  return 0;
240
297
  });
241
- return records.map(roomDraftOf);
298
+ return roomDraftsOf(records);
242
299
  }
300
+ let draftsShown = Promise.resolve();
243
301
  function publishRoomDrafts(waitingOnly) {
244
302
  if (closed)
245
303
  return;
246
- const value = snapshotRoomDrafts();
247
- for (const listener of roomDraftListeners) {
248
- if (waitingOnly && listener.ready)
249
- continue;
250
- listener.ready = true;
251
- listener.notify(value);
252
- }
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
+ });
253
316
  }
254
317
  function presentationOf(app) {
255
318
  const value = presentations.get(app);
256
319
  return value === undefined ? null : structuredClone(value);
257
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
+ }
258
330
  function emitPresentation(app) {
259
331
  if (closed)
260
332
  return;
@@ -303,21 +375,69 @@ export function composeRuntime(options) {
303
375
  return { attentionRequired, reset };
304
376
  }
305
377
  function documentsIn(locator) {
306
- const replica = replicas.get(locator);
307
- if (replica === undefined)
308
- return 0;
309
- const roots = replica.confirmed.record.typedSelection?.roots ?? [];
310
- const weighed = outsideOf(roots);
311
- return (tally(replica.visible.documents, weighed.documents) +
312
- tally(replica.visible.binaries, weighed.binaries));
378
+ return replicas.get(locator)?.count ?? 0;
313
379
  }
314
380
  function listed() {
315
381
  return structuredClone([...rooms.values()].map((room) => {
316
382
  const documents = documentsIn(room.locator);
317
383
  counted.set(room.locator, documents);
318
- 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 };
319
393
  }));
320
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
+ }
321
441
  function roomFacts(reach = null) {
322
442
  const held = [...rooms.values()]
323
443
  .filter((room) => holds(reach, room.locator))
@@ -336,7 +456,7 @@ export function composeRuntime(options) {
336
456
  return { listed: roomsListed, rooms: held };
337
457
  }
338
458
  function emitRooms() {
339
- readSide.recover();
459
+ liveQueries.recover();
340
460
  queueReadiness();
341
461
  const value = listed();
342
462
  for (const listener of roomListeners)
@@ -366,7 +486,7 @@ export function composeRuntime(options) {
366
486
  if (closed)
367
487
  return;
368
488
  queueReadiness();
369
- const next = status.state === 'synced' && demand.conversations && !conversations.current()
489
+ const next = status.state === 'synced' && !conversations.current()
370
490
  ? { ...status, state: 'syncing' }
371
491
  : status;
372
492
  if (same(shown, next))
@@ -375,17 +495,17 @@ export function composeRuntime(options) {
375
495
  for (const listener of statusListeners)
376
496
  listener(shown);
377
497
  }
378
- function entryOf(batch) {
498
+ function entryOf(locator, batch) {
379
499
  if (localSaves)
380
500
  return Object.freeze({ batch, state: 'local' });
381
- const state = pendings.get(batch.id);
501
+ const state = pendings.get(pendingKey(locator, batch.id));
382
502
  if (state?.accepted !== undefined) {
383
503
  return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
384
504
  }
385
505
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
386
506
  }
387
507
  function entriesOf(locator) {
388
- return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
508
+ return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map((batch) => entryOf(locator, batch));
389
509
  }
390
510
  function emitQueue(locator) {
391
511
  const entries = entriesOf(locator);
@@ -395,62 +515,39 @@ export function composeRuntime(options) {
395
515
  }
396
516
  }
397
517
  function applyChangeSet(set) {
398
- const held = {
399
- ...set,
400
- documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
401
- };
402
- readSide.apply(held);
403
- searchIndex.changed(held);
518
+ liveQueries.touched(set);
404
519
  }
405
- async function referenced(locator) {
406
- for (;;) {
407
- const record = replicas.get(locator)?.confirmed.record;
408
- if (record === undefined || closed)
409
- return;
410
- const held = { text: record.documents, binaries: record.binaries };
411
- const composition = compositionOf(record);
412
- if (referencesIndexed(held, composition))
413
- return;
414
- await indexReferrers(held, composition);
415
- }
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;
416
525
  }
417
526
  async function prepareReplica(locator) {
418
- await referenced(locator);
419
- const replica = replicas.get(locator);
420
- if (replica === undefined || closed || prepared.has(locator))
421
- return;
422
- const visible = replica.visible;
423
- const paths = [
424
- ...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
425
- ];
426
- const entered = [];
427
- projecting.add(locator);
428
- try {
429
- await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
430
- const current = replicas.get(locator);
431
- if (current === undefined || closed)
432
- return;
433
- const set = entering(locator, current, chunk);
434
- readSide.apply(set);
435
- entered.push(set);
436
- });
437
- if (!replicas.has(locator) || closed)
527
+ for (;;) {
528
+ const held = replicas.get(locator);
529
+ if (held === undefined || closed || held.indexed)
438
530
  return;
439
- prepared.add(locator);
440
- }
441
- finally {
442
- 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;
443
543
  }
444
- conversations.changed(mergeChangeSets(entered));
445
- readSide.recover();
446
- queueReadiness();
447
544
  }
448
545
  function unprepared() {
449
- return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
546
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
450
547
  }
451
- function unbuilt(locator, error) {
452
- unindexable.add(locator);
453
- 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'}`);
454
551
  queueReadiness();
455
552
  }
456
553
  function prepare(locators = []) {
@@ -466,7 +563,7 @@ export function composeRuntime(options) {
466
563
  if (closed)
467
564
  return;
468
565
  unpreparedDemand.delete(waiting);
469
- await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
566
+ await prepareReplica(waiting).catch((error) => unpreparedBy(waiting, error));
470
567
  }
471
568
  }
472
569
  finally {
@@ -474,17 +571,22 @@ export function composeRuntime(options) {
474
571
  }
475
572
  })();
476
573
  }
574
+ function roomsOfReach(reach) {
575
+ return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
576
+ }
477
577
  function reachedRooms(plan) {
478
- const reach = plan.reach;
479
- const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
578
+ const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
480
579
  const named = plan.rooms ??
481
580
  (reached === null
482
581
  ? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
483
582
  : [...reached]);
484
- 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();
485
586
  }
486
587
  function awaited(locator) {
487
- return rooms.has(locator) || !roomsListed;
588
+ return (!roomsListed ||
589
+ (rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
488
590
  }
489
591
  function covered(plan) {
490
592
  const locators = reachedRooms(plan);
@@ -494,6 +596,7 @@ export function composeRuntime(options) {
494
596
  const pending = [];
495
597
  const coverage = [];
496
598
  const overlay = [];
599
+ const read = [];
497
600
  for (const locator of locators) {
498
601
  const replica = replicas.get(locator);
499
602
  if (replica === undefined) {
@@ -501,21 +604,22 @@ export function composeRuntime(options) {
501
604
  pending.push({ room: locator, awaits: 'acquisition' });
502
605
  continue;
503
606
  }
504
- if (!prepared.has(locator)) {
607
+ if (!replica.indexed) {
505
608
  pending.push({ room: locator, awaits: 'preparation' });
506
609
  continue;
507
610
  }
508
611
  coverage.push({
509
612
  room: locator,
510
- generation: replica.confirmed.record.transfer,
613
+ generation: replica.record.transfer,
511
614
  current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
512
615
  });
616
+ read.push({ locator, key: replica.record.key });
513
617
  for (const batch of replica.queue)
514
618
  overlay.push(batch.id);
515
619
  }
516
620
  if (pending.length > 0)
517
621
  return { pending };
518
- return { coverage: { rooms: coverage, indexes: [], overlay } };
622
+ return { coverage: { rooms: coverage, indexes: [], overlay }, rooms: read };
519
623
  }
520
624
  function awaitPreparation(cover) {
521
625
  if (!('pending' in cover))
@@ -524,66 +628,16 @@ export function composeRuntime(options) {
524
628
  if (waiting.length > 0)
525
629
  prepare(waiting.map(({ room }) => room));
526
630
  }
527
- function acquires(room) {
528
- return (demanded(demand, room) ||
529
- (replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
530
- queuesWithoutReplica.has(room.locator));
531
- }
532
- function acquiredLocators() {
533
- return [...discovered.values()]
534
- .filter(acquires)
535
- .map((room) => room.locator)
536
- .sort();
537
- }
538
- function heldDemand(acquired) {
539
- if (demand.whole)
540
- return null;
541
- return { rooms: acquired.map((room) => room.locator), conversations: demand.conversations };
542
- }
543
- function applyDemand(next) {
544
- const before = acquiredLocators();
545
- const conversed = demand.conversations;
546
- demand = next;
547
- const after = acquiredLocators();
548
- queueReadiness();
549
- if (!conversed && next.conversations)
550
- scheduleCurrency();
551
- if (JSON.stringify(before) === JSON.stringify(after) && conversed === next.conversations)
552
- return;
553
- journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
554
- if (options.adapter.roomStream === undefined)
555
- void sync('unstreamed');
556
- else if (socketOpen)
557
- options.adapter.roomStream.reopen();
558
- }
559
- function acquire(next) {
560
- if (closed)
561
- return;
562
- if (narrowing !== null)
563
- clearTimeout(narrowing);
564
- narrowing = null;
565
- const widening = wider(next, demand);
566
- if (!widening && !narrower(next, demand))
567
- return;
568
- if (widening || !roomsListed) {
569
- applyDemand(next);
570
- return;
571
- }
572
- // A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
573
- narrowing = setTimeout(() => {
574
- narrowing = null;
575
- applyDemand(next);
576
- }, 5_000);
577
- }
578
- function requiredRooms(reach) {
631
+ function requiredRooms(given) {
632
+ const reach = planReach(given);
579
633
  if (reach === null)
580
634
  return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
581
- const named = [...new Set(reach.rooms)].sort();
635
+ const named = [...roomsOfReach(reach)].sort();
582
636
  if (roomsListed)
583
637
  return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
584
638
  return { listed: named.every((room) => replicas.has(room)), rooms: named };
585
639
  }
586
- function roomState(locator, search) {
640
+ function roomState(locator) {
587
641
  if (sourceRefusals.has(locator))
588
642
  return 'refused';
589
643
  const listedRoom = discovered.get(locator);
@@ -594,27 +648,17 @@ export function composeRuntime(options) {
594
648
  return 'unadmitted';
595
649
  if (!replicas.has(locator))
596
650
  return 'acquiring';
597
- if (!prepared.has(locator))
651
+ if (!indexed(locator))
598
652
  return 'preparing';
599
- if (search && !searchIndex.rooms().has(locator))
600
- return 'indexing';
601
653
  return 'complete';
602
654
  }
603
- function conversationCoverage(reach, search) {
655
+ function conversationCoverage(reach) {
604
656
  const declared = reach === null || reach.conversations;
605
657
  if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
606
658
  return 'undeclared';
607
659
  if (!conversations.known() || !conversations.current())
608
660
  return 'acquiring';
609
- if (!search)
610
- return 'complete';
611
- const unindexed = conversations
612
- .searchable()
613
- .filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
614
- if (unindexed.length === 0)
615
- return 'complete';
616
- index([], unindexed);
617
- return 'indexing';
661
+ return 'complete';
618
662
  }
619
663
  function connectionOf() {
620
664
  if (socketOpen)
@@ -641,25 +685,66 @@ export function composeRuntime(options) {
641
685
  ];
642
686
  });
643
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
+ }
644
733
  function readinessFor(port, reach) {
645
734
  const required = requiredRooms(reach);
646
735
  const search = port.search === 'required';
647
- const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
648
- const building = (state) => coverage
649
- .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))
650
739
  .map(({ room }) => room);
651
- const preparing = building('preparing');
652
740
  if (preparing.length > 0)
653
741
  prepare(preparing);
654
- const indexing = building('indexing');
655
- if (indexing.length > 0)
656
- index(indexing, []);
657
742
  return readinessOf({
658
743
  code: port.code,
659
744
  coverage: {
660
745
  listed: required.listed,
661
746
  rooms: coverage,
662
- conversations: conversationCoverage(reach, search),
747
+ conversations: conversationCoverage(reach),
663
748
  search: search ? 'required' : 'unused',
664
749
  },
665
750
  persistence,
@@ -669,6 +754,7 @@ export function composeRuntime(options) {
669
754
  coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
670
755
  ? capacity
671
756
  : port.capacity,
757
+ installation: installationOf(coverage),
672
758
  refused: refusedIn(required.rooms),
673
759
  lost: storageLoss !== null,
674
760
  established: targets[port.target]?.established === true,
@@ -677,7 +763,7 @@ export function composeRuntime(options) {
677
763
  function establish(name) {
678
764
  targets = { ...targets, [name]: { established: true } };
679
765
  queueReadiness();
680
- void durably((local) => local.keepTargets(targets)).catch(() => {
766
+ void durably((local) => local.fact('targets', targets)).catch(() => {
681
767
  journal.record(`target record failed ${name}`);
682
768
  });
683
769
  }
@@ -714,18 +800,12 @@ export function composeRuntime(options) {
714
800
  readinessWatches.delete(watch);
715
801
  };
716
802
  }
717
- function layerBytes(layer) {
803
+ function pageBytes(page) {
718
804
  let bytes = 0;
719
- for (const [path, text] of Object.entries(layer.documents))
720
- bytes += (path.length + text.length) * 2;
805
+ for (const { path, content } of page.entries)
806
+ bytes += (path.length + (content?.length ?? 0)) * 2;
721
807
  return bytes;
722
808
  }
723
- function grownBy(next, previous) {
724
- if (previous === undefined)
725
- return layerBytes(next);
726
- const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
727
- return layerBytes({ ...next, documents });
728
- }
729
809
  function exceeded(error) {
730
810
  return (typeof error === 'object' &&
731
811
  error !== null &&
@@ -733,7 +813,7 @@ export function composeRuntime(options) {
733
813
  }
734
814
  async function estimated() {
735
815
  const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
736
- return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
816
+ return estimate === null ? null : Math.max(0, estimate.total - estimate.usage);
737
817
  }
738
818
  function pause(locator, cause) {
739
819
  paused.add(locator);
@@ -754,7 +834,8 @@ export function composeRuntime(options) {
754
834
  const available = await estimated().catch(() => null);
755
835
  if (available === null || available < (held.needed ?? 0))
756
836
  return;
757
- if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
837
+ if (held.cause === 'storage-exceeded' &&
838
+ (held.available === null || available <= held.available))
758
839
  return;
759
840
  resume();
760
841
  }
@@ -772,10 +853,7 @@ export function composeRuntime(options) {
772
853
  void sync('unstreamed');
773
854
  }
774
855
  async function reclaimed(local) {
775
- const unreferenced = [...transfers.keys()].filter((locator) => {
776
- const room = discovered.get(locator);
777
- return room === undefined || !acquires(room);
778
- });
856
+ const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
779
857
  for (const locator of unreferenced) {
780
858
  await local.abandon(locator);
781
859
  transfers.delete(locator);
@@ -799,68 +877,129 @@ export function composeRuntime(options) {
799
877
  }
800
878
  async function exceededOn(locator, bytes) {
801
879
  const available = await estimated().catch(() => null);
802
- pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
880
+ pause(locator, { cause: 'storage-exceeded', needed: bytes, available });
803
881
  }
804
- async function rowsQuery(plan) {
805
- const before = covered(plan);
806
- awaitPreparation(before);
807
- if ('pending' in before)
808
- return before;
809
- const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
810
- awaitPreparation(cover);
811
- if ('pending' in cover)
882
+ function seedRoomHeld(room, version) {
883
+ return indexed(room) && reachedBy(room, version);
884
+ }
885
+ function seedHeld(at) {
886
+ return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
887
+ }
888
+ function seedApplies(plan) {
889
+ const seed = seeds.get(plan.key);
890
+ if (seed === undefined)
891
+ return null;
892
+ if (!seedHeld(seed.at))
893
+ return seed;
894
+ seeds.delete(plan.key);
895
+ return null;
896
+ }
897
+ function seedBehind(plan, cover) {
898
+ const seed = seedApplies(plan);
899
+ if (seed === null || 'pending' in cover)
812
900
  return cover;
813
- return 'groups' in rows
814
- ? { groups: rows.groups, coverage: cover.coverage }
815
- : { documents: rows.documents, coverage: cover.coverage };
901
+ return {
902
+ pending: Object.entries(seed.at)
903
+ .filter(([room, version]) => !seedRoomHeld(room, version))
904
+ .map(([room]) => ({ room, awaits: 'acquisition' })),
905
+ };
906
+ }
907
+ function seed(entry) {
908
+ if (seedHeld(entry.at))
909
+ return;
910
+ for (const { query, rows } of entry.queries) {
911
+ const key = queryKey(query);
912
+ seeds.set(key, { rows, at: entry.at });
913
+ for (const shown of seedWatchers.get(key) ?? [])
914
+ shown();
915
+ }
816
916
  }
817
- async function indexConversation(conversation) {
818
- searchIndex.indexMessages(conversation, [], false);
819
- let after = null;
917
+ function sameCover(left, right) {
918
+ return JSON.stringify(left) === JSON.stringify(right);
919
+ }
920
+ async function coveredRows(plan, cover) {
820
921
  for (;;) {
821
- const batch = await durably((local) => local.messagesAfter(conversation, after));
822
- if (closed)
823
- return;
824
- searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
825
- if (batch.length === 0)
826
- return;
827
- after = batch[batch.length - 1].identity;
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
+ }
828
962
  }
963
+ return null;
829
964
  }
830
- function indexNext() {
831
- const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
832
- if (room !== undefined) {
833
- roomsToIndex.delete(room);
834
- return () => prepared.has(room)
835
- ? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
836
- : undefined;
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
+ };
837
971
  }
838
- const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
839
- if (conversation === undefined)
840
- return null;
841
- conversationsToIndex.delete(conversation);
842
- return () => indexConversation(conversation);
843
- }
844
- function index(rooms, named) {
845
- for (const room of rooms)
846
- roomsToIndex.add(room);
847
- for (const conversation of named)
848
- conversationsToIndex.add(conversation);
849
- if (indexing)
850
- return;
851
- indexing = true;
852
- void (async () => {
853
- try {
854
- for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
855
- await yielded();
856
- await unit();
857
- queueReadiness();
858
- }
859
- }
860
- finally {
861
- indexing = false;
862
- }
863
- })().catch(() => { });
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
+ }
986
+ async function rowsQuery(plan) {
987
+ const before = covered(plan);
988
+ awaitPreparation(before);
989
+ const seeded = seedApplies(plan);
990
+ if (seeded !== null)
991
+ return seededResult(seeded).result;
992
+ if ('pending' in before)
993
+ return before;
994
+ const answered = await coveredRows(plan, () => {
995
+ const cover = covered(plan);
996
+ awaitPreparation(cover);
997
+ return cover;
998
+ });
999
+ if ('pending' in answered)
1000
+ return answered.pending;
1001
+ const held = answered.cover;
1002
+ return { ...answered.rows, coverage: held.coverage };
864
1003
  }
865
1004
  function publishChangeSet(set) {
866
1005
  applyChangeSet(set);
@@ -870,7 +1009,10 @@ export function composeRuntime(options) {
870
1009
  for (const locator of set.queues)
871
1010
  emitQueue(locator);
872
1011
  if (set.drafts.length > 0 ||
873
- 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
+ })) {
874
1016
  publishRoomDrafts(false);
875
1017
  }
876
1018
  for (const app of set.presentations)
@@ -883,9 +1025,22 @@ export function composeRuntime(options) {
883
1025
  if (reason === 'revoked-authorization') {
884
1026
  await exclusive(async () => {
885
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;
886
1034
  try {
887
1035
  const local = await openLocal();
888
- 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
+ });
889
1044
  }
890
1045
  catch {
891
1046
  journal.record('model revocation persistence failed');
@@ -901,16 +1056,16 @@ export function composeRuntime(options) {
901
1056
  liveModels.delete(key);
902
1057
  }
903
1058
  const changed = [];
904
- for (const [locator, room] of rooms) {
905
- const before = replicas.get(locator) ?? null;
906
- if (before !== null) {
907
- const record = {
908
- ...before.confirmed.record,
909
- model: revoked,
910
- selectionModel: revoked,
911
- };
912
- replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
913
- }
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;
914
1069
  rooms.set(locator, {
915
1070
  ...room,
916
1071
  model: modelTypes(revoked),
@@ -918,9 +1073,10 @@ export function composeRuntime(options) {
918
1073
  ? null
919
1074
  : { ...room.typedSelection, model: modelTypes(revoked) },
920
1075
  });
921
- changed.push(changeSet(locator, before, replicas.get(locator) ?? null, { room: true }));
1076
+ changed.push(changeSet(locator, { documents: null, room: true }));
922
1077
  }
923
1078
  publishChangeSet(mergeChangeSets(changed));
1079
+ prepare();
924
1080
  });
925
1081
  }
926
1082
  }
@@ -951,41 +1107,57 @@ export function composeRuntime(options) {
951
1107
  model: modelTypes(modelOf(room.locator, 'typed-selection')),
952
1108
  },
953
1109
  providerOwnedPaths: room.providerOwnedPaths,
954
- roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
1110
+ roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
955
1111
  synchronizationAvailable,
956
1112
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
957
1113
  });
958
1114
  }
959
- async function applyDiscovery(found, started) {
1115
+ async function applyDiscovery(found, started, covered) {
1116
+ const lockedBefore = lockedRooms();
960
1117
  for (const room of found) {
961
1118
  const fence = fences.get(room.locator);
962
1119
  if (fence !== undefined && fence.erased && fence.at < started)
963
1120
  fences.delete(room.locator);
964
1121
  }
1122
+ await loseRooms(found
1123
+ .filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
1124
+ .map((room) => room.locator));
965
1125
  const listed = found.filter((room) => !fences.has(room.locator));
966
1126
  const listedLocators = new Set(listed.map((room) => room.locator));
967
1127
  for (const locator of listedLocators)
968
1128
  scopedRefusals.delete(locator);
969
- discovered.clear();
1129
+ if (covered === undefined)
1130
+ discovered.clear();
1131
+ else
1132
+ for (const locator of covered)
1133
+ discovered.delete(locator);
970
1134
  for (const room of listed)
971
1135
  discovered.set(room.locator, room);
972
- 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));
973
1137
  if (options.adapter.exhaustiveDiscovery === true)
974
1138
  await loseRooms(unlisted);
975
1139
  else
976
1140
  unlistedKept(unlisted);
977
1141
  for (const room of listed) {
978
- const held = replicas.get(room.locator)?.confirmed.record;
1142
+ const held = replicas.get(room.locator)?.record;
979
1143
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
980
1144
  list(held, false);
981
1145
  continue;
982
1146
  }
983
1147
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
984
1148
  }
1149
+ confirmLocks(listed);
985
1150
  roomsListed = true;
986
1151
  emitRooms();
1152
+ relocked(lockedBefore);
987
1153
  return listed;
988
1154
  }
1155
+ function heldLocally(locator) {
1156
+ return (replicas.has(locator) ||
1157
+ roomDrafts.has(locator) ||
1158
+ transfers.has(locator) ||
1159
+ queuesWithoutReplica.has(locator));
1160
+ }
989
1161
  function unlistedHeld(listed) {
990
1162
  return [
991
1163
  ...new Set([
@@ -998,7 +1170,7 @@ export function composeRuntime(options) {
998
1170
  }
999
1171
  function unlistedKept(unlisted) {
1000
1172
  for (const locator of unlisted) {
1001
- const held = replicas.get(locator)?.confirmed.record;
1173
+ const held = replicas.get(locator)?.record;
1002
1174
  if (held !== undefined)
1003
1175
  list(held, false);
1004
1176
  else
@@ -1007,16 +1179,16 @@ export function composeRuntime(options) {
1007
1179
  }
1008
1180
  function fence(locators) {
1009
1181
  const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1010
- const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
1011
- room: true,
1012
- queue: true,
1182
+ const removed = fresh.map((locator) => ({
1183
+ locator,
1013
1184
  drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
1014
1185
  }));
1015
1186
  for (const locator of fresh) {
1016
1187
  fences.set(locator, { at: discoveries, erased: false });
1017
1188
  lastFenced.set(locator, ++fencings);
1189
+ confirmed.delete(locator);
1018
1190
  for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1019
- pendings.delete(batch.id);
1191
+ pendings.delete(pendingKey(locator, batch.id));
1020
1192
  replicas.delete(locator);
1021
1193
  queuesWithoutReplica.delete(locator);
1022
1194
  rooms.delete(locator);
@@ -1028,7 +1200,7 @@ export function composeRuntime(options) {
1028
1200
  unreadable.delete(locator);
1029
1201
  stale.delete(locator);
1030
1202
  repairable.delete(locator);
1031
- unindexable.delete(locator);
1203
+ unpreparable.delete(locator);
1032
1204
  paused.delete(locator);
1033
1205
  scopedRefusals.delete(locator);
1034
1206
  sourceRefusals.delete(locator);
@@ -1043,26 +1215,48 @@ export function composeRuntime(options) {
1043
1215
  }
1044
1216
  if (fresh.length === 0)
1045
1217
  return fresh;
1046
- publishChangeSet(mergeChangeSets(removed));
1047
- for (const locator of fresh)
1048
- prepared.delete(locator);
1049
- searchIndex.forget(fresh);
1050
- readSide.recover();
1218
+ publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, { documents: null, room: true, queue: true, drafts }))));
1219
+ liveQueries.recover();
1051
1220
  moveTo({ ...status, ...attention() });
1052
1221
  return fresh;
1053
1222
  }
1054
- async function erased(local, locators) {
1055
- await local.erase(locators);
1223
+ async function eraseFenced(locators) {
1056
1224
  for (const locator of locators) {
1225
+ while (await durably((local) => local.erase(locator, ERASE_BATCH)))
1226
+ await yielded();
1057
1227
  const held = fences.get(locator);
1058
1228
  if (held !== undefined)
1059
1229
  held.erased = true;
1060
1230
  }
1061
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
+ }
1062
1251
  async function loseRooms(locators) {
1063
1252
  const fresh = fence(locators);
1064
- if (fresh.length > 0)
1065
- 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);
1066
1260
  }
1067
1261
  function restored(key, persisted) {
1068
1262
  const prior = models.get(key);
@@ -1083,54 +1277,56 @@ export function composeRuntime(options) {
1083
1277
  return model;
1084
1278
  }
1085
1279
  async function hydrate(opened) {
1086
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
1087
- opened.all(),
1280
+ const [held, queues, kept, heldConversations, chatDrafts] = await Promise.all([
1281
+ opened.opened(),
1088
1282
  opened.queues(),
1089
1283
  opened.roomDrafts(),
1090
- opened.presentation(),
1091
1284
  opened.conversations(),
1092
1285
  opened.chatDrafts(),
1093
- opened.lastCompleted(),
1094
- opened.providerLimits(),
1095
- opened.capabilities(),
1096
- opened.member(),
1097
- opened.transfers(),
1098
- opened.targets(),
1099
1286
  ]);
1100
- 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;
1101
1291
  transfers.clear();
1102
- for (const record of staged)
1103
- transfers.set(record.locator, record);
1104
- limits = declared;
1105
- capabilities = named;
1106
- 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;
1107
1297
  roomDrafts.clear();
1108
1298
  for (const record of kept)
1109
1299
  rememberRoomDraft(record);
1110
1300
  presentations.clear();
1111
- for (const record of presented)
1301
+ for (const record of held.presentation)
1112
1302
  presentations.set(record.app, record.value);
1113
- conversations.hydrate(held, chatDrafts);
1114
- const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1303
+ conversations.hydrate(heldConversations, chatDrafts);
1115
1304
  pendings.clear();
1116
1305
  for (const queue of queues) {
1117
- for (const { batch, state } of queue.pending)
1118
- pendings.set(batch.id, state);
1306
+ for (const { batch, state } of queue.pending) {
1307
+ pendings.set(pendingKey(queue.locator, batch.id), state);
1308
+ }
1119
1309
  }
1120
- const previousReplicas = new Map(replicas);
1121
1310
  replicas.clear();
1122
1311
  queuesWithoutReplica.clear();
1123
- const replicated = new Set(records.map((record) => record.locator));
1124
1312
  for (const [locator, queue] of queuedBatches) {
1125
- if (!replicated.has(locator))
1313
+ if (!held.rooms.has(locator))
1126
1314
  queuesWithoutReplica.set(locator, queue);
1127
1315
  }
1128
- for (const record of records) {
1129
- const model = restored(modelKey(record.locator, 'collection'), record.model);
1130
- const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
1131
- const prior = previousReplicas.get(record.locator);
1132
- const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
1133
- replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
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
+ });
1134
1330
  const existing = rooms.get(record.locator);
1135
1331
  if (existing === undefined)
1136
1332
  list(record, false);
@@ -1149,11 +1345,15 @@ export function composeRuntime(options) {
1149
1345
  },
1150
1346
  });
1151
1347
  }
1152
- }
1348
+ });
1349
+ for (const key of held.orphans)
1350
+ await opened.release(key);
1351
+ if (held.erasing || held.orphans.length > 0)
1352
+ queueMicrotask(sweep);
1153
1353
  return {
1154
- lastCompleted,
1354
+ lastCompleted: held.lastCompleted,
1155
1355
  locators: new Set([...queuedBatches.keys(), ...replicas.keys()]),
1156
- read: mergeChangeSets([...new Set([...previousReplicas.keys(), ...replicas.keys()])].map((locator) => changeSet(locator, previousReplicas.get(locator) ?? null, replicas.get(locator) ?? null))),
1356
+ read: mergeChangeSets([...replicas.keys()].map((locator) => changeSet(locator, { documents: null }))),
1157
1357
  };
1158
1358
  }
1159
1359
  async function readState(opened) {
@@ -1172,8 +1372,7 @@ export function composeRuntime(options) {
1172
1372
  async function open() {
1173
1373
  if (closed)
1174
1374
  throw new Error('runtime closed');
1175
- const opened = await openRuntimeStore(isFenced);
1176
- store = opened;
1375
+ const opened = openRuntimeStore(isFenced, options.database);
1177
1376
  const interrupted = await opened.fences();
1178
1377
  for (const locator of interrupted) {
1179
1378
  fences.set(locator, { at: discoveries, erased: false });
@@ -1192,9 +1391,17 @@ export function composeRuntime(options) {
1192
1391
  emitStorageLoss();
1193
1392
  }
1194
1393
  const lastCompleted = await readState(opened);
1195
- if (interrupted.length > 0)
1196
- await erased(opened, interrupted);
1394
+ if (interrupted.length > 0) {
1395
+ queueMicrotask(() => {
1396
+ void eraseFenced(interrupted).catch(() => {
1397
+ journal.record('erasure failed');
1398
+ });
1399
+ });
1400
+ }
1197
1401
  moveTo({ ...status, lastCompleted, ...attention() });
1402
+ void exclusive(() => unpair(opened)).catch(() => {
1403
+ journal.record('unpair failed');
1404
+ });
1198
1405
  return opened;
1199
1406
  }
1200
1407
  function durably(work) {
@@ -1215,78 +1422,260 @@ export function composeRuntime(options) {
1215
1422
  mutations = next.then(() => undefined, () => undefined);
1216
1423
  return next;
1217
1424
  }
1218
- function withdrawable(batch) {
1219
- const state = pendings.get(batch.id);
1220
- 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 };
1221
1511
  }
1222
- async function settle(local, locator, before, confirmed, queue, settling = {}) {
1512
+ function replayed(record, confirmedLayer, queue) {
1513
+ return stepped(replaying(confirmedLayer, queue, record.locator, compositionOf(record)));
1514
+ }
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';
1223
1566
  const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
1224
- const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
1225
- const kept = queue.filter((batch) => !withdrawn.includes(batch));
1226
- const unqueued = withdrawn.map((batch) => ({
1227
- batch,
1228
- returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
1229
- }));
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
+ }
1230
1584
  const returning = [
1231
- ...[...unqueued].reverse(),
1232
- ...(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 ?? []),
1233
1590
  ];
1234
1591
  const written = new Map();
1235
- for (const { batch, returned } of returning) {
1236
- if (returned === null)
1592
+ for (const { batch, returned: reason } of returning) {
1593
+ if (reason === null || batch.between?.destination.room === locator)
1237
1594
  continue;
1238
- for (const saved of pendings.get(batch.id)?.drafts ?? []) {
1595
+ for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
1239
1596
  const existing = written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path);
1240
1597
  written.set(saved.path, {
1241
1598
  locator,
1242
1599
  path: saved.path,
1243
1600
  revision: roomDraftRevision(),
1244
1601
  parts: returnedInto(existing?.parts, saved.parts),
1245
- returned,
1602
+ returned: reason,
1246
1603
  });
1247
1604
  }
1248
1605
  }
1249
- await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
1250
- for (const batch of previousQueue) {
1251
- if (!kept.includes(batch))
1252
- pendings.delete(batch.id);
1253
- }
1254
- for (const record of written.values())
1255
- rememberRoomDraft(record);
1256
- try {
1257
- await local.write({
1258
- next: confirmed.record,
1259
- previous: before?.confirmed.record ?? null,
1260
- ...(settling.stored === undefined ? {} : { stored: settling.stored }),
1261
- });
1262
- }
1263
- catch (error) {
1264
- const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
1265
- if (held !== null)
1266
- replicas.set(locator, held);
1267
- else if (kept.length > 0)
1268
- queuesWithoutReplica.set(locator, kept);
1269
- else
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];
1270
1651
  queuesWithoutReplica.delete(locator);
1271
- publishChangeSet(changeSet(locator, before ?? null, held, {
1272
- queue: kept.length !== previousQueue.length,
1273
- drafts: [...written.keys()],
1274
- }));
1275
- moveTo({ ...status, ...attention() });
1276
- throw error;
1277
- }
1278
- return () => {
1279
- queuesWithoutReplica.delete(locator);
1280
- for (const { batch, returned } of unqueued) {
1281
- const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
1282
- journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1283
- }
1284
- replicas.set(locator, replicaOf(confirmed, kept, before));
1285
- return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
1286
- room: settling.room,
1287
- queue: kept.length !== previousQueue.length,
1288
- drafts: [...written.keys()],
1289
- });
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
+ },
1290
1679
  };
1291
1680
  }
1292
1681
  function adoptModels(locator, record) {
@@ -1307,7 +1696,7 @@ export function composeRuntime(options) {
1307
1696
  const taken = (answer, previous, attached) => {
1308
1697
  if (answer === undefined)
1309
1698
  return previous;
1310
- const { state, refused } = attachedModel(answer, previous);
1699
+ const { state, refused } = attachedModel(answer, previous, attached === 'collection' ? roomTypes : NO_ROOM_TYPES);
1311
1700
  if (refused !== null) {
1312
1701
  const where = refused.location === undefined ? '' : ` ${refused.location}`;
1313
1702
  journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
@@ -1320,70 +1709,86 @@ export function composeRuntime(options) {
1320
1709
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
1321
1710
  };
1322
1711
  }
1323
- function publish(local, room, confirmed, landing, live) {
1324
- const { base, stored } = landing;
1325
- return exclusive(async () => {
1326
- if (stored !== undefined) {
1327
- await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
1328
- }
1712
+ function publish(local, room, record, landing, live) {
1713
+ const landed = exclusive(async () => {
1329
1714
  if (!live()) {
1330
1715
  caughtUpTo(room.locator);
1331
1716
  return true;
1332
1717
  }
1333
1718
  const prior = replicas.get(room.locator);
1334
- if (base !== undefined &&
1335
- (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
1336
- prior.confirmed.record.revisions !== base.record.revisions)) {
1719
+ if (landing.kind === 'refreshed' &&
1720
+ (prior === undefined || prior.record.key !== record.key || prior.confirmed !== landing.base)) {
1721
+ await local.unrefresh(record.key);
1337
1722
  return false;
1338
1723
  }
1339
- adoptModels(room.locator, confirmed.record);
1724
+ adoptModels(room.locator, record);
1340
1725
  const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
1341
- const roomVersion = confirmed.record.roomVersion;
1726
+ const roomVersion = record.roomVersion;
1342
1727
  const accepted = queue.filter((batch) => {
1343
- const accepted = pendings.get(batch.id)?.accepted;
1344
- 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);
1345
1730
  });
1346
- let settled;
1731
+ let set;
1347
1732
  try {
1348
- 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
+ });
1349
1748
  }
1350
1749
  catch (error) {
1351
1750
  if (!exceeded(error))
1352
1751
  throw error;
1353
- await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
1752
+ await exceededOn(room.locator, null);
1354
1753
  caughtUpTo(room.locator);
1355
1754
  return true;
1356
1755
  }
1357
- const set = settled();
1358
- if (stored !== undefined) {
1756
+ if (landing.kind === 'bootstrapped') {
1359
1757
  transfers.delete(room.locator);
1360
1758
  catching.delete(room.locator);
1759
+ sweep();
1361
1760
  }
1362
1761
  for (const batch of accepted)
1363
- pendings.delete(batch.id);
1762
+ pendings.delete(pendingKey(room.locator, batch.id));
1364
1763
  stale.delete(room.locator);
1365
- unindexable.delete(room.locator);
1764
+ unpreparable.delete(room.locator);
1366
1765
  if (unreadable.delete(room.locator))
1367
1766
  scopedRefusals.delete(room.locator);
1368
1767
  sourceRefusals.delete(room.locator);
1369
1768
  declarationRefusals.delete(room.locator);
1370
1769
  list(room, true);
1371
- if (prior === undefined)
1372
- prepared.add(room.locator);
1373
1770
  publishChangeSet(set);
1771
+ await unpair(local);
1374
1772
  moveTo({ ...status, ...attention() });
1375
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
+ }
1376
1777
  return true;
1377
1778
  });
1779
+ return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
1378
1780
  }
1379
- async function saveRoomDrafts(locator, named, writtenBy) {
1781
+ async function savedRoom(locator) {
1380
1782
  const local = await openLocal();
1381
- await referenced(locator);
1382
1783
  const replica = replicas.get(locator);
1383
1784
  const room = rooms.get(locator);
1384
1785
  if (replica === undefined || room === undefined) {
1385
1786
  throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1386
1787
  }
1788
+ return { local, replica, room };
1789
+ }
1790
+ async function saveRoomDrafts(locator, named, writtenBy) {
1791
+ const target = await savedRoom(locator);
1387
1792
  const saved = [];
1388
1793
  for (const { path, revision } of named) {
1389
1794
  const record = roomDrafts.get(locator)?.get(path);
@@ -1393,12 +1798,396 @@ export function composeRuntime(options) {
1393
1798
  if (!saved.some((draft) => draft.path === path))
1394
1799
  saved.push({ path, parts: record.parts });
1395
1800
  }
1396
- 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)));
2065
+ if (changed.length > 0)
2066
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
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)));
1397
2188
  if (changed.length > 0)
1398
2189
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
1399
- const parts = saved.map(({ path, parts }) => changesOf(path, parts));
1400
- const changes = parts.flatMap(({ changes }) => changes);
1401
- const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
2190
+ const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
1402
2191
  if (requeue.kind === 'refused')
1403
2192
  return { outcome: 'refused', ...requeue.refusal };
1404
2193
  const foreign = [...requeue.withdrawn.keys()].some((origin) => {
@@ -1416,23 +2205,23 @@ export function composeRuntime(options) {
1416
2205
  const withdrawing = requeue.withdrawn.size > 0;
1417
2206
  const input = {
1418
2207
  local: localSaves,
1419
- baseRoomVersion: replica.confirmed.record.roomVersion,
1420
- composition: compositionFor(replica.confirmed.record),
2208
+ baseRoomVersion: replica.record.roomVersion,
2209
+ composition: compositionFor(replica.record),
1421
2210
  operations: room.operations,
1422
2211
  providerOwnedPaths: room.providerOwnedPaths,
1423
2212
  providerLimits: limits,
1424
2213
  handle: room.handle,
1425
- visible: replica.visible,
2214
+ visible: saving.visible,
1426
2215
  changes,
1427
2216
  bases: parts.flatMap(({ bases }) => bases),
2217
+ queued: queuedDocuments(replica.queue),
1428
2218
  writtenBy,
1429
- clean: {
1430
- text: replica.confirmed.record.documents,
1431
- binaries: replica.confirmed.record.binaries,
1432
- composition: compositionOf(replica.confirmed.record),
1433
- },
2219
+ unmodeled: await unmodeledIn(replica.record),
2220
+ validate: validatorOf(replica.record, saving.confirmed),
1434
2221
  };
1435
- 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;
1436
2225
  if (checked?.kind === 'refused')
1437
2226
  return { outcome: 'refused', ...checked.refusal };
1438
2227
  const owners = [...requeue.owners].flatMap((origin) => {
@@ -1448,13 +2237,14 @@ export function composeRuntime(options) {
1448
2237
  };
1449
2238
  }
1450
2239
  const built = !withdrawing
1451
- ? buildBatch(input)
2240
+ ? await buildBatch(input)
1452
2241
  : requeue.changes.length === 0
1453
2242
  ? null
1454
- : buildBatch({
2243
+ : await buildBatch({
1455
2244
  ...input,
1456
- visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
2245
+ visible: await stepped(saving.visibleOver(requeue.queue)),
1457
2246
  changes: requeue.changes,
2247
+ queued: queuedDocuments(requeue.queue),
1458
2248
  });
1459
2249
  if (built?.kind === 'refused')
1460
2250
  return { outcome: 'refused', ...built.refusal };
@@ -1471,7 +2261,7 @@ export function composeRuntime(options) {
1471
2261
  for (const [origin, replaced] of withdrawn) {
1472
2262
  if (replaced === null)
1473
2263
  continue;
1474
- const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
2264
+ const carried = withdrawnDrafts(pendings.get(pendingKey(locator, origin))?.drafts ?? [], changes);
1475
2265
  drafted.set(replaced.id, replaced === join?.batch
1476
2266
  ? [
1477
2267
  ...carried.map((held) => joinedDraft(held, saved)),
@@ -1485,59 +2275,77 @@ export function composeRuntime(options) {
1485
2275
  for (const replaced of withdrawn.values()) {
1486
2276
  if (replaced === null || !batches.includes(replaced))
1487
2277
  continue;
1488
- const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
2278
+ const limited = await stepped(limiting(batches, replaced, limits, saving.visibleOver));
1489
2279
  if (limited !== null)
1490
2280
  return { outcome: 'refused', ...limited };
1491
2281
  }
1492
- const consumed = saved.map(({ path }) => path);
1493
- try {
1494
- 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
+ {
1495
2287
  locator,
2288
+ saving,
2289
+ before: replica,
1496
2290
  queue: batches,
1497
- saved: drafted,
1498
- consumedRoomDrafts: consumed,
1499
- });
1500
- }
1501
- catch {
2291
+ write: {
2292
+ locator,
2293
+ queue: batches,
2294
+ saved: drafted,
2295
+ consumedRoomDrafts: left,
2296
+ roomDrafts: arrived,
2297
+ },
2298
+ },
2299
+ ]);
2300
+ if (done === null)
1502
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}`);
1503
2305
  }
1504
2306
  if (fences.has(locator))
1505
2307
  throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1506
2308
  for (const origin of withdrawn.keys())
1507
- pendings.delete(origin);
2309
+ pendings.delete(pendingKey(locator, origin));
1508
2310
  for (const [id, drafts] of drafted) {
1509
- 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
+ });
1510
2316
  }
1511
- if (!localSaves)
1512
- recorded = false;
1513
- for (const path of consumed)
2317
+ for (const path of left)
1514
2318
  forgetRoomDraft(locator, path);
1515
- replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
1516
- 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] ?? [],
1517
2324
  queue: true,
1518
- drafts: consumed,
2325
+ drafts: [...left, ...arrived.map(({ path }) => path)],
1519
2326
  }));
1520
- moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
1521
- submitLater(locator);
2327
+ queuedSave(locator);
1522
2328
  const identifier = join?.batch.id ??
1523
2329
  built?.batch.id ??
1524
2330
  [...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
1525
2331
  return { outcome: 'saved', identifier };
1526
2332
  }
1527
- async function mark(local, locator, identifier, submission, lost) {
1528
- const state = pendings.get(identifier);
1529
- if (!continuing() ||
1530
- state === undefined ||
1531
- (JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
1532
- return;
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 }));
1533
2348
  }
1534
- await local.submit(locator, identifier, submission, lost);
1535
- if (fences.has(locator))
1536
- return;
1537
- pendings.set(identifier, { ...state, submission, lost });
1538
- const replica = replicas.get(locator);
1539
- if (replica !== undefined)
1540
- publishChangeSet(changeSet(locator, replica, replica, { queue: true }));
1541
2349
  moveTo({ ...status, ...attention() });
1542
2350
  }
1543
2351
  async function keepUpload(local, locator, identifier, upload) {
@@ -1546,7 +2354,8 @@ export function composeRuntime(options) {
1546
2354
  await local.keepUpload(locator, identifier, upload);
1547
2355
  if (fences.has(locator))
1548
2356
  throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1549
- pendings.set(identifier, { ...pendings.get(identifier), upload });
2357
+ const key = pendingKey(locator, identifier);
2358
+ pendings.set(key, { ...pendings.get(key), upload });
1550
2359
  if (!continuing())
1551
2360
  throw new Error('connection refused');
1552
2361
  }
@@ -1579,7 +2388,7 @@ export function composeRuntime(options) {
1579
2388
  if (fencedSince(locator, since))
1580
2389
  return failure({ kind: 'room-not-readable', locator });
1581
2390
  if (result.kind === 'scoped-refusal') {
1582
- const room = replicas.get(result.locator)?.confirmed.record;
2391
+ const room = replicas.get(result.locator)?.record;
1583
2392
  if (room !== undefined)
1584
2393
  await scoped(result, room, await openLocal());
1585
2394
  }
@@ -1592,21 +2401,62 @@ export function composeRuntime(options) {
1592
2401
  return UNCERTAIN;
1593
2402
  }
1594
2403
  }
1595
- 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) {
1596
2440
  for (const batch of replicas.get(locator)?.queue ?? []) {
1597
2441
  const change = batch.changes[queuedFile(batch, path, localId)];
1598
2442
  if (change?.kind === 'binary')
1599
- return change.blob;
2443
+ return change;
1600
2444
  }
1601
- return null;
2445
+ const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2446
+ return drafted?.localId === localId ? drafted : null;
2447
+ }
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
+ };
1602
2453
  }
1603
2454
  function binaryRoom(request) {
1604
2455
  if (request.kind === 'path')
1605
2456
  return request.locator;
1606
- const scope = options.adapter.referrerScope(request.referrer);
1607
- return scope?.kind === 'room' ? scope.locator : null;
2457
+ return options.adapter.referrerScope(request.referrer)?.locator ?? null;
1608
2458
  }
1609
- async function readBinary(request) {
2459
+ async function transferBinary(request, transfer, answer) {
1610
2460
  if (closed || refusal !== null)
1611
2461
  return UNCERTAIN;
1612
2462
  try {
@@ -1622,69 +2472,116 @@ export function composeRuntime(options) {
1622
2472
  ? `transfer binary ${request.locator} ${request.path}`
1623
2473
  : `transfer binary ${request.fileRef}`);
1624
2474
  try {
1625
- const result = await options.adapter.binary(authorization, request);
1626
- journal.record(`outcome binary ${result.kind}`);
2475
+ const result = await transfer(authorization);
2476
+ const settled = result;
2477
+ journal.record(`outcome binary ${settled.kind}`);
1627
2478
  const locator = binaryRoom(request);
1628
2479
  if (locator !== null && fencedSince(locator, since))
1629
2480
  return failure({ kind: 'room-not-readable', locator });
1630
- if (result.kind === 'scoped-refusal') {
1631
- const room = replicas.get(result.locator)?.confirmed.record;
2481
+ if (settled.kind === 'scoped-refusal') {
2482
+ const room = replicas.get(settled.locator)?.record;
1632
2483
  if (room !== undefined)
1633
- await scoped(result, room, await openLocal());
2484
+ await scoped(settled, room, await openLocal());
1634
2485
  }
1635
- if (result.kind === 'revoked-authorization')
2486
+ if (settled.kind === 'revoked-authorization')
1636
2487
  await refuse('revoked-authorization');
1637
- return binaryAnswer(result);
2488
+ return answer(result);
1638
2489
  }
1639
2490
  catch {
1640
2491
  journal.record('outcome binary transport-uncertainty');
1641
2492
  return UNCERTAIN;
1642
2493
  }
1643
2494
  }
1644
- async function readBinaryPath(locator, path, revision) {
1645
- await ready;
2495
+ async function readBinary(request, name) {
2496
+ if (closed)
2497
+ return UNCERTAIN;
2498
+ const held = await keptFiles.find(name);
2499
+ if (held !== null)
2500
+ return held;
2501
+ return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2502
+ }
2503
+ async function binaryPath(locator, path, revision) {
1646
2504
  if (revision !== undefined)
1647
- return readBinary({ kind: 'path', locator, path, revision });
1648
- const visible = replicas.get(locator)?.visible;
1649
- const held = visible?.binaries[path];
1650
- if (visible === undefined || held === undefined)
1651
- return absentBinary(path);
1652
- if (held.state === 'pending') {
1653
- const blob = queuedBlob(locator, path, held.localId);
1654
- return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1655
- }
1656
- const record = binaryRecord(path, held, visible.revisions[path]);
1657
- if (record.state !== 'accepted')
1658
- return absentBinary(path);
2505
+ return { kind: 'path', locator, path, revision };
1659
2506
  const replica = replicas.get(locator);
1660
- const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
2507
+ if (replica === undefined)
2508
+ return null;
2509
+ const local = await openLocal();
2510
+ const [held] = await local.entries(replica.record.key, 'visible', [path]);
2511
+ if (held?.binary === undefined || held.binary === null)
2512
+ return null;
2513
+ if (held.binary.state === 'pending')
2514
+ return { kind: 'pending', localId: held.binary.localId };
2515
+ const record = binaryRecord(path, held.binary, held.revision ?? undefined);
2516
+ if (record.state !== 'accepted')
2517
+ return null;
2518
+ const queued = replica.queue.flatMap((batch) => batch.changes);
2519
+ const sources = queued.flatMap((change) => (change.kind === 'move' ? [change.path] : []));
2520
+ const confirmedLayer = await stepped(slicing(await local.entries(replica.record.key, 'confirmed', [path, ...sources]), [
2521
+ path,
2522
+ ...sources,
2523
+ ]));
1661
2524
  const holds = (at) => {
1662
- const kept = replica?.confirmed.record.binaries[at];
2525
+ const kept = confirmedLayer.binaries[at];
1663
2526
  return (kept?.state === 'accepted' &&
1664
2527
  kept.fileRef === record.fileRef &&
1665
- replica?.confirmed.record.revisions[at] === record.revision);
2528
+ confirmedLayer.revisions[at] === record.revision);
1666
2529
  };
1667
- let confirmed = path;
1668
- for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
2530
+ let confirmedAt = path;
2531
+ for (let index = queued.length - 1; index >= 0 && !holds(confirmedAt); index -= 1) {
1669
2532
  const change = queued[index];
1670
- if (change.kind === 'move' && change.to === confirmed)
1671
- confirmed = change.path;
2533
+ if (change.kind === 'move' && change.to === confirmedAt)
2534
+ confirmedAt = change.path;
1672
2535
  }
1673
- return readBinary({
2536
+ return {
1674
2537
  kind: 'path',
1675
2538
  locator,
1676
- path: confirmed,
2539
+ path: confirmedAt,
1677
2540
  revision: record.revision,
1678
2541
  fileRef: record.fileRef,
1679
- });
2542
+ };
2543
+ }
2544
+ async function readBinaryPath(locator, path, revision) {
2545
+ await ready;
2546
+ const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2547
+ const asked = drafted !== undefined && drafted.localId === revision
2548
+ ? { kind: 'pending', localId: revision }
2549
+ : await binaryPath(locator, path, revision);
2550
+ if (asked === null)
2551
+ return absentBinary(path);
2552
+ if (asked.kind === 'pending') {
2553
+ const facts = queuedFacts(locator, path, asked.localId);
2554
+ return facts === null
2555
+ ? absentBinary(path)
2556
+ : {
2557
+ kind: 'binary',
2558
+ file: asked.localId,
2559
+ byteLength: facts.byteLength,
2560
+ mediaType: facts.mediaType,
2561
+ };
2562
+ }
2563
+ return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
2564
+ }
2565
+ async function linkBinaryPath(locator, path, revision) {
2566
+ await ready;
2567
+ const asked = await binaryPath(locator, path, revision);
2568
+ if (asked === null || asked.kind === 'pending')
2569
+ return absentBinary(path);
2570
+ return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
1680
2571
  }
1681
2572
  async function readBinaryReference(reference) {
1682
2573
  await ready;
1683
- return readBinary({
1684
- kind: 'reference',
1685
- fileRef: reference.fileRef,
1686
- referrer: reference.referrer,
1687
- });
2574
+ const scope = options.adapter.referrerScope(reference.referrer);
2575
+ if (scope === null)
2576
+ return absentReference(reference.fileRef);
2577
+ return readBinary({ kind: 'reference', fileRef: reference.fileRef, referrer: reference.referrer }, { locator: scope.locator, path: scope.path ?? '', revision: reference.fileRef });
2578
+ }
2579
+ function readPart(file, part) {
2580
+ return options.database.ask({ kind: 'file.part', key: file, part });
2581
+ }
2582
+ async function cleanUp() {
2583
+ const freed = await options.database.ask({ kind: 'opened.clean' });
2584
+ return freed + ((await options.shell.cleanUp?.()) ?? 0);
1688
2585
  }
1689
2586
  function declaredAgents() {
1690
2587
  for (const capability of capabilities) {
@@ -1702,7 +2599,14 @@ export function composeRuntime(options) {
1702
2599
  }
1703
2600
  return undefined;
1704
2601
  }
1705
- async function invokeAction(name, input) {
2602
+ function calledRooms(name, input) {
2603
+ const member = declaredAction(name)?.room;
2604
+ if (member === undefined || input === null || typeof input !== 'object')
2605
+ return [];
2606
+ const named = input[member];
2607
+ return (Array.isArray(named) ? named : [named]).filter((room) => typeof room === 'string');
2608
+ }
2609
+ async function invokeAction(name, input, dispatching) {
1706
2610
  await ready;
1707
2611
  if (closed || refusal !== null)
1708
2612
  return UNCERTAIN;
@@ -1716,11 +2620,12 @@ export function composeRuntime(options) {
1716
2620
  }
1717
2621
  if (closed || authorization === undefined || refusal !== null)
1718
2622
  return UNCERTAIN;
2623
+ await dispatching?.();
1719
2624
  journal.record(`transfer action ${name}`);
1720
2625
  try {
1721
2626
  const result = await options.adapter.action(authorization, { name, input });
1722
2627
  if (result.kind === 'scoped-refusal') {
1723
- const room = replicas.get(result.locator)?.confirmed.record;
2628
+ const room = replicas.get(result.locator)?.record;
1724
2629
  if (room !== undefined)
1725
2630
  await scoped(result, room, await openLocal());
1726
2631
  }
@@ -1736,107 +2641,521 @@ export function composeRuntime(options) {
1736
2641
  return UNCERTAIN;
1737
2642
  }
1738
2643
  }
2644
+ function unsettled(result) {
2645
+ return result.kind !== 'action' && result.retryable;
2646
+ }
2647
+ async function recoveredAction(name, input) {
2648
+ const operation = operationOf(input);
2649
+ if (operation === null)
2650
+ return invokeAction(name, input);
2651
+ const kept = async () => (await openLocal()).keepCall({ operation, name, input });
2652
+ calling.add(operation);
2653
+ try {
2654
+ let result = await invokeAction(name, input, kept);
2655
+ if (unsettled(result)) {
2656
+ await recovery.attempt(`action ${operation}`, async () => {
2657
+ if (closed || refusal !== null)
2658
+ return 'done';
2659
+ if (!socketOpen)
2660
+ return 'again';
2661
+ result = await invokeAction(name, input, kept);
2662
+ return unsettled(result) ? 'again' : 'done';
2663
+ }, { again: true });
2664
+ }
2665
+ if (!unsettled(result))
2666
+ await (await openLocal()).dropCall(operation);
2667
+ return result;
2668
+ }
2669
+ finally {
2670
+ calling.delete(operation);
2671
+ }
2672
+ }
2673
+ async function resendCalls(signal) {
2674
+ if (!socketOpen || closed || stopping)
2675
+ return 'done';
2676
+ const local = await openLocal();
2677
+ for (const held of await local.calls()) {
2678
+ if (calling.has(held.operation))
2679
+ continue;
2680
+ if (closed || signal.aborted)
2681
+ return 'done';
2682
+ if (pastHorizon(held.operation)) {
2683
+ await local.dropCall(held.operation);
2684
+ continue;
2685
+ }
2686
+ calling.add(held.operation);
2687
+ try {
2688
+ if (unsettled(await invokeAction(held.name, held.input)))
2689
+ return 'again';
2690
+ await local.dropCall(held.operation);
2691
+ }
2692
+ finally {
2693
+ calling.delete(held.operation);
2694
+ }
2695
+ }
2696
+ return 'done';
2697
+ }
1739
2698
  function continuing() {
1740
2699
  return !closed && !discoveryPaused;
1741
2700
  }
1742
2701
  function reachedBy(locator, roomVersion) {
1743
2702
  const held = replicas.get(locator);
1744
- return (held !== undefined &&
1745
- options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion));
2703
+ return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
2704
+ }
2705
+ async function writtenAfter(locator, roomVersion, path) {
2706
+ const held = replicas.get(locator);
2707
+ if (held === undefined)
2708
+ return false;
2709
+ const [entry] = await (await openLocal()).entries(held.record.key, 'confirmed', [path]);
2710
+ const written = entry?.written ?? undefined;
2711
+ return (written !== undefined &&
2712
+ options.adapter.reached(locator, written, roomVersion) &&
2713
+ !options.adapter.reached(locator, roomVersion, written));
2714
+ }
2715
+ function overriddenTexts(texts, overrides) {
2716
+ return async (paths) => {
2717
+ const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
2718
+ for (const path of paths) {
2719
+ const entry = overrides.get(path);
2720
+ if (entry === undefined)
2721
+ continue;
2722
+ if (entry === null)
2723
+ known.delete(path);
2724
+ else
2725
+ known.set(path, entry.binary !== null ? null : entry.content);
2726
+ }
2727
+ return known;
2728
+ };
2729
+ }
2730
+ async function unreadableRoom(texts, walk, paths) {
2731
+ if (paths !== null) {
2732
+ const text = {};
2733
+ for (const [path, content] of await texts(paths))
2734
+ if (content !== null)
2735
+ text[path] = content;
2736
+ return stepped(unreadableIn(text, Object.keys(text)));
2737
+ }
2738
+ for (let after = null;;) {
2739
+ const page = await walk(after);
2740
+ const text = {};
2741
+ for (const { path, content } of page.entries)
2742
+ if (content !== null)
2743
+ text[path] = content;
2744
+ const refused = await stepped(unreadableIn(text, Object.keys(text)));
2745
+ if (refused !== null)
2746
+ return refused;
2747
+ if (page.next === null)
2748
+ return null;
2749
+ after = page.next;
2750
+ }
2751
+ }
2752
+ async function validatedRoom(local, record, view, previous, overrides = null) {
2753
+ const composition = compositionOf(record);
2754
+ const modeled = record.model.status !== 'unavailable' || record.selectionModel.status !== 'unavailable';
2755
+ const lookup = overrides === null
2756
+ ? lookupOf(local, record.key, view)
2757
+ : overriddenLookup(lookupOf(local, record.key, view), overrides);
2758
+ const texts = overrides === null
2759
+ ? textsOf(local, record.key, view)
2760
+ : overriddenTexts(textsOf(local, record.key, view), overrides);
2761
+ const walk = overrides === null
2762
+ ? walkOf(local, record.key, view)
2763
+ : overriddenWalk(walkOf(local, record.key, view), overrides);
2764
+ let changes = null;
2765
+ let reshaped = false;
2766
+ if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
2767
+ const [was, now] = await Promise.all([
2768
+ local.entries(record.key, 'confirmed', previous.changed),
2769
+ overrides === null
2770
+ ? local.entries(record.key, view, previous.changed)
2771
+ : Promise.resolve([]),
2772
+ ]);
2773
+ const before = new Map(was.map((entry) => [entry.path, entry]));
2774
+ const after = overrides === null
2775
+ ? new Map(now.map((entry) => [entry.path, entry]))
2776
+ : overrides;
2777
+ changes = previous.changed.flatMap((path) => {
2778
+ const one = before.get(path) ?? null;
2779
+ const other = after.get(path) ?? null;
2780
+ reshaped ||= textDiffers(one, other);
2781
+ const held = textOf(one);
2782
+ const next = textOf(other);
2783
+ return held === next ? [] : [{ path, was: held, now: next }];
2784
+ });
2785
+ }
2786
+ const named = changes === null ? null : changes.map(({ path }) => path);
2787
+ const textChanged = changes === null || changes.length > 0 || reshaped;
2788
+ const modelsChanged = previous === null || !sameComposition(compositionOf(previous.record), composition);
2789
+ if (modeled && (textChanged || modelsChanged)) {
2790
+ const clean = previous !== null &&
2791
+ changes !== null &&
2792
+ (await changedCleanly(changes, compositionOf(previous.record), composition, {
2793
+ lookup,
2794
+ texts,
2795
+ referrers: referrersOf(local, record.key, 'confirmed'),
2796
+ }));
2797
+ if (clean)
2798
+ return null;
2799
+ const verdict = await validatedCollection(walk, lookup, composition);
2800
+ if (verdict.verdict === 'accept')
2801
+ return null;
2802
+ if (verdict.rule !== 'model-unavailable')
2803
+ return { kind: 'refused', refusal: verdict };
2804
+ return unreadableRoom(texts, walk, named);
2805
+ }
2806
+ if (textChanged)
2807
+ return unreadableRoom(texts, walk, named);
2808
+ return null;
2809
+ }
2810
+ function refreshingOf(local, key, liveness, replaying = null) {
2811
+ const write = replaying === null ? exclusive : installWrite;
2812
+ return {
2813
+ async turn() {
2814
+ if (replaying === null)
2815
+ return;
2816
+ installing(replaying, 'replaying');
2817
+ await screened();
2818
+ },
2819
+ prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
2820
+ async stage(page) {
2821
+ for (let written = 0;;) {
2822
+ if (replaying !== null)
2823
+ await screened();
2824
+ const end = batchEnd(page.entries, written);
2825
+ const last = end === page.entries.length;
2826
+ const entries = page.entries.slice(written, end);
2827
+ const staged = await write(async () => {
2828
+ if (!liveness())
2829
+ return false;
2830
+ await timed(() => local.refresh(key, last
2831
+ ? { ...page, entries }
2832
+ : { entries, removals: [], folders: [], folderRemovals: [] }));
2833
+ return true;
2834
+ });
2835
+ if (!staged)
2836
+ return false;
2837
+ if (last)
2838
+ return true;
2839
+ written = end;
2840
+ }
2841
+ },
2842
+ };
2843
+ }
2844
+ function batchEnd(entries, from) {
2845
+ let end = from;
2846
+ let spent = 0;
2847
+ while (end < entries.length) {
2848
+ const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
2849
+ if (spent + size > stageBytes && end > from)
2850
+ break;
2851
+ spent += size;
2852
+ end += 1;
2853
+ }
2854
+ return end;
2855
+ }
2856
+ async function timed(work) {
2857
+ const started = performance.now();
2858
+ const done = await work();
2859
+ const spent = performance.now() - started;
2860
+ if (spent < STAGE_AIM_MS / 2)
2861
+ stageBytes = Math.min(STAGE_BYTES, Math.round(stageBytes * 1.5));
2862
+ else if (spent > STAGE_AIM_MS * 2) {
2863
+ stageBytes = Math.max(STAGE_FLOOR, Math.round(stageBytes / 1.5));
2864
+ }
2865
+ return done;
2866
+ }
2867
+ function installWrite(work) {
2868
+ const next = installWrites.then(() => exclusive(work), () => exclusive(work));
2869
+ installWrites = next.then(() => undefined, () => undefined);
2870
+ return next;
2871
+ }
2872
+ function screened() {
2873
+ if (onScreen || closed)
2874
+ return Promise.resolve();
2875
+ queueReadiness();
2876
+ return new Promise((resolve) => shownWaiters.push(resolve));
2877
+ }
2878
+ async function heldPart() {
2879
+ while (partsFree === 0)
2880
+ await new Promise((admit) => partWaiters.push(admit));
2881
+ partsFree -= 1;
2882
+ return freed();
2883
+ }
2884
+ function freed() {
2885
+ let released = false;
2886
+ return () => {
2887
+ if (released)
2888
+ return;
2889
+ released = true;
2890
+ partsFree += 1;
2891
+ partWaiters.shift()?.();
2892
+ };
2893
+ }
2894
+ function installing(locator, step, share = null) {
2895
+ installs.set(locator, { step, share });
2896
+ queueReadiness();
1746
2897
  }
1747
- function writtenAfter(locator, roomVersion, path) {
1748
- const written = replicas.get(locator)?.confirmed.record.written[path];
1749
- return (written !== undefined &&
1750
- options.adapter.reached(locator, written, roomVersion) &&
1751
- !options.adapter.reached(locator, roomVersion, written));
2898
+ function installed(locator) {
2899
+ if (installs.delete(locator))
2900
+ queueReadiness();
1752
2901
  }
1753
- async function pullRefresh(authorized, confirmed, live) {
1754
- if (!live())
2902
+ function screen(value) {
2903
+ if (value === onScreen)
2904
+ return;
2905
+ onScreen = value;
2906
+ journal.record(`installation ${value ? 'shown' : 'hidden'}`);
2907
+ queueReadiness();
2908
+ if (!value)
2909
+ return;
2910
+ for (const resolve of shownWaiters.splice(0))
2911
+ resolve();
2912
+ }
2913
+ async function pullRefresh(authorized, held, liveness) {
2914
+ if (!liveness())
1755
2915
  return unconfirmed;
2916
+ const turn = generation;
2917
+ const { record } = held;
2918
+ const base = held.confirmed;
2919
+ const local = await openLocal();
1756
2920
  try {
1757
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1758
- const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
1759
- journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
1760
- return result;
2921
+ await exclusive(() => local.unrefresh(record.key));
2922
+ const result = await advancement(reads, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, liveness));
2923
+ if (result.kind !== 'advanced') {
2924
+ journal.record(`transfer refresh ${record.locator} ${result.kind}`);
2925
+ return result;
2926
+ }
2927
+ const refused = await validatedRoom(local, result.record, 'refreshed', {
2928
+ record,
2929
+ changed: [...result.changed],
2930
+ });
2931
+ if (refused !== null) {
2932
+ journal.record(`transfer refresh ${record.locator} refused`);
2933
+ return refused;
2934
+ }
2935
+ journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
2936
+ return { kind: 'advanced', record: result.record, base };
1761
2937
  }
1762
2938
  catch (error) {
1763
- journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
2939
+ journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
1764
2940
  throw error;
1765
2941
  }
2942
+ finally {
2943
+ reads.finished(record.locator, turn);
2944
+ }
2945
+ }
2946
+ const heldContents = new Map();
2947
+ function heldContentOf(local, key) {
2948
+ const known = heldContents.get(key);
2949
+ if (known !== undefined)
2950
+ return known;
2951
+ if (heldContents.size > ROOM_READS)
2952
+ heldContents.clear();
2953
+ const made = {
2954
+ files: (after, limit) => local.files(key, 'confirmed', after, limit),
2955
+ async contents(paths) {
2956
+ return new Map((await local.entries(key, 'confirmed', paths)).flatMap(({ path, content, revision }) => content === null || revision === null ? [] : [[path, { content, revision }]]));
2957
+ },
2958
+ };
2959
+ heldContents.set(key, made);
2960
+ return made;
1766
2961
  }
1767
- function staging(local, live) {
2962
+ function staging(local, liveness) {
1768
2963
  return {
1769
2964
  held: (locator) => transfers.get(locator) ?? null,
1770
- staged: (record) => local.staged(record),
1771
- stage: (record, page) => exclusive(async () => {
1772
- if (!live())
1773
- return false;
1774
- const bytes = layerBytes(page);
1775
- if (!(await admitted(local, record.locator, bytes)))
1776
- return false;
1777
- try {
1778
- await local.stage(record, page);
2965
+ async turn(locator, step) {
2966
+ installing(locator, step);
2967
+ for (;;) {
2968
+ await screened();
2969
+ const release = step === 'downloading' ? await heldPart() : () => { };
2970
+ if (onScreen || closed)
2971
+ return release;
2972
+ release();
1779
2973
  }
1780
- catch (error) {
1781
- if (!exceeded(error))
1782
- throw error;
1783
- await exceededOn(record.locator, bytes);
1784
- return false;
1785
- }
1786
- transfers.set(record.locator, record);
1787
- readSide.recover();
1788
- return true;
2974
+ },
2975
+ spare() {
2976
+ if (!onScreen || closed || partsFree === 0)
2977
+ return null;
2978
+ partsFree -= 1;
2979
+ return freed();
2980
+ },
2981
+ arriving(locator) {
2982
+ let ended = false;
2983
+ return {
2984
+ at(token, bytes) {
2985
+ if (ended)
2986
+ return;
2987
+ const held = arrivals.get(locator) ?? new Map();
2988
+ arrivals.set(locator, held.set(token, bytes));
2989
+ const now = performance.now();
2990
+ if (now - arrivalReported < ARRIVAL_REPORT_MS)
2991
+ return;
2992
+ arrivalReported = now;
2993
+ queueReadiness();
2994
+ },
2995
+ end() {
2996
+ ended = true;
2997
+ if (arrivals.delete(locator))
2998
+ queueReadiness();
2999
+ },
3000
+ };
3001
+ },
3002
+ received: (locator, token) => local.transferPart(locator, token),
3003
+ keep: (locator, token, received) => installWrite(async () => {
3004
+ if (liveness())
3005
+ await local.keepPart(locator, token, received);
1789
3006
  }),
3007
+ announced(locator) {
3008
+ const later = announced.get(locator);
3009
+ return later === undefined ? [] : [later];
3010
+ },
3011
+ async stage(record, page, composition, received) {
3012
+ const { locator } = record;
3013
+ const bytes = pageBytes(page);
3014
+ const whole = received?.byteLength ?? 0;
3015
+ const from = record.writing?.written ?? 0;
3016
+ const token = record.writing?.token ?? null;
3017
+ const entries = page.entries.length;
3018
+ let held = record;
3019
+ if (token !== null)
3020
+ arrivals.get(locator)?.delete(token);
3021
+ installing(locator, 'writing', { written: from, entries, bytes: whole });
3022
+ for (let written = from;;) {
3023
+ await screened();
3024
+ if (!liveness())
3025
+ return null;
3026
+ const end = batchEnd(page.entries, written);
3027
+ const last = end === entries;
3028
+ const next = last
3029
+ ? {
3030
+ ...held,
3031
+ writing: null,
3032
+ done: {
3033
+ parts: held.done.parts + (received === null ? 0 : 1),
3034
+ bytes: held.done.bytes + whole,
3035
+ },
3036
+ }
3037
+ : { ...held, writing: { token, written: end } };
3038
+ const key = await installWrite(async () => {
3039
+ if (!liveness())
3040
+ return null;
3041
+ if (written === from && !(await admitted(local, locator, bytes)))
3042
+ return null;
3043
+ try {
3044
+ return await timed(() => local.stage(next, composition, {
3045
+ entries: page.entries.slice(written, end),
3046
+ folders: last ? page.folders : [],
3047
+ }, last && received !== null && token !== null ? token : undefined));
3048
+ }
3049
+ catch (error) {
3050
+ if (!exceeded(error))
3051
+ throw error;
3052
+ await exceededOn(locator, bytes);
3053
+ return null;
3054
+ }
3055
+ });
3056
+ if (key === null)
3057
+ return null;
3058
+ const arrived = !transfers.has(locator);
3059
+ held = { ...next, key };
3060
+ transfers.set(locator, held);
3061
+ if (arrived)
3062
+ liveQueries.recover();
3063
+ if (last) {
3064
+ installing(locator, held.pageToken === null ? 'replaying' : 'downloading');
3065
+ liveQueries.recover();
3066
+ return held;
3067
+ }
3068
+ installing(locator, 'writing', { written: end, entries, bytes: whole });
3069
+ written = end;
3070
+ }
3071
+ },
3072
+ async refreshing(locator, key) {
3073
+ await exclusive(() => local.unrefresh(key));
3074
+ return refreshingOf(local, key, liveness, locator);
3075
+ },
1790
3076
  abandon: (locator) => exclusive(async () => {
1791
- if (!live())
3077
+ if (!liveness())
1792
3078
  return;
1793
3079
  await local.abandon(locator);
1794
3080
  transfers.delete(locator);
1795
- readSide.recover();
3081
+ liveQueries.recover();
3082
+ sweep();
3083
+ }),
3084
+ setAside: (locator) => exclusive(async () => {
3085
+ if (!liveness())
3086
+ return;
3087
+ await local.abandon(locator, true);
3088
+ transfers.delete(locator);
3089
+ liveQueries.recover();
1796
3090
  }),
3091
+ release: (key) => exclusive(async () => {
3092
+ await local.release(key);
3093
+ sweep();
3094
+ }),
3095
+ content: (key) => heldContentOf(local, key),
3096
+ validate: (record, view) => validatedRoom(local, record, view, null),
1797
3097
  };
1798
3098
  }
1799
- function reusable(room) {
1800
- const record = replicas.get(room.locator)?.confirmed.record;
3099
+ function reusable(room, local) {
3100
+ const record = replicas.get(room.locator)?.record;
1801
3101
  if (record === undefined || record.replica.selection !== room.replica.selection)
1802
3102
  return null;
1803
- return { documents: record.documents, revisions: record.revisions };
3103
+ return heldContentOf(local, record.key);
1804
3104
  }
1805
- async function pullBootstrap(authorized, room, local, live) {
1806
- if (!live())
3105
+ async function pullBootstrap(authorized, room, local, liveness) {
3106
+ if (!liveness())
1807
3107
  return unconfirmed;
3108
+ const turn = generation;
1808
3109
  if (replicas.has(room.locator)) {
1809
3110
  catching.add(room.locator);
1810
- readSide.recover();
3111
+ liveQueries.recover();
1811
3112
  }
1812
3113
  try {
1813
- const result = await bootstrapRoom(options.adapter, authorized, room, {
3114
+ const result = await bootstrapRoom(reads, authorized, room, {
1814
3115
  collection: modelOf(room.locator),
1815
3116
  selection: modelOf(room.locator, 'typed-selection'),
1816
- }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
1817
- const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
3117
+ }, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
3118
+ const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
1818
3119
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1819
- if (result.kind !== 'staged')
3120
+ if (result.kind !== 'staged') {
1820
3121
  caughtUpTo(room.locator);
3122
+ installed(room.locator);
3123
+ }
1821
3124
  return result;
1822
3125
  }
1823
3126
  catch (error) {
1824
3127
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
1825
3128
  caughtUpTo(room.locator);
3129
+ installed(room.locator);
1826
3130
  throw error;
1827
3131
  }
3132
+ finally {
3133
+ reads.finished(room.locator, turn);
3134
+ }
1828
3135
  }
1829
3136
  function caughtUpTo(locator) {
1830
3137
  if (catching.delete(locator))
1831
- readSide.recover();
3138
+ liveQueries.recover();
3139
+ }
3140
+ function behind(room) {
3141
+ const held = replicas.get(room.locator)?.record.roomVersion;
3142
+ return (held !== undefined &&
3143
+ [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
1832
3144
  }
1833
3145
  async function advance(room, local, authorized, live) {
1834
- await referenced(room.locator);
3146
+ const carried = reads.carried(room.locator);
3147
+ const withheld = await advanceOnce(room, local, authorized, live);
3148
+ if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
3149
+ return withheld;
3150
+ }
3151
+ return advanceOnce(room, local, authorized, live);
3152
+ }
3153
+ async function advanceOnce(room, local, authorized, live) {
1835
3154
  if (!live() || paused.has(room.locator))
1836
3155
  return unconfirmed;
1837
3156
  const held = replicas.get(room.locator);
1838
3157
  if (held !== undefined) {
1839
- const refreshed = await pullRefresh(authorized, held.confirmed, live);
3158
+ const refreshed = await pullRefresh(authorized, held, live);
1840
3159
  if (!live())
1841
3160
  return unconfirmed;
1842
3161
  if (refreshed.kind === 'still-current') {
@@ -1851,10 +3170,11 @@ export function composeRuntime(options) {
1851
3170
  return null;
1852
3171
  }
1853
3172
  if (refreshed.kind === 'advanced') {
1854
- if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
1855
- const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
3173
+ const landing = { kind: 'refreshed', base: refreshed.base };
3174
+ if (!(await publish(local, room, refreshed.record, landing, live))) {
3175
+ const now = replicas.get(room.locator)?.record.roomVersion;
1856
3176
  const reached = now !== undefined &&
1857
- options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
3177
+ options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
1858
3178
  return reached ? null : advance(room, local, authorized, live);
1859
3179
  }
1860
3180
  await retainModel(local, room, 'collection', modelOf(room.locator));
@@ -1864,17 +3184,18 @@ export function composeRuntime(options) {
1864
3184
  return refreshed;
1865
3185
  if (opened !== null) {
1866
3186
  owed.add(room.locator);
1867
- readSide.recover();
3187
+ liveQueries.recover();
1868
3188
  return unconfirmed;
1869
3189
  }
3190
+ if (reloadsDue.get(room.locator) === generation)
3191
+ return unconfirmed;
1870
3192
  }
1871
3193
  const bootstrapped = await pullBootstrap(authorized, room, local, live);
1872
- if (!live())
1873
- return unconfirmed;
1874
- if (bootstrapped.kind !== 'staged')
1875
- return bootstrapped;
1876
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
1877
- return null;
3194
+ if (bootstrapped.kind === 'staged') {
3195
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3196
+ return live() ? null : unconfirmed;
3197
+ }
3198
+ return live() ? bootstrapped : unconfirmed;
1878
3199
  }
1879
3200
  async function withhold(withheld, room, local, progress) {
1880
3201
  if (withheld.kind === 'unconfirmed')
@@ -1946,57 +3267,254 @@ export function composeRuntime(options) {
1946
3267
  (accepted.author === undefined ? '' : ` author ${accepted.author}`) +
1947
3268
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1948
3269
  }
1949
- async function adoptAccepted(room, local, submitted, accepted) {
1950
- await referenced(room.locator);
3270
+ function settling(acceptance) {
3271
+ return acceptance !== null && 'apply' in acceptance;
3272
+ }
3273
+ function refusalOf(acceptance) {
3274
+ return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
3275
+ }
3276
+ async function acceptance(room, local, submitted, accepted) {
1951
3277
  if (!continuing())
1952
3278
  return unconfirmed;
1953
3279
  const current = replicas.get(room.locator);
1954
3280
  if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1955
3281
  return null;
1956
3282
  }
1957
- const held = current.confirmed.record.roomVersion;
3283
+ const held = current.record.roomVersion;
1958
3284
  logAccepted(room.locator, submitted, accepted);
1959
- const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1960
- ? { kind: 'staged', confirmed: current.confirmed }
1961
- : applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
1962
- ? accepted.roomVersion
1963
- : held, accepted.roomVersion);
1964
- if (applied.kind !== 'staged') {
1965
- const state = pendings.get(submitted.id);
1966
- await local.accept(room.locator, submitted.id, accepted.roomVersion);
1967
- if (state !== undefined && !fences.has(room.locator))
1968
- pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1969
- emitQueue(room.locator);
1970
- return applied;
1971
- }
1972
- const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1973
- pendings.delete(submitted.id);
1974
- unreadable.delete(room.locator);
1975
- sourceRefusals.delete(room.locator);
1976
- declarationRefusals.delete(room.locator);
1977
- const set = settled();
1978
- list(room, true);
1979
- publishChangeSet(set);
1980
- moveTo({ ...status, ...attention() });
1981
- return null;
3285
+ let record = current.record;
3286
+ let changes;
3287
+ if (!options.adapter.reached(room.locator, held, accepted.roomVersion)) {
3288
+ const paths = [...returnedPaths(submitted, accepted)];
3289
+ const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
3290
+ const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
3291
+ record = {
3292
+ ...record,
3293
+ roomVersion: options.adapter.follows(room.locator, held, accepted.roomVersion)
3294
+ ? accepted.roomVersion
3295
+ : held,
3296
+ };
3297
+ const overrides = new Map([
3298
+ ...back.set.map((entry) => [entry.path, entry]),
3299
+ ...back.removed.map((path) => [path, null]),
3300
+ ]);
3301
+ const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
3302
+ if (refused !== null) {
3303
+ const key = pendingKey(room.locator, submitted.id);
3304
+ return {
3305
+ locator: room.locator,
3306
+ left: [],
3307
+ commit: {
3308
+ rooms: [],
3309
+ writes: [
3310
+ {
3311
+ locator: room.locator,
3312
+ accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
3313
+ },
3314
+ ],
3315
+ },
3316
+ refused,
3317
+ apply() {
3318
+ const state = pendings.get(key);
3319
+ if (state !== undefined && !fences.has(room.locator))
3320
+ pendings.set(key, { ...state, accepted: accepted.roomVersion });
3321
+ return changeSet(room.locator, { queue: true });
3322
+ },
3323
+ };
3324
+ }
3325
+ changes = back;
3326
+ }
3327
+ const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
3328
+ return {
3329
+ ...settled,
3330
+ apply(answer) {
3331
+ const set = settled.apply(answer);
3332
+ pendings.delete(pendingKey(room.locator, submitted.id));
3333
+ unreadable.delete(room.locator);
3334
+ sourceRefusals.delete(room.locator);
3335
+ declarationRefusals.delete(room.locator);
3336
+ list(room, true);
3337
+ return set;
3338
+ },
3339
+ };
3340
+ }
3341
+ async function adoptedTogether(local, acceptances) {
3342
+ const settlements = acceptances.filter(settling);
3343
+ if (settlements.length > 0) {
3344
+ publishChangeSet(await together(local, settlements));
3345
+ moveTo({ ...status, ...attention() });
3346
+ }
3347
+ await unpair(local);
1982
3348
  }
1983
3349
  async function adopted(room, local, submitted, accepted) {
1984
- const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
3350
+ const failed = await exclusive(async () => {
3351
+ const adopting = await acceptance(room, local, submitted, accepted);
3352
+ await adoptedTogether(local, [adopting]);
3353
+ return refusalOf(adopting);
3354
+ });
3355
+ if (failed === null)
3356
+ return true;
3357
+ if (failed.kind === 'refused' && continuing())
3358
+ refusedSource(room, failed);
3359
+ return false;
3360
+ }
3361
+ async function adoptedMove(room, partner, local, submitted, accepted) {
3362
+ const other = discovered.get(partner);
3363
+ const failed = await exclusive(async () => {
3364
+ const left = await acceptance(room, local, submitted, accepted);
3365
+ const arriving = replicas.get(partner)?.queue.find((batch) => batch.id === submitted.id);
3366
+ const arrived = other === undefined || arriving === undefined || accepted.destination === undefined
3367
+ ? null
3368
+ : await acceptance(other, local, arriving, {
3369
+ kind: 'accepted',
3370
+ ...accepted.destination,
3371
+ });
3372
+ await adoptedTogether(local, [left, arrived]);
3373
+ const refusedArrival = refusalOf(arrived);
3374
+ if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
3375
+ refusedSource(other, refusedArrival);
3376
+ }
3377
+ return refusalOf(left);
3378
+ });
3379
+ submitLater(partner);
1985
3380
  if (failed === null)
1986
3381
  return true;
1987
3382
  if (failed.kind === 'refused' && continuing())
1988
3383
  refusedSource(room, failed);
1989
3384
  return false;
1990
3385
  }
3386
+ function queueIn(locator) {
3387
+ return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
3388
+ }
3389
+ function behindIn(locator, batch) {
3390
+ const held = replicas.get(locator)?.queue;
3391
+ if (held === undefined)
3392
+ return queuesWithoutReplica.get(locator) !== undefined;
3393
+ return held.findIndex((one) => one.id === batch.id) > 0;
3394
+ }
3395
+ function unpaired(skipped) {
3396
+ for (const locator of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
3397
+ for (const batch of queueIn(locator)) {
3398
+ const partner = partnerOf(batch, locator);
3399
+ if (partner === null || skipped.has(pendingKey(locator, batch.id)))
3400
+ continue;
3401
+ if (queueIn(partner).some((held) => held.id === batch.id))
3402
+ continue;
3403
+ if (unpairedIn(locator, batch))
3404
+ return { locator, batch };
3405
+ }
3406
+ }
3407
+ return null;
3408
+ }
3409
+ async function unpairing(local, locator, batches) {
3410
+ const ids = new Set(batches.map(({ id }) => id));
3411
+ const leaving = batches.map((batch) => ({
3412
+ batch,
3413
+ returned: !localSaves && leavingIn(batch, locator) ? returnedBy('withdrawn') : null,
3414
+ }));
3415
+ const outcome = localSaves ? 'dropped' : 'unpaired';
3416
+ const current = replicas.get(locator);
3417
+ if (current !== undefined) {
3418
+ const settled = await settlement(local, locator, current, current.record, current.queue.filter((one) => !ids.has(one.id)), { leaving });
3419
+ return {
3420
+ ...settled,
3421
+ apply(answer) {
3422
+ for (const batch of batches)
3423
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
3424
+ const set = settled.apply(answer);
3425
+ submitLater(locator);
3426
+ return set;
3427
+ },
3428
+ };
3429
+ }
3430
+ const queue = queueIn(locator).filter((one) => !ids.has(one.id));
3431
+ const written = new Map();
3432
+ for (const { batch, returned } of leaving) {
3433
+ if (returned === null)
3434
+ continue;
3435
+ for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
3436
+ written.set(saved.path, {
3437
+ locator,
3438
+ path: saved.path,
3439
+ revision: roomDraftRevision(),
3440
+ parts: returnedInto((written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path))?.parts, saved.parts),
3441
+ returned,
3442
+ });
3443
+ }
3444
+ }
3445
+ const carried = carriedBack(locator, leaving.flatMap(({ batch, returned }) => (returned === null ? [] : [batch])), written);
3446
+ return {
3447
+ locator,
3448
+ left: batches,
3449
+ commit: {
3450
+ rooms: [],
3451
+ writes: [
3452
+ {
3453
+ locator,
3454
+ queue,
3455
+ roomDrafts: [...written.values()],
3456
+ consumedRoomDrafts: carried.taken,
3457
+ },
3458
+ ...carried.writes,
3459
+ ],
3460
+ },
3461
+ apply() {
3462
+ for (const batch of batches) {
3463
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
3464
+ pendings.delete(pendingKey(locator, batch.id));
3465
+ }
3466
+ if (queue.length > 0)
3467
+ queuesWithoutReplica.set(locator, queue);
3468
+ else
3469
+ queuesWithoutReplica.delete(locator);
3470
+ for (const record of written.values())
3471
+ rememberRoomDraft(record);
3472
+ carried.remember();
3473
+ submitLater(locator);
3474
+ return mergeChangeSets([
3475
+ changeSet(locator, {
3476
+ queue: true,
3477
+ drafts: [...written.keys(), ...carried.taken],
3478
+ }),
3479
+ ...carried.changes,
3480
+ ]);
3481
+ },
3482
+ };
3483
+ }
3484
+ async function unpair(local) {
3485
+ const handled = new Set();
3486
+ for (let held = unpaired(handled); held !== null && continuing(); held = unpaired(handled)) {
3487
+ const { locator, batch } = held;
3488
+ handled.add(pendingKey(locator, batch.id));
3489
+ publishChangeSet(await together(local, [await unpairing(local, locator, [batch])]));
3490
+ }
3491
+ if (handled.size === 0)
3492
+ return;
3493
+ moveTo({ ...status, ...attention() });
3494
+ }
3495
+ async function discardHeld(head, room, local, signal) {
3496
+ const partner = partnerOf(head, room.locator);
3497
+ const other = partner === null ? undefined : discovered.get(partner);
3498
+ await discardPending(other === undefined ? [room] : [room, other], local, signal);
3499
+ }
1991
3500
  async function submitRoom(room, local, authorized, progress, signal) {
1992
3501
  while (continuing() && authorization !== undefined && !signal.aborted) {
1993
3502
  const replica = replicas.get(room.locator);
1994
3503
  const head = replica?.queue[0];
1995
3504
  if (replica === undefined || head === undefined)
1996
3505
  return 'done';
1997
- if (pendings.get(head.id)?.accepted !== undefined)
3506
+ const state = pendings.get(pendingKey(room.locator, head.id));
3507
+ if (state?.accepted !== undefined)
3508
+ return 'done';
3509
+ const partner = partnerOf(head, room.locator);
3510
+ if (partner !== null && !leavingIn(head, room.locator)) {
3511
+ submitLater(partner);
1998
3512
  return 'done';
1999
- const state = pendings.get(head.id);
3513
+ }
3514
+ if (partner !== null && behindIn(partner, head)) {
3515
+ progress.complete = false;
3516
+ return 'done';
3517
+ }
2000
3518
  const { submission, lost: earlier } = state ?? {
2001
3519
  submission: { state: 'waiting' },
2002
3520
  lost: false,
@@ -2012,16 +3530,16 @@ export function composeRuntime(options) {
2012
3530
  }
2013
3531
  const lost = earlier || submission.state === 'submitted';
2014
3532
  if (lost && pastHorizon(head.id)) {
2015
- await discardPending(room, local, signal);
3533
+ await discardHeld(head, room, local, signal);
2016
3534
  return 'done';
2017
3535
  }
2018
3536
  let result;
2019
3537
  journal.record(`submission ${room.locator} ${head.id}`);
2020
- await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
3538
+ await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
2021
3539
  if (discoveryPaused || fences.has(room.locator))
2022
3540
  return 'done';
2023
3541
  if (signal.aborted || writesHeld()) {
2024
- await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
3542
+ await exclusive(() => mark(local, room.locator, head, submission, earlier));
2025
3543
  if (signal.aborted)
2026
3544
  return 'done';
2027
3545
  progress.complete = false;
@@ -2029,11 +3547,17 @@ export function composeRuntime(options) {
2029
3547
  return 'done';
2030
3548
  }
2031
3549
  const opensBefore = openSignals;
2032
- const answered = awaitingAnswer(room.locator);
3550
+ const answeredHere = awaitingAnswer(room.locator);
3551
+ const answeredThere = partner === null ? () => { } : awaitingAnswer(partner);
3552
+ const answered = () => {
3553
+ answeredHere();
3554
+ answeredThere();
3555
+ };
2033
3556
  try {
2034
3557
  result = await options.adapter.mutate(authorized, {
2035
3558
  locator: room.locator,
2036
3559
  changeBatch: head,
3560
+ file: batchFile(head),
2037
3561
  ...(state?.upload === undefined ? {} : { upload: state.upload }),
2038
3562
  }, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
2039
3563
  }
@@ -2058,9 +3582,10 @@ export function composeRuntime(options) {
2058
3582
  return 'again';
2059
3583
  }
2060
3584
  if (result.kind === 'room-not-readable') {
3585
+ const unread = discovered.get(result.locator) ?? room;
2061
3586
  progress.complete = false;
2062
- unreadable.add(room.locator);
2063
- list(room, false);
3587
+ unreadable.add(unread.locator);
3588
+ list(unread, false);
2064
3589
  emitRooms();
2065
3590
  return 'done';
2066
3591
  }
@@ -2075,24 +3600,30 @@ export function composeRuntime(options) {
2075
3600
  }
2076
3601
  if (result.kind === 'write-halted') {
2077
3602
  journal.record(`outcome ${room.locator} ${head.id} write-halted`);
2078
- await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, lost));
3603
+ await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
2079
3604
  if (serviceMode === 'open' && openSignals !== opensBefore)
2080
3605
  continue;
2081
3606
  haltedRefusal = true;
2082
3607
  progress.complete = false;
2083
3608
  return 'done';
2084
3609
  }
3610
+ const other = partner === null ? undefined : discovered.get(partner);
2085
3611
  if (result.kind === 'conflicted' ||
2086
3612
  result.kind === 'rejected' ||
2087
3613
  result.kind === 'expired' ||
2088
3614
  result.kind === 'retryable-refusal') {
2089
3615
  if (result.kind === 'expired' && lost && result.restored !== true) {
2090
3616
  journal.record(`outcome ${room.locator} ${head.id} expired`);
2091
- await discardPending(room, local, signal);
3617
+ await discardHeld(head, room, local, signal);
2092
3618
  return 'done';
2093
3619
  }
2094
3620
  if (result.kind === 'expired') {
2095
- await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
3621
+ await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
3622
+ const there = other === undefined ? null : await advance(other, local, authorized, continuing);
3623
+ if (other !== undefined && there !== null) {
3624
+ await withhold(there, other, local, progress);
3625
+ return 'done';
3626
+ }
2096
3627
  const withheld = await advance(room, local, authorized, continuing);
2097
3628
  if (withheld !== null) {
2098
3629
  await withhold(withheld, room, local, progress);
@@ -2109,11 +3640,25 @@ export function composeRuntime(options) {
2109
3640
  if (result.kind === 'retryable-refusal') {
2110
3641
  const reset = result.reset;
2111
3642
  progress.complete = false;
2112
- await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
3643
+ await exclusive(() => mark(local, room.locator, head, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
2113
3644
  return reset === undefined ? 'again' : { at: Date.parse(reset) };
2114
3645
  }
2115
3646
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
2116
3647
  const left = leaving(head, result);
3648
+ const refreshed = async () => ({
3649
+ there: other === undefined ? null : await advance(other, local, authorized, continuing),
3650
+ here: await advance(room, local, authorized, continuing),
3651
+ });
3652
+ const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
3653
+ if (judged !== null && (judged.there !== null || judged.here !== null)) {
3654
+ await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
3655
+ if (other !== undefined && judged.there !== null) {
3656
+ await withhold(judged.there, other, local, progress);
3657
+ }
3658
+ if (judged.here !== null)
3659
+ await withhold(judged.here, room, local, progress);
3660
+ return 'done';
3661
+ }
2117
3662
  await exclusive(async () => {
2118
3663
  if (!continuing())
2119
3664
  return;
@@ -2121,25 +3666,29 @@ export function composeRuntime(options) {
2121
3666
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
2122
3667
  return;
2123
3668
  }
2124
- const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
2125
- leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
2126
- (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
2127
- ? { ...left, returned: null }
2128
- : left,
2129
- });
2130
- publishChangeSet(settled());
3669
+ const confirmedLayer = await sliceAt(current.record, 'confirmed', [head]);
3670
+ const held = (result.kind === 'expired' &&
3671
+ (await landedWhole(room.locator, head, confirmedLayer))) ||
3672
+ (result.kind === 'conflicted' &&
3673
+ (await landedWhole(room.locator, head, confirmedLayer, result.affected)));
3674
+ publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
3675
+ await unpair(local);
2131
3676
  moveTo({ ...status, ...attention() });
2132
3677
  });
2133
3678
  if (result.kind === 'expired')
2134
3679
  continue;
2135
- const withheld = await advance(room, local, authorized, continuing);
2136
- if (withheld !== null) {
2137
- await withhold(withheld, room, local, progress);
3680
+ const { there, here } = judged ?? (await refreshed());
3681
+ if (other !== undefined && there !== null)
3682
+ await withhold(there, other, local, progress);
3683
+ if (here !== null) {
3684
+ await withhold(here, room, local, progress);
2138
3685
  return 'done';
2139
3686
  }
2140
3687
  continue;
2141
3688
  }
2142
- const kept = await adopted(room, local, head, result).finally(answered);
3689
+ const kept = await (partner === null
3690
+ ? adopted(room, local, head, result)
3691
+ : adoptedMove(room, partner, local, head, result)).finally(answered);
2143
3692
  if (!kept) {
2144
3693
  progress.complete = false;
2145
3694
  return 'done';
@@ -2150,44 +3699,67 @@ export function composeRuntime(options) {
2150
3699
  function pastHorizon(identity) {
2151
3700
  return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
2152
3701
  }
2153
- async function discardPending(room, local, signal) {
2154
- const { locator } = room;
3702
+ async function discardPending(rooms, local, signal) {
2155
3703
  const discarded = await exclusive(async () => {
2156
- if (fences.has(locator))
2157
- return false;
2158
- const before = replicas.get(locator);
2159
- const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
2160
- fences.set(locator, { at: discoveries, erased: false });
2161
- lastFenced.set(locator, ++fencings);
2162
- try {
2163
- await local.discard(locator);
2164
- }
2165
- finally {
2166
- fences.delete(locator);
3704
+ const held = rooms
3705
+ .filter(({ locator }) => !fences.has(locator))
3706
+ .map((room) => ({
3707
+ room,
3708
+ before: replicas.get(room.locator),
3709
+ queue: queueIn(room.locator),
3710
+ }));
3711
+ if (held.length === 0)
3712
+ return [];
3713
+ const answer = await local.commit({
3714
+ rooms: held.flatMap(({ room, before }) => before === undefined
3715
+ ? []
3716
+ : [
3717
+ {
3718
+ locator: room.locator,
3719
+ key: before.record.key,
3720
+ roots: before.record.typedSelection?.roots ?? [],
3721
+ overlay: { set: [], cleared: [...before.overlaid] },
3722
+ },
3723
+ ]),
3724
+ discards: held.map(({ room }) => room.locator),
3725
+ });
3726
+ for (const { room, before, queue } of held) {
3727
+ const { locator } = room;
3728
+ lastFenced.set(locator, ++fencings);
3729
+ for (const batch of queue)
3730
+ pendings.delete(pendingKey(locator, batch.id));
3731
+ queuesWithoutReplica.delete(locator);
3732
+ if (before !== undefined) {
3733
+ replicas.set(locator, {
3734
+ ...before,
3735
+ queue: [],
3736
+ overlaid: new Set(),
3737
+ count: answer.counts[locator] ?? before.count,
3738
+ });
3739
+ }
3740
+ journal.record(`room discarded ${locator}`);
2167
3741
  }
2168
- for (const batch of queue)
2169
- pendings.delete(batch.id);
2170
- queuesWithoutReplica.delete(locator);
2171
- const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
2172
- if (after !== null)
2173
- replicas.set(locator, after);
2174
- journal.record(`room discarded ${locator}`);
2175
- publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
3742
+ publishChangeSet(mergeChangeSets(held.map(({ room }) => changeSet(room.locator, {
3743
+ documents: answer.changed[room.locator] ?? [],
3744
+ queue: true,
3745
+ }))));
2176
3746
  moveTo({ ...status, ...attention() });
2177
- return true;
3747
+ return held.map(({ room }) => room);
2178
3748
  });
2179
- if (!discarded)
2180
- return;
2181
- const progress = { complete: true, reachable: true };
2182
- await roomTask(locator, async (live) => {
2183
- await Promise.race([
2184
- reload(room, local, progress, () => live() && !signal.aborted),
2185
- abandonedOn(signal),
2186
- ]);
2187
- });
2188
- if (signal.aborted)
2189
- return;
2190
- await rest(progress);
3749
+ for (const room of discarded) {
3750
+ if (signal.aborted)
3751
+ return;
3752
+ const progress = { complete: true, reachable: true };
3753
+ await roomTask(room.locator, async (live) => {
3754
+ await Promise.race([
3755
+ reload(room, local, progress, () => live() && !signal.aborted),
3756
+ abandonedOn(signal),
3757
+ ]);
3758
+ });
3759
+ if (signal.aborted)
3760
+ return;
3761
+ await rest(progress);
3762
+ }
2191
3763
  }
2192
3764
  function awaitingAnswer(locator) {
2193
3765
  let release = () => { };
@@ -2204,7 +3776,7 @@ export function composeRuntime(options) {
2204
3776
  async function retainModel(local, room, attached, next) {
2205
3777
  const key = modelKey(room.locator, attached);
2206
3778
  const previous = modelOf(room.locator, attached);
2207
- const heldBefore = replicas.get(room.locator)?.confirmed.record;
3779
+ const heldBefore = replicas.get(room.locator)?.record;
2208
3780
  if (JSON.stringify(previous) === JSON.stringify(next) &&
2209
3781
  (heldBefore === undefined ||
2210
3782
  (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
@@ -2220,36 +3792,56 @@ export function composeRuntime(options) {
2220
3792
  if (!continuing())
2221
3793
  return;
2222
3794
  const held = replicas.get(room.locator);
3795
+ let changed = null;
2223
3796
  if (held !== undefined) {
2224
3797
  const declared = {
2225
- ...held.confirmed.record,
3798
+ ...held.record,
2226
3799
  operations: room.operations,
2227
3800
  replica: room.replica,
2228
3801
  typedSelection: room.typedSelection ?? null,
2229
3802
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
2230
3803
  };
2231
- const reclassified = sameMembership(declared, held.confirmed.record)
2232
- ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
2233
- : readReclassified(declared, held.confirmed);
2234
- const record = reclassified.kind === 'staged'
3804
+ let read = 'staged';
3805
+ if (!sameMembership(declared, held.record)) {
3806
+ const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), compositionOf(declared));
3807
+ read =
3808
+ verdict.verdict !== 'refuse'
3809
+ ? 'staged'
3810
+ : verdict.rule === 'model-unavailable'
3811
+ ? 'deferred'
3812
+ : { kind: 'refused', refusal: verdict };
3813
+ }
3814
+ const record = read === 'staged'
2235
3815
  ? declared
2236
- : {
2237
- ...declared,
2238
- typedSelection: held.confirmed.record.typedSelection,
2239
- };
2240
- const read = reclassified.kind === 'deferred'
2241
- ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
2242
- : reclassified;
2243
- await local.write({ next: record, previous: held.confirmed.record });
2244
- if (read.kind === 'staged') {
2245
- replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
3816
+ : { ...declared, typedSelection: held.record.typedSelection };
3817
+ const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
3818
+ const replayedLayer = await replayed(record, confirmedLayer, held.queue);
3819
+ const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
3820
+ const answer = await local.commit({
3821
+ rooms: [
3822
+ {
3823
+ locator: room.locator,
3824
+ key: record.key,
3825
+ record: storedRecord(record),
3826
+ roots: record.typedSelection?.roots ?? [],
3827
+ composition: compositionOf(record),
3828
+ overlay: diff.overlay,
3829
+ },
3830
+ ],
3831
+ });
3832
+ const { count, indexed } = committedRoom(room.locator, answer);
3833
+ replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
3834
+ changed = changeSet(room.locator, {
3835
+ documents: answer.changed[room.locator] ?? [],
3836
+ room: true,
3837
+ });
3838
+ if (read === 'staged' || read === 'deferred') {
2246
3839
  if (declarationRefusals.delete(room.locator)) {
2247
3840
  sourceRefusals.delete(room.locator);
2248
3841
  unreadable.delete(room.locator);
2249
3842
  }
2250
3843
  }
2251
- else if (read.kind === 'refused') {
2252
- replicas.set(room.locator, { ...held, confirmed: { ...held.confirmed, record } });
3844
+ else {
2253
3845
  refusedSource(room, read);
2254
3846
  declarationRefusals.add(room.locator);
2255
3847
  }
@@ -2260,13 +3852,18 @@ export function composeRuntime(options) {
2260
3852
  invalidModels.delete(key);
2261
3853
  }
2262
3854
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
2263
- if (held === undefined)
3855
+ if (changed === null)
2264
3856
  emitRooms();
2265
3857
  else
2266
- publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
3858
+ publishChangeSet(changed);
2267
3859
  });
2268
3860
  }
2269
3861
  async function scoped(result, room, local) {
3862
+ if (result.scope === 'content' && !result.retryable) {
3863
+ journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
3864
+ await loseRooms([room.locator]);
3865
+ return;
3866
+ }
2270
3867
  scopedRefusals.set(room.locator, {
2271
3868
  code: result.code,
2272
3869
  fields: result.fields,
@@ -2280,15 +3877,7 @@ export function composeRuntime(options) {
2280
3877
  });
2281
3878
  else if (result.scope !== 'operation') {
2282
3879
  unreadable.add(room.locator);
2283
- scopedAway = {
2284
- ...room,
2285
- replica: { ...room.replica, admission: 'unavailable' },
2286
- operations: {
2287
- ...room.operations,
2288
- ...(result.scope === 'content' ? { read: [] } : {}),
2289
- ...(result.scope === 'sync' ? { sync: [] } : {}),
2290
- },
2291
- };
3880
+ scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
2292
3881
  await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
2293
3882
  }
2294
3883
  if (!continuing())
@@ -2299,30 +3888,22 @@ export function composeRuntime(options) {
2299
3888
  }
2300
3889
  function heldRooms(listed) {
2301
3890
  return listed.flatMap((room) => {
2302
- const record = replicas.get(room.locator)?.confirmed.record;
3891
+ const record = replicas.get(room.locator)?.record;
2303
3892
  if (record === undefined || record.replica.selection !== room.replica.selection)
2304
3893
  return [];
2305
- return [
2306
- {
2307
- locator: room.locator,
2308
- roomVersion: record.roomVersion,
2309
- documents: record.documents,
2310
- revisions: record.revisions,
2311
- },
2312
- ];
3894
+ return [{ locator: room.locator, roomVersion: record.roomVersion }];
2313
3895
  });
2314
3896
  }
2315
3897
  async function synchronize(listed, local, progress, attempt, live) {
2316
- const acquired = listed.filter(acquires);
2317
- const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
3898
+ const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
2318
3899
  if (!live())
2319
3900
  return;
2320
- const ready = acquired.filter((_, index) => {
3901
+ const ready = listed.filter((_, index) => {
2321
3902
  const outcome = prepared[index];
2322
3903
  return outcome?.status === 'fulfilled' && outcome.value;
2323
3904
  });
2324
3905
  if (attempt === 'opened') {
2325
- options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
3906
+ options.adapter.roomStream?.hold(heldRooms(ready));
2326
3907
  handed = true;
2327
3908
  }
2328
3909
  discovering?.release();
@@ -2338,7 +3919,7 @@ export function composeRuntime(options) {
2338
3919
  progress.complete = false;
2339
3920
  return false;
2340
3921
  }
2341
- const prior = replicas.get(room.locator)?.confirmed.record;
3922
+ const prior = replicas.get(room.locator)?.record;
2342
3923
  if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
2343
3924
  list(prior, false);
2344
3925
  emitRooms();
@@ -2350,19 +3931,10 @@ export function composeRuntime(options) {
2350
3931
  await retainModel(local, room, 'collection', modelOf(room.locator));
2351
3932
  if (discoveryPaused || authorization === undefined)
2352
3933
  return false;
2353
- if (room.replica.admission !== 'admitted') {
2354
- if (replicas.has(room.locator))
2355
- progress.complete = false;
2356
- return false;
2357
- }
2358
- return true;
3934
+ return room.replica.admission === 'admitted';
2359
3935
  }
2360
3936
  async function caughtUp(room, local, progress, live) {
2361
- const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
2362
- if (held === undefined || authorization === undefined || !live())
2363
- return;
2364
- const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
2365
- if (!behind)
3937
+ if (authorization === undefined || !live() || !behind(room))
2366
3938
  return;
2367
3939
  const withheld = await advance(room, local, authorization, live);
2368
3940
  if (withheld !== null)
@@ -2390,23 +3962,24 @@ export function composeRuntime(options) {
2390
3962
  }
2391
3963
  async function reload(room, local, progress, live) {
2392
3964
  try {
3965
+ if (live())
3966
+ reloadsDue.delete(room.locator);
2393
3967
  if (!live() || authorization === undefined)
2394
3968
  return;
2395
3969
  const bootstrapped = await pullBootstrap(authorization, room, local, live);
2396
- if (!live())
2397
- return;
2398
3970
  if (bootstrapped.kind !== 'staged') {
2399
- await withhold(bootstrapped, room, local, progress);
3971
+ if (live())
3972
+ await withhold(bootstrapped, room, local, progress);
2400
3973
  return;
2401
3974
  }
2402
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
3975
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
2403
3976
  await caughtUp(room, local, progress, live);
2404
3977
  }
2405
3978
  finally {
2406
3979
  caughtUpTo(room.locator);
2407
3980
  }
2408
3981
  }
2409
- async function discoverRooms(local, progress, attempt, live) {
3982
+ async function discoverRooms(local, progress, attempt, live, scope) {
2410
3983
  if (closed || authorization === undefined) {
2411
3984
  notifyReady();
2412
3985
  return;
@@ -2416,7 +3989,7 @@ export function composeRuntime(options) {
2416
3989
  const started = discoveries;
2417
3990
  let result;
2418
3991
  try {
2419
- result = await options.adapter.discover(authorization);
3992
+ result = await options.adapter.discover(authorization, scope);
2420
3993
  }
2421
3994
  catch {
2422
3995
  result = { kind: 'transport-uncertainty' };
@@ -2456,12 +4029,13 @@ export function composeRuntime(options) {
2456
4029
  }
2457
4030
  for (const room of result.rooms) {
2458
4031
  if (room.operations === undefined ||
2459
- !['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
4032
+ !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
2460
4033
  room.operations[operation].every((scope) => typeof scope === 'string')) ||
2461
4034
  room.replica === undefined ||
2462
4035
  typeof room.replica.selection !== 'string' ||
2463
4036
  room.replica.selection.length === 0 ||
2464
- !['admitted', 'unavailable'].includes(room.replica.admission)) {
4037
+ !['admitted', 'unavailable'].includes(room.replica.admission) ||
4038
+ !['locked', 'open'].includes(room.replica.offline)) {
2465
4039
  journal.record(`invalid model or replica declaration ${room.locator}`);
2466
4040
  await refuse('missing-required-guarantee');
2467
4041
  notifyReady();
@@ -2485,15 +4059,16 @@ export function composeRuntime(options) {
2485
4059
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
2486
4060
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
2487
4061
  limits = result.providerLimits;
2488
- await local.declare(limits, capabilities);
4062
+ await local.fact('providerLimits', limits);
4063
+ await local.fact('capabilities', capabilities);
2489
4064
  }
2490
4065
  const signedIn = result.member ?? null;
2491
4066
  if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
2492
- await local.keepMember(signedIn);
4067
+ await local.fact('member', signedIn);
2493
4068
  member = signedIn;
2494
4069
  emitMember();
2495
4070
  }
2496
- await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
4071
+ await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
2497
4072
  }
2498
4073
  async function ensureAuthorization() {
2499
4074
  if (authorization !== undefined)
@@ -2590,9 +4165,44 @@ export function composeRuntime(options) {
2590
4165
  options.adapter.roomStream?.reopen();
2591
4166
  });
2592
4167
  }
2593
- function closedSocket(revoked) {
4168
+ function rediscover(scope) {
4169
+ return task(OPENING_LANE, async (live) => {
4170
+ if (!live() || !socketOpen)
4171
+ return;
4172
+ const progress = { complete: true, reachable: true };
4173
+ const gate = discover();
4174
+ moveTo(transferring(status));
4175
+ try {
4176
+ const local = await openLocal();
4177
+ if (!live())
4178
+ return;
4179
+ await ensureAuthorization();
4180
+ if (!live())
4181
+ return;
4182
+ await discoverRooms(local, progress, 'authorized', live, scope);
4183
+ }
4184
+ catch {
4185
+ progress.complete = false;
4186
+ }
4187
+ finally {
4188
+ gate.release();
4189
+ notifyReady();
4190
+ }
4191
+ if (!live())
4192
+ return;
4193
+ if (!progress.complete)
4194
+ complete = false;
4195
+ await rest(progress, live);
4196
+ });
4197
+ }
4198
+ function closedSocket(revoked, lost) {
2594
4199
  const opened = socketOpen;
2595
4200
  socketOpen = false;
4201
+ connectionLost();
4202
+ if (lost) {
4203
+ reads.lost();
4204
+ conversations.lost();
4205
+ }
2596
4206
  if (revoked) {
2597
4207
  void refuse('revoked-authorization');
2598
4208
  return;
@@ -2609,9 +4219,10 @@ export function composeRuntime(options) {
2609
4219
  function applyOpening(expired) {
2610
4220
  const progress = underway?.generation === generation ? underway.progress : null;
2611
4221
  const cycle = progress ?? { complete: true, reachable: true };
2612
- const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
2613
4222
  if (opened === generation)
2614
4223
  opened = null;
4224
+ const reloading = progress === null ? [] : owedReloads(expired);
4225
+ const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
2615
4226
  return task(OPENING_LANE, async (live) => {
2616
4227
  underway = null;
2617
4228
  if (!live())
@@ -2621,24 +4232,17 @@ export function composeRuntime(options) {
2621
4232
  await rest(cycle, live);
2622
4233
  return;
2623
4234
  }
2624
- await roomsCarried(expired);
4235
+ await roomsCarried([...expired, ...reloading]);
2625
4236
  if (!live())
2626
4237
  return;
2627
- const replaced = new Set(owed);
2628
- owed.clear();
2629
4238
  try {
2630
4239
  const local = await openLocal();
2631
- const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
2632
- const reloading = held.filter((room) => !expired.includes(room.locator) &&
2633
- (replaced.has(room.locator) || revoked(room.locator)));
2634
- for (const room of reloading)
2635
- catching.add(room.locator);
2636
- readSide.recover();
2637
- const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
2638
- const reloaded = [...(await reloads), ...(await replacing)];
4240
+ const reloaded = await replacing;
2639
4241
  if (progress.reachable) {
2640
- for (const room of held) {
2641
- if (!expired.includes(room.locator) && !reloading.includes(room)) {
4242
+ for (const room of discovered.values()) {
4243
+ if (replicas.has(room.locator) &&
4244
+ !expired.includes(room.locator) &&
4245
+ !reloading.includes(room.locator)) {
2642
4246
  await confirmModels(room, local);
2643
4247
  }
2644
4248
  }
@@ -2655,14 +4259,29 @@ export function composeRuntime(options) {
2655
4259
  await finishOpening(progress, live);
2656
4260
  });
2657
4261
  }
4262
+ function owedReloads(expired) {
4263
+ const reloading = [...discovered.values()]
4264
+ .filter((room) => replicas.has(room.locator) &&
4265
+ !expired.includes(room.locator) &&
4266
+ (owed.has(room.locator) || revoked(room.locator)))
4267
+ .map((room) => room.locator);
4268
+ owed.clear();
4269
+ for (const locator of reloading)
4270
+ catching.add(locator);
4271
+ liveQueries.recover();
4272
+ return reloading;
4273
+ }
2658
4274
  async function reloadNow(replaced, progress) {
4275
+ const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
4276
+ for (const room of reloaded)
4277
+ reloadsDue.set(room.locator, generation);
2659
4278
  try {
2660
4279
  const local = await openLocal();
2661
- return await Promise.allSettled([...discovered.values()]
2662
- .filter((room) => replicas.has(room.locator) && replaced(room))
2663
- .map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
4280
+ return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
2664
4281
  }
2665
4282
  catch {
4283
+ for (const room of reloaded)
4284
+ reloadsDue.delete(room.locator);
2666
4285
  progress.reachable = false;
2667
4286
  return [];
2668
4287
  }
@@ -2779,6 +4398,7 @@ export function composeRuntime(options) {
2779
4398
  progress.complete = false;
2780
4399
  return;
2781
4400
  }
4401
+ reads.applied();
2782
4402
  submitAll();
2783
4403
  for (const locator of stale)
2784
4404
  if (repairable.has(locator))
@@ -2787,10 +4407,11 @@ export function composeRuntime(options) {
2787
4407
  if (status.providerUnreachable)
2788
4408
  conversations.reconnected();
2789
4409
  const reading = { complete: true, reachable: true };
2790
- if (demand.conversations)
2791
- await conversations.catchUp(reading);
4410
+ await conversations.catchUp(reading);
2792
4411
  if (progress.reachable && reading.reachable)
2793
4412
  await conversations.resend(reading);
4413
+ if (progress.reachable)
4414
+ void recovery.attempt('calls', resendCalls);
2794
4415
  if (!reading.reachable) {
2795
4416
  progress.reachable = false;
2796
4417
  resendLater();
@@ -2814,7 +4435,7 @@ export function composeRuntime(options) {
2814
4435
  if (synced && !recorded) {
2815
4436
  const completedAt = new Date().toISOString();
2816
4437
  try {
2817
- await (await openLocal()).complete(completedAt);
4438
+ await (await openLocal()).fact('lastCompleted', completedAt);
2818
4439
  if (!live())
2819
4440
  return;
2820
4441
  recorded = true;
@@ -2851,11 +4472,11 @@ export function composeRuntime(options) {
2851
4472
  return;
2852
4473
  const room = discovered.get(locator);
2853
4474
  const held = replicas.get(locator);
2854
- if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
4475
+ if (room === undefined || room.replica.admission !== 'admitted')
2855
4476
  return;
2856
4477
  if (held !== undefined &&
2857
- (held.confirmed.record.replica.selection !== room.replica.selection ||
2858
- options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
4478
+ (held.record.replica.selection !== room.replica.selection ||
4479
+ options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
2859
4480
  return;
2860
4481
  }
2861
4482
  const during = underway?.generation === generation ? underway.progress : null;
@@ -2902,18 +4523,29 @@ export function composeRuntime(options) {
2902
4523
  return;
2903
4524
  if (event.kind === 'opened') {
2904
4525
  socketOpen = true;
4526
+ connectionLive();
2905
4527
  generation += 1;
4528
+ reads.opened();
2906
4529
  conversations.reopened();
2907
4530
  recovery.resume();
2908
4531
  void sync('opened');
2909
4532
  }
2910
4533
  else if (event.kind === 'closed')
2911
- closedSocket(event.revoked);
4534
+ closedSocket(event.revoked, event.lost);
2912
4535
  else if (event.kind === 'opening')
2913
4536
  void applyOpening(event.expired);
4537
+ else if (event.kind === 'authority-changed')
4538
+ void rediscover(event.scope);
2914
4539
  else
2915
4540
  void applyRoom(event.locator, event.roomVersion);
2916
- }, receipts);
4541
+ }, receipts, heldRevisions);
4542
+ }
4543
+ async function heldRevisions(locator, paths) {
4544
+ const held = replicas.get(locator);
4545
+ if (held === undefined || paths.length === 0)
4546
+ return new Map();
4547
+ const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
4548
+ return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
2917
4549
  }
2918
4550
  function writesHeld() {
2919
4551
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -2965,7 +4597,7 @@ export function composeRuntime(options) {
2965
4597
  if (!live())
2966
4598
  return;
2967
4599
  await conversations.reconcile();
2968
- if (conversations.current() || !demand.conversations) {
4600
+ if (conversations.current()) {
2969
4601
  show();
2970
4602
  return;
2971
4603
  }
@@ -2983,10 +4615,12 @@ export function composeRuntime(options) {
2983
4615
  if (closed)
2984
4616
  return;
2985
4617
  closed = true;
2986
- if (narrowing !== null)
2987
- clearTimeout(narrowing);
2988
4618
  if (rechecking !== null)
2989
4619
  clearInterval(rechecking);
4620
+ for (const resolve of shownWaiters.splice(0))
4621
+ resolve();
4622
+ connectionLive();
4623
+ lockListeners.clear();
2990
4624
  readinessWatches.clear();
2991
4625
  withdrawStream?.();
2992
4626
  withdrawStream = undefined;
@@ -2995,17 +4629,13 @@ export function composeRuntime(options) {
2995
4629
  withdrawServiceMode?.();
2996
4630
  conversations.close();
2997
4631
  void recovery.retire();
2998
- store?.close();
2999
- void opening?.then((opened) => {
3000
- opened.close();
3001
- }, () => { });
3002
4632
  roomListeners.clear();
3003
4633
  refusalListeners.clear();
3004
4634
  storageLossListeners.clear();
3005
4635
  statusListeners.clear();
3006
4636
  serviceModeListeners.clear();
3007
4637
  memberListeners.clear();
3008
- readSide.close();
4638
+ liveQueries.close();
3009
4639
  queueSubscriptions.clear();
3010
4640
  roomDraftListeners.clear();
3011
4641
  presentationListeners.clear();
@@ -3032,71 +4662,98 @@ export function composeRuntime(options) {
3032
4662
  if (options.adapter.roomStream === undefined)
3033
4663
  void sync('unstreamed');
3034
4664
  function planned(input, reach) {
3035
- return planOf(input, roomFacts(reach), planReach(reach));
4665
+ const plan = planOf(input, roomFacts(reach), planReach(reach));
4666
+ const named = plan.paths !== null &&
4667
+ plan.rooms !== null &&
4668
+ plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
4669
+ return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
3036
4670
  }
3037
4671
  function watchPlan(plan, deliver) {
3038
- return readSide.watch(plan, identityOf(plan), () => {
4672
+ let seeded = false;
4673
+ let pending = true;
4674
+ const shown = () => {
4675
+ const seed = seeds.get(plan.key);
4676
+ if (seed === undefined || seeded || !pending)
4677
+ return;
4678
+ seeded = true;
4679
+ deliver(seededResult(seed));
4680
+ };
4681
+ const watchers = seedWatchers.get(plan.key) ?? new Set();
4682
+ seedWatchers.set(plan.key, watchers);
4683
+ watchers.add(shown);
4684
+ const stop = liveQueries.watch(plan, identityOf(plan), () => {
3039
4685
  const cover = covered(plan);
3040
4686
  awaitPreparation(cover);
3041
- return cover;
3042
- }, deliver);
4687
+ return seedBehind(plan, cover);
4688
+ }, (message) => {
4689
+ const answered = message;
4690
+ pending = whole(answered) && 'pending' in answered.result;
4691
+ if (pending && seeds.has(plan.key)) {
4692
+ shown();
4693
+ return;
4694
+ }
4695
+ seeded = false;
4696
+ deliver(message);
4697
+ });
4698
+ return () => {
4699
+ watchers.delete(shown);
4700
+ if (watchers.size === 0)
4701
+ seedWatchers.delete(plan.key);
4702
+ stop();
4703
+ };
3043
4704
  }
3044
- async function searched(input, reach) {
3045
- const plan = searchPlanOf(input);
3046
- const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
4705
+ async function searched(input, reach, locked) {
4706
+ const asked = searchPlanOf(input);
4707
+ const filter = {
4708
+ ...filterOf(asked.documents ?? {}, roomFacts(reach), planReach(reach)),
4709
+ locked,
4710
+ };
4711
+ const unlocking = (filter.rooms ?? [])
4712
+ .filter((room) => locked.includes(room))
4713
+ .map((room) => ({ room, awaits: 'unlock' }));
3047
4714
  for (;;) {
3048
4715
  const cover = covered(filter);
3049
4716
  awaitPreparation(cover);
3050
- const pending = 'pending' in cover ? [...cover.pending] : [];
3051
- const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3052
- const held = covered({
3053
- ...filter,
3054
- rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
3055
- });
3056
- const covering = 'pending' in held ? [] : held.coverage.rooms;
3057
- const unindexed = covering
3058
- .filter(({ room }) => !searchIndex.rooms().has(room))
3059
- .map(({ room }) => room);
3060
- for (const room of unindexed)
3061
- pending.push({ room, awaits: 'index' });
3062
- const listed = conversations.searchable();
3063
- const named = reach !== null && !reach.conversations
3064
- ? []
3065
- : (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
3066
- const waiting = named.filter((one) => !searchIndex.conversations().has(one));
3067
- for (const conversation of waiting)
3068
- pending.push({ conversation, awaits: 'index' });
3069
- if (unindexed.length > 0 || waiting.length > 0)
3070
- index(unindexed, waiting);
4717
+ const pending = [...unlocking, ...('pending' in cover ? cover.pending : [])];
3071
4718
  const complete = pending.length === 0 && !('pending' in cover);
4719
+ const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3072
4720
  const answering = complete
3073
4721
  ? filter
3074
- : {
3075
- ...filter,
3076
- rooms: covering
3077
- .filter(({ room }) => searchIndex.rooms().has(room))
3078
- .map(({ room }) => room),
3079
- };
4722
+ : { ...filter, rooms: reachedRooms(filter).filter((room) => !awaited.has(room)) };
3080
4723
  const answered = covered(answering);
3081
4724
  if ('pending' in answered)
3082
4725
  continue;
3083
- const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
3084
- const seen = searchIndex.changes();
3085
- await searchIndex.caughtUp();
3086
- const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(answering);
3087
- if (selected !== null && 'pending' in selected)
3088
- continue;
3089
- if (searchIndex.changes() !== seen ||
3090
- JSON.stringify(covered(answering)) !== JSON.stringify(answered))
4726
+ const listed = conversations.searchable();
4727
+ const named = reach !== null && !reach.conversations
4728
+ ? []
4729
+ : (asked.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
4730
+ const hits = await (await openLocal()).search({
4731
+ text: asked.text,
4732
+ limit: capped(asked.limit),
4733
+ after: asked.after,
4734
+ total: asked.total,
4735
+ documents: {
4736
+ plan: answering,
4737
+ rooms: answered.rooms,
4738
+ beneath: asked.collection
4739
+ ? answered.rooms.map(({ locator, key }) => ({
4740
+ key,
4741
+ roots: replicas.get(locator)?.record.typedSelection?.roots ?? [],
4742
+ }))
4743
+ : [],
4744
+ },
4745
+ conversations: named,
4746
+ });
4747
+ if (JSON.stringify(covered(answering)) !== JSON.stringify(answered))
3091
4748
  continue;
3092
- const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, complete ? null : new Set(answering.rooms));
3093
4749
  const coverage = {
3094
4750
  rooms: answered.coverage.rooms,
3095
- conversations: searchedConversations,
3096
- index: searchIndex.generation(),
4751
+ conversations: named,
3097
4752
  overlay: answered.coverage.overlay,
3098
4753
  };
3099
- return complete ? { ...hits, coverage } : { pending, ...hits, coverage };
4754
+ return complete
4755
+ ? { ...hits, coverage }
4756
+ : { pending, documents: hits.documents, messages: hits.messages, coverage };
3100
4757
  }
3101
4758
  }
3102
4759
  const watch = (topic, input, deliver) => {
@@ -3105,13 +4762,17 @@ export function composeRuntime(options) {
3105
4762
  roomDraftListeners.add(listener);
3106
4763
  void exclusive(async () => {
3107
4764
  await openLocal();
3108
- if (closed || !roomDraftListeners.has(listener))
3109
- return;
3110
- listener.ready = true;
3111
4765
  }).then(() => {
3112
- if (closed || !listener.ready || !roomDraftListeners.has(listener))
4766
+ if (closed || !roomDraftListeners.has(listener))
3113
4767
  return;
3114
- listener.notify(snapshotRoomDrafts());
4768
+ const snapshot = snapshotRoomDrafts();
4769
+ draftsShown = draftsShown.then(async () => {
4770
+ const value = await snapshot.catch(() => null);
4771
+ if (value === null || closed || !roomDraftListeners.has(listener))
4772
+ return;
4773
+ listener.ready = true;
4774
+ listener.notify(value);
4775
+ });
3115
4776
  }, () => { });
3116
4777
  return () => {
3117
4778
  roomDraftListeners.delete(listener);
@@ -3206,7 +4867,7 @@ export function composeRuntime(options) {
3206
4867
  return listed();
3207
4868
  },
3208
4869
  query: ((input) => rowsQuery(planned(input, null))),
3209
- search: (input) => searched(input, null),
4870
+ search: (input) => searched(input, null, []),
3210
4871
  types(locator) {
3211
4872
  return {
3212
4873
  collection: modelTypes(modelOf(locator)),
@@ -3226,9 +4887,9 @@ export function composeRuntime(options) {
3226
4887
  return listed;
3227
4888
  });
3228
4889
  },
3229
- read(room, path) {
4890
+ async read(room, path) {
3230
4891
  const record = roomDrafts.get(room)?.get(path);
3231
- return record === undefined ? null : roomDraftOf(record);
4892
+ return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
3232
4893
  },
3233
4894
  write(room, path, revision, input) {
3234
4895
  return exclusive(async () => {
@@ -3238,11 +4899,11 @@ export function composeRuntime(options) {
3238
4899
  const local = await openLocal();
3239
4900
  const current = roomDrafts.get(room)?.get(path);
3240
4901
  if ((current?.revision ?? null) !== revision) {
3241
- return staleWrite(current === undefined ? null : roomDraftOf(current));
4902
+ return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
3242
4903
  }
3243
4904
  const file = parts.file === undefined
3244
4905
  ? undefined
3245
- : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
4906
+ : await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
3246
4907
  const record = {
3247
4908
  locator: room,
3248
4909
  path,
@@ -3250,10 +4911,12 @@ export function composeRuntime(options) {
3250
4911
  parts: held(current?.parts, parts, file),
3251
4912
  returned: current?.returned ?? null,
3252
4913
  };
3253
- await local.keepRoomDraft(record);
4914
+ await local.keepRoomDraft(record, parts.file === undefined || file === undefined
4915
+ ? []
4916
+ : [{ key: file.localId, blob: parts.file.blob }]);
3254
4917
  rememberRoomDraft(record);
3255
- publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
3256
- return { outcome: 'written', roomDraft: roomDraftOf(record) };
4918
+ publishChangeSet(changeSet(room, { drafts: [path] }));
4919
+ return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
3257
4920
  });
3258
4921
  },
3259
4922
  discard(room, path, revision) {
@@ -3261,11 +4924,11 @@ export function composeRuntime(options) {
3261
4924
  const local = await openLocal();
3262
4925
  const current = roomDrafts.get(room)?.get(path);
3263
4926
  if (current === undefined || current.revision !== revision) {
3264
- return staleWrite(current === undefined ? null : roomDraftOf(current));
4927
+ return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
3265
4928
  }
3266
4929
  await local.discardRoomDraft(room, path);
3267
4930
  forgetRoomDraft(room, path);
3268
- publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
4931
+ publishChangeSet(changeSet(room, { drafts: [path] }));
3269
4932
  moveTo({ ...status, ...attention() });
3270
4933
  return { outcome: 'written', roomDraft: null };
3271
4934
  });
@@ -3281,29 +4944,39 @@ export function composeRuntime(options) {
3281
4944
  });
3282
4945
  },
3283
4946
  write(app, value) {
3284
- return exclusive(async () => {
3285
- const local = await openLocal();
3286
- const stored = structuredClone(value);
3287
- await local.present({ app, value: stored });
3288
- presentations.set(app, stored);
3289
- publishChangeSet(changeSet('', null, null, { presentation: app }));
3290
- });
4947
+ return keepPresentation(app, () => structuredClone(value));
4948
+ },
4949
+ edit(app, members) {
4950
+ return keepPresentation(app, (held) => Object.fromEntries([
4951
+ ...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(members, member)),
4952
+ ...Object.entries(structuredClone(members)).filter(([, value]) => value !== null),
4953
+ ]));
3291
4954
  },
3292
4955
  },
3293
4956
  save(room, named, writtenBy) {
3294
4957
  return exclusive(() => saveRoomDrafts(room, named, writtenBy));
3295
4958
  },
4959
+ saveParts(room, parts, writtenBy) {
4960
+ return exclusive(() => saveParts(room, parts, writtenBy));
4961
+ },
3296
4962
  history(locator, path, query) {
3297
4963
  return readHistory(locator, path, query);
3298
4964
  },
4965
+ resolve(room, path, destination) {
4966
+ return resolveReference(room, path, destination);
4967
+ },
3299
4968
  binaryFile: {
4969
+ part: readPart,
3300
4970
  read: readBinaryPath,
4971
+ link: linkBinaryPath,
3301
4972
  readReference: readBinaryReference,
3302
4973
  },
3303
- call(name, input) {
3304
- return invokeAction(name, input);
4974
+ async call(name, input) {
4975
+ await ready;
4976
+ const room = calledRooms(name, input).find((one) => locked(one));
4977
+ return room === undefined ? recoveredAction(name, input) : lockedRoom(room);
3305
4978
  },
3306
- reached(locator, roomVersion, path) {
4979
+ async reached(locator, roomVersion, path) {
3307
4980
  return path === undefined
3308
4981
  ? reachedBy(locator, roomVersion)
3309
4982
  : writtenAfter(locator, roomVersion, path);
@@ -3311,12 +4984,13 @@ export function composeRuntime(options) {
3311
4984
  log() {
3312
4985
  return journal.export();
3313
4986
  },
4987
+ cleanUp,
3314
4988
  persistence() {
3315
4989
  return persistence === 'granted';
3316
4990
  },
3317
4991
  async retry() {
3318
4992
  journal.record('retry');
3319
- unindexable.clear();
4993
+ unpreparable.clear();
3320
4994
  queueReadiness();
3321
4995
  resume();
3322
4996
  },
@@ -3346,13 +5020,17 @@ export function composeRuntime(options) {
3346
5020
  watchPlan,
3347
5021
  search: searched,
3348
5022
  watchReadiness,
5023
+ locked,
5024
+ lockedRooms,
5025
+ watchLocks,
5026
+ located,
3349
5027
  };
3350
5028
  return {
3351
5029
  md,
5030
+ seed,
3352
5031
  stop,
3353
5032
  lose: loseRooms,
3354
5033
  watch,
3355
- acquire,
3356
5034
  persisted(answer) {
3357
5035
  if (answer === persistence)
3358
5036
  return;
@@ -3360,6 +5038,7 @@ export function composeRuntime(options) {
3360
5038
  journal.record(`persistence ${answer}`);
3361
5039
  queueReadiness();
3362
5040
  },
5041
+ shown: screen,
3363
5042
  within: (reach, port) => narrowed(reachable, reach, port),
3364
5043
  };
3365
5044
  }