@agentshouse/mdruntime 0.1.0-ccd098ff75bbf → 0.1.0-cce1d81ef3d6d

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 (110) hide show
  1. package/dist/adapter.d.ts +65 -48
  2. package/dist/adapter.d.ts.map +1 -1
  3. package/dist/attached-model.d.ts.map +1 -1
  4. package/dist/attached-model.js +18 -1
  5. package/dist/bootstrap.d.ts +9 -11
  6. package/dist/bootstrap.d.ts.map +1 -1
  7. package/dist/bootstrap.js +97 -85
  8. package/dist/client.d.ts +6 -7
  9. package/dist/client.d.ts.map +1 -1
  10. package/dist/client.js +52 -6
  11. package/dist/collection.d.ts +5 -3
  12. package/dist/collection.d.ts.map +1 -1
  13. package/dist/collection.js +23 -17
  14. package/dist/compile.d.ts.map +1 -1
  15. package/dist/compile.js +65 -19
  16. package/dist/connection.d.ts +5 -4
  17. package/dist/connection.d.ts.map +1 -1
  18. package/dist/connection.js +925 -900
  19. package/dist/conversation.d.ts +11 -11
  20. package/dist/conversation.d.ts.map +1 -1
  21. package/dist/conversation.js +204 -129
  22. package/dist/durability.d.ts +0 -1
  23. package/dist/durability.d.ts.map +1 -1
  24. package/dist/entry.d.ts +1 -0
  25. package/dist/entry.d.ts.map +1 -1
  26. package/dist/entry.js +1 -1
  27. package/dist/file.d.ts +2 -11
  28. package/dist/file.d.ts.map +1 -1
  29. package/dist/file.js +6 -4
  30. package/dist/fulltext.d.ts.map +1 -1
  31. package/dist/fulltext.js +8 -6
  32. package/dist/host.d.ts +0 -2
  33. package/dist/host.d.ts.map +1 -1
  34. package/dist/host.js +10 -11
  35. package/dist/index.d.ts +6 -6
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +2 -1
  38. package/dist/media.d.ts +8 -0
  39. package/dist/media.d.ts.map +1 -0
  40. package/dist/media.js +218 -0
  41. package/dist/memory.d.ts +8 -5
  42. package/dist/memory.d.ts.map +1 -1
  43. package/dist/memory.js +174 -82
  44. package/dist/model.d.ts +6 -1
  45. package/dist/model.d.ts.map +1 -1
  46. package/dist/model.js +6 -9
  47. package/dist/observation.d.ts +1 -0
  48. package/dist/observation.d.ts.map +1 -1
  49. package/dist/observation.js +1 -1
  50. package/dist/outcome.d.ts +13 -5
  51. package/dist/outcome.d.ts.map +1 -1
  52. package/dist/outcome.js +16 -2
  53. package/dist/paths.d.ts +1 -0
  54. package/dist/paths.d.ts.map +1 -1
  55. package/dist/paths.js +4 -0
  56. package/dist/query.d.ts +6 -0
  57. package/dist/query.d.ts.map +1 -1
  58. package/dist/reach.d.ts +8 -1
  59. package/dist/reach.d.ts.map +1 -1
  60. package/dist/reach.js +49 -22
  61. package/dist/readiness.d.ts +3 -15
  62. package/dist/readiness.d.ts.map +1 -1
  63. package/dist/readiness.js +4 -10
  64. package/dist/recovery.d.ts +1 -0
  65. package/dist/recovery.d.ts.map +1 -1
  66. package/dist/recovery.js +26 -0
  67. package/dist/refresh.d.ts +5 -4
  68. package/dist/refresh.d.ts.map +1 -1
  69. package/dist/refresh.js +4 -4
  70. package/dist/replica.d.ts +2 -3
  71. package/dist/replica.d.ts.map +1 -1
  72. package/dist/replica.js +2 -5
  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 +4 -2
  77. package/dist/rows.d.ts.map +1 -1
  78. package/dist/rows.js +29 -20
  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/shared.d.ts +0 -1
  83. package/dist/shared.d.ts.map +1 -1
  84. package/dist/shared.js +0 -10
  85. package/dist/status.d.ts +4 -3
  86. package/dist/status.d.ts.map +1 -1
  87. package/dist/status.js +3 -1
  88. package/dist/store.d.ts +18 -32
  89. package/dist/store.d.ts.map +1 -1
  90. package/dist/store.js +25 -60
  91. package/dist/stored.d.ts +9 -24
  92. package/dist/stored.d.ts.map +1 -1
  93. package/dist/stored.js +207 -204
  94. package/dist/tables.d.ts +16 -45
  95. package/dist/tables.d.ts.map +1 -1
  96. package/dist/tables.js +7 -53
  97. package/dist/transfer.d.ts +0 -5
  98. package/dist/transfer.d.ts.map +1 -1
  99. package/dist/validation.d.ts +3 -4
  100. package/dist/validation.d.ts.map +1 -1
  101. package/dist/validation.js +35 -96
  102. package/dist/wire.d.ts +8 -8
  103. package/dist/wire.d.ts.map +1 -1
  104. package/dist/wire.js +2 -2
  105. package/dist/words.d.ts.map +1 -1
  106. package/dist/words.js +8 -2
  107. package/package.json +2 -2
  108. package/dist/reads.d.ts +0 -11
  109. package/dist/reads.d.ts.map +0 -1
  110. package/dist/reads.js +0 -100
@@ -1,4 +1,4 @@
1
- import { escapePointer, parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
1
+ import { parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
2
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';
@@ -12,18 +12,18 @@ import { bootstrapRoom } from './bootstrap.js';
12
12
  import { changeSet, mergeChangeSets } from './change-set.js';
13
13
  import { createConversationStore, } from './conversation.js';
14
14
  import { leaving as leavingIn, partnerOf, queuedFile, } from './change.js';
15
- import { available, collectionOf, unreadable as unreadableIn, validRoot, } from './collection.js';
15
+ import { available, collectionOf, composedOf, validRoot, } from './collection.js';
16
16
  import { runtimeError } from './failure.js';
17
17
  import { createLog } from './log.js';
18
18
  import { binaryRecord } from './binary.js';
19
+ import { attachedFile } from './file.js';
19
20
  import { openedFiles } from './opened.js';
20
- import { ABSENT_TARGET, absentBinary, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
21
+ import { ABSENT_TARGET, absentBinary, attachAnswer, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, OFFLINE, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
21
22
  import { createRecovery } from './recovery.js';
22
- import { doorReads } from './reads.js';
23
23
  import { advancement } from './refresh.js';
24
24
  import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
25
- import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, sameMembership, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
26
- import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
25
+ import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
26
+ import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, readSavedParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
27
27
  import { same, saving, STARTING, transferring, } from './status.js';
28
28
  import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
29
29
  import { readinessOf, } from './readiness.js';
@@ -31,9 +31,9 @@ import { FencedMap } from './fence.js';
31
31
  import { sameBinary } from './overlay.js';
32
32
  import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
33
33
  import { identityTime, roomDraftRevision } from './identity.js';
34
- import { beneath, normalize } from './paths.js';
34
+ import { ancestors, beneath, normalize } from './paths.js';
35
35
  import { openRuntimeStore, } from './store.js';
36
- import { changedCleanly, sameComposition, validatedCollection, } from './validation.js';
36
+ import { validatedNeighbourhood } from './validation.js';
37
37
  import { unconfirmed } from './transfer.js';
38
38
  import { governs, holds, narrowed, planReach, } from './reach.js';
39
39
  function within(root, path) {
@@ -54,40 +54,37 @@ const CONVERSATION_LANE = 'conversations';
54
54
  const ROOM_READS = 256;
55
55
  const ROOM_ATTEMPTS = 16;
56
56
  const ERASE_BATCH = 512;
57
- const REDERIVE_BATCH = 256;
58
- const WALK_BYTES = 1 << 20;
57
+ const DERIVE_BYTES = 1 << 23;
58
+ const DERIVE_FLOOR = 1 << 18;
59
+ // An index build aims at this much database work, so a Save, a query or the next part's progress waits behind at most one short build.
60
+ const DERIVE_AIM_MS = 250;
61
+ // A build past its floor of text stops after a second and a half of work, its full-text writes included, so a Room whose text costs ten times the usual holds a Save, a query or the next part's progress behind no longer build.
62
+ const DERIVE_LONGEST_MS = 1_500;
59
63
  const QUERY_BYTES = 1 << 20;
60
64
  const STAGE_BYTES = 1 << 20;
61
65
  const STAGE_FLOOR = 1 << 16;
62
66
  // A batch aims at this much database work, so a Save or a query waits behind at most one short write.
63
67
  const STAGE_AIM_MS = 50;
64
- const PARTS_HELD = 4;
65
- // 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.
68
+ const STREAMS_HELD = 4;
69
+ const NOTHING_HELD = { bytes: 0 };
70
+ const QUEUED_TEXT = 1 << 23;
71
+ // Bytes still arriving are reported at most this often, so readiness is not rebuilt for every chunk the network hands over while a slow link still moves the figure.
66
72
  const ARRIVAL_REPORT_MS = 250;
67
- const INSTALL_STEPS = [
68
- 'writing',
69
- 'downloading',
70
- 'replaying',
71
- 'listing',
72
- ];
73
- const CLEAN_CHANGES = 2_000;
74
73
  // A Room that locks offline stays readable this long after the connection drops, so a laptop waking or changing network keeps working while a disconnected device locks soon after.
75
74
  const LOCK_GRACE_MS = 120_000;
76
75
  function operationOf(input) {
77
76
  const operation = input?.operation_id;
78
77
  return typeof operation === 'string' ? operation : null;
79
78
  }
80
- function abandonedOn(signal) {
81
- return new Promise((resolve) => {
82
- if (signal.aborted)
83
- resolve(null);
84
- else
85
- signal.addEventListener('abort', () => resolve(null), { once: true });
79
+ function readUntil(read, signal) {
80
+ if (signal.aborted)
81
+ return Promise.resolve({ kind: 'transport-uncertainty' });
82
+ return new Promise((resolve, reject) => {
83
+ const stop = () => resolve({ kind: 'transport-uncertainty' });
84
+ signal.addEventListener('abort', stop, { once: true });
85
+ read.then(resolve, reject).finally(() => signal.removeEventListener('abort', stop));
86
86
  });
87
87
  }
88
- function roomLane(locator) {
89
- return `room ${locator}`;
90
- }
91
88
  function pendingKey(locator, id) {
92
89
  return `${locator}\u0000${id}`;
93
90
  }
@@ -103,11 +100,14 @@ export function composeRuntime(options) {
103
100
  const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
104
101
  let discoveries = 0;
105
102
  const replicas = new FencedMap(isFenced);
103
+ const reopened = new Set();
104
+ const usableWhenHeld = options.usableWhenHeld ?? [];
106
105
  const queuesWithoutReplica = new FencedMap(isFenced);
107
106
  const pendings = new Map();
108
107
  const seeds = new Map();
109
108
  const seedWatchers = new Map();
110
109
  const roomDrafts = new FencedMap(isFenced);
110
+ const attachedFiles = new FencedMap(isFenced);
111
111
  const presentations = new Map();
112
112
  let limits = [];
113
113
  let capabilities = [];
@@ -119,11 +119,19 @@ export function composeRuntime(options) {
119
119
  const liveModels = new FencedMap(isFenced);
120
120
  const invalidModels = new Set();
121
121
  const discovered = new Map();
122
- const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
122
+ let inFlight = new AbortController();
123
+ const refreshed = options.adapter.refresh?.bind(options.adapter);
124
+ const reads = {
125
+ bootstrap: (authorized, request) => readUntil(options.adapter.bootstrap(authorized, request), inFlight.signal),
126
+ ...(refreshed === undefined
127
+ ? {}
128
+ : {
129
+ refresh: (authorized, request) => readUntil(refreshed(authorized, request), inFlight.signal),
130
+ }),
131
+ reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
132
+ };
123
133
  let roomsListed = false;
124
134
  const scopedRefusals = new Map();
125
- const sourceRefusals = new Map();
126
- const declarationRefusals = new Set();
127
135
  function modelOf(locator) {
128
136
  return models.get(locator) ?? NO_MODEL;
129
137
  }
@@ -146,10 +154,10 @@ export function composeRuntime(options) {
146
154
  let closed = false;
147
155
  let stopping = false;
148
156
  let withdrawStream;
149
- const announced = new Map();
150
157
  let underway = null;
151
158
  let streamed = null;
152
159
  let handed = false;
160
+ let naming = 0;
153
161
  const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
154
162
  const repairing = new Set();
155
163
  const calling = new Set();
@@ -160,16 +168,16 @@ export function composeRuntime(options) {
160
168
  let recorded = false;
161
169
  const stale = new Set();
162
170
  let generation = 0;
163
- let discovering = null;
164
- let opened = null;
165
- const owed = new Set();
166
- const reloadsDue = new Map();
171
+ const reloadsDue = new Set();
167
172
  const lanes = new Map();
168
- let roomReads = { generation, free: ROOM_READS, waiting: [] };
173
+ const roomLanes = new Map();
174
+ let roomWork = 0;
175
+ const roomReads = { free: ROOM_READS, waiting: [] };
169
176
  const working = new Set();
170
177
  let mutations = Promise.resolve();
171
178
  let gathering = null;
172
179
  const roomListeners = new Set();
180
+ const unlistedRooms = new Set();
173
181
  const refusalListeners = new Set();
174
182
  const storageLossListeners = new Set();
175
183
  const statusListeners = new Set();
@@ -180,6 +188,7 @@ export function composeRuntime(options) {
180
188
  let withdrawConversations;
181
189
  let currencyQueued = null;
182
190
  let socketOpen = false;
191
+ let announced = false;
183
192
  const confirmed = new Set();
184
193
  let lockGrace = null;
185
194
  const lockListeners = new Set();
@@ -213,31 +222,32 @@ export function composeRuntime(options) {
213
222
  let readinessQueued = false;
214
223
  const catching = new Set();
215
224
  const transfers = new FencedMap(isFenced);
216
- const installs = new Map();
225
+ const installs = new Set();
217
226
  const arrivals = new Map();
227
+ const streaming = new Set();
228
+ const underived = new Map();
229
+ const unindexed = new Map();
230
+ const startedOver = new Map();
218
231
  let arrivalReported = 0;
219
- let onScreen = options.shell.onScreen;
220
- const shownWaiters = [];
221
- let partsFree = PARTS_HELD;
222
- const partWaiters = [];
223
- let stageBytes = STAGE_FLOOR * 4;
224
- let installWrites = Promise.resolve();
232
+ let streamsFree = STREAMS_HELD;
233
+ let queuedText = 0;
234
+ const installWaiters = [];
235
+ const stageBudget = {
236
+ bytes: STAGE_FLOOR * 4,
237
+ floor: STAGE_FLOOR,
238
+ ceiling: STAGE_BYTES,
239
+ aim: STAGE_AIM_MS,
240
+ };
241
+ const deriveBudget = {
242
+ bytes: DERIVE_FLOOR * 4,
243
+ floor: DERIVE_FLOOR,
244
+ ceiling: DERIVE_BYTES,
245
+ aim: DERIVE_AIM_MS,
246
+ };
247
+ const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
225
248
  const conversations = createConversationStore({
226
- agents: declaredAgents,
227
- documents: (declaration, after) => rowsQuery(planOf({
228
- rooms: [declaration.locator],
229
- types: [declaration.type],
230
- select: {
231
- fields: [
232
- declaration.identity,
233
- declaration.name,
234
- ...(declaration.retired === undefined ? [] : [declaration.retired]),
235
- ].map((field) => `/${escapePointer(field)}`),
236
- },
237
- limit: MAXIMUM_WINDOW,
238
- ...(after === undefined ? {} : { after }),
239
- }, roomFacts())),
240
249
  call: (name, input) => invokeAction(name, input),
250
+ history: (request) => readConversationHistory(request),
241
251
  durably,
242
252
  stream: () => options.adapter.conversations,
243
253
  publish: publishChangeSet,
@@ -249,10 +259,19 @@ export function composeRuntime(options) {
249
259
  });
250
260
  const journal = createLog();
251
261
  const keptFiles = openedFiles(database, isFenced);
252
- let notifyReady = () => { };
262
+ let readyFromStore = () => { };
253
263
  const ready = new Promise((resolve) => {
254
- notifyReady = resolve;
264
+ readyFromStore = resolve;
265
+ });
266
+ let notifyDiscovered = () => { };
267
+ const firstDiscovery = new Promise((resolve) => {
268
+ notifyDiscovered = resolve;
255
269
  });
270
+ let roomsNamed = false;
271
+ function notifyReady() {
272
+ notifyDiscovered();
273
+ readyFromStore();
274
+ }
256
275
  function rememberRoomDraft(record) {
257
276
  const room = roomDrafts.get(record.locator) ?? new Map();
258
277
  room.set(record.path, record);
@@ -339,7 +358,9 @@ export function composeRuntime(options) {
339
358
  }
340
359
  const file = parts.file === undefined
341
360
  ? undefined
342
- : await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
361
+ : await heldFile(parts.file.blob, parts.file.base === undefined
362
+ ? binaryBase(await visibleOf(room, [path]), path)
363
+ : parts.file.base);
343
364
  const record = {
344
365
  locator: room,
345
366
  path,
@@ -374,6 +395,20 @@ export function composeRuntime(options) {
374
395
  (queuesWithoutReplica.size > 0 ||
375
396
  [...replicas.values()].some((replica) => replica.queue.length > 0)));
376
397
  }
398
+ function unsent() {
399
+ const changed = new Map();
400
+ for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
401
+ for (const { batch, state } of entriesOf(room)) {
402
+ if (state === 'local')
403
+ continue;
404
+ for (const { path } of batch.changes)
405
+ changed.set(`${room}\u0000${path}`, { room, path });
406
+ }
407
+ }
408
+ return [...changed]
409
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
410
+ .map(([, change]) => change);
411
+ }
377
412
  function attention() {
378
413
  const attentionRequired = [];
379
414
  for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
@@ -385,16 +420,9 @@ export function composeRuntime(options) {
385
420
  returned: structuredClone(record.returned),
386
421
  });
387
422
  }
388
- for (const [room, refusal] of sourceRefusals)
389
- attentionRequired.push({ room, refusal: structuredClone(refusal) });
390
423
  attentionRequired.sort((left, right) => {
391
424
  if (left.room !== right.room)
392
425
  return left.room < right.room ? -1 : 1;
393
- if ('refusal' in left || 'refusal' in right) {
394
- if ('refusal' in left && 'refusal' in right)
395
- return 0;
396
- return 'refusal' in left ? -1 : 1;
397
- }
398
426
  if (left.path !== right.path)
399
427
  return left.path < right.path ? -1 : 1;
400
428
  return 0;
@@ -406,7 +434,7 @@ export function composeRuntime(options) {
406
434
  if (reset === null || submission.reset < reset)
407
435
  reset = submission.reset;
408
436
  }
409
- return { attentionRequired, reset };
437
+ return { attentionRequired, unsent: unsent(), reset };
410
438
  }
411
439
  function documentsIn(locator) {
412
440
  return replicas.get(locator)?.count ?? 0;
@@ -495,11 +523,26 @@ export function composeRuntime(options) {
495
523
  const value = listed();
496
524
  for (const listener of roomListeners)
497
525
  listener(value);
526
+ emitKnownRooms();
527
+ }
528
+ function emitKnownRooms() {
529
+ conversations.learned();
530
+ if (unlistedRooms.size === 0)
531
+ return;
532
+ if (!roomsListed && rooms.size === 0 && refusal === null && !status.providerUnreachable)
533
+ return;
534
+ const value = listed();
535
+ for (const listener of [...unlistedRooms]) {
536
+ unlistedRooms.delete(listener);
537
+ roomListeners.add(listener);
538
+ listener(value);
539
+ }
498
540
  }
499
541
  function emitRefusal() {
500
542
  queueReadiness();
501
543
  for (const listener of refusalListeners)
502
544
  listener(refusal);
545
+ emitKnownRooms();
503
546
  }
504
547
  function emitStorageLoss() {
505
548
  queueReadiness();
@@ -520,6 +563,7 @@ export function composeRuntime(options) {
520
563
  if (closed)
521
564
  return;
522
565
  queueReadiness();
566
+ emitKnownRooms();
523
567
  const next = status.state === 'synced' && !conversations.current()
524
568
  ? { ...status, state: 'syncing' }
525
569
  : status;
@@ -533,9 +577,6 @@ export function composeRuntime(options) {
533
577
  if (localSaves)
534
578
  return Object.freeze({ batch, state: 'local' });
535
579
  const state = pendings.get(pendingKey(locator, batch.id));
536
- if (state?.accepted !== undefined) {
537
- return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
538
- }
539
580
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
540
581
  }
541
582
  function entriesOf(locator) {
@@ -563,11 +604,14 @@ export function composeRuntime(options) {
563
604
  if (held === undefined || closed || held.indexed)
564
605
  return;
565
606
  const key = held.record.key;
566
- const done = await exclusive(async () => (await openLocal()).rederive(key, REDERIVE_BATCH));
607
+ const done = await exclusive(async () => {
608
+ const local = await openLocal();
609
+ return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR));
610
+ });
567
611
  const now = replicas.get(locator);
568
612
  if (now?.record.key !== key)
569
613
  continue;
570
- if (!done)
614
+ if (!done.done)
571
615
  continue;
572
616
  replicas.set(locator, { ...now, indexed: true });
573
617
  publishChangeSet(changeSet(locator, { documents: null }));
@@ -645,7 +689,7 @@ export function composeRuntime(options) {
645
689
  coverage.push({
646
690
  room: locator,
647
691
  generation: replica.record.transfer,
648
- current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
692
+ current: !(catching.has(locator) || transfers.has(locator)),
649
693
  });
650
694
  read.push({ locator, key: replica.record.key });
651
695
  for (const batch of replica.queue)
@@ -672,8 +716,6 @@ export function composeRuntime(options) {
672
716
  return { listed: named.every((room) => replicas.has(room)), rooms: named };
673
717
  }
674
718
  function roomState(locator) {
675
- if (sourceRefusals.has(locator))
676
- return 'refused';
677
719
  const listedRoom = discovered.get(locator);
678
720
  if (roomsListed &&
679
721
  (listedRoom === undefined ||
@@ -688,7 +730,7 @@ export function composeRuntime(options) {
688
730
  }
689
731
  function conversationCoverage(reach) {
690
732
  const declared = reach === null || reach.conversations;
691
- if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
733
+ if (!declared || options.adapter.conversations === undefined)
692
734
  return 'undeclared';
693
735
  if (!conversations.known() || !conversations.current())
694
736
  return 'acquiring';
@@ -703,67 +745,47 @@ export function composeRuntime(options) {
703
745
  ? 'connected'
704
746
  : 'connecting';
705
747
  }
706
- function refusedIn(locators) {
707
- return locators.flatMap((room) => {
708
- const held = sourceRefusals.get(room);
709
- if (held === undefined)
710
- return [];
711
- const { file, location } = held.fields;
712
- return [
713
- {
714
- room,
715
- name: discovered.get(room)?.name ?? room,
716
- file: typeof file === 'string' ? file : '',
717
- rule: held.code,
718
- location: typeof location === 'string' ? location : null,
719
- },
720
- ];
721
- });
748
+ function bytesOf(room, underway) {
749
+ const transfer = transfers.get(room);
750
+ if (transfer !== undefined) {
751
+ const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
752
+ return {
753
+ done: transfer.done.bytes - Math.ceil(owed),
754
+ total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
755
+ };
756
+ }
757
+ const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
758
+ return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
722
759
  }
723
760
  function installationOf(coverage) {
724
761
  const rooms = { done: 0, total: 0 };
725
- const parts = { done: 0, total: 0 };
726
762
  const bytes = { done: 0, total: 0 };
727
- const received = { done: 0, total: 0 };
728
- const steps = new Set();
729
- let waiting = false;
763
+ let installing = 0;
764
+ let downloading = false;
730
765
  for (const { room, state } of coverage) {
731
766
  if (state === 'unadmitted')
732
767
  continue;
733
768
  rooms.total += 1;
734
769
  if (state === 'complete')
735
770
  rooms.done += 1;
736
- const transfer = transfers.get(room);
737
- const held = installs.get(room);
738
- const inventory = transfer?.inventory ?? replicas.get(room)?.record.inventory ?? null;
739
- if (inventory !== null) {
740
- const done = transfer?.done ?? inventory;
741
- const share = held?.share ?? null;
742
- parts.total += inventory.parts;
743
- bytes.total += inventory.bytes;
744
- parts.done += done.parts;
745
- bytes.done += done.bytes;
746
- if (share !== null && share.entries > 0) {
747
- bytes.done += Math.floor((share.bytes * share.written) / share.entries);
748
- }
749
- received.total += inventory.bytes;
750
- received.done += done.bytes + (share?.bytes ?? 0);
751
- for (const arrived of arrivals.get(room)?.values() ?? [])
752
- received.done += arrived;
753
- }
754
- const installing = held !== undefined && state !== 'complete';
755
- if (installing)
756
- steps.add(held.step);
757
- waiting ||= installing || state === 'acquiring';
758
- }
759
- return {
760
- step: INSTALL_STEPS.find((step) => steps.has(step)) ?? null,
761
- paused: waiting && !onScreen ? 'hidden' : null,
762
- rooms,
763
- parts,
764
- bytes,
765
- received,
766
- };
771
+ const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
772
+ if (underway)
773
+ installing += 1;
774
+ if (underway && (streaming.has(room) || !arrivals.has(room)))
775
+ downloading = true;
776
+ const now = bytesOf(room, underway);
777
+ const earlier = underway ? startedOver.get(room) : undefined;
778
+ bytes.done += Math.max(now.done, earlier?.done ?? 0);
779
+ bytes.total += Math.max(now.total, earlier?.total ?? 0);
780
+ }
781
+ const step = installing === 0
782
+ ? null
783
+ : downloading
784
+ ? 'downloading'
785
+ : bytes.done < bytes.total
786
+ ? 'writing'
787
+ : 'replaying';
788
+ return { step, rooms, bytes };
767
789
  }
768
790
  function readinessFor(port, reach) {
769
791
  const required = requiredRooms(reach);
@@ -787,9 +809,9 @@ export function composeRuntime(options) {
787
809
  house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
788
810
  space: full ? 'full' : port.space,
789
811
  installation: installationOf(coverage),
790
- refused: refusedIn(required.rooms),
791
812
  lost: storageLoss !== null,
792
- established: targets[port.target]?.established === true,
813
+ established: targets[port.target]?.established === true ||
814
+ (usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
793
815
  });
794
816
  }
795
817
  function establish(name) {
@@ -842,7 +864,8 @@ export function composeRuntime(options) {
842
864
  probeEachMinute();
843
865
  }
844
866
  function installedTarget() {
845
- return Object.values(targets).some((target) => target.established);
867
+ return ((usableWhenHeld.length > 0 && reopened.size > 0) ||
868
+ Object.values(targets).some((target) => target.established));
846
869
  }
847
870
  function probeEachMinute() {
848
871
  if (!full || probing !== null || closed || !installedTarget())
@@ -873,8 +896,12 @@ export function composeRuntime(options) {
873
896
  unpreparable.clear();
874
897
  journal.record('space freed');
875
898
  queueReadiness();
876
- if (!closed && authorization !== undefined)
877
- void sync('unstreamed');
899
+ if (closed || authorization === undefined)
900
+ return;
901
+ if (options.adapter.roomStream === undefined)
902
+ void sync('unstreamed', null);
903
+ else
904
+ void resynchronize();
878
905
  }
879
906
  function seedRoomHeld(room, version) {
880
907
  return indexed(room) && reachedBy(room, version);
@@ -1000,6 +1027,10 @@ export function composeRuntime(options) {
1000
1027
  }
1001
1028
  function publishChangeSet(set) {
1002
1029
  applyChangeSet(set);
1030
+ for (const { locator, path } of set.documents) {
1031
+ if (path !== null)
1032
+ attachedFiles.get(locator)?.delete(path);
1033
+ }
1003
1034
  if (set.rooms.length > 0 ||
1004
1035
  [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
1005
1036
  emitRooms();
@@ -1018,6 +1049,8 @@ export function composeRuntime(options) {
1018
1049
  }
1019
1050
  async function refuse(reason, provider = null) {
1020
1051
  discoveryPaused = true;
1052
+ inFlight.abort();
1053
+ inFlight = new AbortController();
1021
1054
  try {
1022
1055
  if (reason === 'revoked-authorization') {
1023
1056
  await exclusive(async () => {
@@ -1035,7 +1068,7 @@ export function composeRuntime(options) {
1035
1068
  locator,
1036
1069
  key: record.key,
1037
1070
  record: storedRecord(record),
1038
- composition: composed(record),
1071
+ composed: composedOf(composed(record)),
1039
1072
  })),
1040
1073
  });
1041
1074
  }
@@ -1102,10 +1135,10 @@ export function composeRuntime(options) {
1102
1135
  providerOwnedPaths: room.providerOwnedPaths,
1103
1136
  roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
1104
1137
  synchronizationAvailable,
1105
- refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
1138
+ refusal: scopedRefusals.get(room.locator) ?? null,
1106
1139
  });
1107
1140
  }
1108
- async function applyDiscovery(found, started, covered) {
1141
+ async function applyDiscovery(found, started, left) {
1109
1142
  const lockedBefore = lockedRooms();
1110
1143
  for (const room of found) {
1111
1144
  const fence = fences.get(room.locator);
@@ -1119,18 +1152,11 @@ export function composeRuntime(options) {
1119
1152
  const listedLocators = new Set(listed.map((room) => room.locator));
1120
1153
  for (const locator of listedLocators)
1121
1154
  scopedRefusals.delete(locator);
1122
- if (covered === undefined)
1155
+ if (left === undefined)
1123
1156
  discovered.clear();
1124
- else
1125
- for (const locator of covered)
1126
- discovered.delete(locator);
1127
1157
  for (const room of listed)
1128
1158
  discovered.set(room.locator, room);
1129
- const unlisted = unlistedHeld(new Set(found.map((room) => room.locator))).filter((locator) => covered === undefined || covered.includes(locator));
1130
- if (options.adapter.exhaustiveDiscovery === true)
1131
- await loseRooms(unlisted);
1132
- else
1133
- unlistedKept(unlisted);
1159
+ await loseRooms(left ?? unlistedHeld(new Set(found.map((room) => room.locator))));
1134
1160
  for (const room of listed) {
1135
1161
  const held = replicas.get(room.locator)?.record;
1136
1162
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -1161,15 +1187,6 @@ export function composeRuntime(options) {
1161
1187
  ]),
1162
1188
  ].filter((locator) => !listed.has(locator));
1163
1189
  }
1164
- function unlistedKept(unlisted) {
1165
- for (const locator of unlisted) {
1166
- const held = replicas.get(locator)?.record;
1167
- if (held !== undefined)
1168
- list(held, false);
1169
- else
1170
- rooms.delete(locator);
1171
- }
1172
- }
1173
1190
  function fence(locators) {
1174
1191
  const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1175
1192
  const removed = fresh.map((locator) => ({
@@ -1186,17 +1203,16 @@ export function composeRuntime(options) {
1186
1203
  queuesWithoutReplica.delete(locator);
1187
1204
  rooms.delete(locator);
1188
1205
  roomDrafts.delete(locator);
1189
- transfers.delete(locator);
1206
+ attachedFiles.delete(locator);
1207
+ untransferred(locator);
1208
+ startedOver.delete(locator);
1190
1209
  catching.delete(locator);
1191
1210
  discovered.delete(locator);
1192
- announced.delete(locator);
1193
1211
  unreadable.delete(locator);
1194
1212
  stale.delete(locator);
1195
1213
  repairable.delete(locator);
1196
1214
  unpreparable.delete(locator);
1197
1215
  scopedRefusals.delete(locator);
1198
- sourceRefusals.delete(locator);
1199
- declarationRefusals.delete(locator);
1200
1216
  models.delete(locator);
1201
1217
  liveModels.delete(locator);
1202
1218
  invalidModels.delete(locator);
@@ -1285,8 +1301,9 @@ export function composeRuntime(options) {
1285
1301
  const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1286
1302
  targets = held.targets;
1287
1303
  transfers.clear();
1288
- for (const [locator, record] of held.transfers)
1289
- transfers.set(locator, record);
1304
+ arrivals.clear();
1305
+ streaming.clear();
1306
+ startedOver.clear();
1290
1307
  limits = held.providerLimits;
1291
1308
  capabilities = held.capabilities;
1292
1309
  member = held.member;
@@ -1296,7 +1313,7 @@ export function composeRuntime(options) {
1296
1313
  presentations.clear();
1297
1314
  for (const record of held.presentation)
1298
1315
  presentations.set(record.app, record.value);
1299
- conversations.hydrate(heldConversations, chatDrafts);
1316
+ conversations.hydrate(heldConversations, chatDrafts, held.conversationList, held.conversationCursor);
1300
1317
  pendings.clear();
1301
1318
  for (const queue of queues) {
1302
1319
  for (const { batch, state } of queue.pending) {
@@ -1304,6 +1321,7 @@ export function composeRuntime(options) {
1304
1321
  }
1305
1322
  }
1306
1323
  replicas.clear();
1324
+ reopened.clear();
1307
1325
  queuesWithoutReplica.clear();
1308
1326
  for (const [locator, queue] of queuedBatches) {
1309
1327
  if (!held.rooms.has(locator))
@@ -1313,6 +1331,7 @@ export function composeRuntime(options) {
1313
1331
  if (fences.has(stored.locator))
1314
1332
  return;
1315
1333
  const record = { ...stored, model: restored(stored.locator, stored.model) };
1334
+ reopened.add(record.locator);
1316
1335
  replicas.set(record.locator, {
1317
1336
  record,
1318
1337
  queue: queuedBatches.get(record.locator) ?? [],
@@ -1323,7 +1342,7 @@ export function composeRuntime(options) {
1323
1342
  });
1324
1343
  const existing = rooms.get(record.locator);
1325
1344
  if (existing === undefined)
1326
- list(record, false);
1345
+ list(record, record.roomVersion !== null);
1327
1346
  else {
1328
1347
  rooms.set(record.locator, {
1329
1348
  ...existing,
@@ -1337,8 +1356,14 @@ export function composeRuntime(options) {
1337
1356
  });
1338
1357
  }
1339
1358
  });
1340
- for (const key of held.orphans)
1341
- await opened.release(key);
1359
+ try {
1360
+ for (const key of held.orphans)
1361
+ await opened.release(key);
1362
+ }
1363
+ catch (error) {
1364
+ if (!refusedForSpace(error))
1365
+ throw error;
1366
+ }
1342
1367
  if (held.erasing || held.orphans.length > 0)
1343
1368
  queueMicrotask(sweep);
1344
1369
  return {
@@ -1540,8 +1565,6 @@ export function composeRuntime(options) {
1540
1565
  function unpairedIn(locator, batch) {
1541
1566
  if (!queueIn(locator).some((held) => held.id === batch.id))
1542
1567
  return false;
1543
- if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
1544
- return false;
1545
1568
  return !leavingIn(batch, locator) || withdrawable(locator)(batch);
1546
1569
  }
1547
1570
  async function together(local, settlements) {
@@ -1626,6 +1649,7 @@ export function composeRuntime(options) {
1626
1649
  key: record.key,
1627
1650
  record: storedRecord(record),
1628
1651
  roots: record.typedSelection?.roots ?? [],
1652
+ composed: composedOf(composition),
1629
1653
  ...settling.commit,
1630
1654
  ...(overrides === undefined
1631
1655
  ? {}
@@ -1709,39 +1733,44 @@ export function composeRuntime(options) {
1709
1733
  models.set(locator, state);
1710
1734
  return { collection: state };
1711
1735
  }
1712
- function publish(local, room, record, landing, live) {
1736
+ function publish(local, room, reached, landing, live) {
1713
1737
  const landed = exclusive(async () => {
1714
1738
  if (!live()) {
1715
1739
  caughtUpTo(room.locator);
1716
1740
  return true;
1717
1741
  }
1718
1742
  const prior = replicas.get(room.locator);
1719
- if (landing.kind === 'refreshed' &&
1720
- (prior === undefined || prior.record.key !== record.key || prior.confirmed !== landing.base)) {
1721
- await local.unrefresh(record.key);
1743
+ if (!stands(room.locator, landing.from)) {
1744
+ if (landing.kind === 'refreshed') {
1745
+ await local.unrefresh(reached.key);
1746
+ return false;
1747
+ }
1748
+ caughtUpTo(room.locator);
1749
+ await abandoned(local, room.locator);
1722
1750
  return false;
1723
1751
  }
1752
+ const record = landing.kind === 'refreshed' && prior !== undefined
1753
+ ? {
1754
+ ...prior.record,
1755
+ roomVersion: reached.roomVersion,
1756
+ ...(landing.remodelled ? { model: reached.model } : {}),
1757
+ }
1758
+ : reached;
1724
1759
  adoptModels(room.locator, record);
1725
1760
  const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
1726
- const roomVersion = record.roomVersion;
1727
- const accepted = queue.filter((batch) => {
1728
- const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
1729
- return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
1730
- });
1731
1761
  let set;
1732
1762
  try {
1733
- set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
1763
+ set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
1734
1764
  ? {
1735
1765
  room: true,
1736
1766
  view: 'refreshed',
1737
- commit: { refreshed: true, composition: composed(record) },
1767
+ commit: { refreshed: true },
1738
1768
  }
1739
1769
  : {
1740
1770
  room: true,
1741
1771
  view: landing.refreshed ? 'refreshed' : 'confirmed',
1742
1772
  commit: {
1743
1773
  published: { from: prior?.record.key ?? null },
1744
- composition: composed(record),
1745
1774
  ...(landing.refreshed ? { refreshed: true } : {}),
1746
1775
  },
1747
1776
  });
@@ -1753,24 +1782,22 @@ export function composeRuntime(options) {
1753
1782
  return true;
1754
1783
  }
1755
1784
  if (landing.kind === 'bootstrapped') {
1756
- transfers.delete(room.locator);
1785
+ untransferred(room.locator);
1786
+ startedOver.delete(room.locator);
1757
1787
  catching.delete(room.locator);
1758
- sweep();
1788
+ if (prior !== undefined && prior.record.key !== record.key)
1789
+ sweep();
1790
+ name(room.locator);
1759
1791
  }
1760
- for (const batch of accepted)
1761
- pendings.delete(pendingKey(room.locator, batch.id));
1762
1792
  stale.delete(room.locator);
1763
1793
  unpreparable.delete(room.locator);
1764
1794
  if (unreadable.delete(room.locator))
1765
1795
  scopedRefusals.delete(room.locator);
1766
- sourceRefusals.delete(room.locator);
1767
- declarationRefusals.delete(room.locator);
1768
1796
  list(room, true);
1769
1797
  publishChangeSet(set);
1770
1798
  await unpair(local);
1771
1799
  moveTo({ ...status, ...attention() });
1772
- options.adapter.roomStream?.published(room.locator, roomVersion);
1773
- if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1800
+ if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1774
1801
  submitLater(room.locator);
1775
1802
  }
1776
1803
  return true;
@@ -1800,12 +1827,7 @@ export function composeRuntime(options) {
1800
1827
  return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
1801
1828
  }
1802
1829
  async function saveParts(locator, named, writtenBy) {
1803
- const read = named.map(({ path, parts }) => ({ path, parts: readParts(parts) }));
1804
- if (read.some(({ path }, at) => path === '' ||
1805
- normalize(path) !== path ||
1806
- read.findIndex((one) => one.path === path) !== at)) {
1807
- throw runtimeError('room-draft-malformed');
1808
- }
1830
+ const read = readSavedParts(named);
1809
1831
  const target = await savedRoom(locator);
1810
1832
  const saved = [];
1811
1833
  const files = [];
@@ -1814,7 +1836,7 @@ export function composeRuntime(options) {
1814
1836
  for (const { path, parts } of read) {
1815
1837
  const file = parts.file === undefined
1816
1838
  ? undefined
1817
- : await heldFile(parts.file.blob, binaryBase(visible, path));
1839
+ : await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
1818
1840
  if (parts.file !== undefined && file !== undefined) {
1819
1841
  files.push({ key: file.localId, blob: parts.file.blob });
1820
1842
  }
@@ -1849,13 +1871,14 @@ export function composeRuntime(options) {
1849
1871
  const referring = removed.length === 0
1850
1872
  ? []
1851
1873
  : (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
1852
- const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
1874
+ const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
1853
1875
  const { visible } = await replayed(record, confirmedLayer, replica.queue);
1854
1876
  const composition = composed(record);
1855
1877
  return {
1856
1878
  record,
1857
1879
  confirmed: confirmedLayer,
1858
1880
  visible,
1881
+ folders: new Set(await local.folders(record.key, 'visible', paths)),
1859
1882
  *visibleOver(queue) {
1860
1883
  return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
1861
1884
  },
@@ -1878,18 +1901,6 @@ export function composeRuntime(options) {
1878
1901
  return undefined;
1879
1902
  return entry.binary !== null ? null : entry.content;
1880
1903
  }
1881
- function walkOf(local, key, view) {
1882
- return async (after) => {
1883
- const page = await local.walk(key, view, after, WALK_BYTES);
1884
- return {
1885
- entries: page.entries.map(({ path, content, binary }) => ({
1886
- path,
1887
- content: binary !== null ? null : content,
1888
- })),
1889
- next: page.next,
1890
- };
1891
- };
1892
- }
1893
1904
  function lookupOf(local, key, view) {
1894
1905
  return async (paths) => {
1895
1906
  if (paths.length === 0)
@@ -1906,37 +1917,6 @@ export function composeRuntime(options) {
1906
1917
  const read = parseFrontmatter(entry.content ?? '');
1907
1918
  return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
1908
1919
  }
1909
- function overriddenWalk(walk, overrides) {
1910
- const sorted = [...overrides.keys()].sort();
1911
- const beyond = (path) => {
1912
- if (path === null)
1913
- return 0;
1914
- let low = 0;
1915
- let high = sorted.length;
1916
- while (low < high) {
1917
- const middle = (low + high) >>> 1;
1918
- if (sorted[middle] <= path)
1919
- low = middle + 1;
1920
- else
1921
- high = middle;
1922
- }
1923
- return low;
1924
- };
1925
- return async (after) => {
1926
- const page = await walk(after);
1927
- const end = page.next;
1928
- const added = sorted
1929
- .slice(beyond(after), end === null ? sorted.length : beyond(end))
1930
- .flatMap((path) => {
1931
- const entry = overrides.get(path);
1932
- return entry === null || entry === undefined
1933
- ? []
1934
- : [{ path, content: entry.binary !== null ? null : entry.content }];
1935
- });
1936
- const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
1937
- return { entries, next: end };
1938
- };
1939
- }
1940
1920
  function overriddenLookup(lookup, overrides) {
1941
1921
  return async (paths) => {
1942
1922
  const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
@@ -1954,9 +1934,8 @@ export function composeRuntime(options) {
1954
1934
  }
1955
1935
  function validatorOf(record, confirmedLayer) {
1956
1936
  return async (resulting, touched) => {
1957
- const composition = compositionFor(record);
1958
1937
  const local = await openLocal();
1959
- const changed = [];
1938
+ const changes = [];
1960
1939
  const overrides = new Map();
1961
1940
  const pace = paced();
1962
1941
  for (const path of new Set([...confirmedLayer.held, ...touched])) {
@@ -1967,19 +1946,13 @@ export function composeRuntime(options) {
1967
1946
  continue;
1968
1947
  overrides.set(path, now);
1969
1948
  if (textOf(was) !== textOf(now))
1970
- changed.push({ path, was: textOf(was), now: textOf(now) });
1949
+ changes.push({ path, was: textOf(was), now: textOf(now) });
1971
1950
  }
1972
- const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
1973
- const clean = available(composition) &&
1974
- changed.length <= CLEAN_CHANGES &&
1975
- (await changedCleanly(changed, composed(record), composition, {
1976
- lookup,
1977
- texts: textsOf(local, record.key, 'confirmed'),
1978
- referrers: referrersOf(local, record.key, 'confirmed'),
1979
- }));
1980
- if (clean)
1981
- return { verdict: 'accept' };
1982
- return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
1951
+ return validatedNeighbourhood(changes, compositionFor(record), {
1952
+ lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
1953
+ texts: textsOf(local, record.key, 'confirmed'),
1954
+ referrers: referrersOf(local, record.key, 'confirmed'),
1955
+ });
1983
1956
  };
1984
1957
  }
1985
1958
  function textsOf(local, key, view) {
@@ -2003,6 +1976,7 @@ export function composeRuntime(options) {
2003
1976
  operations: room.operations,
2004
1977
  providerOwnedPaths: room.providerOwnedPaths,
2005
1978
  visible: saving.visible,
1979
+ folders: saving.folders,
2006
1980
  queued: queuedDocuments(replica.queue),
2007
1981
  unmodeled: await unmodeledIn(record),
2008
1982
  validate: validatorOf(record, saving.confirmed),
@@ -2023,6 +1997,7 @@ export function composeRuntime(options) {
2023
1997
  locator,
2024
1998
  key: saving.record.key,
2025
1999
  roots: saving.record.typedSelection?.roots ?? [],
2000
+ composed: composedOf(composed(saving.record)),
2026
2001
  overlay: diff.overlay,
2027
2002
  });
2028
2003
  }
@@ -2054,7 +2029,7 @@ export function composeRuntime(options) {
2054
2029
  }
2055
2030
  const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
2056
2031
  const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
2057
- const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, sourceSaving.visible)));
2032
+ const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
2058
2033
  if (changed.length > 0)
2059
2034
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2060
2035
  const target = replicas.get(destination);
@@ -2179,7 +2154,7 @@ export function composeRuntime(options) {
2179
2154
  ...saved.map(({ path }) => path),
2180
2155
  ...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
2181
2156
  ]);
2182
- const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, underneath(held, path, saving.visible)));
2157
+ const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
2183
2158
  if (changed.length > 0)
2184
2159
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2185
2160
  const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
@@ -2207,6 +2182,7 @@ export function composeRuntime(options) {
2207
2182
  providerLimits: limits,
2208
2183
  handle: room.handle,
2209
2184
  visible: saving.visible,
2185
+ folders: saving.folders,
2210
2186
  changes,
2211
2187
  bases: parts.flatMap(({ bases }) => bases),
2212
2188
  queued: queuedDocuments(replica.queue),
@@ -2357,6 +2333,9 @@ export function composeRuntime(options) {
2357
2333
  async function readHistory(locator, path, query = {}) {
2358
2334
  if (closed || refusal !== null)
2359
2335
  return UNCERTAIN;
2336
+ await firstDiscovery;
2337
+ if (!roomsNamed)
2338
+ return UNCERTAIN;
2360
2339
  try {
2361
2340
  await ensureAuthorization();
2362
2341
  }
@@ -2449,6 +2428,9 @@ export function composeRuntime(options) {
2449
2428
  async function transferBinary(request, transfer, answer) {
2450
2429
  if (closed || refusal !== null)
2451
2430
  return UNCERTAIN;
2431
+ await firstDiscovery;
2432
+ if (!roomsNamed)
2433
+ return UNCERTAIN;
2452
2434
  try {
2453
2435
  await ensureAuthorization();
2454
2436
  }
@@ -2479,23 +2461,27 @@ export function composeRuntime(options) {
2479
2461
  return UNCERTAIN;
2480
2462
  }
2481
2463
  }
2464
+ function fetchBinary(request, name) {
2465
+ return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2466
+ }
2482
2467
  async function readBinary(request, name) {
2483
2468
  if (closed)
2484
2469
  return UNCERTAIN;
2485
2470
  const held = await keptFiles.find(name);
2486
2471
  if (held !== null)
2487
2472
  return held;
2488
- return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2473
+ return fetchBinary(request, name);
2489
2474
  }
2490
2475
  async function binaryPath(locator, path, revision) {
2491
2476
  if (revision !== undefined)
2492
2477
  return { kind: 'path', locator, path, revision };
2493
2478
  const replica = replicas.get(locator);
2494
- if (replica === undefined)
2495
- return null;
2496
2479
  const local = await openLocal();
2497
- const [held] = await local.entries(replica.record.key, 'visible', [path]);
2498
- if (held?.binary === undefined || held.binary === null)
2480
+ const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
2481
+ const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
2482
+ if (attached !== undefined)
2483
+ return { kind: 'path', locator, path, revision: attached.revision };
2484
+ if (replica === undefined || held?.binary === undefined || held.binary === null)
2499
2485
  return null;
2500
2486
  if (held.binary.state === 'pending')
2501
2487
  return { kind: 'pending', localId: held.binary.localId };
@@ -2527,33 +2513,83 @@ export function composeRuntime(options) {
2527
2513
  revision: record.revision,
2528
2514
  };
2529
2515
  }
2530
- async function readBinaryPath(locator, path, revision) {
2516
+ async function askedBinary(locator, path, revision) {
2531
2517
  await ready;
2532
2518
  const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2533
- const asked = drafted !== undefined && drafted.localId === revision
2519
+ return drafted !== undefined && drafted.localId === revision
2534
2520
  ? { kind: 'pending', localId: revision }
2535
- : await binaryPath(locator, path, revision);
2521
+ : binaryPath(locator, path, revision);
2522
+ }
2523
+ function pendingBinary(locator, path, localId) {
2524
+ const facts = queuedFacts(locator, path, localId);
2525
+ return facts === null
2526
+ ? absentBinary(path)
2527
+ : {
2528
+ kind: 'binary',
2529
+ file: localId,
2530
+ byteLength: facts.byteLength,
2531
+ mediaType: facts.mediaType,
2532
+ };
2533
+ }
2534
+ async function readBinaryPath(locator, path, revision) {
2535
+ const asked = await askedBinary(locator, path, revision);
2536
2536
  if (asked === null)
2537
2537
  return absentBinary(path);
2538
- if (asked.kind === 'pending') {
2539
- const facts = queuedFacts(locator, path, asked.localId);
2540
- return facts === null
2541
- ? absentBinary(path)
2542
- : {
2543
- kind: 'binary',
2544
- file: asked.localId,
2545
- byteLength: facts.byteLength,
2546
- mediaType: facts.mediaType,
2547
- };
2548
- }
2538
+ if (asked.kind === 'pending')
2539
+ return pendingBinary(locator, path, asked.localId);
2549
2540
  return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
2550
2541
  }
2542
+ async function heldMediaType(locator, path) {
2543
+ const replica = replicas.get(locator);
2544
+ const [held] = replica === undefined
2545
+ ? []
2546
+ : await (await openLocal()).entries(replica.record.key, 'visible', [path]);
2547
+ return held === undefined
2548
+ ? attachedFiles.get(locator)?.get(path)?.mediaType
2549
+ : held.binary?.mediaType;
2550
+ }
2551
+ function linkOf(asked, held) {
2552
+ return transferBinary(asked, (authorization) => options.adapter.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
2553
+ }
2551
2554
  async function linkBinaryPath(locator, path, revision) {
2552
2555
  await ready;
2553
2556
  const asked = await binaryPath(locator, path, revision);
2554
2557
  if (asked === null || asked.kind === 'pending')
2555
2558
  return absentBinary(path);
2556
- return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
2559
+ return linkOf(asked, revision === undefined ? await heldMediaType(locator, path) : undefined);
2560
+ }
2561
+ async function showBinaryPath(locator, path, revision) {
2562
+ const asked = await askedBinary(locator, path, revision);
2563
+ if (asked === null)
2564
+ return absentBinary(path);
2565
+ if (asked.kind === 'pending')
2566
+ return pendingBinary(locator, path, asked.localId);
2567
+ if (closed)
2568
+ return UNCERTAIN;
2569
+ const name = { locator, path: asked.path, revision: asked.revision };
2570
+ const kept = await keptFiles.find(name);
2571
+ if (kept !== null)
2572
+ return kept;
2573
+ const mediaType = await heldMediaType(locator, path);
2574
+ if (mediaType?.startsWith('image/') === true)
2575
+ return fetchBinary(asked, name);
2576
+ return linkOf(asked, revision === undefined ? mediaType : undefined);
2577
+ }
2578
+ async function attachBinary(locator, file) {
2579
+ await ready;
2580
+ await firstDiscovery;
2581
+ if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
2582
+ return OFFLINE;
2583
+ const request = await attachedFile(locator, file);
2584
+ return transferBinary({ locator, path: file.name }, async (held) => {
2585
+ const result = await options.adapter.attach(held, request);
2586
+ if (result.kind === 'attached') {
2587
+ const landed = attachedFiles.get(locator) ?? new Map();
2588
+ landed.set(result.path, { revision: result.revision, mediaType: request.mediaType });
2589
+ attachedFiles.set(locator, landed);
2590
+ }
2591
+ return result;
2592
+ }, attachAnswer);
2557
2593
  }
2558
2594
  function readPart(file, part) {
2559
2595
  return database.ask({ kind: 'file.part', key: file, part });
@@ -2562,13 +2598,6 @@ export function composeRuntime(options) {
2562
2598
  const freed = await database.ask({ kind: 'opened.clean' });
2563
2599
  return freed + ((await options.shell.cleanUp?.()) ?? 0);
2564
2600
  }
2565
- function declaredAgents() {
2566
- for (const capability of capabilities) {
2567
- if (capability.conversationAgents !== undefined)
2568
- return capability.conversationAgents;
2569
- }
2570
- return undefined;
2571
- }
2572
2601
  function declaredAction(name) {
2573
2602
  for (const capability of capabilities) {
2574
2603
  for (const action of capability.actions ?? []) {
@@ -2620,6 +2649,24 @@ export function composeRuntime(options) {
2620
2649
  return UNCERTAIN;
2621
2650
  }
2622
2651
  }
2652
+ async function readConversationHistory(request) {
2653
+ await ready;
2654
+ const stream = options.adapter.conversations;
2655
+ if (closed || refusal !== null || stream?.history === undefined)
2656
+ return UNCERTAIN;
2657
+ journal.record('transfer conversation history');
2658
+ try {
2659
+ const result = await stream.history(request);
2660
+ if (result.kind === 'revoked-authorization')
2661
+ await refuse('revoked-authorization');
2662
+ journal.record(`outcome conversation history ${result.kind}`);
2663
+ return calledResult(result);
2664
+ }
2665
+ catch {
2666
+ journal.record('outcome conversation history transport-uncertainty');
2667
+ return UNCERTAIN;
2668
+ }
2669
+ }
2623
2670
  function unsettled(result) {
2624
2671
  return result.kind !== 'action' && result.retryable;
2625
2672
  }
@@ -2691,122 +2738,23 @@ export function composeRuntime(options) {
2691
2738
  options.adapter.reached(locator, written, roomVersion) &&
2692
2739
  !options.adapter.reached(locator, roomVersion, written));
2693
2740
  }
2694
- function overriddenTexts(texts, overrides) {
2695
- return async (paths) => {
2696
- const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
2697
- for (const path of paths) {
2698
- const entry = overrides.get(path);
2699
- if (entry === undefined)
2700
- continue;
2701
- if (entry === null)
2702
- known.delete(path);
2703
- else
2704
- known.set(path, entry.binary !== null ? null : entry.content);
2705
- }
2706
- return known;
2707
- };
2708
- }
2709
- async function unreadableRoom(texts, walk, paths) {
2710
- if (paths !== null) {
2711
- const text = {};
2712
- for (const [path, content] of await texts(paths))
2713
- if (content !== null)
2714
- text[path] = content;
2715
- return stepped(unreadableIn(text, Object.keys(text)));
2716
- }
2717
- for (let after = null;;) {
2718
- const page = await walk(after);
2719
- const text = {};
2720
- for (const { path, content } of page.entries)
2721
- if (content !== null)
2722
- text[path] = content;
2723
- const refused = await stepped(unreadableIn(text, Object.keys(text)));
2724
- if (refused !== null)
2725
- return refused;
2726
- if (page.next === null)
2727
- return null;
2728
- after = page.next;
2729
- }
2730
- }
2731
- async function validatedRoom(local, record, view, previous, overrides = null) {
2732
- const composition = composed(record);
2733
- const modeled = record.model.status !== 'unavailable' || record.typedSelection !== null;
2734
- const lookup = overrides === null
2735
- ? lookupOf(local, record.key, view)
2736
- : overriddenLookup(lookupOf(local, record.key, view), overrides);
2737
- const texts = overrides === null
2738
- ? textsOf(local, record.key, view)
2739
- : overriddenTexts(textsOf(local, record.key, view), overrides);
2740
- const walk = overrides === null
2741
- ? walkOf(local, record.key, view)
2742
- : overriddenWalk(walkOf(local, record.key, view), overrides);
2743
- let changes = null;
2744
- let reshaped = false;
2745
- if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
2746
- const [was, now] = await Promise.all([
2747
- local.entries(record.key, 'confirmed', previous.changed),
2748
- overrides === null
2749
- ? local.entries(record.key, view, previous.changed)
2750
- : Promise.resolve([]),
2751
- ]);
2752
- const before = new Map(was.map((entry) => [entry.path, entry]));
2753
- const after = overrides === null
2754
- ? new Map(now.map((entry) => [entry.path, entry]))
2755
- : overrides;
2756
- changes = previous.changed.flatMap((path) => {
2757
- const one = before.get(path) ?? null;
2758
- const other = after.get(path) ?? null;
2759
- reshaped ||= textDiffers(one, other);
2760
- const held = textOf(one);
2761
- const next = textOf(other);
2762
- return held === next ? [] : [{ path, was: held, now: next }];
2763
- });
2764
- }
2765
- const named = changes === null ? null : changes.map(({ path }) => path);
2766
- const textChanged = changes === null || changes.length > 0 || reshaped;
2767
- const modelsChanged = previous === null || !sameComposition(composed(previous.record), composition);
2768
- if (modeled && (textChanged || modelsChanged)) {
2769
- const clean = previous !== null &&
2770
- changes !== null &&
2771
- (await changedCleanly(changes, composed(previous.record), composition, {
2772
- lookup,
2773
- texts,
2774
- referrers: referrersOf(local, record.key, 'confirmed'),
2775
- }));
2776
- if (clean)
2777
- return null;
2778
- const verdict = await validatedCollection(walk, lookup, composition);
2779
- if (verdict.verdict === 'accept')
2780
- return null;
2781
- if (verdict.rule !== 'model-unavailable')
2782
- return { kind: 'refused', refusal: verdict };
2783
- return unreadableRoom(texts, walk, named);
2784
- }
2785
- if (textChanged)
2786
- return unreadableRoom(texts, walk, named);
2787
- return null;
2788
- }
2789
- function refreshingOf(local, key, liveness, replaying = null) {
2790
- const write = replaying === null ? exclusive : installWrite;
2741
+ function refreshingOf(local, key, composition, liveness, replaying = null) {
2742
+ const write = replaying === null ? exclusive : finishingWrite;
2791
2743
  return {
2792
- async turn() {
2793
- if (replaying === null)
2794
- return;
2795
- installing(replaying, 'replaying');
2796
- await screened();
2744
+ turn() {
2745
+ if (replaying !== null)
2746
+ installing(replaying);
2797
2747
  },
2798
2748
  prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
2799
2749
  async stage(page) {
2800
2750
  for (let written = 0;;) {
2801
- if (replaying !== null)
2802
- await screened();
2803
2751
  const end = batchEnd(page.entries, written);
2804
2752
  const last = end === page.entries.length;
2805
2753
  const entries = page.entries.slice(written, end);
2806
2754
  const staged = await write(async () => {
2807
2755
  if (!liveness())
2808
2756
  return false;
2809
- await timed(() => local.refresh(key, last
2757
+ await timed(stageBudget, () => local.refresh(key, composition, last
2810
2758
  ? { ...page, entries }
2811
2759
  : { entries, removals: [], folders: [], folderRemovals: [] }));
2812
2760
  return true;
@@ -2825,94 +2773,180 @@ export function composeRuntime(options) {
2825
2773
  let spent = 0;
2826
2774
  while (end < entries.length) {
2827
2775
  const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
2828
- if (spent + size > stageBytes && end > from)
2776
+ if (spent + size > stageBudget.bytes && end > from)
2829
2777
  break;
2830
2778
  spent += size;
2831
2779
  end += 1;
2832
2780
  }
2833
2781
  return end;
2834
2782
  }
2835
- async function timed(work) {
2783
+ async function timed(budget, work) {
2836
2784
  const started = performance.now();
2837
2785
  const done = await work();
2838
2786
  const spent = performance.now() - started;
2839
- if (spent < STAGE_AIM_MS / 2)
2840
- stageBytes = Math.min(STAGE_BYTES, Math.round(stageBytes * 1.5));
2841
- else if (spent > STAGE_AIM_MS * 2) {
2842
- stageBytes = Math.max(STAGE_FLOOR, Math.round(stageBytes / 1.5));
2787
+ if (spent < budget.aim / 2)
2788
+ budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
2789
+ else if (spent > budget.aim * 2) {
2790
+ budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
2843
2791
  }
2844
2792
  return done;
2845
2793
  }
2846
2794
  function installWrite(work) {
2847
- const next = installWrites.then(() => exclusive(work), () => exclusive(work));
2848
- installWrites = next.then(() => undefined, () => undefined);
2849
- return next;
2795
+ return installTurn(installTurns.starting, work);
2796
+ }
2797
+ function finishingWrite(work) {
2798
+ return installTurn(installTurns.finishing, work);
2799
+ }
2800
+ function installTurn(queue, work) {
2801
+ return new Promise((resolve, reject) => {
2802
+ queue.push(() => void exclusive(work)
2803
+ .then(resolve, reject)
2804
+ .finally(() => {
2805
+ installTurns.busy = false;
2806
+ nextInstallTurn();
2807
+ }));
2808
+ nextInstallTurn();
2809
+ });
2850
2810
  }
2851
- function screened() {
2852
- if (onScreen || closed)
2853
- return Promise.resolve();
2854
- queueReadiness();
2855
- return new Promise((resolve) => shownWaiters.push(resolve));
2811
+ function nextInstallTurn() {
2812
+ if (installTurns.busy || installTurns.ahead > 0)
2813
+ return;
2814
+ const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
2815
+ if (turn === undefined)
2816
+ return;
2817
+ installTurns.busy = true;
2818
+ turn();
2819
+ }
2820
+ async function ahead(work) {
2821
+ let taken = false;
2822
+ const hold = {
2823
+ take() {
2824
+ if (taken)
2825
+ return;
2826
+ taken = true;
2827
+ installTurns.ahead += 1;
2828
+ },
2829
+ release() {
2830
+ if (!taken)
2831
+ return;
2832
+ taken = false;
2833
+ installTurns.ahead -= 1;
2834
+ resumeInstall();
2835
+ },
2836
+ };
2837
+ hold.take();
2838
+ try {
2839
+ return await work(hold);
2840
+ }
2841
+ finally {
2842
+ hold.release();
2843
+ }
2844
+ }
2845
+ function resumeInstall() {
2846
+ if (installTurns.ahead > 0)
2847
+ return;
2848
+ void yielded().then(nextInstallTurn);
2849
+ }
2850
+ async function heldStream() {
2851
+ while (streamsFree === 0)
2852
+ await new Promise((admit) => installWaiters.push(admit));
2853
+ streamsFree -= 1;
2854
+ return once(() => {
2855
+ streamsFree += 1;
2856
+ });
2856
2857
  }
2857
- async function heldPart() {
2858
- while (partsFree === 0)
2859
- await new Promise((admit) => partWaiters.push(admit));
2860
- partsFree -= 1;
2861
- return freed();
2858
+ async function unqueued() {
2859
+ while (queuedText >= QUEUED_TEXT) {
2860
+ await new Promise((admit) => installWaiters.push(admit));
2861
+ }
2862
2862
  }
2863
- function freed() {
2863
+ function once(release) {
2864
2864
  let released = false;
2865
2865
  return () => {
2866
2866
  if (released)
2867
2867
  return;
2868
2868
  released = true;
2869
- partsFree += 1;
2870
- partWaiters.shift()?.();
2869
+ release();
2870
+ for (const admit of installWaiters.splice(0))
2871
+ admit();
2871
2872
  };
2872
2873
  }
2873
- function installing(locator, step, share = null) {
2874
- installs.set(locator, { step, share });
2874
+ function installing(locator) {
2875
+ installs.add(locator);
2875
2876
  queueReadiness();
2876
2877
  }
2877
- function installed(locator) {
2878
- if (installs.delete(locator))
2879
- queueReadiness();
2878
+ function untransferred(locator) {
2879
+ transfers.delete(locator);
2880
+ arrivals.delete(locator);
2881
+ streaming.delete(locator);
2882
+ underived.delete(locator);
2883
+ unindexed.delete(locator);
2884
+ }
2885
+ function setBack(locator) {
2886
+ const now = bytesOf(locator, true);
2887
+ const earlier = startedOver.get(locator);
2888
+ startedOver.set(locator, {
2889
+ done: Math.max(now.done, earlier?.done ?? 0),
2890
+ total: Math.max(now.total, earlier?.total ?? 0),
2891
+ });
2880
2892
  }
2881
- function screen(value) {
2882
- if (value === onScreen)
2893
+ function owedIndexes(locator, bytes, text) {
2894
+ if (bytes <= 0 || text <= 0)
2883
2895
  return;
2884
- onScreen = value;
2885
- journal.record(`installation ${value ? 'shown' : 'hidden'}`);
2896
+ unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
2897
+ }
2898
+ function built(locator, { done, text }) {
2899
+ const owed = unindexed.get(locator) ?? [];
2900
+ let left = text;
2901
+ while (!done && left > 0 && owed.length > 0) {
2902
+ const [first] = owed;
2903
+ if (left >= first.text) {
2904
+ left -= first.text;
2905
+ owed.shift();
2906
+ continue;
2907
+ }
2908
+ owed[0] = {
2909
+ bytes: (first.bytes * (first.text - left)) / first.text,
2910
+ text: first.text - left,
2911
+ };
2912
+ left = 0;
2913
+ }
2914
+ if (done || owed.length === 0)
2915
+ unindexed.delete(locator);
2886
2916
  queueReadiness();
2887
- if (!value)
2888
- return;
2889
- for (const resolve of shownWaiters.splice(0))
2890
- resolve();
2891
2917
  }
2892
- async function pullRefresh(authorized, held, liveness) {
2918
+ function installed(locator) {
2919
+ if (installs.delete(locator))
2920
+ queueReadiness();
2921
+ }
2922
+ async function pullRefresh(authorized, held, liveness, hold, refresh) {
2893
2923
  if (!liveness())
2894
2924
  return unconfirmed;
2895
- const turn = generation;
2896
2925
  const { record } = held;
2897
- const base = held.confirmed;
2926
+ const from = standingOf(record.locator);
2898
2927
  const local = await openLocal();
2899
2928
  try {
2900
2929
  await exclusive(() => local.unrefresh(record.key));
2901
- const result = await advancement(reads, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, liveness));
2930
+ const result = await advancement(async (authorization, request) => {
2931
+ hold.release();
2932
+ try {
2933
+ return await refresh(authorization, request);
2934
+ }
2935
+ finally {
2936
+ hold.take();
2937
+ }
2938
+ }, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, composed(record), liveness));
2902
2939
  if (result.kind !== 'advanced') {
2903
2940
  journal.record(`transfer refresh ${record.locator} ${result.kind}`);
2904
2941
  return result;
2905
2942
  }
2906
- const refused = await validatedRoom(local, result.record, 'refreshed', {
2907
- record,
2908
- changed: [...result.changed],
2909
- });
2910
- if (refused !== null) {
2911
- journal.record(`transfer refresh ${record.locator} refused`);
2912
- return refused;
2913
- }
2914
2943
  journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
2915
- return { kind: 'advanced', record: result.record, base };
2944
+ return {
2945
+ kind: 'advanced',
2946
+ record: result.record,
2947
+ from,
2948
+ remodelled: result.record.model !== record.model,
2949
+ };
2916
2950
  }
2917
2951
  catch (error) {
2918
2952
  if (refusedForSpace(error))
@@ -2920,9 +2954,6 @@ export function composeRuntime(options) {
2920
2954
  journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
2921
2955
  throw error;
2922
2956
  }
2923
- finally {
2924
- reads.finished(record.locator, turn);
2925
- }
2926
2957
  }
2927
2958
  const heldContents = new Map();
2928
2959
  function heldContentOf(local, key) {
@@ -2940,33 +2971,50 @@ export function composeRuntime(options) {
2940
2971
  heldContents.set(key, made);
2941
2972
  return made;
2942
2973
  }
2974
+ async function abandoned(local, locator) {
2975
+ await local.abandon(locator);
2976
+ setBack(locator);
2977
+ untransferred(locator);
2978
+ liveQueries.recover();
2979
+ sweep();
2980
+ }
2943
2981
  function staging(local, liveness) {
2982
+ const derived = async (locator, key, composition) => {
2983
+ underived.delete(locator);
2984
+ for (;;) {
2985
+ if (!liveness())
2986
+ return false;
2987
+ const done = await finishingWrite(async () => liveness()
2988
+ ? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
2989
+ : null);
2990
+ if (done === null)
2991
+ return false;
2992
+ built(locator, done);
2993
+ if (done.done)
2994
+ return true;
2995
+ }
2996
+ };
2944
2997
  return {
2945
2998
  held: (locator) => transfers.get(locator) ?? null,
2946
2999
  async turn(locator, step) {
2947
- installing(locator, step);
2948
- for (;;) {
2949
- await screened();
2950
- const release = step === 'downloading' ? await heldPart() : () => { };
2951
- if (onScreen || closed)
2952
- return release;
2953
- release();
2954
- }
3000
+ installing(locator);
3001
+ return step === 'downloading' ? heldStream() : () => { };
2955
3002
  },
2956
- spare() {
2957
- if (!onScreen || closed || partsFree === 0)
2958
- return null;
2959
- partsFree -= 1;
2960
- return freed();
3003
+ unqueued,
3004
+ queued(text) {
3005
+ queuedText += text;
3006
+ return once(() => {
3007
+ queuedText -= text;
3008
+ });
2961
3009
  },
2962
3010
  arriving(locator) {
2963
3011
  let ended = false;
2964
3012
  return {
2965
- at(token, bytes) {
3013
+ at(bytes) {
2966
3014
  if (ended)
2967
3015
  return;
2968
- const held = arrivals.get(locator) ?? new Map();
2969
- arrivals.set(locator, held.set(token, bytes));
3016
+ arrivals.set(locator, bytes);
3017
+ streaming.add(locator);
2970
3018
  const now = performance.now();
2971
3019
  if (now - arrivalReported < ARRIVAL_REPORT_MS)
2972
3020
  return;
@@ -2974,96 +3022,64 @@ export function composeRuntime(options) {
2974
3022
  queueReadiness();
2975
3023
  },
2976
3024
  end() {
3025
+ if (ended)
3026
+ return;
2977
3027
  ended = true;
2978
- if (arrivals.delete(locator))
3028
+ if (streaming.delete(locator))
2979
3029
  queueReadiness();
2980
3030
  },
2981
3031
  };
2982
3032
  },
2983
- received: (locator, token) => local.transferPart(locator, token),
2984
- keep: (locator, token, received) => installWrite(async () => {
2985
- if (liveness())
2986
- await local.keepPart(locator, token, received);
2987
- }),
2988
- announced(locator) {
2989
- const later = announced.get(locator);
2990
- return later === undefined ? [] : [later];
2991
- },
2992
- async stage(record, page, composition, received) {
3033
+ streamed: options.adapter.roomStream !== undefined,
3034
+ async stage(record, page, composition) {
2993
3035
  const { locator } = record;
2994
- const whole = received?.byteLength ?? 0;
2995
- const from = record.writing?.written ?? 0;
2996
- const token = record.writing?.token ?? null;
2997
- const entries = page.entries.length;
2998
- let held = record;
2999
- if (token !== null)
3000
- arrivals.get(locator)?.delete(token);
3001
- installing(locator, 'writing', { written: from, entries, bytes: whole });
3002
- for (let written = from;;) {
3003
- await screened();
3036
+ installing(locator);
3037
+ if (!liveness())
3038
+ return null;
3039
+ const key = await installWrite(async () => {
3004
3040
  if (!liveness())
3005
3041
  return null;
3006
- const end = batchEnd(page.entries, written);
3007
- const last = end === entries;
3008
- const next = last
3009
- ? {
3010
- ...held,
3011
- writing: null,
3012
- done: {
3013
- parts: held.done.parts + (received === null ? 0 : 1),
3014
- bytes: held.done.bytes + whole,
3015
- },
3016
- }
3017
- : { ...held, writing: { token, written: end } };
3018
- const key = await installWrite(async () => {
3019
- if (!liveness())
3020
- return null;
3021
- return timed(() => local.stage(next, composition, {
3022
- entries: page.entries.slice(written, end),
3023
- folders: last ? page.folders : [],
3024
- }, last && received !== null && token !== null ? token : undefined));
3025
- });
3026
- if (key === null)
3027
- return null;
3028
- const arrived = !transfers.has(locator);
3029
- held = { ...next, key };
3030
- transfers.set(locator, held);
3031
- if (arrived)
3032
- liveQueries.recover();
3033
- if (last) {
3034
- installing(locator, held.pageToken === null ? 'replaying' : 'downloading');
3035
- liveQueries.recover();
3036
- return held;
3037
- }
3038
- installing(locator, 'writing', { written: end, entries, bytes: whole });
3039
- written = end;
3040
- }
3042
+ return local.stage(record, composition, page);
3043
+ });
3044
+ if (key === null || !liveness())
3045
+ return null;
3046
+ const arrived = !transfers.has(locator);
3047
+ const prior = transfers.get(locator)?.done.bytes ?? 0;
3048
+ const held = { ...record, key };
3049
+ transfers.set(locator, held);
3050
+ if (arrived)
3051
+ liveQueries.recover();
3052
+ installing(locator);
3053
+ liveQueries.recover();
3054
+ const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
3055
+ owedIndexes(locator, record.done.bytes - prior, text);
3056
+ const owed = text + (underived.get(locator) ?? 0);
3057
+ underived.set(locator, owed);
3058
+ if (owed < deriveBudget.bytes)
3059
+ return held;
3060
+ return (await derived(locator, key, composition)) ? held : null;
3041
3061
  },
3042
- async refreshing(locator, key) {
3062
+ derive: derived,
3063
+ async refreshing(locator, key, composition) {
3043
3064
  await exclusive(() => local.unrefresh(key));
3044
- return refreshingOf(local, key, liveness, locator);
3065
+ return refreshingOf(local, key, composition, liveness, locator);
3045
3066
  },
3046
3067
  abandon: (locator) => exclusive(async () => {
3047
- if (!liveness())
3048
- return;
3049
- await local.abandon(locator);
3050
- transfers.delete(locator);
3051
- liveQueries.recover();
3052
- sweep();
3068
+ if (liveness())
3069
+ await abandoned(local, locator);
3053
3070
  }),
3054
- setAside: (locator) => exclusive(async () => {
3071
+ setAside: async (locator) => {
3055
3072
  if (!liveness())
3056
3073
  return;
3057
- await local.abandon(locator, true);
3058
- transfers.delete(locator);
3074
+ setBack(locator);
3075
+ untransferred(locator);
3059
3076
  liveQueries.recover();
3060
- }),
3077
+ },
3061
3078
  release: (key) => exclusive(async () => {
3062
3079
  await local.release(key);
3063
3080
  sweep();
3064
3081
  }),
3065
3082
  content: (key) => heldContentOf(local, key),
3066
- validate: (record, view) => validatedRoom(local, record, view, null),
3067
3083
  };
3068
3084
  }
3069
3085
  function reusable(room, local) {
@@ -3075,7 +3091,6 @@ export function composeRuntime(options) {
3075
3091
  async function pullBootstrap(authorized, room, local, liveness) {
3076
3092
  if (!liveness())
3077
3093
  return unconfirmed;
3078
- const turn = generation;
3079
3094
  if (replicas.has(room.locator)) {
3080
3095
  catching.add(room.locator);
3081
3096
  liveQueries.recover();
@@ -3098,9 +3113,6 @@ export function composeRuntime(options) {
3098
3113
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
3099
3114
  throw error;
3100
3115
  }
3101
- finally {
3102
- reads.finished(room.locator, turn);
3103
- }
3104
3116
  }
3105
3117
  function caughtUpTo(locator) {
3106
3118
  if (catching.delete(locator))
@@ -3108,72 +3120,82 @@ export function composeRuntime(options) {
3108
3120
  }
3109
3121
  function behind(room) {
3110
3122
  const held = replicas.get(room.locator)?.record.roomVersion;
3111
- return (held !== undefined &&
3112
- [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
3123
+ return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
3113
3124
  }
3114
- async function advance(room, local, authorized, live) {
3115
- const carried = reads.carried(room.locator);
3116
- const withheld = await advanceOnce(room, local, authorized, live);
3117
- if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
3118
- return withheld;
3119
- }
3120
- return advanceOnce(room, local, authorized, live);
3125
+ function standingOf(locator) {
3126
+ const held = replicas.get(locator);
3127
+ return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
3128
+ }
3129
+ function stands(locator, from) {
3130
+ const now = standingOf(locator);
3131
+ return now.key === from.key && now.confirmed === from.confirmed;
3121
3132
  }
3122
- async function advanceOnce(room, local, authorized, running) {
3133
+ async function advance(room, local, authorized, running) {
3123
3134
  const live = () => running() && !full;
3124
3135
  if (!live())
3125
3136
  return unconfirmed;
3126
3137
  const held = replicas.get(room.locator);
3127
- if (held !== undefined) {
3128
- const refreshed = await pullRefresh(authorized, held, live);
3129
- if (!live())
3130
- return unconfirmed;
3131
- if (refreshed.kind === 'still-current') {
3132
- stale.delete(room.locator);
3133
- if (sourceRefusals.has(room.locator))
3134
- return null;
3135
- if (unreadable.delete(room.locator)) {
3136
- scopedRefusals.delete(room.locator);
3137
- list(room, true);
3138
- emitRooms();
3139
- }
3140
- return null;
3141
- }
3142
- if (refreshed.kind === 'advanced') {
3143
- const landing = { kind: 'refreshed', base: refreshed.base };
3144
- if (!(await publish(local, room, refreshed.record, landing, live))) {
3145
- const now = replicas.get(room.locator)?.record.roomVersion;
3146
- const reached = now !== undefined &&
3147
- options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3148
- return reached ? null : advance(room, local, authorized, live);
3149
- }
3150
- await retainModel(local, room, modelOf(room.locator));
3151
- return null;
3152
- }
3153
- if (refreshed.kind !== 'beyond-horizon')
3138
+ if (held !== undefined && options.adapter.roomStream !== undefined) {
3139
+ stale.delete(room.locator);
3140
+ name(room.locator);
3141
+ return null;
3142
+ }
3143
+ const refresh = reads.refresh;
3144
+ if (held !== undefined && refresh !== undefined) {
3145
+ const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
3146
+ if (refreshed === 'behind')
3147
+ return advance(room, local, authorized, live);
3148
+ if (refreshed !== 'beyond-horizon')
3154
3149
  return refreshed;
3155
- if (opened !== null) {
3156
- owed.add(room.locator);
3157
- liveQueries.recover();
3158
- return unconfirmed;
3159
- }
3160
- if (reloadsDue.get(room.locator) === generation)
3150
+ if (reloadsDue.has(room.locator))
3161
3151
  return unconfirmed;
3162
3152
  }
3153
+ const from = standingOf(room.locator);
3163
3154
  const bootstrapped = await pullBootstrap(authorized, room, local, live);
3164
- if (bootstrapped.kind === 'staged') {
3165
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3155
+ if (bootstrapped.kind !== 'staged')
3156
+ return live() ? bootstrapped : unconfirmed;
3157
+ const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
3158
+ if (await publish(local, room, bootstrapped.record, landing, live)) {
3166
3159
  return live() ? null : unconfirmed;
3167
3160
  }
3168
- return live() ? bootstrapped : unconfirmed;
3161
+ return reachedBy(room.locator, bootstrapped.record.roomVersion)
3162
+ ? null
3163
+ : advance(room, local, authorized, live);
3164
+ }
3165
+ async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
3166
+ const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
3167
+ if (!live())
3168
+ return unconfirmed;
3169
+ if (refreshed.kind === 'still-current') {
3170
+ stale.delete(room.locator);
3171
+ if (unreadable.delete(room.locator)) {
3172
+ scopedRefusals.delete(room.locator);
3173
+ list(room, true);
3174
+ emitRooms();
3175
+ }
3176
+ return null;
3177
+ }
3178
+ if (refreshed.kind === 'advanced') {
3179
+ const landing = {
3180
+ kind: 'refreshed',
3181
+ from: refreshed.from,
3182
+ remodelled: refreshed.remodelled,
3183
+ };
3184
+ if (!(await publish(local, room, refreshed.record, landing, live))) {
3185
+ const now = replicas.get(room.locator)?.record.roomVersion;
3186
+ const reached = now !== undefined &&
3187
+ options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3188
+ return reached ? null : 'behind';
3189
+ }
3190
+ await retainModel(local, room, modelOf(room.locator));
3191
+ return null;
3192
+ }
3193
+ return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
3169
3194
  }
3170
3195
  async function withhold(withheld, room, local, progress) {
3171
- if (withheld.kind === 'unconfirmed')
3196
+ if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
3172
3197
  return;
3173
- if (withheld.kind === 'refused')
3174
- stale.delete(room.locator);
3175
- else
3176
- stale.add(room.locator);
3198
+ stale.add(room.locator);
3177
3199
  if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
3178
3200
  const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
3179
3201
  repairable.set(room.locator, reset);
@@ -3195,30 +3217,10 @@ export function composeRuntime(options) {
3195
3217
  list(room, false);
3196
3218
  emitRooms();
3197
3219
  }
3198
- else if (withheld.kind === 'refused') {
3199
- refusedSource(room, withheld);
3200
- }
3201
3220
  else if (withheld.kind === 'revoked-authorization') {
3202
3221
  await refuse('revoked-authorization');
3203
3222
  }
3204
3223
  }
3205
- function refusedSource(room, { refusal }) {
3206
- sourceRefusals.set(room.locator, {
3207
- code: refusal.rule,
3208
- retryable: false,
3209
- fields: {
3210
- file: refusal.file,
3211
- ...(refusal.location === undefined ? {} : { location: refusal.location }),
3212
- ...(refusal.referrers === undefined ? {} : { referrers: refusal.referrers }),
3213
- },
3214
- });
3215
- declarationRefusals.delete(room.locator);
3216
- unreadable.add(room.locator);
3217
- list(room, false);
3218
- emitRooms();
3219
- moveTo({ ...status, ...attention() });
3220
- journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
3221
- }
3222
3224
  function leaving(batch, result) {
3223
3225
  if (result.kind === 'expired')
3224
3226
  return { batch, returned: returnedBy('expired') };
@@ -3240,8 +3242,8 @@ export function composeRuntime(options) {
3240
3242
  function settling(acceptance) {
3241
3243
  return acceptance !== null && 'apply' in acceptance;
3242
3244
  }
3243
- function refusalOf(acceptance) {
3244
- return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
3245
+ function failureOf(acceptance) {
3246
+ return settling(acceptance) ? null : acceptance;
3245
3247
  }
3246
3248
  async function acceptance(room, local, submitted, accepted) {
3247
3249
  if (!continuing())
@@ -3264,34 +3266,6 @@ export function composeRuntime(options) {
3264
3266
  ? accepted.roomVersion
3265
3267
  : held,
3266
3268
  };
3267
- const overrides = new Map([
3268
- ...back.set.map((entry) => [entry.path, entry]),
3269
- ...back.removed.map((path) => [path, null]),
3270
- ]);
3271
- const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
3272
- if (refused !== null) {
3273
- const key = pendingKey(room.locator, submitted.id);
3274
- return {
3275
- locator: room.locator,
3276
- left: [],
3277
- commit: {
3278
- rooms: [],
3279
- writes: [
3280
- {
3281
- locator: room.locator,
3282
- accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
3283
- },
3284
- ],
3285
- },
3286
- refused,
3287
- apply() {
3288
- const state = pendings.get(key);
3289
- if (state !== undefined && !fences.has(room.locator))
3290
- pendings.set(key, { ...state, accepted: accepted.roomVersion });
3291
- return changeSet(room.locator, { queue: true });
3292
- },
3293
- };
3294
- }
3295
3269
  changes = back;
3296
3270
  }
3297
3271
  const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
@@ -3301,8 +3275,6 @@ export function composeRuntime(options) {
3301
3275
  const set = settled.apply(answer);
3302
3276
  pendings.delete(pendingKey(room.locator, submitted.id));
3303
3277
  unreadable.delete(room.locator);
3304
- sourceRefusals.delete(room.locator);
3305
- declarationRefusals.delete(room.locator);
3306
3278
  list(room, true);
3307
3279
  return set;
3308
3280
  },
@@ -3320,13 +3292,9 @@ export function composeRuntime(options) {
3320
3292
  const failed = await exclusive(async () => {
3321
3293
  const adopting = await acceptance(room, local, submitted, accepted);
3322
3294
  await adoptedTogether(local, [adopting]);
3323
- return refusalOf(adopting);
3295
+ return failureOf(adopting);
3324
3296
  });
3325
- if (failed === null)
3326
- return true;
3327
- if (failed.kind === 'refused' && continuing())
3328
- refusedSource(room, failed);
3329
- return false;
3297
+ return failed === null;
3330
3298
  }
3331
3299
  async function adoptedMove(room, partner, local, submitted, accepted) {
3332
3300
  const other = discovered.get(partner);
@@ -3340,18 +3308,10 @@ export function composeRuntime(options) {
3340
3308
  ...accepted.destination,
3341
3309
  });
3342
3310
  await adoptedTogether(local, [left, arrived]);
3343
- const refusedArrival = refusalOf(arrived);
3344
- if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
3345
- refusedSource(other, refusedArrival);
3346
- }
3347
- return refusalOf(left);
3311
+ return failureOf(left);
3348
3312
  });
3349
3313
  submitLater(partner);
3350
- if (failed === null)
3351
- return true;
3352
- if (failed.kind === 'refused' && continuing())
3353
- refusedSource(room, failed);
3354
- return false;
3314
+ return failed === null;
3355
3315
  }
3356
3316
  function queueIn(locator) {
3357
3317
  return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
@@ -3474,8 +3434,6 @@ export function composeRuntime(options) {
3474
3434
  if (replica === undefined || head === undefined)
3475
3435
  return 'done';
3476
3436
  const state = pendings.get(pendingKey(room.locator, head.id));
3477
- if (state?.accepted !== undefined)
3478
- return 'done';
3479
3437
  const partner = partnerOf(head, room.locator);
3480
3438
  if (partner !== null && !leavingIn(head, room.locator)) {
3481
3439
  submitLater(partner);
@@ -3589,12 +3547,12 @@ export function composeRuntime(options) {
3589
3547
  }
3590
3548
  if (result.kind === 'expired') {
3591
3549
  await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
3592
- const there = other === undefined ? null : await advance(other, local, authorized, continuing);
3550
+ const there = other === undefined ? null : await advanced(other, local, authorized);
3593
3551
  if (other !== undefined && there !== null) {
3594
3552
  await withhold(there, other, local, progress);
3595
3553
  return 'done';
3596
3554
  }
3597
- const withheld = await advance(room, local, authorized, continuing);
3555
+ const withheld = await advanced(room, local, authorized);
3598
3556
  if (withheld !== null) {
3599
3557
  await withhold(withheld, room, local, progress);
3600
3558
  return 'done';
@@ -3616,8 +3574,8 @@ export function composeRuntime(options) {
3616
3574
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
3617
3575
  const left = leaving(head, result);
3618
3576
  const refreshed = async () => ({
3619
- there: other === undefined ? null : await advance(other, local, authorized, continuing),
3620
- here: await advance(room, local, authorized, continuing),
3577
+ there: other === undefined ? null : await advanced(other, local, authorized),
3578
+ here: await advanced(room, local, authorized),
3621
3579
  });
3622
3580
  const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
3623
3581
  if (judged !== null && (judged.there !== null || judged.here !== null)) {
@@ -3688,6 +3646,7 @@ export function composeRuntime(options) {
3688
3646
  locator: room.locator,
3689
3647
  key: before.record.key,
3690
3648
  roots: before.record.typedSelection?.roots ?? [],
3649
+ composed: composedOf(composed(before.record)),
3691
3650
  overlay: { set: [], cleared: [...before.overlaid] },
3692
3651
  },
3693
3652
  ]),
@@ -3720,39 +3679,37 @@ export function composeRuntime(options) {
3720
3679
  if (signal.aborted)
3721
3680
  return;
3722
3681
  const progress = { complete: true, reachable: true };
3723
- await roomTask(room.locator, async (live) => {
3724
- await Promise.race([
3725
- reload(room, local, progress, () => live() && !signal.aborted),
3726
- abandonedOn(signal),
3727
- ]);
3728
- });
3682
+ await roomTask(room.locator, (live) => reload(room, local, progress, live));
3729
3683
  if (signal.aborted)
3730
3684
  return;
3731
3685
  await rest(progress);
3732
3686
  }
3733
3687
  }
3734
3688
  function awaitingAnswer(locator) {
3689
+ answering.get(locator)?.release();
3735
3690
  let release = () => { };
3736
- const awaited = new Promise((resolve) => {
3691
+ const answered = new Promise((resolve) => {
3737
3692
  release = resolve;
3738
3693
  });
3739
- answering.set(locator, { generation, answered: awaited });
3694
+ const awaited = { answered, release };
3695
+ answering.set(locator, awaited);
3740
3696
  return () => {
3741
- if (answering.get(locator)?.answered === awaited)
3697
+ if (answering.get(locator) === awaited)
3742
3698
  answering.delete(locator);
3743
3699
  release();
3744
3700
  };
3745
3701
  }
3702
+ function declaredAsHeld(room) {
3703
+ const held = replicas.get(room.locator)?.record;
3704
+ return (held === undefined ||
3705
+ (JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
3706
+ JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
3707
+ JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
3708
+ }
3746
3709
  async function retainModel(local, room, next) {
3747
3710
  const key = room.locator;
3748
3711
  const previous = modelOf(room.locator);
3749
- const heldBefore = replicas.get(room.locator)?.record;
3750
- if (JSON.stringify(previous) === JSON.stringify(next) &&
3751
- (heldBefore === undefined ||
3752
- (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
3753
- JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
3754
- JSON.stringify(heldBefore.typedSelection) ===
3755
- JSON.stringify(room.typedSelection ?? null))))
3712
+ if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(room))
3756
3713
  return;
3757
3714
  if (next.status === 'unavailable') {
3758
3715
  invalidModels.add(key);
@@ -3764,26 +3721,13 @@ export function composeRuntime(options) {
3764
3721
  const held = replicas.get(room.locator);
3765
3722
  let changed = null;
3766
3723
  if (held !== undefined) {
3767
- const declared = {
3724
+ const record = {
3768
3725
  ...held.record,
3769
3726
  operations: room.operations,
3770
3727
  replica: room.replica,
3771
3728
  typedSelection: room.typedSelection ?? null,
3772
3729
  model: next,
3773
3730
  };
3774
- let read = 'staged';
3775
- if (!sameMembership(declared, held.record)) {
3776
- const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), composed(declared));
3777
- read =
3778
- verdict.verdict !== 'refuse'
3779
- ? 'staged'
3780
- : verdict.rule === 'model-unavailable'
3781
- ? 'deferred'
3782
- : { kind: 'refused', refusal: verdict };
3783
- }
3784
- const record = read === 'staged'
3785
- ? declared
3786
- : { ...declared, typedSelection: held.record.typedSelection };
3787
3731
  const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
3788
3732
  const replayedLayer = await replayed(record, confirmedLayer, held.queue);
3789
3733
  const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
@@ -3794,7 +3738,7 @@ export function composeRuntime(options) {
3794
3738
  key: record.key,
3795
3739
  record: storedRecord(record),
3796
3740
  roots: record.typedSelection?.roots ?? [],
3797
- composition: composed(record),
3741
+ composed: composedOf(composed(record)),
3798
3742
  overlay: diff.overlay,
3799
3743
  },
3800
3744
  ],
@@ -3805,29 +3749,22 @@ export function composeRuntime(options) {
3805
3749
  documents: answer.changed[room.locator] ?? [],
3806
3750
  room: true,
3807
3751
  });
3808
- if (read === 'staged' || read === 'deferred') {
3809
- if (declarationRefusals.delete(room.locator)) {
3810
- sourceRefusals.delete(room.locator);
3811
- unreadable.delete(room.locator);
3812
- }
3813
- }
3814
- else {
3815
- refusedSource(room, read);
3816
- declarationRefusals.add(room.locator);
3817
- }
3818
3752
  }
3819
- models.set(key, next);
3820
- if (next.status === 'current') {
3821
- liveModels.set(key, JSON.stringify(next.snapshot));
3822
- invalidModels.delete(key);
3823
- }
3824
- list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
3753
+ heldModel(room, next);
3825
3754
  if (changed === null)
3826
3755
  emitRooms();
3827
3756
  else
3828
3757
  publishChangeSet(changed);
3829
3758
  });
3830
3759
  }
3760
+ function heldModel(room, next) {
3761
+ models.set(room.locator, next);
3762
+ if (next.status === 'current') {
3763
+ liveModels.set(room.locator, JSON.stringify(next.snapshot));
3764
+ invalidModels.delete(room.locator);
3765
+ }
3766
+ list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
3767
+ }
3831
3768
  async function scoped(result, room, local) {
3832
3769
  if (result.scope === 'content' && !result.retryable) {
3833
3770
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
@@ -3856,6 +3793,17 @@ export function composeRuntime(options) {
3856
3793
  emitRooms();
3857
3794
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
3858
3795
  }
3796
+ function name(locator) {
3797
+ const stream = options.adapter.roomStream;
3798
+ const record = replicas.get(locator)?.record;
3799
+ const room = discovered.get(locator);
3800
+ if (stream === undefined || record === undefined || room === undefined)
3801
+ return;
3802
+ if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
3803
+ return;
3804
+ naming += 1;
3805
+ stream.hold([{ locator, roomVersion: record.roomVersion }]);
3806
+ }
3859
3807
  function heldRooms(listed) {
3860
3808
  return listed.flatMap((room) => {
3861
3809
  const record = replicas.get(room.locator)?.record;
@@ -3873,14 +3821,19 @@ export function composeRuntime(options) {
3873
3821
  return outcome?.status === 'fulfilled' && outcome.value;
3874
3822
  });
3875
3823
  if (attempt === 'opened') {
3824
+ naming += 1;
3876
3825
  options.adapter.roomStream?.hold(heldRooms(ready));
3877
3826
  handed = true;
3878
3827
  }
3879
- discovering?.release();
3880
- const loaded = await Promise.allSettled(ready
3881
- .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
3882
- .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
3883
- const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
3828
+ for (const room of ready) {
3829
+ if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
3830
+ if (attempt === 'authorized')
3831
+ name(room.locator);
3832
+ continue;
3833
+ }
3834
+ void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
3835
+ }
3836
+ const failed = prepared.find((room) => room.status === 'rejected');
3884
3837
  if (failed !== undefined)
3885
3838
  throw failed.reason;
3886
3839
  }
@@ -3904,7 +3857,9 @@ export function composeRuntime(options) {
3904
3857
  return room.replica.admission === 'admitted';
3905
3858
  }
3906
3859
  async function caughtUp(room, local, progress, live) {
3907
- if (authorization === undefined || !live() || !behind(room))
3860
+ if (options.adapter.roomStream !== undefined || authorization === undefined)
3861
+ return;
3862
+ if (!live() || !behind(room))
3908
3863
  return;
3909
3864
  const withheld = await advance(room, local, authorization, live);
3910
3865
  if (withheld !== null)
@@ -3917,6 +3872,8 @@ export function composeRuntime(options) {
3917
3872
  async function loadRoom(room, local, progress, live) {
3918
3873
  if (!live() || authorization === undefined)
3919
3874
  return;
3875
+ if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
3876
+ return;
3920
3877
  if (replicas.has(room.locator) && revoked(room.locator)) {
3921
3878
  await reload(room, local, progress, live);
3922
3879
  return;
@@ -3934,20 +3891,53 @@ export function composeRuntime(options) {
3934
3891
  reloadsDue.delete(room.locator);
3935
3892
  if (!live() || authorization === undefined)
3936
3893
  return;
3894
+ const from = standingOf(room.locator);
3937
3895
  const bootstrapped = await pullBootstrap(authorization, room, local, live);
3938
3896
  if (bootstrapped.kind !== 'staged') {
3939
3897
  if (live())
3940
3898
  await withhold(bootstrapped, room, local, progress);
3941
3899
  return;
3942
3900
  }
3943
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3901
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
3944
3902
  await caughtUp(room, local, progress, live);
3945
3903
  }
3946
3904
  finally {
3947
3905
  caughtUpTo(room.locator);
3948
3906
  }
3949
3907
  }
3950
- async function discoverRooms(local, progress, attempt, live, scope) {
3908
+ async function asked(authorized) {
3909
+ try {
3910
+ return (await options.adapter.discover?.(authorized)) ?? { kind: 'transport-uncertainty' };
3911
+ }
3912
+ catch {
3913
+ return { kind: 'transport-uncertainty' };
3914
+ }
3915
+ }
3916
+ function malformed(listed) {
3917
+ for (const room of listed) {
3918
+ if (room.operations === undefined ||
3919
+ !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
3920
+ room.operations[operation].every((scope) => typeof scope === 'string')) ||
3921
+ room.replica === undefined ||
3922
+ typeof room.replica.selection !== 'string' ||
3923
+ room.replica.selection.length === 0 ||
3924
+ !['admitted', 'unavailable'].includes(room.replica.admission) ||
3925
+ !['locked', 'open'].includes(room.replica.offline)) {
3926
+ return `invalid model or replica declaration ${room.locator}`;
3927
+ }
3928
+ if (!declaredSelection(room))
3929
+ return `invalid typed selection declaration ${room.locator}`;
3930
+ }
3931
+ return null;
3932
+ }
3933
+ async function signIn(local, signedIn) {
3934
+ if (JSON.stringify(signedIn) === JSON.stringify(member))
3935
+ return;
3936
+ await local.fact('member', signedIn);
3937
+ member = signedIn;
3938
+ emitMember();
3939
+ }
3940
+ async function discoverRooms(local, progress, attempt, live, found) {
3951
3941
  if (closed || authorization === undefined) {
3952
3942
  notifyReady();
3953
3943
  return;
@@ -3955,13 +3945,9 @@ export function composeRuntime(options) {
3955
3945
  moveTo(transferring(status));
3956
3946
  discoveries += 1;
3957
3947
  const started = discoveries;
3958
- let result;
3959
- try {
3960
- result = await options.adapter.discover(authorization, scope);
3961
- }
3962
- catch {
3963
- result = { kind: 'transport-uncertainty' };
3964
- }
3948
+ const result = found ?? (await asked(authorization));
3949
+ if (result.kind === 'discovery')
3950
+ roomsNamed = true;
3965
3951
  if (!live()) {
3966
3952
  progress.complete = false;
3967
3953
  notifyReady();
@@ -3969,9 +3955,7 @@ export function composeRuntime(options) {
3969
3955
  }
3970
3956
  journal.record(`connection discover ${result.kind}`);
3971
3957
  if (result.kind === 'transport-uncertainty') {
3972
- for (const room of rooms.values()) {
3973
- await retainModel(local, room, cached(modelOf(room.locator)));
3974
- }
3958
+ await cachedModels(local);
3975
3959
  progress.reachable = false;
3976
3960
  progress.complete = false;
3977
3961
  notifyReady();
@@ -3992,26 +3976,12 @@ export function composeRuntime(options) {
3992
3976
  notifyReady();
3993
3977
  return;
3994
3978
  }
3995
- for (const room of result.rooms) {
3996
- if (room.operations === undefined ||
3997
- !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
3998
- room.operations[operation].every((scope) => typeof scope === 'string')) ||
3999
- room.replica === undefined ||
4000
- typeof room.replica.selection !== 'string' ||
4001
- room.replica.selection.length === 0 ||
4002
- !['admitted', 'unavailable'].includes(room.replica.admission) ||
4003
- !['locked', 'open'].includes(room.replica.offline)) {
4004
- journal.record(`invalid model or replica declaration ${room.locator}`);
4005
- await refuse('missing-required-guarantee');
4006
- notifyReady();
4007
- return;
4008
- }
4009
- if (!declaredSelection(room)) {
4010
- journal.record(`invalid typed selection declaration ${room.locator}`);
4011
- await refuse('missing-required-guarantee');
4012
- notifyReady();
4013
- return;
4014
- }
3979
+ const invalid = malformed(result.rooms);
3980
+ if (invalid !== null) {
3981
+ journal.record(invalid);
3982
+ await refuse('missing-required-guarantee');
3983
+ notifyReady();
3984
+ return;
4015
3985
  }
4016
3986
  if (refusal !== null) {
4017
3987
  refusal = null;
@@ -4027,13 +3997,8 @@ export function composeRuntime(options) {
4027
3997
  await local.fact('providerLimits', limits);
4028
3998
  await local.fact('capabilities', capabilities);
4029
3999
  }
4030
- const signedIn = result.member ?? null;
4031
- if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
4032
- await local.fact('member', signedIn);
4033
- member = signedIn;
4034
- emitMember();
4035
- }
4036
- await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
4000
+ await signIn(local, result.member ?? null);
4001
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
4037
4002
  }
4038
4003
  async function ensureAuthorization() {
4039
4004
  if (authorization !== undefined)
@@ -4048,23 +4013,45 @@ export function composeRuntime(options) {
4048
4013
  }
4049
4014
  }
4050
4015
  async function admittedRead(work) {
4051
- if (roomReads.generation !== generation) {
4052
- roomReads = { generation, free: ROOM_READS, waiting: [] };
4016
+ while (roomReads.free === 0) {
4017
+ await new Promise((admit) => roomReads.waiting.push(admit));
4053
4018
  }
4054
- const reads = roomReads;
4055
- while (reads.free === 0)
4056
- await new Promise((admit) => reads.waiting.push(admit));
4057
- reads.free -= 1;
4019
+ roomReads.free -= 1;
4058
4020
  try {
4059
4021
  await work();
4060
4022
  }
4061
4023
  finally {
4062
- reads.free += 1;
4063
- reads.waiting.shift()?.();
4024
+ roomReads.free += 1;
4025
+ roomReads.waiting.shift()?.();
4064
4026
  }
4065
4027
  }
4028
+ function inRoom(locator, work) {
4029
+ roomWork += 1;
4030
+ const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
4031
+ try {
4032
+ return await work(continuing);
4033
+ }
4034
+ finally {
4035
+ roomWork -= 1;
4036
+ }
4037
+ });
4038
+ const chain = ran.then(() => undefined, () => undefined);
4039
+ roomLanes.set(locator, chain);
4040
+ working.add(chain);
4041
+ void chain.finally(() => {
4042
+ working.delete(chain);
4043
+ if (roomLanes.get(locator) === chain)
4044
+ roomLanes.delete(locator);
4045
+ });
4046
+ return ran;
4047
+ }
4066
4048
  function roomTask(locator, work) {
4067
- return task(roomLane(locator), (live) => admittedRead(() => work(live)));
4049
+ if (stopping)
4050
+ return Promise.resolve();
4051
+ return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
4052
+ }
4053
+ function advanced(room, local, authorized) {
4054
+ return inRoom(room.locator, (live) => advance(room, local, authorized, live));
4068
4055
  }
4069
4056
  function task(lane, work) {
4070
4057
  if (stopping)
@@ -4080,23 +4067,19 @@ export function composeRuntime(options) {
4080
4067
  void run.finally(() => working.delete(run));
4081
4068
  return run;
4082
4069
  }
4083
- function sync(attempt) {
4070
+ function sync(attempt, found) {
4084
4071
  return task(OPENING_LANE, async (live) => {
4085
4072
  if (!live()) {
4086
- notifyReady();
4073
+ readyFromStore();
4087
4074
  return;
4088
4075
  }
4089
4076
  const progress = { complete: true, reachable: true };
4090
- const gate = discover();
4091
- if (attempt === 'opened')
4092
- opened = generation;
4093
- underway = attempt === 'opened' ? { generation, progress } : null;
4077
+ underway = attempt === 'opened' ? progress : null;
4094
4078
  if (attempt === 'opened')
4095
4079
  streamed = progress;
4096
4080
  complete = false;
4097
4081
  recorded = false;
4098
4082
  moveTo(transferring(status));
4099
- handed = false;
4100
4083
  try {
4101
4084
  const local = await openLocal();
4102
4085
  if (!live())
@@ -4104,7 +4087,7 @@ export function composeRuntime(options) {
4104
4087
  await ensureAuthorization();
4105
4088
  if (!live())
4106
4089
  return;
4107
- await discoverRooms(local, progress, attempt, live);
4090
+ await discoverRooms(local, progress, attempt, live, found);
4108
4091
  }
4109
4092
  catch {
4110
4093
  progress.complete = false;
@@ -4112,7 +4095,6 @@ export function composeRuntime(options) {
4112
4095
  progress.reachable = false;
4113
4096
  }
4114
4097
  finally {
4115
- gate.release();
4116
4098
  notifyReady();
4117
4099
  }
4118
4100
  if (!live())
@@ -4124,35 +4106,53 @@ export function composeRuntime(options) {
4124
4106
  if (handed || closed || progress.reachable)
4125
4107
  return;
4126
4108
  underway = null;
4127
- opened = null;
4128
- journal.record('connection reopen');
4129
4109
  await rest(progress, live);
4130
- options.adapter.roomStream?.reopen();
4131
4110
  });
4132
4111
  }
4133
- function rediscover(scope) {
4112
+ function resynchronize() {
4134
4113
  return task(OPENING_LANE, async (live) => {
4135
- if (!live() || !socketOpen)
4114
+ if (!live() || !announced)
4136
4115
  return;
4137
4116
  const progress = { complete: true, reachable: true };
4138
- const gate = discover();
4139
4117
  moveTo(transferring(status));
4140
4118
  try {
4141
4119
  const local = await openLocal();
4142
4120
  if (!live())
4143
4121
  return;
4144
- await ensureAuthorization();
4122
+ await synchronize([...discovered.values()], local, progress, 'unstreamed', live);
4123
+ }
4124
+ catch {
4125
+ progress.complete = false;
4126
+ }
4127
+ if (live())
4128
+ await finishOpening(progress, live);
4129
+ });
4130
+ }
4131
+ function changeAuthority(change) {
4132
+ return task(OPENING_LANE, async (live) => {
4133
+ if (!live() || !announced)
4134
+ return;
4135
+ const progress = { complete: true, reachable: true };
4136
+ moveTo(transferring(status));
4137
+ try {
4138
+ const local = await openLocal();
4145
4139
  if (!live())
4146
4140
  return;
4147
- await discoverRooms(local, progress, 'authorized', live, scope);
4141
+ const invalid = malformed(change.rooms);
4142
+ if (invalid !== null) {
4143
+ journal.record(invalid);
4144
+ await refuse('missing-required-guarantee');
4145
+ return;
4146
+ }
4147
+ if (change.member !== undefined)
4148
+ await signIn(local, change.member);
4149
+ discoveries += 1;
4150
+ const listed = await applyDiscovery(change.rooms, discoveries, change.left);
4151
+ await synchronize(listed, local, progress, 'authorized', live);
4148
4152
  }
4149
4153
  catch {
4150
4154
  progress.complete = false;
4151
4155
  }
4152
- finally {
4153
- gate.release();
4154
- notifyReady();
4155
- }
4156
4156
  if (!live())
4157
4157
  return;
4158
4158
  if (!progress.complete)
@@ -4161,13 +4161,15 @@ export function composeRuntime(options) {
4161
4161
  });
4162
4162
  }
4163
4163
  function closedSocket(revoked, lost) {
4164
- const opened = socketOpen;
4164
+ notifyReady();
4165
+ const opened = announced;
4165
4166
  socketOpen = false;
4167
+ announced = false;
4168
+ handed = false;
4169
+ naming = 0;
4166
4170
  connectionLost();
4167
- if (lost) {
4168
- reads.lost();
4171
+ if (lost)
4169
4172
  conversations.lost();
4170
- }
4171
4173
  if (revoked) {
4172
4174
  void refuse('revoked-authorization');
4173
4175
  return;
@@ -4180,16 +4182,29 @@ export function composeRuntime(options) {
4180
4182
  state: status.state === 'synced' ? 'syncing' : status.state,
4181
4183
  providerUnreachable: status.providerUnreachable || !opened,
4182
4184
  });
4185
+ void closing(opened);
4183
4186
  }
4184
- function applyOpening(expired) {
4185
- const progress = underway?.generation === generation ? underway.progress : null;
4187
+ function closing(opened) {
4188
+ return task(OPENING_LANE, async (live) => {
4189
+ if (!live())
4190
+ return;
4191
+ const local = await openLocal();
4192
+ if (!opened && !socketOpen)
4193
+ await cachedModels(local);
4194
+ await rest(null, live);
4195
+ });
4196
+ }
4197
+ function cachedModels(local) {
4198
+ return settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
4199
+ }
4200
+ function caughtUpRooms(expired) {
4201
+ const progress = underway;
4202
+ underway = null;
4203
+ naming = Math.max(0, naming - 1);
4186
4204
  const cycle = progress ?? { complete: true, reachable: true };
4187
- if (opened === generation)
4188
- opened = null;
4189
4205
  const reloading = progress === null ? [] : owedReloads(expired);
4190
4206
  const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
4191
4207
  return task(OPENING_LANE, async (live) => {
4192
- underway = null;
4193
4208
  if (!live())
4194
4209
  return;
4195
4210
  if (progress === null) {
@@ -4197,24 +4212,14 @@ export function composeRuntime(options) {
4197
4212
  await rest(cycle, live);
4198
4213
  return;
4199
4214
  }
4200
- await roomsCarried([...expired, ...reloading]);
4201
- if (!live())
4202
- return;
4203
4215
  try {
4204
4216
  const local = await openLocal();
4205
- const reloaded = await replacing;
4217
+ await replacing;
4206
4218
  if (progress.reachable) {
4207
- for (const room of discovered.values()) {
4208
- if (replicas.has(room.locator) &&
4209
- !expired.includes(room.locator) &&
4210
- !reloading.includes(room.locator)) {
4211
- await confirmModels(room, local);
4212
- }
4213
- }
4219
+ await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
4220
+ !expired.includes(room.locator) &&
4221
+ !reloading.includes(room.locator)), 'current', local);
4214
4222
  }
4215
- const failed = reloaded.find((outcome) => outcome.status === 'rejected');
4216
- if (failed !== undefined)
4217
- throw failed.reason;
4218
4223
  }
4219
4224
  catch {
4220
4225
  progress.complete = false;
@@ -4226,11 +4231,8 @@ export function composeRuntime(options) {
4226
4231
  }
4227
4232
  function owedReloads(expired) {
4228
4233
  const reloading = [...discovered.values()]
4229
- .filter((room) => replicas.has(room.locator) &&
4230
- !expired.includes(room.locator) &&
4231
- (owed.has(room.locator) || revoked(room.locator)))
4234
+ .filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
4232
4235
  .map((room) => room.locator);
4233
- owed.clear();
4234
4236
  for (const locator of reloading)
4235
4237
  catching.add(locator);
4236
4238
  liveQueries.recover();
@@ -4239,22 +4241,44 @@ export function composeRuntime(options) {
4239
4241
  async function reloadNow(replaced, progress) {
4240
4242
  const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
4241
4243
  for (const room of reloaded)
4242
- reloadsDue.set(room.locator, generation);
4244
+ reloadsDue.add(room.locator);
4243
4245
  try {
4244
4246
  const local = await openLocal();
4245
- return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
4247
+ for (const room of reloaded) {
4248
+ void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
4249
+ }
4246
4250
  }
4247
4251
  catch {
4248
4252
  for (const room of reloaded)
4249
4253
  reloadsDue.delete(room.locator);
4250
4254
  progress.reachable = false;
4251
- return [];
4252
4255
  }
4253
4256
  }
4254
- async function confirmModels(room, local) {
4255
- const model = modelOf(room.locator);
4256
- if (model.status === 'cached')
4257
- await retainModel(local, room, { ...model, status: 'current' });
4257
+ async function settleModels(held, status, local) {
4258
+ const owed = (room) => {
4259
+ const model = modelOf(room.locator);
4260
+ return model.status !== 'unavailable' && model.status !== status;
4261
+ };
4262
+ const settled = await exclusive(async () => {
4263
+ if (!continuing())
4264
+ return [];
4265
+ const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
4266
+ for (const room of unchanged) {
4267
+ const next = { ...modelOf(room.locator), status };
4268
+ const replica = replicas.get(room.locator);
4269
+ if (replica !== undefined)
4270
+ replicas.set(room.locator, { ...replica, record: { ...replica.record, model: next } });
4271
+ heldModel(room, next);
4272
+ }
4273
+ if (unchanged.length > 0)
4274
+ emitRooms();
4275
+ return unchanged;
4276
+ });
4277
+ for (const room of held) {
4278
+ if (settled.includes(room) || !owed(room))
4279
+ continue;
4280
+ await retainModel(local, room, { ...modelOf(room.locator), status });
4281
+ }
4258
4282
  }
4259
4283
  function submitLater(locator) {
4260
4284
  if (localSaves)
@@ -4298,14 +4322,10 @@ export function composeRuntime(options) {
4298
4322
  try {
4299
4323
  const local = await openLocal();
4300
4324
  await roomTask(locator, async (live) => {
4301
- const current = () => live() && !signal.aborted;
4302
- if (!current() || authorization === undefined)
4325
+ if (!live() || authorization === undefined)
4303
4326
  return;
4304
- const withheld = await Promise.race([
4305
- advance(room, local, authorization, current),
4306
- abandonedOn(signal),
4307
- ]);
4308
- if (withheld !== null && current())
4327
+ const withheld = await advance(room, local, authorization, live);
4328
+ if (withheld !== null && live())
4309
4329
  await withhold(withheld, room, local, progress);
4310
4330
  });
4311
4331
  }
@@ -4360,14 +4380,11 @@ export function composeRuntime(options) {
4360
4380
  progress.complete = false;
4361
4381
  return;
4362
4382
  }
4363
- reads.applied();
4364
4383
  submitAll();
4365
4384
  for (const locator of stale)
4366
4385
  if (repairable.has(locator))
4367
4386
  repairLater(locator, {});
4368
4387
  await conversations.reconcile();
4369
- if (status.providerUnreachable)
4370
- conversations.reconnected();
4371
4388
  const reading = { complete: true, reachable: true };
4372
4389
  await conversations.catchUp(reading);
4373
4390
  if (progress.reachable && reading.reachable)
@@ -4393,7 +4410,7 @@ export function composeRuntime(options) {
4393
4410
  if (!live())
4394
4411
  return;
4395
4412
  let lastCompleted = status.lastCompleted;
4396
- let synced = complete && stale.size === 0 && !pending();
4413
+ let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
4397
4414
  if (synced && !recorded) {
4398
4415
  const completedAt = new Date().toISOString();
4399
4416
  try {
@@ -4408,74 +4425,69 @@ export function composeRuntime(options) {
4408
4425
  }
4409
4426
  }
4410
4427
  moveTo({
4411
- state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
4428
+ state: pending()
4429
+ ? 'pending'
4430
+ : synced && complete && roomWork === 0 && naming === 0
4431
+ ? 'synced'
4432
+ : 'syncing',
4412
4433
  lastCompleted,
4413
- providerUnreachable: !progress.reachable,
4434
+ providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
4414
4435
  ...attention(),
4415
4436
  });
4416
4437
  }
4417
- function discover() {
4418
- let release = () => { };
4419
- const done = new Promise((resolve) => {
4420
- release = resolve;
4421
- });
4422
- discovering = { generation, done, release };
4423
- return discovering;
4424
- }
4425
- function applyRoom(locator, roomVersion) {
4426
- announced.set(locator, roomVersion);
4427
- return task(roomLane(locator), async (live) => {
4428
- if (discovering?.generation === generation)
4429
- await discovering.done;
4430
- const saving = answering.get(locator);
4431
- if (saving?.generation === generation)
4432
- await saving.answered;
4433
- if (!live() || authorization === undefined)
4434
- return;
4435
- const room = discovered.get(locator);
4436
- const held = replicas.get(locator);
4437
- if (room === undefined || room.replica.admission !== 'admitted')
4438
- return;
4439
- if (held !== undefined &&
4440
- (held.record.replica.selection !== room.replica.selection ||
4441
- options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
4442
- return;
4443
- }
4444
- const during = underway?.generation === generation ? underway.progress : null;
4445
- const progress = during ?? { complete: true, reachable: true };
4446
- if (during === null)
4447
- moveTo(transferring(status));
4438
+ function applyRoom(change) {
4439
+ const { locator } = change;
4440
+ if (stopping)
4441
+ return Promise.resolve();
4442
+ return inRoom(locator, async (live) => {
4443
+ await answering.get(locator)?.answered;
4444
+ const progress = underway ?? { complete: true, reachable: true };
4448
4445
  try {
4449
4446
  const local = await openLocal();
4450
- await admittedRead(async () => {
4451
- if (!live() || authorization === undefined)
4452
- return;
4453
- const withheld = await advance(room, local, authorization, live);
4454
- if (withheld !== null)
4455
- await withhold(withheld, room, local, progress);
4456
- });
4447
+ for (;;) {
4448
+ if (!live())
4449
+ return null;
4450
+ const room = discovered.get(locator);
4451
+ const held = replicas.get(locator);
4452
+ if (room === undefined ||
4453
+ held === undefined ||
4454
+ room.replica.admission !== 'admitted' ||
4455
+ held.record.replica.selection !== room.replica.selection) {
4456
+ return null;
4457
+ }
4458
+ const at = held.record.roomVersion;
4459
+ if (options.adapter.reached(locator, at, change.roomVersion))
4460
+ return progress;
4461
+ if (!options.adapter.reached(locator, at, change.from)) {
4462
+ name(locator);
4463
+ return progress;
4464
+ }
4465
+ if (underway === null)
4466
+ moveTo(transferring(status));
4467
+ const writing = () => live() && !full;
4468
+ const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, () => change.read()));
4469
+ if (refreshed.kind !== 'advanced')
4470
+ return progress;
4471
+ const landing = {
4472
+ kind: 'refreshed',
4473
+ from: refreshed.from,
4474
+ remodelled: refreshed.remodelled,
4475
+ };
4476
+ if (await publish(local, room, refreshed.record, landing, writing)) {
4477
+ stale.delete(locator);
4478
+ await retainModel(local, room, modelOf(locator));
4479
+ return progress;
4480
+ }
4481
+ }
4457
4482
  }
4458
4483
  catch {
4484
+ if (!stale.has(locator))
4485
+ name(locator);
4459
4486
  stale.add(locator);
4487
+ return progress;
4460
4488
  }
4461
- finally {
4462
- if (during === null)
4463
- await rest(progress, live);
4464
- }
4465
- });
4489
+ }).then((progress) => rest(progress), () => undefined);
4466
4490
  }
4467
- function roomsCarried(replacing) {
4468
- const replaced = new Set(replacing.map(roomLane));
4469
- return Promise.all([...lanes]
4470
- .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
4471
- .map(([, held]) => held.chain));
4472
- }
4473
- const receipts = {
4474
- heads: async () => (await openLocal()).receivedHeads(),
4475
- parts: async (key, count) => (await openLocal()).receivedParts(key, count),
4476
- keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
4477
- drop: async (keys) => (await openLocal()).dropReceived(keys),
4478
- };
4479
4491
  function observeStream() {
4480
4492
  const stream = options.adapter.roomStream;
4481
4493
  if (stream === undefined || withdrawStream !== undefined)
@@ -4485,29 +4497,29 @@ export function composeRuntime(options) {
4485
4497
  return;
4486
4498
  if (event.kind === 'opened') {
4487
4499
  socketOpen = true;
4500
+ announced = false;
4501
+ handed = false;
4502
+ naming = 0;
4488
4503
  connectionLive();
4489
4504
  generation += 1;
4490
- reads.opened();
4491
4505
  conversations.reopened();
4492
4506
  recovery.resume();
4493
- void sync('opened');
4507
+ }
4508
+ else if (event.kind === 'discovered') {
4509
+ if (!socketOpen)
4510
+ return;
4511
+ announced = true;
4512
+ void sync('opened', event.discovery);
4494
4513
  }
4495
4514
  else if (event.kind === 'closed')
4496
4515
  closedSocket(event.revoked, event.lost);
4497
- else if (event.kind === 'opening')
4498
- void applyOpening(event.expired);
4516
+ else if (event.kind === 'caught-up')
4517
+ void caughtUpRooms(event.expired);
4499
4518
  else if (event.kind === 'authority-changed')
4500
- void rediscover(event.scope);
4519
+ void changeAuthority(event);
4501
4520
  else
4502
- void applyRoom(event.locator, event.roomVersion);
4503
- }, receipts, heldRevisions);
4504
- }
4505
- async function heldRevisions(locator, paths) {
4506
- const held = replicas.get(locator);
4507
- if (held === undefined || paths.length === 0)
4508
- return new Map();
4509
- const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
4510
- return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
4521
+ void applyRoom(event);
4522
+ });
4511
4523
  }
4512
4524
  function writesHeld() {
4513
4525
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -4577,10 +4589,9 @@ export function composeRuntime(options) {
4577
4589
  if (closed)
4578
4590
  return;
4579
4591
  closed = true;
4592
+ notifyDiscovered();
4580
4593
  if (probing !== null)
4581
4594
  clearInterval(probing);
4582
- for (const resolve of shownWaiters.splice(0))
4583
- resolve();
4584
4595
  connectionLive();
4585
4596
  lockListeners.clear();
4586
4597
  readinessWatches.clear();
@@ -4592,6 +4603,7 @@ export function composeRuntime(options) {
4592
4603
  conversations.close();
4593
4604
  void recovery.retire();
4594
4605
  roomListeners.clear();
4606
+ unlistedRooms.clear();
4595
4607
  refusalListeners.clear();
4596
4608
  storageLossListeners.clear();
4597
4609
  statusListeners.clear();
@@ -4604,6 +4616,7 @@ export function composeRuntime(options) {
4604
4616
  }
4605
4617
  async function stop() {
4606
4618
  stopping = true;
4619
+ inFlight.abort();
4607
4620
  await recovery.retire();
4608
4621
  await Promise.all(working);
4609
4622
  await mutations;
@@ -4617,12 +4630,12 @@ export function composeRuntime(options) {
4617
4630
  observeStream();
4618
4631
  void openLocal().then(() => {
4619
4632
  if (capabilities.length > 0)
4620
- notifyReady();
4633
+ readyFromStore();
4621
4634
  }, () => {
4622
- notifyReady();
4635
+ readyFromStore();
4623
4636
  });
4624
4637
  if (options.adapter.roomStream === undefined)
4625
- void sync('unstreamed');
4638
+ void sync('unstreamed', null);
4626
4639
  function planned(input, reach) {
4627
4640
  const plan = planOf(input, roomFacts(reach), planReach(reach));
4628
4641
  const named = plan.paths !== null &&
@@ -4770,9 +4783,10 @@ export function composeRuntime(options) {
4770
4783
  return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
4771
4784
  }
4772
4785
  if (topic === 'rooms') {
4773
- roomListeners.add(deliver);
4774
- deliver(listed());
4786
+ unlistedRooms.add(deliver);
4787
+ emitKnownRooms();
4775
4788
  return () => {
4789
+ unlistedRooms.delete(deliver);
4776
4790
  roomListeners.delete(deliver);
4777
4791
  };
4778
4792
  }
@@ -4826,7 +4840,17 @@ export function composeRuntime(options) {
4826
4840
  return openLocal().then(() => undefined);
4827
4841
  },
4828
4842
  rooms() {
4829
- return listed();
4843
+ if (closed)
4844
+ return Promise.resolve([]);
4845
+ return new Promise((settle) => {
4846
+ const answer = (value) => {
4847
+ unlistedRooms.delete(answer);
4848
+ roomListeners.delete(answer);
4849
+ settle(value);
4850
+ };
4851
+ unlistedRooms.add(answer);
4852
+ emitKnownRooms();
4853
+ });
4830
4854
  },
4831
4855
  query: ((input) => rowsQuery(planned(input, null))),
4832
4856
  search: (input) => searched(input, null, []),
@@ -4915,6 +4939,8 @@ export function composeRuntime(options) {
4915
4939
  part: readPart,
4916
4940
  read: readBinaryPath,
4917
4941
  link: linkBinaryPath,
4942
+ show: showBinaryPath,
4943
+ attach: attachBinary,
4918
4944
  },
4919
4945
  async call(name, input) {
4920
4946
  await ready;
@@ -4952,7 +4978,7 @@ export function composeRuntime(options) {
4952
4978
  authorization = undefined;
4953
4979
  discoveryPaused = false;
4954
4980
  if (options.adapter.roomStream === undefined)
4955
- await sync('unstreamed');
4981
+ await sync('unstreamed', null);
4956
4982
  else
4957
4983
  await task(OPENING_LANE, ensureAuthorization);
4958
4984
  },
@@ -4969,12 +4995,12 @@ export function composeRuntime(options) {
4969
4995
  lockedRooms,
4970
4996
  watchLocks,
4971
4997
  located,
4998
+ room: (locator) => listed().find((one) => one.locator === locator),
4972
4999
  };
4973
5000
  return {
4974
5001
  md,
4975
5002
  seed,
4976
5003
  stop,
4977
- lose: loseRooms,
4978
5004
  watch,
4979
5005
  persisted(answer) {
4980
5006
  if (answer === persistence)
@@ -4983,7 +5009,6 @@ export function composeRuntime(options) {
4983
5009
  journal.record(`persistence ${answer}`);
4984
5010
  queueReadiness();
4985
5011
  },
4986
- shown: screen,
4987
5012
  within: (reach, port) => {
4988
5013
  if (full)
4989
5014
  void probed();