@agentshouse/mdruntime 0.1.0-cf890bb538119 → 0.1.0-cfac557170cf3

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 (189) hide show
  1. package/README.md +10 -8
  2. package/dist/adapter.d.ts +31 -18
  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 +15 -4
  11. package/dist/bootstrap.d.ts.map +1 -1
  12. package/dist/bootstrap.js +133 -90
  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 -0
  17. package/dist/collection.d.ts.map +1 -1
  18. package/dist/collection.js +23 -2
  19. package/dist/compile.d.ts.map +1 -1
  20. package/dist/compile.js +81 -29
  21. package/dist/connection.d.ts +9 -7
  22. package/dist/connection.d.ts.map +1 -1
  23. package/dist/connection.js +961 -476
  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 +2 -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 +4 -3
  39. package/dist/host.d.ts.map +1 -1
  40. package/dist/host.js +43 -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 -19
  52. package/dist/model.d.ts +6 -0
  53. package/dist/model.d.ts.map +1 -1
  54. package/dist/model.js +7 -1
  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 -3
  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 +68 -45
  70. package/dist/readiness.d.ts +17 -7
  71. package/dist/readiness.d.ts.map +1 -1
  72. package/dist/readiness.js +6 -6
  73. package/dist/reads.d.ts.map +1 -1
  74. package/dist/reads.js +10 -17
  75. package/dist/recovery.d.ts +1 -0
  76. package/dist/recovery.d.ts.map +1 -1
  77. package/dist/recovery.js +26 -0
  78. package/dist/refresh.d.ts +6 -2
  79. package/dist/refresh.d.ts.map +1 -1
  80. package/dist/refresh.js +61 -27
  81. package/dist/replica.d.ts +5 -4
  82. package/dist/replica.d.ts.map +1 -1
  83. package/dist/replica.js +4 -3
  84. package/dist/room-draft.d.ts +18 -2
  85. package/dist/room-draft.d.ts.map +1 -1
  86. package/dist/room-draft.js +73 -6
  87. package/dist/rows.d.ts +15 -6
  88. package/dist/rows.d.ts.map +1 -1
  89. package/dist/rows.js +102 -74
  90. package/dist/save.d.ts +2 -0
  91. package/dist/save.d.ts.map +1 -1
  92. package/dist/save.js +13 -7
  93. package/dist/status.d.ts +5 -0
  94. package/dist/status.d.ts.map +1 -1
  95. package/dist/status.js +3 -1
  96. package/dist/store.d.ts +18 -12
  97. package/dist/store.d.ts.map +1 -1
  98. package/dist/store.js +25 -20
  99. package/dist/stored.d.ts +10 -13
  100. package/dist/stored.d.ts.map +1 -1
  101. package/dist/stored.js +200 -175
  102. package/dist/tables.d.ts +26 -6
  103. package/dist/tables.d.ts.map +1 -1
  104. package/dist/tables.js +57 -6
  105. package/dist/wire.d.ts +5 -5
  106. package/dist/wire.d.ts.map +1 -1
  107. package/dist/wire.js +2 -2
  108. package/dist/words.d.ts.map +1 -1
  109. package/dist/words.js +8 -2
  110. package/package.json +11 -18
  111. package/AGENTS.md +0 -319
  112. package/dist/chat/acts.d.ts +0 -33
  113. package/dist/chat/acts.d.ts.map +0 -1
  114. package/dist/chat/acts.js +0 -36
  115. package/dist/chat/context.d.ts +0 -3
  116. package/dist/chat/context.d.ts.map +0 -1
  117. package/dist/chat/context.js +0 -10
  118. package/dist/chat/earlier.d.ts +0 -10
  119. package/dist/chat/earlier.d.ts.map +0 -1
  120. package/dist/chat/earlier.js +0 -54
  121. package/dist/chat/house.d.ts +0 -25
  122. package/dist/chat/house.d.ts.map +0 -1
  123. package/dist/chat/house.js +0 -38
  124. package/dist/chat/input.d.ts +0 -11
  125. package/dist/chat/input.d.ts.map +0 -1
  126. package/dist/chat/input.js +0 -92
  127. package/dist/chat/kit.d.ts +0 -61
  128. package/dist/chat/kit.d.ts.map +0 -1
  129. package/dist/chat/kit.js +0 -123
  130. package/dist/chat/lasting.d.ts +0 -6
  131. package/dist/chat/lasting.d.ts.map +0 -1
  132. package/dist/chat/lasting.js +0 -19
  133. package/dist/chat/list.d.ts +0 -28
  134. package/dist/chat/list.d.ts.map +0 -1
  135. package/dist/chat/list.js +0 -103
  136. package/dist/chat/markdown.d.ts +0 -21
  137. package/dist/chat/markdown.d.ts.map +0 -1
  138. package/dist/chat/markdown.js +0 -246
  139. package/dist/chat/message.d.ts +0 -7
  140. package/dist/chat/message.d.ts.map +0 -1
  141. package/dist/chat/message.js +0 -254
  142. package/dist/chat/messages.d.ts +0 -35
  143. package/dist/chat/messages.d.ts.map +0 -1
  144. package/dist/chat/messages.js +0 -210
  145. package/dist/chat/name.d.ts +0 -7
  146. package/dist/chat/name.d.ts.map +0 -1
  147. package/dist/chat/name.js +0 -37
  148. package/dist/chat/opened.d.ts +0 -12
  149. package/dist/chat/opened.d.ts.map +0 -1
  150. package/dist/chat/opened.js +0 -19
  151. package/dist/chat/overlay.d.ts +0 -32
  152. package/dist/chat/overlay.d.ts.map +0 -1
  153. package/dist/chat/overlay.js +0 -103
  154. package/dist/chat/parts.d.ts +0 -58
  155. package/dist/chat/parts.d.ts.map +0 -1
  156. package/dist/chat/parts.js +0 -120
  157. package/dist/chat/pieces.d.ts +0 -62
  158. package/dist/chat/pieces.d.ts.map +0 -1
  159. package/dist/chat/pieces.js +0 -1110
  160. package/dist/chat/process.d.ts +0 -20
  161. package/dist/chat/process.d.ts.map +0 -1
  162. package/dist/chat/process.js +0 -32
  163. package/dist/chat/provider.d.ts +0 -70
  164. package/dist/chat/provider.d.ts.map +0 -1
  165. package/dist/chat/provider.js +0 -635
  166. package/dist/chat/question.d.ts +0 -9
  167. package/dist/chat/question.d.ts.map +0 -1
  168. package/dist/chat/question.js +0 -69
  169. package/dist/chat/questions.d.ts +0 -31
  170. package/dist/chat/questions.d.ts.map +0 -1
  171. package/dist/chat/questions.js +0 -111
  172. package/dist/chat/read.d.ts +0 -5
  173. package/dist/chat/read.d.ts.map +0 -1
  174. package/dist/chat/read.js +0 -13
  175. package/dist/chat/ready.d.ts +0 -37
  176. package/dist/chat/ready.d.ts.map +0 -1
  177. package/dist/chat/ready.js +0 -93
  178. package/dist/chat/room-file.d.ts +0 -8
  179. package/dist/chat/room-file.d.ts.map +0 -1
  180. package/dist/chat/room-file.js +0 -72
  181. package/dist/chat/steady.d.ts +0 -3
  182. package/dist/chat/steady.d.ts.map +0 -1
  183. package/dist/chat/steady.js +0 -26
  184. package/dist/chat/style.d.ts +0 -4
  185. package/dist/chat/style.d.ts.map +0 -1
  186. package/dist/chat/style.js +0 -307
  187. package/dist/chat.d.ts +0 -15
  188. package/dist/chat.d.ts.map +0 -1
  189. package/dist/chat.js +0 -8
@@ -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,27 +12,27 @@ 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, unreadable as unreadableIn, 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
23
  import { doorReads } from './reads.js';
23
24
  import { advancement } from './refresh.js';
24
25
  import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
25
26
  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';
27
+ import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, readSavedParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
27
28
  import { same, saving, STARTING, transferring, } from './status.js';
28
- import { detectStorageLoss, nextGeneration } from './durability.js';
29
- import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
29
+ import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
30
+ import { readinessOf, } from './readiness.js';
30
31
  import { FencedMap } from './fence.js';
31
- import { fingerprint } from './stored.js';
32
32
  import { sameBinary } from './overlay.js';
33
33
  import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
34
34
  import { identityTime, roomDraftRevision } from './identity.js';
35
- import { beneath, normalize } from './paths.js';
35
+ import { ancestors, beneath, normalize } from './paths.js';
36
36
  import { openRuntimeStore, } from './store.js';
37
37
  import { changedCleanly, sameComposition, validatedCollection, } from './validation.js';
38
38
  import { unconfirmed } from './transfer.js';
@@ -55,10 +55,23 @@ const CONVERSATION_LANE = 'conversations';
55
55
  const ROOM_READS = 256;
56
56
  const ROOM_ATTEMPTS = 16;
57
57
  const ERASE_BATCH = 512;
58
- const REDERIVE_BATCH = 256;
58
+ const DERIVE_BYTES = 1 << 23;
59
+ const DERIVE_FLOOR = 1 << 18;
60
+ // An index build aims at this much database work, so a Save, a query or the next part's progress waits behind at most one short build.
61
+ const DERIVE_AIM_MS = 250;
62
+ // A build past its floor of text stops after a second and a half of work, so a Room whose text costs ten times the usual still reports progress within the 2 s bound.
63
+ const DERIVE_LONGEST_MS = 1_500;
59
64
  const WALK_BYTES = 1 << 20;
60
65
  const QUERY_BYTES = 1 << 20;
61
66
  const STAGE_BYTES = 1 << 20;
67
+ const STAGE_FLOOR = 1 << 16;
68
+ // A batch aims at this much database work, so a Save or a query waits behind at most one short write.
69
+ const STAGE_AIM_MS = 50;
70
+ const STREAMS_HELD = 4;
71
+ const NOTHING_HELD = { parts: 0, bytes: 0 };
72
+ const QUEUED_BYTES = 1 << 23;
73
+ // Bytes still arriving are reported at most this often, well inside the 2 s progress bound, so readiness is not rebuilt for every chunk the network hands over.
74
+ const ARRIVAL_REPORT_MS = 250;
62
75
  const CLEAN_CHANGES = 2_000;
63
76
  // 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
77
  const LOCK_GRACE_MS = 120_000;
@@ -82,7 +95,7 @@ function pendingKey(locator, id) {
82
95
  }
83
96
  export function composeRuntime(options) {
84
97
  const localSaves = options.adapter.localSaves === true;
85
- const roomTypes = roomTypesOf(options.adapter.roomTypes);
98
+ const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
86
99
  let authorization = options.authorization;
87
100
  let opening;
88
101
  const fences = new Map();
@@ -92,20 +105,23 @@ export function composeRuntime(options) {
92
105
  const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
93
106
  let discoveries = 0;
94
107
  const replicas = new FencedMap(isFenced);
108
+ const reopened = new Set();
109
+ const usableWhenHeld = options.usableWhenHeld ?? [];
95
110
  const queuesWithoutReplica = new FencedMap(isFenced);
96
111
  const pendings = new Map();
97
112
  const seeds = new Map();
98
113
  const seedWatchers = new Map();
99
114
  const roomDrafts = new FencedMap(isFenced);
115
+ const attachedFiles = new FencedMap(isFenced);
100
116
  const presentations = new Map();
101
117
  let limits = [];
102
118
  let capabilities = [];
103
119
  const rooms = new FencedMap(isFenced);
104
120
  const counted = new Map();
105
121
  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);
122
+ const builtIn = builtInModel(roomTypes);
123
+ const models = new FencedMap(isFenced);
124
+ const liveModels = new FencedMap(isFenced);
109
125
  const invalidModels = new Set();
110
126
  const discovered = new Map();
111
127
  const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
@@ -113,23 +129,19 @@ export function composeRuntime(options) {
113
129
  const scopedRefusals = new Map();
114
130
  const sourceRefusals = new Map();
115
131
  const declarationRefusals = new Set();
116
- function modelKey(locator, attached) {
117
- return `${attached} ${locator}`;
132
+ function modelOf(locator) {
133
+ return models.get(locator) ?? NO_MODEL;
118
134
  }
119
- function modelOf(locator, attached = 'collection') {
120
- return models.get(modelKey(locator, attached)) ?? NO_MODEL;
121
- }
122
- function savedModel(locator, attached) {
123
- return invalidModels.has(modelKey(locator, attached))
135
+ function savedModel(locator) {
136
+ return invalidModels.has(locator)
124
137
  ? { status: 'unavailable', reason: 'revoked' }
125
- : modelOf(locator, attached);
138
+ : modelOf(locator);
139
+ }
140
+ function composed(record) {
141
+ return compositionOf(record, builtIn);
126
142
  }
127
143
  function compositionFor(record) {
128
- return compositionOf({
129
- ...record,
130
- model: savedModel(record.locator, 'collection'),
131
- selectionModel: savedModel(record.locator, 'typed-selection'),
132
- });
144
+ return composed({ ...record, model: savedModel(record.locator) });
133
145
  }
134
146
  let refusal = null;
135
147
  let storageLoss = null;
@@ -161,7 +173,9 @@ export function composeRuntime(options) {
161
173
  let roomReads = { generation, 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,55 @@ 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 unchecked = new Map();
228
+ let arrivalReported = 0;
229
+ let onScreen = options.shell.onScreen;
230
+ const shownWaiters = [];
231
+ let streams = { generation, free: STREAMS_HELD };
232
+ let queuedBytes = 0;
233
+ const installWaiters = [];
234
+ const stageBudget = {
235
+ bytes: STAGE_FLOOR * 4,
236
+ floor: STAGE_FLOOR,
237
+ ceiling: STAGE_BYTES,
238
+ aim: STAGE_AIM_MS,
239
+ };
240
+ const deriveBudget = {
241
+ bytes: DERIVE_FLOOR * 4,
242
+ floor: DERIVE_FLOOR,
243
+ ceiling: DERIVE_BYTES,
244
+ aim: DERIVE_AIM_MS,
245
+ };
246
+ const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
197
247
  const conversations = createConversationStore({
198
248
  agents: declaredAgents,
199
249
  documents: (declaration, after) => rowsQuery(planOf({
@@ -214,16 +264,27 @@ export function composeRuntime(options) {
214
264
  stream: () => options.adapter.conversations,
215
265
  publish: publishChangeSet,
216
266
  online: () => connectionOf() !== 'unreachable' && refusal === null,
267
+ listed: () => roomsListed,
268
+ full: () => full,
217
269
  closed: () => closed,
218
270
  unanswered: resendLater,
219
271
  expired: (operation) => pastHorizon(operation),
220
272
  });
221
273
  const journal = createLog();
222
- const keptFiles = openedFiles(options.database, isFenced);
223
- let notifyReady = () => { };
274
+ const keptFiles = openedFiles(database, isFenced);
275
+ let readyFromStore = () => { };
224
276
  const ready = new Promise((resolve) => {
225
- notifyReady = resolve;
277
+ readyFromStore = resolve;
226
278
  });
279
+ let notifyDiscovered = () => { };
280
+ const firstDiscovery = new Promise((resolve) => {
281
+ notifyDiscovered = resolve;
282
+ });
283
+ let roomsNamed = false;
284
+ function notifyReady() {
285
+ notifyDiscovered();
286
+ readyFromStore();
287
+ }
227
288
  function rememberRoomDraft(record) {
228
289
  const room = roomDrafts.get(record.locator) ?? new Map();
229
290
  room.set(record.path, record);
@@ -243,7 +304,7 @@ export function composeRuntime(options) {
243
304
  return undefined;
244
305
  const local = await openLocal();
245
306
  const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
246
- return { ...slice, collection: collectionOf(slice.documents, compositionOf(held.record)) };
307
+ return { ...slice, collection: collectionOf(slice.documents, composed(held.record)) };
247
308
  }
248
309
  function roomDraftOf(record, visible) {
249
310
  return structuredClone({
@@ -299,14 +360,40 @@ export function composeRuntime(options) {
299
360
  const value = presentations.get(app);
300
361
  return value === undefined ? null : structuredClone(value);
301
362
  }
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
- });
363
+ async function writtenRoomDraft(room, path, revision, input, next) {
364
+ const parts = readParts(input);
365
+ if (path === '' || normalize(path) !== path)
366
+ throw runtimeError('room-draft-malformed');
367
+ const local = await openLocal();
368
+ const current = roomDrafts.get(room)?.get(path);
369
+ if ((current?.revision ?? null) !== revision) {
370
+ return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
371
+ }
372
+ const file = parts.file === undefined
373
+ ? undefined
374
+ : await heldFile(parts.file.blob, parts.file.base === undefined
375
+ ? binaryBase(await visibleOf(room, [path]), path)
376
+ : parts.file.base);
377
+ const record = {
378
+ locator: room,
379
+ path,
380
+ revision: next,
381
+ parts: held(current?.parts, parts, file),
382
+ returned: current?.returned ?? null,
383
+ };
384
+ await local.keepRoomDraft(record, parts.file === undefined || file === undefined
385
+ ? []
386
+ : [{ key: file.localId, blob: parts.file.blob }]);
387
+ rememberRoomDraft(record);
388
+ publishChangeSet(changeSet(room, { drafts: [path] }));
389
+ return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
390
+ }
391
+ async function keptPresentation(app, next) {
392
+ const local = await openLocal();
393
+ const stored = next(presentations.get(app));
394
+ await local.present({ app, value: stored });
395
+ presentations.set(app, stored);
396
+ publishChangeSet(changeSet('', { presentation: app }));
310
397
  }
311
398
  function emitPresentation(app) {
312
399
  if (closed)
@@ -321,6 +408,20 @@ export function composeRuntime(options) {
321
408
  (queuesWithoutReplica.size > 0 ||
322
409
  [...replicas.values()].some((replica) => replica.queue.length > 0)));
323
410
  }
411
+ function unsent() {
412
+ const changed = new Map();
413
+ for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
414
+ for (const { batch, state } of entriesOf(room)) {
415
+ if (state === 'accepted' || state === 'local')
416
+ continue;
417
+ for (const { path } of batch.changes)
418
+ changed.set(`${room}\u0000${path}`, { room, path });
419
+ }
420
+ }
421
+ return [...changed]
422
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
423
+ .map(([, change]) => change);
424
+ }
324
425
  function attention() {
325
426
  const attentionRequired = [];
326
427
  for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
@@ -353,7 +454,7 @@ export function composeRuntime(options) {
353
454
  if (reset === null || submission.reset < reset)
354
455
  reset = submission.reset;
355
456
  }
356
- return { attentionRequired, reset };
457
+ return { attentionRequired, unsent: unsent(), reset };
357
458
  }
358
459
  function documentsIn(locator) {
359
460
  return replicas.get(locator)?.count ?? 0;
@@ -426,7 +527,7 @@ export function composeRuntime(options) {
426
527
  const governed = governs(reach, room);
427
528
  const attached = [
428
529
  ...(governed.collection ? [modelOf(room.locator)] : []),
429
- ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
530
+ ...(governed.selection ? [builtIn] : []),
430
531
  ];
431
532
  return {
432
533
  locator: room.locator,
@@ -442,11 +543,26 @@ export function composeRuntime(options) {
442
543
  const value = listed();
443
544
  for (const listener of roomListeners)
444
545
  listener(value);
546
+ emitKnownRooms();
547
+ }
548
+ function emitKnownRooms() {
549
+ conversations.learned();
550
+ if (unlistedRooms.size === 0)
551
+ return;
552
+ if (!roomsListed && rooms.size === 0 && refusal === null && !status.providerUnreachable)
553
+ return;
554
+ const value = listed();
555
+ for (const listener of [...unlistedRooms]) {
556
+ unlistedRooms.delete(listener);
557
+ roomListeners.add(listener);
558
+ listener(value);
559
+ }
445
560
  }
446
561
  function emitRefusal() {
447
562
  queueReadiness();
448
563
  for (const listener of refusalListeners)
449
564
  listener(refusal);
565
+ emitKnownRooms();
450
566
  }
451
567
  function emitStorageLoss() {
452
568
  queueReadiness();
@@ -467,6 +583,7 @@ export function composeRuntime(options) {
467
583
  if (closed)
468
584
  return;
469
585
  queueReadiness();
586
+ emitKnownRooms();
470
587
  const next = status.state === 'synced' && !conversations.current()
471
588
  ? { ...status, state: 'syncing' }
472
589
  : status;
@@ -501,18 +618,25 @@ export function composeRuntime(options) {
501
618
  function unpublished(error) {
502
619
  journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
503
620
  }
621
+ function indexed(locator) {
622
+ return replicas.get(locator)?.indexed === true;
623
+ }
504
624
  async function prepareReplica(locator) {
505
625
  for (;;) {
506
626
  const held = replicas.get(locator);
507
- if (held === undefined || closed || prepared.has(locator))
627
+ if (held === undefined || closed || held.indexed)
508
628
  return;
509
629
  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)
630
+ const done = await exclusive(async () => {
631
+ const local = await openLocal();
632
+ return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR));
633
+ });
634
+ const now = replicas.get(locator);
635
+ if (now?.record.key !== key)
512
636
  continue;
513
- if (!done)
637
+ if (!done.done)
514
638
  continue;
515
- prepared.add(locator);
639
+ replicas.set(locator, { ...now, indexed: true });
516
640
  publishChangeSet(changeSet(locator, { documents: null }));
517
641
  liveQueries.recover();
518
642
  queueReadiness();
@@ -520,7 +644,7 @@ export function composeRuntime(options) {
520
644
  }
521
645
  }
522
646
  function unprepared() {
523
- return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unpreparable.has(locator));
647
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
524
648
  }
525
649
  function unpreparedBy(locator, error) {
526
650
  unpreparable.add(locator);
@@ -549,7 +673,11 @@ export function composeRuntime(options) {
549
673
  })();
550
674
  }
551
675
  function roomsOfReach(reach) {
552
- return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
676
+ return new Set([
677
+ ...reach.rooms,
678
+ ...reach.documents.map(roomOfKey),
679
+ ...reach.areas.flatMap((area) => (area.types.length === 0 ? [] : [area.room])),
680
+ ]);
553
681
  }
554
682
  function reachedRooms(plan) {
555
683
  const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
@@ -581,7 +709,7 @@ export function composeRuntime(options) {
581
709
  pending.push({ room: locator, awaits: 'acquisition' });
582
710
  continue;
583
711
  }
584
- if (!prepared.has(locator)) {
712
+ if (!replica.indexed) {
585
713
  pending.push({ room: locator, awaits: 'preparation' });
586
714
  continue;
587
715
  }
@@ -625,7 +753,7 @@ export function composeRuntime(options) {
625
753
  return 'unadmitted';
626
754
  if (!replicas.has(locator))
627
755
  return 'acquiring';
628
- if (!prepared.has(locator))
756
+ if (!indexed(locator))
629
757
  return 'preparing';
630
758
  return 'complete';
631
759
  }
@@ -655,6 +783,7 @@ export function composeRuntime(options) {
655
783
  return [
656
784
  {
657
785
  room,
786
+ name: discovered.get(room)?.name ?? room,
658
787
  file: typeof file === 'string' ? file : '',
659
788
  rule: held.code,
660
789
  location: typeof location === 'string' ? location : null,
@@ -662,6 +791,59 @@ export function composeRuntime(options) {
662
791
  ];
663
792
  });
664
793
  }
794
+ function installationOf(coverage) {
795
+ const rooms = { done: 0, total: 0 };
796
+ const parts = { done: 0, total: 0 };
797
+ const bytes = { done: 0, total: 0 };
798
+ let installing = 0;
799
+ let downloading = false;
800
+ let checking = 0;
801
+ for (const { room, state } of coverage) {
802
+ if (state === 'unadmitted')
803
+ continue;
804
+ rooms.total += 1;
805
+ if (state === 'complete')
806
+ rooms.done += 1;
807
+ const transfer = transfers.get(room);
808
+ const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
809
+ if (underway)
810
+ installing += 1;
811
+ if (underway && (streaming.has(room) || !arrivals.has(room)))
812
+ downloading = true;
813
+ if (transfer !== undefined) {
814
+ const check = unchecked.get(room);
815
+ const unchecking = check === undefined ? 0 : check.bytes * (1 - Math.min(1, check.done / check.text));
816
+ const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
817
+ const arrived = arrivals.get(room);
818
+ parts.done += transfer.done.parts;
819
+ parts.total += Math.max(arrived?.parts ?? 0, transfer.done.parts);
820
+ bytes.done += transfer.done.bytes - Math.ceil(owed + unchecking);
821
+ checking += Math.ceil(owed + unchecking) - Math.ceil(owed);
822
+ bytes.total += Math.max(arrived?.bytes ?? 0, transfer.done.bytes);
823
+ continue;
824
+ }
825
+ const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
826
+ const arrived = arrivals.get(room) ?? held;
827
+ parts.done += held.parts;
828
+ parts.total += arrived.parts;
829
+ bytes.done += held.bytes;
830
+ bytes.total += arrived.bytes;
831
+ }
832
+ const step = installing === 0
833
+ ? null
834
+ : downloading
835
+ ? 'downloading'
836
+ : bytes.done + checking < bytes.total
837
+ ? 'writing'
838
+ : 'replaying';
839
+ return {
840
+ step,
841
+ paused: installing > 0 && !onScreen ? 'hidden' : null,
842
+ rooms,
843
+ parts,
844
+ bytes,
845
+ };
846
+ }
665
847
  function readinessFor(port, reach) {
666
848
  const required = requiredRooms(reach);
667
849
  const search = port.search === 'required';
@@ -682,18 +864,18 @@ export function composeRuntime(options) {
682
864
  persistence,
683
865
  connection: connectionOf(),
684
866
  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,
867
+ space: full ? 'full' : port.space,
868
+ installation: installationOf(coverage),
689
869
  refused: refusedIn(required.rooms),
690
870
  lost: storageLoss !== null,
691
- established: targets[port.target]?.established === true,
871
+ established: targets[port.target]?.established === true ||
872
+ (usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
692
873
  });
693
874
  }
694
875
  function establish(name) {
695
876
  targets = { ...targets, [name]: { established: true } };
696
877
  queueReadiness();
878
+ probeEachMinute();
697
879
  void durably((local) => local.fact('targets', targets)).catch(() => {
698
880
  journal.record(`target record failed ${name}`);
699
881
  });
@@ -731,87 +913,52 @@ export function composeRuntime(options) {
731
913
  readinessWatches.delete(watch);
732
914
  };
733
915
  }
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 ?? '-'}`);
916
+ function filled() {
917
+ if (full || closed)
918
+ return;
919
+ full = true;
920
+ journal.record('space full');
753
921
  queueReadiness();
754
- if (rechecking !== null)
922
+ probeEachMinute();
923
+ }
924
+ function installedTarget() {
925
+ return ((usableWhenHeld.length > 0 && reopened.size > 0) ||
926
+ Object.values(targets).some((target) => target.established));
927
+ }
928
+ function probeEachMinute() {
929
+ if (!full || probing !== null || closed || !installedTarget())
755
930
  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();
931
+ // 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.
932
+ probing = setInterval(() => {
933
+ void probed();
759
934
  }, 60_000);
760
935
  }
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();
936
+ function probed() {
937
+ testing ??= database.ask({ kind: 'space.probe' }).then(() => {
938
+ testing = null;
939
+ spaceFreed();
940
+ return true;
941
+ }, () => {
942
+ testing = null;
943
+ return false;
944
+ });
945
+ return testing;
772
946
  }
773
- function resume() {
774
- if (rechecking !== null)
775
- clearInterval(rechecking);
776
- rechecking = null;
777
- if (paused.size === 0 && capacity === null)
947
+ function spaceFreed() {
948
+ if (!full)
778
949
  return;
779
- paused.clear();
780
- capacity = null;
781
- journal.record('capacity resumed');
950
+ full = false;
951
+ if (probing !== null)
952
+ clearInterval(probing);
953
+ probing = null;
954
+ unpreparable.clear();
955
+ journal.record('space freed');
782
956
  queueReadiness();
783
957
  if (!closed && authorization !== undefined)
784
958
  void sync('unstreamed');
785
959
  }
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
960
  function seedRoomHeld(room, version) {
814
- return prepared.has(room) && reachedBy(room, version);
961
+ return indexed(room) && reachedBy(room, version);
815
962
  }
816
963
  function seedHeld(at) {
817
964
  return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
@@ -934,6 +1081,10 @@ export function composeRuntime(options) {
934
1081
  }
935
1082
  function publishChangeSet(set) {
936
1083
  applyChangeSet(set);
1084
+ for (const { locator, path } of set.documents) {
1085
+ if (path !== null)
1086
+ attachedFiles.get(locator)?.delete(path);
1087
+ }
937
1088
  if (set.rooms.length > 0 ||
938
1089
  [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
939
1090
  emitRooms();
@@ -959,27 +1110,24 @@ export function composeRuntime(options) {
959
1110
  const records = [...replicas].map(([locator, held]) => ({
960
1111
  locator,
961
1112
  held,
962
- record: { ...held.record, model: revoked, selectionModel: revoked },
1113
+ record: { ...held.record, model: revoked },
963
1114
  }));
1115
+ let answer = null;
964
1116
  try {
965
1117
  const local = await openLocal();
966
- await local.commit({
1118
+ answer = await local.commit({
967
1119
  rooms: records.map(({ locator, record }) => ({
968
1120
  locator,
969
1121
  key: record.key,
970
1122
  record: storedRecord(record),
971
- composition: compositionOf(record),
1123
+ composed: composedOf(composed(record)),
972
1124
  })),
973
1125
  });
974
1126
  }
975
1127
  catch {
976
1128
  journal.record('model revocation persistence failed');
977
1129
  }
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
- }
1130
+ const keys = new Set([...models.keys(), ...rooms.keys()]);
983
1131
  for (const key of keys) {
984
1132
  invalidModels.add(key);
985
1133
  models.set(key, revoked);
@@ -989,8 +1137,8 @@ export function composeRuntime(options) {
989
1137
  for (const { locator, held, record } of records) {
990
1138
  if (replicas.get(locator) !== held)
991
1139
  continue;
992
- recomposed(locator, held.record, record);
993
- replicas.set(locator, { ...held, record });
1140
+ const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
1141
+ replicas.set(locator, { ...held, record, indexed });
994
1142
  }
995
1143
  for (const locator of [...rooms.keys()]) {
996
1144
  const room = rooms.get(locator);
@@ -1034,7 +1182,7 @@ export function composeRuntime(options) {
1034
1182
  ? null
1035
1183
  : {
1036
1184
  roots: selection.roots,
1037
- model: modelTypes(modelOf(room.locator, 'typed-selection')),
1185
+ model: modelTypes(builtIn),
1038
1186
  },
1039
1187
  providerOwnedPaths: room.providerOwnedPaths,
1040
1188
  roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
@@ -1123,7 +1271,8 @@ export function composeRuntime(options) {
1123
1271
  queuesWithoutReplica.delete(locator);
1124
1272
  rooms.delete(locator);
1125
1273
  roomDrafts.delete(locator);
1126
- transfers.delete(locator);
1274
+ attachedFiles.delete(locator);
1275
+ untransferred(locator);
1127
1276
  catching.delete(locator);
1128
1277
  discovered.delete(locator);
1129
1278
  announced.delete(locator);
@@ -1131,17 +1280,12 @@ export function composeRuntime(options) {
1131
1280
  stale.delete(locator);
1132
1281
  repairable.delete(locator);
1133
1282
  unpreparable.delete(locator);
1134
- paused.delete(locator);
1135
- prepared.delete(locator);
1136
1283
  scopedRefusals.delete(locator);
1137
1284
  sourceRefusals.delete(locator);
1138
1285
  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
- }
1286
+ models.delete(locator);
1287
+ liveModels.delete(locator);
1288
+ invalidModels.delete(locator);
1145
1289
  journal.record(`room lost ${locator}`);
1146
1290
  }
1147
1291
  if (fresh.length === 0)
@@ -1189,7 +1333,14 @@ export function composeRuntime(options) {
1189
1333
  });
1190
1334
  await eraseFenced(fresh);
1191
1335
  }
1192
- function restored(key, persisted) {
1336
+ function retyped(persisted) {
1337
+ if (persisted.status === 'unavailable')
1338
+ return persisted;
1339
+ const { state } = attachedModel({ kind: 'model', snapshot: persisted.snapshot }, NO_MODEL, roomTypes);
1340
+ return state.status === 'unavailable' ? state : { ...persisted, types: state.types };
1341
+ }
1342
+ function restored(key, held) {
1343
+ const persisted = retyped(held);
1193
1344
  const prior = models.get(key);
1194
1345
  if (prior !== undefined &&
1195
1346
  prior.status !== 'unavailable' &&
@@ -1220,8 +1371,8 @@ export function composeRuntime(options) {
1220
1371
  const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1221
1372
  targets = held.targets;
1222
1373
  transfers.clear();
1223
- for (const [locator, record] of held.transfers)
1224
- transfers.set(locator, record);
1374
+ arrivals.clear();
1375
+ streaming.clear();
1225
1376
  limits = held.providerLimits;
1226
1377
  capabilities = held.capabilities;
1227
1378
  member = held.member;
@@ -1231,7 +1382,7 @@ export function composeRuntime(options) {
1231
1382
  presentations.clear();
1232
1383
  for (const record of held.presentation)
1233
1384
  presentations.set(record.app, record.value);
1234
- conversations.hydrate(heldConversations, chatDrafts);
1385
+ conversations.hydrate(heldConversations, chatDrafts, held.conversationList);
1235
1386
  pendings.clear();
1236
1387
  for (const queue of queues) {
1237
1388
  for (const { batch, state } of queue.pending) {
@@ -1239,7 +1390,7 @@ export function composeRuntime(options) {
1239
1390
  }
1240
1391
  }
1241
1392
  replicas.clear();
1242
- prepared.clear();
1393
+ reopened.clear();
1243
1394
  queuesWithoutReplica.clear();
1244
1395
  for (const [locator, queue] of queuedBatches) {
1245
1396
  if (!held.rooms.has(locator))
@@ -1248,39 +1399,40 @@ export function composeRuntime(options) {
1248
1399
  records.forEach((stored, index) => {
1249
1400
  if (fences.has(stored.locator))
1250
1401
  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 };
1402
+ const record = { ...stored, model: restored(stored.locator, stored.model) };
1403
+ reopened.add(record.locator);
1254
1404
  replicas.set(record.locator, {
1255
1405
  record,
1256
1406
  queue: queuedBatches.get(record.locator) ?? [],
1257
1407
  overlaid: held.overlaid.get(record.key) ?? new Set(),
1258
1408
  count: counts[index] ?? 0,
1259
1409
  confirmed: 0,
1410
+ indexed: !held.unindexed.has(record.key),
1260
1411
  });
1261
- if (!held.unprepared.has(record.key))
1262
- prepared.add(record.locator);
1263
1412
  const existing = rooms.get(record.locator);
1264
1413
  if (existing === undefined)
1265
- list(record, false);
1414
+ list(record, record.roomVersion !== null);
1266
1415
  else {
1267
1416
  rooms.set(record.locator, {
1268
1417
  ...existing,
1269
1418
  roomVersion: record.roomVersion,
1270
1419
  operations: record.operations,
1271
1420
  replica: record.replica,
1272
- model: modelTypes(model),
1421
+ model: modelTypes(record.model),
1273
1422
  typedSelection: record.typedSelection === null
1274
1423
  ? null
1275
- : {
1276
- roots: record.typedSelection.roots,
1277
- model: modelTypes(selectionModel),
1278
- },
1424
+ : { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
1279
1425
  });
1280
1426
  }
1281
1427
  });
1282
- for (const key of held.orphans)
1283
- await opened.release(key);
1428
+ try {
1429
+ for (const key of held.orphans)
1430
+ await opened.release(key);
1431
+ }
1432
+ catch (error) {
1433
+ if (!refusedForSpace(error))
1434
+ throw error;
1435
+ }
1284
1436
  if (held.erasing || held.orphans.length > 0)
1285
1437
  queueMicrotask(sweep);
1286
1438
  return {
@@ -1305,7 +1457,7 @@ export function composeRuntime(options) {
1305
1457
  async function open() {
1306
1458
  if (closed)
1307
1459
  throw new Error('runtime closed');
1308
- const opened = openRuntimeStore(isFenced, options.database);
1460
+ const opened = openRuntimeStore(isFenced, database);
1309
1461
  const interrupted = await opened.fences();
1310
1462
  for (const locator of interrupted) {
1311
1463
  fences.set(locator, { at: discoveries, erased: false });
@@ -1351,10 +1503,29 @@ export function composeRuntime(options) {
1351
1503
  return opening;
1352
1504
  }
1353
1505
  function exclusive(work) {
1506
+ gathering = null;
1354
1507
  const next = mutations.then(work, work);
1355
1508
  mutations = next.then(() => undefined, () => undefined);
1356
1509
  return next;
1357
1510
  }
1511
+ function gathered(key, input, merge, work) {
1512
+ const tail = gathering;
1513
+ if (tail !== null && tail.key === key) {
1514
+ const merged = merge(tail.input, input);
1515
+ if (merged !== null) {
1516
+ tail.input = merged;
1517
+ return tail.done;
1518
+ }
1519
+ }
1520
+ const entry = { key, input, done: Promise.resolve() };
1521
+ entry.done = exclusive(() => {
1522
+ if (gathering === entry)
1523
+ gathering = null;
1524
+ return work(entry.input);
1525
+ });
1526
+ gathering = entry;
1527
+ return entry.done;
1528
+ }
1358
1529
  function withdrawable(locator) {
1359
1530
  return (batch) => {
1360
1531
  const state = pendings.get(pendingKey(locator, batch.id));
@@ -1436,26 +1607,14 @@ export function composeRuntime(options) {
1436
1607
  },
1437
1608
  };
1438
1609
  }
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))
1610
+ function committedRoom(locator, answer) {
1611
+ const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
1612
+ if (!indexed)
1454
1613
  prepare([locator]);
1455
- return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0 };
1614
+ return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
1456
1615
  }
1457
1616
  function replayed(record, confirmedLayer, queue) {
1458
- return stepped(replaying(confirmedLayer, queue, record.locator, compositionOf(record)));
1617
+ return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
1459
1618
  }
1460
1619
  async function overlayDiff(record, before, visible, touched) {
1461
1620
  const kept = before !== undefined && before.record.key === record.key
@@ -1518,7 +1677,7 @@ export function composeRuntime(options) {
1518
1677
  const confirmedLayer = overrides === undefined
1519
1678
  ? read
1520
1679
  : await stepped(slicingWith(read, overrides.set, overrides.removed));
1521
- const composition = compositionOf(record);
1680
+ const composition = composed(record);
1522
1681
  const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
1523
1682
  const founded = kept.filter((batch) => !queue.includes(batch));
1524
1683
  const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
@@ -1561,6 +1720,7 @@ export function composeRuntime(options) {
1561
1720
  key: record.key,
1562
1721
  record: storedRecord(record),
1563
1722
  roots: record.typedSelection?.roots ?? [],
1723
+ composed: composedOf(composition),
1564
1724
  ...settling.commit,
1565
1725
  ...(overrides === undefined
1566
1726
  ? {}
@@ -1602,14 +1762,14 @@ export function composeRuntime(options) {
1602
1762
  const confirmedChanged = settling.commit?.published !== undefined ||
1603
1763
  settling.commit?.refreshed === true ||
1604
1764
  overrides !== undefined;
1605
- recomposed(locator, before?.record, record);
1606
- const { count } = committedRoom(locator, record, answer);
1765
+ const { count, indexed } = committedRoom(locator, answer);
1607
1766
  replicas.set(locator, {
1608
1767
  record,
1609
1768
  queue: kept,
1610
1769
  overlaid: touched,
1611
1770
  count,
1612
1771
  confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
1772
+ indexed,
1613
1773
  });
1614
1774
  return mergeChangeSets([
1615
1775
  changeSet(locator, {
@@ -1624,38 +1784,28 @@ export function composeRuntime(options) {
1624
1784
  };
1625
1785
  }
1626
1786
  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
- }
1787
+ const model = record.model;
1788
+ models.set(locator, model);
1789
+ invalidModels.delete(locator);
1790
+ if (model.status === 'current')
1791
+ liveModels.set(locator, JSON.stringify(model.snapshot));
1792
+ else if (model.status === 'unavailable')
1793
+ liveModels.delete(locator);
1639
1794
  }
1640
1795
  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
- };
1796
+ const answer = answers?.collection;
1797
+ if (answer === undefined)
1798
+ return held;
1799
+ const { state, refused } = attachedModel(answer, held.collection, roomTypes);
1800
+ if (refused !== null) {
1801
+ const where = refused.location === undefined ? '' : ` ${refused.location}`;
1802
+ journal.record(`model invalid ${locator} collection ${refused.identity} ${refused.rule}${where}`);
1803
+ }
1804
+ models.set(locator, state);
1805
+ return { collection: state };
1656
1806
  }
1657
1807
  function publish(local, room, record, landing, live) {
1658
- return exclusive(async () => {
1808
+ const landed = exclusive(async () => {
1659
1809
  if (!live()) {
1660
1810
  caughtUpTo(room.locator);
1661
1811
  return true;
@@ -1679,29 +1829,28 @@ export function composeRuntime(options) {
1679
1829
  ? {
1680
1830
  room: true,
1681
1831
  view: 'refreshed',
1682
- commit: { refreshed: true, composition: compositionOf(record) },
1832
+ commit: { refreshed: true },
1683
1833
  }
1684
1834
  : {
1685
1835
  room: true,
1686
1836
  view: landing.refreshed ? 'refreshed' : 'confirmed',
1687
1837
  commit: {
1688
1838
  published: { from: prior?.record.key ?? null },
1689
- composition: compositionOf(record),
1690
1839
  ...(landing.refreshed ? { refreshed: true } : {}),
1691
1840
  },
1692
1841
  });
1693
1842
  }
1694
1843
  catch (error) {
1695
- if (!exceeded(error))
1844
+ if (!refusedForSpace(error))
1696
1845
  throw error;
1697
- await exceededOn(room.locator, null);
1698
1846
  caughtUpTo(room.locator);
1699
1847
  return true;
1700
1848
  }
1701
1849
  if (landing.kind === 'bootstrapped') {
1702
- transfers.delete(room.locator);
1850
+ untransferred(room.locator);
1703
1851
  catching.delete(room.locator);
1704
- sweep();
1852
+ if (prior !== undefined && prior.record.key !== record.key)
1853
+ sweep();
1705
1854
  }
1706
1855
  for (const batch of accepted)
1707
1856
  pendings.delete(pendingKey(room.locator, batch.id));
@@ -1721,6 +1870,7 @@ export function composeRuntime(options) {
1721
1870
  }
1722
1871
  return true;
1723
1872
  });
1873
+ return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
1724
1874
  }
1725
1875
  async function savedRoom(locator) {
1726
1876
  const local = await openLocal();
@@ -1745,12 +1895,7 @@ export function composeRuntime(options) {
1745
1895
  return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
1746
1896
  }
1747
1897
  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
- }
1898
+ const read = readSavedParts(named);
1754
1899
  const target = await savedRoom(locator);
1755
1900
  const saved = [];
1756
1901
  const files = [];
@@ -1759,7 +1904,7 @@ export function composeRuntime(options) {
1759
1904
  for (const { path, parts } of read) {
1760
1905
  const file = parts.file === undefined
1761
1906
  ? undefined
1762
- : await heldFile(parts.file.blob, binaryBase(visible, path));
1907
+ : await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
1763
1908
  if (parts.file !== undefined && file !== undefined) {
1764
1909
  files.push({ key: file.localId, blob: parts.file.blob });
1765
1910
  }
@@ -1794,13 +1939,14 @@ export function composeRuntime(options) {
1794
1939
  const referring = removed.length === 0
1795
1940
  ? []
1796
1941
  : (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
1797
- const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
1942
+ const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
1798
1943
  const { visible } = await replayed(record, confirmedLayer, replica.queue);
1799
- const composition = compositionOf(record);
1944
+ const composition = composed(record);
1800
1945
  return {
1801
1946
  record,
1802
1947
  confirmed: confirmedLayer,
1803
1948
  visible,
1949
+ folders: new Set(await local.folders(record.key, 'visible', paths)),
1804
1950
  *visibleOver(queue) {
1805
1951
  return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
1806
1952
  },
@@ -1812,13 +1958,7 @@ export function composeRuntime(options) {
1812
1958
  return false;
1813
1959
  const local = await openLocal();
1814
1960
  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)));
1961
+ return local.typedWithin(record.key, 'visible', roots, false);
1822
1962
  }
1823
1963
  function textDiffers(left, right) {
1824
1964
  return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
@@ -1923,7 +2063,7 @@ export function composeRuntime(options) {
1923
2063
  const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
1924
2064
  const clean = available(composition) &&
1925
2065
  changed.length <= CLEAN_CHANGES &&
1926
- (await changedCleanly(changed, compositionOf(record), composition, {
2066
+ (await changedCleanly(changed, composed(record), composition, {
1927
2067
  lookup,
1928
2068
  texts: textsOf(local, record.key, 'confirmed'),
1929
2069
  referrers: referrersOf(local, record.key, 'confirmed'),
@@ -1954,6 +2094,7 @@ export function composeRuntime(options) {
1954
2094
  operations: room.operations,
1955
2095
  providerOwnedPaths: room.providerOwnedPaths,
1956
2096
  visible: saving.visible,
2097
+ folders: saving.folders,
1957
2098
  queued: queuedDocuments(replica.queue),
1958
2099
  unmodeled: await unmodeledIn(record),
1959
2100
  validate: validatorOf(record, saving.confirmed),
@@ -1974,6 +2115,7 @@ export function composeRuntime(options) {
1974
2115
  locator,
1975
2116
  key: saving.record.key,
1976
2117
  roots: saving.record.typedSelection?.roots ?? [],
2118
+ composed: composedOf(composed(saving.record)),
1977
2119
  overlay: diff.overlay,
1978
2120
  });
1979
2121
  }
@@ -2005,7 +2147,7 @@ export function composeRuntime(options) {
2005
2147
  }
2006
2148
  const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
2007
2149
  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)));
2150
+ const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
2009
2151
  if (changed.length > 0)
2010
2152
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2011
2153
  const target = replicas.get(destination);
@@ -2117,6 +2259,8 @@ export function composeRuntime(options) {
2117
2259
  submitLater(locator);
2118
2260
  }
2119
2261
  async function committed(locator, target, saved, consumed, writtenBy, files = []) {
2262
+ if (full)
2263
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
2120
2264
  const into = movedInto(locator, saved);
2121
2265
  if (into !== null) {
2122
2266
  return committedMove(locator, into, target, saved, consumed, writtenBy, files);
@@ -2128,7 +2272,7 @@ export function composeRuntime(options) {
2128
2272
  ...saved.map(({ path }) => path),
2129
2273
  ...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
2130
2274
  ]);
2131
- const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, underneath(held, path, saving.visible)));
2275
+ const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
2132
2276
  if (changed.length > 0)
2133
2277
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2134
2278
  const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
@@ -2156,6 +2300,7 @@ export function composeRuntime(options) {
2156
2300
  providerLimits: limits,
2157
2301
  handle: room.handle,
2158
2302
  visible: saving.visible,
2303
+ folders: saving.folders,
2159
2304
  changes,
2160
2305
  bases: parts.flatMap(({ bases }) => bases),
2161
2306
  queued: queuedDocuments(replica.queue),
@@ -2306,6 +2451,9 @@ export function composeRuntime(options) {
2306
2451
  async function readHistory(locator, path, query = {}) {
2307
2452
  if (closed || refusal !== null)
2308
2453
  return UNCERTAIN;
2454
+ await firstDiscovery;
2455
+ if (!roomsNamed)
2456
+ return UNCERTAIN;
2309
2457
  try {
2310
2458
  await ensureAuthorization();
2311
2459
  }
@@ -2395,14 +2543,12 @@ export function composeRuntime(options) {
2395
2543
  return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
2396
2544
  };
2397
2545
  }
2398
- function binaryRoom(request) {
2399
- if (request.kind === 'path')
2400
- return request.locator;
2401
- return options.adapter.referrerScope(request.referrer)?.locator ?? null;
2402
- }
2403
2546
  async function transferBinary(request, transfer, answer) {
2404
2547
  if (closed || refusal !== null)
2405
2548
  return UNCERTAIN;
2549
+ await firstDiscovery;
2550
+ if (!roomsNamed)
2551
+ return UNCERTAIN;
2406
2552
  try {
2407
2553
  await ensureAuthorization();
2408
2554
  }
@@ -2412,16 +2558,13 @@ export function composeRuntime(options) {
2412
2558
  if (closed || authorization === undefined || refusal !== null)
2413
2559
  return UNCERTAIN;
2414
2560
  const since = fencings;
2415
- journal.record(request.kind === 'path'
2416
- ? `transfer binary ${request.locator} ${request.path}`
2417
- : `transfer binary ${request.fileRef}`);
2561
+ journal.record(`transfer binary ${request.locator} ${request.path}`);
2418
2562
  try {
2419
2563
  const result = await transfer(authorization);
2420
2564
  const settled = result;
2421
2565
  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 });
2566
+ if (fencedSince(request.locator, since))
2567
+ return failure({ kind: 'room-not-readable', locator: request.locator });
2425
2568
  if (settled.kind === 'scoped-refusal') {
2426
2569
  const room = replicas.get(settled.locator)?.record;
2427
2570
  if (room !== undefined)
@@ -2436,23 +2579,27 @@ export function composeRuntime(options) {
2436
2579
  return UNCERTAIN;
2437
2580
  }
2438
2581
  }
2582
+ function fetchBinary(request, name) {
2583
+ return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2584
+ }
2439
2585
  async function readBinary(request, name) {
2440
2586
  if (closed)
2441
2587
  return UNCERTAIN;
2442
2588
  const held = await keptFiles.find(name);
2443
2589
  if (held !== null)
2444
2590
  return held;
2445
- return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2591
+ return fetchBinary(request, name);
2446
2592
  }
2447
2593
  async function binaryPath(locator, path, revision) {
2448
2594
  if (revision !== undefined)
2449
2595
  return { kind: 'path', locator, path, revision };
2450
2596
  const replica = replicas.get(locator);
2451
- if (replica === undefined)
2452
- return null;
2453
2597
  const local = await openLocal();
2454
- const [held] = await local.entries(replica.record.key, 'visible', [path]);
2455
- if (held?.binary === undefined || held.binary === null)
2598
+ const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
2599
+ const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
2600
+ if (attached !== undefined)
2601
+ return { kind: 'path', locator, path, revision: attached.revision };
2602
+ if (replica === undefined || held?.binary === undefined || held.binary === null)
2456
2603
  return null;
2457
2604
  if (held.binary.state === 'pending')
2458
2605
  return { kind: 'pending', localId: held.binary.localId };
@@ -2468,7 +2615,7 @@ export function composeRuntime(options) {
2468
2615
  const holds = (at) => {
2469
2616
  const kept = confirmedLayer.binaries[at];
2470
2617
  return (kept?.state === 'accepted' &&
2471
- kept.fileRef === record.fileRef &&
2618
+ kept.sha256 === record.sha256 &&
2472
2619
  confirmedLayer.revisions[at] === record.revision);
2473
2620
  };
2474
2621
  let confirmedAt = path;
@@ -2482,49 +2629,91 @@ export function composeRuntime(options) {
2482
2629
  locator,
2483
2630
  path: confirmedAt,
2484
2631
  revision: record.revision,
2485
- fileRef: record.fileRef,
2486
2632
  };
2487
2633
  }
2488
- async function readBinaryPath(locator, path, revision) {
2634
+ async function askedBinary(locator, path, revision) {
2489
2635
  await ready;
2490
2636
  const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2491
- const asked = drafted !== undefined && drafted.localId === revision
2637
+ return drafted !== undefined && drafted.localId === revision
2492
2638
  ? { kind: 'pending', localId: revision }
2493
- : await binaryPath(locator, path, revision);
2639
+ : binaryPath(locator, path, revision);
2640
+ }
2641
+ function pendingBinary(locator, path, localId) {
2642
+ const facts = queuedFacts(locator, path, localId);
2643
+ return facts === null
2644
+ ? absentBinary(path)
2645
+ : {
2646
+ kind: 'binary',
2647
+ file: localId,
2648
+ byteLength: facts.byteLength,
2649
+ mediaType: facts.mediaType,
2650
+ };
2651
+ }
2652
+ async function readBinaryPath(locator, path, revision) {
2653
+ const asked = await askedBinary(locator, path, revision);
2494
2654
  if (asked === null)
2495
2655
  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
- }
2656
+ if (asked.kind === 'pending')
2657
+ return pendingBinary(locator, path, asked.localId);
2507
2658
  return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
2508
2659
  }
2660
+ async function heldMediaType(locator, path) {
2661
+ const replica = replicas.get(locator);
2662
+ const [held] = replica === undefined
2663
+ ? []
2664
+ : await (await openLocal()).entries(replica.record.key, 'visible', [path]);
2665
+ return held === undefined
2666
+ ? attachedFiles.get(locator)?.get(path)?.mediaType
2667
+ : held.binary?.mediaType;
2668
+ }
2669
+ function linkOf(asked, held) {
2670
+ return transferBinary(asked, (authorization) => options.adapter.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
2671
+ }
2509
2672
  async function linkBinaryPath(locator, path, revision) {
2510
2673
  await ready;
2511
2674
  const asked = await binaryPath(locator, path, revision);
2512
2675
  if (asked === null || asked.kind === 'pending')
2513
2676
  return absentBinary(path);
2514
- return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
2677
+ return linkOf(asked, revision === undefined ? await heldMediaType(locator, path) : undefined);
2515
2678
  }
2516
- async function readBinaryReference(reference) {
2679
+ async function showBinaryPath(locator, path, revision) {
2680
+ const asked = await askedBinary(locator, path, revision);
2681
+ if (asked === null)
2682
+ return absentBinary(path);
2683
+ if (asked.kind === 'pending')
2684
+ return pendingBinary(locator, path, asked.localId);
2685
+ if (closed)
2686
+ return UNCERTAIN;
2687
+ const name = { locator, path: asked.path, revision: asked.revision };
2688
+ const kept = await keptFiles.find(name);
2689
+ if (kept !== null)
2690
+ return kept;
2691
+ const mediaType = await heldMediaType(locator, path);
2692
+ if (mediaType?.startsWith('image/') === true)
2693
+ return fetchBinary(asked, name);
2694
+ return linkOf(asked, revision === undefined ? mediaType : undefined);
2695
+ }
2696
+ async function attachBinary(locator, file) {
2517
2697
  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 });
2698
+ await firstDiscovery;
2699
+ if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
2700
+ return OFFLINE;
2701
+ const request = await attachedFile(locator, file);
2702
+ return transferBinary({ locator, path: file.name }, async (held) => {
2703
+ const result = await options.adapter.attach(held, request);
2704
+ if (result.kind === 'attached') {
2705
+ const landed = attachedFiles.get(locator) ?? new Map();
2706
+ landed.set(result.path, { revision: result.revision, mediaType: request.mediaType });
2707
+ attachedFiles.set(locator, landed);
2708
+ }
2709
+ return result;
2710
+ }, attachAnswer);
2522
2711
  }
2523
2712
  function readPart(file, part) {
2524
- return options.database.ask({ kind: 'file.part', key: file, part });
2713
+ return database.ask({ kind: 'file.part', key: file, part });
2525
2714
  }
2526
2715
  async function cleanUp() {
2527
- const freed = await options.database.ask({ kind: 'opened.clean' });
2716
+ const freed = await database.ask({ kind: 'opened.clean' });
2528
2717
  return freed + ((await options.shell.cleanUp?.()) ?? 0);
2529
2718
  }
2530
2719
  function declaredAgents() {
@@ -2693,18 +2882,25 @@ export function composeRuntime(options) {
2693
2882
  after = page.next;
2694
2883
  }
2695
2884
  }
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';
2885
+ async function validatedRoom(local, record, view, previous, overrides = null, checked = null) {
2886
+ const composition = composed(record);
2887
+ const modeled = record.model.status !== 'unavailable' || record.typedSelection !== null;
2699
2888
  const lookup = overrides === null
2700
2889
  ? lookupOf(local, record.key, view)
2701
2890
  : overriddenLookup(lookupOf(local, record.key, view), overrides);
2702
2891
  const texts = overrides === null
2703
2892
  ? textsOf(local, record.key, view)
2704
2893
  : overriddenTexts(textsOf(local, record.key, view), overrides);
2705
- const walk = overrides === null
2894
+ const walking = overrides === null
2706
2895
  ? walkOf(local, record.key, view)
2707
2896
  : overriddenWalk(walkOf(local, record.key, view), overrides);
2897
+ const walk = checked === null
2898
+ ? walking
2899
+ : async (after) => {
2900
+ const page = await walking(after);
2901
+ checked(page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0));
2902
+ return page;
2903
+ };
2708
2904
  let changes = null;
2709
2905
  let reshaped = false;
2710
2906
  if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
@@ -2729,11 +2925,11 @@ export function composeRuntime(options) {
2729
2925
  }
2730
2926
  const named = changes === null ? null : changes.map(({ path }) => path);
2731
2927
  const textChanged = changes === null || changes.length > 0 || reshaped;
2732
- const modelsChanged = previous === null || !sameComposition(compositionOf(previous.record), composition);
2928
+ const modelsChanged = previous === null || !sameComposition(composed(previous.record), composition);
2733
2929
  if (modeled && (textChanged || modelsChanged)) {
2734
2930
  const clean = previous !== null &&
2735
2931
  changes !== null &&
2736
- (await changedCleanly(changes, compositionOf(previous.record), composition, {
2932
+ (await changedCleanly(changes, composed(previous.record), composition, {
2737
2933
  lookup,
2738
2934
  texts,
2739
2935
  referrers: referrersOf(local, record.key, 'confirmed'),
@@ -2751,43 +2947,210 @@ export function composeRuntime(options) {
2751
2947
  return unreadableRoom(texts, walk, named);
2752
2948
  return null;
2753
2949
  }
2754
- function refreshingOf(local, key, liveness) {
2950
+ function refreshingOf(local, key, composition, liveness, replaying = null) {
2951
+ const write = replaying === null ? exclusive : finishingWrite;
2755
2952
  return {
2953
+ async turn() {
2954
+ if (replaying === null)
2955
+ return;
2956
+ installing(replaying);
2957
+ await screened();
2958
+ },
2756
2959
  prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
2757
2960
  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 () => {
2961
+ for (let written = 0;;) {
2962
+ if (replaying !== null)
2963
+ await screened();
2964
+ const end = batchEnd(page.entries, written);
2965
+ const last = end === page.entries.length;
2966
+ const entries = page.entries.slice(written, end);
2967
+ const staged = await write(async () => {
2762
2968
  if (!liveness())
2763
2969
  return false;
2764
- await local.refresh(key, last
2970
+ await timed(stageBudget, () => local.refresh(key, composition, last
2765
2971
  ? { ...page, entries }
2766
- : { entries, removals: [], folders: [], folderRemovals: [] });
2972
+ : { entries, removals: [], folders: [], folderRemovals: [] }));
2767
2973
  return true;
2768
2974
  });
2769
2975
  if (!staged)
2770
2976
  return false;
2977
+ if (last)
2978
+ return true;
2979
+ written = end;
2771
2980
  }
2772
- return true;
2773
2981
  },
2774
2982
  };
2775
2983
  }
2776
- function batched(entries) {
2777
- const batches = [[]];
2984
+ function batchEnd(entries, from) {
2985
+ let end = from;
2778
2986
  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);
2987
+ while (end < entries.length) {
2988
+ const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
2989
+ if (spent + size > stageBudget.bytes && end > from)
2990
+ break;
2786
2991
  spent += size;
2992
+ end += 1;
2993
+ }
2994
+ return end;
2995
+ }
2996
+ async function timed(budget, work) {
2997
+ const started = performance.now();
2998
+ const done = await work();
2999
+ const spent = performance.now() - started;
3000
+ if (spent < budget.aim / 2)
3001
+ budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
3002
+ else if (spent > budget.aim * 2) {
3003
+ budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
3004
+ }
3005
+ return done;
3006
+ }
3007
+ function installWrite(work) {
3008
+ return installTurn(installTurns.starting, work);
3009
+ }
3010
+ function finishingWrite(work) {
3011
+ return installTurn(installTurns.finishing, work);
3012
+ }
3013
+ function installTurn(queue, work) {
3014
+ return new Promise((resolve, reject) => {
3015
+ queue.push(() => void exclusive(work)
3016
+ .then(resolve, reject)
3017
+ .finally(() => {
3018
+ installTurns.busy = false;
3019
+ nextInstallTurn();
3020
+ }));
3021
+ nextInstallTurn();
3022
+ });
3023
+ }
3024
+ function nextInstallTurn() {
3025
+ if (installTurns.busy || installTurns.ahead > 0)
3026
+ return;
3027
+ const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
3028
+ if (turn === undefined)
3029
+ return;
3030
+ installTurns.busy = true;
3031
+ turn();
3032
+ }
3033
+ async function ahead(work) {
3034
+ let taken = false;
3035
+ const hold = {
3036
+ take() {
3037
+ if (taken)
3038
+ return;
3039
+ taken = true;
3040
+ installTurns.ahead += 1;
3041
+ },
3042
+ release() {
3043
+ if (!taken)
3044
+ return;
3045
+ taken = false;
3046
+ installTurns.ahead -= 1;
3047
+ resumeInstall();
3048
+ },
3049
+ };
3050
+ hold.take();
3051
+ try {
3052
+ return await work(hold);
3053
+ }
3054
+ finally {
3055
+ hold.release();
3056
+ }
3057
+ }
3058
+ function resumeInstall() {
3059
+ if (installTurns.ahead > 0)
3060
+ return;
3061
+ void yielded().then(nextInstallTurn);
3062
+ }
3063
+ function screened() {
3064
+ if (onScreen || closed)
3065
+ return Promise.resolve();
3066
+ queueReadiness();
3067
+ return new Promise((resolve) => shownWaiters.push(resolve));
3068
+ }
3069
+ async function heldStream() {
3070
+ for (;;) {
3071
+ if (streams.generation !== generation)
3072
+ streams = { generation, free: STREAMS_HELD };
3073
+ const held = streams;
3074
+ if (held.free > 0) {
3075
+ held.free -= 1;
3076
+ return once(() => {
3077
+ held.free += 1;
3078
+ });
3079
+ }
3080
+ await new Promise((admit) => installWaiters.push(admit));
3081
+ }
3082
+ }
3083
+ async function unqueued() {
3084
+ while (queuedBytes >= QUEUED_BYTES) {
3085
+ await new Promise((admit) => installWaiters.push(admit));
3086
+ }
3087
+ }
3088
+ function once(release) {
3089
+ let released = false;
3090
+ return () => {
3091
+ if (released)
3092
+ return;
3093
+ released = true;
3094
+ release();
3095
+ for (const admit of installWaiters.splice(0))
3096
+ admit();
3097
+ };
3098
+ }
3099
+ function installing(locator) {
3100
+ installs.add(locator);
3101
+ queueReadiness();
3102
+ }
3103
+ function untransferred(locator) {
3104
+ transfers.delete(locator);
3105
+ arrivals.delete(locator);
3106
+ streaming.delete(locator);
3107
+ underived.delete(locator);
3108
+ unindexed.delete(locator);
3109
+ unchecked.delete(locator);
3110
+ }
3111
+ function owedIndexes(locator, bytes, text) {
3112
+ if (bytes <= 0 || text <= 0)
3113
+ return;
3114
+ unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes: bytes / 2, text }]);
3115
+ const check = unchecked.get(locator) ?? { bytes: 0, text: 0, done: 0 };
3116
+ unchecked.set(locator, { ...check, bytes: check.bytes + bytes / 2, text: check.text + text });
3117
+ }
3118
+ function built(locator, { done, text }) {
3119
+ const owed = unindexed.get(locator) ?? [];
3120
+ let left = text;
3121
+ while (!done && left > 0 && owed.length > 0) {
3122
+ const [first] = owed;
3123
+ if (left >= first.text) {
3124
+ left -= first.text;
3125
+ owed.shift();
3126
+ continue;
3127
+ }
3128
+ owed[0] = {
3129
+ bytes: (first.bytes * (first.text - left)) / first.text,
3130
+ text: first.text - left,
3131
+ };
3132
+ left = 0;
2787
3133
  }
2788
- return batches;
3134
+ if (done || owed.length === 0)
3135
+ unindexed.delete(locator);
3136
+ queueReadiness();
3137
+ }
3138
+ function installed(locator) {
3139
+ if (installs.delete(locator))
3140
+ queueReadiness();
2789
3141
  }
2790
- async function pullRefresh(authorized, held, liveness) {
3142
+ function screen(value) {
3143
+ if (value === onScreen)
3144
+ return;
3145
+ onScreen = value;
3146
+ journal.record(`installation ${value ? 'shown' : 'hidden'}`);
3147
+ queueReadiness();
3148
+ if (!value)
3149
+ return;
3150
+ for (const resolve of shownWaiters.splice(0))
3151
+ resolve();
3152
+ }
3153
+ async function pullRefresh(authorized, held, liveness, hold) {
2791
3154
  if (!liveness())
2792
3155
  return unconfirmed;
2793
3156
  const turn = generation;
@@ -2796,7 +3159,18 @@ export function composeRuntime(options) {
2796
3159
  const local = await openLocal();
2797
3160
  try {
2798
3161
  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));
3162
+ const result = await advancement({
3163
+ ...reads,
3164
+ async refresh(authorization, request) {
3165
+ hold.release();
3166
+ try {
3167
+ return await reads.refresh(authorization, request);
3168
+ }
3169
+ finally {
3170
+ hold.take();
3171
+ }
3172
+ },
3173
+ }, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, composed(record), liveness));
2800
3174
  if (result.kind !== 'advanced') {
2801
3175
  journal.record(`transfer refresh ${record.locator} ${result.kind}`);
2802
3176
  return result;
@@ -2813,6 +3187,8 @@ export function composeRuntime(options) {
2813
3187
  return { kind: 'advanced', record: result.record, base };
2814
3188
  }
2815
3189
  catch (error) {
3190
+ if (refusedForSpace(error))
3191
+ return unconfirmed;
2816
3192
  journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
2817
3193
  throw error;
2818
3194
  }
@@ -2837,62 +3213,142 @@ export function composeRuntime(options) {
2837
3213
  return made;
2838
3214
  }
2839
3215
  function staging(local, liveness) {
3216
+ const derived = async (locator, key, composition) => {
3217
+ underived.delete(locator);
3218
+ for (;;) {
3219
+ await screened();
3220
+ if (!liveness())
3221
+ return false;
3222
+ const done = await finishingWrite(async () => liveness()
3223
+ ? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
3224
+ : null);
3225
+ if (done === null)
3226
+ return false;
3227
+ built(locator, done);
3228
+ if (done.done)
3229
+ return true;
3230
+ }
3231
+ };
2840
3232
  return {
2841
3233
  held: (locator) => transfers.get(locator) ?? null,
3234
+ async turn(locator, step) {
3235
+ installing(locator);
3236
+ for (;;) {
3237
+ await screened();
3238
+ const release = step === 'downloading' ? await heldStream() : () => { };
3239
+ if (onScreen || closed)
3240
+ return release;
3241
+ release();
3242
+ }
3243
+ },
3244
+ unqueued,
3245
+ queued(bytes) {
3246
+ queuedBytes += bytes;
3247
+ return once(() => {
3248
+ queuedBytes -= bytes;
3249
+ });
3250
+ },
3251
+ arriving(locator) {
3252
+ let ended = false;
3253
+ return {
3254
+ at(bytes) {
3255
+ if (ended)
3256
+ return;
3257
+ arrivals.set(locator, { parts: arrivals.get(locator)?.parts ?? 0, bytes });
3258
+ streaming.add(locator);
3259
+ const now = performance.now();
3260
+ if (now - arrivalReported < ARRIVAL_REPORT_MS)
3261
+ return;
3262
+ arrivalReported = now;
3263
+ queueReadiness();
3264
+ },
3265
+ part() {
3266
+ const arrived = arrivals.get(locator);
3267
+ arrivals.set(locator, {
3268
+ parts: (arrived?.parts ?? 0) + 1,
3269
+ bytes: arrived?.bytes ?? 0,
3270
+ });
3271
+ queueReadiness();
3272
+ },
3273
+ end() {
3274
+ if (ended)
3275
+ return;
3276
+ ended = true;
3277
+ if (streaming.delete(locator))
3278
+ queueReadiness();
3279
+ },
3280
+ };
3281
+ },
3282
+ announced(locator) {
3283
+ const later = announced.get(locator);
3284
+ return later === undefined ? [] : [later];
3285
+ },
2842
3286
  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)
3287
+ const { locator } = record;
3288
+ installing(locator);
3289
+ await screened();
3290
+ if (!liveness())
3291
+ return null;
3292
+ const key = await installWrite(async () => {
3293
+ if (!liveness())
2864
3294
  return null;
2865
- key = staged;
2866
- }
2867
- transfers.set(record.locator, { ...record, key });
3295
+ return local.stage(record, composition, page);
3296
+ });
3297
+ if (key === null || !liveness())
3298
+ return null;
3299
+ const arrived = !transfers.has(locator);
3300
+ const prior = transfers.get(locator)?.done.bytes ?? 0;
3301
+ const held = { ...record, key };
3302
+ transfers.set(locator, held);
3303
+ if (arrived)
3304
+ liveQueries.recover();
3305
+ installing(locator);
2868
3306
  liveQueries.recover();
2869
- return key;
3307
+ const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
3308
+ owedIndexes(locator, record.done.bytes - prior, text);
3309
+ const owed = text + (underived.get(locator) ?? 0);
3310
+ underived.set(locator, owed);
3311
+ if (owed < deriveBudget.bytes)
3312
+ return held;
3313
+ return (await derived(locator, key, composition)) ? held : null;
2870
3314
  },
2871
- async refreshing(key) {
3315
+ derive: derived,
3316
+ async refreshing(locator, key, composition) {
2872
3317
  await exclusive(() => local.unrefresh(key));
2873
- return refreshingOf(local, key, liveness);
3318
+ return refreshingOf(local, key, composition, liveness, locator);
2874
3319
  },
2875
3320
  abandon: (locator) => exclusive(async () => {
2876
3321
  if (!liveness())
2877
3322
  return;
2878
3323
  await local.abandon(locator);
2879
- transfers.delete(locator);
3324
+ untransferred(locator);
2880
3325
  liveQueries.recover();
2881
3326
  sweep();
2882
3327
  }),
2883
- setAside: (locator) => exclusive(async () => {
3328
+ setAside: async (locator) => {
2884
3329
  if (!liveness())
2885
3330
  return;
2886
- await local.abandon(locator, true);
2887
- transfers.delete(locator);
3331
+ untransferred(locator);
2888
3332
  liveQueries.recover();
2889
- }),
3333
+ },
2890
3334
  release: (key) => exclusive(async () => {
2891
3335
  await local.release(key);
2892
3336
  sweep();
2893
3337
  }),
2894
3338
  content: (key) => heldContentOf(local, key),
2895
- validate: (record, view) => validatedRoom(local, record, view, null),
3339
+ async validate(record, view) {
3340
+ const { locator } = record;
3341
+ const refused = await validatedRoom(local, record, view, null, null, (text) => {
3342
+ const check = unchecked.get(locator);
3343
+ if (check === undefined)
3344
+ return;
3345
+ unchecked.set(locator, { ...check, done: check.done + text });
3346
+ queueReadiness();
3347
+ });
3348
+ unchecked.delete(locator);
3349
+ queueReadiness();
3350
+ return refused;
3351
+ },
2896
3352
  };
2897
3353
  }
2898
3354
  function reusable(room, local) {
@@ -2910,19 +3366,21 @@ export function composeRuntime(options) {
2910
3366
  liveQueries.recover();
2911
3367
  }
2912
3368
  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));
3369
+ 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
3370
  const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
2918
3371
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
2919
- if (result.kind !== 'staged')
3372
+ if (result.kind !== 'staged') {
2920
3373
  caughtUpTo(room.locator);
3374
+ installed(room.locator);
3375
+ }
2921
3376
  return result;
2922
3377
  }
2923
3378
  catch (error) {
2924
- journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
2925
3379
  caughtUpTo(room.locator);
3380
+ installed(room.locator);
3381
+ if (refusedForSpace(error))
3382
+ return unconfirmed;
3383
+ journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
2926
3384
  throw error;
2927
3385
  }
2928
3386
  finally {
@@ -2946,37 +3404,16 @@ export function composeRuntime(options) {
2946
3404
  }
2947
3405
  return advanceOnce(room, local, authorized, live);
2948
3406
  }
2949
- async function advanceOnce(room, local, authorized, live) {
2950
- if (!live() || paused.has(room.locator))
3407
+ async function advanceOnce(room, local, authorized, running) {
3408
+ const live = () => running() && !full;
3409
+ if (!live())
2951
3410
  return unconfirmed;
2952
3411
  const held = replicas.get(room.locator);
2953
3412
  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')
3413
+ const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold));
3414
+ if (refreshed === 'behind')
3415
+ return advance(room, local, authorized, live);
3416
+ if (refreshed !== 'beyond-horizon')
2980
3417
  return refreshed;
2981
3418
  if (opened !== null) {
2982
3419
  owed.add(room.locator);
@@ -2987,12 +3424,39 @@ export function composeRuntime(options) {
2987
3424
  return unconfirmed;
2988
3425
  }
2989
3426
  const bootstrapped = await pullBootstrap(authorized, room, local, live);
3427
+ if (bootstrapped.kind === 'staged') {
3428
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3429
+ return live() ? null : unconfirmed;
3430
+ }
3431
+ return live() ? bootstrapped : unconfirmed;
3432
+ }
3433
+ async function refreshedRoom(room, local, authorized, held, live, hold) {
3434
+ const refreshed = await pullRefresh(authorized, held, live, hold);
2990
3435
  if (!live())
2991
3436
  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;
3437
+ if (refreshed.kind === 'still-current') {
3438
+ stale.delete(room.locator);
3439
+ if (sourceRefusals.has(room.locator))
3440
+ return null;
3441
+ if (unreadable.delete(room.locator)) {
3442
+ scopedRefusals.delete(room.locator);
3443
+ list(room, true);
3444
+ emitRooms();
3445
+ }
3446
+ return null;
3447
+ }
3448
+ if (refreshed.kind === 'advanced') {
3449
+ const landing = { kind: 'refreshed', base: refreshed.base };
3450
+ if (!(await publish(local, room, refreshed.record, landing, live))) {
3451
+ const now = replicas.get(room.locator)?.record.roomVersion;
3452
+ const reached = now !== undefined &&
3453
+ options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3454
+ return reached ? null : 'behind';
3455
+ }
3456
+ await retainModel(local, room, modelOf(room.locator));
3457
+ return null;
3458
+ }
3459
+ return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
2996
3460
  }
2997
3461
  async function withhold(withheld, room, local, progress) {
2998
3462
  if (withheld.kind === 'unconfirmed')
@@ -3515,6 +3979,7 @@ export function composeRuntime(options) {
3515
3979
  locator: room.locator,
3516
3980
  key: before.record.key,
3517
3981
  roots: before.record.typedSelection?.roots ?? [],
3982
+ composed: composedOf(composed(before.record)),
3518
3983
  overlay: { set: [], cleared: [...before.overlaid] },
3519
3984
  },
3520
3985
  ]),
@@ -3570,16 +4035,17 @@ export function composeRuntime(options) {
3570
4035
  release();
3571
4036
  };
3572
4037
  }
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))))
4038
+ function declaredAsHeld(room) {
4039
+ const held = replicas.get(room.locator)?.record;
4040
+ return (held === undefined ||
4041
+ (JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
4042
+ JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
4043
+ JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
4044
+ }
4045
+ async function retainModel(local, room, next) {
4046
+ const key = room.locator;
4047
+ const previous = modelOf(room.locator);
4048
+ if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(room))
3583
4049
  return;
3584
4050
  if (next.status === 'unavailable') {
3585
4051
  invalidModels.add(key);
@@ -3596,11 +4062,11 @@ export function composeRuntime(options) {
3596
4062
  operations: room.operations,
3597
4063
  replica: room.replica,
3598
4064
  typedSelection: room.typedSelection ?? null,
3599
- ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
4065
+ model: next,
3600
4066
  };
3601
4067
  let read = 'staged';
3602
4068
  if (!sameMembership(declared, held.record)) {
3603
- const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), compositionOf(declared));
4069
+ const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), composed(declared));
3604
4070
  read =
3605
4071
  verdict.verdict !== 'refuse'
3606
4072
  ? 'staged'
@@ -3621,14 +4087,13 @@ export function composeRuntime(options) {
3621
4087
  key: record.key,
3622
4088
  record: storedRecord(record),
3623
4089
  roots: record.typedSelection?.roots ?? [],
3624
- composition: compositionOf(record),
4090
+ composed: composedOf(composed(record)),
3625
4091
  overlay: diff.overlay,
3626
4092
  },
3627
4093
  ],
3628
4094
  });
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 });
4095
+ const { count, indexed } = committedRoom(room.locator, answer);
4096
+ replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
3632
4097
  changed = changeSet(room.locator, {
3633
4098
  documents: answer.changed[room.locator] ?? [],
3634
4099
  room: true,
@@ -3644,18 +4109,21 @@ export function composeRuntime(options) {
3644
4109
  declarationRefusals.add(room.locator);
3645
4110
  }
3646
4111
  }
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));
4112
+ heldModel(room, next);
3653
4113
  if (changed === null)
3654
4114
  emitRooms();
3655
4115
  else
3656
4116
  publishChangeSet(changed);
3657
4117
  });
3658
4118
  }
4119
+ function heldModel(room, next) {
4120
+ models.set(room.locator, next);
4121
+ if (next.status === 'current') {
4122
+ liveModels.set(room.locator, JSON.stringify(next.snapshot));
4123
+ invalidModels.delete(room.locator);
4124
+ }
4125
+ list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
4126
+ }
3659
4127
  async function scoped(result, room, local) {
3660
4128
  if (result.scope === 'content' && !result.retryable) {
3661
4129
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
@@ -3669,14 +4137,14 @@ export function composeRuntime(options) {
3669
4137
  });
3670
4138
  let scopedAway = room;
3671
4139
  if (result.scope === 'model')
3672
- await retainModel(local, room, 'collection', {
4140
+ await retainModel(local, room, {
3673
4141
  status: 'unavailable',
3674
4142
  reason: 'not-readable',
3675
4143
  });
3676
4144
  else if (result.scope !== 'operation') {
3677
4145
  unreadable.add(room.locator);
3678
4146
  scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
3679
- await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
4147
+ await retainModel(local, scopedAway, modelOf(room.locator));
3680
4148
  }
3681
4149
  if (!continuing())
3682
4150
  return;
@@ -3726,7 +4194,7 @@ export function composeRuntime(options) {
3726
4194
  return false;
3727
4195
  }
3728
4196
  if (prior !== undefined)
3729
- await retainModel(local, room, 'collection', modelOf(room.locator));
4197
+ await retainModel(local, room, modelOf(room.locator));
3730
4198
  if (discoveryPaused || authorization === undefined)
3731
4199
  return false;
3732
4200
  return room.replica.admission === 'admitted';
@@ -3739,10 +4207,8 @@ export function composeRuntime(options) {
3739
4207
  await withhold(withheld, room, local, progress);
3740
4208
  }
3741
4209
  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
- });
4210
+ const model = modelOf(locator);
4211
+ return model.status === 'unavailable' && model.reason === 'revoked';
3746
4212
  }
3747
4213
  async function loadRoom(room, local, progress, live) {
3748
4214
  if (!live() || authorization === undefined)
@@ -3765,10 +4231,9 @@ export function composeRuntime(options) {
3765
4231
  if (!live() || authorization === undefined)
3766
4232
  return;
3767
4233
  const bootstrapped = await pullBootstrap(authorization, room, local, live);
3768
- if (!live())
3769
- return;
3770
4234
  if (bootstrapped.kind !== 'staged') {
3771
- await withhold(bootstrapped, room, local, progress);
4235
+ if (live())
4236
+ await withhold(bootstrapped, room, local, progress);
3772
4237
  return;
3773
4238
  }
3774
4239
  await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
@@ -3793,6 +4258,8 @@ export function composeRuntime(options) {
3793
4258
  catch {
3794
4259
  result = { kind: 'transport-uncertainty' };
3795
4260
  }
4261
+ if (result.kind === 'discovery')
4262
+ roomsNamed = true;
3796
4263
  if (!live()) {
3797
4264
  progress.complete = false;
3798
4265
  notifyReady();
@@ -3800,12 +4267,7 @@ export function composeRuntime(options) {
3800
4267
  }
3801
4268
  journal.record(`connection discover ${result.kind}`);
3802
4269
  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
- }
4270
+ await settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
3809
4271
  progress.reachable = false;
3810
4272
  progress.complete = false;
3811
4273
  notifyReady();
@@ -3860,6 +4322,7 @@ export function composeRuntime(options) {
3860
4322
  limits = result.providerLimits;
3861
4323
  await local.fact('providerLimits', limits);
3862
4324
  await local.fact('capabilities', capabilities);
4325
+ conversations.declared();
3863
4326
  }
3864
4327
  const signedIn = result.member ?? null;
3865
4328
  if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
@@ -3917,7 +4380,7 @@ export function composeRuntime(options) {
3917
4380
  function sync(attempt) {
3918
4381
  return task(OPENING_LANE, async (live) => {
3919
4382
  if (!live()) {
3920
- notifyReady();
4383
+ readyFromStore();
3921
4384
  return;
3922
4385
  }
3923
4386
  const progress = { complete: true, reachable: true };
@@ -3995,6 +4458,7 @@ export function composeRuntime(options) {
3995
4458
  });
3996
4459
  }
3997
4460
  function closedSocket(revoked, lost) {
4461
+ notifyDiscovered();
3998
4462
  const opened = socketOpen;
3999
4463
  socketOpen = false;
4000
4464
  connectionLost();
@@ -4038,13 +4502,9 @@ export function composeRuntime(options) {
4038
4502
  const local = await openLocal();
4039
4503
  const reloaded = await replacing;
4040
4504
  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
- }
4505
+ await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
4506
+ !expired.includes(room.locator) &&
4507
+ !reloading.includes(room.locator)), 'current', local);
4048
4508
  }
4049
4509
  const failed = reloaded.find((outcome) => outcome.status === 'rejected');
4050
4510
  if (failed !== undefined)
@@ -4085,12 +4545,30 @@ export function composeRuntime(options) {
4085
4545
  return [];
4086
4546
  }
4087
4547
  }
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' });
4548
+ async function settleModels(held, status, local) {
4549
+ const owed = (room) => {
4550
+ const model = modelOf(room.locator);
4551
+ return model.status !== 'unavailable' && model.status !== status;
4552
+ };
4553
+ const settled = await exclusive(async () => {
4554
+ if (!continuing())
4555
+ return [];
4556
+ const unchanged = held.filter((room) => owed(room) && !declarationRefusals.has(room.locator) && declaredAsHeld(room));
4557
+ for (const room of unchanged) {
4558
+ const next = { ...modelOf(room.locator), status };
4559
+ const replica = replicas.get(room.locator);
4560
+ if (replica !== undefined)
4561
+ replicas.set(room.locator, { ...replica, record: { ...replica.record, model: next } });
4562
+ heldModel(room, next);
4093
4563
  }
4564
+ if (unchanged.length > 0)
4565
+ emitRooms();
4566
+ return unchanged;
4567
+ });
4568
+ for (const room of held) {
4569
+ if (settled.includes(room) || !owed(room))
4570
+ continue;
4571
+ await retainModel(local, room, { ...modelOf(room.locator), status });
4094
4572
  }
4095
4573
  }
4096
4574
  function submitLater(locator) {
@@ -4203,8 +4681,6 @@ export function composeRuntime(options) {
4203
4681
  if (repairable.has(locator))
4204
4682
  repairLater(locator, {});
4205
4683
  await conversations.reconcile();
4206
- if (status.providerUnreachable)
4207
- conversations.reconnected();
4208
4684
  const reading = { complete: true, reachable: true };
4209
4685
  await conversations.catchUp(reading);
4210
4686
  if (progress.reachable && reading.reachable)
@@ -4312,6 +4788,7 @@ export function composeRuntime(options) {
4312
4788
  parts: async (key, count) => (await openLocal()).receivedParts(key, count),
4313
4789
  keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
4314
4790
  drop: async (keys) => (await openLocal()).dropReceived(keys),
4791
+ ahead,
4315
4792
  };
4316
4793
  function observeStream() {
4317
4794
  const stream = options.adapter.roomStream;
@@ -4324,6 +4801,8 @@ export function composeRuntime(options) {
4324
4801
  socketOpen = true;
4325
4802
  connectionLive();
4326
4803
  generation += 1;
4804
+ for (const admit of installWaiters.splice(0))
4805
+ admit();
4327
4806
  reads.opened();
4328
4807
  conversations.reopened();
4329
4808
  recovery.resume();
@@ -4343,7 +4822,7 @@ export function composeRuntime(options) {
4343
4822
  const held = replicas.get(locator);
4344
4823
  if (held === undefined || paths.length === 0)
4345
4824
  return new Map();
4346
- const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
4825
+ const entries = await ahead(async () => (await openLocal()).entries(held.record.key, 'confirmed', paths));
4347
4826
  return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
4348
4827
  }
4349
4828
  function writesHeld() {
@@ -4414,8 +4893,11 @@ export function composeRuntime(options) {
4414
4893
  if (closed)
4415
4894
  return;
4416
4895
  closed = true;
4417
- if (rechecking !== null)
4418
- clearInterval(rechecking);
4896
+ notifyDiscovered();
4897
+ if (probing !== null)
4898
+ clearInterval(probing);
4899
+ for (const resolve of shownWaiters.splice(0))
4900
+ resolve();
4419
4901
  connectionLive();
4420
4902
  lockListeners.clear();
4421
4903
  readinessWatches.clear();
@@ -4427,6 +4909,7 @@ export function composeRuntime(options) {
4427
4909
  conversations.close();
4428
4910
  void recovery.retire();
4429
4911
  roomListeners.clear();
4912
+ unlistedRooms.clear();
4430
4913
  refusalListeners.clear();
4431
4914
  storageLossListeners.clear();
4432
4915
  statusListeners.clear();
@@ -4452,9 +4935,9 @@ export function composeRuntime(options) {
4452
4935
  observeStream();
4453
4936
  void openLocal().then(() => {
4454
4937
  if (capabilities.length > 0)
4455
- notifyReady();
4938
+ readyFromStore();
4456
4939
  }, () => {
4457
- notifyReady();
4940
+ readyFromStore();
4458
4941
  });
4459
4942
  if (options.adapter.roomStream === undefined)
4460
4943
  void sync('unstreamed');
@@ -4602,12 +5085,13 @@ export function composeRuntime(options) {
4602
5085
  };
4603
5086
  }
4604
5087
  if (topic === 'readiness') {
4605
- return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
5088
+ return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
4606
5089
  }
4607
5090
  if (topic === 'rooms') {
4608
- roomListeners.add(deliver);
4609
- deliver(listed());
5091
+ unlistedRooms.add(deliver);
5092
+ emitKnownRooms();
4610
5093
  return () => {
5094
+ unlistedRooms.delete(deliver);
4611
5095
  roomListeners.delete(deliver);
4612
5096
  };
4613
5097
  }
@@ -4661,7 +5145,17 @@ export function composeRuntime(options) {
4661
5145
  return openLocal().then(() => undefined);
4662
5146
  },
4663
5147
  rooms() {
4664
- return listed();
5148
+ if (closed)
5149
+ return Promise.resolve([]);
5150
+ return new Promise((settle) => {
5151
+ const answer = (value) => {
5152
+ unlistedRooms.delete(answer);
5153
+ roomListeners.delete(answer);
5154
+ settle(value);
5155
+ };
5156
+ unlistedRooms.add(answer);
5157
+ emitKnownRooms();
5158
+ });
4665
5159
  },
4666
5160
  query: ((input) => rowsQuery(planned(input, null))),
4667
5161
  search: (input) => searched(input, null, []),
@@ -4671,7 +5165,7 @@ export function composeRuntime(options) {
4671
5165
  typedSelection: rooms.get(locator)?.typedSelection === undefined ||
4672
5166
  rooms.get(locator)?.typedSelection === null
4673
5167
  ? null
4674
- : modelTypes(modelOf(locator, 'typed-selection')),
5168
+ : modelTypes(builtIn),
4675
5169
  };
4676
5170
  },
4677
5171
  roomDraft: {
@@ -4688,33 +5182,17 @@ export function composeRuntime(options) {
4688
5182
  const record = roomDrafts.get(room)?.get(path);
4689
5183
  return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
4690
5184
  },
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
- });
5185
+ write(room, path, revision, input, next = roomDraftRevision()) {
5186
+ if (typeof next !== 'string' || next === '')
5187
+ return Promise.reject(runtimeError('room-draft-malformed'));
5188
+ return gathered(`room-draft\u0000${room}\u0000${path}`, { revision, input, next }, (earlier, later) => {
5189
+ if (later.revision !== earlier.next)
5190
+ return null;
5191
+ const parts = gatheredParts(earlier.input, later.input);
5192
+ return parts === null
5193
+ ? null
5194
+ : { revision: earlier.revision, input: parts, next: later.next };
5195
+ }, (written) => writtenRoomDraft(room, path, written.revision, written.input, written.next));
4718
5196
  },
4719
5197
  discard(room, path, revision) {
4720
5198
  return exclusive(async () => {
@@ -4741,13 +5219,13 @@ export function composeRuntime(options) {
4741
5219
  });
4742
5220
  },
4743
5221
  write(app, value) {
4744
- return keepPresentation(app, () => structuredClone(value));
5222
+ return exclusive(() => keptPresentation(app, () => structuredClone(value)));
4745
5223
  },
4746
5224
  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
- ]));
5225
+ return gathered(`presentation\u0000${app}`, structuredClone(members), (earlier, later) => ({ ...earlier, ...later }), (edited) => keptPresentation(app, (held) => Object.fromEntries([
5226
+ ...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(edited, member)),
5227
+ ...Object.entries(edited).filter(([, value]) => value !== null),
5228
+ ])));
4751
5229
  },
4752
5230
  },
4753
5231
  save(room, named, writtenBy) {
@@ -4766,7 +5244,8 @@ export function composeRuntime(options) {
4766
5244
  part: readPart,
4767
5245
  read: readBinaryPath,
4768
5246
  link: linkBinaryPath,
4769
- readReference: readBinaryReference,
5247
+ show: showBinaryPath,
5248
+ attach: attachBinary,
4770
5249
  },
4771
5250
  async call(name, input) {
4772
5251
  await ready;
@@ -4789,7 +5268,8 @@ export function composeRuntime(options) {
4789
5268
  journal.record('retry');
4790
5269
  unpreparable.clear();
4791
5270
  queueReadiness();
4792
- resume();
5271
+ if (full)
5272
+ await probed();
4793
5273
  },
4794
5274
  observe: observing(watch),
4795
5275
  async reauthorize() {
@@ -4811,7 +5291,6 @@ export function composeRuntime(options) {
4811
5291
  const reachable = {
4812
5292
  md,
4813
5293
  watch,
4814
- referrerScope: (referrer) => options.adapter.referrerScope(referrer),
4815
5294
  plan: planned,
4816
5295
  query: (plan) => rowsQuery(plan),
4817
5296
  watchPlan,
@@ -4821,6 +5300,7 @@ export function composeRuntime(options) {
4821
5300
  lockedRooms,
4822
5301
  watchLocks,
4823
5302
  located,
5303
+ room: (locator) => listed().find((one) => one.locator === locator),
4824
5304
  };
4825
5305
  return {
4826
5306
  md,
@@ -4835,6 +5315,11 @@ export function composeRuntime(options) {
4835
5315
  journal.record(`persistence ${answer}`);
4836
5316
  queueReadiness();
4837
5317
  },
4838
- within: (reach, port) => narrowed(reachable, reach, port),
5318
+ shown: screen,
5319
+ within: (reach, port) => {
5320
+ if (full)
5321
+ void probed();
5322
+ return narrowed(reachable, reach, port);
5323
+ },
4839
5324
  };
4840
5325
  }