@agentshouse/mdruntime 0.1.0-ca68e8b4bbebc → 0.1.0-ca6edc1abd618

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