@agentshouse/mdruntime 0.1.0-cc5745cd9af96 → 0.1.0-ccbb89b0ca151

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 (225) hide show
  1. package/README.md +18 -15
  2. package/dist/adapter.d.ts +56 -32
  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 +30 -8
  11. package/dist/bootstrap.d.ts.map +1 -1
  12. package/dist/bootstrap.js +186 -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 +6 -16
  17. package/dist/change-set.d.ts.map +1 -1
  18. package/dist/change-set.js +11 -94
  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 +9 -5
  23. package/dist/client.d.ts.map +1 -1
  24. package/dist/client.js +80 -6
  25. package/dist/collection.d.ts +6 -20
  26. package/dist/collection.d.ts.map +1 -1
  27. package/dist/collection.js +31 -354
  28. package/dist/compile.d.ts +16 -0
  29. package/dist/compile.d.ts.map +1 -0
  30. package/dist/compile.js +765 -0
  31. package/dist/connection.d.ts +16 -9
  32. package/dist/connection.d.ts.map +1 -1
  33. package/dist/connection.js +2399 -952
  34. package/dist/conversation.d.ts +3 -3
  35. package/dist/conversation.d.ts.map +1 -1
  36. package/dist/conversation.js +80 -68
  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 -24
  46. package/dist/file.d.ts +2 -19
  47. package/dist/file.d.ts.map +1 -1
  48. package/dist/file.js +7 -122
  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 -4
  53. package/dist/host.d.ts.map +1 -1
  54. package/dist/host.js +45 -15
  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 +11 -9
  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 +124 -0
  64. package/dist/media.d.ts +8 -0
  65. package/dist/media.d.ts.map +1 -0
  66. package/dist/media.js +195 -0
  67. package/dist/memory.d.ts +2 -1
  68. package/dist/memory.d.ts.map +1 -1
  69. package/dist/memory.js +63 -22
  70. package/dist/messages.d.ts +28 -0
  71. package/dist/messages.d.ts.map +1 -0
  72. package/dist/messages.js +90 -0
  73. package/dist/model.d.ts.map +1 -1
  74. package/dist/model.js +1 -1
  75. package/dist/name.d.ts +4 -0
  76. package/dist/name.d.ts.map +1 -0
  77. package/dist/name.js +122 -0
  78. package/dist/observation.d.ts +12 -0
  79. package/dist/observation.d.ts.map +1 -1
  80. package/dist/observation.js +84 -0
  81. package/dist/opened.d.ts +22 -0
  82. package/dist/opened.d.ts.map +1 -0
  83. package/dist/opened.js +107 -0
  84. package/dist/outcome.d.ts +40 -5
  85. package/dist/outcome.d.ts.map +1 -1
  86. package/dist/outcome.js +36 -2
  87. package/dist/overlay.d.ts +1 -4
  88. package/dist/overlay.d.ts.map +1 -1
  89. package/dist/overlay.js +22 -148
  90. package/dist/query.d.ts +11 -1
  91. package/dist/query.d.ts.map +1 -1
  92. package/dist/query.js +13 -1
  93. package/dist/reach.d.ts +11 -3
  94. package/dist/reach.d.ts.map +1 -1
  95. package/dist/reach.js +198 -69
  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 -75
  107. package/dist/replica.d.ts +56 -39
  108. package/dist/replica.d.ts.map +1 -1
  109. package/dist/replica.js +277 -197
  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 +54 -13
  113. package/dist/rows.d.ts +38 -31
  114. package/dist/rows.d.ts.map +1 -1
  115. package/dist/rows.js +173 -101
  116. package/dist/save.d.ts +49 -14
  117. package/dist/save.d.ts.map +1 -1
  118. package/dist/save.js +352 -98
  119. package/dist/search.d.ts +11 -43
  120. package/dist/search.d.ts.map +1 -1
  121. package/dist/search.js +0 -213
  122. package/dist/shared.d.ts +4 -3
  123. package/dist/shared.d.ts.map +1 -1
  124. package/dist/shared.js +67 -34
  125. package/dist/store.d.ts +83 -95
  126. package/dist/store.d.ts.map +1 -1
  127. package/dist/store.js +317 -720
  128. package/dist/stored.d.ts +152 -0
  129. package/dist/stored.d.ts.map +1 -0
  130. package/dist/stored.js +607 -0
  131. package/dist/tables.d.ts +352 -0
  132. package/dist/tables.d.ts.map +1 -0
  133. package/dist/tables.js +624 -0
  134. package/dist/turn.d.ts +1 -1
  135. package/dist/turn.d.ts.map +1 -1
  136. package/dist/turn.js +31 -20
  137. package/dist/validation.d.ts +30 -0
  138. package/dist/validation.d.ts.map +1 -0
  139. package/dist/validation.js +162 -0
  140. package/dist/windows.d.ts +2 -6
  141. package/dist/windows.d.ts.map +1 -1
  142. package/dist/windows.js +30 -34
  143. package/dist/wire.d.ts +17 -1
  144. package/dist/wire.d.ts.map +1 -1
  145. package/dist/wire.js +5 -1
  146. package/dist/words.d.ts +19 -0
  147. package/dist/words.d.ts.map +1 -0
  148. package/dist/words.js +150 -0
  149. package/package.json +18 -19
  150. package/AGENTS.md +0 -299
  151. package/dist/chat/acts.d.ts +0 -33
  152. package/dist/chat/acts.d.ts.map +0 -1
  153. package/dist/chat/acts.js +0 -36
  154. package/dist/chat/context.d.ts +0 -3
  155. package/dist/chat/context.d.ts.map +0 -1
  156. package/dist/chat/context.js +0 -10
  157. package/dist/chat/earlier.d.ts +0 -10
  158. package/dist/chat/earlier.d.ts.map +0 -1
  159. package/dist/chat/earlier.js +0 -54
  160. package/dist/chat/input.d.ts +0 -9
  161. package/dist/chat/input.d.ts.map +0 -1
  162. package/dist/chat/input.js +0 -99
  163. package/dist/chat/kit.d.ts +0 -61
  164. package/dist/chat/kit.d.ts.map +0 -1
  165. package/dist/chat/kit.js +0 -123
  166. package/dist/chat/lasting.d.ts +0 -6
  167. package/dist/chat/lasting.d.ts.map +0 -1
  168. package/dist/chat/lasting.js +0 -19
  169. package/dist/chat/list.d.ts +0 -28
  170. package/dist/chat/list.d.ts.map +0 -1
  171. package/dist/chat/list.js +0 -103
  172. package/dist/chat/markdown.d.ts +0 -21
  173. package/dist/chat/markdown.d.ts.map +0 -1
  174. package/dist/chat/markdown.js +0 -246
  175. package/dist/chat/message.d.ts +0 -7
  176. package/dist/chat/message.d.ts.map +0 -1
  177. package/dist/chat/message.js +0 -202
  178. package/dist/chat/messages.d.ts +0 -24
  179. package/dist/chat/messages.d.ts.map +0 -1
  180. package/dist/chat/messages.js +0 -187
  181. package/dist/chat/name.d.ts +0 -6
  182. package/dist/chat/name.d.ts.map +0 -1
  183. package/dist/chat/name.js +0 -37
  184. package/dist/chat/opened.d.ts +0 -11
  185. package/dist/chat/opened.d.ts.map +0 -1
  186. package/dist/chat/opened.js +0 -17
  187. package/dist/chat/overlay.d.ts +0 -32
  188. package/dist/chat/overlay.d.ts.map +0 -1
  189. package/dist/chat/overlay.js +0 -107
  190. package/dist/chat/parts.d.ts +0 -57
  191. package/dist/chat/parts.d.ts.map +0 -1
  192. package/dist/chat/parts.js +0 -119
  193. package/dist/chat/pieces.d.ts +0 -62
  194. package/dist/chat/pieces.d.ts.map +0 -1
  195. package/dist/chat/pieces.js +0 -1110
  196. package/dist/chat/process.d.ts +0 -20
  197. package/dist/chat/process.d.ts.map +0 -1
  198. package/dist/chat/process.js +0 -32
  199. package/dist/chat/provider.d.ts +0 -64
  200. package/dist/chat/provider.d.ts.map +0 -1
  201. package/dist/chat/provider.js +0 -638
  202. package/dist/chat/question.d.ts +0 -9
  203. package/dist/chat/question.d.ts.map +0 -1
  204. package/dist/chat/question.js +0 -69
  205. package/dist/chat/questions.d.ts +0 -31
  206. package/dist/chat/questions.d.ts.map +0 -1
  207. package/dist/chat/questions.js +0 -111
  208. package/dist/chat/read.d.ts +0 -5
  209. package/dist/chat/read.d.ts.map +0 -1
  210. package/dist/chat/read.js +0 -13
  211. package/dist/chat/ready.d.ts +0 -34
  212. package/dist/chat/ready.d.ts.map +0 -1
  213. package/dist/chat/ready.js +0 -91
  214. package/dist/chat/steady.d.ts +0 -3
  215. package/dist/chat/steady.d.ts.map +0 -1
  216. package/dist/chat/steady.js +0 -26
  217. package/dist/chat/style.d.ts +0 -4
  218. package/dist/chat/style.d.ts.map +0 -1
  219. package/dist/chat/style.js +0 -302
  220. package/dist/chat.d.ts +0 -14
  221. package/dist/chat.d.ts.map +0 -1
  222. package/dist/chat.js +0 -8
  223. package/dist/read-side.d.ts +0 -27
  224. package/dist/read-side.d.ts.map +0 -1
  225. package/dist/read-side.js +0 -826
@@ -1,47 +1,42 @@
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
5
  import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
5
- import { createReadSide } from './read-side.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';
9
10
  import { capped, MAXIMUM_WINDOW } from './windows.js';
10
- import { bootstrapRoom } from './bootstrap.js';
11
- import { changeSet, changing, entering, mergeChangeSets } from './change-set.js';
11
+ import { bootstrapRoom, NOTHING_DONE } from './bootstrap.js';
12
+ import { changeSet, mergeChangeSets } from './change-set.js';
12
13
  import { createConversationStore, } from './conversation.js';
13
- import { queuedFile } from './change.js';
14
- 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';
15
16
  import { runtimeError } from './failure.js';
16
17
  import { createLog } from './log.js';
17
18
  import { binaryRecord } from './binary.js';
18
- import { absentBinary, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
19
+ import { attachedFile } from './file.js';
20
+ import { openedFiles } from './opened.js';
21
+ import { ABSENT_TARGET, absentBinary, attachAnswer, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, OFFLINE, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
19
22
  import { createRecovery } from './recovery.js';
20
23
  import { doorReads } from './reads.js';
21
- import { refreshRoom } from './refresh.js';
22
- import { buildBatch, joined, limitedIn, queuedDocuments, requeued, } from './save.js';
23
- import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, replicating, sameMembership, standing, } from './replica.js';
24
- import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
24
+ import { advancement } from './refresh.js';
25
+ import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
26
+ import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, sameMembership, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
27
+ import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
25
28
  import { same, saving, STARTING, transferring, } from './status.js';
26
- import { detectStorageLoss, nextGeneration } from './durability.js';
27
- import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
29
+ import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
30
+ import { readinessOf, } from './readiness.js';
28
31
  import { FencedMap } from './fence.js';
29
- import { tally } from './shared.js';
30
- import { inChunks, ROW_WEIGHT, stepped, yielded } from './turn.js';
32
+ import { sameBinary } from './overlay.js';
33
+ import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
31
34
  import { identityTime, roomDraftRevision } from './identity.js';
32
35
  import { beneath, normalize } from './paths.js';
33
36
  import { openRuntimeStore, } from './store.js';
37
+ import { changedCleanly, sameComposition, validatedCollection, } from './validation.js';
34
38
  import { unconfirmed } from './transfer.js';
35
39
  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.paths === null &&
42
- plan.hop === null &&
43
- plan.backlinks === null);
44
- }
45
40
  function within(root, path) {
46
41
  return root === '' || beneath(root, path);
47
42
  }
@@ -59,24 +54,28 @@ const OPENING_LANE = 'opening';
59
54
  const CONVERSATION_LANE = 'conversations';
60
55
  const ROOM_READS = 256;
61
56
  const ROOM_ATTEMPTS = 16;
57
+ const ERASE_BATCH = 512;
58
+ const DERIVE_BYTES = 1 << 23;
59
+ const DERIVE_FLOOR = 1 << 18;
60
+ // An index build aims at this much database work, so a Save, a query or the next part's progress waits behind at most one short build.
61
+ const DERIVE_AIM_MS = 250;
62
+ const WALK_BYTES = 1 << 20;
63
+ const QUERY_BYTES = 1 << 20;
64
+ const STAGE_BYTES = 1 << 20;
65
+ const STAGE_FLOOR = 1 << 16;
66
+ // A batch aims at this much database work, so a Save or a query waits behind at most one short write.
67
+ const STAGE_AIM_MS = 50;
68
+ const PARTS_HELD = 4;
69
+ const QUEUED_BYTES = 1 << 23;
70
+ // 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.
71
+ const ARRIVAL_REPORT_MS = 250;
72
+ const CLEAN_CHANGES = 2_000;
73
+ // A Room that locks offline stays readable this long after the connection drops, so a laptop waking or changing network keeps working while a disconnected device locks soon after.
74
+ const LOCK_GRACE_MS = 120_000;
62
75
  function operationOf(input) {
63
76
  const operation = input?.operation_id;
64
77
  return typeof operation === 'string' ? operation : null;
65
78
  }
66
- const outside = new Map();
67
- function outsideOf(roots) {
68
- const key = JSON.stringify(roots);
69
- const held = outside.get(key);
70
- if (held !== undefined)
71
- return held;
72
- const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
73
- const made = {
74
- documents: (path) => (markdownPath(path) ? counted(path) : 0),
75
- binaries: counted,
76
- };
77
- outside.set(key, made);
78
- return made;
79
- }
80
79
  function abandonedOn(signal) {
81
80
  return new Promise((resolve) => {
82
81
  if (signal.aborted)
@@ -88,11 +87,14 @@ function abandonedOn(signal) {
88
87
  function roomLane(locator) {
89
88
  return `room ${locator}`;
90
89
  }
90
+ function pendingKey(locator, id) {
91
+ return `${locator}\u0000${id}`;
92
+ }
91
93
  export function composeRuntime(options) {
92
94
  const localSaves = options.adapter.localSaves === true;
95
+ const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
93
96
  let authorization = options.authorization;
94
97
  let opening;
95
- let store;
96
98
  const fences = new Map();
97
99
  const isFenced = (locator) => fences.has(locator);
98
100
  let fencings = 0;
@@ -111,9 +113,9 @@ export function composeRuntime(options) {
111
113
  const rooms = new FencedMap(isFenced);
112
114
  const counted = new Map();
113
115
  const unreadable = new Set();
114
- const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
115
- const models = new FencedMap(isModelFenced);
116
- const liveModels = new FencedMap(isModelFenced);
116
+ const builtIn = builtInModel(roomTypes);
117
+ const models = new FencedMap(isFenced);
118
+ const liveModels = new FencedMap(isFenced);
117
119
  const invalidModels = new Set();
118
120
  const discovered = new Map();
119
121
  const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
@@ -121,23 +123,19 @@ export function composeRuntime(options) {
121
123
  const scopedRefusals = new Map();
122
124
  const sourceRefusals = new Map();
123
125
  const declarationRefusals = new Set();
124
- function modelKey(locator, attached) {
125
- return `${attached} ${locator}`;
126
+ function modelOf(locator) {
127
+ return models.get(locator) ?? NO_MODEL;
126
128
  }
127
- function modelOf(locator, attached = 'collection') {
128
- return models.get(modelKey(locator, attached)) ?? NO_MODEL;
129
- }
130
- function savedModel(locator, attached) {
131
- return invalidModels.has(modelKey(locator, attached))
129
+ function savedModel(locator) {
130
+ return invalidModels.has(locator)
132
131
  ? { status: 'unavailable', reason: 'revoked' }
133
- : modelOf(locator, attached);
132
+ : modelOf(locator);
133
+ }
134
+ function composed(record) {
135
+ return compositionOf(record, builtIn);
134
136
  }
135
137
  function compositionFor(record) {
136
- return compositionOf({
137
- ...record,
138
- model: savedModel(record.locator, 'collection'),
139
- selectionModel: savedModel(record.locator, 'typed-selection'),
140
- });
138
+ return composed({ ...record, model: savedModel(record.locator) });
141
139
  }
142
140
  let refusal = null;
143
141
  let storageLoss = null;
@@ -169,6 +167,7 @@ export function composeRuntime(options) {
169
167
  let roomReads = { generation, free: ROOM_READS, waiting: [] };
170
168
  const working = new Set();
171
169
  let mutations = Promise.resolve();
170
+ let gathering = null;
172
171
  const roomListeners = new Set();
173
172
  const refusalListeners = new Set();
174
173
  const storageLossListeners = new Set();
@@ -176,50 +175,93 @@ export function composeRuntime(options) {
176
175
  const queueSubscriptions = new Set();
177
176
  const roomDraftListeners = new Set();
178
177
  const presentationListeners = new Set();
179
- const readSide = createReadSide();
178
+ const liveQueries = createLiveQueries((plan, cover) => answeredPlan(plan, cover));
180
179
  let withdrawConversations;
181
180
  let currencyQueued = null;
182
181
  let socketOpen = false;
182
+ const confirmed = new Set();
183
+ let lockGrace = null;
184
+ const lockListeners = new Set();
183
185
  let serviceMode = 'open';
184
186
  let haltedRefusal = false;
185
187
  let openSignals = 0;
186
188
  const serviceModeListeners = new Set();
187
189
  let member = null;
188
190
  const memberListeners = new Set();
189
- const prepared = new Set();
190
- const projecting = new Set();
191
191
  const unpreparedDemand = new Set();
192
- const unindexable = new Set();
192
+ const unpreparable = new Set();
193
193
  let preparing = false;
194
194
  let targets = {};
195
195
  let persistence = options.shell.persistence;
196
- let capacity = null;
197
- const paused = new Set();
198
- let rechecking = null;
196
+ let full = false;
197
+ let probing = null;
198
+ let testing = null;
199
+ const database = {
200
+ async ask(request) {
201
+ try {
202
+ return await options.database.ask(request);
203
+ }
204
+ catch (error) {
205
+ if (refusedForSpace(error))
206
+ filled();
207
+ throw error;
208
+ }
209
+ },
210
+ };
199
211
  const readinessWatches = new Set();
200
212
  let readinessQueued = false;
201
213
  const catching = new Set();
202
214
  const transfers = new FencedMap(isFenced);
215
+ const installs = new Map();
216
+ const keptParts = new Map();
217
+ const arrivals = new Map();
218
+ const underived = new Map();
219
+ let arrivalReported = 0;
220
+ let onScreen = options.shell.onScreen;
221
+ const shownWaiters = [];
222
+ let partsFree = PARTS_HELD;
223
+ let queuedBytes = 0;
224
+ const partWaiters = [];
225
+ const stageBudget = {
226
+ bytes: STAGE_FLOOR * 4,
227
+ floor: STAGE_FLOOR,
228
+ ceiling: STAGE_BYTES,
229
+ aim: STAGE_AIM_MS,
230
+ };
231
+ const deriveBudget = {
232
+ bytes: DERIVE_FLOOR * 4,
233
+ floor: DERIVE_FLOOR,
234
+ ceiling: DERIVE_BYTES,
235
+ aim: DERIVE_AIM_MS,
236
+ };
237
+ let installWrites = Promise.resolve();
203
238
  const conversations = createConversationStore({
204
239
  agents: declaredAgents,
205
- documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
240
+ documents: (declaration, after) => rowsQuery(planOf({
241
+ rooms: [declaration.locator],
242
+ types: [declaration.type],
243
+ select: {
244
+ fields: [
245
+ declaration.identity,
246
+ declaration.name,
247
+ ...(declaration.retired === undefined ? [] : [declaration.retired]),
248
+ ].map((field) => `/${escapePointer(field)}`),
249
+ },
250
+ limit: MAXIMUM_WINDOW,
251
+ ...(after === undefined ? {} : { after }),
252
+ }, roomFacts())),
206
253
  call: (name, input) => invokeAction(name, input),
207
254
  durably,
208
255
  stream: () => options.adapter.conversations,
209
256
  publish: publishChangeSet,
210
257
  online: () => connectionOf() !== 'unreachable' && refusal === null,
258
+ full: () => full,
211
259
  closed: () => closed,
212
260
  unanswered: resendLater,
213
261
  expired: (operation) => pastHorizon(operation),
214
262
  });
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
263
  const journal = createLog();
264
+ const keptFiles = openedFiles(database, isFenced);
223
265
  let notifyReady = () => { };
224
266
  const ready = new Promise((resolve) => {
225
267
  notifyReady = resolve;
@@ -237,16 +279,36 @@ export function composeRuntime(options) {
237
279
  if (room.size === 0)
238
280
  roomDrafts.delete(locator);
239
281
  }
240
- function roomDraftOf(record) {
282
+ async function visibleOf(locator, paths) {
283
+ const held = replicas.get(locator);
284
+ if (held === undefined)
285
+ return undefined;
286
+ const local = await openLocal();
287
+ const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
288
+ return { ...slice, collection: collectionOf(slice.documents, composed(held.record)) };
289
+ }
290
+ function roomDraftOf(record, visible) {
241
291
  return structuredClone({
242
292
  room: record.locator,
243
293
  path: record.path,
244
294
  revision: record.revision,
245
295
  parts: record.parts,
246
- changedUnderneath: underneath(record.parts, record.path, replicas.get(record.locator)?.visible),
296
+ changedUnderneath: underneath(record.parts, record.path, visible),
247
297
  returned: record.returned,
248
298
  });
249
299
  }
300
+ async function roomDraftsOf(records) {
301
+ const rooms = new Map();
302
+ for (const record of records) {
303
+ const paths = rooms.get(record.locator) ?? [];
304
+ paths.push(record.path);
305
+ rooms.set(record.locator, paths);
306
+ }
307
+ const layers = new Map();
308
+ for (const [locator, paths] of rooms)
309
+ layers.set(locator, await visibleOf(locator, paths));
310
+ return records.map((record) => roomDraftOf(record, layers.get(record.locator)));
311
+ }
250
312
  function snapshotRoomDrafts() {
251
313
  const records = [...roomDrafts.values()].flatMap((room) => [...room.values()]);
252
314
  records.sort((left, right) => {
@@ -256,31 +318,61 @@ export function composeRuntime(options) {
256
318
  return left.path < right.path ? -1 : 1;
257
319
  return 0;
258
320
  });
259
- return records.map(roomDraftOf);
321
+ return roomDraftsOf(records);
260
322
  }
323
+ let draftsShown = Promise.resolve();
261
324
  function publishRoomDrafts(waitingOnly) {
262
325
  if (closed)
263
326
  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
- }
327
+ const snapshot = snapshotRoomDrafts();
328
+ draftsShown = draftsShown.then(async () => {
329
+ const value = await snapshot.catch(() => null);
330
+ if (value === null || closed)
331
+ return;
332
+ for (const listener of roomDraftListeners) {
333
+ if (waitingOnly && listener.ready)
334
+ continue;
335
+ listener.ready = true;
336
+ listener.notify(value);
337
+ }
338
+ });
271
339
  }
272
340
  function presentationOf(app) {
273
341
  const value = presentations.get(app);
274
342
  return value === undefined ? null : structuredClone(value);
275
343
  }
276
- function keepPresentation(app, next) {
277
- return exclusive(async () => {
278
- const local = await openLocal();
279
- const stored = next(presentations.get(app));
280
- await local.present({ app, value: stored });
281
- presentations.set(app, stored);
282
- publishChangeSet(changeSet('', null, null, { presentation: app }));
283
- });
344
+ async function writtenRoomDraft(room, path, revision, input, next) {
345
+ const parts = readParts(input);
346
+ if (path === '' || normalize(path) !== path)
347
+ throw runtimeError('room-draft-malformed');
348
+ const local = await openLocal();
349
+ const current = roomDrafts.get(room)?.get(path);
350
+ if ((current?.revision ?? null) !== revision) {
351
+ return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
352
+ }
353
+ const file = parts.file === undefined
354
+ ? undefined
355
+ : await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
356
+ const record = {
357
+ locator: room,
358
+ path,
359
+ revision: next,
360
+ parts: held(current?.parts, parts, file),
361
+ returned: current?.returned ?? null,
362
+ };
363
+ await local.keepRoomDraft(record, parts.file === undefined || file === undefined
364
+ ? []
365
+ : [{ key: file.localId, blob: parts.file.blob }]);
366
+ rememberRoomDraft(record);
367
+ publishChangeSet(changeSet(room, { drafts: [path] }));
368
+ return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
369
+ }
370
+ async function keptPresentation(app, next) {
371
+ const local = await openLocal();
372
+ const stored = next(presentations.get(app));
373
+ await local.present({ app, value: stored });
374
+ presentations.set(app, stored);
375
+ publishChangeSet(changeSet('', { presentation: app }));
284
376
  }
285
377
  function emitPresentation(app) {
286
378
  if (closed)
@@ -330,21 +422,69 @@ export function composeRuntime(options) {
330
422
  return { attentionRequired, reset };
331
423
  }
332
424
  function documentsIn(locator) {
333
- const replica = replicas.get(locator);
334
- if (replica === undefined)
335
- return 0;
336
- const roots = replica.confirmed.record.typedSelection?.roots ?? [];
337
- const weighed = outsideOf(roots);
338
- return (tally(replica.visible.documents, weighed.documents) +
339
- tally(replica.visible.binaries, weighed.binaries));
425
+ return replicas.get(locator)?.count ?? 0;
340
426
  }
341
427
  function listed() {
342
428
  return structuredClone([...rooms.values()].map((room) => {
343
429
  const documents = documentsIn(room.locator);
344
430
  counted.set(room.locator, documents);
345
- return { ...room, documents };
431
+ return locked(room.locator)
432
+ ? {
433
+ ...room,
434
+ model: modelTypes(NO_MODEL),
435
+ typedSelection: null,
436
+ documents: 0,
437
+ locked: true,
438
+ }
439
+ : { ...room, documents, locked: false };
346
440
  }));
347
441
  }
442
+ function locked(locator) {
443
+ return rooms.get(locator)?.replica.offline === 'locked' && !confirmed.has(locator);
444
+ }
445
+ function lockedRooms() {
446
+ return [...rooms.keys()].filter(locked).sort();
447
+ }
448
+ function relocked(before) {
449
+ const after = lockedRooms();
450
+ if (JSON.stringify(after) === JSON.stringify(before))
451
+ return;
452
+ journal.record(`locked ${after.length}`);
453
+ emitRooms();
454
+ for (const notify of [...lockListeners])
455
+ notify();
456
+ }
457
+ function confirmLocks(listed) {
458
+ if (!socketOpen)
459
+ return;
460
+ for (const room of listed) {
461
+ if (room.replica.admission === 'admitted')
462
+ confirmed.add(room.locator);
463
+ }
464
+ }
465
+ function connectionLost() {
466
+ if (lockGrace !== null || closed)
467
+ return;
468
+ lockGrace = setTimeout(() => {
469
+ lockGrace = null;
470
+ if (socketOpen || closed)
471
+ return;
472
+ const before = lockedRooms();
473
+ confirmed.clear();
474
+ relocked(before);
475
+ }, LOCK_GRACE_MS);
476
+ }
477
+ function connectionLive() {
478
+ if (lockGrace !== null)
479
+ clearTimeout(lockGrace);
480
+ lockGrace = null;
481
+ }
482
+ function watchLocks(notify) {
483
+ lockListeners.add(notify);
484
+ return () => {
485
+ lockListeners.delete(notify);
486
+ };
487
+ }
348
488
  function roomFacts(reach = null) {
349
489
  const held = [...rooms.values()]
350
490
  .filter((room) => holds(reach, room.locator))
@@ -352,7 +492,7 @@ export function composeRuntime(options) {
352
492
  const governed = governs(reach, room);
353
493
  const attached = [
354
494
  ...(governed.collection ? [modelOf(room.locator)] : []),
355
- ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
495
+ ...(governed.selection ? [builtIn] : []),
356
496
  ];
357
497
  return {
358
498
  locator: room.locator,
@@ -363,7 +503,7 @@ export function composeRuntime(options) {
363
503
  return { listed: roomsListed, rooms: held };
364
504
  }
365
505
  function emitRooms() {
366
- readSide.recover();
506
+ liveQueries.recover();
367
507
  queueReadiness();
368
508
  const value = listed();
369
509
  for (const listener of roomListeners)
@@ -402,17 +542,17 @@ export function composeRuntime(options) {
402
542
  for (const listener of statusListeners)
403
543
  listener(shown);
404
544
  }
405
- function entryOf(batch) {
545
+ function entryOf(locator, batch) {
406
546
  if (localSaves)
407
547
  return Object.freeze({ batch, state: 'local' });
408
- const state = pendings.get(batch.id);
548
+ const state = pendings.get(pendingKey(locator, batch.id));
409
549
  if (state?.accepted !== undefined) {
410
550
  return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
411
551
  }
412
552
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
413
553
  }
414
554
  function entriesOf(locator) {
415
- return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
555
+ return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map((batch) => entryOf(locator, batch));
416
556
  }
417
557
  function emitQueue(locator) {
418
558
  const entries = entriesOf(locator);
@@ -422,66 +562,42 @@ export function composeRuntime(options) {
422
562
  }
423
563
  }
424
564
  function applyChangeSet(set) {
425
- const held = {
426
- ...set,
427
- documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
428
- };
429
- const applied = readSide.apply(held, () => searchIndex.changed(held));
430
- if (applied instanceof Promise)
431
- void applied.catch(unpublished);
565
+ liveQueries.touched(set);
432
566
  }
433
567
  function unpublished(error) {
434
568
  journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
435
569
  }
436
- async function referenced(locator) {
437
- for (;;) {
438
- const record = replicas.get(locator)?.confirmed.record;
439
- if (record === undefined || closed)
440
- return;
441
- const held = { text: record.documents, binaries: record.binaries };
442
- const composition = compositionOf(record);
443
- if (referencesIndexed(held, composition))
444
- return;
445
- await indexReferrers(held, composition);
446
- }
570
+ function indexed(locator) {
571
+ return replicas.get(locator)?.indexed === true;
447
572
  }
448
573
  async function prepareReplica(locator) {
449
- await referenced(locator);
450
- const replica = replicas.get(locator);
451
- if (replica === undefined || closed || prepared.has(locator))
452
- return;
453
- const visible = replica.visible;
454
- const paths = [
455
- ...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
456
- ];
457
- const entered = [];
458
- projecting.add(locator);
459
- try {
460
- await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
461
- const current = replicas.get(locator);
462
- if (current === undefined || closed)
463
- return;
464
- const set = entering(locator, current, chunk);
465
- entered.push(set);
466
- return readSide.apply(set);
467
- });
468
- if (!replicas.has(locator) || closed)
574
+ for (;;) {
575
+ const held = replicas.get(locator);
576
+ if (held === undefined || closed || held.indexed)
469
577
  return;
470
- prepared.add(locator);
471
- }
472
- finally {
473
- projecting.delete(locator);
578
+ const key = held.record.key;
579
+ const done = await exclusive(async () => {
580
+ const local = await openLocal();
581
+ return timed(deriveBudget, () => local.rederive(key, deriveBudget.bytes));
582
+ });
583
+ const now = replicas.get(locator);
584
+ if (now?.record.key !== key)
585
+ continue;
586
+ if (!done)
587
+ continue;
588
+ replicas.set(locator, { ...now, indexed: true });
589
+ publishChangeSet(changeSet(locator, { documents: null }));
590
+ liveQueries.recover();
591
+ queueReadiness();
592
+ return;
474
593
  }
475
- conversations.changed(mergeChangeSets(entered));
476
- readSide.recover();
477
- queueReadiness();
478
594
  }
479
595
  function unprepared() {
480
- return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
596
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
481
597
  }
482
- function unbuilt(locator, error) {
483
- unindexable.add(locator);
484
- journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
598
+ function unpreparedBy(locator, error) {
599
+ unpreparable.add(locator);
600
+ journal.record(`preparation failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
485
601
  queueReadiness();
486
602
  }
487
603
  function prepare(locators = []) {
@@ -497,7 +613,7 @@ export function composeRuntime(options) {
497
613
  if (closed)
498
614
  return;
499
615
  unpreparedDemand.delete(waiting);
500
- await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
616
+ await prepareReplica(waiting).catch((error) => unpreparedBy(waiting, error));
501
617
  }
502
618
  }
503
619
  finally {
@@ -506,7 +622,11 @@ export function composeRuntime(options) {
506
622
  })();
507
623
  }
508
624
  function roomsOfReach(reach) {
509
- return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
625
+ return new Set([
626
+ ...reach.rooms,
627
+ ...reach.documents.map(roomOfKey),
628
+ ...reach.areas.flatMap((area) => (area.types.length === 0 ? [] : [area.room])),
629
+ ]);
510
630
  }
511
631
  function reachedRooms(plan) {
512
632
  const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
@@ -514,7 +634,9 @@ export function composeRuntime(options) {
514
634
  (reached === null
515
635
  ? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
516
636
  : [...reached]);
517
- return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
637
+ return [...new Set(named)]
638
+ .filter((locator) => (reached === null || reached.has(locator)) && !plan.locked.includes(locator))
639
+ .sort();
518
640
  }
519
641
  function awaited(locator) {
520
642
  return (!roomsListed ||
@@ -528,6 +650,7 @@ export function composeRuntime(options) {
528
650
  const pending = [];
529
651
  const coverage = [];
530
652
  const overlay = [];
653
+ const read = [];
531
654
  for (const locator of locators) {
532
655
  const replica = replicas.get(locator);
533
656
  if (replica === undefined) {
@@ -535,21 +658,22 @@ export function composeRuntime(options) {
535
658
  pending.push({ room: locator, awaits: 'acquisition' });
536
659
  continue;
537
660
  }
538
- if (!prepared.has(locator)) {
661
+ if (!replica.indexed) {
539
662
  pending.push({ room: locator, awaits: 'preparation' });
540
663
  continue;
541
664
  }
542
665
  coverage.push({
543
666
  room: locator,
544
- generation: replica.confirmed.record.transfer,
667
+ generation: replica.record.transfer,
545
668
  current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
546
669
  });
670
+ read.push({ locator, key: replica.record.key });
547
671
  for (const batch of replica.queue)
548
672
  overlay.push(batch.id);
549
673
  }
550
674
  if (pending.length > 0)
551
675
  return { pending };
552
- return { coverage: { rooms: coverage, indexes: [], overlay } };
676
+ return { coverage: { rooms: coverage, indexes: [], overlay }, rooms: read };
553
677
  }
554
678
  function awaitPreparation(cover) {
555
679
  if (!('pending' in cover))
@@ -567,7 +691,7 @@ export function composeRuntime(options) {
567
691
  return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
568
692
  return { listed: named.every((room) => replicas.has(room)), rooms: named };
569
693
  }
570
- function roomState(locator, search) {
694
+ function roomState(locator) {
571
695
  if (sourceRefusals.has(locator))
572
696
  return 'refused';
573
697
  const listedRoom = discovered.get(locator);
@@ -578,27 +702,17 @@ export function composeRuntime(options) {
578
702
  return 'unadmitted';
579
703
  if (!replicas.has(locator))
580
704
  return 'acquiring';
581
- if (!prepared.has(locator))
705
+ if (!indexed(locator))
582
706
  return 'preparing';
583
- if (search && !searchIndex.rooms().has(locator))
584
- return 'indexing';
585
707
  return 'complete';
586
708
  }
587
- function conversationCoverage(reach, search) {
709
+ function conversationCoverage(reach) {
588
710
  const declared = reach === null || reach.conversations;
589
711
  if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
590
712
  return 'undeclared';
591
713
  if (!conversations.known() || !conversations.current())
592
714
  return 'acquiring';
593
- if (!search)
594
- return 'complete';
595
- const unindexed = conversations
596
- .searchable()
597
- .filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
598
- if (unindexed.length === 0)
599
- return 'complete';
600
- index([], unindexed);
601
- return 'indexing';
715
+ return 'complete';
602
716
  }
603
717
  function connectionOf() {
604
718
  if (socketOpen)
@@ -618,6 +732,7 @@ export function composeRuntime(options) {
618
732
  return [
619
733
  {
620
734
  room,
735
+ name: discovered.get(room)?.name ?? room,
621
736
  file: typeof file === 'string' ? file : '',
622
737
  rule: held.code,
623
738
  location: typeof location === 'string' ? location : null,
@@ -625,34 +740,85 @@ export function composeRuntime(options) {
625
740
  ];
626
741
  });
627
742
  }
743
+ function installationOf(coverage) {
744
+ const rooms = { done: 0, total: 0 };
745
+ const parts = { done: 0, total: 0 };
746
+ const bytes = { done: 0, total: 0 };
747
+ const received = { done: 0, total: 0 };
748
+ let installing = 0;
749
+ let unlisted = false;
750
+ let listed = false;
751
+ let waiting = false;
752
+ for (const { room, state } of coverage) {
753
+ if (state === 'unadmitted')
754
+ continue;
755
+ rooms.total += 1;
756
+ if (state === 'complete')
757
+ rooms.done += 1;
758
+ const transfer = transfers.get(room);
759
+ const replica = replicas.get(room)?.record.inventory ?? null;
760
+ const inventory = transfer?.inventory ?? replica ?? installs.get(room) ?? null;
761
+ const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
762
+ if (underway)
763
+ installing += 1;
764
+ if (underway && inventory === null)
765
+ unlisted = true;
766
+ if (inventory !== null) {
767
+ listed = true;
768
+ const done = transfer?.done ?? (replica === null ? NOTHING_DONE : inventory);
769
+ parts.total += inventory.parts;
770
+ bytes.total += inventory.bytes;
771
+ parts.done += done.parts;
772
+ bytes.done += done.bytes;
773
+ received.total += inventory.bytes;
774
+ received.done += done.bytes;
775
+ for (const held of [keptParts.get(room), arrivals.get(room)]) {
776
+ for (const arrived of held?.values() ?? [])
777
+ received.done += arrived;
778
+ }
779
+ }
780
+ waiting ||= underway;
781
+ }
782
+ const step = installing === 0
783
+ ? null
784
+ : !listed
785
+ ? 'listing'
786
+ : unlisted || received.done < received.total
787
+ ? 'downloading'
788
+ : bytes.done < bytes.total
789
+ ? 'writing'
790
+ : 'replaying';
791
+ return {
792
+ step,
793
+ paused: waiting && !onScreen ? 'hidden' : null,
794
+ rooms,
795
+ parts,
796
+ bytes,
797
+ received,
798
+ };
799
+ }
628
800
  function readinessFor(port, reach) {
629
801
  const required = requiredRooms(reach);
630
802
  const search = port.search === 'required';
631
- const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
632
- const building = (state) => coverage
633
- .filter((room) => room.state === state && !unindexable.has(room.room))
803
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
804
+ const preparing = coverage
805
+ .filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
634
806
  .map(({ room }) => room);
635
- const preparing = building('preparing');
636
807
  if (preparing.length > 0)
637
808
  prepare(preparing);
638
- const indexing = building('indexing');
639
- if (indexing.length > 0)
640
- index(indexing, []);
641
809
  return readinessOf({
642
810
  code: port.code,
643
811
  coverage: {
644
812
  listed: required.listed,
645
813
  rooms: coverage,
646
- conversations: conversationCoverage(reach, search),
814
+ conversations: conversationCoverage(reach),
647
815
  search: search ? 'required' : 'unused',
648
816
  },
649
817
  persistence,
650
818
  connection: connectionOf(),
651
819
  house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
652
- capacity: capacity !== null &&
653
- coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
654
- ? capacity
655
- : port.capacity,
820
+ space: full ? 'full' : port.space,
821
+ installation: installationOf(coverage),
656
822
  refused: refusedIn(required.rooms),
657
823
  lost: storageLoss !== null,
658
824
  established: targets[port.target]?.established === true,
@@ -661,7 +827,8 @@ export function composeRuntime(options) {
661
827
  function establish(name) {
662
828
  targets = { ...targets, [name]: { established: true } };
663
829
  queueReadiness();
664
- void durably((local) => local.keepTargets(targets)).catch(() => {
830
+ probeEachMinute();
831
+ void durably((local) => local.fact('targets', targets)).catch(() => {
665
832
  journal.record(`target record failed ${name}`);
666
833
  });
667
834
  }
@@ -698,92 +865,51 @@ export function composeRuntime(options) {
698
865
  readinessWatches.delete(watch);
699
866
  };
700
867
  }
701
- function layerBytes(layer) {
702
- let bytes = 0;
703
- for (const [path, text] of Object.entries(layer.documents))
704
- bytes += (path.length + text.length) * 2;
705
- return bytes;
706
- }
707
- function grownBy(next, previous) {
708
- if (previous === undefined)
709
- return layerBytes(next);
710
- const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
711
- return layerBytes({ ...next, documents });
712
- }
713
- function exceeded(error) {
714
- return (typeof error === 'object' &&
715
- error !== null &&
716
- error.name === 'QuotaExceededError');
717
- }
718
- async function estimated() {
719
- const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
720
- return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
721
- }
722
- function pause(locator, cause) {
723
- paused.add(locator);
724
- capacity = cause;
725
- journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
868
+ function filled() {
869
+ if (full || closed)
870
+ return;
871
+ full = true;
872
+ journal.record('space full');
726
873
  queueReadiness();
727
- if (rechecking !== null)
874
+ probeEachMinute();
875
+ }
876
+ function installedTarget() {
877
+ return Object.values(targets).some((target) => target.established);
878
+ }
879
+ function probeEachMinute() {
880
+ if (!full || probing !== null || closed || !installedTarget())
728
881
  return;
729
- // A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
730
- rechecking = setInterval(() => {
731
- void recheck();
882
+ // 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.
883
+ probing = setInterval(() => {
884
+ void probed();
732
885
  }, 60_000);
733
886
  }
734
- async function recheck() {
735
- const held = capacity;
736
- if (held === null || closed)
737
- return;
738
- const available = await estimated().catch(() => null);
739
- if (available === null || available < (held.needed ?? 0))
740
- return;
741
- if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
742
- return;
743
- resume();
887
+ function probed() {
888
+ testing ??= database.ask({ kind: 'space.probe' }).then(() => {
889
+ testing = null;
890
+ spaceFreed();
891
+ return true;
892
+ }, () => {
893
+ testing = null;
894
+ return false;
895
+ });
896
+ return testing;
744
897
  }
745
- function resume() {
746
- if (rechecking !== null)
747
- clearInterval(rechecking);
748
- rechecking = null;
749
- if (paused.size === 0 && capacity === null)
898
+ function spaceFreed() {
899
+ if (!full)
750
900
  return;
751
- paused.clear();
752
- capacity = null;
753
- journal.record('capacity resumed');
901
+ full = false;
902
+ if (probing !== null)
903
+ clearInterval(probing);
904
+ probing = null;
905
+ unpreparable.clear();
906
+ journal.record('space freed');
754
907
  queueReadiness();
755
908
  if (!closed && authorization !== undefined)
756
909
  void sync('unstreamed');
757
910
  }
758
- async function reclaimed(local) {
759
- const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
760
- for (const locator of unreferenced) {
761
- await local.abandon(locator);
762
- transfers.delete(locator);
763
- }
764
- if (unreferenced.length > 0)
765
- journal.record(`capacity reclaimed ${unreferenced.length}`);
766
- return unreferenced.length > 0;
767
- }
768
- async function admitted(local, locator, needed) {
769
- if (paused.has(locator))
770
- return false;
771
- let available = await estimated().catch(() => null);
772
- if (available === null || available >= needed)
773
- return true;
774
- if (await reclaimed(local))
775
- available = await estimated().catch(() => null);
776
- if (available === null || available >= needed)
777
- return true;
778
- pause(locator, { cause: 'insufficient-space', needed, available });
779
- return false;
780
- }
781
- async function exceededOn(locator, bytes) {
782
- const available = await estimated().catch(() => null);
783
- pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
784
- }
785
911
  function seedRoomHeld(room, version) {
786
- return prepared.has(room) && reachedBy(room, version);
912
+ return indexed(room) && reachedBy(room, version);
787
913
  }
788
914
  function seedHeld(at) {
789
915
  return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
@@ -817,6 +943,75 @@ export function composeRuntime(options) {
817
943
  shown();
818
944
  }
819
945
  }
946
+ function sameCover(left, right) {
947
+ return JSON.stringify(left) === JSON.stringify(right);
948
+ }
949
+ async function coveredRows(plan, cover) {
950
+ for (;;) {
951
+ const before = cover();
952
+ if ('pending' in before)
953
+ return { pending: before };
954
+ const local = await openLocal();
955
+ const keys = before.rooms.map(({ key }) => key);
956
+ const changes = local.changes(keys);
957
+ const current = () => local.changes(keys) === changes;
958
+ const rows = await windowRows(local, plan, before.rooms, current);
959
+ const after = cover();
960
+ if (rows !== null && sameCover(before, after))
961
+ return { rows, cover: before, current };
962
+ }
963
+ }
964
+ async function windowRows(local, plan, rooms, unchanged) {
965
+ const first = await local.query(plan, rooms, QUERY_BYTES);
966
+ if (first.more === undefined || !('documents' in first)) {
967
+ const { more: _more, ...rows } = first;
968
+ return rows;
969
+ }
970
+ const documents = [...first.documents];
971
+ for (let more = first.more; more !== undefined;) {
972
+ if (!unchanged())
973
+ return null;
974
+ const page = await local.query({
975
+ ...plan,
976
+ after: { text: more, values: JSON.parse(more) },
977
+ limit: plan.limit === null ? null : plan.limit - documents.length,
978
+ total: false,
979
+ }, rooms, QUERY_BYTES);
980
+ if (!('documents' in page))
981
+ return null;
982
+ documents.push(...page.documents);
983
+ more = page.more;
984
+ if (more === undefined) {
985
+ return {
986
+ documents,
987
+ ...(page.next === undefined ? {} : { next: page.next }),
988
+ ...(first.total === undefined ? {} : { total: first.total }),
989
+ };
990
+ }
991
+ }
992
+ return null;
993
+ }
994
+ function keyedOf(rows, coverage) {
995
+ if ('groups' in rows) {
996
+ return {
997
+ result: { ...rows, coverage },
998
+ keys: rows.groups.map((group) => JSON.stringify(group.key)),
999
+ };
1000
+ }
1001
+ return {
1002
+ result: { ...rows, coverage },
1003
+ keys: rows.documents.map((document) => documentKey(document.room, document.path)),
1004
+ };
1005
+ }
1006
+ async function answeredPlan(plan, cover) {
1007
+ const answered = await coveredRows(plan, cover);
1008
+ if ('pending' in answered) {
1009
+ const pending = answered.pending;
1010
+ return { result: pending, keys: [], current: () => true };
1011
+ }
1012
+ const held = answered.cover;
1013
+ return { ...keyedOf(answered.rows, held.coverage), current: answered.current };
1014
+ }
820
1015
  async function rowsQuery(plan) {
821
1016
  const before = covered(plan);
822
1017
  awaitPreparation(before);
@@ -825,59 +1020,15 @@ export function composeRuntime(options) {
825
1020
  return seededResult(seeded).result;
826
1021
  if ('pending' in before)
827
1022
  return before;
828
- const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
829
- awaitPreparation(cover);
830
- if ('pending' in cover)
1023
+ const answered = await coveredRows(plan, () => {
1024
+ const cover = covered(plan);
1025
+ awaitPreparation(cover);
831
1026
  return cover;
832
- return { ...rows, coverage: cover.coverage };
833
- }
834
- async function indexConversation(conversation) {
835
- searchIndex.indexMessages(conversation, [], false);
836
- let after = null;
837
- for (;;) {
838
- const batch = await durably((local) => local.messagesAfter(conversation, after));
839
- if (closed)
840
- return;
841
- searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
842
- if (batch.length === 0)
843
- return;
844
- after = batch[batch.length - 1].identity;
845
- }
846
- }
847
- function indexNext() {
848
- const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
849
- if (room !== undefined) {
850
- roomsToIndex.delete(room);
851
- return () => prepared.has(room)
852
- ? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
853
- : undefined;
854
- }
855
- const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
856
- if (conversation === undefined)
857
- return null;
858
- conversationsToIndex.delete(conversation);
859
- return () => indexConversation(conversation);
860
- }
861
- function index(rooms, named) {
862
- for (const room of rooms)
863
- roomsToIndex.add(room);
864
- for (const conversation of named)
865
- conversationsToIndex.add(conversation);
866
- if (indexing)
867
- return;
868
- indexing = true;
869
- void (async () => {
870
- try {
871
- for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
872
- await yielded();
873
- await unit();
874
- queueReadiness();
875
- }
876
- }
877
- finally {
878
- indexing = false;
879
- }
880
- })().catch(() => { });
1027
+ });
1028
+ if ('pending' in answered)
1029
+ return answered.pending;
1030
+ const held = answered.cover;
1031
+ return { ...answered.rows, coverage: held.coverage };
881
1032
  }
882
1033
  function publishChangeSet(set) {
883
1034
  applyChangeSet(set);
@@ -887,7 +1038,10 @@ export function composeRuntime(options) {
887
1038
  for (const locator of set.queues)
888
1039
  emitQueue(locator);
889
1040
  if (set.drafts.length > 0 ||
890
- set.documents.some((change) => roomDrafts.get(change.locator)?.has(change.path) === true)) {
1041
+ set.documents.some((change) => {
1042
+ const held = roomDrafts.get(change.locator);
1043
+ return held !== undefined && (change.path === null || held.has(change.path));
1044
+ })) {
891
1045
  publishRoomDrafts(false);
892
1046
  }
893
1047
  for (const app of set.presentations)
@@ -900,34 +1054,43 @@ export function composeRuntime(options) {
900
1054
  if (reason === 'revoked-authorization') {
901
1055
  await exclusive(async () => {
902
1056
  const revoked = { status: 'unavailable', reason: 'revoked' };
1057
+ const records = [...replicas].map(([locator, held]) => ({
1058
+ locator,
1059
+ held,
1060
+ record: { ...held.record, model: revoked },
1061
+ }));
1062
+ let answer = null;
903
1063
  try {
904
1064
  const local = await openLocal();
905
- await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
1065
+ answer = await local.commit({
1066
+ rooms: records.map(({ locator, record }) => ({
1067
+ locator,
1068
+ key: record.key,
1069
+ record: storedRecord(record),
1070
+ composition: composed(record),
1071
+ })),
1072
+ });
906
1073
  }
907
1074
  catch {
908
1075
  journal.record('model revocation persistence failed');
909
1076
  }
910
- const keys = new Set(models.keys());
911
- for (const locator of rooms.keys()) {
912
- keys.add(modelKey(locator, 'collection'));
913
- keys.add(modelKey(locator, 'typed-selection'));
914
- }
1077
+ const keys = new Set([...models.keys(), ...rooms.keys()]);
915
1078
  for (const key of keys) {
916
1079
  invalidModels.add(key);
917
1080
  models.set(key, revoked);
918
1081
  liveModels.delete(key);
919
1082
  }
920
1083
  const changed = [];
921
- for (const [locator, room] of rooms) {
922
- const before = replicas.get(locator) ?? null;
923
- if (before !== null) {
924
- const record = {
925
- ...before.confirmed.record,
926
- model: revoked,
927
- selectionModel: revoked,
928
- };
929
- replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
930
- }
1084
+ for (const { locator, held, record } of records) {
1085
+ if (replicas.get(locator) !== held)
1086
+ continue;
1087
+ const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
1088
+ replicas.set(locator, { ...held, record, indexed });
1089
+ }
1090
+ for (const locator of [...rooms.keys()]) {
1091
+ const room = rooms.get(locator);
1092
+ if (room === undefined)
1093
+ continue;
931
1094
  rooms.set(locator, {
932
1095
  ...room,
933
1096
  model: modelTypes(revoked),
@@ -935,9 +1098,10 @@ export function composeRuntime(options) {
935
1098
  ? null
936
1099
  : { ...room.typedSelection, model: modelTypes(revoked) },
937
1100
  });
938
- changed.push(changeSet(locator, before, replicas.get(locator) ?? null, { room: true }));
1101
+ changed.push(changeSet(locator, { documents: null, room: true }));
939
1102
  }
940
1103
  publishChangeSet(mergeChangeSets(changed));
1104
+ prepare();
941
1105
  });
942
1106
  }
943
1107
  }
@@ -965,15 +1129,16 @@ export function composeRuntime(options) {
965
1129
  ? null
966
1130
  : {
967
1131
  roots: selection.roots,
968
- model: modelTypes(modelOf(room.locator, 'typed-selection')),
1132
+ model: modelTypes(builtIn),
969
1133
  },
970
1134
  providerOwnedPaths: room.providerOwnedPaths,
971
- roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
1135
+ roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
972
1136
  synchronizationAvailable,
973
1137
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
974
1138
  });
975
1139
  }
976
1140
  async function applyDiscovery(found, started, covered) {
1141
+ const lockedBefore = lockedRooms();
977
1142
  for (const room of found) {
978
1143
  const fence = fences.get(room.locator);
979
1144
  if (fence !== undefined && fence.erased && fence.at < started)
@@ -999,15 +1164,17 @@ export function composeRuntime(options) {
999
1164
  else
1000
1165
  unlistedKept(unlisted);
1001
1166
  for (const room of listed) {
1002
- const held = replicas.get(room.locator)?.confirmed.record;
1167
+ const held = replicas.get(room.locator)?.record;
1003
1168
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
1004
1169
  list(held, false);
1005
1170
  continue;
1006
1171
  }
1007
1172
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
1008
1173
  }
1174
+ confirmLocks(listed);
1009
1175
  roomsListed = true;
1010
1176
  emitRooms();
1177
+ relocked(lockedBefore);
1011
1178
  return listed;
1012
1179
  }
1013
1180
  function heldLocally(locator) {
@@ -1028,7 +1195,7 @@ export function composeRuntime(options) {
1028
1195
  }
1029
1196
  function unlistedKept(unlisted) {
1030
1197
  for (const locator of unlisted) {
1031
- const held = replicas.get(locator)?.confirmed.record;
1198
+ const held = replicas.get(locator)?.record;
1032
1199
  if (held !== undefined)
1033
1200
  list(held, false);
1034
1201
  else
@@ -1037,64 +1204,89 @@ export function composeRuntime(options) {
1037
1204
  }
1038
1205
  function fence(locators) {
1039
1206
  const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1040
- const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
1041
- room: true,
1042
- queue: true,
1207
+ const removed = fresh.map((locator) => ({
1208
+ locator,
1043
1209
  drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
1044
1210
  }));
1045
1211
  for (const locator of fresh) {
1046
1212
  fences.set(locator, { at: discoveries, erased: false });
1047
1213
  lastFenced.set(locator, ++fencings);
1214
+ confirmed.delete(locator);
1048
1215
  for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1049
- pendings.delete(batch.id);
1216
+ pendings.delete(pendingKey(locator, batch.id));
1050
1217
  replicas.delete(locator);
1051
1218
  queuesWithoutReplica.delete(locator);
1052
1219
  rooms.delete(locator);
1053
1220
  roomDrafts.delete(locator);
1054
- transfers.delete(locator);
1221
+ untransferred(locator);
1055
1222
  catching.delete(locator);
1056
1223
  discovered.delete(locator);
1057
1224
  announced.delete(locator);
1058
1225
  unreadable.delete(locator);
1059
1226
  stale.delete(locator);
1060
1227
  repairable.delete(locator);
1061
- unindexable.delete(locator);
1062
- paused.delete(locator);
1228
+ unpreparable.delete(locator);
1063
1229
  scopedRefusals.delete(locator);
1064
1230
  sourceRefusals.delete(locator);
1065
1231
  declarationRefusals.delete(locator);
1066
- for (const attached of ['collection', 'typed-selection']) {
1067
- const key = modelKey(locator, attached);
1068
- models.delete(key);
1069
- liveModels.delete(key);
1070
- invalidModels.delete(key);
1071
- }
1232
+ models.delete(locator);
1233
+ liveModels.delete(locator);
1234
+ invalidModels.delete(locator);
1072
1235
  journal.record(`room lost ${locator}`);
1073
1236
  }
1074
1237
  if (fresh.length === 0)
1075
1238
  return fresh;
1076
- publishChangeSet(mergeChangeSets(removed));
1077
- for (const locator of fresh)
1078
- prepared.delete(locator);
1079
- searchIndex.forget(fresh);
1080
- readSide.recover();
1239
+ publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, { documents: null, room: true, queue: true, drafts }))));
1240
+ liveQueries.recover();
1081
1241
  moveTo({ ...status, ...attention() });
1082
1242
  return fresh;
1083
1243
  }
1084
- async function erased(local, locators) {
1085
- await local.erase(locators);
1244
+ async function eraseFenced(locators) {
1086
1245
  for (const locator of locators) {
1246
+ while (await durably((local) => local.erase(locator, ERASE_BATCH)))
1247
+ await yielded();
1087
1248
  const held = fences.get(locator);
1088
1249
  if (held !== undefined)
1089
1250
  held.erased = true;
1090
1251
  }
1091
1252
  }
1253
+ let sweeping = false;
1254
+ function sweep() {
1255
+ if (sweeping || closed)
1256
+ return;
1257
+ sweeping = true;
1258
+ void (async () => {
1259
+ try {
1260
+ while (!closed && (await durably((local) => local.erase(null, ERASE_BATCH)))) {
1261
+ await yielded();
1262
+ }
1263
+ }
1264
+ catch {
1265
+ journal.record('erasure failed');
1266
+ }
1267
+ finally {
1268
+ sweeping = false;
1269
+ }
1270
+ })();
1271
+ }
1092
1272
  async function loseRooms(locators) {
1093
1273
  const fresh = fence(locators);
1094
- if (fresh.length > 0)
1095
- await durably((local) => erased(local, fresh));
1274
+ if (fresh.length === 0)
1275
+ return;
1276
+ await durably(async (local) => {
1277
+ await local.fence(fresh);
1278
+ await unpair(local);
1279
+ });
1280
+ await eraseFenced(fresh);
1281
+ }
1282
+ function retyped(persisted) {
1283
+ if (persisted.status === 'unavailable')
1284
+ return persisted;
1285
+ const { state } = attachedModel({ kind: 'model', snapshot: persisted.snapshot }, NO_MODEL, roomTypes);
1286
+ return state.status === 'unavailable' ? state : { ...persisted, types: state.types };
1096
1287
  }
1097
- function restored(key, persisted) {
1288
+ function restored(key, held) {
1289
+ const persisted = retyped(held);
1098
1290
  const prior = models.get(key);
1099
1291
  if (prior !== undefined &&
1100
1292
  prior.status !== 'unavailable' &&
@@ -1113,54 +1305,58 @@ export function composeRuntime(options) {
1113
1305
  return model;
1114
1306
  }
1115
1307
  async function hydrate(opened) {
1116
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
1117
- opened.all(),
1308
+ const [held, queues, kept, heldConversations, chatDrafts] = await Promise.all([
1309
+ opened.opened(),
1118
1310
  opened.queues(),
1119
1311
  opened.roomDrafts(),
1120
- opened.presentation(),
1121
1312
  opened.conversations(),
1122
1313
  opened.chatDrafts(),
1123
- opened.lastCompleted(),
1124
- opened.providerLimits(),
1125
- opened.capabilities(),
1126
- opened.member(),
1127
- opened.transfers(),
1128
- opened.targets(),
1129
1314
  ]);
1130
- targets = heldTargets;
1315
+ const records = [...held.rooms.values()];
1316
+ const counts = await opened.counts(records.map(({ key, typedSelection }) => ({ key, roots: typedSelection?.roots ?? [] })));
1317
+ const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1318
+ targets = held.targets;
1131
1319
  transfers.clear();
1132
- for (const record of staged)
1133
- transfers.set(record.locator, record);
1134
- limits = declared;
1135
- capabilities = named;
1136
- member = known;
1320
+ for (const [locator, record] of held.transfers)
1321
+ transfers.set(locator, record);
1322
+ arrivals.clear();
1323
+ keptParts.clear();
1324
+ for (const [locator, parts] of held.kept)
1325
+ keptParts.set(locator, new Map(parts));
1326
+ limits = held.providerLimits;
1327
+ capabilities = held.capabilities;
1328
+ member = held.member;
1137
1329
  roomDrafts.clear();
1138
1330
  for (const record of kept)
1139
1331
  rememberRoomDraft(record);
1140
1332
  presentations.clear();
1141
- for (const record of presented)
1333
+ for (const record of held.presentation)
1142
1334
  presentations.set(record.app, record.value);
1143
- conversations.hydrate(held, chatDrafts);
1144
- const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1335
+ conversations.hydrate(heldConversations, chatDrafts);
1145
1336
  pendings.clear();
1146
1337
  for (const queue of queues) {
1147
- for (const { batch, state } of queue.pending)
1148
- pendings.set(batch.id, state);
1338
+ for (const { batch, state } of queue.pending) {
1339
+ pendings.set(pendingKey(queue.locator, batch.id), state);
1340
+ }
1149
1341
  }
1150
- const previousReplicas = new Map(replicas);
1151
1342
  replicas.clear();
1152
1343
  queuesWithoutReplica.clear();
1153
- const replicated = new Set(records.map((record) => record.locator));
1154
1344
  for (const [locator, queue] of queuedBatches) {
1155
- if (!replicated.has(locator))
1345
+ if (!held.rooms.has(locator))
1156
1346
  queuesWithoutReplica.set(locator, queue);
1157
1347
  }
1158
- for (const record of records) {
1159
- const model = restored(modelKey(record.locator, 'collection'), record.model);
1160
- const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
1161
- const prior = previousReplicas.get(record.locator);
1162
- const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
1163
- replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
1348
+ records.forEach((stored, index) => {
1349
+ if (fences.has(stored.locator))
1350
+ return;
1351
+ const record = { ...stored, model: restored(stored.locator, stored.model) };
1352
+ replicas.set(record.locator, {
1353
+ record,
1354
+ queue: queuedBatches.get(record.locator) ?? [],
1355
+ overlaid: held.overlaid.get(record.key) ?? new Set(),
1356
+ count: counts[index] ?? 0,
1357
+ confirmed: 0,
1358
+ indexed: !held.unindexed.has(record.key),
1359
+ });
1164
1360
  const existing = rooms.get(record.locator);
1165
1361
  if (existing === undefined)
1166
1362
  list(record, false);
@@ -1170,20 +1366,21 @@ export function composeRuntime(options) {
1170
1366
  roomVersion: record.roomVersion,
1171
1367
  operations: record.operations,
1172
1368
  replica: record.replica,
1173
- model: modelTypes(model),
1369
+ model: modelTypes(record.model),
1174
1370
  typedSelection: record.typedSelection === null
1175
1371
  ? null
1176
- : {
1177
- roots: record.typedSelection.roots,
1178
- model: modelTypes(selectionModel),
1179
- },
1372
+ : { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
1180
1373
  });
1181
1374
  }
1182
- }
1375
+ });
1376
+ for (const key of held.orphans)
1377
+ await opened.release(key);
1378
+ if (held.erasing || held.orphans.length > 0)
1379
+ queueMicrotask(sweep);
1183
1380
  return {
1184
- lastCompleted,
1381
+ lastCompleted: held.lastCompleted,
1185
1382
  locators: new Set([...queuedBatches.keys(), ...replicas.keys()]),
1186
- read: mergeChangeSets([...new Set([...previousReplicas.keys(), ...replicas.keys()])].map((locator) => changeSet(locator, previousReplicas.get(locator) ?? null, replicas.get(locator) ?? null))),
1383
+ read: mergeChangeSets([...replicas.keys()].map((locator) => changeSet(locator, { documents: null }))),
1187
1384
  };
1188
1385
  }
1189
1386
  async function readState(opened) {
@@ -1202,8 +1399,7 @@ export function composeRuntime(options) {
1202
1399
  async function open() {
1203
1400
  if (closed)
1204
1401
  throw new Error('runtime closed');
1205
- const opened = await openRuntimeStore(isFenced);
1206
- store = opened;
1402
+ const opened = openRuntimeStore(isFenced, database);
1207
1403
  const interrupted = await opened.fences();
1208
1404
  for (const locator of interrupted) {
1209
1405
  fences.set(locator, { at: discoveries, erased: false });
@@ -1222,9 +1418,17 @@ export function composeRuntime(options) {
1222
1418
  emitStorageLoss();
1223
1419
  }
1224
1420
  const lastCompleted = await readState(opened);
1225
- if (interrupted.length > 0)
1226
- await erased(opened, interrupted);
1421
+ if (interrupted.length > 0) {
1422
+ queueMicrotask(() => {
1423
+ void eraseFenced(interrupted).catch(() => {
1424
+ journal.record('erasure failed');
1425
+ });
1426
+ });
1427
+ }
1227
1428
  moveTo({ ...status, lastCompleted, ...attention() });
1429
+ void exclusive(() => unpair(opened)).catch(() => {
1430
+ journal.record('unpair failed');
1431
+ });
1228
1432
  return opened;
1229
1433
  }
1230
1434
  function durably(work) {
@@ -1241,180 +1445,376 @@ export function composeRuntime(options) {
1241
1445
  return opening;
1242
1446
  }
1243
1447
  function exclusive(work) {
1448
+ gathering = null;
1244
1449
  const next = mutations.then(work, work);
1245
1450
  mutations = next.then(() => undefined, () => undefined);
1246
1451
  return next;
1247
1452
  }
1248
- function withdrawable(batch) {
1249
- const state = pendings.get(batch.id);
1250
- return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
1453
+ function gathered(key, input, merge, work) {
1454
+ const tail = gathering;
1455
+ if (tail !== null && tail.key === key) {
1456
+ const merged = merge(tail.input, input);
1457
+ if (merged !== null) {
1458
+ tail.input = merged;
1459
+ return tail.done;
1460
+ }
1461
+ }
1462
+ const entry = { key, input, done: Promise.resolve() };
1463
+ entry.done = exclusive(() => {
1464
+ if (gathering === entry)
1465
+ gathering = null;
1466
+ return work(entry.input);
1467
+ });
1468
+ gathering = entry;
1469
+ return entry.done;
1470
+ }
1471
+ function withdrawable(locator) {
1472
+ return (batch) => {
1473
+ const state = pendings.get(pendingKey(locator, batch.id));
1474
+ return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
1475
+ };
1476
+ }
1477
+ const REWRITE_LINKS = ['field', 'body'];
1478
+ const CHECKED_LINKS = ['field', 'section'];
1479
+ async function sliceAt(record, view, queue, extra = [], moved = []) {
1480
+ const local = await openLocal();
1481
+ const needed = new Set([...touchedBy(queue), ...extra]);
1482
+ const movedPaths = [...new Set([...movedBy(queue, record.locator), ...moved])];
1483
+ if (movedPaths.length > 0) {
1484
+ for (const { path } of await local.referrers(record.key, view, movedPaths, REWRITE_LINKS)) {
1485
+ needed.add(path);
1486
+ }
1487
+ }
1488
+ const paths = [...needed];
1489
+ return stepped(slicing(await local.entries(record.key, view, paths), paths));
1251
1490
  }
1252
- async function settle(local, locator, before, confirmed, queue, settling = {}) {
1491
+ async function landedWhole(locator, batch, confirmedLayer, current) {
1492
+ if (!landed(batch, confirmedLayer, current))
1493
+ return false;
1494
+ const partner = partnerOf(batch, locator);
1495
+ if (partner === null)
1496
+ return true;
1497
+ const other = replicas.get(partner);
1498
+ const half = other?.queue.find((one) => one.id === batch.id);
1499
+ if (other === undefined || half === undefined)
1500
+ return false;
1501
+ return landed(half, await sliceAt(other.record, 'confirmed', [half]));
1502
+ }
1503
+ function carriedBack(locator, unlanded, written) {
1504
+ const back = new Map();
1505
+ const taken = [];
1506
+ for (const batch of unlanded) {
1507
+ const between = batch.between;
1508
+ if (between === undefined)
1509
+ continue;
1510
+ const source = between.source.room;
1511
+ const destination = between.destination.room;
1512
+ for (const { path: to } of pendings.get(pendingKey(destination, batch.id))?.drafts ?? []) {
1513
+ const record = roomDrafts.get(destination)?.get(to);
1514
+ const path = between.moves.find((move) => move.to === to)?.path;
1515
+ if (record === undefined || path === undefined)
1516
+ continue;
1517
+ const [into, key] = source === locator ? [written, path] : [back, documentKey(source, path)];
1518
+ const existing = into.get(key) ?? roomDrafts.get(source)?.get(path);
1519
+ taken.push({ locator: destination, path: to });
1520
+ into.set(key, {
1521
+ locator: source,
1522
+ path,
1523
+ revision: roomDraftRevision(),
1524
+ parts: returnedInto(existing?.parts, record.parts),
1525
+ returned: existing?.returned ?? null,
1526
+ });
1527
+ }
1528
+ }
1529
+ const records = [...back.values()];
1530
+ const elsewhere = taken.filter((one) => one.locator !== locator);
1531
+ const rooms = new Set([...records, ...elsewhere].map((one) => one.locator));
1532
+ return {
1533
+ taken: taken.filter((one) => one.locator === locator).map(({ path }) => path),
1534
+ writes: [...rooms].map((room) => ({
1535
+ locator: room,
1536
+ roomDrafts: records.filter((record) => record.locator === room),
1537
+ consumedRoomDrafts: elsewhere.filter((one) => one.locator === room).map(({ path }) => path),
1538
+ })),
1539
+ changes: [...rooms].map((room) => changeSet(room, {
1540
+ drafts: [...records, ...elsewhere]
1541
+ .filter((one) => one.locator === room)
1542
+ .map(({ path }) => path),
1543
+ })),
1544
+ remember() {
1545
+ for (const one of taken)
1546
+ forgetRoomDraft(one.locator, one.path);
1547
+ for (const record of records)
1548
+ rememberRoomDraft(record);
1549
+ },
1550
+ };
1551
+ }
1552
+ function committedRoom(locator, answer) {
1553
+ const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
1554
+ if (!indexed)
1555
+ prepare([locator]);
1556
+ return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
1557
+ }
1558
+ function replayed(record, confirmedLayer, queue) {
1559
+ return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
1560
+ }
1561
+ async function overlayDiff(record, before, visible, touched) {
1562
+ const kept = before !== undefined && before.record.key === record.key
1563
+ ? before.overlaid
1564
+ : new Set();
1565
+ return {
1566
+ overlay: {
1567
+ set: await stepped(overlayOf(visible, touched)),
1568
+ cleared: [...kept].filter((path) => !touched.has(path)),
1569
+ },
1570
+ touched,
1571
+ };
1572
+ }
1573
+ async function settle(local, locator, before, record, queue, settling = {}) {
1574
+ return together(local, [await settlement(local, locator, before, record, queue, settling)]);
1575
+ }
1576
+ function unpairedIn(locator, batch) {
1577
+ if (!queueIn(locator).some((held) => held.id === batch.id))
1578
+ return false;
1579
+ if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
1580
+ return false;
1581
+ return !leavingIn(batch, locator) || withdrawable(locator)(batch);
1582
+ }
1583
+ async function together(local, settlements) {
1584
+ const settled = new Set(settlements.map(({ locator }) => locator));
1585
+ const all = [...settlements];
1586
+ for (let wave = settlements; wave.length > 0;) {
1587
+ const partnered = new Map();
1588
+ for (const { locator, left } of wave) {
1589
+ for (const batch of left) {
1590
+ const partner = partnerOf(batch, locator);
1591
+ if (partner === null || settled.has(partner) || !unpairedIn(partner, batch))
1592
+ continue;
1593
+ partnered.set(partner, [...(partnered.get(partner) ?? []), batch]);
1594
+ }
1595
+ }
1596
+ const partners = [];
1597
+ for (const [partner, batches] of partnered) {
1598
+ settled.add(partner);
1599
+ partners.push(await unpairing(local, partner, batches));
1600
+ }
1601
+ all.push(...partners);
1602
+ wave = partners;
1603
+ }
1604
+ const answer = await local.commit({
1605
+ rooms: all.flatMap(({ commit }) => commit.rooms),
1606
+ writes: all.flatMap(({ commit }) => commit.writes),
1607
+ });
1608
+ return mergeChangeSets(all.map((one) => one.apply(answer)));
1609
+ }
1610
+ async function settlement(local, locator, before, record, queue, settling = {}) {
1611
+ const view = settling.view ?? 'confirmed';
1253
1612
  const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
1254
- const { withdrawn, kept } = await stepped(standing(confirmed, queue, withdrawable, localSaves));
1613
+ const overrides = settling.changes;
1614
+ const read = await sliceAt(record, view, queue, [
1615
+ ...(before !== undefined && before.record.key === record.key ? before.overlaid : []),
1616
+ ...(overrides?.set.map(({ path }) => path) ?? []),
1617
+ ...(overrides?.removed ?? []),
1618
+ ]);
1619
+ const confirmedLayer = overrides === undefined
1620
+ ? read
1621
+ : await stepped(slicingWith(read, overrides.set, overrides.removed));
1622
+ const composition = composed(record);
1623
+ const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
1255
1624
  const founded = kept.filter((batch) => !queue.includes(batch));
1256
1625
  const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
1257
- const unqueued = withdrawn.map((batch) => ({
1258
- batch,
1259
- returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
1260
- }));
1626
+ const unqueued = [];
1627
+ for (const batch of withdrawn) {
1628
+ unqueued.push({ batch, landed: await landedWhole(locator, batch, confirmedLayer) });
1629
+ }
1261
1630
  const returning = [
1262
- ...[...unqueued].reverse(),
1263
- ...(settling.leaving === undefined ? [] : [settling.leaving]),
1631
+ ...[...unqueued].reverse().map(({ batch, landed: held }) => ({
1632
+ batch,
1633
+ returned: localSaves || held ? null : returnedBy('withdrawn'),
1634
+ })),
1635
+ ...(settling.leaving ?? []),
1264
1636
  ];
1265
1637
  const written = new Map();
1266
- for (const { batch, returned } of returning) {
1267
- if (returned === null)
1638
+ for (const { batch, returned: reason } of returning) {
1639
+ if (reason === null || batch.between?.destination.room === locator)
1268
1640
  continue;
1269
- for (const saved of pendings.get(batch.id)?.drafts ?? []) {
1641
+ for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
1270
1642
  const existing = written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path);
1271
1643
  written.set(saved.path, {
1272
1644
  locator,
1273
1645
  path: saved.path,
1274
1646
  revision: roomDraftRevision(),
1275
1647
  parts: returnedInto(existing?.parts, saved.parts),
1276
- returned,
1648
+ returned: reason,
1277
1649
  });
1278
1650
  }
1279
1651
  }
1280
- await local.keepAuthored({ locator, queue: kept, founded, roomDrafts: [...written.values()] });
1652
+ const carried = carriedBack(locator, [
1653
+ ...[...unqueued].reverse().flatMap(({ batch, landed: held }) => (held ? [] : [batch])),
1654
+ ...(settling.leaving ?? []).flatMap(({ batch, returned }) => returned === null ? [] : [batch]),
1655
+ ], written);
1656
+ const replayedLayer = await replayed(record, confirmedLayer, kept);
1657
+ const { overlay, touched } = await overlayDiff(record, before, replayedLayer.visible, replayedLayer.touched);
1658
+ const commit = {
1659
+ rooms: [
1660
+ {
1661
+ locator,
1662
+ key: record.key,
1663
+ record: storedRecord(record),
1664
+ roots: record.typedSelection?.roots ?? [],
1665
+ ...settling.commit,
1666
+ ...(overrides === undefined
1667
+ ? {}
1668
+ : { confirmed: { set: overrides.set, removed: overrides.removed } }),
1669
+ overlay,
1670
+ },
1671
+ ],
1672
+ writes: [
1673
+ {
1674
+ locator,
1675
+ queue: kept,
1676
+ founded,
1677
+ roomDrafts: [...written.values()],
1678
+ consumedRoomDrafts: carried.taken,
1679
+ },
1680
+ ...carried.writes,
1681
+ ],
1682
+ };
1281
1683
  const staying = new Set(kept.map((batch) => batch.id));
1282
- for (const batch of previousQueue) {
1283
- if (!staying.has(batch.id))
1284
- pendings.delete(batch.id);
1285
- }
1286
- for (const record of written.values())
1287
- rememberRoomDraft(record);
1288
- try {
1289
- await local.write({
1290
- next: confirmed.record,
1291
- previous: before?.confirmed.record ?? null,
1292
- ...(settling.stored === undefined ? {} : { stored: settling.stored }),
1293
- });
1294
- }
1295
- catch (error) {
1296
- const held = before === undefined ? null : await stepped(replicating(before.confirmed, kept, before));
1297
- const set = await stepped(changing(locator, before ?? null, held, {
1298
- queue: queueChanged,
1299
- drafts: [...written.keys()],
1300
- }));
1301
- if (held !== null)
1302
- replicas.set(locator, held);
1303
- else if (kept.length > 0)
1304
- queuesWithoutReplica.set(locator, kept);
1305
- else
1684
+ return {
1685
+ locator,
1686
+ left: previousQueue.filter((batch) => !staying.has(batch.id)),
1687
+ commit,
1688
+ apply: (answer) => {
1689
+ for (const batch of previousQueue) {
1690
+ if (!staying.has(batch.id))
1691
+ pendings.delete(pendingKey(locator, batch.id));
1692
+ }
1693
+ for (const draft of written.values())
1694
+ rememberRoomDraft(draft);
1695
+ carried.remember();
1696
+ const drafts = [...written.keys(), ...carried.taken];
1306
1697
  queuesWithoutReplica.delete(locator);
1307
- publishChangeSet(set);
1308
- moveTo({ ...status, ...attention() });
1309
- throw error;
1310
- }
1311
- const changed = { room: settling.room, queue: queueChanged, drafts: [...written.keys()] };
1312
- const next = await stepped(replicating(confirmed, kept, before));
1313
- const set = await stepped(changing(locator, before ?? null, next, changed));
1314
- return () => {
1315
- queuesWithoutReplica.delete(locator);
1316
- for (const { batch, returned } of unqueued) {
1317
- const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
1318
- journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1319
- }
1320
- replicas.set(locator, next);
1321
- return heldChange(locator, before ?? null, next, set, changed);
1698
+ for (const { batch, landed: held } of unqueued) {
1699
+ const outcome = localSaves ? 'dropped' : held ? 'landed' : 'withdrawn';
1700
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1701
+ }
1702
+ const changed = answer.changed[locator];
1703
+ const confirmedChanged = settling.commit?.published !== undefined ||
1704
+ settling.commit?.refreshed === true ||
1705
+ overrides !== undefined;
1706
+ const { count, indexed } = committedRoom(locator, answer);
1707
+ replicas.set(locator, {
1708
+ record,
1709
+ queue: kept,
1710
+ overlaid: touched,
1711
+ count,
1712
+ confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
1713
+ indexed,
1714
+ });
1715
+ return mergeChangeSets([
1716
+ changeSet(locator, {
1717
+ documents: changed === undefined ? [] : changed,
1718
+ room: settling.room,
1719
+ queue: queueChanged,
1720
+ drafts,
1721
+ }),
1722
+ ...carried.changes,
1723
+ ]);
1724
+ },
1322
1725
  };
1323
1726
  }
1324
- function heldChange(locator, before, next, set, changed) {
1325
- const held = replicas.get(locator) ?? null;
1326
- return held === next ? set : changeSet(locator, before, held, changed);
1327
- }
1328
1727
  function adoptModels(locator, record) {
1329
- for (const [attached, model] of [
1330
- ['collection', record.model],
1331
- ['typed-selection', record.selectionModel],
1332
- ]) {
1333
- const key = modelKey(locator, attached);
1334
- models.set(key, model);
1335
- invalidModels.delete(key);
1336
- if (model.status === 'current')
1337
- liveModels.set(key, JSON.stringify(model.snapshot));
1338
- else if (model.status === 'unavailable')
1339
- liveModels.delete(key);
1340
- }
1728
+ const model = record.model;
1729
+ models.set(locator, model);
1730
+ invalidModels.delete(locator);
1731
+ if (model.status === 'current')
1732
+ liveModels.set(locator, JSON.stringify(model.snapshot));
1733
+ else if (model.status === 'unavailable')
1734
+ liveModels.delete(locator);
1341
1735
  }
1342
1736
  function remodel(locator, answers, held) {
1343
- const taken = (answer, previous, attached) => {
1344
- if (answer === undefined)
1345
- return previous;
1346
- const { state, refused } = attachedModel(answer, previous);
1347
- if (refused !== null) {
1348
- const where = refused.location === undefined ? '' : ` ${refused.location}`;
1349
- journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
1350
- }
1351
- models.set(modelKey(locator, attached), state);
1352
- return state;
1353
- };
1354
- return {
1355
- collection: taken(answers?.collection, held.collection, 'collection'),
1356
- selection: taken(answers?.selection, held.selection, 'typed-selection'),
1357
- };
1358
- }
1359
- function publish(local, room, confirmed, landing, live) {
1360
- const { base, stored } = landing;
1361
- return exclusive(async () => {
1362
- if (stored !== undefined) {
1363
- await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
1364
- }
1737
+ const answer = answers?.collection;
1738
+ if (answer === undefined)
1739
+ return held;
1740
+ const { state, refused } = attachedModel(answer, held.collection, roomTypes);
1741
+ if (refused !== null) {
1742
+ const where = refused.location === undefined ? '' : ` ${refused.location}`;
1743
+ journal.record(`model invalid ${locator} collection ${refused.identity} ${refused.rule}${where}`);
1744
+ }
1745
+ models.set(locator, state);
1746
+ return { collection: state };
1747
+ }
1748
+ function publish(local, room, record, landing, live) {
1749
+ const landed = exclusive(async () => {
1365
1750
  if (!live()) {
1366
1751
  caughtUpTo(room.locator);
1367
1752
  return true;
1368
1753
  }
1369
1754
  const prior = replicas.get(room.locator);
1370
- if (base !== undefined &&
1371
- (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
1372
- prior.confirmed.record.revisions !== base.record.revisions)) {
1755
+ if (landing.kind === 'refreshed' &&
1756
+ (prior === undefined || prior.record.key !== record.key || prior.confirmed !== landing.base)) {
1757
+ await local.unrefresh(record.key);
1373
1758
  return false;
1374
1759
  }
1375
- adoptModels(room.locator, confirmed.record);
1760
+ adoptModels(room.locator, record);
1376
1761
  const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
1377
- const roomVersion = confirmed.record.roomVersion;
1762
+ const roomVersion = record.roomVersion;
1378
1763
  const accepted = queue.filter((batch) => {
1379
- const accepted = pendings.get(batch.id)?.accepted;
1380
- return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
1764
+ const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
1765
+ return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
1381
1766
  });
1382
- let settled;
1767
+ let set;
1383
1768
  try {
1384
- settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
1769
+ set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
1770
+ ? {
1771
+ room: true,
1772
+ view: 'refreshed',
1773
+ commit: { refreshed: true, composition: composed(record) },
1774
+ }
1775
+ : {
1776
+ room: true,
1777
+ view: landing.refreshed ? 'refreshed' : 'confirmed',
1778
+ commit: {
1779
+ published: { from: prior?.record.key ?? null },
1780
+ composition: composed(record),
1781
+ ...(landing.refreshed ? { refreshed: true } : {}),
1782
+ },
1783
+ });
1385
1784
  }
1386
1785
  catch (error) {
1387
- if (!exceeded(error))
1786
+ if (!refusedForSpace(error))
1388
1787
  throw error;
1389
- await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
1390
1788
  caughtUpTo(room.locator);
1391
1789
  return true;
1392
1790
  }
1393
- const set = settled();
1394
- if (stored !== undefined) {
1395
- transfers.delete(room.locator);
1791
+ if (landing.kind === 'bootstrapped') {
1792
+ untransferred(room.locator);
1396
1793
  catching.delete(room.locator);
1794
+ sweep();
1397
1795
  }
1398
1796
  for (const batch of accepted)
1399
- pendings.delete(batch.id);
1797
+ pendings.delete(pendingKey(room.locator, batch.id));
1400
1798
  stale.delete(room.locator);
1401
- unindexable.delete(room.locator);
1799
+ unpreparable.delete(room.locator);
1402
1800
  if (unreadable.delete(room.locator))
1403
1801
  scopedRefusals.delete(room.locator);
1404
1802
  sourceRefusals.delete(room.locator);
1405
1803
  declarationRefusals.delete(room.locator);
1406
1804
  list(room, true);
1407
- if (prior === undefined)
1408
- prepared.add(room.locator);
1409
1805
  publishChangeSet(set);
1806
+ await unpair(local);
1410
1807
  moveTo({ ...status, ...attention() });
1411
1808
  options.adapter.roomStream?.published(room.locator, roomVersion);
1809
+ if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1810
+ submitLater(room.locator);
1811
+ }
1412
1812
  return true;
1413
1813
  });
1814
+ return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
1414
1815
  }
1415
1816
  async function savedRoom(locator) {
1416
1817
  const local = await openLocal();
1417
- await referenced(locator);
1418
1818
  const replica = replicas.get(locator);
1419
1819
  const room = rooms.get(locator);
1420
1820
  if (replica === undefined || room === undefined) {
@@ -1444,13 +1844,19 @@ export function composeRuntime(options) {
1444
1844
  }
1445
1845
  const target = await savedRoom(locator);
1446
1846
  const saved = [];
1847
+ const files = [];
1848
+ const filed = read.filter(({ parts }) => parts.file !== undefined).map(({ path }) => path);
1849
+ const visible = filed.length === 0 ? undefined : await visibleOf(locator, filed);
1447
1850
  for (const { path, parts } of read) {
1448
1851
  const file = parts.file === undefined
1449
1852
  ? undefined
1450
- : await heldFile(parts.file.blob, binaryBase(target.replica.visible, path));
1853
+ : await heldFile(parts.file.blob, binaryBase(visible, path));
1854
+ if (parts.file !== undefined && file !== undefined) {
1855
+ files.push({ key: file.localId, blob: parts.file.blob });
1856
+ }
1451
1857
  saved.push({ path, parts: held(undefined, parts, file) });
1452
1858
  }
1453
- return committed(locator, target, saved, new Set(), writtenBy);
1859
+ return committed(locator, target, saved, new Set(), writtenBy, files);
1454
1860
  }
1455
1861
  function carriedDrafts(locator, saved, consumed) {
1456
1862
  const holding = roomDrafts.get(locator);
@@ -1464,13 +1870,355 @@ export function composeRuntime(options) {
1464
1870
  return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
1465
1871
  }));
1466
1872
  }
1467
- async function committed(locator, { local, replica, room }, saved, consumed, writtenBy) {
1468
- const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
1873
+ async function savingIn(replica, changes, extra, leaving = []) {
1874
+ const record = replica.record;
1875
+ const local = await openLocal();
1876
+ const paths = [
1877
+ ...extra,
1878
+ ...changes.flatMap((change) => change.kind === 'move' ? [change.path, change.to] : [change.path]),
1879
+ ];
1880
+ const moved = [
1881
+ ...leaving,
1882
+ ...changes.flatMap((change) => (change.kind === 'move' ? [change.path] : [])),
1883
+ ];
1884
+ const removed = changes.flatMap((change) => (change.kind === 'remove' ? [change.path] : []));
1885
+ const referring = removed.length === 0
1886
+ ? []
1887
+ : (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
1888
+ const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
1889
+ const { visible } = await replayed(record, confirmedLayer, replica.queue);
1890
+ const composition = composed(record);
1891
+ return {
1892
+ record,
1893
+ confirmed: confirmedLayer,
1894
+ visible,
1895
+ *visibleOver(queue) {
1896
+ return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
1897
+ },
1898
+ };
1899
+ }
1900
+ async function unmodeledIn(record) {
1901
+ const composition = compositionFor(record);
1902
+ if (available(composition))
1903
+ return false;
1904
+ const local = await openLocal();
1905
+ const roots = record.typedSelection?.roots ?? [];
1906
+ return local.typedWithin(record.key, 'visible', roots, false);
1907
+ }
1908
+ function textDiffers(left, right) {
1909
+ return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
1910
+ !sameBinary(left?.binary ?? undefined, right?.binary ?? undefined));
1911
+ }
1912
+ function textOf(entry) {
1913
+ if (entry === null)
1914
+ return undefined;
1915
+ return entry.binary !== null ? null : entry.content;
1916
+ }
1917
+ function walkOf(local, key, view) {
1918
+ return async (after) => {
1919
+ const page = await local.walk(key, view, after, WALK_BYTES);
1920
+ return {
1921
+ entries: page.entries.map(({ path, content, binary }) => ({
1922
+ path,
1923
+ content: binary !== null ? null : content,
1924
+ })),
1925
+ next: page.next,
1926
+ };
1927
+ };
1928
+ }
1929
+ function lookupOf(local, key, view) {
1930
+ return async (paths) => {
1931
+ if (paths.length === 0)
1932
+ return new Map();
1933
+ return new Map((await local.typed(key, view, paths)).map(({ path, type, binary }) => [
1934
+ path,
1935
+ { type: type ?? undefined, binary },
1936
+ ]));
1937
+ };
1938
+ }
1939
+ function declaredOf(entry) {
1940
+ if (entry.binary !== null)
1941
+ return { type: undefined, binary: true };
1942
+ const read = parseFrontmatter(entry.content ?? '');
1943
+ return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
1944
+ }
1945
+ function overriddenWalk(walk, overrides) {
1946
+ const sorted = [...overrides.keys()].sort();
1947
+ const beyond = (path) => {
1948
+ if (path === null)
1949
+ return 0;
1950
+ let low = 0;
1951
+ let high = sorted.length;
1952
+ while (low < high) {
1953
+ const middle = (low + high) >>> 1;
1954
+ if (sorted[middle] <= path)
1955
+ low = middle + 1;
1956
+ else
1957
+ high = middle;
1958
+ }
1959
+ return low;
1960
+ };
1961
+ return async (after) => {
1962
+ const page = await walk(after);
1963
+ const end = page.next;
1964
+ const added = sorted
1965
+ .slice(beyond(after), end === null ? sorted.length : beyond(end))
1966
+ .flatMap((path) => {
1967
+ const entry = overrides.get(path);
1968
+ return entry === null || entry === undefined
1969
+ ? []
1970
+ : [{ path, content: entry.binary !== null ? null : entry.content }];
1971
+ });
1972
+ const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
1973
+ return { entries, next: end };
1974
+ };
1975
+ }
1976
+ function overriddenLookup(lookup, overrides) {
1977
+ return async (paths) => {
1978
+ const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
1979
+ for (const path of paths) {
1980
+ const entry = overrides.get(path);
1981
+ if (entry === undefined)
1982
+ continue;
1983
+ if (entry === null)
1984
+ known.delete(path);
1985
+ else
1986
+ known.set(path, declaredOf(entry));
1987
+ }
1988
+ return known;
1989
+ };
1990
+ }
1991
+ function validatorOf(record, confirmedLayer) {
1992
+ return async (resulting, touched) => {
1993
+ const composition = compositionFor(record);
1994
+ const local = await openLocal();
1995
+ const changed = [];
1996
+ const overrides = new Map();
1997
+ const pace = paced();
1998
+ for (const path of new Set([...confirmedLayer.held, ...touched])) {
1999
+ await pace(ROW_WEIGHT);
2000
+ const was = entryAt(confirmedLayer, path);
2001
+ const now = entryAt(resulting, path);
2002
+ if (!textDiffers(was, now))
2003
+ continue;
2004
+ overrides.set(path, now);
2005
+ if (textOf(was) !== textOf(now))
2006
+ changed.push({ path, was: textOf(was), now: textOf(now) });
2007
+ }
2008
+ const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
2009
+ const clean = available(composition) &&
2010
+ changed.length <= CLEAN_CHANGES &&
2011
+ (await changedCleanly(changed, composed(record), composition, {
2012
+ lookup,
2013
+ texts: textsOf(local, record.key, 'confirmed'),
2014
+ referrers: referrersOf(local, record.key, 'confirmed'),
2015
+ }));
2016
+ if (clean)
2017
+ return { verdict: 'accept' };
2018
+ return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
2019
+ };
2020
+ }
2021
+ function textsOf(local, key, view) {
2022
+ return async (paths) => new Map((await local.entries(key, view, paths)).map(({ path, content, binary }) => [
2023
+ path,
2024
+ binary !== null ? null : content,
2025
+ ]));
2026
+ }
2027
+ function referrersOf(local, key, view) {
2028
+ return async (targets) => [
2029
+ ...new Set((await local.referrers(key, view, targets, CHECKED_LINKS)).map(({ path }) => path)),
2030
+ ];
2031
+ }
2032
+ async function sideOf(locator, replica, room, saving) {
2033
+ const record = replica.record;
2034
+ return {
2035
+ room: locator,
2036
+ handle: room.handle,
2037
+ baseRoomVersion: record.roomVersion,
2038
+ composition: compositionFor(record),
2039
+ operations: room.operations,
2040
+ providerOwnedPaths: room.providerOwnedPaths,
2041
+ visible: saving.visible,
2042
+ queued: queuedDocuments(replica.queue),
2043
+ unmodeled: await unmodeledIn(record),
2044
+ validate: validatorOf(record, saving.confirmed),
2045
+ };
2046
+ }
2047
+ function movedInto(locator, saved) {
2048
+ const into = saved.find(({ parts }) => parts.move?.room !== undefined && parts.move.room !== locator);
2049
+ return into?.parts.move?.room ?? null;
2050
+ }
2051
+ async function saveCommit(local, files, rooms) {
2052
+ const touched = new Map();
2053
+ const commits = [];
2054
+ for (const { locator, saving, before, queue } of rooms) {
2055
+ const replayedLayer = await replayed(saving.record, saving.confirmed, queue);
2056
+ const diff = await overlayDiff(saving.record, before, replayedLayer.visible, replayedLayer.touched);
2057
+ touched.set(locator, diff.touched);
2058
+ commits.push({
2059
+ locator,
2060
+ key: saving.record.key,
2061
+ roots: saving.record.typedSelection?.roots ?? [],
2062
+ overlay: diff.overlay,
2063
+ });
2064
+ }
2065
+ try {
2066
+ const answer = await local.commit({
2067
+ rooms: commits,
2068
+ writes: rooms.map(({ write }) => write),
2069
+ files,
2070
+ whole: true,
2071
+ });
2072
+ return { answer, touched };
2073
+ }
2074
+ catch {
2075
+ return null;
2076
+ }
2077
+ }
2078
+ function savedReplica(locator, before, queue, touched, answer) {
2079
+ replicas.set(locator, {
2080
+ ...before,
2081
+ queue,
2082
+ overlaid: touched,
2083
+ count: answer.counts[locator] ?? before.count,
2084
+ });
2085
+ }
2086
+ async function committedMove(locator, destination, { local, replica, room }, saved, consumed, writtenBy, files) {
2087
+ const mixed = saved.find(({ parts }) => Object.keys(parts).length !== 1 || parts.move?.room !== destination);
2088
+ if (mixed !== undefined) {
2089
+ return { outcome: 'refused', code: 'mixed-room-move', retryable: false, file: mixed.path };
2090
+ }
2091
+ const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
2092
+ const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
2093
+ const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, sourceSaving.visible)));
1469
2094
  if (changed.length > 0)
1470
2095
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
1471
- const parts = saved.map(({ path, parts }) => changesOf(path, parts));
1472
- const changes = parts.flatMap(({ changes }) => changes);
1473
- const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
2096
+ const target = replicas.get(destination);
2097
+ const listedRoom = rooms.get(destination);
2098
+ if (target === undefined || listedRoom === undefined) {
2099
+ throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
2100
+ }
2101
+ const arriving = await savingIn(target, [], moves.map(({ to }) => to));
2102
+ const built = await buildMove({
2103
+ source: await sideOf(locator, replica, room, sourceSaving),
2104
+ destination: await sideOf(destination, target, listedRoom, arriving),
2105
+ moves,
2106
+ providerLimits: limits,
2107
+ local: localSaves,
2108
+ ...(writtenBy === undefined ? {} : { writtenBy }),
2109
+ });
2110
+ if (built.kind === 'refused')
2111
+ return { outcome: 'refused', ...built.refusal };
2112
+ const id = built.leaving.id;
2113
+ const carrying = saved.flatMap(({ path, parts }) => {
2114
+ const record = roomDrafts.get(locator)?.get(path);
2115
+ const to = moveTarget(parts.move);
2116
+ if (record === undefined || consumed.has(path) || roomDrafts.get(destination)?.has(to)) {
2117
+ return [];
2118
+ }
2119
+ const moved = {
2120
+ ...record,
2121
+ locator: destination,
2122
+ path: to,
2123
+ revision: roomDraftRevision(),
2124
+ };
2125
+ return [{ from: path, record: moved }];
2126
+ });
2127
+ const carried = carrying.map(({ record }) => record);
2128
+ const marked = carried.map(({ path }) => ({ path, parts: {} }));
2129
+ const left = [...consumed, ...carrying.map(({ from }) => from)];
2130
+ const leavingQueue = [...replica.queue, built.leaving];
2131
+ const arrivingQueue = [...target.queue, built.arriving];
2132
+ const committedBoth = await saveCommit(local, files, [
2133
+ {
2134
+ locator,
2135
+ saving: sourceSaving,
2136
+ before: replica,
2137
+ queue: leavingQueue,
2138
+ write: {
2139
+ locator,
2140
+ queue: leavingQueue,
2141
+ saved: new Map([[id, saved]]),
2142
+ consumedRoomDrafts: left,
2143
+ },
2144
+ },
2145
+ {
2146
+ locator: destination,
2147
+ saving: arriving,
2148
+ before: target,
2149
+ queue: arrivingQueue,
2150
+ write: {
2151
+ locator: destination,
2152
+ queue: arrivingQueue,
2153
+ saved: new Map([[id, marked]]),
2154
+ roomDrafts: carried,
2155
+ },
2156
+ },
2157
+ ]);
2158
+ if (committedBoth === null)
2159
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
2160
+ const { answer, touched } = committedBoth;
2161
+ if (answer.fenced !== null) {
2162
+ throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2163
+ }
2164
+ if (fences.has(locator) || fences.has(destination)) {
2165
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2166
+ }
2167
+ pendings.set(pendingKey(locator, id), {
2168
+ drafts: saved,
2169
+ submission: { state: 'waiting' },
2170
+ lost: false,
2171
+ });
2172
+ pendings.set(pendingKey(destination, id), {
2173
+ drafts: marked,
2174
+ submission: { state: 'waiting' },
2175
+ lost: false,
2176
+ });
2177
+ for (const path of left)
2178
+ forgetRoomDraft(locator, path);
2179
+ for (const record of carried)
2180
+ rememberRoomDraft(record);
2181
+ savedReplica(locator, replica, leavingQueue, touched.get(locator), answer);
2182
+ savedReplica(destination, target, arrivingQueue, touched.get(destination), answer);
2183
+ publishChangeSet(mergeChangeSets([
2184
+ changeSet(locator, {
2185
+ documents: answer.changed[locator] ?? [],
2186
+ queue: true,
2187
+ drafts: left,
2188
+ }),
2189
+ changeSet(destination, {
2190
+ documents: answer.changed[destination] ?? [],
2191
+ queue: true,
2192
+ drafts: carried.map(({ path }) => path),
2193
+ }),
2194
+ ]));
2195
+ queuedSave(locator);
2196
+ return { outcome: 'saved', identifier: id };
2197
+ }
2198
+ function queuedSave(locator) {
2199
+ if (!localSaves)
2200
+ recorded = false;
2201
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
2202
+ submitLater(locator);
2203
+ }
2204
+ async function committed(locator, target, saved, consumed, writtenBy, files = []) {
2205
+ if (full)
2206
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
2207
+ const into = movedInto(locator, saved);
2208
+ if (into !== null) {
2209
+ return committedMove(locator, into, target, saved, consumed, writtenBy, files);
2210
+ }
2211
+ const { local, replica, room } = target;
2212
+ const parts = saved.map(({ path, parts: held }) => changesOf(path, held));
2213
+ const changes = parts.flatMap(({ changes: made }) => made);
2214
+ const saving = await savingIn(replica, changes, [
2215
+ ...saved.map(({ path }) => path),
2216
+ ...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
2217
+ ]);
2218
+ const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, underneath(held, path, saving.visible)));
2219
+ if (changed.length > 0)
2220
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2221
+ const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
1474
2222
  if (requeue.kind === 'refused')
1475
2223
  return { outcome: 'refused', ...requeue.refusal };
1476
2224
  const foreign = [...requeue.withdrawn.keys()].some((origin) => {
@@ -1488,22 +2236,19 @@ export function composeRuntime(options) {
1488
2236
  const withdrawing = requeue.withdrawn.size > 0;
1489
2237
  const input = {
1490
2238
  local: localSaves,
1491
- baseRoomVersion: replica.confirmed.record.roomVersion,
1492
- composition: compositionFor(replica.confirmed.record),
2239
+ baseRoomVersion: replica.record.roomVersion,
2240
+ composition: compositionFor(replica.record),
1493
2241
  operations: room.operations,
1494
2242
  providerOwnedPaths: room.providerOwnedPaths,
1495
2243
  providerLimits: limits,
1496
2244
  handle: room.handle,
1497
- visible: replica.visible,
2245
+ visible: saving.visible,
1498
2246
  changes,
1499
2247
  bases: parts.flatMap(({ bases }) => bases),
1500
2248
  queued: queuedDocuments(replica.queue),
1501
2249
  writtenBy,
1502
- clean: {
1503
- text: replica.confirmed.record.documents,
1504
- binaries: replica.confirmed.record.binaries,
1505
- composition: compositionOf(replica.confirmed.record),
1506
- },
2250
+ unmodeled: await unmodeledIn(replica.record),
2251
+ validate: validatorOf(replica.record, saving.confirmed),
1507
2252
  };
1508
2253
  const checked = withdrawing
1509
2254
  ? await buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
@@ -1528,8 +2273,7 @@ export function composeRuntime(options) {
1528
2273
  ? null
1529
2274
  : await buildBatch({
1530
2275
  ...input,
1531
- visible: (await stepped(replicating(replica.confirmed, requeue.queue, replica)))
1532
- .visible,
2276
+ visible: await stepped(saving.visibleOver(requeue.queue)),
1533
2277
  changes: requeue.changes,
1534
2278
  queued: queuedDocuments(requeue.queue),
1535
2279
  });
@@ -1548,7 +2292,7 @@ export function composeRuntime(options) {
1548
2292
  for (const [origin, replaced] of withdrawn) {
1549
2293
  if (replaced === null)
1550
2294
  continue;
1551
- const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
2295
+ const carried = withdrawnDrafts(pendings.get(pendingKey(locator, origin))?.drafts ?? [], changes);
1552
2296
  drafted.set(replaced.id, replaced === join?.batch
1553
2297
  ? [
1554
2298
  ...carried.map((held) => joinedDraft(held, saved)),
@@ -1562,66 +2306,77 @@ export function composeRuntime(options) {
1562
2306
  for (const replaced of withdrawn.values()) {
1563
2307
  if (replaced === null || !batches.includes(replaced))
1564
2308
  continue;
1565
- const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
2309
+ const limited = await stepped(limiting(batches, replaced, limits, saving.visibleOver));
1566
2310
  if (limited !== null)
1567
2311
  return { outcome: 'refused', ...limited };
1568
2312
  }
1569
2313
  const carried = carriedDrafts(locator, saved, consumed);
1570
2314
  const left = [...consumed, ...carried.keys()];
1571
2315
  const arrived = [...carried.values()];
1572
- try {
1573
- await local.keepAuthored({
2316
+ const done = await saveCommit(local, files, [
2317
+ {
1574
2318
  locator,
2319
+ saving,
2320
+ before: replica,
1575
2321
  queue: batches,
1576
- saved: drafted,
1577
- consumedRoomDrafts: left,
1578
- roomDrafts: arrived,
1579
- });
1580
- }
1581
- catch {
2322
+ write: {
2323
+ locator,
2324
+ queue: batches,
2325
+ saved: drafted,
2326
+ consumedRoomDrafts: left,
2327
+ roomDrafts: arrived,
2328
+ },
2329
+ },
2330
+ ]);
2331
+ if (done === null)
1582
2332
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2333
+ const { answer, touched } = done;
2334
+ if (answer.fenced !== null) {
2335
+ throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
1583
2336
  }
1584
- const next = await stepped(replicating(replica.confirmed, batches, replica));
1585
- const set = await stepped(changing(locator, replica, next, {
1586
- queue: true,
1587
- drafts: [...left, ...arrived.map(({ path }) => path)],
1588
- }));
1589
2337
  if (fences.has(locator))
1590
2338
  throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1591
2339
  for (const origin of withdrawn.keys())
1592
- pendings.delete(origin);
2340
+ pendings.delete(pendingKey(locator, origin));
1593
2341
  for (const [id, drafts] of drafted) {
1594
- pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
2342
+ pendings.set(pendingKey(locator, id), {
2343
+ drafts,
2344
+ submission: { state: 'waiting' },
2345
+ lost: false,
2346
+ });
1595
2347
  }
1596
- if (!localSaves)
1597
- recorded = false;
1598
2348
  for (const path of left)
1599
2349
  forgetRoomDraft(locator, path);
1600
2350
  for (const record of arrived)
1601
2351
  rememberRoomDraft(record);
1602
- replicas.set(locator, next);
1603
- publishChangeSet(set);
1604
- moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
1605
- submitLater(locator);
2352
+ savedReplica(locator, replica, batches, touched.get(locator), answer);
2353
+ publishChangeSet(changeSet(locator, {
2354
+ documents: answer.changed[locator] ?? [],
2355
+ queue: true,
2356
+ drafts: [...left, ...arrived.map(({ path }) => path)],
2357
+ }));
2358
+ queuedSave(locator);
1606
2359
  const identifier = join?.batch.id ??
1607
2360
  built?.batch.id ??
1608
2361
  [...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
1609
2362
  return { outcome: 'saved', identifier };
1610
2363
  }
1611
- async function mark(local, locator, identifier, submission, lost) {
1612
- const state = pendings.get(identifier);
1613
- if (!continuing() ||
1614
- state === undefined ||
1615
- (JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
1616
- return;
2364
+ async function mark(local, locator, batch, submission, lost) {
2365
+ const partner = partnerOf(batch, locator);
2366
+ for (const held of partner === null ? [locator] : [locator, partner]) {
2367
+ const state = pendings.get(pendingKey(held, batch.id));
2368
+ if (!continuing() ||
2369
+ state === undefined ||
2370
+ (JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
2371
+ continue;
2372
+ }
2373
+ await local.submit(held, batch.id, submission, lost);
2374
+ if (fences.has(held))
2375
+ continue;
2376
+ pendings.set(pendingKey(held, batch.id), { ...state, submission, lost });
2377
+ if (replicas.has(held))
2378
+ publishChangeSet(changeSet(held, { queue: true }));
1617
2379
  }
1618
- await local.submit(locator, identifier, submission, lost);
1619
- if (fences.has(locator))
1620
- return;
1621
- pendings.set(identifier, { ...state, submission, lost });
1622
- const replica = replicas.get(locator);
1623
- if (replica !== undefined)
1624
- publishChangeSet(changeSet(locator, replica, replica, { queue: true }));
1625
2380
  moveTo({ ...status, ...attention() });
1626
2381
  }
1627
2382
  async function keepUpload(local, locator, identifier, upload) {
@@ -1630,7 +2385,8 @@ export function composeRuntime(options) {
1630
2385
  await local.keepUpload(locator, identifier, upload);
1631
2386
  if (fences.has(locator))
1632
2387
  throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1633
- pendings.set(identifier, { ...pendings.get(identifier), upload });
2388
+ const key = pendingKey(locator, identifier);
2389
+ pendings.set(key, { ...pendings.get(key), upload });
1634
2390
  if (!continuing())
1635
2391
  throw new Error('connection refused');
1636
2392
  }
@@ -1663,7 +2419,7 @@ export function composeRuntime(options) {
1663
2419
  if (fencedSince(locator, since))
1664
2420
  return failure({ kind: 'room-not-readable', locator });
1665
2421
  if (result.kind === 'scoped-refusal') {
1666
- const room = replicas.get(result.locator)?.confirmed.record;
2422
+ const room = replicas.get(result.locator)?.record;
1667
2423
  if (room !== undefined)
1668
2424
  await scoped(result, room, await openLocal());
1669
2425
  }
@@ -1676,19 +2432,55 @@ export function composeRuntime(options) {
1676
2432
  return UNCERTAIN;
1677
2433
  }
1678
2434
  }
1679
- function queuedBlob(locator, path, localId) {
2435
+ function located(room, path, destination) {
2436
+ const crossing = roomTarget(destination);
2437
+ if (crossing === null) {
2438
+ const named = target(path, destination);
2439
+ return named === null || !rooms.has(room) ? null : { room, path: named };
2440
+ }
2441
+ const held = [...rooms.values()].find((listed) => listed.handle === crossing.handle);
2442
+ return held === undefined ? null : { room: held.locator, path: crossing.path };
2443
+ }
2444
+ async function resolveReference(room, path, destination) {
2445
+ if (roomTarget(destination) === null && target(path, destination) === null) {
2446
+ return ABSENT_TARGET;
2447
+ }
2448
+ const at = located(room, path, destination);
2449
+ if (at === null)
2450
+ return UNREADABLE;
2451
+ if (locked(at.room))
2452
+ return LOCKED_UNREADABLE;
2453
+ const held = replicas.get(at.room);
2454
+ if (held !== undefined &&
2455
+ (await (await openLocal()).entries(held.record.key, 'visible', [at.path])).length > 0) {
2456
+ return { kind: 'document', ...at };
2457
+ }
2458
+ const history = await readHistory(at.room, at.path);
2459
+ if (history.kind === 'no-history' || history.kind === 'revision')
2460
+ return ABSENT_TARGET;
2461
+ if (history.kind !== 'history')
2462
+ return history;
2463
+ const [last] = history.revisions;
2464
+ if (last === undefined)
2465
+ return ABSENT_TARGET;
2466
+ return history.deleted === true
2467
+ ? { kind: 'deleted', ...at, revision: last }
2468
+ : { kind: 'document', ...at };
2469
+ }
2470
+ function queuedFacts(locator, path, localId) {
1680
2471
  for (const batch of replicas.get(locator)?.queue ?? []) {
1681
2472
  const change = batch.changes[queuedFile(batch, path, localId)];
1682
2473
  if (change?.kind === 'binary')
1683
- return change.blob;
2474
+ return change;
1684
2475
  }
1685
- return null;
2476
+ const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2477
+ return drafted?.localId === localId ? drafted : null;
1686
2478
  }
1687
- function binaryRoom(request) {
1688
- if (request.kind === 'path')
1689
- return request.locator;
1690
- const scope = options.adapter.referrerScope(request.referrer);
1691
- return scope?.kind === 'room' ? scope.locator : null;
2479
+ function batchFile(batch) {
2480
+ return async (localId) => {
2481
+ const change = batch.changes.find((held) => held.kind === 'binary' && held.localId === localId);
2482
+ return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
2483
+ };
1692
2484
  }
1693
2485
  async function transferBinary(request, transfer, answer) {
1694
2486
  if (closed || refusal !== null)
@@ -1702,18 +2494,15 @@ export function composeRuntime(options) {
1702
2494
  if (closed || authorization === undefined || refusal !== null)
1703
2495
  return UNCERTAIN;
1704
2496
  const since = fencings;
1705
- journal.record(request.kind === 'path'
1706
- ? `transfer binary ${request.locator} ${request.path}`
1707
- : `transfer binary ${request.fileRef}`);
2497
+ journal.record(`transfer binary ${request.locator} ${request.path}`);
1708
2498
  try {
1709
2499
  const result = await transfer(authorization);
1710
2500
  const settled = result;
1711
2501
  journal.record(`outcome binary ${settled.kind}`);
1712
- const locator = binaryRoom(request);
1713
- if (locator !== null && fencedSince(locator, since))
1714
- return failure({ kind: 'room-not-readable', locator });
2502
+ if (fencedSince(request.locator, since))
2503
+ return failure({ kind: 'room-not-readable', locator: request.locator });
1715
2504
  if (settled.kind === 'scoped-refusal') {
1716
- const room = replicas.get(settled.locator)?.confirmed.record;
2505
+ const room = replicas.get(settled.locator)?.record;
1717
2506
  if (room !== undefined)
1718
2507
  await scoped(settled, room, await openLocal());
1719
2508
  }
@@ -1726,68 +2515,130 @@ export function composeRuntime(options) {
1726
2515
  return UNCERTAIN;
1727
2516
  }
1728
2517
  }
1729
- function readBinary(request) {
1730
- return transferBinary(request, (held) => options.adapter.binary(held, request), binaryAnswer);
2518
+ function fetchBinary(request, name) {
2519
+ return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2520
+ }
2521
+ async function readBinary(request, name) {
2522
+ if (closed)
2523
+ return UNCERTAIN;
2524
+ const held = await keptFiles.find(name);
2525
+ if (held !== null)
2526
+ return held;
2527
+ return fetchBinary(request, name);
1731
2528
  }
1732
- function binaryPath(locator, path, revision) {
2529
+ async function binaryPath(locator, path, revision) {
1733
2530
  if (revision !== undefined)
1734
2531
  return { kind: 'path', locator, path, revision };
1735
- const visible = replicas.get(locator)?.visible;
1736
- const held = visible?.binaries[path];
1737
- if (visible === undefined || held === undefined)
2532
+ const replica = replicas.get(locator);
2533
+ if (replica === undefined)
1738
2534
  return null;
1739
- if (held.state === 'pending')
1740
- return { kind: 'pending', localId: held.localId };
1741
- const record = binaryRecord(path, held, visible.revisions[path]);
2535
+ const local = await openLocal();
2536
+ const [held] = await local.entries(replica.record.key, 'visible', [path]);
2537
+ if (held?.binary === undefined || held.binary === null)
2538
+ return null;
2539
+ if (held.binary.state === 'pending')
2540
+ return { kind: 'pending', localId: held.binary.localId };
2541
+ const record = binaryRecord(path, held.binary, held.revision ?? undefined);
1742
2542
  if (record.state !== 'accepted')
1743
2543
  return null;
1744
- const replica = replicas.get(locator);
1745
- const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
2544
+ const queued = replica.queue.flatMap((batch) => batch.changes);
2545
+ const sources = queued.flatMap((change) => (change.kind === 'move' ? [change.path] : []));
2546
+ const confirmedLayer = await stepped(slicing(await local.entries(replica.record.key, 'confirmed', [path, ...sources]), [
2547
+ path,
2548
+ ...sources,
2549
+ ]));
1746
2550
  const holds = (at) => {
1747
- const kept = replica?.confirmed.record.binaries[at];
2551
+ const kept = confirmedLayer.binaries[at];
1748
2552
  return (kept?.state === 'accepted' &&
1749
- kept.fileRef === record.fileRef &&
1750
- replica?.confirmed.record.revisions[at] === record.revision);
2553
+ kept.sha256 === record.sha256 &&
2554
+ confirmedLayer.revisions[at] === record.revision);
1751
2555
  };
1752
- let confirmed = path;
1753
- for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
2556
+ let confirmedAt = path;
2557
+ for (let index = queued.length - 1; index >= 0 && !holds(confirmedAt); index -= 1) {
1754
2558
  const change = queued[index];
1755
- if (change.kind === 'move' && change.to === confirmed)
1756
- confirmed = change.path;
2559
+ if (change.kind === 'move' && change.to === confirmedAt)
2560
+ confirmedAt = change.path;
1757
2561
  }
1758
2562
  return {
1759
2563
  kind: 'path',
1760
2564
  locator,
1761
- path: confirmed,
2565
+ path: confirmedAt,
1762
2566
  revision: record.revision,
1763
- fileRef: record.fileRef,
1764
2567
  };
1765
2568
  }
1766
- async function readBinaryPath(locator, path, revision) {
2569
+ async function askedBinary(locator, path, revision) {
1767
2570
  await ready;
1768
- const asked = binaryPath(locator, path, revision);
2571
+ const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2572
+ return drafted !== undefined && drafted.localId === revision
2573
+ ? { kind: 'pending', localId: revision }
2574
+ : binaryPath(locator, path, revision);
2575
+ }
2576
+ function pendingBinary(locator, path, localId) {
2577
+ const facts = queuedFacts(locator, path, localId);
2578
+ return facts === null
2579
+ ? absentBinary(path)
2580
+ : {
2581
+ kind: 'binary',
2582
+ file: localId,
2583
+ byteLength: facts.byteLength,
2584
+ mediaType: facts.mediaType,
2585
+ };
2586
+ }
2587
+ async function readBinaryPath(locator, path, revision) {
2588
+ const asked = await askedBinary(locator, path, revision);
1769
2589
  if (asked === null)
1770
2590
  return absentBinary(path);
1771
- if (asked.kind === 'pending') {
1772
- const blob = queuedBlob(locator, path, asked.localId);
1773
- return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1774
- }
1775
- return readBinary(asked);
2591
+ if (asked.kind === 'pending')
2592
+ return pendingBinary(locator, path, asked.localId);
2593
+ return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
2594
+ }
2595
+ async function heldMediaType(locator, path) {
2596
+ const replica = replicas.get(locator);
2597
+ if (replica === undefined)
2598
+ return undefined;
2599
+ const [held] = await (await openLocal()).entries(replica.record.key, 'visible', [path]);
2600
+ return held?.binary?.mediaType;
2601
+ }
2602
+ function linkOf(asked, held) {
2603
+ return transferBinary(asked, (authorization) => options.adapter.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
1776
2604
  }
1777
2605
  async function linkBinaryPath(locator, path, revision) {
1778
2606
  await ready;
1779
- const asked = binaryPath(locator, path, revision);
2607
+ const asked = await binaryPath(locator, path, revision);
1780
2608
  if (asked === null || asked.kind === 'pending')
1781
2609
  return absentBinary(path);
1782
- return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
2610
+ return linkOf(asked, revision === undefined ? await heldMediaType(locator, path) : undefined);
1783
2611
  }
1784
- async function readBinaryReference(reference) {
2612
+ async function showBinaryPath(locator, path, revision) {
2613
+ const asked = await askedBinary(locator, path, revision);
2614
+ if (asked === null)
2615
+ return absentBinary(path);
2616
+ if (asked.kind === 'pending')
2617
+ return pendingBinary(locator, path, asked.localId);
2618
+ if (closed)
2619
+ return UNCERTAIN;
2620
+ const name = { locator, path: asked.path, revision: asked.revision };
2621
+ const kept = await keptFiles.find(name);
2622
+ if (kept !== null)
2623
+ return kept;
2624
+ const mediaType = await heldMediaType(locator, path);
2625
+ if (mediaType?.startsWith('image/') === true)
2626
+ return fetchBinary(asked, name);
2627
+ return linkOf(asked, revision === undefined ? mediaType : undefined);
2628
+ }
2629
+ async function attachBinary(locator, file) {
1785
2630
  await ready;
1786
- return readBinary({
1787
- kind: 'reference',
1788
- fileRef: reference.fileRef,
1789
- referrer: reference.referrer,
1790
- });
2631
+ if (closed || connectionOf() === 'unreachable' || refusal !== null)
2632
+ return OFFLINE;
2633
+ const request = await attachedFile(locator, file);
2634
+ return transferBinary({ locator, path: file.name }, (held) => options.adapter.attach(held, request), attachAnswer);
2635
+ }
2636
+ function readPart(file, part) {
2637
+ return database.ask({ kind: 'file.part', key: file, part });
2638
+ }
2639
+ async function cleanUp() {
2640
+ const freed = await database.ask({ kind: 'opened.clean' });
2641
+ return freed + ((await options.shell.cleanUp?.()) ?? 0);
1791
2642
  }
1792
2643
  function declaredAgents() {
1793
2644
  for (const capability of capabilities) {
@@ -1805,6 +2656,13 @@ export function composeRuntime(options) {
1805
2656
  }
1806
2657
  return undefined;
1807
2658
  }
2659
+ function calledRooms(name, input) {
2660
+ const member = declaredAction(name)?.room;
2661
+ if (member === undefined || input === null || typeof input !== 'object')
2662
+ return [];
2663
+ const named = input[member];
2664
+ return (Array.isArray(named) ? named : [named]).filter((room) => typeof room === 'string');
2665
+ }
1808
2666
  async function invokeAction(name, input, dispatching) {
1809
2667
  await ready;
1810
2668
  if (closed || refusal !== null)
@@ -1824,7 +2682,7 @@ export function composeRuntime(options) {
1824
2682
  try {
1825
2683
  const result = await options.adapter.action(authorization, { name, input });
1826
2684
  if (result.kind === 'scoped-refusal') {
1827
- const room = replicas.get(result.locator)?.confirmed.record;
2685
+ const room = replicas.get(result.locator)?.record;
1828
2686
  if (room !== undefined)
1829
2687
  await scoped(result, room, await openLocal());
1830
2688
  }
@@ -1892,100 +2750,442 @@ export function composeRuntime(options) {
1892
2750
  calling.delete(held.operation);
1893
2751
  }
1894
2752
  }
1895
- return 'done';
2753
+ return 'done';
2754
+ }
2755
+ function continuing() {
2756
+ return !closed && !discoveryPaused;
2757
+ }
2758
+ function reachedBy(locator, roomVersion) {
2759
+ const held = replicas.get(locator);
2760
+ return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
2761
+ }
2762
+ async function writtenAfter(locator, roomVersion, path) {
2763
+ const held = replicas.get(locator);
2764
+ if (held === undefined)
2765
+ return false;
2766
+ const [entry] = await (await openLocal()).entries(held.record.key, 'confirmed', [path]);
2767
+ const written = entry?.written ?? undefined;
2768
+ return (written !== undefined &&
2769
+ options.adapter.reached(locator, written, roomVersion) &&
2770
+ !options.adapter.reached(locator, roomVersion, written));
2771
+ }
2772
+ function overriddenTexts(texts, overrides) {
2773
+ return async (paths) => {
2774
+ const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
2775
+ for (const path of paths) {
2776
+ const entry = overrides.get(path);
2777
+ if (entry === undefined)
2778
+ continue;
2779
+ if (entry === null)
2780
+ known.delete(path);
2781
+ else
2782
+ known.set(path, entry.binary !== null ? null : entry.content);
2783
+ }
2784
+ return known;
2785
+ };
2786
+ }
2787
+ async function unreadableRoom(texts, walk, paths) {
2788
+ if (paths !== null) {
2789
+ const text = {};
2790
+ for (const [path, content] of await texts(paths))
2791
+ if (content !== null)
2792
+ text[path] = content;
2793
+ return stepped(unreadableIn(text, Object.keys(text)));
2794
+ }
2795
+ for (let after = null;;) {
2796
+ const page = await walk(after);
2797
+ const text = {};
2798
+ for (const { path, content } of page.entries)
2799
+ if (content !== null)
2800
+ text[path] = content;
2801
+ const refused = await stepped(unreadableIn(text, Object.keys(text)));
2802
+ if (refused !== null)
2803
+ return refused;
2804
+ if (page.next === null)
2805
+ return null;
2806
+ after = page.next;
2807
+ }
2808
+ }
2809
+ async function validatedRoom(local, record, view, previous, overrides = null) {
2810
+ const composition = composed(record);
2811
+ const modeled = record.model.status !== 'unavailable' || record.typedSelection !== null;
2812
+ const lookup = overrides === null
2813
+ ? lookupOf(local, record.key, view)
2814
+ : overriddenLookup(lookupOf(local, record.key, view), overrides);
2815
+ const texts = overrides === null
2816
+ ? textsOf(local, record.key, view)
2817
+ : overriddenTexts(textsOf(local, record.key, view), overrides);
2818
+ const walk = overrides === null
2819
+ ? walkOf(local, record.key, view)
2820
+ : overriddenWalk(walkOf(local, record.key, view), overrides);
2821
+ let changes = null;
2822
+ let reshaped = false;
2823
+ if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
2824
+ const [was, now] = await Promise.all([
2825
+ local.entries(record.key, 'confirmed', previous.changed),
2826
+ overrides === null
2827
+ ? local.entries(record.key, view, previous.changed)
2828
+ : Promise.resolve([]),
2829
+ ]);
2830
+ const before = new Map(was.map((entry) => [entry.path, entry]));
2831
+ const after = overrides === null
2832
+ ? new Map(now.map((entry) => [entry.path, entry]))
2833
+ : overrides;
2834
+ changes = previous.changed.flatMap((path) => {
2835
+ const one = before.get(path) ?? null;
2836
+ const other = after.get(path) ?? null;
2837
+ reshaped ||= textDiffers(one, other);
2838
+ const held = textOf(one);
2839
+ const next = textOf(other);
2840
+ return held === next ? [] : [{ path, was: held, now: next }];
2841
+ });
2842
+ }
2843
+ const named = changes === null ? null : changes.map(({ path }) => path);
2844
+ const textChanged = changes === null || changes.length > 0 || reshaped;
2845
+ const modelsChanged = previous === null || !sameComposition(composed(previous.record), composition);
2846
+ if (modeled && (textChanged || modelsChanged)) {
2847
+ const clean = previous !== null &&
2848
+ changes !== null &&
2849
+ (await changedCleanly(changes, composed(previous.record), composition, {
2850
+ lookup,
2851
+ texts,
2852
+ referrers: referrersOf(local, record.key, 'confirmed'),
2853
+ }));
2854
+ if (clean)
2855
+ return null;
2856
+ const verdict = await validatedCollection(walk, lookup, composition);
2857
+ if (verdict.verdict === 'accept')
2858
+ return null;
2859
+ if (verdict.rule !== 'model-unavailable')
2860
+ return { kind: 'refused', refusal: verdict };
2861
+ return unreadableRoom(texts, walk, named);
2862
+ }
2863
+ if (textChanged)
2864
+ return unreadableRoom(texts, walk, named);
2865
+ return null;
1896
2866
  }
1897
- function continuing() {
1898
- return !closed && !discoveryPaused;
2867
+ function refreshingOf(local, key, liveness, replaying = null) {
2868
+ const write = replaying === null ? exclusive : installWrite;
2869
+ return {
2870
+ async turn() {
2871
+ if (replaying === null)
2872
+ return;
2873
+ installing(replaying);
2874
+ await screened();
2875
+ },
2876
+ prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
2877
+ async stage(page) {
2878
+ for (let written = 0;;) {
2879
+ if (replaying !== null)
2880
+ await screened();
2881
+ const end = batchEnd(page.entries, written);
2882
+ const last = end === page.entries.length;
2883
+ const entries = page.entries.slice(written, end);
2884
+ const staged = await write(async () => {
2885
+ if (!liveness())
2886
+ return false;
2887
+ await timed(stageBudget, () => local.refresh(key, last
2888
+ ? { ...page, entries }
2889
+ : { entries, removals: [], folders: [], folderRemovals: [] }));
2890
+ return true;
2891
+ });
2892
+ if (!staged)
2893
+ return false;
2894
+ if (last)
2895
+ return true;
2896
+ written = end;
2897
+ }
2898
+ },
2899
+ };
1899
2900
  }
1900
- function reachedBy(locator, roomVersion) {
1901
- const held = replicas.get(locator);
1902
- return (held !== undefined &&
1903
- options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion));
2901
+ function batchEnd(entries, from) {
2902
+ let end = from;
2903
+ let spent = 0;
2904
+ while (end < entries.length) {
2905
+ const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
2906
+ if (spent + size > stageBudget.bytes && end > from)
2907
+ break;
2908
+ spent += size;
2909
+ end += 1;
2910
+ }
2911
+ return end;
2912
+ }
2913
+ async function timed(budget, work) {
2914
+ const started = performance.now();
2915
+ const done = await work();
2916
+ const spent = performance.now() - started;
2917
+ if (spent < budget.aim / 2)
2918
+ budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
2919
+ else if (spent > budget.aim * 2) {
2920
+ budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
2921
+ }
2922
+ return done;
2923
+ }
2924
+ function installWrite(work) {
2925
+ const next = installWrites.then(() => exclusive(work), () => exclusive(work));
2926
+ installWrites = next.then(() => undefined, () => undefined);
2927
+ return next;
1904
2928
  }
1905
- function writtenAfter(locator, roomVersion, path) {
1906
- const written = replicas.get(locator)?.confirmed.record.written[path];
1907
- return (written !== undefined &&
1908
- options.adapter.reached(locator, written, roomVersion) &&
1909
- !options.adapter.reached(locator, roomVersion, written));
2929
+ function screened() {
2930
+ if (onScreen || closed)
2931
+ return Promise.resolve();
2932
+ queueReadiness();
2933
+ return new Promise((resolve) => shownWaiters.push(resolve));
1910
2934
  }
1911
- async function pullRefresh(authorized, confirmed, live) {
1912
- if (!live())
2935
+ async function heldPart() {
2936
+ while (partsFree === 0 || queuedBytes >= QUEUED_BYTES) {
2937
+ await new Promise((admit) => partWaiters.push(admit));
2938
+ }
2939
+ partsFree -= 1;
2940
+ return once(() => {
2941
+ partsFree += 1;
2942
+ });
2943
+ }
2944
+ function once(release) {
2945
+ let released = false;
2946
+ return () => {
2947
+ if (released)
2948
+ return;
2949
+ released = true;
2950
+ release();
2951
+ for (const admit of partWaiters.splice(0))
2952
+ admit();
2953
+ };
2954
+ }
2955
+ function installing(locator, inventory = null) {
2956
+ installs.set(locator, inventory ?? installs.get(locator) ?? null);
2957
+ queueReadiness();
2958
+ }
2959
+ function untransferred(locator) {
2960
+ transfers.delete(locator);
2961
+ keptParts.delete(locator);
2962
+ underived.delete(locator);
2963
+ }
2964
+ function installed(locator) {
2965
+ if (installs.delete(locator))
2966
+ queueReadiness();
2967
+ }
2968
+ function screen(value) {
2969
+ if (value === onScreen)
2970
+ return;
2971
+ onScreen = value;
2972
+ journal.record(`installation ${value ? 'shown' : 'hidden'}`);
2973
+ queueReadiness();
2974
+ if (!value)
2975
+ return;
2976
+ for (const resolve of shownWaiters.splice(0))
2977
+ resolve();
2978
+ }
2979
+ async function pullRefresh(authorized, held, liveness) {
2980
+ if (!liveness())
1913
2981
  return unconfirmed;
1914
2982
  const turn = generation;
2983
+ const { record } = held;
2984
+ const base = held.confirmed;
2985
+ const local = await openLocal();
1915
2986
  try {
1916
- const result = await refreshRoom(reads, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1917
- const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
1918
- journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
1919
- return result;
2987
+ await exclusive(() => local.unrefresh(record.key));
2988
+ const result = await advancement(reads, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, liveness));
2989
+ if (result.kind !== 'advanced') {
2990
+ journal.record(`transfer refresh ${record.locator} ${result.kind}`);
2991
+ return result;
2992
+ }
2993
+ const refused = await validatedRoom(local, result.record, 'refreshed', {
2994
+ record,
2995
+ changed: [...result.changed],
2996
+ });
2997
+ if (refused !== null) {
2998
+ journal.record(`transfer refresh ${record.locator} refused`);
2999
+ return refused;
3000
+ }
3001
+ journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
3002
+ return { kind: 'advanced', record: result.record, base };
1920
3003
  }
1921
3004
  catch (error) {
1922
- journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
3005
+ if (refusedForSpace(error))
3006
+ return unconfirmed;
3007
+ journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
1923
3008
  throw error;
1924
3009
  }
1925
3010
  finally {
1926
- reads.finished(confirmed.record.locator, turn);
3011
+ reads.finished(record.locator, turn);
3012
+ }
3013
+ }
3014
+ const heldContents = new Map();
3015
+ function heldContentOf(local, key) {
3016
+ const known = heldContents.get(key);
3017
+ if (known !== undefined)
3018
+ return known;
3019
+ if (heldContents.size > ROOM_READS)
3020
+ heldContents.clear();
3021
+ const made = {
3022
+ files: (after, limit) => local.files(key, 'confirmed', after, limit),
3023
+ async contents(paths) {
3024
+ return new Map((await local.entries(key, 'confirmed', paths)).flatMap(({ path, content, revision }) => content === null || revision === null ? [] : [[path, { content, revision }]]));
3025
+ },
3026
+ };
3027
+ heldContents.set(key, made);
3028
+ return made;
3029
+ }
3030
+ async function keptTogether({ local, parts }) {
3031
+ const live = parts.filter((one) => one.live() && transfers.has(one.locator));
3032
+ await local.keepParts(live.map(({ locator, token, part }) => ({ locator, token, part })));
3033
+ for (const { locator, token, part } of live) {
3034
+ const held = keptParts.get(locator) ?? new Map();
3035
+ keptParts.set(locator, held.set(token, part.byteLength));
3036
+ arrivals.get(locator)?.delete(token);
1927
3037
  }
3038
+ queueReadiness();
1928
3039
  }
1929
- function staging(local, live) {
1930
- return {
1931
- held: (locator) => transfers.get(locator) ?? null,
1932
- staged: (record) => local.staged(record),
1933
- stage: (record, page) => exclusive(async () => {
1934
- if (!live())
3040
+ function staging(local, liveness) {
3041
+ const derived = async (locator, key) => {
3042
+ underived.delete(locator);
3043
+ for (;;) {
3044
+ await screened();
3045
+ if (!liveness())
1935
3046
  return false;
1936
- const bytes = layerBytes(page);
1937
- if (!(await admitted(local, record.locator, bytes)))
3047
+ const done = await installWrite(async () => liveness() ? timed(deriveBudget, () => local.rederive(key, deriveBudget.bytes)) : null);
3048
+ if (done === null)
1938
3049
  return false;
1939
- try {
1940
- await local.stage(record, page);
3050
+ if (done)
3051
+ return true;
3052
+ }
3053
+ };
3054
+ return {
3055
+ held: (locator) => transfers.get(locator) ?? null,
3056
+ async turn(locator, step) {
3057
+ installing(locator);
3058
+ for (;;) {
3059
+ await screened();
3060
+ const release = step === 'downloading' ? await heldPart() : () => { };
3061
+ if (onScreen || closed)
3062
+ return release;
3063
+ release();
1941
3064
  }
1942
- catch (error) {
1943
- if (!exceeded(error))
1944
- throw error;
1945
- await exceededOn(record.locator, bytes);
1946
- return false;
3065
+ },
3066
+ queued(bytes) {
3067
+ queuedBytes += bytes;
3068
+ return once(() => {
3069
+ queuedBytes -= bytes;
3070
+ });
3071
+ },
3072
+ arriving(locator) {
3073
+ let ended = false;
3074
+ return {
3075
+ at(token, bytes) {
3076
+ if (ended || keptParts.get(locator)?.has(token) === true)
3077
+ return;
3078
+ const held = arrivals.get(locator) ?? new Map();
3079
+ arrivals.set(locator, held.set(token, bytes));
3080
+ const now = performance.now();
3081
+ if (now - arrivalReported < ARRIVAL_REPORT_MS)
3082
+ return;
3083
+ arrivalReported = now;
3084
+ queueReadiness();
3085
+ },
3086
+ end() {
3087
+ ended = true;
3088
+ if (arrivals.delete(locator))
3089
+ queueReadiness();
3090
+ },
3091
+ };
3092
+ },
3093
+ received: (locator, token) => local.transferPart(locator, token),
3094
+ keep: (locator, token, received) => gathered('kept parts', { local, parts: [{ locator, token, part: received, live: liveness }] }, (earlier, later) => earlier.local === later.local
3095
+ ? { local: earlier.local, parts: [...earlier.parts, ...later.parts] }
3096
+ : null, keptTogether),
3097
+ announced(locator) {
3098
+ const later = announced.get(locator);
3099
+ return later === undefined ? [] : [later];
3100
+ },
3101
+ async stage(record, page, composition, written) {
3102
+ const { locator } = record;
3103
+ installing(locator, record.inventory);
3104
+ await screened();
3105
+ if (!liveness())
3106
+ return null;
3107
+ const key = await installWrite(async () => {
3108
+ if (!liveness())
3109
+ return null;
3110
+ return local.stage(record, composition, page, written ?? undefined);
3111
+ });
3112
+ if (key === null || !liveness())
3113
+ return null;
3114
+ const arrived = !transfers.has(locator);
3115
+ const held = { ...record, key };
3116
+ transfers.set(locator, held);
3117
+ if (written !== null) {
3118
+ keptParts.get(locator)?.delete(written);
3119
+ arrivals.get(locator)?.delete(written);
1947
3120
  }
1948
- transfers.set(record.locator, record);
1949
- readSide.recover();
1950
- return true;
1951
- }),
3121
+ if (arrived)
3122
+ liveQueries.recover();
3123
+ installing(locator);
3124
+ liveQueries.recover();
3125
+ const owed = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), underived.get(locator) ?? 0);
3126
+ underived.set(locator, owed);
3127
+ if (owed < deriveBudget.bytes)
3128
+ return held;
3129
+ return (await derived(locator, key)) ? held : null;
3130
+ },
3131
+ derive: derived,
3132
+ async refreshing(locator, key) {
3133
+ await exclusive(() => local.unrefresh(key));
3134
+ return refreshingOf(local, key, liveness, locator);
3135
+ },
1952
3136
  abandon: (locator) => exclusive(async () => {
1953
- if (!live())
3137
+ if (!liveness())
1954
3138
  return;
1955
3139
  await local.abandon(locator);
1956
- transfers.delete(locator);
1957
- readSide.recover();
3140
+ untransferred(locator);
3141
+ liveQueries.recover();
3142
+ sweep();
3143
+ }),
3144
+ setAside: (locator) => exclusive(async () => {
3145
+ if (!liveness())
3146
+ return;
3147
+ await local.abandon(locator, true);
3148
+ untransferred(locator);
3149
+ liveQueries.recover();
3150
+ }),
3151
+ release: (key) => exclusive(async () => {
3152
+ await local.release(key);
3153
+ sweep();
1958
3154
  }),
3155
+ content: (key) => heldContentOf(local, key),
3156
+ validate: (record, view) => validatedRoom(local, record, view, null),
1959
3157
  };
1960
3158
  }
1961
- function reusable(room) {
1962
- const record = replicas.get(room.locator)?.confirmed.record;
3159
+ function reusable(room, local) {
3160
+ const record = replicas.get(room.locator)?.record;
1963
3161
  if (record === undefined || record.replica.selection !== room.replica.selection)
1964
3162
  return null;
1965
- return { documents: record.documents, revisions: record.revisions };
3163
+ return heldContentOf(local, record.key);
1966
3164
  }
1967
- async function pullBootstrap(authorized, room, local, live) {
1968
- if (!live())
3165
+ async function pullBootstrap(authorized, room, local, liveness) {
3166
+ if (!liveness())
1969
3167
  return unconfirmed;
1970
3168
  const turn = generation;
1971
3169
  if (replicas.has(room.locator)) {
1972
3170
  catching.add(room.locator);
1973
- readSide.recover();
3171
+ liveQueries.recover();
1974
3172
  }
1975
3173
  try {
1976
- const result = await bootstrapRoom(reads, authorized, room, {
1977
- collection: modelOf(room.locator),
1978
- selection: modelOf(room.locator, 'typed-selection'),
1979
- }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
1980
- const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
3174
+ 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));
3175
+ const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
1981
3176
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1982
- if (result.kind !== 'staged')
3177
+ if (result.kind !== 'staged') {
1983
3178
  caughtUpTo(room.locator);
3179
+ installed(room.locator);
3180
+ }
1984
3181
  return result;
1985
3182
  }
1986
3183
  catch (error) {
1987
- journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
1988
3184
  caughtUpTo(room.locator);
3185
+ installed(room.locator);
3186
+ if (refusedForSpace(error))
3187
+ return unconfirmed;
3188
+ journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
1989
3189
  throw error;
1990
3190
  }
1991
3191
  finally {
@@ -1994,10 +3194,10 @@ export function composeRuntime(options) {
1994
3194
  }
1995
3195
  function caughtUpTo(locator) {
1996
3196
  if (catching.delete(locator))
1997
- readSide.recover();
3197
+ liveQueries.recover();
1998
3198
  }
1999
3199
  function behind(room) {
2000
- const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
3200
+ const held = replicas.get(room.locator)?.record.roomVersion;
2001
3201
  return (held !== undefined &&
2002
3202
  [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
2003
3203
  }
@@ -2009,13 +3209,13 @@ export function composeRuntime(options) {
2009
3209
  }
2010
3210
  return advanceOnce(room, local, authorized, live);
2011
3211
  }
2012
- async function advanceOnce(room, local, authorized, live) {
2013
- await referenced(room.locator);
2014
- if (!live() || paused.has(room.locator))
3212
+ async function advanceOnce(room, local, authorized, running) {
3213
+ const live = () => running() && !full;
3214
+ if (!live())
2015
3215
  return unconfirmed;
2016
3216
  const held = replicas.get(room.locator);
2017
3217
  if (held !== undefined) {
2018
- const refreshed = await pullRefresh(authorized, held.confirmed, live);
3218
+ const refreshed = await pullRefresh(authorized, held, live);
2019
3219
  if (!live())
2020
3220
  return unconfirmed;
2021
3221
  if (refreshed.kind === 'still-current') {
@@ -2030,32 +3230,32 @@ export function composeRuntime(options) {
2030
3230
  return null;
2031
3231
  }
2032
3232
  if (refreshed.kind === 'advanced') {
2033
- if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
2034
- const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
3233
+ const landing = { kind: 'refreshed', base: refreshed.base };
3234
+ if (!(await publish(local, room, refreshed.record, landing, live))) {
3235
+ const now = replicas.get(room.locator)?.record.roomVersion;
2035
3236
  const reached = now !== undefined &&
2036
- options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
3237
+ options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
2037
3238
  return reached ? null : advance(room, local, authorized, live);
2038
3239
  }
2039
- await retainModel(local, room, 'collection', modelOf(room.locator));
3240
+ await retainModel(local, room, modelOf(room.locator));
2040
3241
  return null;
2041
3242
  }
2042
3243
  if (refreshed.kind !== 'beyond-horizon')
2043
3244
  return refreshed;
2044
3245
  if (opened !== null) {
2045
3246
  owed.add(room.locator);
2046
- readSide.recover();
3247
+ liveQueries.recover();
2047
3248
  return unconfirmed;
2048
3249
  }
2049
3250
  if (reloadsDue.get(room.locator) === generation)
2050
3251
  return unconfirmed;
2051
3252
  }
2052
3253
  const bootstrapped = await pullBootstrap(authorized, room, local, live);
2053
- if (!live())
2054
- return unconfirmed;
2055
- if (bootstrapped.kind !== 'staged')
2056
- return bootstrapped;
2057
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
2058
- return null;
3254
+ if (bootstrapped.kind === 'staged') {
3255
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3256
+ return live() ? null : unconfirmed;
3257
+ }
3258
+ return live() ? bootstrapped : unconfirmed;
2059
3259
  }
2060
3260
  async function withhold(withheld, room, local, progress) {
2061
3261
  if (withheld.kind === 'unconfirmed')
@@ -2127,57 +3327,254 @@ export function composeRuntime(options) {
2127
3327
  (accepted.author === undefined ? '' : ` author ${accepted.author}`) +
2128
3328
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
2129
3329
  }
2130
- async function adoptAccepted(room, local, submitted, accepted) {
2131
- await referenced(room.locator);
3330
+ function settling(acceptance) {
3331
+ return acceptance !== null && 'apply' in acceptance;
3332
+ }
3333
+ function refusalOf(acceptance) {
3334
+ return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
3335
+ }
3336
+ async function acceptance(room, local, submitted, accepted) {
2132
3337
  if (!continuing())
2133
3338
  return unconfirmed;
2134
3339
  const current = replicas.get(room.locator);
2135
3340
  if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
2136
3341
  return null;
2137
3342
  }
2138
- const held = current.confirmed.record.roomVersion;
3343
+ const held = current.record.roomVersion;
2139
3344
  logAccepted(room.locator, submitted, accepted);
2140
- const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
2141
- ? { kind: 'staged', confirmed: current.confirmed }
2142
- : await applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
2143
- ? accepted.roomVersion
2144
- : held, accepted.roomVersion);
2145
- if (applied.kind !== 'staged') {
2146
- const state = pendings.get(submitted.id);
2147
- await local.accept(room.locator, submitted.id, accepted.roomVersion);
2148
- if (state !== undefined && !fences.has(room.locator))
2149
- pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
2150
- emitQueue(room.locator);
2151
- return applied;
2152
- }
2153
- const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
2154
- pendings.delete(submitted.id);
2155
- unreadable.delete(room.locator);
2156
- sourceRefusals.delete(room.locator);
2157
- declarationRefusals.delete(room.locator);
2158
- const set = settled();
2159
- list(room, true);
2160
- publishChangeSet(set);
2161
- moveTo({ ...status, ...attention() });
2162
- return null;
3345
+ let record = current.record;
3346
+ let changes;
3347
+ if (!options.adapter.reached(room.locator, held, accepted.roomVersion)) {
3348
+ const paths = [...returnedPaths(submitted, accepted)];
3349
+ const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
3350
+ const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
3351
+ record = {
3352
+ ...record,
3353
+ roomVersion: options.adapter.follows(room.locator, held, accepted.roomVersion)
3354
+ ? accepted.roomVersion
3355
+ : held,
3356
+ };
3357
+ const overrides = new Map([
3358
+ ...back.set.map((entry) => [entry.path, entry]),
3359
+ ...back.removed.map((path) => [path, null]),
3360
+ ]);
3361
+ const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
3362
+ if (refused !== null) {
3363
+ const key = pendingKey(room.locator, submitted.id);
3364
+ return {
3365
+ locator: room.locator,
3366
+ left: [],
3367
+ commit: {
3368
+ rooms: [],
3369
+ writes: [
3370
+ {
3371
+ locator: room.locator,
3372
+ accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
3373
+ },
3374
+ ],
3375
+ },
3376
+ refused,
3377
+ apply() {
3378
+ const state = pendings.get(key);
3379
+ if (state !== undefined && !fences.has(room.locator))
3380
+ pendings.set(key, { ...state, accepted: accepted.roomVersion });
3381
+ return changeSet(room.locator, { queue: true });
3382
+ },
3383
+ };
3384
+ }
3385
+ changes = back;
3386
+ }
3387
+ const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
3388
+ return {
3389
+ ...settled,
3390
+ apply(answer) {
3391
+ const set = settled.apply(answer);
3392
+ pendings.delete(pendingKey(room.locator, submitted.id));
3393
+ unreadable.delete(room.locator);
3394
+ sourceRefusals.delete(room.locator);
3395
+ declarationRefusals.delete(room.locator);
3396
+ list(room, true);
3397
+ return set;
3398
+ },
3399
+ };
3400
+ }
3401
+ async function adoptedTogether(local, acceptances) {
3402
+ const settlements = acceptances.filter(settling);
3403
+ if (settlements.length > 0) {
3404
+ publishChangeSet(await together(local, settlements));
3405
+ moveTo({ ...status, ...attention() });
3406
+ }
3407
+ await unpair(local);
2163
3408
  }
2164
3409
  async function adopted(room, local, submitted, accepted) {
2165
- const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
3410
+ const failed = await exclusive(async () => {
3411
+ const adopting = await acceptance(room, local, submitted, accepted);
3412
+ await adoptedTogether(local, [adopting]);
3413
+ return refusalOf(adopting);
3414
+ });
3415
+ if (failed === null)
3416
+ return true;
3417
+ if (failed.kind === 'refused' && continuing())
3418
+ refusedSource(room, failed);
3419
+ return false;
3420
+ }
3421
+ async function adoptedMove(room, partner, local, submitted, accepted) {
3422
+ const other = discovered.get(partner);
3423
+ const failed = await exclusive(async () => {
3424
+ const left = await acceptance(room, local, submitted, accepted);
3425
+ const arriving = replicas.get(partner)?.queue.find((batch) => batch.id === submitted.id);
3426
+ const arrived = other === undefined || arriving === undefined || accepted.destination === undefined
3427
+ ? null
3428
+ : await acceptance(other, local, arriving, {
3429
+ kind: 'accepted',
3430
+ ...accepted.destination,
3431
+ });
3432
+ await adoptedTogether(local, [left, arrived]);
3433
+ const refusedArrival = refusalOf(arrived);
3434
+ if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
3435
+ refusedSource(other, refusedArrival);
3436
+ }
3437
+ return refusalOf(left);
3438
+ });
3439
+ submitLater(partner);
2166
3440
  if (failed === null)
2167
3441
  return true;
2168
3442
  if (failed.kind === 'refused' && continuing())
2169
3443
  refusedSource(room, failed);
2170
3444
  return false;
2171
3445
  }
3446
+ function queueIn(locator) {
3447
+ return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
3448
+ }
3449
+ function behindIn(locator, batch) {
3450
+ const held = replicas.get(locator)?.queue;
3451
+ if (held === undefined)
3452
+ return queuesWithoutReplica.get(locator) !== undefined;
3453
+ return held.findIndex((one) => one.id === batch.id) > 0;
3454
+ }
3455
+ function unpaired(skipped) {
3456
+ for (const locator of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
3457
+ for (const batch of queueIn(locator)) {
3458
+ const partner = partnerOf(batch, locator);
3459
+ if (partner === null || skipped.has(pendingKey(locator, batch.id)))
3460
+ continue;
3461
+ if (queueIn(partner).some((held) => held.id === batch.id))
3462
+ continue;
3463
+ if (unpairedIn(locator, batch))
3464
+ return { locator, batch };
3465
+ }
3466
+ }
3467
+ return null;
3468
+ }
3469
+ async function unpairing(local, locator, batches) {
3470
+ const ids = new Set(batches.map(({ id }) => id));
3471
+ const leaving = batches.map((batch) => ({
3472
+ batch,
3473
+ returned: !localSaves && leavingIn(batch, locator) ? returnedBy('withdrawn') : null,
3474
+ }));
3475
+ const outcome = localSaves ? 'dropped' : 'unpaired';
3476
+ const current = replicas.get(locator);
3477
+ if (current !== undefined) {
3478
+ const settled = await settlement(local, locator, current, current.record, current.queue.filter((one) => !ids.has(one.id)), { leaving });
3479
+ return {
3480
+ ...settled,
3481
+ apply(answer) {
3482
+ for (const batch of batches)
3483
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
3484
+ const set = settled.apply(answer);
3485
+ submitLater(locator);
3486
+ return set;
3487
+ },
3488
+ };
3489
+ }
3490
+ const queue = queueIn(locator).filter((one) => !ids.has(one.id));
3491
+ const written = new Map();
3492
+ for (const { batch, returned } of leaving) {
3493
+ if (returned === null)
3494
+ continue;
3495
+ for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
3496
+ written.set(saved.path, {
3497
+ locator,
3498
+ path: saved.path,
3499
+ revision: roomDraftRevision(),
3500
+ parts: returnedInto((written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path))?.parts, saved.parts),
3501
+ returned,
3502
+ });
3503
+ }
3504
+ }
3505
+ const carried = carriedBack(locator, leaving.flatMap(({ batch, returned }) => (returned === null ? [] : [batch])), written);
3506
+ return {
3507
+ locator,
3508
+ left: batches,
3509
+ commit: {
3510
+ rooms: [],
3511
+ writes: [
3512
+ {
3513
+ locator,
3514
+ queue,
3515
+ roomDrafts: [...written.values()],
3516
+ consumedRoomDrafts: carried.taken,
3517
+ },
3518
+ ...carried.writes,
3519
+ ],
3520
+ },
3521
+ apply() {
3522
+ for (const batch of batches) {
3523
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
3524
+ pendings.delete(pendingKey(locator, batch.id));
3525
+ }
3526
+ if (queue.length > 0)
3527
+ queuesWithoutReplica.set(locator, queue);
3528
+ else
3529
+ queuesWithoutReplica.delete(locator);
3530
+ for (const record of written.values())
3531
+ rememberRoomDraft(record);
3532
+ carried.remember();
3533
+ submitLater(locator);
3534
+ return mergeChangeSets([
3535
+ changeSet(locator, {
3536
+ queue: true,
3537
+ drafts: [...written.keys(), ...carried.taken],
3538
+ }),
3539
+ ...carried.changes,
3540
+ ]);
3541
+ },
3542
+ };
3543
+ }
3544
+ async function unpair(local) {
3545
+ const handled = new Set();
3546
+ for (let held = unpaired(handled); held !== null && continuing(); held = unpaired(handled)) {
3547
+ const { locator, batch } = held;
3548
+ handled.add(pendingKey(locator, batch.id));
3549
+ publishChangeSet(await together(local, [await unpairing(local, locator, [batch])]));
3550
+ }
3551
+ if (handled.size === 0)
3552
+ return;
3553
+ moveTo({ ...status, ...attention() });
3554
+ }
3555
+ async function discardHeld(head, room, local, signal) {
3556
+ const partner = partnerOf(head, room.locator);
3557
+ const other = partner === null ? undefined : discovered.get(partner);
3558
+ await discardPending(other === undefined ? [room] : [room, other], local, signal);
3559
+ }
2172
3560
  async function submitRoom(room, local, authorized, progress, signal) {
2173
3561
  while (continuing() && authorization !== undefined && !signal.aborted) {
2174
3562
  const replica = replicas.get(room.locator);
2175
3563
  const head = replica?.queue[0];
2176
3564
  if (replica === undefined || head === undefined)
2177
3565
  return 'done';
2178
- if (pendings.get(head.id)?.accepted !== undefined)
3566
+ const state = pendings.get(pendingKey(room.locator, head.id));
3567
+ if (state?.accepted !== undefined)
3568
+ return 'done';
3569
+ const partner = partnerOf(head, room.locator);
3570
+ if (partner !== null && !leavingIn(head, room.locator)) {
3571
+ submitLater(partner);
3572
+ return 'done';
3573
+ }
3574
+ if (partner !== null && behindIn(partner, head)) {
3575
+ progress.complete = false;
2179
3576
  return 'done';
2180
- const state = pendings.get(head.id);
3577
+ }
2181
3578
  const { submission, lost: earlier } = state ?? {
2182
3579
  submission: { state: 'waiting' },
2183
3580
  lost: false,
@@ -2193,16 +3590,16 @@ export function composeRuntime(options) {
2193
3590
  }
2194
3591
  const lost = earlier || submission.state === 'submitted';
2195
3592
  if (lost && pastHorizon(head.id)) {
2196
- await discardPending(room, local, signal);
3593
+ await discardHeld(head, room, local, signal);
2197
3594
  return 'done';
2198
3595
  }
2199
3596
  let result;
2200
3597
  journal.record(`submission ${room.locator} ${head.id}`);
2201
- await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
3598
+ await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
2202
3599
  if (discoveryPaused || fences.has(room.locator))
2203
3600
  return 'done';
2204
3601
  if (signal.aborted || writesHeld()) {
2205
- await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
3602
+ await exclusive(() => mark(local, room.locator, head, submission, earlier));
2206
3603
  if (signal.aborted)
2207
3604
  return 'done';
2208
3605
  progress.complete = false;
@@ -2210,11 +3607,17 @@ export function composeRuntime(options) {
2210
3607
  return 'done';
2211
3608
  }
2212
3609
  const opensBefore = openSignals;
2213
- const answered = awaitingAnswer(room.locator);
3610
+ const answeredHere = awaitingAnswer(room.locator);
3611
+ const answeredThere = partner === null ? () => { } : awaitingAnswer(partner);
3612
+ const answered = () => {
3613
+ answeredHere();
3614
+ answeredThere();
3615
+ };
2214
3616
  try {
2215
3617
  result = await options.adapter.mutate(authorized, {
2216
3618
  locator: room.locator,
2217
3619
  changeBatch: head,
3620
+ file: batchFile(head),
2218
3621
  ...(state?.upload === undefined ? {} : { upload: state.upload }),
2219
3622
  }, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
2220
3623
  }
@@ -2239,9 +3642,10 @@ export function composeRuntime(options) {
2239
3642
  return 'again';
2240
3643
  }
2241
3644
  if (result.kind === 'room-not-readable') {
3645
+ const unread = discovered.get(result.locator) ?? room;
2242
3646
  progress.complete = false;
2243
- unreadable.add(room.locator);
2244
- list(room, false);
3647
+ unreadable.add(unread.locator);
3648
+ list(unread, false);
2245
3649
  emitRooms();
2246
3650
  return 'done';
2247
3651
  }
@@ -2256,24 +3660,30 @@ export function composeRuntime(options) {
2256
3660
  }
2257
3661
  if (result.kind === 'write-halted') {
2258
3662
  journal.record(`outcome ${room.locator} ${head.id} write-halted`);
2259
- await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, lost));
3663
+ await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
2260
3664
  if (serviceMode === 'open' && openSignals !== opensBefore)
2261
3665
  continue;
2262
3666
  haltedRefusal = true;
2263
3667
  progress.complete = false;
2264
3668
  return 'done';
2265
3669
  }
3670
+ const other = partner === null ? undefined : discovered.get(partner);
2266
3671
  if (result.kind === 'conflicted' ||
2267
3672
  result.kind === 'rejected' ||
2268
3673
  result.kind === 'expired' ||
2269
3674
  result.kind === 'retryable-refusal') {
2270
3675
  if (result.kind === 'expired' && lost && result.restored !== true) {
2271
3676
  journal.record(`outcome ${room.locator} ${head.id} expired`);
2272
- await discardPending(room, local, signal);
3677
+ await discardHeld(head, room, local, signal);
2273
3678
  return 'done';
2274
3679
  }
2275
3680
  if (result.kind === 'expired') {
2276
- await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
3681
+ await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
3682
+ const there = other === undefined ? null : await advance(other, local, authorized, continuing);
3683
+ if (other !== undefined && there !== null) {
3684
+ await withhold(there, other, local, progress);
3685
+ return 'done';
3686
+ }
2277
3687
  const withheld = await advance(room, local, authorized, continuing);
2278
3688
  if (withheld !== null) {
2279
3689
  await withhold(withheld, room, local, progress);
@@ -2290,11 +3700,25 @@ export function composeRuntime(options) {
2290
3700
  if (result.kind === 'retryable-refusal') {
2291
3701
  const reset = result.reset;
2292
3702
  progress.complete = false;
2293
- await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
3703
+ await exclusive(() => mark(local, room.locator, head, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
2294
3704
  return reset === undefined ? 'again' : { at: Date.parse(reset) };
2295
3705
  }
2296
3706
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
2297
3707
  const left = leaving(head, result);
3708
+ const refreshed = async () => ({
3709
+ there: other === undefined ? null : await advance(other, local, authorized, continuing),
3710
+ here: await advance(room, local, authorized, continuing),
3711
+ });
3712
+ const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
3713
+ if (judged !== null && (judged.there !== null || judged.here !== null)) {
3714
+ await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
3715
+ if (other !== undefined && judged.there !== null) {
3716
+ await withhold(judged.there, other, local, progress);
3717
+ }
3718
+ if (judged.here !== null)
3719
+ await withhold(judged.here, room, local, progress);
3720
+ return 'done';
3721
+ }
2298
3722
  await exclusive(async () => {
2299
3723
  if (!continuing())
2300
3724
  return;
@@ -2302,25 +3726,29 @@ export function composeRuntime(options) {
2302
3726
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
2303
3727
  return;
2304
3728
  }
2305
- const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
2306
- leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
2307
- (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
2308
- ? { ...left, returned: null }
2309
- : left,
2310
- });
2311
- publishChangeSet(settled());
3729
+ const confirmedLayer = await sliceAt(current.record, 'confirmed', [head]);
3730
+ const held = (result.kind === 'expired' &&
3731
+ (await landedWhole(room.locator, head, confirmedLayer))) ||
3732
+ (result.kind === 'conflicted' &&
3733
+ (await landedWhole(room.locator, head, confirmedLayer, result.affected)));
3734
+ publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
3735
+ await unpair(local);
2312
3736
  moveTo({ ...status, ...attention() });
2313
3737
  });
2314
3738
  if (result.kind === 'expired')
2315
3739
  continue;
2316
- const withheld = await advance(room, local, authorized, continuing);
2317
- if (withheld !== null) {
2318
- await withhold(withheld, room, local, progress);
3740
+ const { there, here } = judged ?? (await refreshed());
3741
+ if (other !== undefined && there !== null)
3742
+ await withhold(there, other, local, progress);
3743
+ if (here !== null) {
3744
+ await withhold(here, room, local, progress);
2319
3745
  return 'done';
2320
3746
  }
2321
3747
  continue;
2322
3748
  }
2323
- const kept = await adopted(room, local, head, result).finally(answered);
3749
+ const kept = await (partner === null
3750
+ ? adopted(room, local, head, result)
3751
+ : adoptedMove(room, partner, local, head, result)).finally(answered);
2324
3752
  if (!kept) {
2325
3753
  progress.complete = false;
2326
3754
  return 'done';
@@ -2331,44 +3759,67 @@ export function composeRuntime(options) {
2331
3759
  function pastHorizon(identity) {
2332
3760
  return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
2333
3761
  }
2334
- async function discardPending(room, local, signal) {
2335
- const { locator } = room;
3762
+ async function discardPending(rooms, local, signal) {
2336
3763
  const discarded = await exclusive(async () => {
2337
- if (fences.has(locator))
2338
- return false;
2339
- const before = replicas.get(locator);
2340
- const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
2341
- fences.set(locator, { at: discoveries, erased: false });
2342
- lastFenced.set(locator, ++fencings);
2343
- try {
2344
- await local.discard(locator);
2345
- }
2346
- finally {
2347
- fences.delete(locator);
3764
+ const held = rooms
3765
+ .filter(({ locator }) => !fences.has(locator))
3766
+ .map((room) => ({
3767
+ room,
3768
+ before: replicas.get(room.locator),
3769
+ queue: queueIn(room.locator),
3770
+ }));
3771
+ if (held.length === 0)
3772
+ return [];
3773
+ const answer = await local.commit({
3774
+ rooms: held.flatMap(({ room, before }) => before === undefined
3775
+ ? []
3776
+ : [
3777
+ {
3778
+ locator: room.locator,
3779
+ key: before.record.key,
3780
+ roots: before.record.typedSelection?.roots ?? [],
3781
+ overlay: { set: [], cleared: [...before.overlaid] },
3782
+ },
3783
+ ]),
3784
+ discards: held.map(({ room }) => room.locator),
3785
+ });
3786
+ for (const { room, before, queue } of held) {
3787
+ const { locator } = room;
3788
+ lastFenced.set(locator, ++fencings);
3789
+ for (const batch of queue)
3790
+ pendings.delete(pendingKey(locator, batch.id));
3791
+ queuesWithoutReplica.delete(locator);
3792
+ if (before !== undefined) {
3793
+ replicas.set(locator, {
3794
+ ...before,
3795
+ queue: [],
3796
+ overlaid: new Set(),
3797
+ count: answer.counts[locator] ?? before.count,
3798
+ });
3799
+ }
3800
+ journal.record(`room discarded ${locator}`);
2348
3801
  }
2349
- for (const batch of queue)
2350
- pendings.delete(batch.id);
2351
- queuesWithoutReplica.delete(locator);
2352
- const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
2353
- if (after !== null)
2354
- replicas.set(locator, after);
2355
- journal.record(`room discarded ${locator}`);
2356
- publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
3802
+ publishChangeSet(mergeChangeSets(held.map(({ room }) => changeSet(room.locator, {
3803
+ documents: answer.changed[room.locator] ?? [],
3804
+ queue: true,
3805
+ }))));
2357
3806
  moveTo({ ...status, ...attention() });
2358
- return true;
2359
- });
2360
- if (!discarded)
2361
- return;
2362
- const progress = { complete: true, reachable: true };
2363
- await roomTask(locator, async (live) => {
2364
- await Promise.race([
2365
- reload(room, local, progress, () => live() && !signal.aborted),
2366
- abandonedOn(signal),
2367
- ]);
3807
+ return held.map(({ room }) => room);
2368
3808
  });
2369
- if (signal.aborted)
2370
- return;
2371
- await rest(progress);
3809
+ for (const room of discarded) {
3810
+ if (signal.aborted)
3811
+ return;
3812
+ const progress = { complete: true, reachable: true };
3813
+ await roomTask(room.locator, async (live) => {
3814
+ await Promise.race([
3815
+ reload(room, local, progress, () => live() && !signal.aborted),
3816
+ abandonedOn(signal),
3817
+ ]);
3818
+ });
3819
+ if (signal.aborted)
3820
+ return;
3821
+ await rest(progress);
3822
+ }
2372
3823
  }
2373
3824
  function awaitingAnswer(locator) {
2374
3825
  let release = () => { };
@@ -2382,10 +3833,10 @@ export function composeRuntime(options) {
2382
3833
  release();
2383
3834
  };
2384
3835
  }
2385
- async function retainModel(local, room, attached, next) {
2386
- const key = modelKey(room.locator, attached);
2387
- const previous = modelOf(room.locator, attached);
2388
- const heldBefore = replicas.get(room.locator)?.confirmed.record;
3836
+ async function retainModel(local, room, next) {
3837
+ const key = room.locator;
3838
+ const previous = modelOf(room.locator);
3839
+ const heldBefore = replicas.get(room.locator)?.record;
2389
3840
  if (JSON.stringify(previous) === JSON.stringify(next) &&
2390
3841
  (heldBefore === undefined ||
2391
3842
  (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
@@ -2404,32 +3855,47 @@ export function composeRuntime(options) {
2404
3855
  let changed = null;
2405
3856
  if (held !== undefined) {
2406
3857
  const declared = {
2407
- ...held.confirmed.record,
3858
+ ...held.record,
2408
3859
  operations: room.operations,
2409
3860
  replica: room.replica,
2410
3861
  typedSelection: room.typedSelection ?? null,
2411
- ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
3862
+ model: next,
2412
3863
  };
2413
- const reclassified = sameMembership(declared, held.confirmed.record)
2414
- ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
2415
- : await readReclassified(declared, held.confirmed);
2416
- const record = reclassified.kind === 'staged'
3864
+ let read = 'staged';
3865
+ if (!sameMembership(declared, held.record)) {
3866
+ const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), composed(declared));
3867
+ read =
3868
+ verdict.verdict !== 'refuse'
3869
+ ? 'staged'
3870
+ : verdict.rule === 'model-unavailable'
3871
+ ? 'deferred'
3872
+ : { kind: 'refused', refusal: verdict };
3873
+ }
3874
+ const record = read === 'staged'
2417
3875
  ? declared
2418
- : {
2419
- ...declared,
2420
- typedSelection: held.confirmed.record.typedSelection,
2421
- };
2422
- const read = reclassified.kind === 'deferred'
2423
- ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
2424
- : reclassified;
2425
- await local.write({ next: record, previous: held.confirmed.record });
2426
- const replica = read.kind === 'staged'
2427
- ? await stepped(replicating(read.confirmed, held.queue, held))
2428
- : { ...held, confirmed: { ...held.confirmed, record } };
2429
- const set = await stepped(changing(room.locator, held, replica, { room: true }));
2430
- replicas.set(room.locator, replica);
2431
- changed = heldChange(room.locator, held, replica, set, { room: true });
2432
- if (read.kind === 'staged') {
3876
+ : { ...declared, typedSelection: held.record.typedSelection };
3877
+ const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
3878
+ const replayedLayer = await replayed(record, confirmedLayer, held.queue);
3879
+ const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
3880
+ const answer = await local.commit({
3881
+ rooms: [
3882
+ {
3883
+ locator: room.locator,
3884
+ key: record.key,
3885
+ record: storedRecord(record),
3886
+ roots: record.typedSelection?.roots ?? [],
3887
+ composition: composed(record),
3888
+ overlay: diff.overlay,
3889
+ },
3890
+ ],
3891
+ });
3892
+ const { count, indexed } = committedRoom(room.locator, answer);
3893
+ replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
3894
+ changed = changeSet(room.locator, {
3895
+ documents: answer.changed[room.locator] ?? [],
3896
+ room: true,
3897
+ });
3898
+ if (read === 'staged' || read === 'deferred') {
2433
3899
  if (declarationRefusals.delete(room.locator)) {
2434
3900
  sourceRefusals.delete(room.locator);
2435
3901
  unreadable.delete(room.locator);
@@ -2453,7 +3919,7 @@ export function composeRuntime(options) {
2453
3919
  });
2454
3920
  }
2455
3921
  async function scoped(result, room, local) {
2456
- if ((result.scope === 'content' || result.scope === 'sync') && !result.retryable) {
3922
+ if (result.scope === 'content' && !result.retryable) {
2457
3923
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
2458
3924
  await loseRooms([room.locator]);
2459
3925
  return;
@@ -2465,22 +3931,14 @@ export function composeRuntime(options) {
2465
3931
  });
2466
3932
  let scopedAway = room;
2467
3933
  if (result.scope === 'model')
2468
- await retainModel(local, room, 'collection', {
3934
+ await retainModel(local, room, {
2469
3935
  status: 'unavailable',
2470
3936
  reason: 'not-readable',
2471
3937
  });
2472
3938
  else if (result.scope !== 'operation') {
2473
3939
  unreadable.add(room.locator);
2474
- scopedAway = {
2475
- ...room,
2476
- replica: { ...room.replica, admission: 'unavailable' },
2477
- operations: {
2478
- ...room.operations,
2479
- ...(result.scope === 'content' ? { read: [] } : {}),
2480
- ...(result.scope === 'sync' ? { sync: [] } : {}),
2481
- },
2482
- };
2483
- await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
3940
+ scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
3941
+ await retainModel(local, scopedAway, modelOf(room.locator));
2484
3942
  }
2485
3943
  if (!continuing())
2486
3944
  return;
@@ -2490,17 +3948,10 @@ export function composeRuntime(options) {
2490
3948
  }
2491
3949
  function heldRooms(listed) {
2492
3950
  return listed.flatMap((room) => {
2493
- const record = replicas.get(room.locator)?.confirmed.record;
3951
+ const record = replicas.get(room.locator)?.record;
2494
3952
  if (record === undefined || record.replica.selection !== room.replica.selection)
2495
3953
  return [];
2496
- return [
2497
- {
2498
- locator: room.locator,
2499
- roomVersion: record.roomVersion,
2500
- documents: record.documents,
2501
- revisions: record.revisions,
2502
- },
2503
- ];
3954
+ return [{ locator: room.locator, roomVersion: record.roomVersion }];
2504
3955
  });
2505
3956
  }
2506
3957
  async function synchronize(listed, local, progress, attempt, live) {
@@ -2528,7 +3979,7 @@ export function composeRuntime(options) {
2528
3979
  progress.complete = false;
2529
3980
  return false;
2530
3981
  }
2531
- const prior = replicas.get(room.locator)?.confirmed.record;
3982
+ const prior = replicas.get(room.locator)?.record;
2532
3983
  if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
2533
3984
  list(prior, false);
2534
3985
  emitRooms();
@@ -2537,7 +3988,7 @@ export function composeRuntime(options) {
2537
3988
  return false;
2538
3989
  }
2539
3990
  if (prior !== undefined)
2540
- await retainModel(local, room, 'collection', modelOf(room.locator));
3991
+ await retainModel(local, room, modelOf(room.locator));
2541
3992
  if (discoveryPaused || authorization === undefined)
2542
3993
  return false;
2543
3994
  return room.replica.admission === 'admitted';
@@ -2550,10 +4001,8 @@ export function composeRuntime(options) {
2550
4001
  await withhold(withheld, room, local, progress);
2551
4002
  }
2552
4003
  function revoked(locator) {
2553
- return ['collection', 'typed-selection'].some((attached) => {
2554
- const model = modelOf(locator, attached);
2555
- return model.status === 'unavailable' && model.reason === 'revoked';
2556
- });
4004
+ const model = modelOf(locator);
4005
+ return model.status === 'unavailable' && model.reason === 'revoked';
2557
4006
  }
2558
4007
  async function loadRoom(room, local, progress, live) {
2559
4008
  if (!live() || authorization === undefined)
@@ -2576,13 +4025,12 @@ export function composeRuntime(options) {
2576
4025
  if (!live() || authorization === undefined)
2577
4026
  return;
2578
4027
  const bootstrapped = await pullBootstrap(authorization, room, local, live);
2579
- if (!live())
2580
- return;
2581
4028
  if (bootstrapped.kind !== 'staged') {
2582
- await withhold(bootstrapped, room, local, progress);
4029
+ if (live())
4030
+ await withhold(bootstrapped, room, local, progress);
2583
4031
  return;
2584
4032
  }
2585
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
4033
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
2586
4034
  await caughtUp(room, local, progress, live);
2587
4035
  }
2588
4036
  finally {
@@ -2612,10 +4060,7 @@ export function composeRuntime(options) {
2612
4060
  journal.record(`connection discover ${result.kind}`);
2613
4061
  if (result.kind === 'transport-uncertainty') {
2614
4062
  for (const room of rooms.values()) {
2615
- await retainModel(local, room, 'collection', cached(modelOf(room.locator)));
2616
- if (room.typedSelection !== null) {
2617
- await retainModel(local, room, 'typed-selection', cached(modelOf(room.locator, 'typed-selection')));
2618
- }
4063
+ await retainModel(local, room, cached(modelOf(room.locator)));
2619
4064
  }
2620
4065
  progress.reachable = false;
2621
4066
  progress.complete = false;
@@ -2639,12 +4084,13 @@ export function composeRuntime(options) {
2639
4084
  }
2640
4085
  for (const room of result.rooms) {
2641
4086
  if (room.operations === undefined ||
2642
- !['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
4087
+ !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
2643
4088
  room.operations[operation].every((scope) => typeof scope === 'string')) ||
2644
4089
  room.replica === undefined ||
2645
4090
  typeof room.replica.selection !== 'string' ||
2646
4091
  room.replica.selection.length === 0 ||
2647
- !['admitted', 'unavailable'].includes(room.replica.admission)) {
4092
+ !['admitted', 'unavailable'].includes(room.replica.admission) ||
4093
+ !['locked', 'open'].includes(room.replica.offline)) {
2648
4094
  journal.record(`invalid model or replica declaration ${room.locator}`);
2649
4095
  await refuse('missing-required-guarantee');
2650
4096
  notifyReady();
@@ -2668,11 +4114,12 @@ export function composeRuntime(options) {
2668
4114
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
2669
4115
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
2670
4116
  limits = result.providerLimits;
2671
- await local.declare(limits, capabilities);
4117
+ await local.fact('providerLimits', limits);
4118
+ await local.fact('capabilities', capabilities);
2672
4119
  }
2673
4120
  const signedIn = result.member ?? null;
2674
4121
  if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
2675
- await local.keepMember(signedIn);
4122
+ await local.fact('member', signedIn);
2676
4123
  member = signedIn;
2677
4124
  emitMember();
2678
4125
  }
@@ -2806,6 +4253,7 @@ export function composeRuntime(options) {
2806
4253
  function closedSocket(revoked, lost) {
2807
4254
  const opened = socketOpen;
2808
4255
  socketOpen = false;
4256
+ connectionLost();
2809
4257
  if (lost) {
2810
4258
  reads.lost();
2811
4259
  conversations.lost();
@@ -2875,7 +4323,7 @@ export function composeRuntime(options) {
2875
4323
  owed.clear();
2876
4324
  for (const locator of reloading)
2877
4325
  catching.add(locator);
2878
- readSide.recover();
4326
+ liveQueries.recover();
2879
4327
  return reloading;
2880
4328
  }
2881
4329
  async function reloadNow(replaced, progress) {
@@ -2894,12 +4342,9 @@ export function composeRuntime(options) {
2894
4342
  }
2895
4343
  }
2896
4344
  async function confirmModels(room, local) {
2897
- for (const attached of ['collection', 'typed-selection']) {
2898
- const model = modelOf(room.locator, attached);
2899
- if (model.status === 'cached') {
2900
- await retainModel(local, room, attached, { ...model, status: 'current' });
2901
- }
2902
- }
4345
+ const model = modelOf(room.locator);
4346
+ if (model.status === 'cached')
4347
+ await retainModel(local, room, { ...model, status: 'current' });
2903
4348
  }
2904
4349
  function submitLater(locator) {
2905
4350
  if (localSaves)
@@ -3042,7 +4487,7 @@ export function composeRuntime(options) {
3042
4487
  if (synced && !recorded) {
3043
4488
  const completedAt = new Date().toISOString();
3044
4489
  try {
3045
- await (await openLocal()).complete(completedAt);
4490
+ await (await openLocal()).fact('lastCompleted', completedAt);
3046
4491
  if (!live())
3047
4492
  return;
3048
4493
  recorded = true;
@@ -3082,8 +4527,8 @@ export function composeRuntime(options) {
3082
4527
  if (room === undefined || room.replica.admission !== 'admitted')
3083
4528
  return;
3084
4529
  if (held !== undefined &&
3085
- (held.confirmed.record.replica.selection !== room.replica.selection ||
3086
- options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
4530
+ (held.record.replica.selection !== room.replica.selection ||
4531
+ options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
3087
4532
  return;
3088
4533
  }
3089
4534
  const during = underway?.generation === generation ? underway.progress : null;
@@ -3130,6 +4575,7 @@ export function composeRuntime(options) {
3130
4575
  return;
3131
4576
  if (event.kind === 'opened') {
3132
4577
  socketOpen = true;
4578
+ connectionLive();
3133
4579
  generation += 1;
3134
4580
  reads.opened();
3135
4581
  conversations.reopened();
@@ -3144,7 +4590,14 @@ export function composeRuntime(options) {
3144
4590
  void rediscover(event.scope);
3145
4591
  else
3146
4592
  void applyRoom(event.locator, event.roomVersion);
3147
- }, receipts);
4593
+ }, receipts, heldRevisions);
4594
+ }
4595
+ async function heldRevisions(locator, paths) {
4596
+ const held = replicas.get(locator);
4597
+ if (held === undefined || paths.length === 0)
4598
+ return new Map();
4599
+ const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
4600
+ return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
3148
4601
  }
3149
4602
  function writesHeld() {
3150
4603
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -3214,8 +4667,12 @@ export function composeRuntime(options) {
3214
4667
  if (closed)
3215
4668
  return;
3216
4669
  closed = true;
3217
- if (rechecking !== null)
3218
- clearInterval(rechecking);
4670
+ if (probing !== null)
4671
+ clearInterval(probing);
4672
+ for (const resolve of shownWaiters.splice(0))
4673
+ resolve();
4674
+ connectionLive();
4675
+ lockListeners.clear();
3219
4676
  readinessWatches.clear();
3220
4677
  withdrawStream?.();
3221
4678
  withdrawStream = undefined;
@@ -3224,17 +4681,13 @@ export function composeRuntime(options) {
3224
4681
  withdrawServiceMode?.();
3225
4682
  conversations.close();
3226
4683
  void recovery.retire();
3227
- store?.close();
3228
- void opening?.then((opened) => {
3229
- opened.close();
3230
- }, () => { });
3231
4684
  roomListeners.clear();
3232
4685
  refusalListeners.clear();
3233
4686
  storageLossListeners.clear();
3234
4687
  statusListeners.clear();
3235
4688
  serviceModeListeners.clear();
3236
4689
  memberListeners.clear();
3237
- readSide.close();
4690
+ liveQueries.close();
3238
4691
  queueSubscriptions.clear();
3239
4692
  roomDraftListeners.clear();
3240
4693
  presentationListeners.clear();
@@ -3280,7 +4733,7 @@ export function composeRuntime(options) {
3280
4733
  const watchers = seedWatchers.get(plan.key) ?? new Set();
3281
4734
  seedWatchers.set(plan.key, watchers);
3282
4735
  watchers.add(shown);
3283
- const stop = readSide.watch(plan, identityOf(plan), () => {
4736
+ const stop = liveQueries.watch(plan, identityOf(plan), () => {
3284
4737
  const cover = covered(plan);
3285
4738
  awaitPreparation(cover);
3286
4739
  return seedBehind(plan, cover);
@@ -3301,66 +4754,53 @@ export function composeRuntime(options) {
3301
4754
  stop();
3302
4755
  };
3303
4756
  }
3304
- async function searched(input, reach) {
4757
+ async function searched(input, reach, locked) {
3305
4758
  const asked = searchPlanOf(input);
3306
- const plan = { ...asked, limit: capped(asked.limit) };
3307
- const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
4759
+ const filter = {
4760
+ ...filterOf(asked.documents ?? {}, roomFacts(reach), planReach(reach)),
4761
+ locked,
4762
+ };
4763
+ const unlocking = (filter.rooms ?? [])
4764
+ .filter((room) => locked.includes(room))
4765
+ .map((room) => ({ room, awaits: 'unlock' }));
3308
4766
  for (;;) {
3309
4767
  const cover = covered(filter);
3310
4768
  awaitPreparation(cover);
3311
- const pending = 'pending' in cover ? [...cover.pending] : [];
3312
- const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3313
- const held = covered({
3314
- ...filter,
3315
- rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
3316
- });
3317
- const covering = 'pending' in held ? [] : held.coverage.rooms;
3318
- const unindexed = covering
3319
- .filter(({ room }) => !searchIndex.rooms().has(room))
3320
- .map(({ room }) => room);
3321
- for (const room of unindexed)
3322
- pending.push({ room, awaits: 'index' });
3323
- const listed = conversations.searchable();
3324
- const named = reach !== null && !reach.conversations
3325
- ? []
3326
- : (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
3327
- const waiting = named.filter((one) => !searchIndex.conversations().has(one));
3328
- for (const conversation of waiting)
3329
- pending.push({ conversation, awaits: 'index' });
3330
- if (unindexed.length > 0 || waiting.length > 0)
3331
- index(unindexed, waiting);
4769
+ const pending = [...unlocking, ...('pending' in cover ? cover.pending : [])];
3332
4770
  const complete = pending.length === 0 && !('pending' in cover);
4771
+ const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3333
4772
  const answering = complete
3334
4773
  ? filter
3335
- : {
3336
- ...filter,
3337
- rooms: covering
3338
- .filter(({ room }) => searchIndex.rooms().has(room))
3339
- .map(({ room }) => room),
3340
- };
4774
+ : { ...filter, rooms: reachedRooms(filter).filter((room) => !awaited.has(room)) };
3341
4775
  const answered = covered(answering);
3342
4776
  if ('pending' in answered)
3343
4777
  continue;
3344
- const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
3345
- await readSide.settled();
3346
- const seen = searchIndex.changes();
3347
- await searchIndex.caughtUp();
3348
- const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
3349
- if (selected !== null && 'pending' in selected)
3350
- continue;
3351
- if (searchIndex.changes() !== seen ||
3352
- JSON.stringify(covered(answering)) !== JSON.stringify(answered))
4778
+ const listed = conversations.searchable();
4779
+ const named = reach !== null && !reach.conversations
4780
+ ? []
4781
+ : (asked.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
4782
+ const hits = await (await openLocal()).search({
4783
+ text: asked.text,
4784
+ limit: capped(asked.limit),
4785
+ after: asked.after,
4786
+ total: asked.total,
4787
+ documents: {
4788
+ plan: answering,
4789
+ rooms: answered.rooms,
4790
+ beneath: asked.collection
4791
+ ? answered.rooms.map(({ locator, key }) => ({
4792
+ key,
4793
+ roots: replicas.get(locator)?.record.typedSelection?.roots ?? [],
4794
+ }))
4795
+ : [],
4796
+ },
4797
+ conversations: named,
4798
+ });
4799
+ if (JSON.stringify(covered(answering)) !== JSON.stringify(answered))
3353
4800
  continue;
3354
- const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms === null ? null : new Set(answering.rooms), plan.collection
3355
- ? new Map(answered.coverage.rooms.map(({ room }) => [
3356
- room,
3357
- replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
3358
- ]))
3359
- : null);
3360
4801
  const coverage = {
3361
4802
  rooms: answered.coverage.rooms,
3362
- conversations: searchedConversations,
3363
- index: searchIndex.generation(),
4803
+ conversations: named,
3364
4804
  overlay: answered.coverage.overlay,
3365
4805
  };
3366
4806
  return complete
@@ -3374,13 +4814,17 @@ export function composeRuntime(options) {
3374
4814
  roomDraftListeners.add(listener);
3375
4815
  void exclusive(async () => {
3376
4816
  await openLocal();
3377
- if (closed || !roomDraftListeners.has(listener))
3378
- return;
3379
- listener.ready = true;
3380
4817
  }).then(() => {
3381
- if (closed || !listener.ready || !roomDraftListeners.has(listener))
4818
+ if (closed || !roomDraftListeners.has(listener))
3382
4819
  return;
3383
- listener.notify(snapshotRoomDrafts());
4820
+ const snapshot = snapshotRoomDrafts();
4821
+ draftsShown = draftsShown.then(async () => {
4822
+ const value = await snapshot.catch(() => null);
4823
+ if (value === null || closed || !roomDraftListeners.has(listener))
4824
+ return;
4825
+ listener.ready = true;
4826
+ listener.notify(value);
4827
+ });
3384
4828
  }, () => { });
3385
4829
  return () => {
3386
4830
  roomDraftListeners.delete(listener);
@@ -3413,7 +4857,7 @@ export function composeRuntime(options) {
3413
4857
  };
3414
4858
  }
3415
4859
  if (topic === 'readiness') {
3416
- return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
4860
+ return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
3417
4861
  }
3418
4862
  if (topic === 'rooms') {
3419
4863
  roomListeners.add(deliver);
@@ -3475,14 +4919,14 @@ export function composeRuntime(options) {
3475
4919
  return listed();
3476
4920
  },
3477
4921
  query: ((input) => rowsQuery(planned(input, null))),
3478
- search: (input) => searched(input, null),
4922
+ search: (input) => searched(input, null, []),
3479
4923
  types(locator) {
3480
4924
  return {
3481
4925
  collection: modelTypes(modelOf(locator)),
3482
4926
  typedSelection: rooms.get(locator)?.typedSelection === undefined ||
3483
4927
  rooms.get(locator)?.typedSelection === null
3484
4928
  ? null
3485
- : modelTypes(modelOf(locator, 'typed-selection')),
4929
+ : modelTypes(builtIn),
3486
4930
  };
3487
4931
  },
3488
4932
  roomDraft: {
@@ -3495,46 +4939,32 @@ export function composeRuntime(options) {
3495
4939
  return listed;
3496
4940
  });
3497
4941
  },
3498
- read(room, path) {
4942
+ async read(room, path) {
3499
4943
  const record = roomDrafts.get(room)?.get(path);
3500
- return record === undefined ? null : roomDraftOf(record);
4944
+ return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
3501
4945
  },
3502
- write(room, path, revision, input) {
3503
- return exclusive(async () => {
3504
- const parts = readParts(input);
3505
- if (path === '' || normalize(path) !== path)
3506
- throw runtimeError('room-draft-malformed');
3507
- const local = await openLocal();
3508
- const current = roomDrafts.get(room)?.get(path);
3509
- if ((current?.revision ?? null) !== revision) {
3510
- return staleWrite(current === undefined ? null : roomDraftOf(current));
3511
- }
3512
- const file = parts.file === undefined
3513
- ? undefined
3514
- : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
3515
- const record = {
3516
- locator: room,
3517
- path,
3518
- revision: roomDraftRevision(),
3519
- parts: held(current?.parts, parts, file),
3520
- returned: current?.returned ?? null,
3521
- };
3522
- await local.keepRoomDraft(record);
3523
- rememberRoomDraft(record);
3524
- publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
3525
- return { outcome: 'written', roomDraft: roomDraftOf(record) };
3526
- });
4946
+ write(room, path, revision, input, next = roomDraftRevision()) {
4947
+ if (typeof next !== 'string' || next === '')
4948
+ return Promise.reject(runtimeError('room-draft-malformed'));
4949
+ return gathered(`room-draft\u0000${room}\u0000${path}`, { revision, input, next }, (earlier, later) => {
4950
+ if (later.revision !== earlier.next)
4951
+ return null;
4952
+ const parts = gatheredParts(earlier.input, later.input);
4953
+ return parts === null
4954
+ ? null
4955
+ : { revision: earlier.revision, input: parts, next: later.next };
4956
+ }, (written) => writtenRoomDraft(room, path, written.revision, written.input, written.next));
3527
4957
  },
3528
4958
  discard(room, path, revision) {
3529
4959
  return exclusive(async () => {
3530
4960
  const local = await openLocal();
3531
4961
  const current = roomDrafts.get(room)?.get(path);
3532
4962
  if (current === undefined || current.revision !== revision) {
3533
- return staleWrite(current === undefined ? null : roomDraftOf(current));
4963
+ return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
3534
4964
  }
3535
4965
  await local.discardRoomDraft(room, path);
3536
4966
  forgetRoomDraft(room, path);
3537
- publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
4967
+ publishChangeSet(changeSet(room, { drafts: [path] }));
3538
4968
  moveTo({ ...status, ...attention() });
3539
4969
  return { outcome: 'written', roomDraft: null };
3540
4970
  });
@@ -3550,13 +4980,13 @@ export function composeRuntime(options) {
3550
4980
  });
3551
4981
  },
3552
4982
  write(app, value) {
3553
- return keepPresentation(app, () => structuredClone(value));
4983
+ return exclusive(() => keptPresentation(app, () => structuredClone(value)));
3554
4984
  },
3555
4985
  edit(app, members) {
3556
- return keepPresentation(app, (held) => Object.fromEntries([
3557
- ...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(members, member)),
3558
- ...Object.entries(structuredClone(members)).filter(([, value]) => value !== null),
3559
- ]));
4986
+ return gathered(`presentation\u0000${app}`, structuredClone(members), (earlier, later) => ({ ...earlier, ...later }), (edited) => keptPresentation(app, (held) => Object.fromEntries([
4987
+ ...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(edited, member)),
4988
+ ...Object.entries(edited).filter(([, value]) => value !== null),
4989
+ ])));
3560
4990
  },
3561
4991
  },
3562
4992
  save(room, named, writtenBy) {
@@ -3568,15 +4998,22 @@ export function composeRuntime(options) {
3568
4998
  history(locator, path, query) {
3569
4999
  return readHistory(locator, path, query);
3570
5000
  },
5001
+ resolve(room, path, destination) {
5002
+ return resolveReference(room, path, destination);
5003
+ },
3571
5004
  binaryFile: {
5005
+ part: readPart,
3572
5006
  read: readBinaryPath,
3573
5007
  link: linkBinaryPath,
3574
- readReference: readBinaryReference,
5008
+ show: showBinaryPath,
5009
+ attach: attachBinary,
3575
5010
  },
3576
- call(name, input) {
3577
- return recoveredAction(name, input);
5011
+ async call(name, input) {
5012
+ await ready;
5013
+ const room = calledRooms(name, input).find((one) => locked(one));
5014
+ return room === undefined ? recoveredAction(name, input) : lockedRoom(room);
3578
5015
  },
3579
- reached(locator, roomVersion, path) {
5016
+ async reached(locator, roomVersion, path) {
3580
5017
  return path === undefined
3581
5018
  ? reachedBy(locator, roomVersion)
3582
5019
  : writtenAfter(locator, roomVersion, path);
@@ -3584,14 +5021,16 @@ export function composeRuntime(options) {
3584
5021
  log() {
3585
5022
  return journal.export();
3586
5023
  },
5024
+ cleanUp,
3587
5025
  persistence() {
3588
5026
  return persistence === 'granted';
3589
5027
  },
3590
5028
  async retry() {
3591
5029
  journal.record('retry');
3592
- unindexable.clear();
5030
+ unpreparable.clear();
3593
5031
  queueReadiness();
3594
- resume();
5032
+ if (full)
5033
+ await probed();
3595
5034
  },
3596
5035
  observe: observing(watch),
3597
5036
  async reauthorize() {
@@ -3613,12 +5052,15 @@ export function composeRuntime(options) {
3613
5052
  const reachable = {
3614
5053
  md,
3615
5054
  watch,
3616
- referrerScope: (referrer) => options.adapter.referrerScope(referrer),
3617
5055
  plan: planned,
3618
5056
  query: (plan) => rowsQuery(plan),
3619
5057
  watchPlan,
3620
5058
  search: searched,
3621
5059
  watchReadiness,
5060
+ locked,
5061
+ lockedRooms,
5062
+ watchLocks,
5063
+ located,
3622
5064
  };
3623
5065
  return {
3624
5066
  md,
@@ -3633,6 +5075,11 @@ export function composeRuntime(options) {
3633
5075
  journal.record(`persistence ${answer}`);
3634
5076
  queueReadiness();
3635
5077
  },
3636
- within: (reach, port) => narrowed(reachable, reach, port),
5078
+ shown: screen,
5079
+ within: (reach, port) => {
5080
+ if (full)
5081
+ void probed();
5082
+ return narrowed(reachable, reach, port);
5083
+ },
3637
5084
  };
3638
5085
  }