@agentshouse/mdruntime 0.1.0-ce15a88ae8478 → 0.1.0-ce5c5bb29be5a

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 (198) hide show
  1. package/README.md +10 -8
  2. package/dist/adapter.d.ts +30 -34
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/attached-model.d.ts +2 -1
  5. package/dist/attached-model.d.ts.map +1 -1
  6. package/dist/attached-model.js +28 -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 +16 -8
  11. package/dist/bootstrap.d.ts.map +1 -1
  12. package/dist/bootstrap.js +139 -94
  13. package/dist/client.d.ts +6 -7
  14. package/dist/client.d.ts.map +1 -1
  15. package/dist/client.js +54 -7
  16. package/dist/collection.d.ts +5 -3
  17. package/dist/collection.d.ts.map +1 -1
  18. package/dist/collection.js +24 -19
  19. package/dist/compile.d.ts.map +1 -1
  20. package/dist/compile.js +82 -30
  21. package/dist/connection.d.ts +8 -7
  22. package/dist/connection.d.ts.map +1 -1
  23. package/dist/connection.js +1047 -915
  24. package/dist/conversation.d.ts +7 -5
  25. package/dist/conversation.d.ts.map +1 -1
  26. package/dist/conversation.js +166 -41
  27. package/dist/durability.d.ts +1 -5
  28. package/dist/durability.d.ts.map +1 -1
  29. package/dist/durability.js +5 -0
  30. package/dist/entry.d.ts +1 -0
  31. package/dist/entry.d.ts.map +1 -1
  32. package/dist/entry.js +1 -1
  33. package/dist/file.d.ts +2 -11
  34. package/dist/file.d.ts.map +1 -1
  35. package/dist/file.js +6 -4
  36. package/dist/fulltext.d.ts.map +1 -1
  37. package/dist/fulltext.js +8 -6
  38. package/dist/host.d.ts +3 -3
  39. package/dist/host.d.ts.map +1 -1
  40. package/dist/host.js +40 -15
  41. package/dist/index.d.ts +9 -9
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +4 -3
  44. package/dist/live.d.ts.map +1 -1
  45. package/dist/live.js +2 -1
  46. package/dist/media.d.ts +8 -0
  47. package/dist/media.d.ts.map +1 -0
  48. package/dist/media.js +218 -0
  49. package/dist/memory.d.ts +2 -2
  50. package/dist/memory.d.ts.map +1 -1
  51. package/dist/memory.js +46 -20
  52. package/dist/model.d.ts +6 -1
  53. package/dist/model.d.ts.map +1 -1
  54. package/dist/model.js +7 -10
  55. package/dist/observation.d.ts +1 -0
  56. package/dist/observation.d.ts.map +1 -1
  57. package/dist/observation.js +1 -1
  58. package/dist/outcome.d.ts +13 -6
  59. package/dist/outcome.d.ts.map +1 -1
  60. package/dist/outcome.js +16 -5
  61. package/dist/overlay.js +1 -1
  62. package/dist/paths.d.ts +1 -0
  63. package/dist/paths.d.ts.map +1 -1
  64. package/dist/paths.js +4 -0
  65. package/dist/query.d.ts +6 -0
  66. package/dist/query.d.ts.map +1 -1
  67. package/dist/reach.d.ts +8 -3
  68. package/dist/reach.d.ts.map +1 -1
  69. package/dist/reach.js +69 -46
  70. package/dist/readiness.d.ts +13 -14
  71. package/dist/readiness.d.ts.map +1 -1
  72. package/dist/readiness.js +2 -8
  73. package/dist/recovery.d.ts +1 -0
  74. package/dist/recovery.d.ts.map +1 -1
  75. package/dist/recovery.js +26 -0
  76. package/dist/refresh.d.ts +6 -2
  77. package/dist/refresh.d.ts.map +1 -1
  78. package/dist/refresh.js +61 -27
  79. package/dist/replica.d.ts +5 -5
  80. package/dist/replica.d.ts.map +1 -1
  81. package/dist/replica.js +4 -7
  82. package/dist/room-draft.d.ts +18 -2
  83. package/dist/room-draft.d.ts.map +1 -1
  84. package/dist/room-draft.js +73 -6
  85. package/dist/rows.d.ts +15 -6
  86. package/dist/rows.d.ts.map +1 -1
  87. package/dist/rows.js +102 -74
  88. package/dist/save.d.ts +2 -0
  89. package/dist/save.d.ts.map +1 -1
  90. package/dist/save.js +13 -7
  91. package/dist/shared.d.ts +0 -1
  92. package/dist/shared.d.ts.map +1 -1
  93. package/dist/shared.js +0 -10
  94. package/dist/status.d.ts +4 -3
  95. package/dist/status.d.ts.map +1 -1
  96. package/dist/status.js +3 -1
  97. package/dist/store.d.ts +19 -26
  98. package/dist/store.d.ts.map +1 -1
  99. package/dist/store.js +26 -51
  100. package/dist/stored.d.ts +10 -18
  101. package/dist/stored.d.ts.map +1 -1
  102. package/dist/stored.js +209 -193
  103. package/dist/tables.d.ts +28 -35
  104. package/dist/tables.d.ts.map +1 -1
  105. package/dist/tables.js +58 -50
  106. package/dist/transfer.d.ts +0 -5
  107. package/dist/transfer.d.ts.map +1 -1
  108. package/dist/validation.d.ts +3 -4
  109. package/dist/validation.d.ts.map +1 -1
  110. package/dist/validation.js +35 -96
  111. package/dist/wire.d.ts +5 -5
  112. package/dist/wire.d.ts.map +1 -1
  113. package/dist/wire.js +2 -2
  114. package/dist/words.d.ts.map +1 -1
  115. package/dist/words.js +8 -2
  116. package/package.json +11 -18
  117. package/AGENTS.md +0 -319
  118. package/dist/chat/acts.d.ts +0 -33
  119. package/dist/chat/acts.d.ts.map +0 -1
  120. package/dist/chat/acts.js +0 -36
  121. package/dist/chat/context.d.ts +0 -3
  122. package/dist/chat/context.d.ts.map +0 -1
  123. package/dist/chat/context.js +0 -10
  124. package/dist/chat/earlier.d.ts +0 -10
  125. package/dist/chat/earlier.d.ts.map +0 -1
  126. package/dist/chat/earlier.js +0 -54
  127. package/dist/chat/house.d.ts +0 -25
  128. package/dist/chat/house.d.ts.map +0 -1
  129. package/dist/chat/house.js +0 -38
  130. package/dist/chat/input.d.ts +0 -11
  131. package/dist/chat/input.d.ts.map +0 -1
  132. package/dist/chat/input.js +0 -92
  133. package/dist/chat/kit.d.ts +0 -61
  134. package/dist/chat/kit.d.ts.map +0 -1
  135. package/dist/chat/kit.js +0 -123
  136. package/dist/chat/lasting.d.ts +0 -6
  137. package/dist/chat/lasting.d.ts.map +0 -1
  138. package/dist/chat/lasting.js +0 -19
  139. package/dist/chat/list.d.ts +0 -28
  140. package/dist/chat/list.d.ts.map +0 -1
  141. package/dist/chat/list.js +0 -103
  142. package/dist/chat/markdown.d.ts +0 -21
  143. package/dist/chat/markdown.d.ts.map +0 -1
  144. package/dist/chat/markdown.js +0 -246
  145. package/dist/chat/message.d.ts +0 -7
  146. package/dist/chat/message.d.ts.map +0 -1
  147. package/dist/chat/message.js +0 -254
  148. package/dist/chat/messages.d.ts +0 -35
  149. package/dist/chat/messages.d.ts.map +0 -1
  150. package/dist/chat/messages.js +0 -210
  151. package/dist/chat/name.d.ts +0 -7
  152. package/dist/chat/name.d.ts.map +0 -1
  153. package/dist/chat/name.js +0 -37
  154. package/dist/chat/opened.d.ts +0 -12
  155. package/dist/chat/opened.d.ts.map +0 -1
  156. package/dist/chat/opened.js +0 -19
  157. package/dist/chat/overlay.d.ts +0 -32
  158. package/dist/chat/overlay.d.ts.map +0 -1
  159. package/dist/chat/overlay.js +0 -103
  160. package/dist/chat/parts.d.ts +0 -58
  161. package/dist/chat/parts.d.ts.map +0 -1
  162. package/dist/chat/parts.js +0 -120
  163. package/dist/chat/pieces.d.ts +0 -62
  164. package/dist/chat/pieces.d.ts.map +0 -1
  165. package/dist/chat/pieces.js +0 -1110
  166. package/dist/chat/process.d.ts +0 -20
  167. package/dist/chat/process.d.ts.map +0 -1
  168. package/dist/chat/process.js +0 -32
  169. package/dist/chat/provider.d.ts +0 -70
  170. package/dist/chat/provider.d.ts.map +0 -1
  171. package/dist/chat/provider.js +0 -635
  172. package/dist/chat/question.d.ts +0 -9
  173. package/dist/chat/question.d.ts.map +0 -1
  174. package/dist/chat/question.js +0 -69
  175. package/dist/chat/questions.d.ts +0 -31
  176. package/dist/chat/questions.d.ts.map +0 -1
  177. package/dist/chat/questions.js +0 -111
  178. package/dist/chat/read.d.ts +0 -5
  179. package/dist/chat/read.d.ts.map +0 -1
  180. package/dist/chat/read.js +0 -13
  181. package/dist/chat/ready.d.ts +0 -37
  182. package/dist/chat/ready.d.ts.map +0 -1
  183. package/dist/chat/ready.js +0 -93
  184. package/dist/chat/room-file.d.ts +0 -8
  185. package/dist/chat/room-file.d.ts.map +0 -1
  186. package/dist/chat/room-file.js +0 -72
  187. package/dist/chat/steady.d.ts +0 -3
  188. package/dist/chat/steady.d.ts.map +0 -1
  189. package/dist/chat/steady.js +0 -26
  190. package/dist/chat/style.d.ts +0 -4
  191. package/dist/chat/style.d.ts.map +0 -1
  192. package/dist/chat/style.js +0 -307
  193. package/dist/chat.d.ts +0 -15
  194. package/dist/chat.d.ts.map +0 -1
  195. package/dist/chat.js +0 -8
  196. package/dist/reads.d.ts +0 -11
  197. package/dist/reads.d.ts.map +0 -1
  198. package/dist/reads.js +0 -98
@@ -1,5 +1,5 @@
1
1
  import { escapePointer, parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
2
- import { attachedModel, NO_ROOM_TYPES, roomTypesOf } from './attached-model.js';
2
+ import { attachedModel, builtInModel, roomTypesOf } from './attached-model.js';
3
3
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
4
4
  import { observing, whole } from './observation.js';
5
5
  import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
@@ -12,29 +12,28 @@ import { bootstrapRoom } from './bootstrap.js';
12
12
  import { changeSet, mergeChangeSets } from './change-set.js';
13
13
  import { createConversationStore, } from './conversation.js';
14
14
  import { leaving as leavingIn, partnerOf, queuedFile, } from './change.js';
15
- import { available, collectionOf, unreadable as unreadableIn, validRoot, } from './collection.js';
15
+ import { available, collectionOf, composedOf, validRoot, } from './collection.js';
16
16
  import { runtimeError } from './failure.js';
17
17
  import { createLog } from './log.js';
18
18
  import { binaryRecord } from './binary.js';
19
+ import { attachedFile } from './file.js';
19
20
  import { openedFiles } from './opened.js';
20
- import { ABSENT_TARGET, absentBinary, absentReference, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
21
+ import { ABSENT_TARGET, absentBinary, attachAnswer, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, OFFLINE, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
21
22
  import { createRecovery } from './recovery.js';
22
- import { doorReads } from './reads.js';
23
23
  import { advancement } from './refresh.js';
24
24
  import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
25
- import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, sameMembership, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
26
- import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
25
+ import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
26
+ import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, readSavedParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
27
27
  import { same, saving, STARTING, transferring, } from './status.js';
28
- import { detectStorageLoss, nextGeneration } from './durability.js';
29
- import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
28
+ import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
29
+ import { readinessOf, } from './readiness.js';
30
30
  import { FencedMap } from './fence.js';
31
- import { fingerprint } from './stored.js';
32
31
  import { sameBinary } from './overlay.js';
33
32
  import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
34
33
  import { identityTime, roomDraftRevision } from './identity.js';
35
- import { beneath, normalize } from './paths.js';
34
+ import { ancestors, beneath, normalize } from './paths.js';
36
35
  import { openRuntimeStore, } from './store.js';
37
- import { changedCleanly, sameComposition, validatedCollection, } from './validation.js';
36
+ import { validatedNeighbourhood } from './validation.js';
38
37
  import { unconfirmed } from './transfer.js';
39
38
  import { governs, holds, narrowed, planReach, } from './reach.js';
40
39
  function within(root, path) {
@@ -55,34 +54,43 @@ const CONVERSATION_LANE = 'conversations';
55
54
  const ROOM_READS = 256;
56
55
  const ROOM_ATTEMPTS = 16;
57
56
  const ERASE_BATCH = 512;
58
- const REDERIVE_BATCH = 256;
59
- const WALK_BYTES = 1 << 20;
57
+ const DERIVE_BYTES = 1 << 23;
58
+ const DERIVE_FLOOR = 1 << 18;
59
+ // 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.
60
+ const DERIVE_AIM_MS = 250;
61
+ // A build past its floor of text stops after a second and a half of work, its full-text writes included, so a Room whose text costs ten times the usual holds a Save, a query or the next part's progress behind no longer build.
62
+ const DERIVE_LONGEST_MS = 1_500;
60
63
  const QUERY_BYTES = 1 << 20;
61
64
  const STAGE_BYTES = 1 << 20;
62
- const CLEAN_CHANGES = 2_000;
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 STREAMS_HELD = 4;
69
+ const NOTHING_HELD = { bytes: 0 };
70
+ const QUEUED_TEXT = 1 << 23;
71
+ // Bytes still arriving are reported at most this often, so readiness is not rebuilt for every chunk the network hands over while a slow link still moves the figure.
72
+ const ARRIVAL_REPORT_MS = 250;
63
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.
64
74
  const LOCK_GRACE_MS = 120_000;
65
75
  function operationOf(input) {
66
76
  const operation = input?.operation_id;
67
77
  return typeof operation === 'string' ? operation : null;
68
78
  }
69
- function abandonedOn(signal) {
70
- return new Promise((resolve) => {
71
- if (signal.aborted)
72
- resolve(null);
73
- else
74
- signal.addEventListener('abort', () => resolve(null), { once: true });
79
+ function readUntil(read, signal) {
80
+ if (signal.aborted)
81
+ return Promise.resolve({ kind: 'transport-uncertainty' });
82
+ return new Promise((resolve, reject) => {
83
+ const stop = () => resolve({ kind: 'transport-uncertainty' });
84
+ signal.addEventListener('abort', stop, { once: true });
85
+ read.then(resolve, reject).finally(() => signal.removeEventListener('abort', stop));
75
86
  });
76
87
  }
77
- function roomLane(locator) {
78
- return `room ${locator}`;
79
- }
80
88
  function pendingKey(locator, id) {
81
89
  return `${locator}\u0000${id}`;
82
90
  }
83
91
  export function composeRuntime(options) {
84
92
  const localSaves = options.adapter.localSaves === true;
85
- const roomTypes = roomTypesOf(options.adapter.roomTypes);
93
+ const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
86
94
  let authorization = options.authorization;
87
95
  let opening;
88
96
  const fences = new Map();
@@ -92,44 +100,46 @@ export function composeRuntime(options) {
92
100
  const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
93
101
  let discoveries = 0;
94
102
  const replicas = new FencedMap(isFenced);
103
+ const reopened = new Set();
104
+ const usableWhenHeld = options.usableWhenHeld ?? [];
95
105
  const queuesWithoutReplica = new FencedMap(isFenced);
96
106
  const pendings = new Map();
97
107
  const seeds = new Map();
98
108
  const seedWatchers = new Map();
99
109
  const roomDrafts = new FencedMap(isFenced);
110
+ const attachedFiles = new FencedMap(isFenced);
100
111
  const presentations = new Map();
101
112
  let limits = [];
102
113
  let capabilities = [];
103
114
  const rooms = new FencedMap(isFenced);
104
115
  const counted = new Map();
105
116
  const unreadable = new Set();
106
- const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
107
- const models = new FencedMap(isModelFenced);
108
- const liveModels = new FencedMap(isModelFenced);
117
+ const builtIn = builtInModel(roomTypes);
118
+ const models = new FencedMap(isFenced);
119
+ const liveModels = new FencedMap(isFenced);
109
120
  const invalidModels = new Set();
110
121
  const discovered = new Map();
111
- const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
122
+ let inFlight = new AbortController();
123
+ const reads = {
124
+ bootstrap: (authorized, request) => readUntil(options.adapter.bootstrap(authorized, request), inFlight.signal),
125
+ refresh: (authorized, request) => readUntil(options.adapter.refresh(authorized, request), inFlight.signal),
126
+ reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
127
+ };
112
128
  let roomsListed = false;
113
129
  const scopedRefusals = new Map();
114
- const sourceRefusals = new Map();
115
- const declarationRefusals = new Set();
116
- function modelKey(locator, attached) {
117
- return `${attached} ${locator}`;
118
- }
119
- function modelOf(locator, attached = 'collection') {
120
- return models.get(modelKey(locator, attached)) ?? NO_MODEL;
130
+ function modelOf(locator) {
131
+ return models.get(locator) ?? NO_MODEL;
121
132
  }
122
- function savedModel(locator, attached) {
123
- return invalidModels.has(modelKey(locator, attached))
133
+ function savedModel(locator) {
134
+ return invalidModels.has(locator)
124
135
  ? { status: 'unavailable', reason: 'revoked' }
125
- : modelOf(locator, attached);
136
+ : modelOf(locator);
137
+ }
138
+ function composed(record) {
139
+ return compositionOf(record, builtIn);
126
140
  }
127
141
  function compositionFor(record) {
128
- return compositionOf({
129
- ...record,
130
- model: savedModel(record.locator, 'collection'),
131
- selectionModel: savedModel(record.locator, 'typed-selection'),
132
- });
142
+ return composed({ ...record, model: savedModel(record.locator) });
133
143
  }
134
144
  let refusal = null;
135
145
  let storageLoss = null;
@@ -154,14 +164,18 @@ export function composeRuntime(options) {
154
164
  const stale = new Set();
155
165
  let generation = 0;
156
166
  let discovering = null;
157
- let opened = null;
167
+ let opened = false;
158
168
  const owed = new Set();
159
- const reloadsDue = new Map();
169
+ const reloadsDue = new Set();
160
170
  const lanes = new Map();
161
- let roomReads = { generation, free: ROOM_READS, waiting: [] };
171
+ const roomLanes = new Map();
172
+ let roomWork = 0;
173
+ const roomReads = { free: ROOM_READS, waiting: [] };
162
174
  const working = new Set();
163
175
  let mutations = Promise.resolve();
176
+ let gathering = null;
164
177
  const roomListeners = new Set();
178
+ const unlistedRooms = new Set();
165
179
  const refusalListeners = new Set();
166
180
  const storageLossListeners = new Set();
167
181
  const statusListeners = new Set();
@@ -181,19 +195,53 @@ export function composeRuntime(options) {
181
195
  const serviceModeListeners = new Set();
182
196
  let member = null;
183
197
  const memberListeners = new Set();
184
- const prepared = new Set();
185
198
  const unpreparedDemand = new Set();
186
199
  const unpreparable = new Set();
187
200
  let preparing = false;
188
201
  let targets = {};
189
202
  let persistence = options.shell.persistence;
190
- let capacity = null;
191
- const paused = new Set();
192
- let rechecking = null;
203
+ let full = false;
204
+ let probing = null;
205
+ let testing = null;
206
+ const database = {
207
+ async ask(request) {
208
+ try {
209
+ return await options.database.ask(request);
210
+ }
211
+ catch (error) {
212
+ if (refusedForSpace(error))
213
+ filled();
214
+ throw error;
215
+ }
216
+ },
217
+ };
193
218
  const readinessWatches = new Set();
194
219
  let readinessQueued = false;
195
220
  const catching = new Set();
196
221
  const transfers = new FencedMap(isFenced);
222
+ const installs = new Set();
223
+ const arrivals = new Map();
224
+ const streaming = new Set();
225
+ const underived = new Map();
226
+ const unindexed = new Map();
227
+ const startedOver = new Map();
228
+ let arrivalReported = 0;
229
+ let streamsFree = STREAMS_HELD;
230
+ let queuedText = 0;
231
+ const installWaiters = [];
232
+ const stageBudget = {
233
+ bytes: STAGE_FLOOR * 4,
234
+ floor: STAGE_FLOOR,
235
+ ceiling: STAGE_BYTES,
236
+ aim: STAGE_AIM_MS,
237
+ };
238
+ const deriveBudget = {
239
+ bytes: DERIVE_FLOOR * 4,
240
+ floor: DERIVE_FLOOR,
241
+ ceiling: DERIVE_BYTES,
242
+ aim: DERIVE_AIM_MS,
243
+ };
244
+ const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
197
245
  const conversations = createConversationStore({
198
246
  agents: declaredAgents,
199
247
  documents: (declaration, after) => rowsQuery(planOf({
@@ -214,16 +262,27 @@ export function composeRuntime(options) {
214
262
  stream: () => options.adapter.conversations,
215
263
  publish: publishChangeSet,
216
264
  online: () => connectionOf() !== 'unreachable' && refusal === null,
265
+ listed: () => roomsListed,
266
+ full: () => full,
217
267
  closed: () => closed,
218
268
  unanswered: resendLater,
219
269
  expired: (operation) => pastHorizon(operation),
220
270
  });
221
271
  const journal = createLog();
222
- const keptFiles = openedFiles(options.database, isFenced);
223
- let notifyReady = () => { };
272
+ const keptFiles = openedFiles(database, isFenced);
273
+ let readyFromStore = () => { };
224
274
  const ready = new Promise((resolve) => {
225
- notifyReady = resolve;
275
+ readyFromStore = resolve;
226
276
  });
277
+ let notifyDiscovered = () => { };
278
+ const firstDiscovery = new Promise((resolve) => {
279
+ notifyDiscovered = resolve;
280
+ });
281
+ let roomsNamed = false;
282
+ function notifyReady() {
283
+ notifyDiscovered();
284
+ readyFromStore();
285
+ }
227
286
  function rememberRoomDraft(record) {
228
287
  const room = roomDrafts.get(record.locator) ?? new Map();
229
288
  room.set(record.path, record);
@@ -243,7 +302,7 @@ export function composeRuntime(options) {
243
302
  return undefined;
244
303
  const local = await openLocal();
245
304
  const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
246
- return { ...slice, collection: collectionOf(slice.documents, compositionOf(held.record)) };
305
+ return { ...slice, collection: collectionOf(slice.documents, composed(held.record)) };
247
306
  }
248
307
  function roomDraftOf(record, visible) {
249
308
  return structuredClone({
@@ -299,14 +358,40 @@ export function composeRuntime(options) {
299
358
  const value = presentations.get(app);
300
359
  return value === undefined ? null : structuredClone(value);
301
360
  }
302
- function keepPresentation(app, next) {
303
- return exclusive(async () => {
304
- const local = await openLocal();
305
- const stored = next(presentations.get(app));
306
- await local.present({ app, value: stored });
307
- presentations.set(app, stored);
308
- publishChangeSet(changeSet('', { presentation: app }));
309
- });
361
+ async function writtenRoomDraft(room, path, revision, input, next) {
362
+ const parts = readParts(input);
363
+ if (path === '' || normalize(path) !== path)
364
+ throw runtimeError('room-draft-malformed');
365
+ const local = await openLocal();
366
+ const current = roomDrafts.get(room)?.get(path);
367
+ if ((current?.revision ?? null) !== revision) {
368
+ return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
369
+ }
370
+ const file = parts.file === undefined
371
+ ? undefined
372
+ : await heldFile(parts.file.blob, parts.file.base === undefined
373
+ ? binaryBase(await visibleOf(room, [path]), path)
374
+ : parts.file.base);
375
+ const record = {
376
+ locator: room,
377
+ path,
378
+ revision: next,
379
+ parts: held(current?.parts, parts, file),
380
+ returned: current?.returned ?? null,
381
+ };
382
+ await local.keepRoomDraft(record, parts.file === undefined || file === undefined
383
+ ? []
384
+ : [{ key: file.localId, blob: parts.file.blob }]);
385
+ rememberRoomDraft(record);
386
+ publishChangeSet(changeSet(room, { drafts: [path] }));
387
+ return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
388
+ }
389
+ async function keptPresentation(app, next) {
390
+ const local = await openLocal();
391
+ const stored = next(presentations.get(app));
392
+ await local.present({ app, value: stored });
393
+ presentations.set(app, stored);
394
+ publishChangeSet(changeSet('', { presentation: app }));
310
395
  }
311
396
  function emitPresentation(app) {
312
397
  if (closed)
@@ -321,6 +406,20 @@ export function composeRuntime(options) {
321
406
  (queuesWithoutReplica.size > 0 ||
322
407
  [...replicas.values()].some((replica) => replica.queue.length > 0)));
323
408
  }
409
+ function unsent() {
410
+ const changed = new Map();
411
+ for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
412
+ for (const { batch, state } of entriesOf(room)) {
413
+ if (state === 'local')
414
+ continue;
415
+ for (const { path } of batch.changes)
416
+ changed.set(`${room}\u0000${path}`, { room, path });
417
+ }
418
+ }
419
+ return [...changed]
420
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
421
+ .map(([, change]) => change);
422
+ }
324
423
  function attention() {
325
424
  const attentionRequired = [];
326
425
  for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
@@ -332,16 +431,9 @@ export function composeRuntime(options) {
332
431
  returned: structuredClone(record.returned),
333
432
  });
334
433
  }
335
- for (const [room, refusal] of sourceRefusals)
336
- attentionRequired.push({ room, refusal: structuredClone(refusal) });
337
434
  attentionRequired.sort((left, right) => {
338
435
  if (left.room !== right.room)
339
436
  return left.room < right.room ? -1 : 1;
340
- if ('refusal' in left || 'refusal' in right) {
341
- if ('refusal' in left && 'refusal' in right)
342
- return 0;
343
- return 'refusal' in left ? -1 : 1;
344
- }
345
437
  if (left.path !== right.path)
346
438
  return left.path < right.path ? -1 : 1;
347
439
  return 0;
@@ -353,7 +445,7 @@ export function composeRuntime(options) {
353
445
  if (reset === null || submission.reset < reset)
354
446
  reset = submission.reset;
355
447
  }
356
- return { attentionRequired, reset };
448
+ return { attentionRequired, unsent: unsent(), reset };
357
449
  }
358
450
  function documentsIn(locator) {
359
451
  return replicas.get(locator)?.count ?? 0;
@@ -426,7 +518,7 @@ export function composeRuntime(options) {
426
518
  const governed = governs(reach, room);
427
519
  const attached = [
428
520
  ...(governed.collection ? [modelOf(room.locator)] : []),
429
- ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
521
+ ...(governed.selection ? [builtIn] : []),
430
522
  ];
431
523
  return {
432
524
  locator: room.locator,
@@ -442,11 +534,26 @@ export function composeRuntime(options) {
442
534
  const value = listed();
443
535
  for (const listener of roomListeners)
444
536
  listener(value);
537
+ emitKnownRooms();
538
+ }
539
+ function emitKnownRooms() {
540
+ conversations.learned();
541
+ if (unlistedRooms.size === 0)
542
+ return;
543
+ if (!roomsListed && rooms.size === 0 && refusal === null && !status.providerUnreachable)
544
+ return;
545
+ const value = listed();
546
+ for (const listener of [...unlistedRooms]) {
547
+ unlistedRooms.delete(listener);
548
+ roomListeners.add(listener);
549
+ listener(value);
550
+ }
445
551
  }
446
552
  function emitRefusal() {
447
553
  queueReadiness();
448
554
  for (const listener of refusalListeners)
449
555
  listener(refusal);
556
+ emitKnownRooms();
450
557
  }
451
558
  function emitStorageLoss() {
452
559
  queueReadiness();
@@ -467,6 +574,7 @@ export function composeRuntime(options) {
467
574
  if (closed)
468
575
  return;
469
576
  queueReadiness();
577
+ emitKnownRooms();
470
578
  const next = status.state === 'synced' && !conversations.current()
471
579
  ? { ...status, state: 'syncing' }
472
580
  : status;
@@ -480,9 +588,6 @@ export function composeRuntime(options) {
480
588
  if (localSaves)
481
589
  return Object.freeze({ batch, state: 'local' });
482
590
  const state = pendings.get(pendingKey(locator, batch.id));
483
- if (state?.accepted !== undefined) {
484
- return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
485
- }
486
591
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
487
592
  }
488
593
  function entriesOf(locator) {
@@ -501,18 +606,25 @@ export function composeRuntime(options) {
501
606
  function unpublished(error) {
502
607
  journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
503
608
  }
609
+ function indexed(locator) {
610
+ return replicas.get(locator)?.indexed === true;
611
+ }
504
612
  async function prepareReplica(locator) {
505
613
  for (;;) {
506
614
  const held = replicas.get(locator);
507
- if (held === undefined || closed || prepared.has(locator))
615
+ if (held === undefined || closed || held.indexed)
508
616
  return;
509
617
  const key = held.record.key;
510
- const done = await exclusive(async () => (await openLocal()).rederive(key, REDERIVE_BATCH));
511
- if (replicas.get(locator)?.record.key !== key)
618
+ const done = await exclusive(async () => {
619
+ const local = await openLocal();
620
+ return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR));
621
+ });
622
+ const now = replicas.get(locator);
623
+ if (now?.record.key !== key)
512
624
  continue;
513
- if (!done)
625
+ if (!done.done)
514
626
  continue;
515
- prepared.add(locator);
627
+ replicas.set(locator, { ...now, indexed: true });
516
628
  publishChangeSet(changeSet(locator, { documents: null }));
517
629
  liveQueries.recover();
518
630
  queueReadiness();
@@ -520,7 +632,7 @@ export function composeRuntime(options) {
520
632
  }
521
633
  }
522
634
  function unprepared() {
523
- return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unpreparable.has(locator));
635
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
524
636
  }
525
637
  function unpreparedBy(locator, error) {
526
638
  unpreparable.add(locator);
@@ -549,7 +661,11 @@ export function composeRuntime(options) {
549
661
  })();
550
662
  }
551
663
  function roomsOfReach(reach) {
552
- return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
664
+ return new Set([
665
+ ...reach.rooms,
666
+ ...reach.documents.map(roomOfKey),
667
+ ...reach.areas.flatMap((area) => (area.types.length === 0 ? [] : [area.room])),
668
+ ]);
553
669
  }
554
670
  function reachedRooms(plan) {
555
671
  const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
@@ -581,7 +697,7 @@ export function composeRuntime(options) {
581
697
  pending.push({ room: locator, awaits: 'acquisition' });
582
698
  continue;
583
699
  }
584
- if (!prepared.has(locator)) {
700
+ if (!replica.indexed) {
585
701
  pending.push({ room: locator, awaits: 'preparation' });
586
702
  continue;
587
703
  }
@@ -615,8 +731,6 @@ export function composeRuntime(options) {
615
731
  return { listed: named.every((room) => replicas.has(room)), rooms: named };
616
732
  }
617
733
  function roomState(locator) {
618
- if (sourceRefusals.has(locator))
619
- return 'refused';
620
734
  const listedRoom = discovered.get(locator);
621
735
  if (roomsListed &&
622
736
  (listedRoom === undefined ||
@@ -625,7 +739,7 @@ export function composeRuntime(options) {
625
739
  return 'unadmitted';
626
740
  if (!replicas.has(locator))
627
741
  return 'acquiring';
628
- if (!prepared.has(locator))
742
+ if (!indexed(locator))
629
743
  return 'preparing';
630
744
  return 'complete';
631
745
  }
@@ -646,21 +760,47 @@ export function composeRuntime(options) {
646
760
  ? 'connected'
647
761
  : 'connecting';
648
762
  }
649
- function refusedIn(locators) {
650
- return locators.flatMap((room) => {
651
- const held = sourceRefusals.get(room);
652
- if (held === undefined)
653
- return [];
654
- const { file, location } = held.fields;
655
- return [
656
- {
657
- room,
658
- file: typeof file === 'string' ? file : '',
659
- rule: held.code,
660
- location: typeof location === 'string' ? location : null,
661
- },
662
- ];
663
- });
763
+ function bytesOf(room, underway) {
764
+ const transfer = transfers.get(room);
765
+ if (transfer !== undefined) {
766
+ const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
767
+ return {
768
+ done: transfer.done.bytes - Math.ceil(owed),
769
+ total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
770
+ };
771
+ }
772
+ const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
773
+ return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
774
+ }
775
+ function installationOf(coverage) {
776
+ const rooms = { done: 0, total: 0 };
777
+ const bytes = { done: 0, total: 0 };
778
+ let installing = 0;
779
+ let downloading = false;
780
+ for (const { room, state } of coverage) {
781
+ if (state === 'unadmitted')
782
+ continue;
783
+ rooms.total += 1;
784
+ if (state === 'complete')
785
+ rooms.done += 1;
786
+ const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
787
+ if (underway)
788
+ installing += 1;
789
+ if (underway && (streaming.has(room) || !arrivals.has(room)))
790
+ downloading = true;
791
+ const now = bytesOf(room, underway);
792
+ const earlier = underway ? startedOver.get(room) : undefined;
793
+ bytes.done += Math.max(now.done, earlier?.done ?? 0);
794
+ bytes.total += Math.max(now.total, earlier?.total ?? 0);
795
+ }
796
+ const step = installing === 0
797
+ ? null
798
+ : downloading
799
+ ? 'downloading'
800
+ : bytes.done < bytes.total
801
+ ? 'writing'
802
+ : 'replaying';
803
+ return { step, rooms, bytes };
664
804
  }
665
805
  function readinessFor(port, reach) {
666
806
  const required = requiredRooms(reach);
@@ -682,18 +822,17 @@ export function composeRuntime(options) {
682
822
  persistence,
683
823
  connection: connectionOf(),
684
824
  house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
685
- capacity: capacity !== null &&
686
- coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
687
- ? capacity
688
- : port.capacity,
689
- refused: refusedIn(required.rooms),
825
+ space: full ? 'full' : port.space,
826
+ installation: installationOf(coverage),
690
827
  lost: storageLoss !== null,
691
- established: targets[port.target]?.established === true,
828
+ established: targets[port.target]?.established === true ||
829
+ (usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
692
830
  });
693
831
  }
694
832
  function establish(name) {
695
833
  targets = { ...targets, [name]: { established: true } };
696
834
  queueReadiness();
835
+ probeEachMinute();
697
836
  void durably((local) => local.fact('targets', targets)).catch(() => {
698
837
  journal.record(`target record failed ${name}`);
699
838
  });
@@ -731,87 +870,52 @@ export function composeRuntime(options) {
731
870
  readinessWatches.delete(watch);
732
871
  };
733
872
  }
734
- function pageBytes(page) {
735
- let bytes = 0;
736
- for (const { path, content } of page.entries)
737
- bytes += (path.length + (content?.length ?? 0)) * 2;
738
- return bytes;
739
- }
740
- function exceeded(error) {
741
- return (typeof error === 'object' &&
742
- error !== null &&
743
- error.name === 'QuotaExceededError');
744
- }
745
- async function estimated() {
746
- const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
747
- return estimate === null ? null : Math.max(0, estimate.total - estimate.usage);
748
- }
749
- function pause(locator, cause) {
750
- paused.add(locator);
751
- capacity = cause;
752
- journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
873
+ function filled() {
874
+ if (full || closed)
875
+ return;
876
+ full = true;
877
+ journal.record('space full');
753
878
  queueReadiness();
754
- if (rechecking !== null)
879
+ probeEachMinute();
880
+ }
881
+ function installedTarget() {
882
+ return ((usableWhenHeld.length > 0 && reopened.size > 0) ||
883
+ Object.values(targets).some((target) => target.established));
884
+ }
885
+ function probeEachMinute() {
886
+ if (!full || probing !== null || closed || !installedTarget())
755
887
  return;
756
- // A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
757
- rechecking = setInterval(() => {
758
- void recheck();
888
+ // 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.
889
+ probing = setInterval(() => {
890
+ void probed();
759
891
  }, 60_000);
760
892
  }
761
- async function recheck() {
762
- const held = capacity;
763
- if (held === null || closed)
764
- return;
765
- const available = await estimated().catch(() => null);
766
- if (available === null || available < (held.needed ?? 0))
767
- return;
768
- if (held.cause === 'storage-exceeded' &&
769
- (held.available === null || available <= held.available))
770
- return;
771
- resume();
893
+ function probed() {
894
+ testing ??= database.ask({ kind: 'space.probe' }).then(() => {
895
+ testing = null;
896
+ spaceFreed();
897
+ return true;
898
+ }, () => {
899
+ testing = null;
900
+ return false;
901
+ });
902
+ return testing;
772
903
  }
773
- function resume() {
774
- if (rechecking !== null)
775
- clearInterval(rechecking);
776
- rechecking = null;
777
- if (paused.size === 0 && capacity === null)
904
+ function spaceFreed() {
905
+ if (!full)
778
906
  return;
779
- paused.clear();
780
- capacity = null;
781
- journal.record('capacity resumed');
907
+ full = false;
908
+ if (probing !== null)
909
+ clearInterval(probing);
910
+ probing = null;
911
+ unpreparable.clear();
912
+ journal.record('space freed');
782
913
  queueReadiness();
783
914
  if (!closed && authorization !== undefined)
784
915
  void sync('unstreamed');
785
916
  }
786
- async function reclaimed(local) {
787
- const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
788
- for (const locator of unreferenced) {
789
- await local.abandon(locator);
790
- transfers.delete(locator);
791
- }
792
- if (unreferenced.length > 0)
793
- journal.record(`capacity reclaimed ${unreferenced.length}`);
794
- return unreferenced.length > 0;
795
- }
796
- async function admitted(local, locator, needed) {
797
- if (paused.has(locator))
798
- return false;
799
- let available = await estimated().catch(() => null);
800
- if (available === null || available >= needed)
801
- return true;
802
- if (await reclaimed(local))
803
- available = await estimated().catch(() => null);
804
- if (available === null || available >= needed)
805
- return true;
806
- pause(locator, { cause: 'insufficient-space', needed, available });
807
- return false;
808
- }
809
- async function exceededOn(locator, bytes) {
810
- const available = await estimated().catch(() => null);
811
- pause(locator, { cause: 'storage-exceeded', needed: bytes, available });
812
- }
813
917
  function seedRoomHeld(room, version) {
814
- return prepared.has(room) && reachedBy(room, version);
918
+ return indexed(room) && reachedBy(room, version);
815
919
  }
816
920
  function seedHeld(at) {
817
921
  return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
@@ -934,6 +1038,10 @@ export function composeRuntime(options) {
934
1038
  }
935
1039
  function publishChangeSet(set) {
936
1040
  applyChangeSet(set);
1041
+ for (const { locator, path } of set.documents) {
1042
+ if (path !== null)
1043
+ attachedFiles.get(locator)?.delete(path);
1044
+ }
937
1045
  if (set.rooms.length > 0 ||
938
1046
  [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
939
1047
  emitRooms();
@@ -952,6 +1060,8 @@ export function composeRuntime(options) {
952
1060
  }
953
1061
  async function refuse(reason, provider = null) {
954
1062
  discoveryPaused = true;
1063
+ inFlight.abort();
1064
+ inFlight = new AbortController();
955
1065
  try {
956
1066
  if (reason === 'revoked-authorization') {
957
1067
  await exclusive(async () => {
@@ -959,27 +1069,24 @@ export function composeRuntime(options) {
959
1069
  const records = [...replicas].map(([locator, held]) => ({
960
1070
  locator,
961
1071
  held,
962
- record: { ...held.record, model: revoked, selectionModel: revoked },
1072
+ record: { ...held.record, model: revoked },
963
1073
  }));
1074
+ let answer = null;
964
1075
  try {
965
1076
  const local = await openLocal();
966
- await local.commit({
1077
+ answer = await local.commit({
967
1078
  rooms: records.map(({ locator, record }) => ({
968
1079
  locator,
969
1080
  key: record.key,
970
1081
  record: storedRecord(record),
971
- composition: compositionOf(record),
1082
+ composed: composedOf(composed(record)),
972
1083
  })),
973
1084
  });
974
1085
  }
975
1086
  catch {
976
1087
  journal.record('model revocation persistence failed');
977
1088
  }
978
- const keys = new Set(models.keys());
979
- for (const locator of rooms.keys()) {
980
- keys.add(modelKey(locator, 'collection'));
981
- keys.add(modelKey(locator, 'typed-selection'));
982
- }
1089
+ const keys = new Set([...models.keys(), ...rooms.keys()]);
983
1090
  for (const key of keys) {
984
1091
  invalidModels.add(key);
985
1092
  models.set(key, revoked);
@@ -989,8 +1096,8 @@ export function composeRuntime(options) {
989
1096
  for (const { locator, held, record } of records) {
990
1097
  if (replicas.get(locator) !== held)
991
1098
  continue;
992
- recomposed(locator, held.record, record);
993
- replicas.set(locator, { ...held, record });
1099
+ const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
1100
+ replicas.set(locator, { ...held, record, indexed });
994
1101
  }
995
1102
  for (const locator of [...rooms.keys()]) {
996
1103
  const room = rooms.get(locator);
@@ -1034,12 +1141,12 @@ export function composeRuntime(options) {
1034
1141
  ? null
1035
1142
  : {
1036
1143
  roots: selection.roots,
1037
- model: modelTypes(modelOf(room.locator, 'typed-selection')),
1144
+ model: modelTypes(builtIn),
1038
1145
  },
1039
1146
  providerOwnedPaths: room.providerOwnedPaths,
1040
1147
  roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
1041
1148
  synchronizationAvailable,
1042
- refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
1149
+ refusal: scopedRefusals.get(room.locator) ?? null,
1043
1150
  });
1044
1151
  }
1045
1152
  async function applyDiscovery(found, started, covered) {
@@ -1123,7 +1230,9 @@ export function composeRuntime(options) {
1123
1230
  queuesWithoutReplica.delete(locator);
1124
1231
  rooms.delete(locator);
1125
1232
  roomDrafts.delete(locator);
1126
- transfers.delete(locator);
1233
+ attachedFiles.delete(locator);
1234
+ untransferred(locator);
1235
+ startedOver.delete(locator);
1127
1236
  catching.delete(locator);
1128
1237
  discovered.delete(locator);
1129
1238
  announced.delete(locator);
@@ -1131,17 +1240,10 @@ export function composeRuntime(options) {
1131
1240
  stale.delete(locator);
1132
1241
  repairable.delete(locator);
1133
1242
  unpreparable.delete(locator);
1134
- paused.delete(locator);
1135
- prepared.delete(locator);
1136
1243
  scopedRefusals.delete(locator);
1137
- sourceRefusals.delete(locator);
1138
- declarationRefusals.delete(locator);
1139
- for (const attached of ['collection', 'typed-selection']) {
1140
- const key = modelKey(locator, attached);
1141
- models.delete(key);
1142
- liveModels.delete(key);
1143
- invalidModels.delete(key);
1144
- }
1244
+ models.delete(locator);
1245
+ liveModels.delete(locator);
1246
+ invalidModels.delete(locator);
1145
1247
  journal.record(`room lost ${locator}`);
1146
1248
  }
1147
1249
  if (fresh.length === 0)
@@ -1189,7 +1291,14 @@ export function composeRuntime(options) {
1189
1291
  });
1190
1292
  await eraseFenced(fresh);
1191
1293
  }
1192
- function restored(key, persisted) {
1294
+ function retyped(persisted) {
1295
+ if (persisted.status === 'unavailable')
1296
+ return persisted;
1297
+ const { state } = attachedModel({ kind: 'model', snapshot: persisted.snapshot }, NO_MODEL, roomTypes);
1298
+ return state.status === 'unavailable' ? state : { ...persisted, types: state.types };
1299
+ }
1300
+ function restored(key, held) {
1301
+ const persisted = retyped(held);
1193
1302
  const prior = models.get(key);
1194
1303
  if (prior !== undefined &&
1195
1304
  prior.status !== 'unavailable' &&
@@ -1220,8 +1329,9 @@ export function composeRuntime(options) {
1220
1329
  const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1221
1330
  targets = held.targets;
1222
1331
  transfers.clear();
1223
- for (const [locator, record] of held.transfers)
1224
- transfers.set(locator, record);
1332
+ arrivals.clear();
1333
+ streaming.clear();
1334
+ startedOver.clear();
1225
1335
  limits = held.providerLimits;
1226
1336
  capabilities = held.capabilities;
1227
1337
  member = held.member;
@@ -1231,7 +1341,7 @@ export function composeRuntime(options) {
1231
1341
  presentations.clear();
1232
1342
  for (const record of held.presentation)
1233
1343
  presentations.set(record.app, record.value);
1234
- conversations.hydrate(heldConversations, chatDrafts);
1344
+ conversations.hydrate(heldConversations, chatDrafts, held.conversationList);
1235
1345
  pendings.clear();
1236
1346
  for (const queue of queues) {
1237
1347
  for (const { batch, state } of queue.pending) {
@@ -1239,7 +1349,7 @@ export function composeRuntime(options) {
1239
1349
  }
1240
1350
  }
1241
1351
  replicas.clear();
1242
- prepared.clear();
1352
+ reopened.clear();
1243
1353
  queuesWithoutReplica.clear();
1244
1354
  for (const [locator, queue] of queuedBatches) {
1245
1355
  if (!held.rooms.has(locator))
@@ -1248,39 +1358,40 @@ export function composeRuntime(options) {
1248
1358
  records.forEach((stored, index) => {
1249
1359
  if (fences.has(stored.locator))
1250
1360
  return;
1251
- const model = restored(modelKey(stored.locator, 'collection'), stored.model);
1252
- const selectionModel = restored(modelKey(stored.locator, 'typed-selection'), stored.selectionModel);
1253
- const record = { ...stored, model, selectionModel };
1361
+ const record = { ...stored, model: restored(stored.locator, stored.model) };
1362
+ reopened.add(record.locator);
1254
1363
  replicas.set(record.locator, {
1255
1364
  record,
1256
1365
  queue: queuedBatches.get(record.locator) ?? [],
1257
1366
  overlaid: held.overlaid.get(record.key) ?? new Set(),
1258
1367
  count: counts[index] ?? 0,
1259
1368
  confirmed: 0,
1369
+ indexed: !held.unindexed.has(record.key),
1260
1370
  });
1261
- if (!held.unprepared.has(record.key))
1262
- prepared.add(record.locator);
1263
1371
  const existing = rooms.get(record.locator);
1264
1372
  if (existing === undefined)
1265
- list(record, false);
1373
+ list(record, record.roomVersion !== null);
1266
1374
  else {
1267
1375
  rooms.set(record.locator, {
1268
1376
  ...existing,
1269
1377
  roomVersion: record.roomVersion,
1270
1378
  operations: record.operations,
1271
1379
  replica: record.replica,
1272
- model: modelTypes(model),
1380
+ model: modelTypes(record.model),
1273
1381
  typedSelection: record.typedSelection === null
1274
1382
  ? null
1275
- : {
1276
- roots: record.typedSelection.roots,
1277
- model: modelTypes(selectionModel),
1278
- },
1383
+ : { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
1279
1384
  });
1280
1385
  }
1281
1386
  });
1282
- for (const key of held.orphans)
1283
- await opened.release(key);
1387
+ try {
1388
+ for (const key of held.orphans)
1389
+ await opened.release(key);
1390
+ }
1391
+ catch (error) {
1392
+ if (!refusedForSpace(error))
1393
+ throw error;
1394
+ }
1284
1395
  if (held.erasing || held.orphans.length > 0)
1285
1396
  queueMicrotask(sweep);
1286
1397
  return {
@@ -1305,7 +1416,7 @@ export function composeRuntime(options) {
1305
1416
  async function open() {
1306
1417
  if (closed)
1307
1418
  throw new Error('runtime closed');
1308
- const opened = openRuntimeStore(isFenced, options.database);
1419
+ const opened = openRuntimeStore(isFenced, database);
1309
1420
  const interrupted = await opened.fences();
1310
1421
  for (const locator of interrupted) {
1311
1422
  fences.set(locator, { at: discoveries, erased: false });
@@ -1351,10 +1462,29 @@ export function composeRuntime(options) {
1351
1462
  return opening;
1352
1463
  }
1353
1464
  function exclusive(work) {
1465
+ gathering = null;
1354
1466
  const next = mutations.then(work, work);
1355
1467
  mutations = next.then(() => undefined, () => undefined);
1356
1468
  return next;
1357
1469
  }
1470
+ function gathered(key, input, merge, work) {
1471
+ const tail = gathering;
1472
+ if (tail !== null && tail.key === key) {
1473
+ const merged = merge(tail.input, input);
1474
+ if (merged !== null) {
1475
+ tail.input = merged;
1476
+ return tail.done;
1477
+ }
1478
+ }
1479
+ const entry = { key, input, done: Promise.resolve() };
1480
+ entry.done = exclusive(() => {
1481
+ if (gathering === entry)
1482
+ gathering = null;
1483
+ return work(entry.input);
1484
+ });
1485
+ gathering = entry;
1486
+ return entry.done;
1487
+ }
1358
1488
  function withdrawable(locator) {
1359
1489
  return (batch) => {
1360
1490
  const state = pendings.get(pendingKey(locator, batch.id));
@@ -1436,26 +1566,14 @@ export function composeRuntime(options) {
1436
1566
  },
1437
1567
  };
1438
1568
  }
1439
- function recomposed(locator, before, after) {
1440
- if (before !== undefined &&
1441
- before.key === after.key &&
1442
- fingerprint(compositionOf(before)) === fingerprint(compositionOf(after))) {
1443
- return;
1444
- }
1445
- if (before !== undefined && before.key === after.key)
1446
- prepared.delete(locator);
1447
- }
1448
- function committedRoom(locator, record, answer) {
1449
- if (answer.prepared[locator] === false)
1450
- prepared.delete(locator);
1451
- else if (answer.prepared[locator] === true)
1452
- prepared.add(locator);
1453
- if (!prepared.has(locator))
1569
+ function committedRoom(locator, answer) {
1570
+ const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
1571
+ if (!indexed)
1454
1572
  prepare([locator]);
1455
- return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0 };
1573
+ return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
1456
1574
  }
1457
1575
  function replayed(record, confirmedLayer, queue) {
1458
- return stepped(replaying(confirmedLayer, queue, record.locator, compositionOf(record)));
1576
+ return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
1459
1577
  }
1460
1578
  async function overlayDiff(record, before, visible, touched) {
1461
1579
  const kept = before !== undefined && before.record.key === record.key
@@ -1475,8 +1593,6 @@ export function composeRuntime(options) {
1475
1593
  function unpairedIn(locator, batch) {
1476
1594
  if (!queueIn(locator).some((held) => held.id === batch.id))
1477
1595
  return false;
1478
- if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
1479
- return false;
1480
1596
  return !leavingIn(batch, locator) || withdrawable(locator)(batch);
1481
1597
  }
1482
1598
  async function together(local, settlements) {
@@ -1518,7 +1634,7 @@ export function composeRuntime(options) {
1518
1634
  const confirmedLayer = overrides === undefined
1519
1635
  ? read
1520
1636
  : await stepped(slicingWith(read, overrides.set, overrides.removed));
1521
- const composition = compositionOf(record);
1637
+ const composition = composed(record);
1522
1638
  const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
1523
1639
  const founded = kept.filter((batch) => !queue.includes(batch));
1524
1640
  const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
@@ -1561,6 +1677,7 @@ export function composeRuntime(options) {
1561
1677
  key: record.key,
1562
1678
  record: storedRecord(record),
1563
1679
  roots: record.typedSelection?.roots ?? [],
1680
+ composed: composedOf(composition),
1564
1681
  ...settling.commit,
1565
1682
  ...(overrides === undefined
1566
1683
  ? {}
@@ -1602,14 +1719,14 @@ export function composeRuntime(options) {
1602
1719
  const confirmedChanged = settling.commit?.published !== undefined ||
1603
1720
  settling.commit?.refreshed === true ||
1604
1721
  overrides !== undefined;
1605
- recomposed(locator, before?.record, record);
1606
- const { count } = committedRoom(locator, record, answer);
1722
+ const { count, indexed } = committedRoom(locator, answer);
1607
1723
  replicas.set(locator, {
1608
1724
  record,
1609
1725
  queue: kept,
1610
1726
  overlaid: touched,
1611
1727
  count,
1612
1728
  confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
1729
+ indexed,
1613
1730
  });
1614
1731
  return mergeChangeSets([
1615
1732
  changeSet(locator, {
@@ -1624,103 +1741,95 @@ export function composeRuntime(options) {
1624
1741
  };
1625
1742
  }
1626
1743
  function adoptModels(locator, record) {
1627
- for (const [attached, model] of [
1628
- ['collection', record.model],
1629
- ['typed-selection', record.selectionModel],
1630
- ]) {
1631
- const key = modelKey(locator, attached);
1632
- models.set(key, model);
1633
- invalidModels.delete(key);
1634
- if (model.status === 'current')
1635
- liveModels.set(key, JSON.stringify(model.snapshot));
1636
- else if (model.status === 'unavailable')
1637
- liveModels.delete(key);
1638
- }
1744
+ const model = record.model;
1745
+ models.set(locator, model);
1746
+ invalidModels.delete(locator);
1747
+ if (model.status === 'current')
1748
+ liveModels.set(locator, JSON.stringify(model.snapshot));
1749
+ else if (model.status === 'unavailable')
1750
+ liveModels.delete(locator);
1639
1751
  }
1640
1752
  function remodel(locator, answers, held) {
1641
- const taken = (answer, previous, attached) => {
1642
- if (answer === undefined)
1643
- return previous;
1644
- const { state, refused } = attachedModel(answer, previous, attached === 'collection' ? roomTypes : NO_ROOM_TYPES);
1645
- if (refused !== null) {
1646
- const where = refused.location === undefined ? '' : ` ${refused.location}`;
1647
- journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
1648
- }
1649
- models.set(modelKey(locator, attached), state);
1650
- return state;
1651
- };
1652
- return {
1653
- collection: taken(answers?.collection, held.collection, 'collection'),
1654
- selection: taken(answers?.selection, held.selection, 'typed-selection'),
1655
- };
1753
+ const answer = answers?.collection;
1754
+ if (answer === undefined)
1755
+ return held;
1756
+ const { state, refused } = attachedModel(answer, held.collection, roomTypes);
1757
+ if (refused !== null) {
1758
+ const where = refused.location === undefined ? '' : ` ${refused.location}`;
1759
+ journal.record(`model invalid ${locator} collection ${refused.identity} ${refused.rule}${where}`);
1760
+ }
1761
+ models.set(locator, state);
1762
+ return { collection: state };
1656
1763
  }
1657
- function publish(local, room, record, landing, live) {
1658
- return exclusive(async () => {
1764
+ function publish(local, room, reached, landing, live) {
1765
+ const landed = exclusive(async () => {
1659
1766
  if (!live()) {
1660
1767
  caughtUpTo(room.locator);
1661
1768
  return true;
1662
1769
  }
1663
1770
  const prior = replicas.get(room.locator);
1664
- if (landing.kind === 'refreshed' &&
1665
- (prior === undefined || prior.record.key !== record.key || prior.confirmed !== landing.base)) {
1666
- await local.unrefresh(record.key);
1771
+ if (!stands(room.locator, landing.from)) {
1772
+ if (landing.kind === 'refreshed') {
1773
+ await local.unrefresh(reached.key);
1774
+ return false;
1775
+ }
1776
+ caughtUpTo(room.locator);
1777
+ await abandoned(local, room.locator);
1667
1778
  return false;
1668
1779
  }
1780
+ const record = landing.kind === 'refreshed' && prior !== undefined
1781
+ ? {
1782
+ ...prior.record,
1783
+ roomVersion: reached.roomVersion,
1784
+ ...(landing.remodelled ? { model: reached.model } : {}),
1785
+ }
1786
+ : reached;
1669
1787
  adoptModels(room.locator, record);
1670
1788
  const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
1671
- const roomVersion = record.roomVersion;
1672
- const accepted = queue.filter((batch) => {
1673
- const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
1674
- return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
1675
- });
1676
1789
  let set;
1677
1790
  try {
1678
- set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
1791
+ set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
1679
1792
  ? {
1680
1793
  room: true,
1681
1794
  view: 'refreshed',
1682
- commit: { refreshed: true, composition: compositionOf(record) },
1795
+ commit: { refreshed: true },
1683
1796
  }
1684
1797
  : {
1685
1798
  room: true,
1686
1799
  view: landing.refreshed ? 'refreshed' : 'confirmed',
1687
1800
  commit: {
1688
1801
  published: { from: prior?.record.key ?? null },
1689
- composition: compositionOf(record),
1690
1802
  ...(landing.refreshed ? { refreshed: true } : {}),
1691
1803
  },
1692
1804
  });
1693
1805
  }
1694
1806
  catch (error) {
1695
- if (!exceeded(error))
1807
+ if (!refusedForSpace(error))
1696
1808
  throw error;
1697
- await exceededOn(room.locator, null);
1698
1809
  caughtUpTo(room.locator);
1699
1810
  return true;
1700
1811
  }
1701
1812
  if (landing.kind === 'bootstrapped') {
1702
- transfers.delete(room.locator);
1813
+ untransferred(room.locator);
1814
+ startedOver.delete(room.locator);
1703
1815
  catching.delete(room.locator);
1704
- sweep();
1816
+ if (prior !== undefined && prior.record.key !== record.key)
1817
+ sweep();
1705
1818
  }
1706
- for (const batch of accepted)
1707
- pendings.delete(pendingKey(room.locator, batch.id));
1708
1819
  stale.delete(room.locator);
1709
1820
  unpreparable.delete(room.locator);
1710
1821
  if (unreadable.delete(room.locator))
1711
1822
  scopedRefusals.delete(room.locator);
1712
- sourceRefusals.delete(room.locator);
1713
- declarationRefusals.delete(room.locator);
1714
1823
  list(room, true);
1715
1824
  publishChangeSet(set);
1716
1825
  await unpair(local);
1717
1826
  moveTo({ ...status, ...attention() });
1718
- options.adapter.roomStream?.published(room.locator, roomVersion);
1719
- if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1827
+ if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1720
1828
  submitLater(room.locator);
1721
1829
  }
1722
1830
  return true;
1723
1831
  });
1832
+ return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
1724
1833
  }
1725
1834
  async function savedRoom(locator) {
1726
1835
  const local = await openLocal();
@@ -1745,12 +1854,7 @@ export function composeRuntime(options) {
1745
1854
  return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
1746
1855
  }
1747
1856
  async function saveParts(locator, named, writtenBy) {
1748
- const read = named.map(({ path, parts }) => ({ path, parts: readParts(parts) }));
1749
- if (read.some(({ path }, at) => path === '' ||
1750
- normalize(path) !== path ||
1751
- read.findIndex((one) => one.path === path) !== at)) {
1752
- throw runtimeError('room-draft-malformed');
1753
- }
1857
+ const read = readSavedParts(named);
1754
1858
  const target = await savedRoom(locator);
1755
1859
  const saved = [];
1756
1860
  const files = [];
@@ -1759,7 +1863,7 @@ export function composeRuntime(options) {
1759
1863
  for (const { path, parts } of read) {
1760
1864
  const file = parts.file === undefined
1761
1865
  ? undefined
1762
- : await heldFile(parts.file.blob, binaryBase(visible, path));
1866
+ : await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
1763
1867
  if (parts.file !== undefined && file !== undefined) {
1764
1868
  files.push({ key: file.localId, blob: parts.file.blob });
1765
1869
  }
@@ -1794,13 +1898,14 @@ export function composeRuntime(options) {
1794
1898
  const referring = removed.length === 0
1795
1899
  ? []
1796
1900
  : (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
1797
- const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
1901
+ const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
1798
1902
  const { visible } = await replayed(record, confirmedLayer, replica.queue);
1799
- const composition = compositionOf(record);
1903
+ const composition = composed(record);
1800
1904
  return {
1801
1905
  record,
1802
1906
  confirmed: confirmedLayer,
1803
1907
  visible,
1908
+ folders: new Set(await local.folders(record.key, 'visible', paths)),
1804
1909
  *visibleOver(queue) {
1805
1910
  return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
1806
1911
  },
@@ -1812,13 +1917,7 @@ export function composeRuntime(options) {
1812
1917
  return false;
1813
1918
  const local = await openLocal();
1814
1919
  const roots = record.typedSelection?.roots ?? [];
1815
- if (composition.model.status === 'unavailable' &&
1816
- (await local.typedWithin(record.key, 'visible', roots, false))) {
1817
- return true;
1818
- }
1819
- return (composition.selection !== null &&
1820
- composition.selection.model.status === 'unavailable' &&
1821
- (await local.typedWithin(record.key, 'visible', roots, true)));
1920
+ return local.typedWithin(record.key, 'visible', roots, false);
1822
1921
  }
1823
1922
  function textDiffers(left, right) {
1824
1923
  return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
@@ -1829,18 +1928,6 @@ export function composeRuntime(options) {
1829
1928
  return undefined;
1830
1929
  return entry.binary !== null ? null : entry.content;
1831
1930
  }
1832
- function walkOf(local, key, view) {
1833
- return async (after) => {
1834
- const page = await local.walk(key, view, after, WALK_BYTES);
1835
- return {
1836
- entries: page.entries.map(({ path, content, binary }) => ({
1837
- path,
1838
- content: binary !== null ? null : content,
1839
- })),
1840
- next: page.next,
1841
- };
1842
- };
1843
- }
1844
1931
  function lookupOf(local, key, view) {
1845
1932
  return async (paths) => {
1846
1933
  if (paths.length === 0)
@@ -1857,37 +1944,6 @@ export function composeRuntime(options) {
1857
1944
  const read = parseFrontmatter(entry.content ?? '');
1858
1945
  return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
1859
1946
  }
1860
- function overriddenWalk(walk, overrides) {
1861
- const sorted = [...overrides.keys()].sort();
1862
- const beyond = (path) => {
1863
- if (path === null)
1864
- return 0;
1865
- let low = 0;
1866
- let high = sorted.length;
1867
- while (low < high) {
1868
- const middle = (low + high) >>> 1;
1869
- if (sorted[middle] <= path)
1870
- low = middle + 1;
1871
- else
1872
- high = middle;
1873
- }
1874
- return low;
1875
- };
1876
- return async (after) => {
1877
- const page = await walk(after);
1878
- const end = page.next;
1879
- const added = sorted
1880
- .slice(beyond(after), end === null ? sorted.length : beyond(end))
1881
- .flatMap((path) => {
1882
- const entry = overrides.get(path);
1883
- return entry === null || entry === undefined
1884
- ? []
1885
- : [{ path, content: entry.binary !== null ? null : entry.content }];
1886
- });
1887
- const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
1888
- return { entries, next: end };
1889
- };
1890
- }
1891
1947
  function overriddenLookup(lookup, overrides) {
1892
1948
  return async (paths) => {
1893
1949
  const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
@@ -1905,9 +1961,8 @@ export function composeRuntime(options) {
1905
1961
  }
1906
1962
  function validatorOf(record, confirmedLayer) {
1907
1963
  return async (resulting, touched) => {
1908
- const composition = compositionFor(record);
1909
1964
  const local = await openLocal();
1910
- const changed = [];
1965
+ const changes = [];
1911
1966
  const overrides = new Map();
1912
1967
  const pace = paced();
1913
1968
  for (const path of new Set([...confirmedLayer.held, ...touched])) {
@@ -1918,19 +1973,13 @@ export function composeRuntime(options) {
1918
1973
  continue;
1919
1974
  overrides.set(path, now);
1920
1975
  if (textOf(was) !== textOf(now))
1921
- changed.push({ path, was: textOf(was), now: textOf(now) });
1976
+ changes.push({ path, was: textOf(was), now: textOf(now) });
1922
1977
  }
1923
- const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
1924
- const clean = available(composition) &&
1925
- changed.length <= CLEAN_CHANGES &&
1926
- (await changedCleanly(changed, compositionOf(record), composition, {
1927
- lookup,
1928
- texts: textsOf(local, record.key, 'confirmed'),
1929
- referrers: referrersOf(local, record.key, 'confirmed'),
1930
- }));
1931
- if (clean)
1932
- return { verdict: 'accept' };
1933
- return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
1978
+ return validatedNeighbourhood(changes, compositionFor(record), {
1979
+ lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
1980
+ texts: textsOf(local, record.key, 'confirmed'),
1981
+ referrers: referrersOf(local, record.key, 'confirmed'),
1982
+ });
1934
1983
  };
1935
1984
  }
1936
1985
  function textsOf(local, key, view) {
@@ -1954,6 +2003,7 @@ export function composeRuntime(options) {
1954
2003
  operations: room.operations,
1955
2004
  providerOwnedPaths: room.providerOwnedPaths,
1956
2005
  visible: saving.visible,
2006
+ folders: saving.folders,
1957
2007
  queued: queuedDocuments(replica.queue),
1958
2008
  unmodeled: await unmodeledIn(record),
1959
2009
  validate: validatorOf(record, saving.confirmed),
@@ -1974,6 +2024,7 @@ export function composeRuntime(options) {
1974
2024
  locator,
1975
2025
  key: saving.record.key,
1976
2026
  roots: saving.record.typedSelection?.roots ?? [],
2027
+ composed: composedOf(composed(saving.record)),
1977
2028
  overlay: diff.overlay,
1978
2029
  });
1979
2030
  }
@@ -2005,7 +2056,7 @@ export function composeRuntime(options) {
2005
2056
  }
2006
2057
  const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
2007
2058
  const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
2008
- const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, sourceSaving.visible)));
2059
+ const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
2009
2060
  if (changed.length > 0)
2010
2061
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2011
2062
  const target = replicas.get(destination);
@@ -2117,6 +2168,8 @@ export function composeRuntime(options) {
2117
2168
  submitLater(locator);
2118
2169
  }
2119
2170
  async function committed(locator, target, saved, consumed, writtenBy, files = []) {
2171
+ if (full)
2172
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
2120
2173
  const into = movedInto(locator, saved);
2121
2174
  if (into !== null) {
2122
2175
  return committedMove(locator, into, target, saved, consumed, writtenBy, files);
@@ -2128,7 +2181,7 @@ export function composeRuntime(options) {
2128
2181
  ...saved.map(({ path }) => path),
2129
2182
  ...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
2130
2183
  ]);
2131
- const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, underneath(held, path, saving.visible)));
2184
+ const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
2132
2185
  if (changed.length > 0)
2133
2186
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2134
2187
  const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
@@ -2156,6 +2209,7 @@ export function composeRuntime(options) {
2156
2209
  providerLimits: limits,
2157
2210
  handle: room.handle,
2158
2211
  visible: saving.visible,
2212
+ folders: saving.folders,
2159
2213
  changes,
2160
2214
  bases: parts.flatMap(({ bases }) => bases),
2161
2215
  queued: queuedDocuments(replica.queue),
@@ -2306,6 +2360,9 @@ export function composeRuntime(options) {
2306
2360
  async function readHistory(locator, path, query = {}) {
2307
2361
  if (closed || refusal !== null)
2308
2362
  return UNCERTAIN;
2363
+ await firstDiscovery;
2364
+ if (!roomsNamed)
2365
+ return UNCERTAIN;
2309
2366
  try {
2310
2367
  await ensureAuthorization();
2311
2368
  }
@@ -2395,14 +2452,12 @@ export function composeRuntime(options) {
2395
2452
  return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
2396
2453
  };
2397
2454
  }
2398
- function binaryRoom(request) {
2399
- if (request.kind === 'path')
2400
- return request.locator;
2401
- return options.adapter.referrerScope(request.referrer)?.locator ?? null;
2402
- }
2403
2455
  async function transferBinary(request, transfer, answer) {
2404
2456
  if (closed || refusal !== null)
2405
2457
  return UNCERTAIN;
2458
+ await firstDiscovery;
2459
+ if (!roomsNamed)
2460
+ return UNCERTAIN;
2406
2461
  try {
2407
2462
  await ensureAuthorization();
2408
2463
  }
@@ -2412,16 +2467,13 @@ export function composeRuntime(options) {
2412
2467
  if (closed || authorization === undefined || refusal !== null)
2413
2468
  return UNCERTAIN;
2414
2469
  const since = fencings;
2415
- journal.record(request.kind === 'path'
2416
- ? `transfer binary ${request.locator} ${request.path}`
2417
- : `transfer binary ${request.fileRef}`);
2470
+ journal.record(`transfer binary ${request.locator} ${request.path}`);
2418
2471
  try {
2419
2472
  const result = await transfer(authorization);
2420
2473
  const settled = result;
2421
2474
  journal.record(`outcome binary ${settled.kind}`);
2422
- const locator = binaryRoom(request);
2423
- if (locator !== null && fencedSince(locator, since))
2424
- return failure({ kind: 'room-not-readable', locator });
2475
+ if (fencedSince(request.locator, since))
2476
+ return failure({ kind: 'room-not-readable', locator: request.locator });
2425
2477
  if (settled.kind === 'scoped-refusal') {
2426
2478
  const room = replicas.get(settled.locator)?.record;
2427
2479
  if (room !== undefined)
@@ -2436,23 +2488,27 @@ export function composeRuntime(options) {
2436
2488
  return UNCERTAIN;
2437
2489
  }
2438
2490
  }
2491
+ function fetchBinary(request, name) {
2492
+ return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2493
+ }
2439
2494
  async function readBinary(request, name) {
2440
2495
  if (closed)
2441
2496
  return UNCERTAIN;
2442
2497
  const held = await keptFiles.find(name);
2443
2498
  if (held !== null)
2444
2499
  return held;
2445
- return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2500
+ return fetchBinary(request, name);
2446
2501
  }
2447
2502
  async function binaryPath(locator, path, revision) {
2448
2503
  if (revision !== undefined)
2449
2504
  return { kind: 'path', locator, path, revision };
2450
2505
  const replica = replicas.get(locator);
2451
- if (replica === undefined)
2452
- return null;
2453
2506
  const local = await openLocal();
2454
- const [held] = await local.entries(replica.record.key, 'visible', [path]);
2455
- if (held?.binary === undefined || held.binary === null)
2507
+ const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
2508
+ const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
2509
+ if (attached !== undefined)
2510
+ return { kind: 'path', locator, path, revision: attached.revision };
2511
+ if (replica === undefined || held?.binary === undefined || held.binary === null)
2456
2512
  return null;
2457
2513
  if (held.binary.state === 'pending')
2458
2514
  return { kind: 'pending', localId: held.binary.localId };
@@ -2468,7 +2524,7 @@ export function composeRuntime(options) {
2468
2524
  const holds = (at) => {
2469
2525
  const kept = confirmedLayer.binaries[at];
2470
2526
  return (kept?.state === 'accepted' &&
2471
- kept.fileRef === record.fileRef &&
2527
+ kept.sha256 === record.sha256 &&
2472
2528
  confirmedLayer.revisions[at] === record.revision);
2473
2529
  };
2474
2530
  let confirmedAt = path;
@@ -2482,49 +2538,91 @@ export function composeRuntime(options) {
2482
2538
  locator,
2483
2539
  path: confirmedAt,
2484
2540
  revision: record.revision,
2485
- fileRef: record.fileRef,
2486
2541
  };
2487
2542
  }
2488
- async function readBinaryPath(locator, path, revision) {
2543
+ async function askedBinary(locator, path, revision) {
2489
2544
  await ready;
2490
2545
  const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2491
- const asked = drafted !== undefined && drafted.localId === revision
2546
+ return drafted !== undefined && drafted.localId === revision
2492
2547
  ? { kind: 'pending', localId: revision }
2493
- : await binaryPath(locator, path, revision);
2548
+ : binaryPath(locator, path, revision);
2549
+ }
2550
+ function pendingBinary(locator, path, localId) {
2551
+ const facts = queuedFacts(locator, path, localId);
2552
+ return facts === null
2553
+ ? absentBinary(path)
2554
+ : {
2555
+ kind: 'binary',
2556
+ file: localId,
2557
+ byteLength: facts.byteLength,
2558
+ mediaType: facts.mediaType,
2559
+ };
2560
+ }
2561
+ async function readBinaryPath(locator, path, revision) {
2562
+ const asked = await askedBinary(locator, path, revision);
2494
2563
  if (asked === null)
2495
2564
  return absentBinary(path);
2496
- if (asked.kind === 'pending') {
2497
- const facts = queuedFacts(locator, path, asked.localId);
2498
- return facts === null
2499
- ? absentBinary(path)
2500
- : {
2501
- kind: 'binary',
2502
- file: asked.localId,
2503
- byteLength: facts.byteLength,
2504
- mediaType: facts.mediaType,
2505
- };
2506
- }
2565
+ if (asked.kind === 'pending')
2566
+ return pendingBinary(locator, path, asked.localId);
2507
2567
  return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
2508
2568
  }
2569
+ async function heldMediaType(locator, path) {
2570
+ const replica = replicas.get(locator);
2571
+ const [held] = replica === undefined
2572
+ ? []
2573
+ : await (await openLocal()).entries(replica.record.key, 'visible', [path]);
2574
+ return held === undefined
2575
+ ? attachedFiles.get(locator)?.get(path)?.mediaType
2576
+ : held.binary?.mediaType;
2577
+ }
2578
+ function linkOf(asked, held) {
2579
+ return transferBinary(asked, (authorization) => options.adapter.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
2580
+ }
2509
2581
  async function linkBinaryPath(locator, path, revision) {
2510
2582
  await ready;
2511
2583
  const asked = await binaryPath(locator, path, revision);
2512
2584
  if (asked === null || asked.kind === 'pending')
2513
2585
  return absentBinary(path);
2514
- return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
2586
+ return linkOf(asked, revision === undefined ? await heldMediaType(locator, path) : undefined);
2515
2587
  }
2516
- async function readBinaryReference(reference) {
2588
+ async function showBinaryPath(locator, path, revision) {
2589
+ const asked = await askedBinary(locator, path, revision);
2590
+ if (asked === null)
2591
+ return absentBinary(path);
2592
+ if (asked.kind === 'pending')
2593
+ return pendingBinary(locator, path, asked.localId);
2594
+ if (closed)
2595
+ return UNCERTAIN;
2596
+ const name = { locator, path: asked.path, revision: asked.revision };
2597
+ const kept = await keptFiles.find(name);
2598
+ if (kept !== null)
2599
+ return kept;
2600
+ const mediaType = await heldMediaType(locator, path);
2601
+ if (mediaType?.startsWith('image/') === true)
2602
+ return fetchBinary(asked, name);
2603
+ return linkOf(asked, revision === undefined ? mediaType : undefined);
2604
+ }
2605
+ async function attachBinary(locator, file) {
2517
2606
  await ready;
2518
- const scope = options.adapter.referrerScope(reference.referrer);
2519
- if (scope === null)
2520
- return absentReference(reference.fileRef);
2521
- return readBinary({ kind: 'reference', fileRef: reference.fileRef, referrer: reference.referrer }, { locator: scope.locator, path: scope.path ?? '', revision: reference.fileRef });
2607
+ await firstDiscovery;
2608
+ if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
2609
+ return OFFLINE;
2610
+ const request = await attachedFile(locator, file);
2611
+ return transferBinary({ locator, path: file.name }, async (held) => {
2612
+ const result = await options.adapter.attach(held, request);
2613
+ if (result.kind === 'attached') {
2614
+ const landed = attachedFiles.get(locator) ?? new Map();
2615
+ landed.set(result.path, { revision: result.revision, mediaType: request.mediaType });
2616
+ attachedFiles.set(locator, landed);
2617
+ }
2618
+ return result;
2619
+ }, attachAnswer);
2522
2620
  }
2523
2621
  function readPart(file, part) {
2524
- return options.database.ask({ kind: 'file.part', key: file, part });
2622
+ return database.ask({ kind: 'file.part', key: file, part });
2525
2623
  }
2526
2624
  async function cleanUp() {
2527
- const freed = await options.database.ask({ kind: 'opened.clean' });
2625
+ const freed = await database.ask({ kind: 'opened.clean' });
2528
2626
  return freed + ((await options.shell.cleanUp?.()) ?? 0);
2529
2627
  }
2530
2628
  function declaredAgents() {
@@ -2656,169 +2754,225 @@ export function composeRuntime(options) {
2656
2754
  options.adapter.reached(locator, written, roomVersion) &&
2657
2755
  !options.adapter.reached(locator, roomVersion, written));
2658
2756
  }
2659
- function overriddenTexts(texts, overrides) {
2660
- return async (paths) => {
2661
- const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
2662
- for (const path of paths) {
2663
- const entry = overrides.get(path);
2664
- if (entry === undefined)
2665
- continue;
2666
- if (entry === null)
2667
- known.delete(path);
2668
- else
2669
- known.set(path, entry.binary !== null ? null : entry.content);
2670
- }
2671
- return known;
2672
- };
2673
- }
2674
- async function unreadableRoom(texts, walk, paths) {
2675
- if (paths !== null) {
2676
- const text = {};
2677
- for (const [path, content] of await texts(paths))
2678
- if (content !== null)
2679
- text[path] = content;
2680
- return stepped(unreadableIn(text, Object.keys(text)));
2681
- }
2682
- for (let after = null;;) {
2683
- const page = await walk(after);
2684
- const text = {};
2685
- for (const { path, content } of page.entries)
2686
- if (content !== null)
2687
- text[path] = content;
2688
- const refused = await stepped(unreadableIn(text, Object.keys(text)));
2689
- if (refused !== null)
2690
- return refused;
2691
- if (page.next === null)
2692
- return null;
2693
- after = page.next;
2694
- }
2695
- }
2696
- async function validatedRoom(local, record, view, previous, overrides = null) {
2697
- const composition = compositionOf(record);
2698
- const modeled = record.model.status !== 'unavailable' || record.selectionModel.status !== 'unavailable';
2699
- const lookup = overrides === null
2700
- ? lookupOf(local, record.key, view)
2701
- : overriddenLookup(lookupOf(local, record.key, view), overrides);
2702
- const texts = overrides === null
2703
- ? textsOf(local, record.key, view)
2704
- : overriddenTexts(textsOf(local, record.key, view), overrides);
2705
- const walk = overrides === null
2706
- ? walkOf(local, record.key, view)
2707
- : overriddenWalk(walkOf(local, record.key, view), overrides);
2708
- let changes = null;
2709
- let reshaped = false;
2710
- if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
2711
- const [was, now] = await Promise.all([
2712
- local.entries(record.key, 'confirmed', previous.changed),
2713
- overrides === null
2714
- ? local.entries(record.key, view, previous.changed)
2715
- : Promise.resolve([]),
2716
- ]);
2717
- const before = new Map(was.map((entry) => [entry.path, entry]));
2718
- const after = overrides === null
2719
- ? new Map(now.map((entry) => [entry.path, entry]))
2720
- : overrides;
2721
- changes = previous.changed.flatMap((path) => {
2722
- const one = before.get(path) ?? null;
2723
- const other = after.get(path) ?? null;
2724
- reshaped ||= textDiffers(one, other);
2725
- const held = textOf(one);
2726
- const next = textOf(other);
2727
- return held === next ? [] : [{ path, was: held, now: next }];
2728
- });
2729
- }
2730
- const named = changes === null ? null : changes.map(({ path }) => path);
2731
- const textChanged = changes === null || changes.length > 0 || reshaped;
2732
- const modelsChanged = previous === null || !sameComposition(compositionOf(previous.record), composition);
2733
- if (modeled && (textChanged || modelsChanged)) {
2734
- const clean = previous !== null &&
2735
- changes !== null &&
2736
- (await changedCleanly(changes, compositionOf(previous.record), composition, {
2737
- lookup,
2738
- texts,
2739
- referrers: referrersOf(local, record.key, 'confirmed'),
2740
- }));
2741
- if (clean)
2742
- return null;
2743
- const verdict = await validatedCollection(walk, lookup, composition);
2744
- if (verdict.verdict === 'accept')
2745
- return null;
2746
- if (verdict.rule !== 'model-unavailable')
2747
- return { kind: 'refused', refusal: verdict };
2748
- return unreadableRoom(texts, walk, named);
2749
- }
2750
- if (textChanged)
2751
- return unreadableRoom(texts, walk, named);
2752
- return null;
2753
- }
2754
- function refreshingOf(local, key, liveness) {
2757
+ function refreshingOf(local, key, composition, liveness, replaying = null) {
2758
+ const write = replaying === null ? exclusive : finishingWrite;
2755
2759
  return {
2760
+ turn() {
2761
+ if (replaying !== null)
2762
+ installing(replaying);
2763
+ },
2756
2764
  prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
2757
2765
  async stage(page) {
2758
- const batches = batched(page.entries);
2759
- for (const [index, entries] of batches.entries()) {
2760
- const last = index === batches.length - 1;
2761
- const staged = await exclusive(async () => {
2766
+ for (let written = 0;;) {
2767
+ const end = batchEnd(page.entries, written);
2768
+ const last = end === page.entries.length;
2769
+ const entries = page.entries.slice(written, end);
2770
+ const staged = await write(async () => {
2762
2771
  if (!liveness())
2763
2772
  return false;
2764
- await local.refresh(key, last
2773
+ await timed(stageBudget, () => local.refresh(key, composition, last
2765
2774
  ? { ...page, entries }
2766
- : { entries, removals: [], folders: [], folderRemovals: [] });
2775
+ : { entries, removals: [], folders: [], folderRemovals: [] }));
2767
2776
  return true;
2768
2777
  });
2769
2778
  if (!staged)
2770
2779
  return false;
2780
+ if (last)
2781
+ return true;
2782
+ written = end;
2771
2783
  }
2772
- return true;
2773
2784
  },
2774
2785
  };
2775
2786
  }
2776
- function batched(entries) {
2777
- const batches = [[]];
2787
+ function batchEnd(entries, from) {
2788
+ let end = from;
2778
2789
  let spent = 0;
2779
- for (const entry of entries) {
2780
- const size = (entry.content?.length ?? 0) + ROW_WEIGHT;
2781
- if (spent + size > STAGE_BYTES && batches.at(-1).length > 0) {
2782
- batches.push([]);
2783
- spent = 0;
2784
- }
2785
- batches.at(-1).push(entry);
2790
+ while (end < entries.length) {
2791
+ const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
2792
+ if (spent + size > stageBudget.bytes && end > from)
2793
+ break;
2786
2794
  spent += size;
2795
+ end += 1;
2796
+ }
2797
+ return end;
2798
+ }
2799
+ async function timed(budget, work) {
2800
+ const started = performance.now();
2801
+ const done = await work();
2802
+ const spent = performance.now() - started;
2803
+ if (spent < budget.aim / 2)
2804
+ budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
2805
+ else if (spent > budget.aim * 2) {
2806
+ budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
2807
+ }
2808
+ return done;
2809
+ }
2810
+ function installWrite(work) {
2811
+ return installTurn(installTurns.starting, work);
2812
+ }
2813
+ function finishingWrite(work) {
2814
+ return installTurn(installTurns.finishing, work);
2815
+ }
2816
+ function installTurn(queue, work) {
2817
+ return new Promise((resolve, reject) => {
2818
+ queue.push(() => void exclusive(work)
2819
+ .then(resolve, reject)
2820
+ .finally(() => {
2821
+ installTurns.busy = false;
2822
+ nextInstallTurn();
2823
+ }));
2824
+ nextInstallTurn();
2825
+ });
2826
+ }
2827
+ function nextInstallTurn() {
2828
+ if (installTurns.busy || installTurns.ahead > 0)
2829
+ return;
2830
+ const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
2831
+ if (turn === undefined)
2832
+ return;
2833
+ installTurns.busy = true;
2834
+ turn();
2835
+ }
2836
+ async function ahead(work) {
2837
+ let taken = false;
2838
+ const hold = {
2839
+ take() {
2840
+ if (taken)
2841
+ return;
2842
+ taken = true;
2843
+ installTurns.ahead += 1;
2844
+ },
2845
+ release() {
2846
+ if (!taken)
2847
+ return;
2848
+ taken = false;
2849
+ installTurns.ahead -= 1;
2850
+ resumeInstall();
2851
+ },
2852
+ };
2853
+ hold.take();
2854
+ try {
2855
+ return await work(hold);
2856
+ }
2857
+ finally {
2858
+ hold.release();
2859
+ }
2860
+ }
2861
+ function resumeInstall() {
2862
+ if (installTurns.ahead > 0)
2863
+ return;
2864
+ void yielded().then(nextInstallTurn);
2865
+ }
2866
+ async function heldStream() {
2867
+ while (streamsFree === 0)
2868
+ await new Promise((admit) => installWaiters.push(admit));
2869
+ streamsFree -= 1;
2870
+ return once(() => {
2871
+ streamsFree += 1;
2872
+ });
2873
+ }
2874
+ async function unqueued() {
2875
+ while (queuedText >= QUEUED_TEXT) {
2876
+ await new Promise((admit) => installWaiters.push(admit));
2787
2877
  }
2788
- return batches;
2789
2878
  }
2790
- async function pullRefresh(authorized, held, liveness) {
2879
+ function once(release) {
2880
+ let released = false;
2881
+ return () => {
2882
+ if (released)
2883
+ return;
2884
+ released = true;
2885
+ release();
2886
+ for (const admit of installWaiters.splice(0))
2887
+ admit();
2888
+ };
2889
+ }
2890
+ function installing(locator) {
2891
+ installs.add(locator);
2892
+ queueReadiness();
2893
+ }
2894
+ function untransferred(locator) {
2895
+ transfers.delete(locator);
2896
+ arrivals.delete(locator);
2897
+ streaming.delete(locator);
2898
+ underived.delete(locator);
2899
+ unindexed.delete(locator);
2900
+ }
2901
+ function setBack(locator) {
2902
+ const now = bytesOf(locator, true);
2903
+ const earlier = startedOver.get(locator);
2904
+ startedOver.set(locator, {
2905
+ done: Math.max(now.done, earlier?.done ?? 0),
2906
+ total: Math.max(now.total, earlier?.total ?? 0),
2907
+ });
2908
+ }
2909
+ function owedIndexes(locator, bytes, text) {
2910
+ if (bytes <= 0 || text <= 0)
2911
+ return;
2912
+ unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
2913
+ }
2914
+ function built(locator, { done, text }) {
2915
+ const owed = unindexed.get(locator) ?? [];
2916
+ let left = text;
2917
+ while (!done && left > 0 && owed.length > 0) {
2918
+ const [first] = owed;
2919
+ if (left >= first.text) {
2920
+ left -= first.text;
2921
+ owed.shift();
2922
+ continue;
2923
+ }
2924
+ owed[0] = {
2925
+ bytes: (first.bytes * (first.text - left)) / first.text,
2926
+ text: first.text - left,
2927
+ };
2928
+ left = 0;
2929
+ }
2930
+ if (done || owed.length === 0)
2931
+ unindexed.delete(locator);
2932
+ queueReadiness();
2933
+ }
2934
+ function installed(locator) {
2935
+ if (installs.delete(locator))
2936
+ queueReadiness();
2937
+ }
2938
+ async function pullRefresh(authorized, held, liveness, hold) {
2791
2939
  if (!liveness())
2792
2940
  return unconfirmed;
2793
- const turn = generation;
2794
2941
  const { record } = held;
2795
- const base = held.confirmed;
2942
+ const from = standingOf(record.locator);
2796
2943
  const local = await openLocal();
2797
2944
  try {
2798
2945
  await exclusive(() => local.unrefresh(record.key));
2799
- const result = await advancement(reads, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, liveness));
2946
+ const result = await advancement({
2947
+ ...reads,
2948
+ async refresh(authorization, request) {
2949
+ hold.release();
2950
+ try {
2951
+ return await reads.refresh(authorization, request);
2952
+ }
2953
+ finally {
2954
+ hold.take();
2955
+ }
2956
+ },
2957
+ }, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, composed(record), liveness));
2800
2958
  if (result.kind !== 'advanced') {
2801
2959
  journal.record(`transfer refresh ${record.locator} ${result.kind}`);
2802
2960
  return result;
2803
2961
  }
2804
- const refused = await validatedRoom(local, result.record, 'refreshed', {
2805
- record,
2806
- changed: [...result.changed],
2807
- });
2808
- if (refused !== null) {
2809
- journal.record(`transfer refresh ${record.locator} refused`);
2810
- return refused;
2811
- }
2812
2962
  journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
2813
- return { kind: 'advanced', record: result.record, base };
2963
+ return {
2964
+ kind: 'advanced',
2965
+ record: result.record,
2966
+ from,
2967
+ remodelled: result.record.model !== record.model,
2968
+ };
2814
2969
  }
2815
2970
  catch (error) {
2971
+ if (refusedForSpace(error))
2972
+ return unconfirmed;
2816
2973
  journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
2817
2974
  throw error;
2818
2975
  }
2819
- finally {
2820
- reads.finished(record.locator, turn);
2821
- }
2822
2976
  }
2823
2977
  const heldContents = new Map();
2824
2978
  function heldContentOf(local, key) {
@@ -2836,63 +2990,118 @@ export function composeRuntime(options) {
2836
2990
  heldContents.set(key, made);
2837
2991
  return made;
2838
2992
  }
2993
+ async function abandoned(local, locator) {
2994
+ await local.abandon(locator);
2995
+ setBack(locator);
2996
+ untransferred(locator);
2997
+ liveQueries.recover();
2998
+ sweep();
2999
+ }
2839
3000
  function staging(local, liveness) {
3001
+ const derived = async (locator, key, composition) => {
3002
+ underived.delete(locator);
3003
+ for (;;) {
3004
+ if (!liveness())
3005
+ return false;
3006
+ const done = await finishingWrite(async () => liveness()
3007
+ ? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
3008
+ : null);
3009
+ if (done === null)
3010
+ return false;
3011
+ built(locator, done);
3012
+ if (done.done)
3013
+ return true;
3014
+ }
3015
+ };
2840
3016
  return {
2841
3017
  held: (locator) => transfers.get(locator) ?? null,
3018
+ async turn(locator, step) {
3019
+ installing(locator);
3020
+ return step === 'downloading' ? heldStream() : () => { };
3021
+ },
3022
+ unqueued,
3023
+ queued(text) {
3024
+ queuedText += text;
3025
+ return once(() => {
3026
+ queuedText -= text;
3027
+ });
3028
+ },
3029
+ arriving(locator) {
3030
+ let ended = false;
3031
+ return {
3032
+ at(bytes) {
3033
+ if (ended)
3034
+ return;
3035
+ arrivals.set(locator, bytes);
3036
+ streaming.add(locator);
3037
+ const now = performance.now();
3038
+ if (now - arrivalReported < ARRIVAL_REPORT_MS)
3039
+ return;
3040
+ arrivalReported = now;
3041
+ queueReadiness();
3042
+ },
3043
+ end() {
3044
+ if (ended)
3045
+ return;
3046
+ ended = true;
3047
+ if (streaming.delete(locator))
3048
+ queueReadiness();
3049
+ },
3050
+ };
3051
+ },
3052
+ announced(locator) {
3053
+ const later = announced.get(locator);
3054
+ return later === undefined ? [] : [later];
3055
+ },
2842
3056
  async stage(record, page, composition) {
2843
- const bytes = pageBytes(page);
2844
- const batches = batched(page.entries);
2845
- let key = record.key;
2846
- for (const [index, entries] of batches.entries()) {
2847
- const last = index === batches.length - 1;
2848
- const staged = await exclusive(async () => {
2849
- if (!liveness())
2850
- return null;
2851
- if (index === 0 && !(await admitted(local, record.locator, bytes)))
2852
- return null;
2853
- try {
2854
- return await local.stage(record, composition, { entries, folders: last ? page.folders : [] }, key, last);
2855
- }
2856
- catch (error) {
2857
- if (!exceeded(error))
2858
- throw error;
2859
- await exceededOn(record.locator, bytes);
2860
- return null;
2861
- }
2862
- });
2863
- if (staged === null)
3057
+ const { locator } = record;
3058
+ installing(locator);
3059
+ if (!liveness())
3060
+ return null;
3061
+ const key = await installWrite(async () => {
3062
+ if (!liveness())
2864
3063
  return null;
2865
- key = staged;
2866
- }
2867
- transfers.set(record.locator, { ...record, key });
3064
+ return local.stage(record, composition, page);
3065
+ });
3066
+ if (key === null || !liveness())
3067
+ return null;
3068
+ const arrived = !transfers.has(locator);
3069
+ const prior = transfers.get(locator)?.done.bytes ?? 0;
3070
+ const held = { ...record, key };
3071
+ transfers.set(locator, held);
3072
+ if (arrived)
3073
+ liveQueries.recover();
3074
+ installing(locator);
2868
3075
  liveQueries.recover();
2869
- return key;
3076
+ const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
3077
+ owedIndexes(locator, record.done.bytes - prior, text);
3078
+ const owed = text + (underived.get(locator) ?? 0);
3079
+ underived.set(locator, owed);
3080
+ if (owed < deriveBudget.bytes)
3081
+ return held;
3082
+ return (await derived(locator, key, composition)) ? held : null;
2870
3083
  },
2871
- async refreshing(key) {
3084
+ derive: derived,
3085
+ async refreshing(locator, key, composition) {
2872
3086
  await exclusive(() => local.unrefresh(key));
2873
- return refreshingOf(local, key, liveness);
3087
+ return refreshingOf(local, key, composition, liveness, locator);
2874
3088
  },
2875
3089
  abandon: (locator) => exclusive(async () => {
2876
- if (!liveness())
2877
- return;
2878
- await local.abandon(locator);
2879
- transfers.delete(locator);
2880
- liveQueries.recover();
2881
- sweep();
3090
+ if (liveness())
3091
+ await abandoned(local, locator);
2882
3092
  }),
2883
- setAside: (locator) => exclusive(async () => {
3093
+ setAside: async (locator) => {
2884
3094
  if (!liveness())
2885
3095
  return;
2886
- await local.abandon(locator, true);
2887
- transfers.delete(locator);
3096
+ setBack(locator);
3097
+ untransferred(locator);
2888
3098
  liveQueries.recover();
2889
- }),
3099
+ },
2890
3100
  release: (key) => exclusive(async () => {
2891
3101
  await local.release(key);
2892
3102
  sweep();
2893
3103
  }),
2894
3104
  content: (key) => heldContentOf(local, key),
2895
- validate: (record, view) => validatedRoom(local, record, view, null),
2896
3105
  };
2897
3106
  }
2898
3107
  function reusable(room, local) {
@@ -2904,30 +3113,28 @@ export function composeRuntime(options) {
2904
3113
  async function pullBootstrap(authorized, room, local, liveness) {
2905
3114
  if (!liveness())
2906
3115
  return unconfirmed;
2907
- const turn = generation;
2908
3116
  if (replicas.has(room.locator)) {
2909
3117
  catching.add(room.locator);
2910
3118
  liveQueries.recover();
2911
3119
  }
2912
3120
  try {
2913
- const result = await bootstrapRoom(reads, authorized, room, {
2914
- collection: modelOf(room.locator),
2915
- selection: modelOf(room.locator, 'typed-selection'),
2916
- }, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
3121
+ 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));
2917
3122
  const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
2918
3123
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
2919
- if (result.kind !== 'staged')
3124
+ if (result.kind !== 'staged') {
2920
3125
  caughtUpTo(room.locator);
3126
+ installed(room.locator);
3127
+ }
2921
3128
  return result;
2922
3129
  }
2923
3130
  catch (error) {
2924
- journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
2925
3131
  caughtUpTo(room.locator);
3132
+ installed(room.locator);
3133
+ if (refusedForSpace(error))
3134
+ return unconfirmed;
3135
+ journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
2926
3136
  throw error;
2927
3137
  }
2928
- finally {
2929
- reads.finished(room.locator, turn);
2930
- }
2931
3138
  }
2932
3139
  function caughtUpTo(locator) {
2933
3140
  if (catching.delete(locator))
@@ -2938,69 +3145,79 @@ export function composeRuntime(options) {
2938
3145
  return (held !== undefined &&
2939
3146
  [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
2940
3147
  }
2941
- async function advance(room, local, authorized, live) {
2942
- const carried = reads.carried(room.locator);
2943
- const withheld = await advanceOnce(room, local, authorized, live);
2944
- if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
2945
- return withheld;
2946
- }
2947
- return advanceOnce(room, local, authorized, live);
3148
+ function standingOf(locator) {
3149
+ const held = replicas.get(locator);
3150
+ return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
3151
+ }
3152
+ function stands(locator, from) {
3153
+ const now = standingOf(locator);
3154
+ return now.key === from.key && now.confirmed === from.confirmed;
2948
3155
  }
2949
- async function advanceOnce(room, local, authorized, live) {
2950
- if (!live() || paused.has(room.locator))
3156
+ async function advance(room, local, authorized, running) {
3157
+ const live = () => running() && !full;
3158
+ if (!live())
2951
3159
  return unconfirmed;
2952
3160
  const held = replicas.get(room.locator);
2953
3161
  if (held !== undefined) {
2954
- const refreshed = await pullRefresh(authorized, held, live);
2955
- if (!live())
2956
- return unconfirmed;
2957
- if (refreshed.kind === 'still-current') {
2958
- stale.delete(room.locator);
2959
- if (sourceRefusals.has(room.locator))
2960
- return null;
2961
- if (unreadable.delete(room.locator)) {
2962
- scopedRefusals.delete(room.locator);
2963
- list(room, true);
2964
- emitRooms();
2965
- }
2966
- return null;
2967
- }
2968
- if (refreshed.kind === 'advanced') {
2969
- const landing = { kind: 'refreshed', base: refreshed.base };
2970
- if (!(await publish(local, room, refreshed.record, landing, live))) {
2971
- const now = replicas.get(room.locator)?.record.roomVersion;
2972
- const reached = now !== undefined &&
2973
- options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
2974
- return reached ? null : advance(room, local, authorized, live);
2975
- }
2976
- await retainModel(local, room, 'collection', modelOf(room.locator));
2977
- return null;
2978
- }
2979
- if (refreshed.kind !== 'beyond-horizon')
3162
+ const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold));
3163
+ if (refreshed === 'behind')
3164
+ return advance(room, local, authorized, live);
3165
+ if (refreshed !== 'beyond-horizon')
2980
3166
  return refreshed;
2981
- if (opened !== null) {
3167
+ if (opened) {
2982
3168
  owed.add(room.locator);
2983
3169
  liveQueries.recover();
2984
3170
  return unconfirmed;
2985
3171
  }
2986
- if (reloadsDue.get(room.locator) === generation)
3172
+ if (reloadsDue.has(room.locator))
2987
3173
  return unconfirmed;
2988
3174
  }
3175
+ const from = standingOf(room.locator);
2989
3176
  const bootstrapped = await pullBootstrap(authorized, room, local, live);
3177
+ if (bootstrapped.kind !== 'staged')
3178
+ return live() ? bootstrapped : unconfirmed;
3179
+ const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
3180
+ if (await publish(local, room, bootstrapped.record, landing, live)) {
3181
+ return live() ? null : unconfirmed;
3182
+ }
3183
+ return reachedBy(room.locator, bootstrapped.record.roomVersion)
3184
+ ? null
3185
+ : advance(room, local, authorized, live);
3186
+ }
3187
+ async function refreshedRoom(room, local, authorized, held, live, hold) {
3188
+ const refreshed = await pullRefresh(authorized, held, live, hold);
2990
3189
  if (!live())
2991
3190
  return unconfirmed;
2992
- if (bootstrapped.kind !== 'staged')
2993
- return bootstrapped;
2994
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
2995
- return null;
3191
+ if (refreshed.kind === 'still-current') {
3192
+ stale.delete(room.locator);
3193
+ if (unreadable.delete(room.locator)) {
3194
+ scopedRefusals.delete(room.locator);
3195
+ list(room, true);
3196
+ emitRooms();
3197
+ }
3198
+ return null;
3199
+ }
3200
+ if (refreshed.kind === 'advanced') {
3201
+ const landing = {
3202
+ kind: 'refreshed',
3203
+ from: refreshed.from,
3204
+ remodelled: refreshed.remodelled,
3205
+ };
3206
+ if (!(await publish(local, room, refreshed.record, landing, live))) {
3207
+ const now = replicas.get(room.locator)?.record.roomVersion;
3208
+ const reached = now !== undefined &&
3209
+ options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3210
+ return reached ? null : 'behind';
3211
+ }
3212
+ await retainModel(local, room, modelOf(room.locator));
3213
+ return null;
3214
+ }
3215
+ return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
2996
3216
  }
2997
3217
  async function withhold(withheld, room, local, progress) {
2998
3218
  if (withheld.kind === 'unconfirmed')
2999
3219
  return;
3000
- if (withheld.kind === 'refused')
3001
- stale.delete(room.locator);
3002
- else
3003
- stale.add(room.locator);
3220
+ stale.add(room.locator);
3004
3221
  if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
3005
3222
  const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
3006
3223
  repairable.set(room.locator, reset);
@@ -3022,30 +3239,10 @@ export function composeRuntime(options) {
3022
3239
  list(room, false);
3023
3240
  emitRooms();
3024
3241
  }
3025
- else if (withheld.kind === 'refused') {
3026
- refusedSource(room, withheld);
3027
- }
3028
3242
  else if (withheld.kind === 'revoked-authorization') {
3029
3243
  await refuse('revoked-authorization');
3030
3244
  }
3031
3245
  }
3032
- function refusedSource(room, { refusal }) {
3033
- sourceRefusals.set(room.locator, {
3034
- code: refusal.rule,
3035
- retryable: false,
3036
- fields: {
3037
- file: refusal.file,
3038
- ...(refusal.location === undefined ? {} : { location: refusal.location }),
3039
- ...(refusal.referrers === undefined ? {} : { referrers: refusal.referrers }),
3040
- },
3041
- });
3042
- declarationRefusals.delete(room.locator);
3043
- unreadable.add(room.locator);
3044
- list(room, false);
3045
- emitRooms();
3046
- moveTo({ ...status, ...attention() });
3047
- journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
3048
- }
3049
3246
  function leaving(batch, result) {
3050
3247
  if (result.kind === 'expired')
3051
3248
  return { batch, returned: returnedBy('expired') };
@@ -3067,8 +3264,8 @@ export function composeRuntime(options) {
3067
3264
  function settling(acceptance) {
3068
3265
  return acceptance !== null && 'apply' in acceptance;
3069
3266
  }
3070
- function refusalOf(acceptance) {
3071
- return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
3267
+ function failureOf(acceptance) {
3268
+ return settling(acceptance) ? null : acceptance;
3072
3269
  }
3073
3270
  async function acceptance(room, local, submitted, accepted) {
3074
3271
  if (!continuing())
@@ -3091,34 +3288,6 @@ export function composeRuntime(options) {
3091
3288
  ? accepted.roomVersion
3092
3289
  : held,
3093
3290
  };
3094
- const overrides = new Map([
3095
- ...back.set.map((entry) => [entry.path, entry]),
3096
- ...back.removed.map((path) => [path, null]),
3097
- ]);
3098
- const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
3099
- if (refused !== null) {
3100
- const key = pendingKey(room.locator, submitted.id);
3101
- return {
3102
- locator: room.locator,
3103
- left: [],
3104
- commit: {
3105
- rooms: [],
3106
- writes: [
3107
- {
3108
- locator: room.locator,
3109
- accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
3110
- },
3111
- ],
3112
- },
3113
- refused,
3114
- apply() {
3115
- const state = pendings.get(key);
3116
- if (state !== undefined && !fences.has(room.locator))
3117
- pendings.set(key, { ...state, accepted: accepted.roomVersion });
3118
- return changeSet(room.locator, { queue: true });
3119
- },
3120
- };
3121
- }
3122
3291
  changes = back;
3123
3292
  }
3124
3293
  const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
@@ -3128,8 +3297,6 @@ export function composeRuntime(options) {
3128
3297
  const set = settled.apply(answer);
3129
3298
  pendings.delete(pendingKey(room.locator, submitted.id));
3130
3299
  unreadable.delete(room.locator);
3131
- sourceRefusals.delete(room.locator);
3132
- declarationRefusals.delete(room.locator);
3133
3300
  list(room, true);
3134
3301
  return set;
3135
3302
  },
@@ -3147,13 +3314,9 @@ export function composeRuntime(options) {
3147
3314
  const failed = await exclusive(async () => {
3148
3315
  const adopting = await acceptance(room, local, submitted, accepted);
3149
3316
  await adoptedTogether(local, [adopting]);
3150
- return refusalOf(adopting);
3317
+ return failureOf(adopting);
3151
3318
  });
3152
- if (failed === null)
3153
- return true;
3154
- if (failed.kind === 'refused' && continuing())
3155
- refusedSource(room, failed);
3156
- return false;
3319
+ return failed === null;
3157
3320
  }
3158
3321
  async function adoptedMove(room, partner, local, submitted, accepted) {
3159
3322
  const other = discovered.get(partner);
@@ -3167,18 +3330,10 @@ export function composeRuntime(options) {
3167
3330
  ...accepted.destination,
3168
3331
  });
3169
3332
  await adoptedTogether(local, [left, arrived]);
3170
- const refusedArrival = refusalOf(arrived);
3171
- if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
3172
- refusedSource(other, refusedArrival);
3173
- }
3174
- return refusalOf(left);
3333
+ return failureOf(left);
3175
3334
  });
3176
3335
  submitLater(partner);
3177
- if (failed === null)
3178
- return true;
3179
- if (failed.kind === 'refused' && continuing())
3180
- refusedSource(room, failed);
3181
- return false;
3336
+ return failed === null;
3182
3337
  }
3183
3338
  function queueIn(locator) {
3184
3339
  return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
@@ -3301,8 +3456,6 @@ export function composeRuntime(options) {
3301
3456
  if (replica === undefined || head === undefined)
3302
3457
  return 'done';
3303
3458
  const state = pendings.get(pendingKey(room.locator, head.id));
3304
- if (state?.accepted !== undefined)
3305
- return 'done';
3306
3459
  const partner = partnerOf(head, room.locator);
3307
3460
  if (partner !== null && !leavingIn(head, room.locator)) {
3308
3461
  submitLater(partner);
@@ -3416,12 +3569,12 @@ export function composeRuntime(options) {
3416
3569
  }
3417
3570
  if (result.kind === 'expired') {
3418
3571
  await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
3419
- const there = other === undefined ? null : await advance(other, local, authorized, continuing);
3572
+ const there = other === undefined ? null : await advanced(other, local, authorized);
3420
3573
  if (other !== undefined && there !== null) {
3421
3574
  await withhold(there, other, local, progress);
3422
3575
  return 'done';
3423
3576
  }
3424
- const withheld = await advance(room, local, authorized, continuing);
3577
+ const withheld = await advanced(room, local, authorized);
3425
3578
  if (withheld !== null) {
3426
3579
  await withhold(withheld, room, local, progress);
3427
3580
  return 'done';
@@ -3443,8 +3596,8 @@ export function composeRuntime(options) {
3443
3596
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
3444
3597
  const left = leaving(head, result);
3445
3598
  const refreshed = async () => ({
3446
- there: other === undefined ? null : await advance(other, local, authorized, continuing),
3447
- here: await advance(room, local, authorized, continuing),
3599
+ there: other === undefined ? null : await advanced(other, local, authorized),
3600
+ here: await advanced(room, local, authorized),
3448
3601
  });
3449
3602
  const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
3450
3603
  if (judged !== null && (judged.there !== null || judged.here !== null)) {
@@ -3515,6 +3668,7 @@ export function composeRuntime(options) {
3515
3668
  locator: room.locator,
3516
3669
  key: before.record.key,
3517
3670
  roots: before.record.typedSelection?.roots ?? [],
3671
+ composed: composedOf(composed(before.record)),
3518
3672
  overlay: { set: [], cleared: [...before.overlaid] },
3519
3673
  },
3520
3674
  ]),
@@ -3547,39 +3701,37 @@ export function composeRuntime(options) {
3547
3701
  if (signal.aborted)
3548
3702
  return;
3549
3703
  const progress = { complete: true, reachable: true };
3550
- await roomTask(room.locator, async (live) => {
3551
- await Promise.race([
3552
- reload(room, local, progress, () => live() && !signal.aborted),
3553
- abandonedOn(signal),
3554
- ]);
3555
- });
3704
+ await roomTask(room.locator, (live) => reload(room, local, progress, live));
3556
3705
  if (signal.aborted)
3557
3706
  return;
3558
3707
  await rest(progress);
3559
3708
  }
3560
3709
  }
3561
3710
  function awaitingAnswer(locator) {
3711
+ answering.get(locator)?.release();
3562
3712
  let release = () => { };
3563
- const awaited = new Promise((resolve) => {
3713
+ const answered = new Promise((resolve) => {
3564
3714
  release = resolve;
3565
3715
  });
3566
- answering.set(locator, { generation, answered: awaited });
3716
+ const awaited = { answered, release };
3717
+ answering.set(locator, awaited);
3567
3718
  return () => {
3568
- if (answering.get(locator)?.answered === awaited)
3719
+ if (answering.get(locator) === awaited)
3569
3720
  answering.delete(locator);
3570
3721
  release();
3571
3722
  };
3572
3723
  }
3573
- async function retainModel(local, room, attached, next) {
3574
- const key = modelKey(room.locator, attached);
3575
- const previous = modelOf(room.locator, attached);
3576
- const heldBefore = replicas.get(room.locator)?.record;
3577
- if (JSON.stringify(previous) === JSON.stringify(next) &&
3578
- (heldBefore === undefined ||
3579
- (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
3580
- JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
3581
- JSON.stringify(heldBefore.typedSelection) ===
3582
- JSON.stringify(room.typedSelection ?? null))))
3724
+ function declaredAsHeld(room) {
3725
+ const held = replicas.get(room.locator)?.record;
3726
+ return (held === undefined ||
3727
+ (JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
3728
+ JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
3729
+ JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
3730
+ }
3731
+ async function retainModel(local, room, next) {
3732
+ const key = room.locator;
3733
+ const previous = modelOf(room.locator);
3734
+ if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(room))
3583
3735
  return;
3584
3736
  if (next.status === 'unavailable') {
3585
3737
  invalidModels.add(key);
@@ -3591,26 +3743,13 @@ export function composeRuntime(options) {
3591
3743
  const held = replicas.get(room.locator);
3592
3744
  let changed = null;
3593
3745
  if (held !== undefined) {
3594
- const declared = {
3746
+ const record = {
3595
3747
  ...held.record,
3596
3748
  operations: room.operations,
3597
3749
  replica: room.replica,
3598
3750
  typedSelection: room.typedSelection ?? null,
3599
- ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
3751
+ model: next,
3600
3752
  };
3601
- let read = 'staged';
3602
- if (!sameMembership(declared, held.record)) {
3603
- const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), compositionOf(declared));
3604
- read =
3605
- verdict.verdict !== 'refuse'
3606
- ? 'staged'
3607
- : verdict.rule === 'model-unavailable'
3608
- ? 'deferred'
3609
- : { kind: 'refused', refusal: verdict };
3610
- }
3611
- const record = read === 'staged'
3612
- ? declared
3613
- : { ...declared, typedSelection: held.record.typedSelection };
3614
3753
  const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
3615
3754
  const replayedLayer = await replayed(record, confirmedLayer, held.queue);
3616
3755
  const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
@@ -3621,41 +3760,33 @@ export function composeRuntime(options) {
3621
3760
  key: record.key,
3622
3761
  record: storedRecord(record),
3623
3762
  roots: record.typedSelection?.roots ?? [],
3624
- composition: compositionOf(record),
3763
+ composed: composedOf(composed(record)),
3625
3764
  overlay: diff.overlay,
3626
3765
  },
3627
3766
  ],
3628
3767
  });
3629
- recomposed(room.locator, held.record, record);
3630
- const { count } = committedRoom(room.locator, record, answer);
3631
- replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count });
3768
+ const { count, indexed } = committedRoom(room.locator, answer);
3769
+ replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
3632
3770
  changed = changeSet(room.locator, {
3633
3771
  documents: answer.changed[room.locator] ?? [],
3634
3772
  room: true,
3635
3773
  });
3636
- if (read === 'staged' || read === 'deferred') {
3637
- if (declarationRefusals.delete(room.locator)) {
3638
- sourceRefusals.delete(room.locator);
3639
- unreadable.delete(room.locator);
3640
- }
3641
- }
3642
- else {
3643
- refusedSource(room, read);
3644
- declarationRefusals.add(room.locator);
3645
- }
3646
3774
  }
3647
- models.set(key, next);
3648
- if (next.status === 'current') {
3649
- liveModels.set(key, JSON.stringify(next.snapshot));
3650
- invalidModels.delete(key);
3651
- }
3652
- list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
3775
+ heldModel(room, next);
3653
3776
  if (changed === null)
3654
3777
  emitRooms();
3655
3778
  else
3656
3779
  publishChangeSet(changed);
3657
3780
  });
3658
3781
  }
3782
+ function heldModel(room, next) {
3783
+ models.set(room.locator, next);
3784
+ if (next.status === 'current') {
3785
+ liveModels.set(room.locator, JSON.stringify(next.snapshot));
3786
+ invalidModels.delete(room.locator);
3787
+ }
3788
+ list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
3789
+ }
3659
3790
  async function scoped(result, room, local) {
3660
3791
  if (result.scope === 'content' && !result.retryable) {
3661
3792
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
@@ -3669,14 +3800,14 @@ export function composeRuntime(options) {
3669
3800
  });
3670
3801
  let scopedAway = room;
3671
3802
  if (result.scope === 'model')
3672
- await retainModel(local, room, 'collection', {
3803
+ await retainModel(local, room, {
3673
3804
  status: 'unavailable',
3674
3805
  reason: 'not-readable',
3675
3806
  });
3676
3807
  else if (result.scope !== 'operation') {
3677
3808
  unreadable.add(room.locator);
3678
3809
  scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
3679
- await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
3810
+ await retainModel(local, scopedAway, modelOf(room.locator));
3680
3811
  }
3681
3812
  if (!continuing())
3682
3813
  return;
@@ -3705,10 +3836,12 @@ export function composeRuntime(options) {
3705
3836
  handed = true;
3706
3837
  }
3707
3838
  discovering?.release();
3708
- const loaded = await Promise.allSettled(ready
3709
- .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
3710
- .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
3711
- const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
3839
+ for (const room of ready) {
3840
+ if (attempt !== 'unstreamed' && replicas.has(room.locator))
3841
+ continue;
3842
+ void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
3843
+ }
3844
+ const failed = prepared.find((room) => room.status === 'rejected');
3712
3845
  if (failed !== undefined)
3713
3846
  throw failed.reason;
3714
3847
  }
@@ -3726,7 +3859,7 @@ export function composeRuntime(options) {
3726
3859
  return false;
3727
3860
  }
3728
3861
  if (prior !== undefined)
3729
- await retainModel(local, room, 'collection', modelOf(room.locator));
3862
+ await retainModel(local, room, modelOf(room.locator));
3730
3863
  if (discoveryPaused || authorization === undefined)
3731
3864
  return false;
3732
3865
  return room.replica.admission === 'admitted';
@@ -3739,14 +3872,14 @@ export function composeRuntime(options) {
3739
3872
  await withhold(withheld, room, local, progress);
3740
3873
  }
3741
3874
  function revoked(locator) {
3742
- return ['collection', 'typed-selection'].some((attached) => {
3743
- const model = modelOf(locator, attached);
3744
- return model.status === 'unavailable' && model.reason === 'revoked';
3745
- });
3875
+ const model = modelOf(locator);
3876
+ return model.status === 'unavailable' && model.reason === 'revoked';
3746
3877
  }
3747
3878
  async function loadRoom(room, local, progress, live) {
3748
3879
  if (!live() || authorization === undefined)
3749
3880
  return;
3881
+ if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
3882
+ return;
3750
3883
  if (replicas.has(room.locator) && revoked(room.locator)) {
3751
3884
  await reload(room, local, progress, live);
3752
3885
  return;
@@ -3764,14 +3897,14 @@ export function composeRuntime(options) {
3764
3897
  reloadsDue.delete(room.locator);
3765
3898
  if (!live() || authorization === undefined)
3766
3899
  return;
3900
+ const from = standingOf(room.locator);
3767
3901
  const bootstrapped = await pullBootstrap(authorization, room, local, live);
3768
- if (!live())
3769
- return;
3770
3902
  if (bootstrapped.kind !== 'staged') {
3771
- await withhold(bootstrapped, room, local, progress);
3903
+ if (live())
3904
+ await withhold(bootstrapped, room, local, progress);
3772
3905
  return;
3773
3906
  }
3774
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3907
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
3775
3908
  await caughtUp(room, local, progress, live);
3776
3909
  }
3777
3910
  finally {
@@ -3793,6 +3926,8 @@ export function composeRuntime(options) {
3793
3926
  catch {
3794
3927
  result = { kind: 'transport-uncertainty' };
3795
3928
  }
3929
+ if (result.kind === 'discovery')
3930
+ roomsNamed = true;
3796
3931
  if (!live()) {
3797
3932
  progress.complete = false;
3798
3933
  notifyReady();
@@ -3800,12 +3935,7 @@ export function composeRuntime(options) {
3800
3935
  }
3801
3936
  journal.record(`connection discover ${result.kind}`);
3802
3937
  if (result.kind === 'transport-uncertainty') {
3803
- for (const room of rooms.values()) {
3804
- await retainModel(local, room, 'collection', cached(modelOf(room.locator)));
3805
- if (room.typedSelection !== null) {
3806
- await retainModel(local, room, 'typed-selection', cached(modelOf(room.locator, 'typed-selection')));
3807
- }
3808
- }
3938
+ await settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
3809
3939
  progress.reachable = false;
3810
3940
  progress.complete = false;
3811
3941
  notifyReady();
@@ -3860,6 +3990,7 @@ export function composeRuntime(options) {
3860
3990
  limits = result.providerLimits;
3861
3991
  await local.fact('providerLimits', limits);
3862
3992
  await local.fact('capabilities', capabilities);
3993
+ conversations.declared();
3863
3994
  }
3864
3995
  const signedIn = result.member ?? null;
3865
3996
  if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
@@ -3882,23 +4013,45 @@ export function composeRuntime(options) {
3882
4013
  }
3883
4014
  }
3884
4015
  async function admittedRead(work) {
3885
- if (roomReads.generation !== generation) {
3886
- roomReads = { generation, free: ROOM_READS, waiting: [] };
4016
+ while (roomReads.free === 0) {
4017
+ await new Promise((admit) => roomReads.waiting.push(admit));
3887
4018
  }
3888
- const reads = roomReads;
3889
- while (reads.free === 0)
3890
- await new Promise((admit) => reads.waiting.push(admit));
3891
- reads.free -= 1;
4019
+ roomReads.free -= 1;
3892
4020
  try {
3893
4021
  await work();
3894
4022
  }
3895
4023
  finally {
3896
- reads.free += 1;
3897
- reads.waiting.shift()?.();
4024
+ roomReads.free += 1;
4025
+ roomReads.waiting.shift()?.();
3898
4026
  }
3899
4027
  }
4028
+ function inRoom(locator, work) {
4029
+ roomWork += 1;
4030
+ const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
4031
+ try {
4032
+ return await work(continuing);
4033
+ }
4034
+ finally {
4035
+ roomWork -= 1;
4036
+ }
4037
+ });
4038
+ const chain = ran.then(() => undefined, () => undefined);
4039
+ roomLanes.set(locator, chain);
4040
+ working.add(chain);
4041
+ void chain.finally(() => {
4042
+ working.delete(chain);
4043
+ if (roomLanes.get(locator) === chain)
4044
+ roomLanes.delete(locator);
4045
+ });
4046
+ return ran;
4047
+ }
3900
4048
  function roomTask(locator, work) {
3901
- return task(roomLane(locator), (live) => admittedRead(() => work(live)));
4049
+ if (stopping)
4050
+ return Promise.resolve();
4051
+ return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
4052
+ }
4053
+ function advanced(room, local, authorized) {
4054
+ return inRoom(room.locator, (live) => advance(room, local, authorized, live));
3902
4055
  }
3903
4056
  function task(lane, work) {
3904
4057
  if (stopping)
@@ -3917,14 +4070,13 @@ export function composeRuntime(options) {
3917
4070
  function sync(attempt) {
3918
4071
  return task(OPENING_LANE, async (live) => {
3919
4072
  if (!live()) {
3920
- notifyReady();
4073
+ readyFromStore();
3921
4074
  return;
3922
4075
  }
3923
4076
  const progress = { complete: true, reachable: true };
3924
4077
  const gate = discover();
3925
- if (attempt === 'opened')
3926
- opened = generation;
3927
- underway = attempt === 'opened' ? { generation, progress } : null;
4078
+ opened = attempt === 'opened';
4079
+ underway = attempt === 'opened' ? progress : null;
3928
4080
  if (attempt === 'opened')
3929
4081
  streamed = progress;
3930
4082
  complete = false;
@@ -3958,7 +4110,7 @@ export function composeRuntime(options) {
3958
4110
  if (handed || closed || progress.reachable)
3959
4111
  return;
3960
4112
  underway = null;
3961
- opened = null;
4113
+ opened = false;
3962
4114
  journal.record('connection reopen');
3963
4115
  await rest(progress, live);
3964
4116
  options.adapter.roomStream?.reopen();
@@ -3995,13 +4147,12 @@ export function composeRuntime(options) {
3995
4147
  });
3996
4148
  }
3997
4149
  function closedSocket(revoked, lost) {
4150
+ notifyDiscovered();
3998
4151
  const opened = socketOpen;
3999
4152
  socketOpen = false;
4000
4153
  connectionLost();
4001
- if (lost) {
4002
- reads.lost();
4154
+ if (lost)
4003
4155
  conversations.lost();
4004
- }
4005
4156
  if (revoked) {
4006
4157
  void refuse('revoked-authorization');
4007
4158
  return;
@@ -4016,10 +4167,9 @@ export function composeRuntime(options) {
4016
4167
  });
4017
4168
  }
4018
4169
  function applyOpening(expired) {
4019
- const progress = underway?.generation === generation ? underway.progress : null;
4170
+ const progress = underway;
4020
4171
  const cycle = progress ?? { complete: true, reachable: true };
4021
- if (opened === generation)
4022
- opened = null;
4172
+ opened = false;
4023
4173
  const reloading = progress === null ? [] : owedReloads(expired);
4024
4174
  const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
4025
4175
  return task(OPENING_LANE, async (live) => {
@@ -4031,24 +4181,14 @@ export function composeRuntime(options) {
4031
4181
  await rest(cycle, live);
4032
4182
  return;
4033
4183
  }
4034
- await roomsCarried([...expired, ...reloading]);
4035
- if (!live())
4036
- return;
4037
4184
  try {
4038
4185
  const local = await openLocal();
4039
- const reloaded = await replacing;
4186
+ await replacing;
4040
4187
  if (progress.reachable) {
4041
- for (const room of discovered.values()) {
4042
- if (replicas.has(room.locator) &&
4043
- !expired.includes(room.locator) &&
4044
- !reloading.includes(room.locator)) {
4045
- await confirmModels(room, local);
4046
- }
4047
- }
4188
+ await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
4189
+ !expired.includes(room.locator) &&
4190
+ !reloading.includes(room.locator)), 'current', local);
4048
4191
  }
4049
- const failed = reloaded.find((outcome) => outcome.status === 'rejected');
4050
- if (failed !== undefined)
4051
- throw failed.reason;
4052
4192
  }
4053
4193
  catch {
4054
4194
  progress.complete = false;
@@ -4073,24 +4213,43 @@ export function composeRuntime(options) {
4073
4213
  async function reloadNow(replaced, progress) {
4074
4214
  const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
4075
4215
  for (const room of reloaded)
4076
- reloadsDue.set(room.locator, generation);
4216
+ reloadsDue.add(room.locator);
4077
4217
  try {
4078
4218
  const local = await openLocal();
4079
- return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
4219
+ for (const room of reloaded) {
4220
+ void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
4221
+ }
4080
4222
  }
4081
4223
  catch {
4082
4224
  for (const room of reloaded)
4083
4225
  reloadsDue.delete(room.locator);
4084
4226
  progress.reachable = false;
4085
- return [];
4086
4227
  }
4087
4228
  }
4088
- async function confirmModels(room, local) {
4089
- for (const attached of ['collection', 'typed-selection']) {
4090
- const model = modelOf(room.locator, attached);
4091
- if (model.status === 'cached') {
4092
- await retainModel(local, room, attached, { ...model, status: 'current' });
4229
+ async function settleModels(held, status, local) {
4230
+ const owed = (room) => {
4231
+ const model = modelOf(room.locator);
4232
+ return model.status !== 'unavailable' && model.status !== status;
4233
+ };
4234
+ const settled = await exclusive(async () => {
4235
+ if (!continuing())
4236
+ return [];
4237
+ const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
4238
+ for (const room of unchanged) {
4239
+ const next = { ...modelOf(room.locator), status };
4240
+ const replica = replicas.get(room.locator);
4241
+ if (replica !== undefined)
4242
+ replicas.set(room.locator, { ...replica, record: { ...replica.record, model: next } });
4243
+ heldModel(room, next);
4093
4244
  }
4245
+ if (unchanged.length > 0)
4246
+ emitRooms();
4247
+ return unchanged;
4248
+ });
4249
+ for (const room of held) {
4250
+ if (settled.includes(room) || !owed(room))
4251
+ continue;
4252
+ await retainModel(local, room, { ...modelOf(room.locator), status });
4094
4253
  }
4095
4254
  }
4096
4255
  function submitLater(locator) {
@@ -4135,14 +4294,10 @@ export function composeRuntime(options) {
4135
4294
  try {
4136
4295
  const local = await openLocal();
4137
4296
  await roomTask(locator, async (live) => {
4138
- const current = () => live() && !signal.aborted;
4139
- if (!current() || authorization === undefined)
4297
+ if (!live() || authorization === undefined)
4140
4298
  return;
4141
- const withheld = await Promise.race([
4142
- advance(room, local, authorization, current),
4143
- abandonedOn(signal),
4144
- ]);
4145
- if (withheld !== null && current())
4299
+ const withheld = await advance(room, local, authorization, live);
4300
+ if (withheld !== null && live())
4146
4301
  await withhold(withheld, room, local, progress);
4147
4302
  });
4148
4303
  }
@@ -4197,14 +4352,11 @@ export function composeRuntime(options) {
4197
4352
  progress.complete = false;
4198
4353
  return;
4199
4354
  }
4200
- reads.applied();
4201
4355
  submitAll();
4202
4356
  for (const locator of stale)
4203
4357
  if (repairable.has(locator))
4204
4358
  repairLater(locator, {});
4205
4359
  await conversations.reconcile();
4206
- if (status.providerUnreachable)
4207
- conversations.reconnected();
4208
4360
  const reading = { complete: true, reachable: true };
4209
4361
  await conversations.catchUp(reading);
4210
4362
  if (progress.reachable && reading.reachable)
@@ -4230,7 +4382,7 @@ export function composeRuntime(options) {
4230
4382
  if (!live())
4231
4383
  return;
4232
4384
  let lastCompleted = status.lastCompleted;
4233
- let synced = complete && stale.size === 0 && !pending();
4385
+ let synced = complete && roomWork === 0 && stale.size === 0 && !pending();
4234
4386
  if (synced && !recorded) {
4235
4387
  const completedAt = new Date().toISOString();
4236
4388
  try {
@@ -4245,42 +4397,41 @@ export function composeRuntime(options) {
4245
4397
  }
4246
4398
  }
4247
4399
  moveTo({
4248
- state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
4400
+ state: pending() ? 'pending' : synced && complete && roomWork === 0 ? 'synced' : 'syncing',
4249
4401
  lastCompleted,
4250
- providerUnreachable: !progress.reachable,
4402
+ providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
4251
4403
  ...attention(),
4252
4404
  });
4253
4405
  }
4254
4406
  function discover() {
4407
+ discovering?.release();
4255
4408
  let release = () => { };
4256
4409
  const done = new Promise((resolve) => {
4257
4410
  release = resolve;
4258
4411
  });
4259
- discovering = { generation, done, release };
4412
+ discovering = { done, release };
4260
4413
  return discovering;
4261
4414
  }
4262
4415
  function applyRoom(locator, roomVersion) {
4263
4416
  announced.set(locator, roomVersion);
4264
- return task(roomLane(locator), async (live) => {
4265
- if (discovering?.generation === generation)
4266
- await discovering.done;
4267
- const saving = answering.get(locator);
4268
- if (saving?.generation === generation)
4269
- await saving.answered;
4417
+ if (stopping)
4418
+ return Promise.resolve();
4419
+ return inRoom(locator, async (live) => {
4420
+ await discovering?.done;
4421
+ await answering.get(locator)?.answered;
4270
4422
  if (!live() || authorization === undefined)
4271
- return;
4423
+ return null;
4272
4424
  const room = discovered.get(locator);
4273
4425
  const held = replicas.get(locator);
4274
4426
  if (room === undefined || room.replica.admission !== 'admitted')
4275
- return;
4427
+ return null;
4276
4428
  if (held !== undefined &&
4277
4429
  (held.record.replica.selection !== room.replica.selection ||
4278
4430
  options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
4279
- return;
4431
+ return null;
4280
4432
  }
4281
- const during = underway?.generation === generation ? underway.progress : null;
4282
- const progress = during ?? { complete: true, reachable: true };
4283
- if (during === null)
4433
+ const progress = underway ?? { complete: true, reachable: true };
4434
+ if (underway === null)
4284
4435
  moveTo(transferring(status));
4285
4436
  try {
4286
4437
  const local = await openLocal();
@@ -4295,24 +4446,9 @@ export function composeRuntime(options) {
4295
4446
  catch {
4296
4447
  stale.add(locator);
4297
4448
  }
4298
- finally {
4299
- if (during === null)
4300
- await rest(progress, live);
4301
- }
4302
- });
4449
+ return progress;
4450
+ }).then((progress) => rest(progress), () => undefined);
4303
4451
  }
4304
- function roomsCarried(replacing) {
4305
- const replaced = new Set(replacing.map(roomLane));
4306
- return Promise.all([...lanes]
4307
- .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
4308
- .map(([, held]) => held.chain));
4309
- }
4310
- const receipts = {
4311
- heads: async () => (await openLocal()).receivedHeads(),
4312
- parts: async (key, count) => (await openLocal()).receivedParts(key, count),
4313
- keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
4314
- drop: async (keys) => (await openLocal()).dropReceived(keys),
4315
- };
4316
4452
  function observeStream() {
4317
4453
  const stream = options.adapter.roomStream;
4318
4454
  if (stream === undefined || withdrawStream !== undefined)
@@ -4324,7 +4460,6 @@ export function composeRuntime(options) {
4324
4460
  socketOpen = true;
4325
4461
  connectionLive();
4326
4462
  generation += 1;
4327
- reads.opened();
4328
4463
  conversations.reopened();
4329
4464
  recovery.resume();
4330
4465
  void sync('opened');
@@ -4337,14 +4472,7 @@ export function composeRuntime(options) {
4337
4472
  void rediscover(event.scope);
4338
4473
  else
4339
4474
  void applyRoom(event.locator, event.roomVersion);
4340
- }, receipts, heldRevisions);
4341
- }
4342
- async function heldRevisions(locator, paths) {
4343
- const held = replicas.get(locator);
4344
- if (held === undefined || paths.length === 0)
4345
- return new Map();
4346
- const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
4347
- return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
4475
+ });
4348
4476
  }
4349
4477
  function writesHeld() {
4350
4478
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -4414,8 +4542,9 @@ export function composeRuntime(options) {
4414
4542
  if (closed)
4415
4543
  return;
4416
4544
  closed = true;
4417
- if (rechecking !== null)
4418
- clearInterval(rechecking);
4545
+ notifyDiscovered();
4546
+ if (probing !== null)
4547
+ clearInterval(probing);
4419
4548
  connectionLive();
4420
4549
  lockListeners.clear();
4421
4550
  readinessWatches.clear();
@@ -4427,6 +4556,7 @@ export function composeRuntime(options) {
4427
4556
  conversations.close();
4428
4557
  void recovery.retire();
4429
4558
  roomListeners.clear();
4559
+ unlistedRooms.clear();
4430
4560
  refusalListeners.clear();
4431
4561
  storageLossListeners.clear();
4432
4562
  statusListeners.clear();
@@ -4439,6 +4569,7 @@ export function composeRuntime(options) {
4439
4569
  }
4440
4570
  async function stop() {
4441
4571
  stopping = true;
4572
+ inFlight.abort();
4442
4573
  await recovery.retire();
4443
4574
  await Promise.all(working);
4444
4575
  await mutations;
@@ -4452,9 +4583,9 @@ export function composeRuntime(options) {
4452
4583
  observeStream();
4453
4584
  void openLocal().then(() => {
4454
4585
  if (capabilities.length > 0)
4455
- notifyReady();
4586
+ readyFromStore();
4456
4587
  }, () => {
4457
- notifyReady();
4588
+ readyFromStore();
4458
4589
  });
4459
4590
  if (options.adapter.roomStream === undefined)
4460
4591
  void sync('unstreamed');
@@ -4602,12 +4733,13 @@ export function composeRuntime(options) {
4602
4733
  };
4603
4734
  }
4604
4735
  if (topic === 'readiness') {
4605
- return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
4736
+ return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
4606
4737
  }
4607
4738
  if (topic === 'rooms') {
4608
- roomListeners.add(deliver);
4609
- deliver(listed());
4739
+ unlistedRooms.add(deliver);
4740
+ emitKnownRooms();
4610
4741
  return () => {
4742
+ unlistedRooms.delete(deliver);
4611
4743
  roomListeners.delete(deliver);
4612
4744
  };
4613
4745
  }
@@ -4661,7 +4793,17 @@ export function composeRuntime(options) {
4661
4793
  return openLocal().then(() => undefined);
4662
4794
  },
4663
4795
  rooms() {
4664
- return listed();
4796
+ if (closed)
4797
+ return Promise.resolve([]);
4798
+ return new Promise((settle) => {
4799
+ const answer = (value) => {
4800
+ unlistedRooms.delete(answer);
4801
+ roomListeners.delete(answer);
4802
+ settle(value);
4803
+ };
4804
+ unlistedRooms.add(answer);
4805
+ emitKnownRooms();
4806
+ });
4665
4807
  },
4666
4808
  query: ((input) => rowsQuery(planned(input, null))),
4667
4809
  search: (input) => searched(input, null, []),
@@ -4671,7 +4813,7 @@ export function composeRuntime(options) {
4671
4813
  typedSelection: rooms.get(locator)?.typedSelection === undefined ||
4672
4814
  rooms.get(locator)?.typedSelection === null
4673
4815
  ? null
4674
- : modelTypes(modelOf(locator, 'typed-selection')),
4816
+ : modelTypes(builtIn),
4675
4817
  };
4676
4818
  },
4677
4819
  roomDraft: {
@@ -4688,33 +4830,17 @@ export function composeRuntime(options) {
4688
4830
  const record = roomDrafts.get(room)?.get(path);
4689
4831
  return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
4690
4832
  },
4691
- write(room, path, revision, input) {
4692
- return exclusive(async () => {
4693
- const parts = readParts(input);
4694
- if (path === '' || normalize(path) !== path)
4695
- throw runtimeError('room-draft-malformed');
4696
- const local = await openLocal();
4697
- const current = roomDrafts.get(room)?.get(path);
4698
- if ((current?.revision ?? null) !== revision) {
4699
- return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
4700
- }
4701
- const file = parts.file === undefined
4702
- ? undefined
4703
- : await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
4704
- const record = {
4705
- locator: room,
4706
- path,
4707
- revision: roomDraftRevision(),
4708
- parts: held(current?.parts, parts, file),
4709
- returned: current?.returned ?? null,
4710
- };
4711
- await local.keepRoomDraft(record, parts.file === undefined || file === undefined
4712
- ? []
4713
- : [{ key: file.localId, blob: parts.file.blob }]);
4714
- rememberRoomDraft(record);
4715
- publishChangeSet(changeSet(room, { drafts: [path] }));
4716
- return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
4717
- });
4833
+ write(room, path, revision, input, next = roomDraftRevision()) {
4834
+ if (typeof next !== 'string' || next === '')
4835
+ return Promise.reject(runtimeError('room-draft-malformed'));
4836
+ return gathered(`room-draft\u0000${room}\u0000${path}`, { revision, input, next }, (earlier, later) => {
4837
+ if (later.revision !== earlier.next)
4838
+ return null;
4839
+ const parts = gatheredParts(earlier.input, later.input);
4840
+ return parts === null
4841
+ ? null
4842
+ : { revision: earlier.revision, input: parts, next: later.next };
4843
+ }, (written) => writtenRoomDraft(room, path, written.revision, written.input, written.next));
4718
4844
  },
4719
4845
  discard(room, path, revision) {
4720
4846
  return exclusive(async () => {
@@ -4741,13 +4867,13 @@ export function composeRuntime(options) {
4741
4867
  });
4742
4868
  },
4743
4869
  write(app, value) {
4744
- return keepPresentation(app, () => structuredClone(value));
4870
+ return exclusive(() => keptPresentation(app, () => structuredClone(value)));
4745
4871
  },
4746
4872
  edit(app, members) {
4747
- return keepPresentation(app, (held) => Object.fromEntries([
4748
- ...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(members, member)),
4749
- ...Object.entries(structuredClone(members)).filter(([, value]) => value !== null),
4750
- ]));
4873
+ return gathered(`presentation\u0000${app}`, structuredClone(members), (earlier, later) => ({ ...earlier, ...later }), (edited) => keptPresentation(app, (held) => Object.fromEntries([
4874
+ ...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(edited, member)),
4875
+ ...Object.entries(edited).filter(([, value]) => value !== null),
4876
+ ])));
4751
4877
  },
4752
4878
  },
4753
4879
  save(room, named, writtenBy) {
@@ -4766,7 +4892,8 @@ export function composeRuntime(options) {
4766
4892
  part: readPart,
4767
4893
  read: readBinaryPath,
4768
4894
  link: linkBinaryPath,
4769
- readReference: readBinaryReference,
4895
+ show: showBinaryPath,
4896
+ attach: attachBinary,
4770
4897
  },
4771
4898
  async call(name, input) {
4772
4899
  await ready;
@@ -4789,7 +4916,8 @@ export function composeRuntime(options) {
4789
4916
  journal.record('retry');
4790
4917
  unpreparable.clear();
4791
4918
  queueReadiness();
4792
- resume();
4919
+ if (full)
4920
+ await probed();
4793
4921
  },
4794
4922
  observe: observing(watch),
4795
4923
  async reauthorize() {
@@ -4811,7 +4939,6 @@ export function composeRuntime(options) {
4811
4939
  const reachable = {
4812
4940
  md,
4813
4941
  watch,
4814
- referrerScope: (referrer) => options.adapter.referrerScope(referrer),
4815
4942
  plan: planned,
4816
4943
  query: (plan) => rowsQuery(plan),
4817
4944
  watchPlan,
@@ -4821,6 +4948,7 @@ export function composeRuntime(options) {
4821
4948
  lockedRooms,
4822
4949
  watchLocks,
4823
4950
  located,
4951
+ room: (locator) => listed().find((one) => one.locator === locator),
4824
4952
  };
4825
4953
  return {
4826
4954
  md,
@@ -4835,6 +4963,10 @@ export function composeRuntime(options) {
4835
4963
  journal.record(`persistence ${answer}`);
4836
4964
  queueReadiness();
4837
4965
  },
4838
- within: (reach, port) => narrowed(reachable, reach, port),
4966
+ within: (reach, port) => {
4967
+ if (full)
4968
+ void probed();
4969
+ return narrowed(reachable, reach, port);
4970
+ },
4839
4971
  };
4840
4972
  }