@agentshouse/mdruntime 0.1.0-c9b1e59e5baeb → 0.1.0-ca10e63188fc5

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