@agentshouse/mdruntime 0.1.0-c1ffd2bc60355 → 0.1.0-c219104d36ac5

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 (178) hide show
  1. package/README.md +10 -8
  2. package/dist/adapter.d.ts +25 -7
  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/bootstrap.d.ts +8 -8
  8. package/dist/bootstrap.d.ts.map +1 -1
  9. package/dist/bootstrap.js +90 -84
  10. package/dist/chain.d.ts.map +1 -1
  11. package/dist/chain.js +7 -3
  12. package/dist/client.d.ts +4 -4
  13. package/dist/client.d.ts.map +1 -1
  14. package/dist/client.js +52 -5
  15. package/dist/collection.d.ts +5 -0
  16. package/dist/collection.d.ts.map +1 -1
  17. package/dist/collection.js +23 -2
  18. package/dist/compile.d.ts.map +1 -1
  19. package/dist/compile.js +74 -24
  20. package/dist/connection.d.ts +5 -2
  21. package/dist/connection.d.ts.map +1 -1
  22. package/dist/connection.js +654 -359
  23. package/dist/conversation.d.ts +5 -5
  24. package/dist/conversation.d.ts.map +1 -1
  25. package/dist/conversation.js +126 -33
  26. package/dist/entry.d.ts +1 -0
  27. package/dist/entry.d.ts.map +1 -1
  28. package/dist/entry.js +1 -1
  29. package/dist/file.d.ts +2 -11
  30. package/dist/file.d.ts.map +1 -1
  31. package/dist/file.js +6 -4
  32. package/dist/fulltext.d.ts.map +1 -1
  33. package/dist/fulltext.js +8 -6
  34. package/dist/host.d.ts.map +1 -1
  35. package/dist/host.js +4 -2
  36. package/dist/index.d.ts +3 -4
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -1
  39. package/dist/live.d.ts.map +1 -1
  40. package/dist/live.js +2 -1
  41. package/dist/media.d.ts +8 -0
  42. package/dist/media.d.ts.map +1 -0
  43. package/dist/media.js +208 -0
  44. package/dist/memory.d.ts +2 -2
  45. package/dist/memory.d.ts.map +1 -1
  46. package/dist/memory.js +47 -9
  47. package/dist/model.d.ts +6 -0
  48. package/dist/model.d.ts.map +1 -1
  49. package/dist/model.js +7 -1
  50. package/dist/observation.d.ts +1 -0
  51. package/dist/observation.d.ts.map +1 -1
  52. package/dist/observation.js +1 -1
  53. package/dist/outcome.d.ts +13 -2
  54. package/dist/outcome.d.ts.map +1 -1
  55. package/dist/outcome.js +16 -2
  56. package/dist/paths.d.ts +1 -0
  57. package/dist/paths.d.ts.map +1 -1
  58. package/dist/paths.js +4 -0
  59. package/dist/query.d.ts +6 -0
  60. package/dist/query.d.ts.map +1 -1
  61. package/dist/reach.d.ts +8 -1
  62. package/dist/reach.d.ts.map +1 -1
  63. package/dist/reach.js +51 -17
  64. package/dist/readiness.d.ts +1 -2
  65. package/dist/readiness.d.ts.map +1 -1
  66. package/dist/reads.d.ts.map +1 -1
  67. package/dist/reads.js +10 -19
  68. package/dist/refresh.d.ts.map +1 -1
  69. package/dist/refresh.js +1 -5
  70. package/dist/replica.d.ts +3 -4
  71. package/dist/replica.d.ts.map +1 -1
  72. package/dist/replica.js +4 -3
  73. package/dist/room-draft.d.ts +17 -2
  74. package/dist/room-draft.d.ts.map +1 -1
  75. package/dist/room-draft.js +40 -6
  76. package/dist/rows.d.ts +15 -6
  77. package/dist/rows.d.ts.map +1 -1
  78. package/dist/rows.js +102 -74
  79. package/dist/save.d.ts +2 -0
  80. package/dist/save.d.ts.map +1 -1
  81. package/dist/save.js +11 -5
  82. package/dist/status.d.ts +5 -0
  83. package/dist/status.d.ts.map +1 -1
  84. package/dist/status.js +3 -1
  85. package/dist/store.d.ts +16 -19
  86. package/dist/store.d.ts.map +1 -1
  87. package/dist/store.js +23 -29
  88. package/dist/stored.d.ts +9 -19
  89. package/dist/stored.d.ts.map +1 -1
  90. package/dist/stored.js +197 -196
  91. package/dist/tables.d.ts +14 -16
  92. package/dist/tables.d.ts.map +1 -1
  93. package/dist/tables.js +6 -9
  94. package/dist/wire.d.ts +8 -4
  95. package/dist/wire.d.ts.map +1 -1
  96. package/dist/wire.js +2 -1
  97. package/dist/words.d.ts.map +1 -1
  98. package/dist/words.js +8 -2
  99. package/package.json +10 -17
  100. package/AGENTS.md +0 -317
  101. package/dist/chat/acts.d.ts +0 -33
  102. package/dist/chat/acts.d.ts.map +0 -1
  103. package/dist/chat/acts.js +0 -36
  104. package/dist/chat/context.d.ts +0 -3
  105. package/dist/chat/context.d.ts.map +0 -1
  106. package/dist/chat/context.js +0 -10
  107. package/dist/chat/earlier.d.ts +0 -10
  108. package/dist/chat/earlier.d.ts.map +0 -1
  109. package/dist/chat/earlier.js +0 -54
  110. package/dist/chat/house.d.ts +0 -25
  111. package/dist/chat/house.d.ts.map +0 -1
  112. package/dist/chat/house.js +0 -38
  113. package/dist/chat/input.d.ts +0 -11
  114. package/dist/chat/input.d.ts.map +0 -1
  115. package/dist/chat/input.js +0 -92
  116. package/dist/chat/kit.d.ts +0 -61
  117. package/dist/chat/kit.d.ts.map +0 -1
  118. package/dist/chat/kit.js +0 -123
  119. package/dist/chat/lasting.d.ts +0 -6
  120. package/dist/chat/lasting.d.ts.map +0 -1
  121. package/dist/chat/lasting.js +0 -19
  122. package/dist/chat/list.d.ts +0 -28
  123. package/dist/chat/list.d.ts.map +0 -1
  124. package/dist/chat/list.js +0 -103
  125. package/dist/chat/markdown.d.ts +0 -20
  126. package/dist/chat/markdown.d.ts.map +0 -1
  127. package/dist/chat/markdown.js +0 -229
  128. package/dist/chat/message.d.ts +0 -7
  129. package/dist/chat/message.d.ts.map +0 -1
  130. package/dist/chat/message.js +0 -254
  131. package/dist/chat/messages.d.ts +0 -35
  132. package/dist/chat/messages.d.ts.map +0 -1
  133. package/dist/chat/messages.js +0 -210
  134. package/dist/chat/name.d.ts +0 -7
  135. package/dist/chat/name.d.ts.map +0 -1
  136. package/dist/chat/name.js +0 -37
  137. package/dist/chat/opened.d.ts +0 -12
  138. package/dist/chat/opened.d.ts.map +0 -1
  139. package/dist/chat/opened.js +0 -19
  140. package/dist/chat/overlay.d.ts +0 -32
  141. package/dist/chat/overlay.d.ts.map +0 -1
  142. package/dist/chat/overlay.js +0 -103
  143. package/dist/chat/parts.d.ts +0 -58
  144. package/dist/chat/parts.d.ts.map +0 -1
  145. package/dist/chat/parts.js +0 -120
  146. package/dist/chat/pieces.d.ts +0 -62
  147. package/dist/chat/pieces.d.ts.map +0 -1
  148. package/dist/chat/pieces.js +0 -1110
  149. package/dist/chat/process.d.ts +0 -20
  150. package/dist/chat/process.d.ts.map +0 -1
  151. package/dist/chat/process.js +0 -32
  152. package/dist/chat/provider.d.ts +0 -70
  153. package/dist/chat/provider.d.ts.map +0 -1
  154. package/dist/chat/provider.js +0 -635
  155. package/dist/chat/question.d.ts +0 -9
  156. package/dist/chat/question.d.ts.map +0 -1
  157. package/dist/chat/question.js +0 -69
  158. package/dist/chat/questions.d.ts +0 -31
  159. package/dist/chat/questions.d.ts.map +0 -1
  160. package/dist/chat/questions.js +0 -111
  161. package/dist/chat/read.d.ts +0 -5
  162. package/dist/chat/read.d.ts.map +0 -1
  163. package/dist/chat/read.js +0 -13
  164. package/dist/chat/ready.d.ts +0 -37
  165. package/dist/chat/ready.d.ts.map +0 -1
  166. package/dist/chat/ready.js +0 -93
  167. package/dist/chat/room-file.d.ts +0 -8
  168. package/dist/chat/room-file.d.ts.map +0 -1
  169. package/dist/chat/room-file.js +0 -72
  170. package/dist/chat/steady.d.ts +0 -3
  171. package/dist/chat/steady.d.ts.map +0 -1
  172. package/dist/chat/steady.js +0 -26
  173. package/dist/chat/style.d.ts +0 -4
  174. package/dist/chat/style.d.ts.map +0 -1
  175. package/dist/chat/style.js +0 -307
  176. package/dist/chat.d.ts +0 -15
  177. package/dist/chat.d.ts.map +0 -1
  178. 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,18 +12,19 @@ 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, 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, gatheredParts, 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
29
  import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
29
30
  import { readinessOf, } from './readiness.js';
@@ -31,7 +32,7 @@ import { FencedMap } from './fence.js';
31
32
  import { sameBinary } from './overlay.js';
32
33
  import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
33
34
  import { identityTime, roomDraftRevision } from './identity.js';
34
- import { beneath, normalize } from './paths.js';
35
+ import { ancestors, beneath, normalize } from './paths.js';
35
36
  import { openRuntimeStore, } from './store.js';
36
37
  import { changedCleanly, sameComposition, validatedCollection, } from './validation.js';
37
38
  import { unconfirmed } from './transfer.js';
@@ -54,22 +55,23 @@ const CONVERSATION_LANE = 'conversations';
54
55
  const ROOM_READS = 256;
55
56
  const ROOM_ATTEMPTS = 16;
56
57
  const ERASE_BATCH = 512;
57
- 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;
58
64
  const WALK_BYTES = 1 << 20;
59
65
  const QUERY_BYTES = 1 << 20;
60
66
  const STAGE_BYTES = 1 << 20;
61
67
  const STAGE_FLOOR = 1 << 16;
62
68
  // A batch aims at this much database work, so a Save or a query waits behind at most one short write.
63
69
  const STAGE_AIM_MS = 50;
64
- const PARTS_HELD = 4;
70
+ const STREAMS_HELD = 4;
71
+ const NOTHING_HELD = { parts: 0, bytes: 0 };
72
+ const QUEUED_BYTES = 1 << 23;
65
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.
66
74
  const ARRIVAL_REPORT_MS = 250;
67
- const INSTALL_STEPS = [
68
- 'writing',
69
- 'downloading',
70
- 'replaying',
71
- 'listing',
72
- ];
73
75
  const CLEAN_CHANGES = 2_000;
74
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.
75
77
  const LOCK_GRACE_MS = 120_000;
@@ -93,7 +95,7 @@ function pendingKey(locator, id) {
93
95
  }
94
96
  export function composeRuntime(options) {
95
97
  const localSaves = options.adapter.localSaves === true;
96
- const roomTypes = roomTypesOf(options.adapter.roomTypes);
98
+ const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
97
99
  let authorization = options.authorization;
98
100
  let opening;
99
101
  const fences = new Map();
@@ -103,20 +105,23 @@ export function composeRuntime(options) {
103
105
  const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
104
106
  let discoveries = 0;
105
107
  const replicas = new FencedMap(isFenced);
108
+ const reopened = new Set();
109
+ const usableWhenHeld = options.usableWhenHeld ?? [];
106
110
  const queuesWithoutReplica = new FencedMap(isFenced);
107
111
  const pendings = new Map();
108
112
  const seeds = new Map();
109
113
  const seedWatchers = new Map();
110
114
  const roomDrafts = new FencedMap(isFenced);
115
+ const attachedFiles = new FencedMap(isFenced);
111
116
  const presentations = new Map();
112
117
  let limits = [];
113
118
  let capabilities = [];
114
119
  const rooms = new FencedMap(isFenced);
115
120
  const counted = new Map();
116
121
  const unreadable = new Set();
117
- const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
118
- const models = new FencedMap(isModelFenced);
119
- const liveModels = new FencedMap(isModelFenced);
122
+ const builtIn = builtInModel(roomTypes);
123
+ const models = new FencedMap(isFenced);
124
+ const liveModels = new FencedMap(isFenced);
120
125
  const invalidModels = new Set();
121
126
  const discovered = new Map();
122
127
  const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
@@ -124,23 +129,19 @@ export function composeRuntime(options) {
124
129
  const scopedRefusals = new Map();
125
130
  const sourceRefusals = new Map();
126
131
  const declarationRefusals = new Set();
127
- function modelKey(locator, attached) {
128
- return `${attached} ${locator}`;
132
+ function modelOf(locator) {
133
+ return models.get(locator) ?? NO_MODEL;
129
134
  }
130
- function modelOf(locator, attached = 'collection') {
131
- return models.get(modelKey(locator, attached)) ?? NO_MODEL;
132
- }
133
- function savedModel(locator, attached) {
134
- return invalidModels.has(modelKey(locator, attached))
135
+ function savedModel(locator) {
136
+ return invalidModels.has(locator)
135
137
  ? { status: 'unavailable', reason: 'revoked' }
136
- : modelOf(locator, attached);
138
+ : modelOf(locator);
139
+ }
140
+ function composed(record) {
141
+ return compositionOf(record, builtIn);
137
142
  }
138
143
  function compositionFor(record) {
139
- return compositionOf({
140
- ...record,
141
- model: savedModel(record.locator, 'collection'),
142
- selectionModel: savedModel(record.locator, 'typed-selection'),
143
- });
144
+ return composed({ ...record, model: savedModel(record.locator) });
144
145
  }
145
146
  let refusal = null;
146
147
  let storageLoss = null;
@@ -174,6 +175,7 @@ export function composeRuntime(options) {
174
175
  let mutations = Promise.resolve();
175
176
  let gathering = null;
176
177
  const roomListeners = new Set();
178
+ const unlistedRooms = new Set();
177
179
  const refusalListeners = new Set();
178
180
  const storageLossListeners = new Set();
179
181
  const statusListeners = new Set();
@@ -217,15 +219,31 @@ export function composeRuntime(options) {
217
219
  let readinessQueued = false;
218
220
  const catching = new Set();
219
221
  const transfers = new FencedMap(isFenced);
220
- const installs = new Map();
222
+ const installs = new Set();
221
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();
222
228
  let arrivalReported = 0;
223
229
  let onScreen = options.shell.onScreen;
224
230
  const shownWaiters = [];
225
- let partsFree = PARTS_HELD;
226
- const partWaiters = [];
227
- let stageBytes = STAGE_FLOOR * 4;
228
- let installWrites = Promise.resolve();
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: [] };
229
247
  const conversations = createConversationStore({
230
248
  agents: declaredAgents,
231
249
  documents: (declaration, after) => rowsQuery(planOf({
@@ -246,6 +264,7 @@ export function composeRuntime(options) {
246
264
  stream: () => options.adapter.conversations,
247
265
  publish: publishChangeSet,
248
266
  online: () => connectionOf() !== 'unreachable' && refusal === null,
267
+ listed: () => roomsListed,
249
268
  full: () => full,
250
269
  closed: () => closed,
251
270
  unanswered: resendLater,
@@ -253,10 +272,19 @@ export function composeRuntime(options) {
253
272
  });
254
273
  const journal = createLog();
255
274
  const keptFiles = openedFiles(database, isFenced);
256
- let notifyReady = () => { };
275
+ let readyFromStore = () => { };
257
276
  const ready = new Promise((resolve) => {
258
- notifyReady = resolve;
277
+ readyFromStore = resolve;
278
+ });
279
+ let notifyDiscovered = () => { };
280
+ const firstDiscovery = new Promise((resolve) => {
281
+ notifyDiscovered = resolve;
259
282
  });
283
+ let roomsNamed = false;
284
+ function notifyReady() {
285
+ notifyDiscovered();
286
+ readyFromStore();
287
+ }
260
288
  function rememberRoomDraft(record) {
261
289
  const room = roomDrafts.get(record.locator) ?? new Map();
262
290
  room.set(record.path, record);
@@ -276,7 +304,7 @@ export function composeRuntime(options) {
276
304
  return undefined;
277
305
  const local = await openLocal();
278
306
  const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
279
- return { ...slice, collection: collectionOf(slice.documents, compositionOf(held.record)) };
307
+ return { ...slice, collection: collectionOf(slice.documents, composed(held.record)) };
280
308
  }
281
309
  function roomDraftOf(record, visible) {
282
310
  return structuredClone({
@@ -343,7 +371,9 @@ export function composeRuntime(options) {
343
371
  }
344
372
  const file = parts.file === undefined
345
373
  ? undefined
346
- : await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
374
+ : await heldFile(parts.file.blob, parts.file.base === undefined
375
+ ? binaryBase(await visibleOf(room, [path]), path)
376
+ : parts.file.base);
347
377
  const record = {
348
378
  locator: room,
349
379
  path,
@@ -378,6 +408,20 @@ export function composeRuntime(options) {
378
408
  (queuesWithoutReplica.size > 0 ||
379
409
  [...replicas.values()].some((replica) => replica.queue.length > 0)));
380
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
+ }
381
425
  function attention() {
382
426
  const attentionRequired = [];
383
427
  for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
@@ -410,7 +454,7 @@ export function composeRuntime(options) {
410
454
  if (reset === null || submission.reset < reset)
411
455
  reset = submission.reset;
412
456
  }
413
- return { attentionRequired, reset };
457
+ return { attentionRequired, unsent: unsent(), reset };
414
458
  }
415
459
  function documentsIn(locator) {
416
460
  return replicas.get(locator)?.count ?? 0;
@@ -483,7 +527,7 @@ export function composeRuntime(options) {
483
527
  const governed = governs(reach, room);
484
528
  const attached = [
485
529
  ...(governed.collection ? [modelOf(room.locator)] : []),
486
- ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
530
+ ...(governed.selection ? [builtIn] : []),
487
531
  ];
488
532
  return {
489
533
  locator: room.locator,
@@ -499,11 +543,26 @@ export function composeRuntime(options) {
499
543
  const value = listed();
500
544
  for (const listener of roomListeners)
501
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
+ }
502
560
  }
503
561
  function emitRefusal() {
504
562
  queueReadiness();
505
563
  for (const listener of refusalListeners)
506
564
  listener(refusal);
565
+ emitKnownRooms();
507
566
  }
508
567
  function emitStorageLoss() {
509
568
  queueReadiness();
@@ -524,6 +583,7 @@ export function composeRuntime(options) {
524
583
  if (closed)
525
584
  return;
526
585
  queueReadiness();
586
+ emitKnownRooms();
527
587
  const next = status.state === 'synced' && !conversations.current()
528
588
  ? { ...status, state: 'syncing' }
529
589
  : status;
@@ -567,11 +627,14 @@ export function composeRuntime(options) {
567
627
  if (held === undefined || closed || held.indexed)
568
628
  return;
569
629
  const key = held.record.key;
570
- const done = await exclusive(async () => (await openLocal()).rederive(key, REDERIVE_BATCH));
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
+ });
571
634
  const now = replicas.get(locator);
572
635
  if (now?.record.key !== key)
573
636
  continue;
574
- if (!done)
637
+ if (!done.done)
575
638
  continue;
576
639
  replicas.set(locator, { ...now, indexed: true });
577
640
  publishChangeSet(changeSet(locator, { documents: null }));
@@ -610,7 +673,11 @@ export function composeRuntime(options) {
610
673
  })();
611
674
  }
612
675
  function roomsOfReach(reach) {
613
- 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
+ ]);
614
681
  }
615
682
  function reachedRooms(plan) {
616
683
  const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
@@ -728,9 +795,9 @@ export function composeRuntime(options) {
728
795
  const rooms = { done: 0, total: 0 };
729
796
  const parts = { done: 0, total: 0 };
730
797
  const bytes = { done: 0, total: 0 };
731
- const received = { done: 0, total: 0 };
732
- const steps = new Set();
733
- let waiting = false;
798
+ let installing = 0;
799
+ let downloading = false;
800
+ let checking = 0;
734
801
  for (const { room, state } of coverage) {
735
802
  if (state === 'unadmitted')
736
803
  continue;
@@ -738,35 +805,43 @@ export function composeRuntime(options) {
738
805
  if (state === 'complete')
739
806
  rooms.done += 1;
740
807
  const transfer = transfers.get(room);
741
- const held = installs.get(room);
742
- const inventory = transfer?.inventory ?? replicas.get(room)?.record.inventory ?? null;
743
- if (inventory !== null) {
744
- const done = transfer?.done ?? inventory;
745
- const share = held?.share ?? null;
746
- parts.total += inventory.parts;
747
- bytes.total += inventory.bytes;
748
- parts.done += done.parts;
749
- bytes.done += done.bytes;
750
- if (share !== null && share.entries > 0) {
751
- bytes.done += Math.floor((share.bytes * share.written) / share.entries);
752
- }
753
- received.total += inventory.bytes;
754
- received.done += done.bytes + (share?.bytes ?? 0);
755
- for (const arrived of arrivals.get(room)?.values() ?? [])
756
- received.done += arrived;
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;
757
824
  }
758
- const installing = held !== undefined && state !== 'complete';
759
- if (installing)
760
- steps.add(held.step);
761
- waiting ||= installing || state === 'acquiring';
762
- }
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';
763
839
  return {
764
- step: INSTALL_STEPS.find((step) => steps.has(step)) ?? null,
765
- paused: waiting && !onScreen ? 'hidden' : null,
840
+ step,
841
+ paused: installing > 0 && !onScreen ? 'hidden' : null,
766
842
  rooms,
767
843
  parts,
768
844
  bytes,
769
- received,
770
845
  };
771
846
  }
772
847
  function readinessFor(port, reach) {
@@ -793,7 +868,8 @@ export function composeRuntime(options) {
793
868
  installation: installationOf(coverage),
794
869
  refused: refusedIn(required.rooms),
795
870
  lost: storageLoss !== null,
796
- 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))),
797
873
  });
798
874
  }
799
875
  function establish(name) {
@@ -846,7 +922,8 @@ export function composeRuntime(options) {
846
922
  probeEachMinute();
847
923
  }
848
924
  function installedTarget() {
849
- return Object.values(targets).some((target) => target.established);
925
+ return ((usableWhenHeld.length > 0 && reopened.size > 0) ||
926
+ Object.values(targets).some((target) => target.established));
850
927
  }
851
928
  function probeEachMinute() {
852
929
  if (!full || probing !== null || closed || !installedTarget())
@@ -1004,6 +1081,10 @@ export function composeRuntime(options) {
1004
1081
  }
1005
1082
  function publishChangeSet(set) {
1006
1083
  applyChangeSet(set);
1084
+ for (const { locator, path } of set.documents) {
1085
+ if (path !== null)
1086
+ attachedFiles.get(locator)?.delete(path);
1087
+ }
1007
1088
  if (set.rooms.length > 0 ||
1008
1089
  [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
1009
1090
  emitRooms();
@@ -1029,7 +1110,7 @@ export function composeRuntime(options) {
1029
1110
  const records = [...replicas].map(([locator, held]) => ({
1030
1111
  locator,
1031
1112
  held,
1032
- record: { ...held.record, model: revoked, selectionModel: revoked },
1113
+ record: { ...held.record, model: revoked },
1033
1114
  }));
1034
1115
  let answer = null;
1035
1116
  try {
@@ -1039,18 +1120,14 @@ export function composeRuntime(options) {
1039
1120
  locator,
1040
1121
  key: record.key,
1041
1122
  record: storedRecord(record),
1042
- composition: compositionOf(record),
1123
+ composed: composedOf(composed(record)),
1043
1124
  })),
1044
1125
  });
1045
1126
  }
1046
1127
  catch {
1047
1128
  journal.record('model revocation persistence failed');
1048
1129
  }
1049
- const keys = new Set(models.keys());
1050
- for (const locator of rooms.keys()) {
1051
- keys.add(modelKey(locator, 'collection'));
1052
- keys.add(modelKey(locator, 'typed-selection'));
1053
- }
1130
+ const keys = new Set([...models.keys(), ...rooms.keys()]);
1054
1131
  for (const key of keys) {
1055
1132
  invalidModels.add(key);
1056
1133
  models.set(key, revoked);
@@ -1105,7 +1182,7 @@ export function composeRuntime(options) {
1105
1182
  ? null
1106
1183
  : {
1107
1184
  roots: selection.roots,
1108
- model: modelTypes(modelOf(room.locator, 'typed-selection')),
1185
+ model: modelTypes(builtIn),
1109
1186
  },
1110
1187
  providerOwnedPaths: room.providerOwnedPaths,
1111
1188
  roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
@@ -1194,7 +1271,8 @@ export function composeRuntime(options) {
1194
1271
  queuesWithoutReplica.delete(locator);
1195
1272
  rooms.delete(locator);
1196
1273
  roomDrafts.delete(locator);
1197
- transfers.delete(locator);
1274
+ attachedFiles.delete(locator);
1275
+ untransferred(locator);
1198
1276
  catching.delete(locator);
1199
1277
  discovered.delete(locator);
1200
1278
  announced.delete(locator);
@@ -1205,12 +1283,9 @@ export function composeRuntime(options) {
1205
1283
  scopedRefusals.delete(locator);
1206
1284
  sourceRefusals.delete(locator);
1207
1285
  declarationRefusals.delete(locator);
1208
- for (const attached of ['collection', 'typed-selection']) {
1209
- const key = modelKey(locator, attached);
1210
- models.delete(key);
1211
- liveModels.delete(key);
1212
- invalidModels.delete(key);
1213
- }
1286
+ models.delete(locator);
1287
+ liveModels.delete(locator);
1288
+ invalidModels.delete(locator);
1214
1289
  journal.record(`room lost ${locator}`);
1215
1290
  }
1216
1291
  if (fresh.length === 0)
@@ -1258,7 +1333,14 @@ export function composeRuntime(options) {
1258
1333
  });
1259
1334
  await eraseFenced(fresh);
1260
1335
  }
1261
- 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);
1262
1344
  const prior = models.get(key);
1263
1345
  if (prior !== undefined &&
1264
1346
  prior.status !== 'unavailable' &&
@@ -1289,8 +1371,8 @@ export function composeRuntime(options) {
1289
1371
  const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1290
1372
  targets = held.targets;
1291
1373
  transfers.clear();
1292
- for (const [locator, record] of held.transfers)
1293
- transfers.set(locator, record);
1374
+ arrivals.clear();
1375
+ streaming.clear();
1294
1376
  limits = held.providerLimits;
1295
1377
  capabilities = held.capabilities;
1296
1378
  member = held.member;
@@ -1300,7 +1382,7 @@ export function composeRuntime(options) {
1300
1382
  presentations.clear();
1301
1383
  for (const record of held.presentation)
1302
1384
  presentations.set(record.app, record.value);
1303
- conversations.hydrate(heldConversations, chatDrafts);
1385
+ conversations.hydrate(heldConversations, chatDrafts, held.conversationList);
1304
1386
  pendings.clear();
1305
1387
  for (const queue of queues) {
1306
1388
  for (const { batch, state } of queue.pending) {
@@ -1308,6 +1390,7 @@ export function composeRuntime(options) {
1308
1390
  }
1309
1391
  }
1310
1392
  replicas.clear();
1393
+ reopened.clear();
1311
1394
  queuesWithoutReplica.clear();
1312
1395
  for (const [locator, queue] of queuedBatches) {
1313
1396
  if (!held.rooms.has(locator))
@@ -1316,9 +1399,8 @@ export function composeRuntime(options) {
1316
1399
  records.forEach((stored, index) => {
1317
1400
  if (fences.has(stored.locator))
1318
1401
  return;
1319
- const model = restored(modelKey(stored.locator, 'collection'), stored.model);
1320
- const selectionModel = restored(modelKey(stored.locator, 'typed-selection'), stored.selectionModel);
1321
- const record = { ...stored, model, selectionModel };
1402
+ const record = { ...stored, model: restored(stored.locator, stored.model) };
1403
+ reopened.add(record.locator);
1322
1404
  replicas.set(record.locator, {
1323
1405
  record,
1324
1406
  queue: queuedBatches.get(record.locator) ?? [],
@@ -1329,25 +1411,28 @@ export function composeRuntime(options) {
1329
1411
  });
1330
1412
  const existing = rooms.get(record.locator);
1331
1413
  if (existing === undefined)
1332
- list(record, false);
1414
+ list(record, record.roomVersion !== null);
1333
1415
  else {
1334
1416
  rooms.set(record.locator, {
1335
1417
  ...existing,
1336
1418
  roomVersion: record.roomVersion,
1337
1419
  operations: record.operations,
1338
1420
  replica: record.replica,
1339
- model: modelTypes(model),
1421
+ model: modelTypes(record.model),
1340
1422
  typedSelection: record.typedSelection === null
1341
1423
  ? null
1342
- : {
1343
- roots: record.typedSelection.roots,
1344
- model: modelTypes(selectionModel),
1345
- },
1424
+ : { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
1346
1425
  });
1347
1426
  }
1348
1427
  });
1349
- for (const key of held.orphans)
1350
- 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
+ }
1351
1436
  if (held.erasing || held.orphans.length > 0)
1352
1437
  queueMicrotask(sweep);
1353
1438
  return {
@@ -1529,7 +1614,7 @@ export function composeRuntime(options) {
1529
1614
  return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
1530
1615
  }
1531
1616
  function replayed(record, confirmedLayer, queue) {
1532
- return stepped(replaying(confirmedLayer, queue, record.locator, compositionOf(record)));
1617
+ return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
1533
1618
  }
1534
1619
  async function overlayDiff(record, before, visible, touched) {
1535
1620
  const kept = before !== undefined && before.record.key === record.key
@@ -1592,7 +1677,7 @@ export function composeRuntime(options) {
1592
1677
  const confirmedLayer = overrides === undefined
1593
1678
  ? read
1594
1679
  : await stepped(slicingWith(read, overrides.set, overrides.removed));
1595
- const composition = compositionOf(record);
1680
+ const composition = composed(record);
1596
1681
  const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
1597
1682
  const founded = kept.filter((batch) => !queue.includes(batch));
1598
1683
  const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
@@ -1635,6 +1720,7 @@ export function composeRuntime(options) {
1635
1720
  key: record.key,
1636
1721
  record: storedRecord(record),
1637
1722
  roots: record.typedSelection?.roots ?? [],
1723
+ composed: composedOf(composition),
1638
1724
  ...settling.commit,
1639
1725
  ...(overrides === undefined
1640
1726
  ? {}
@@ -1698,35 +1784,25 @@ export function composeRuntime(options) {
1698
1784
  };
1699
1785
  }
1700
1786
  function adoptModels(locator, record) {
1701
- for (const [attached, model] of [
1702
- ['collection', record.model],
1703
- ['typed-selection', record.selectionModel],
1704
- ]) {
1705
- const key = modelKey(locator, attached);
1706
- models.set(key, model);
1707
- invalidModels.delete(key);
1708
- if (model.status === 'current')
1709
- liveModels.set(key, JSON.stringify(model.snapshot));
1710
- else if (model.status === 'unavailable')
1711
- liveModels.delete(key);
1712
- }
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);
1713
1794
  }
1714
1795
  function remodel(locator, answers, held) {
1715
- const taken = (answer, previous, attached) => {
1716
- if (answer === undefined)
1717
- return previous;
1718
- const { state, refused } = attachedModel(answer, previous, attached === 'collection' ? roomTypes : NO_ROOM_TYPES);
1719
- if (refused !== null) {
1720
- const where = refused.location === undefined ? '' : ` ${refused.location}`;
1721
- journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
1722
- }
1723
- models.set(modelKey(locator, attached), state);
1724
- return state;
1725
- };
1726
- return {
1727
- collection: taken(answers?.collection, held.collection, 'collection'),
1728
- selection: taken(answers?.selection, held.selection, 'typed-selection'),
1729
- };
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 };
1730
1806
  }
1731
1807
  function publish(local, room, record, landing, live) {
1732
1808
  const landed = exclusive(async () => {
@@ -1753,14 +1829,13 @@ export function composeRuntime(options) {
1753
1829
  ? {
1754
1830
  room: true,
1755
1831
  view: 'refreshed',
1756
- commit: { refreshed: true, composition: compositionOf(record) },
1832
+ commit: { refreshed: true },
1757
1833
  }
1758
1834
  : {
1759
1835
  room: true,
1760
1836
  view: landing.refreshed ? 'refreshed' : 'confirmed',
1761
1837
  commit: {
1762
1838
  published: { from: prior?.record.key ?? null },
1763
- composition: compositionOf(record),
1764
1839
  ...(landing.refreshed ? { refreshed: true } : {}),
1765
1840
  },
1766
1841
  });
@@ -1772,7 +1847,7 @@ export function composeRuntime(options) {
1772
1847
  return true;
1773
1848
  }
1774
1849
  if (landing.kind === 'bootstrapped') {
1775
- transfers.delete(room.locator);
1850
+ untransferred(room.locator);
1776
1851
  catching.delete(room.locator);
1777
1852
  sweep();
1778
1853
  }
@@ -1819,12 +1894,7 @@ export function composeRuntime(options) {
1819
1894
  return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
1820
1895
  }
1821
1896
  async function saveParts(locator, named, writtenBy) {
1822
- const read = named.map(({ path, parts }) => ({ path, parts: readParts(parts) }));
1823
- if (read.some(({ path }, at) => path === '' ||
1824
- normalize(path) !== path ||
1825
- read.findIndex((one) => one.path === path) !== at)) {
1826
- throw runtimeError('room-draft-malformed');
1827
- }
1897
+ const read = readSavedParts(named);
1828
1898
  const target = await savedRoom(locator);
1829
1899
  const saved = [];
1830
1900
  const files = [];
@@ -1833,7 +1903,7 @@ export function composeRuntime(options) {
1833
1903
  for (const { path, parts } of read) {
1834
1904
  const file = parts.file === undefined
1835
1905
  ? undefined
1836
- : await heldFile(parts.file.blob, binaryBase(visible, path));
1906
+ : await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
1837
1907
  if (parts.file !== undefined && file !== undefined) {
1838
1908
  files.push({ key: file.localId, blob: parts.file.blob });
1839
1909
  }
@@ -1868,13 +1938,14 @@ export function composeRuntime(options) {
1868
1938
  const referring = removed.length === 0
1869
1939
  ? []
1870
1940
  : (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
1871
- const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
1941
+ const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
1872
1942
  const { visible } = await replayed(record, confirmedLayer, replica.queue);
1873
- const composition = compositionOf(record);
1943
+ const composition = composed(record);
1874
1944
  return {
1875
1945
  record,
1876
1946
  confirmed: confirmedLayer,
1877
1947
  visible,
1948
+ folders: new Set(await local.folders(record.key, 'visible', paths)),
1878
1949
  *visibleOver(queue) {
1879
1950
  return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
1880
1951
  },
@@ -1886,13 +1957,7 @@ export function composeRuntime(options) {
1886
1957
  return false;
1887
1958
  const local = await openLocal();
1888
1959
  const roots = record.typedSelection?.roots ?? [];
1889
- if (composition.model.status === 'unavailable' &&
1890
- (await local.typedWithin(record.key, 'visible', roots, false))) {
1891
- return true;
1892
- }
1893
- return (composition.selection !== null &&
1894
- composition.selection.model.status === 'unavailable' &&
1895
- (await local.typedWithin(record.key, 'visible', roots, true)));
1960
+ return local.typedWithin(record.key, 'visible', roots, false);
1896
1961
  }
1897
1962
  function textDiffers(left, right) {
1898
1963
  return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
@@ -1997,7 +2062,7 @@ export function composeRuntime(options) {
1997
2062
  const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
1998
2063
  const clean = available(composition) &&
1999
2064
  changed.length <= CLEAN_CHANGES &&
2000
- (await changedCleanly(changed, compositionOf(record), composition, {
2065
+ (await changedCleanly(changed, composed(record), composition, {
2001
2066
  lookup,
2002
2067
  texts: textsOf(local, record.key, 'confirmed'),
2003
2068
  referrers: referrersOf(local, record.key, 'confirmed'),
@@ -2028,6 +2093,7 @@ export function composeRuntime(options) {
2028
2093
  operations: room.operations,
2029
2094
  providerOwnedPaths: room.providerOwnedPaths,
2030
2095
  visible: saving.visible,
2096
+ folders: saving.folders,
2031
2097
  queued: queuedDocuments(replica.queue),
2032
2098
  unmodeled: await unmodeledIn(record),
2033
2099
  validate: validatorOf(record, saving.confirmed),
@@ -2048,6 +2114,7 @@ export function composeRuntime(options) {
2048
2114
  locator,
2049
2115
  key: saving.record.key,
2050
2116
  roots: saving.record.typedSelection?.roots ?? [],
2117
+ composed: composedOf(composed(saving.record)),
2051
2118
  overlay: diff.overlay,
2052
2119
  });
2053
2120
  }
@@ -2079,7 +2146,7 @@ export function composeRuntime(options) {
2079
2146
  }
2080
2147
  const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
2081
2148
  const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
2082
- const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, sourceSaving.visible)));
2149
+ const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
2083
2150
  if (changed.length > 0)
2084
2151
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2085
2152
  const target = replicas.get(destination);
@@ -2204,7 +2271,7 @@ export function composeRuntime(options) {
2204
2271
  ...saved.map(({ path }) => path),
2205
2272
  ...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
2206
2273
  ]);
2207
- const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, underneath(held, path, saving.visible)));
2274
+ const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
2208
2275
  if (changed.length > 0)
2209
2276
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2210
2277
  const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
@@ -2232,6 +2299,7 @@ export function composeRuntime(options) {
2232
2299
  providerLimits: limits,
2233
2300
  handle: room.handle,
2234
2301
  visible: saving.visible,
2302
+ folders: saving.folders,
2235
2303
  changes,
2236
2304
  bases: parts.flatMap(({ bases }) => bases),
2237
2305
  queued: queuedDocuments(replica.queue),
@@ -2382,6 +2450,9 @@ export function composeRuntime(options) {
2382
2450
  async function readHistory(locator, path, query = {}) {
2383
2451
  if (closed || refusal !== null)
2384
2452
  return UNCERTAIN;
2453
+ await firstDiscovery;
2454
+ if (!roomsNamed)
2455
+ return UNCERTAIN;
2385
2456
  try {
2386
2457
  await ensureAuthorization();
2387
2458
  }
@@ -2474,6 +2545,9 @@ export function composeRuntime(options) {
2474
2545
  async function transferBinary(request, transfer, answer) {
2475
2546
  if (closed || refusal !== null)
2476
2547
  return UNCERTAIN;
2548
+ await firstDiscovery;
2549
+ if (!roomsNamed)
2550
+ return UNCERTAIN;
2477
2551
  try {
2478
2552
  await ensureAuthorization();
2479
2553
  }
@@ -2504,23 +2578,27 @@ export function composeRuntime(options) {
2504
2578
  return UNCERTAIN;
2505
2579
  }
2506
2580
  }
2581
+ function fetchBinary(request, name) {
2582
+ return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2583
+ }
2507
2584
  async function readBinary(request, name) {
2508
2585
  if (closed)
2509
2586
  return UNCERTAIN;
2510
2587
  const held = await keptFiles.find(name);
2511
2588
  if (held !== null)
2512
2589
  return held;
2513
- return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2590
+ return fetchBinary(request, name);
2514
2591
  }
2515
2592
  async function binaryPath(locator, path, revision) {
2516
2593
  if (revision !== undefined)
2517
2594
  return { kind: 'path', locator, path, revision };
2518
2595
  const replica = replicas.get(locator);
2519
- if (replica === undefined)
2520
- return null;
2521
2596
  const local = await openLocal();
2522
- const [held] = await local.entries(replica.record.key, 'visible', [path]);
2523
- if (held?.binary === undefined || held.binary === null)
2597
+ const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
2598
+ const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
2599
+ if (attached !== undefined)
2600
+ return { kind: 'path', locator, path, revision: attached.revision };
2601
+ if (replica === undefined || held?.binary === undefined || held.binary === null)
2524
2602
  return null;
2525
2603
  if (held.binary.state === 'pending')
2526
2604
  return { kind: 'pending', localId: held.binary.localId };
@@ -2552,33 +2630,83 @@ export function composeRuntime(options) {
2552
2630
  revision: record.revision,
2553
2631
  };
2554
2632
  }
2555
- async function readBinaryPath(locator, path, revision) {
2633
+ async function askedBinary(locator, path, revision) {
2556
2634
  await ready;
2557
2635
  const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2558
- const asked = drafted !== undefined && drafted.localId === revision
2636
+ return drafted !== undefined && drafted.localId === revision
2559
2637
  ? { kind: 'pending', localId: revision }
2560
- : await binaryPath(locator, path, revision);
2638
+ : binaryPath(locator, path, revision);
2639
+ }
2640
+ function pendingBinary(locator, path, localId) {
2641
+ const facts = queuedFacts(locator, path, localId);
2642
+ return facts === null
2643
+ ? absentBinary(path)
2644
+ : {
2645
+ kind: 'binary',
2646
+ file: localId,
2647
+ byteLength: facts.byteLength,
2648
+ mediaType: facts.mediaType,
2649
+ };
2650
+ }
2651
+ async function readBinaryPath(locator, path, revision) {
2652
+ const asked = await askedBinary(locator, path, revision);
2561
2653
  if (asked === null)
2562
2654
  return absentBinary(path);
2563
- if (asked.kind === 'pending') {
2564
- const facts = queuedFacts(locator, path, asked.localId);
2565
- return facts === null
2566
- ? absentBinary(path)
2567
- : {
2568
- kind: 'binary',
2569
- file: asked.localId,
2570
- byteLength: facts.byteLength,
2571
- mediaType: facts.mediaType,
2572
- };
2573
- }
2655
+ if (asked.kind === 'pending')
2656
+ return pendingBinary(locator, path, asked.localId);
2574
2657
  return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
2575
2658
  }
2659
+ async function heldMediaType(locator, path) {
2660
+ const replica = replicas.get(locator);
2661
+ const [held] = replica === undefined
2662
+ ? []
2663
+ : await (await openLocal()).entries(replica.record.key, 'visible', [path]);
2664
+ return held === undefined
2665
+ ? attachedFiles.get(locator)?.get(path)?.mediaType
2666
+ : held.binary?.mediaType;
2667
+ }
2668
+ function linkOf(asked, held) {
2669
+ return transferBinary(asked, (authorization) => options.adapter.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
2670
+ }
2576
2671
  async function linkBinaryPath(locator, path, revision) {
2577
2672
  await ready;
2578
2673
  const asked = await binaryPath(locator, path, revision);
2579
2674
  if (asked === null || asked.kind === 'pending')
2580
2675
  return absentBinary(path);
2581
- return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
2676
+ return linkOf(asked, revision === undefined ? await heldMediaType(locator, path) : undefined);
2677
+ }
2678
+ async function showBinaryPath(locator, path, revision) {
2679
+ const asked = await askedBinary(locator, path, revision);
2680
+ if (asked === null)
2681
+ return absentBinary(path);
2682
+ if (asked.kind === 'pending')
2683
+ return pendingBinary(locator, path, asked.localId);
2684
+ if (closed)
2685
+ return UNCERTAIN;
2686
+ const name = { locator, path: asked.path, revision: asked.revision };
2687
+ const kept = await keptFiles.find(name);
2688
+ if (kept !== null)
2689
+ return kept;
2690
+ const mediaType = await heldMediaType(locator, path);
2691
+ if (mediaType?.startsWith('image/') === true)
2692
+ return fetchBinary(asked, name);
2693
+ return linkOf(asked, revision === undefined ? mediaType : undefined);
2694
+ }
2695
+ async function attachBinary(locator, file) {
2696
+ await ready;
2697
+ await firstDiscovery;
2698
+ if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
2699
+ return OFFLINE;
2700
+ const request = await attachedFile(locator, file);
2701
+ return transferBinary({ locator, path: file.name }, async (held) => {
2702
+ const result = await options.adapter.attach(held, request);
2703
+ if (result.kind === 'attached') {
2704
+ const landed = attachedFiles.get(locator) ?? new Map();
2705
+ landed.set(result.path, { revision: result.revision, mediaType: request.mediaType });
2706
+ attachedFiles.set(locator, landed);
2707
+ }
2708
+ return result;
2709
+ }, attachAnswer);
2582
2710
  }
2583
2711
  function readPart(file, part) {
2584
2712
  return database.ask({ kind: 'file.part', key: file, part });
@@ -2753,18 +2881,25 @@ export function composeRuntime(options) {
2753
2881
  after = page.next;
2754
2882
  }
2755
2883
  }
2756
- async function validatedRoom(local, record, view, previous, overrides = null) {
2757
- const composition = compositionOf(record);
2758
- const modeled = record.model.status !== 'unavailable' || record.selectionModel.status !== 'unavailable';
2884
+ async function validatedRoom(local, record, view, previous, overrides = null, checked = null) {
2885
+ const composition = composed(record);
2886
+ const modeled = record.model.status !== 'unavailable' || record.typedSelection !== null;
2759
2887
  const lookup = overrides === null
2760
2888
  ? lookupOf(local, record.key, view)
2761
2889
  : overriddenLookup(lookupOf(local, record.key, view), overrides);
2762
2890
  const texts = overrides === null
2763
2891
  ? textsOf(local, record.key, view)
2764
2892
  : overriddenTexts(textsOf(local, record.key, view), overrides);
2765
- const walk = overrides === null
2893
+ const walking = overrides === null
2766
2894
  ? walkOf(local, record.key, view)
2767
2895
  : overriddenWalk(walkOf(local, record.key, view), overrides);
2896
+ const walk = checked === null
2897
+ ? walking
2898
+ : async (after) => {
2899
+ const page = await walking(after);
2900
+ checked(page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0));
2901
+ return page;
2902
+ };
2768
2903
  let changes = null;
2769
2904
  let reshaped = false;
2770
2905
  if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
@@ -2789,11 +2924,11 @@ export function composeRuntime(options) {
2789
2924
  }
2790
2925
  const named = changes === null ? null : changes.map(({ path }) => path);
2791
2926
  const textChanged = changes === null || changes.length > 0 || reshaped;
2792
- const modelsChanged = previous === null || !sameComposition(compositionOf(previous.record), composition);
2927
+ const modelsChanged = previous === null || !sameComposition(composed(previous.record), composition);
2793
2928
  if (modeled && (textChanged || modelsChanged)) {
2794
2929
  const clean = previous !== null &&
2795
2930
  changes !== null &&
2796
- (await changedCleanly(changes, compositionOf(previous.record), composition, {
2931
+ (await changedCleanly(changes, composed(previous.record), composition, {
2797
2932
  lookup,
2798
2933
  texts,
2799
2934
  referrers: referrersOf(local, record.key, 'confirmed'),
@@ -2811,13 +2946,13 @@ export function composeRuntime(options) {
2811
2946
  return unreadableRoom(texts, walk, named);
2812
2947
  return null;
2813
2948
  }
2814
- function refreshingOf(local, key, liveness, replaying = null) {
2815
- const write = replaying === null ? exclusive : installWrite;
2949
+ function refreshingOf(local, key, composition, liveness, replaying = null) {
2950
+ const write = replaying === null ? exclusive : finishingWrite;
2816
2951
  return {
2817
2952
  async turn() {
2818
2953
  if (replaying === null)
2819
2954
  return;
2820
- installing(replaying, 'replaying');
2955
+ installing(replaying);
2821
2956
  await screened();
2822
2957
  },
2823
2958
  prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
@@ -2831,7 +2966,7 @@ export function composeRuntime(options) {
2831
2966
  const staged = await write(async () => {
2832
2967
  if (!liveness())
2833
2968
  return false;
2834
- await timed(() => local.refresh(key, last
2969
+ await timed(stageBudget, () => local.refresh(key, composition, last
2835
2970
  ? { ...page, entries }
2836
2971
  : { entries, removals: [], folders: [], folderRemovals: [] }));
2837
2972
  return true;
@@ -2850,28 +2985,79 @@ export function composeRuntime(options) {
2850
2985
  let spent = 0;
2851
2986
  while (end < entries.length) {
2852
2987
  const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
2853
- if (spent + size > stageBytes && end > from)
2988
+ if (spent + size > stageBudget.bytes && end > from)
2854
2989
  break;
2855
2990
  spent += size;
2856
2991
  end += 1;
2857
2992
  }
2858
2993
  return end;
2859
2994
  }
2860
- async function timed(work) {
2995
+ async function timed(budget, work) {
2861
2996
  const started = performance.now();
2862
2997
  const done = await work();
2863
2998
  const spent = performance.now() - started;
2864
- if (spent < STAGE_AIM_MS / 2)
2865
- stageBytes = Math.min(STAGE_BYTES, Math.round(stageBytes * 1.5));
2866
- else if (spent > STAGE_AIM_MS * 2) {
2867
- stageBytes = Math.max(STAGE_FLOOR, Math.round(stageBytes / 1.5));
2999
+ if (spent < budget.aim / 2)
3000
+ budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
3001
+ else if (spent > budget.aim * 2) {
3002
+ budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
2868
3003
  }
2869
3004
  return done;
2870
3005
  }
2871
3006
  function installWrite(work) {
2872
- const next = installWrites.then(() => exclusive(work), () => exclusive(work));
2873
- installWrites = next.then(() => undefined, () => undefined);
2874
- return next;
3007
+ return installTurn(installTurns.starting, work);
3008
+ }
3009
+ function finishingWrite(work) {
3010
+ return installTurn(installTurns.finishing, work);
3011
+ }
3012
+ function installTurn(queue, work) {
3013
+ return new Promise((resolve, reject) => {
3014
+ queue.push(() => void exclusive(work)
3015
+ .then(resolve, reject)
3016
+ .finally(() => {
3017
+ installTurns.busy = false;
3018
+ nextInstallTurn();
3019
+ }));
3020
+ nextInstallTurn();
3021
+ });
3022
+ }
3023
+ function nextInstallTurn() {
3024
+ if (installTurns.busy || installTurns.ahead > 0)
3025
+ return;
3026
+ const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
3027
+ if (turn === undefined)
3028
+ return;
3029
+ installTurns.busy = true;
3030
+ turn();
3031
+ }
3032
+ async function ahead(work) {
3033
+ let taken = false;
3034
+ const hold = {
3035
+ take() {
3036
+ if (taken)
3037
+ return;
3038
+ taken = true;
3039
+ installTurns.ahead += 1;
3040
+ },
3041
+ release() {
3042
+ if (!taken)
3043
+ return;
3044
+ taken = false;
3045
+ installTurns.ahead -= 1;
3046
+ resumeInstall();
3047
+ },
3048
+ };
3049
+ hold.take();
3050
+ try {
3051
+ return await work(hold);
3052
+ }
3053
+ finally {
3054
+ hold.release();
3055
+ }
3056
+ }
3057
+ function resumeInstall() {
3058
+ if (installTurns.ahead > 0)
3059
+ return;
3060
+ void yielded().then(nextInstallTurn);
2875
3061
  }
2876
3062
  function screened() {
2877
3063
  if (onScreen || closed)
@@ -2879,24 +3065,73 @@ export function composeRuntime(options) {
2879
3065
  queueReadiness();
2880
3066
  return new Promise((resolve) => shownWaiters.push(resolve));
2881
3067
  }
2882
- async function heldPart() {
2883
- while (partsFree === 0)
2884
- await new Promise((admit) => partWaiters.push(admit));
2885
- partsFree -= 1;
2886
- return freed();
3068
+ async function heldStream() {
3069
+ for (;;) {
3070
+ if (streams.generation !== generation)
3071
+ streams = { generation, free: STREAMS_HELD };
3072
+ const held = streams;
3073
+ if (held.free > 0) {
3074
+ held.free -= 1;
3075
+ return once(() => {
3076
+ held.free += 1;
3077
+ });
3078
+ }
3079
+ await new Promise((admit) => installWaiters.push(admit));
3080
+ }
2887
3081
  }
2888
- function freed() {
3082
+ async function unqueued() {
3083
+ while (queuedBytes >= QUEUED_BYTES) {
3084
+ await new Promise((admit) => installWaiters.push(admit));
3085
+ }
3086
+ }
3087
+ function once(release) {
2889
3088
  let released = false;
2890
3089
  return () => {
2891
3090
  if (released)
2892
3091
  return;
2893
3092
  released = true;
2894
- partsFree += 1;
2895
- partWaiters.shift()?.();
3093
+ release();
3094
+ for (const admit of installWaiters.splice(0))
3095
+ admit();
2896
3096
  };
2897
3097
  }
2898
- function installing(locator, step, share = null) {
2899
- installs.set(locator, { step, share });
3098
+ function installing(locator) {
3099
+ installs.add(locator);
3100
+ queueReadiness();
3101
+ }
3102
+ function untransferred(locator) {
3103
+ transfers.delete(locator);
3104
+ arrivals.delete(locator);
3105
+ streaming.delete(locator);
3106
+ underived.delete(locator);
3107
+ unindexed.delete(locator);
3108
+ unchecked.delete(locator);
3109
+ }
3110
+ function owedIndexes(locator, bytes, text) {
3111
+ if (bytes <= 0 || text <= 0)
3112
+ return;
3113
+ unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes: bytes / 2, text }]);
3114
+ const check = unchecked.get(locator) ?? { bytes: 0, text: 0, done: 0 };
3115
+ unchecked.set(locator, { ...check, bytes: check.bytes + bytes / 2, text: check.text + text });
3116
+ }
3117
+ function built(locator, { done, text }) {
3118
+ const owed = unindexed.get(locator) ?? [];
3119
+ let left = text;
3120
+ while (!done && left > 0 && owed.length > 0) {
3121
+ const [first] = owed;
3122
+ if (left >= first.text) {
3123
+ left -= first.text;
3124
+ owed.shift();
3125
+ continue;
3126
+ }
3127
+ owed[0] = {
3128
+ bytes: (first.bytes * (first.text - left)) / first.text,
3129
+ text: first.text - left,
3130
+ };
3131
+ left = 0;
3132
+ }
3133
+ if (done || owed.length === 0)
3134
+ unindexed.delete(locator);
2900
3135
  queueReadiness();
2901
3136
  }
2902
3137
  function installed(locator) {
@@ -2914,7 +3149,7 @@ export function composeRuntime(options) {
2914
3149
  for (const resolve of shownWaiters.splice(0))
2915
3150
  resolve();
2916
3151
  }
2917
- async function pullRefresh(authorized, held, liveness) {
3152
+ async function pullRefresh(authorized, held, liveness, hold) {
2918
3153
  if (!liveness())
2919
3154
  return unconfirmed;
2920
3155
  const turn = generation;
@@ -2923,7 +3158,18 @@ export function composeRuntime(options) {
2923
3158
  const local = await openLocal();
2924
3159
  try {
2925
3160
  await exclusive(() => local.unrefresh(record.key));
2926
- const result = await advancement(reads, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, liveness));
3161
+ const result = await advancement({
3162
+ ...reads,
3163
+ async refresh(authorization, request) {
3164
+ hold.release();
3165
+ try {
3166
+ return await reads.refresh(authorization, request);
3167
+ }
3168
+ finally {
3169
+ hold.take();
3170
+ }
3171
+ },
3172
+ }, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, composed(record), liveness));
2927
3173
  if (result.kind !== 'advanced') {
2928
3174
  journal.record(`transfer refresh ${record.locator} ${result.kind}`);
2929
3175
  return result;
@@ -2966,129 +3212,142 @@ export function composeRuntime(options) {
2966
3212
  return made;
2967
3213
  }
2968
3214
  function staging(local, liveness) {
3215
+ const derived = async (locator, key, composition) => {
3216
+ underived.delete(locator);
3217
+ for (;;) {
3218
+ await screened();
3219
+ if (!liveness())
3220
+ return false;
3221
+ const done = await finishingWrite(async () => liveness()
3222
+ ? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
3223
+ : null);
3224
+ if (done === null)
3225
+ return false;
3226
+ built(locator, done);
3227
+ if (done.done)
3228
+ return true;
3229
+ }
3230
+ };
2969
3231
  return {
2970
3232
  held: (locator) => transfers.get(locator) ?? null,
2971
3233
  async turn(locator, step) {
2972
- installing(locator, step);
3234
+ installing(locator);
2973
3235
  for (;;) {
2974
3236
  await screened();
2975
- const release = step === 'downloading' ? await heldPart() : () => { };
3237
+ const release = step === 'downloading' ? await heldStream() : () => { };
2976
3238
  if (onScreen || closed)
2977
3239
  return release;
2978
3240
  release();
2979
3241
  }
2980
3242
  },
2981
- spare() {
2982
- if (!onScreen || closed || partsFree === 0)
2983
- return null;
2984
- partsFree -= 1;
2985
- return freed();
3243
+ unqueued,
3244
+ queued(bytes) {
3245
+ queuedBytes += bytes;
3246
+ return once(() => {
3247
+ queuedBytes -= bytes;
3248
+ });
2986
3249
  },
2987
3250
  arriving(locator) {
2988
3251
  let ended = false;
2989
3252
  return {
2990
- at(token, bytes) {
3253
+ at(bytes) {
2991
3254
  if (ended)
2992
3255
  return;
2993
- const held = arrivals.get(locator) ?? new Map();
2994
- arrivals.set(locator, held.set(token, bytes));
3256
+ arrivals.set(locator, { parts: arrivals.get(locator)?.parts ?? 0, bytes });
3257
+ streaming.add(locator);
2995
3258
  const now = performance.now();
2996
3259
  if (now - arrivalReported < ARRIVAL_REPORT_MS)
2997
3260
  return;
2998
3261
  arrivalReported = now;
2999
3262
  queueReadiness();
3000
3263
  },
3264
+ part() {
3265
+ const arrived = arrivals.get(locator);
3266
+ arrivals.set(locator, {
3267
+ parts: (arrived?.parts ?? 0) + 1,
3268
+ bytes: arrived?.bytes ?? 0,
3269
+ });
3270
+ queueReadiness();
3271
+ },
3001
3272
  end() {
3273
+ if (ended)
3274
+ return;
3002
3275
  ended = true;
3003
- if (arrivals.delete(locator))
3276
+ if (streaming.delete(locator))
3004
3277
  queueReadiness();
3005
3278
  },
3006
3279
  };
3007
3280
  },
3008
- received: (locator, token) => local.transferPart(locator, token),
3009
- keep: (locator, token, received) => installWrite(async () => {
3010
- if (liveness())
3011
- await local.keepPart(locator, token, received);
3012
- }),
3013
3281
  announced(locator) {
3014
3282
  const later = announced.get(locator);
3015
3283
  return later === undefined ? [] : [later];
3016
3284
  },
3017
- async stage(record, page, composition, received) {
3285
+ async stage(record, page, composition) {
3018
3286
  const { locator } = record;
3019
- const whole = received?.byteLength ?? 0;
3020
- const from = record.writing?.written ?? 0;
3021
- const token = record.writing?.token ?? null;
3022
- const entries = page.entries.length;
3023
- let held = record;
3024
- if (token !== null)
3025
- arrivals.get(locator)?.delete(token);
3026
- installing(locator, 'writing', { written: from, entries, bytes: whole });
3027
- for (let written = from;;) {
3028
- await screened();
3287
+ installing(locator);
3288
+ await screened();
3289
+ if (!liveness())
3290
+ return null;
3291
+ const key = await installWrite(async () => {
3029
3292
  if (!liveness())
3030
3293
  return null;
3031
- const end = batchEnd(page.entries, written);
3032
- const last = end === entries;
3033
- const next = last
3034
- ? {
3035
- ...held,
3036
- writing: null,
3037
- done: {
3038
- parts: held.done.parts + (received === null ? 0 : 1),
3039
- bytes: held.done.bytes + whole,
3040
- },
3041
- }
3042
- : { ...held, writing: { token, written: end } };
3043
- const key = await installWrite(async () => {
3044
- if (!liveness())
3045
- return null;
3046
- return timed(() => local.stage(next, composition, {
3047
- entries: page.entries.slice(written, end),
3048
- folders: last ? page.folders : [],
3049
- }, last && received !== null && token !== null ? token : undefined));
3050
- });
3051
- if (key === null)
3052
- return null;
3053
- const arrived = !transfers.has(locator);
3054
- held = { ...next, key };
3055
- transfers.set(locator, held);
3056
- if (arrived)
3057
- liveQueries.recover();
3058
- if (last) {
3059
- installing(locator, held.pageToken === null ? 'replaying' : 'downloading');
3060
- liveQueries.recover();
3061
- return held;
3062
- }
3063
- installing(locator, 'writing', { written: end, entries, bytes: whole });
3064
- written = end;
3065
- }
3294
+ return local.stage(record, composition, page);
3295
+ });
3296
+ if (key === null || !liveness())
3297
+ return null;
3298
+ const arrived = !transfers.has(locator);
3299
+ const prior = transfers.get(locator)?.done.bytes ?? 0;
3300
+ const held = { ...record, key };
3301
+ transfers.set(locator, held);
3302
+ if (arrived)
3303
+ liveQueries.recover();
3304
+ installing(locator);
3305
+ liveQueries.recover();
3306
+ const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
3307
+ owedIndexes(locator, record.done.bytes - prior, text);
3308
+ const owed = text + (underived.get(locator) ?? 0);
3309
+ underived.set(locator, owed);
3310
+ if (owed < deriveBudget.bytes)
3311
+ return held;
3312
+ return (await derived(locator, key, composition)) ? held : null;
3066
3313
  },
3067
- async refreshing(locator, key) {
3314
+ derive: derived,
3315
+ async refreshing(locator, key, composition) {
3068
3316
  await exclusive(() => local.unrefresh(key));
3069
- return refreshingOf(local, key, liveness, locator);
3317
+ return refreshingOf(local, key, composition, liveness, locator);
3070
3318
  },
3071
3319
  abandon: (locator) => exclusive(async () => {
3072
3320
  if (!liveness())
3073
3321
  return;
3074
3322
  await local.abandon(locator);
3075
- transfers.delete(locator);
3323
+ untransferred(locator);
3076
3324
  liveQueries.recover();
3077
3325
  sweep();
3078
3326
  }),
3079
- setAside: (locator) => exclusive(async () => {
3327
+ setAside: async (locator) => {
3080
3328
  if (!liveness())
3081
3329
  return;
3082
- await local.abandon(locator, true);
3083
- transfers.delete(locator);
3330
+ untransferred(locator);
3084
3331
  liveQueries.recover();
3085
- }),
3332
+ },
3086
3333
  release: (key) => exclusive(async () => {
3087
3334
  await local.release(key);
3088
3335
  sweep();
3089
3336
  }),
3090
3337
  content: (key) => heldContentOf(local, key),
3091
- validate: (record, view) => validatedRoom(local, record, view, null),
3338
+ async validate(record, view) {
3339
+ const { locator } = record;
3340
+ const refused = await validatedRoom(local, record, view, null, null, (text) => {
3341
+ const check = unchecked.get(locator);
3342
+ if (check === undefined)
3343
+ return;
3344
+ unchecked.set(locator, { ...check, done: check.done + text });
3345
+ queueReadiness();
3346
+ });
3347
+ unchecked.delete(locator);
3348
+ queueReadiness();
3349
+ return refused;
3350
+ },
3092
3351
  };
3093
3352
  }
3094
3353
  function reusable(room, local) {
@@ -3106,10 +3365,7 @@ export function composeRuntime(options) {
3106
3365
  liveQueries.recover();
3107
3366
  }
3108
3367
  try {
3109
- const result = await bootstrapRoom(reads, authorized, room, {
3110
- collection: modelOf(room.locator),
3111
- selection: modelOf(room.locator, 'typed-selection'),
3112
- }, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
3368
+ 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));
3113
3369
  const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
3114
3370
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
3115
3371
  if (result.kind !== 'staged') {
@@ -3153,32 +3409,10 @@ export function composeRuntime(options) {
3153
3409
  return unconfirmed;
3154
3410
  const held = replicas.get(room.locator);
3155
3411
  if (held !== undefined) {
3156
- const refreshed = await pullRefresh(authorized, held, live);
3157
- if (!live())
3158
- return unconfirmed;
3159
- if (refreshed.kind === 'still-current') {
3160
- stale.delete(room.locator);
3161
- if (sourceRefusals.has(room.locator))
3162
- return null;
3163
- if (unreadable.delete(room.locator)) {
3164
- scopedRefusals.delete(room.locator);
3165
- list(room, true);
3166
- emitRooms();
3167
- }
3168
- return null;
3169
- }
3170
- if (refreshed.kind === 'advanced') {
3171
- const landing = { kind: 'refreshed', base: refreshed.base };
3172
- if (!(await publish(local, room, refreshed.record, landing, live))) {
3173
- const now = replicas.get(room.locator)?.record.roomVersion;
3174
- const reached = now !== undefined &&
3175
- options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3176
- return reached ? null : advance(room, local, authorized, live);
3177
- }
3178
- await retainModel(local, room, 'collection', modelOf(room.locator));
3179
- return null;
3180
- }
3181
- if (refreshed.kind !== 'beyond-horizon')
3412
+ const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold));
3413
+ if (refreshed === 'behind')
3414
+ return advance(room, local, authorized, live);
3415
+ if (refreshed !== 'beyond-horizon')
3182
3416
  return refreshed;
3183
3417
  if (opened !== null) {
3184
3418
  owed.add(room.locator);
@@ -3195,6 +3429,34 @@ export function composeRuntime(options) {
3195
3429
  }
3196
3430
  return live() ? bootstrapped : unconfirmed;
3197
3431
  }
3432
+ async function refreshedRoom(room, local, authorized, held, live, hold) {
3433
+ const refreshed = await pullRefresh(authorized, held, live, hold);
3434
+ if (!live())
3435
+ return unconfirmed;
3436
+ if (refreshed.kind === 'still-current') {
3437
+ stale.delete(room.locator);
3438
+ if (sourceRefusals.has(room.locator))
3439
+ return null;
3440
+ if (unreadable.delete(room.locator)) {
3441
+ scopedRefusals.delete(room.locator);
3442
+ list(room, true);
3443
+ emitRooms();
3444
+ }
3445
+ return null;
3446
+ }
3447
+ if (refreshed.kind === 'advanced') {
3448
+ const landing = { kind: 'refreshed', base: refreshed.base };
3449
+ if (!(await publish(local, room, refreshed.record, landing, live))) {
3450
+ const now = replicas.get(room.locator)?.record.roomVersion;
3451
+ const reached = now !== undefined &&
3452
+ options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3453
+ return reached ? null : 'behind';
3454
+ }
3455
+ await retainModel(local, room, modelOf(room.locator));
3456
+ return null;
3457
+ }
3458
+ return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
3459
+ }
3198
3460
  async function withhold(withheld, room, local, progress) {
3199
3461
  if (withheld.kind === 'unconfirmed')
3200
3462
  return;
@@ -3716,6 +3978,7 @@ export function composeRuntime(options) {
3716
3978
  locator: room.locator,
3717
3979
  key: before.record.key,
3718
3980
  roots: before.record.typedSelection?.roots ?? [],
3981
+ composed: composedOf(composed(before.record)),
3719
3982
  overlay: { set: [], cleared: [...before.overlaid] },
3720
3983
  },
3721
3984
  ]),
@@ -3771,16 +4034,17 @@ export function composeRuntime(options) {
3771
4034
  release();
3772
4035
  };
3773
4036
  }
3774
- async function retainModel(local, room, attached, next) {
3775
- const key = modelKey(room.locator, attached);
3776
- const previous = modelOf(room.locator, attached);
3777
- const heldBefore = replicas.get(room.locator)?.record;
3778
- if (JSON.stringify(previous) === JSON.stringify(next) &&
3779
- (heldBefore === undefined ||
3780
- (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
3781
- JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
3782
- JSON.stringify(heldBefore.typedSelection) ===
3783
- JSON.stringify(room.typedSelection ?? null))))
4037
+ function declaredAsHeld(room) {
4038
+ const held = replicas.get(room.locator)?.record;
4039
+ return (held === undefined ||
4040
+ (JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
4041
+ JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
4042
+ JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
4043
+ }
4044
+ async function retainModel(local, room, next) {
4045
+ const key = room.locator;
4046
+ const previous = modelOf(room.locator);
4047
+ if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(room))
3784
4048
  return;
3785
4049
  if (next.status === 'unavailable') {
3786
4050
  invalidModels.add(key);
@@ -3797,11 +4061,11 @@ export function composeRuntime(options) {
3797
4061
  operations: room.operations,
3798
4062
  replica: room.replica,
3799
4063
  typedSelection: room.typedSelection ?? null,
3800
- ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
4064
+ model: next,
3801
4065
  };
3802
4066
  let read = 'staged';
3803
4067
  if (!sameMembership(declared, held.record)) {
3804
- const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), compositionOf(declared));
4068
+ const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), composed(declared));
3805
4069
  read =
3806
4070
  verdict.verdict !== 'refuse'
3807
4071
  ? 'staged'
@@ -3822,7 +4086,7 @@ export function composeRuntime(options) {
3822
4086
  key: record.key,
3823
4087
  record: storedRecord(record),
3824
4088
  roots: record.typedSelection?.roots ?? [],
3825
- composition: compositionOf(record),
4089
+ composed: composedOf(composed(record)),
3826
4090
  overlay: diff.overlay,
3827
4091
  },
3828
4092
  ],
@@ -3844,18 +4108,21 @@ export function composeRuntime(options) {
3844
4108
  declarationRefusals.add(room.locator);
3845
4109
  }
3846
4110
  }
3847
- models.set(key, next);
3848
- if (next.status === 'current') {
3849
- liveModels.set(key, JSON.stringify(next.snapshot));
3850
- invalidModels.delete(key);
3851
- }
3852
- list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
4111
+ heldModel(room, next);
3853
4112
  if (changed === null)
3854
4113
  emitRooms();
3855
4114
  else
3856
4115
  publishChangeSet(changed);
3857
4116
  });
3858
4117
  }
4118
+ function heldModel(room, next) {
4119
+ models.set(room.locator, next);
4120
+ if (next.status === 'current') {
4121
+ liveModels.set(room.locator, JSON.stringify(next.snapshot));
4122
+ invalidModels.delete(room.locator);
4123
+ }
4124
+ list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
4125
+ }
3859
4126
  async function scoped(result, room, local) {
3860
4127
  if (result.scope === 'content' && !result.retryable) {
3861
4128
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
@@ -3869,14 +4136,14 @@ export function composeRuntime(options) {
3869
4136
  });
3870
4137
  let scopedAway = room;
3871
4138
  if (result.scope === 'model')
3872
- await retainModel(local, room, 'collection', {
4139
+ await retainModel(local, room, {
3873
4140
  status: 'unavailable',
3874
4141
  reason: 'not-readable',
3875
4142
  });
3876
4143
  else if (result.scope !== 'operation') {
3877
4144
  unreadable.add(room.locator);
3878
4145
  scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
3879
- await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
4146
+ await retainModel(local, scopedAway, modelOf(room.locator));
3880
4147
  }
3881
4148
  if (!continuing())
3882
4149
  return;
@@ -3926,7 +4193,7 @@ export function composeRuntime(options) {
3926
4193
  return false;
3927
4194
  }
3928
4195
  if (prior !== undefined)
3929
- await retainModel(local, room, 'collection', modelOf(room.locator));
4196
+ await retainModel(local, room, modelOf(room.locator));
3930
4197
  if (discoveryPaused || authorization === undefined)
3931
4198
  return false;
3932
4199
  return room.replica.admission === 'admitted';
@@ -3939,10 +4206,8 @@ export function composeRuntime(options) {
3939
4206
  await withhold(withheld, room, local, progress);
3940
4207
  }
3941
4208
  function revoked(locator) {
3942
- return ['collection', 'typed-selection'].some((attached) => {
3943
- const model = modelOf(locator, attached);
3944
- return model.status === 'unavailable' && model.reason === 'revoked';
3945
- });
4209
+ const model = modelOf(locator);
4210
+ return model.status === 'unavailable' && model.reason === 'revoked';
3946
4211
  }
3947
4212
  async function loadRoom(room, local, progress, live) {
3948
4213
  if (!live() || authorization === undefined)
@@ -3992,6 +4257,8 @@ export function composeRuntime(options) {
3992
4257
  catch {
3993
4258
  result = { kind: 'transport-uncertainty' };
3994
4259
  }
4260
+ if (result.kind === 'discovery')
4261
+ roomsNamed = true;
3995
4262
  if (!live()) {
3996
4263
  progress.complete = false;
3997
4264
  notifyReady();
@@ -3999,12 +4266,7 @@ export function composeRuntime(options) {
3999
4266
  }
4000
4267
  journal.record(`connection discover ${result.kind}`);
4001
4268
  if (result.kind === 'transport-uncertainty') {
4002
- for (const room of rooms.values()) {
4003
- await retainModel(local, room, 'collection', cached(modelOf(room.locator)));
4004
- if (room.typedSelection !== null) {
4005
- await retainModel(local, room, 'typed-selection', cached(modelOf(room.locator, 'typed-selection')));
4006
- }
4007
- }
4269
+ await settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
4008
4270
  progress.reachable = false;
4009
4271
  progress.complete = false;
4010
4272
  notifyReady();
@@ -4059,6 +4321,7 @@ export function composeRuntime(options) {
4059
4321
  limits = result.providerLimits;
4060
4322
  await local.fact('providerLimits', limits);
4061
4323
  await local.fact('capabilities', capabilities);
4324
+ conversations.declared();
4062
4325
  }
4063
4326
  const signedIn = result.member ?? null;
4064
4327
  if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
@@ -4116,7 +4379,7 @@ export function composeRuntime(options) {
4116
4379
  function sync(attempt) {
4117
4380
  return task(OPENING_LANE, async (live) => {
4118
4381
  if (!live()) {
4119
- notifyReady();
4382
+ readyFromStore();
4120
4383
  return;
4121
4384
  }
4122
4385
  const progress = { complete: true, reachable: true };
@@ -4194,6 +4457,7 @@ export function composeRuntime(options) {
4194
4457
  });
4195
4458
  }
4196
4459
  function closedSocket(revoked, lost) {
4460
+ notifyDiscovered();
4197
4461
  const opened = socketOpen;
4198
4462
  socketOpen = false;
4199
4463
  connectionLost();
@@ -4237,13 +4501,9 @@ export function composeRuntime(options) {
4237
4501
  const local = await openLocal();
4238
4502
  const reloaded = await replacing;
4239
4503
  if (progress.reachable) {
4240
- for (const room of discovered.values()) {
4241
- if (replicas.has(room.locator) &&
4242
- !expired.includes(room.locator) &&
4243
- !reloading.includes(room.locator)) {
4244
- await confirmModels(room, local);
4245
- }
4246
- }
4504
+ await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
4505
+ !expired.includes(room.locator) &&
4506
+ !reloading.includes(room.locator)), 'current', local);
4247
4507
  }
4248
4508
  const failed = reloaded.find((outcome) => outcome.status === 'rejected');
4249
4509
  if (failed !== undefined)
@@ -4284,12 +4544,30 @@ export function composeRuntime(options) {
4284
4544
  return [];
4285
4545
  }
4286
4546
  }
4287
- async function confirmModels(room, local) {
4288
- for (const attached of ['collection', 'typed-selection']) {
4289
- const model = modelOf(room.locator, attached);
4290
- if (model.status === 'cached') {
4291
- await retainModel(local, room, attached, { ...model, status: 'current' });
4547
+ async function settleModels(held, status, local) {
4548
+ const owed = (room) => {
4549
+ const model = modelOf(room.locator);
4550
+ return model.status !== 'unavailable' && model.status !== status;
4551
+ };
4552
+ const settled = await exclusive(async () => {
4553
+ if (!continuing())
4554
+ return [];
4555
+ const unchanged = held.filter((room) => owed(room) && !declarationRefusals.has(room.locator) && declaredAsHeld(room));
4556
+ for (const room of unchanged) {
4557
+ const next = { ...modelOf(room.locator), status };
4558
+ const replica = replicas.get(room.locator);
4559
+ if (replica !== undefined)
4560
+ replicas.set(room.locator, { ...replica, record: { ...replica.record, model: next } });
4561
+ heldModel(room, next);
4292
4562
  }
4563
+ if (unchanged.length > 0)
4564
+ emitRooms();
4565
+ return unchanged;
4566
+ });
4567
+ for (const room of held) {
4568
+ if (settled.includes(room) || !owed(room))
4569
+ continue;
4570
+ await retainModel(local, room, { ...modelOf(room.locator), status });
4293
4571
  }
4294
4572
  }
4295
4573
  function submitLater(locator) {
@@ -4402,8 +4680,6 @@ export function composeRuntime(options) {
4402
4680
  if (repairable.has(locator))
4403
4681
  repairLater(locator, {});
4404
4682
  await conversations.reconcile();
4405
- if (status.providerUnreachable)
4406
- conversations.reconnected();
4407
4683
  const reading = { complete: true, reachable: true };
4408
4684
  await conversations.catchUp(reading);
4409
4685
  if (progress.reachable && reading.reachable)
@@ -4511,6 +4787,7 @@ export function composeRuntime(options) {
4511
4787
  parts: async (key, count) => (await openLocal()).receivedParts(key, count),
4512
4788
  keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
4513
4789
  drop: async (keys) => (await openLocal()).dropReceived(keys),
4790
+ ahead,
4514
4791
  };
4515
4792
  function observeStream() {
4516
4793
  const stream = options.adapter.roomStream;
@@ -4523,6 +4800,8 @@ export function composeRuntime(options) {
4523
4800
  socketOpen = true;
4524
4801
  connectionLive();
4525
4802
  generation += 1;
4803
+ for (const admit of installWaiters.splice(0))
4804
+ admit();
4526
4805
  reads.opened();
4527
4806
  conversations.reopened();
4528
4807
  recovery.resume();
@@ -4542,7 +4821,7 @@ export function composeRuntime(options) {
4542
4821
  const held = replicas.get(locator);
4543
4822
  if (held === undefined || paths.length === 0)
4544
4823
  return new Map();
4545
- const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
4824
+ const entries = await ahead(async () => (await openLocal()).entries(held.record.key, 'confirmed', paths));
4546
4825
  return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
4547
4826
  }
4548
4827
  function writesHeld() {
@@ -4613,6 +4892,7 @@ export function composeRuntime(options) {
4613
4892
  if (closed)
4614
4893
  return;
4615
4894
  closed = true;
4895
+ notifyDiscovered();
4616
4896
  if (probing !== null)
4617
4897
  clearInterval(probing);
4618
4898
  for (const resolve of shownWaiters.splice(0))
@@ -4628,6 +4908,7 @@ export function composeRuntime(options) {
4628
4908
  conversations.close();
4629
4909
  void recovery.retire();
4630
4910
  roomListeners.clear();
4911
+ unlistedRooms.clear();
4631
4912
  refusalListeners.clear();
4632
4913
  storageLossListeners.clear();
4633
4914
  statusListeners.clear();
@@ -4653,9 +4934,9 @@ export function composeRuntime(options) {
4653
4934
  observeStream();
4654
4935
  void openLocal().then(() => {
4655
4936
  if (capabilities.length > 0)
4656
- notifyReady();
4937
+ readyFromStore();
4657
4938
  }, () => {
4658
- notifyReady();
4939
+ readyFromStore();
4659
4940
  });
4660
4941
  if (options.adapter.roomStream === undefined)
4661
4942
  void sync('unstreamed');
@@ -4806,9 +5087,10 @@ export function composeRuntime(options) {
4806
5087
  return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
4807
5088
  }
4808
5089
  if (topic === 'rooms') {
4809
- roomListeners.add(deliver);
4810
- deliver(listed());
5090
+ unlistedRooms.add(deliver);
5091
+ emitKnownRooms();
4811
5092
  return () => {
5093
+ unlistedRooms.delete(deliver);
4812
5094
  roomListeners.delete(deliver);
4813
5095
  };
4814
5096
  }
@@ -4862,7 +5144,17 @@ export function composeRuntime(options) {
4862
5144
  return openLocal().then(() => undefined);
4863
5145
  },
4864
5146
  rooms() {
4865
- return listed();
5147
+ if (closed)
5148
+ return Promise.resolve([]);
5149
+ return new Promise((settle) => {
5150
+ const answer = (value) => {
5151
+ unlistedRooms.delete(answer);
5152
+ roomListeners.delete(answer);
5153
+ settle(value);
5154
+ };
5155
+ unlistedRooms.add(answer);
5156
+ emitKnownRooms();
5157
+ });
4866
5158
  },
4867
5159
  query: ((input) => rowsQuery(planned(input, null))),
4868
5160
  search: (input) => searched(input, null, []),
@@ -4872,7 +5164,7 @@ export function composeRuntime(options) {
4872
5164
  typedSelection: rooms.get(locator)?.typedSelection === undefined ||
4873
5165
  rooms.get(locator)?.typedSelection === null
4874
5166
  ? null
4875
- : modelTypes(modelOf(locator, 'typed-selection')),
5167
+ : modelTypes(builtIn),
4876
5168
  };
4877
5169
  },
4878
5170
  roomDraft: {
@@ -4951,6 +5243,8 @@ export function composeRuntime(options) {
4951
5243
  part: readPart,
4952
5244
  read: readBinaryPath,
4953
5245
  link: linkBinaryPath,
5246
+ show: showBinaryPath,
5247
+ attach: attachBinary,
4954
5248
  },
4955
5249
  async call(name, input) {
4956
5250
  await ready;
@@ -5005,6 +5299,7 @@ export function composeRuntime(options) {
5005
5299
  lockedRooms,
5006
5300
  watchLocks,
5007
5301
  located,
5302
+ room: (locator) => listed().find((one) => one.locator === locator),
5008
5303
  };
5009
5304
  return {
5010
5305
  md,