@agentshouse/mdruntime 0.1.0-c1db74d14e5fa → 0.1.0-c1ffd2bc60355

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