@agentshouse/mdruntime 0.1.0-c93103f5357ff → 0.1.0-c94cfdbdb6f68

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 (90) hide show
  1. package/AGENTS.md +194 -194
  2. package/README.md +8 -8
  3. package/dist/adapter.d.ts +20 -6
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/bootstrap.d.ts +2 -1
  6. package/dist/bootstrap.d.ts.map +1 -1
  7. package/dist/chat/earlier.d.ts +10 -0
  8. package/dist/chat/earlier.d.ts.map +1 -0
  9. package/dist/chat/earlier.js +54 -0
  10. package/dist/chat/list.d.ts.map +1 -1
  11. package/dist/chat/list.js +39 -3
  12. package/dist/chat/provider.d.ts.map +1 -1
  13. package/dist/chat/provider.js +11 -12
  14. package/dist/chat/question.d.ts +1 -1
  15. package/dist/chat/question.d.ts.map +1 -1
  16. package/dist/chat/question.js +7 -18
  17. package/dist/chat/style.js +1 -1
  18. package/dist/client.d.ts +6 -2
  19. package/dist/client.d.ts.map +1 -1
  20. package/dist/client.js +7 -3
  21. package/dist/collection.d.ts.map +1 -1
  22. package/dist/collection.js +64 -34
  23. package/dist/connection.d.ts +9 -4
  24. package/dist/connection.d.ts.map +1 -1
  25. package/dist/connection.js +312 -154
  26. package/dist/conversation.d.ts +6 -0
  27. package/dist/conversation.d.ts.map +1 -1
  28. package/dist/conversation.js +161 -41
  29. package/dist/entry.d.ts.map +1 -1
  30. package/dist/entry.js +2 -1
  31. package/dist/host.d.ts +0 -3
  32. package/dist/host.d.ts.map +1 -1
  33. package/dist/host.js +8 -3
  34. package/dist/index.d.ts +3 -3
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/memory.d.ts +6 -3
  37. package/dist/memory.d.ts.map +1 -1
  38. package/dist/memory.js +17 -11
  39. package/dist/observation.d.ts +2 -0
  40. package/dist/observation.d.ts.map +1 -1
  41. package/dist/observation.js +9 -3
  42. package/dist/outcome.d.ts +8 -2
  43. package/dist/outcome.d.ts.map +1 -1
  44. package/dist/outcome.js +3 -0
  45. package/dist/query.d.ts +21 -1
  46. package/dist/query.d.ts.map +1 -1
  47. package/dist/query.js +28 -5
  48. package/dist/reach.d.ts.map +1 -1
  49. package/dist/reach.js +13 -0
  50. package/dist/react.d.ts +1 -2
  51. package/dist/react.d.ts.map +1 -1
  52. package/dist/react.js +2 -14
  53. package/dist/read-side.d.ts +3 -1
  54. package/dist/read-side.d.ts.map +1 -1
  55. package/dist/read-side.js +251 -69
  56. package/dist/reads.d.ts +11 -0
  57. package/dist/reads.d.ts.map +1 -0
  58. package/dist/reads.js +98 -0
  59. package/dist/refresh.d.ts +4 -4
  60. package/dist/refresh.d.ts.map +1 -1
  61. package/dist/replica.d.ts +5 -1
  62. package/dist/replica.d.ts.map +1 -1
  63. package/dist/replica.js +19 -3
  64. package/dist/rows.d.ts +1 -0
  65. package/dist/rows.d.ts.map +1 -1
  66. package/dist/rows.js +5 -0
  67. package/dist/save.d.ts +1 -0
  68. package/dist/save.d.ts.map +1 -1
  69. package/dist/save.js +22 -8
  70. package/dist/search.d.ts +15 -2
  71. package/dist/search.d.ts.map +1 -1
  72. package/dist/search.js +76 -17
  73. package/dist/seed.d.ts.map +1 -1
  74. package/dist/seed.js +18 -4
  75. package/dist/store.d.ts +16 -2
  76. package/dist/store.d.ts.map +1 -1
  77. package/dist/store.js +48 -14
  78. package/dist/turn.d.ts +3 -3
  79. package/dist/turn.d.ts.map +1 -1
  80. package/dist/turn.js +11 -5
  81. package/dist/windows.d.ts +26 -0
  82. package/dist/windows.d.ts.map +1 -0
  83. package/dist/windows.js +93 -0
  84. package/dist/wire.d.ts +16 -0
  85. package/dist/wire.d.ts.map +1 -1
  86. package/dist/wire.js +4 -0
  87. package/package.json +2 -2
  88. package/dist/acquisition.d.ts +0 -14
  89. package/dist/acquisition.d.ts.map +0 -1
  90. package/dist/acquisition.js +0 -24
@@ -1,11 +1,12 @@
1
1
  import { attachedModel } from './attached-model.js';
2
2
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
3
3
  import { observing, whole } from './observation.js';
4
- import { filterOf, identityOf, planOf, queryKey, } from './query.js';
4
+ import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
5
5
  import { createReadSide } from './read-side.js';
6
6
  import { seededResult } from './seed.js';
7
7
  import { markdownPath, roomOfKey } from './rows.js';
8
8
  import { createSearchIndex, searchPlanOf, } from './search.js';
9
+ import { capped, MAXIMUM_WINDOW } from './windows.js';
9
10
  import { bootstrapRoom } from './bootstrap.js';
10
11
  import { changeSet, entering, mergeChangeSets } from './change-set.js';
11
12
  import { createConversationStore, } from './conversation.js';
@@ -14,15 +15,15 @@ import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
14
15
  import { runtimeError } from './failure.js';
15
16
  import { createLog } from './log.js';
16
17
  import { binaryRecord } from './binary.js';
17
- import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
18
+ import { absentBinary, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
18
19
  import { createRecovery } from './recovery.js';
20
+ import { doorReads } from './reads.js';
19
21
  import { refreshRoom } from './refresh.js';
20
- import { buildBatch, joined, limitedIn, requeued } from './save.js';
21
- import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
22
- import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
22
+ import { buildBatch, joined, limitedIn, queuedDocuments, requeued, } from './save.js';
23
+ import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, standing, } from './replica.js';
24
+ import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
23
25
  import { same, saving, STARTING, transferring, } from './status.js';
24
26
  import { detectStorageLoss, nextGeneration } from './durability.js';
25
- import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
26
27
  import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
27
28
  import { FencedMap } from './fence.js';
28
29
  import { tally } from './shared.js';
@@ -37,6 +38,7 @@ function roomsAlone(plan) {
37
38
  plan.root.types === null &&
38
39
  plan.root.where === null &&
39
40
  plan.root.pathPrefix === null &&
41
+ plan.paths === null &&
40
42
  plan.hop === null &&
41
43
  plan.backlinks === null);
42
44
  }
@@ -57,6 +59,10 @@ const OPENING_LANE = 'opening';
57
59
  const CONVERSATION_LANE = 'conversations';
58
60
  const ROOM_READS = 256;
59
61
  const ROOM_ATTEMPTS = 16;
62
+ function operationOf(input) {
63
+ const operation = input?.operation_id;
64
+ return typeof operation === 'string' ? operation : null;
65
+ }
60
66
  const outside = new Map();
61
67
  function outsideOf(roots) {
62
68
  const key = JSON.stringify(roots);
@@ -110,6 +116,7 @@ export function composeRuntime(options) {
110
116
  const liveModels = new FencedMap(isModelFenced);
111
117
  const invalidModels = new Set();
112
118
  const discovered = new Map();
119
+ const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
113
120
  let roomsListed = false;
114
121
  const scopedRefusals = new Map();
115
122
  const sourceRefusals = new Map();
@@ -146,6 +153,7 @@ export function composeRuntime(options) {
146
153
  let handed = false;
147
154
  const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
148
155
  const repairing = new Set();
156
+ const calling = new Set();
149
157
  const repairable = new Map();
150
158
  let outcomeRetention = null;
151
159
  const answering = new Map();
@@ -182,8 +190,6 @@ export function composeRuntime(options) {
182
190
  const unpreparedDemand = new Set();
183
191
  const unindexable = new Set();
184
192
  let preparing = false;
185
- let demand = WHOLE_DEMAND;
186
- let narrowing = null;
187
193
  let targets = {};
188
194
  let persistence = options.shell.persistence;
189
195
  let capacity = null;
@@ -200,7 +206,7 @@ export function composeRuntime(options) {
200
206
  durably,
201
207
  stream: () => options.adapter.conversations,
202
208
  publish: publishChangeSet,
203
- online: () => !status.providerUnreachable && refusal === null,
209
+ online: () => connectionOf() !== 'unreachable' && refusal === null,
204
210
  closed: () => closed,
205
211
  unanswered: resendLater,
206
212
  expired: (operation) => pastHorizon(operation),
@@ -266,6 +272,15 @@ export function composeRuntime(options) {
266
272
  const value = presentations.get(app);
267
273
  return value === undefined ? null : structuredClone(value);
268
274
  }
275
+ function keepPresentation(app, next) {
276
+ return exclusive(async () => {
277
+ const local = await openLocal();
278
+ const stored = next(presentations.get(app));
279
+ await local.present({ app, value: stored });
280
+ presentations.set(app, stored);
281
+ publishChangeSet(changeSet('', null, null, { presentation: app }));
282
+ });
283
+ }
269
284
  function emitPresentation(app) {
270
285
  if (closed)
271
286
  return;
@@ -377,7 +392,7 @@ export function composeRuntime(options) {
377
392
  if (closed)
378
393
  return;
379
394
  queueReadiness();
380
- const next = status.state === 'synced' && demand.conversations && !conversations.current()
395
+ const next = status.state === 'synced' && !conversations.current()
381
396
  ? { ...status, state: 'syncing' }
382
397
  : status;
383
398
  if (same(shown, next))
@@ -410,8 +425,12 @@ export function composeRuntime(options) {
410
425
  ...set,
411
426
  documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
412
427
  };
413
- readSide.apply(held);
414
- searchIndex.changed(held);
428
+ const applied = readSide.apply(held, () => searchIndex.changed(held));
429
+ if (applied instanceof Promise)
430
+ void applied.catch(unpublished);
431
+ }
432
+ function unpublished(error) {
433
+ journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
415
434
  }
416
435
  async function referenced(locator) {
417
436
  for (;;) {
@@ -442,8 +461,8 @@ export function composeRuntime(options) {
442
461
  if (current === undefined || closed)
443
462
  return;
444
463
  const set = entering(locator, current, chunk);
445
- readSide.apply(set);
446
464
  entered.push(set);
465
+ return readSide.apply(set);
447
466
  });
448
467
  if (!replicas.has(locator) || closed)
449
468
  return;
@@ -536,57 +555,6 @@ export function composeRuntime(options) {
536
555
  if (waiting.length > 0)
537
556
  prepare(waiting.map(({ room }) => room));
538
557
  }
539
- function acquires(room) {
540
- return (demanded(demand, room) ||
541
- (replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
542
- queuesWithoutReplica.has(room.locator));
543
- }
544
- function acquiredLocators() {
545
- return [...discovered.values()]
546
- .filter(acquires)
547
- .map((room) => room.locator)
548
- .sort();
549
- }
550
- function heldDemand(acquired) {
551
- if (demand.whole)
552
- return null;
553
- return { rooms: acquired.map((room) => room.locator), conversations: demand.conversations };
554
- }
555
- function applyDemand(next) {
556
- const before = acquiredLocators();
557
- const conversed = demand.conversations;
558
- demand = next;
559
- const after = acquiredLocators();
560
- queueReadiness();
561
- if (!conversed && next.conversations)
562
- scheduleCurrency();
563
- if (JSON.stringify(before) === JSON.stringify(after) && conversed === next.conversations)
564
- return;
565
- journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
566
- if (options.adapter.roomStream === undefined)
567
- void sync('unstreamed');
568
- else if (socketOpen)
569
- options.adapter.roomStream.reopen();
570
- }
571
- function acquire(next) {
572
- if (closed)
573
- return;
574
- if (narrowing !== null)
575
- clearTimeout(narrowing);
576
- narrowing = null;
577
- const widening = wider(next, demand);
578
- if (!widening && !narrower(next, demand))
579
- return;
580
- if (widening || !roomsListed) {
581
- applyDemand(next);
582
- return;
583
- }
584
- // A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
585
- narrowing = setTimeout(() => {
586
- narrowing = null;
587
- applyDemand(next);
588
- }, 5_000);
589
- }
590
558
  function requiredRooms(reach) {
591
559
  if (reach === null)
592
560
  return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
@@ -784,10 +752,7 @@ export function composeRuntime(options) {
784
752
  void sync('unstreamed');
785
753
  }
786
754
  async function reclaimed(local) {
787
- const unreferenced = [...transfers.keys()].filter((locator) => {
788
- const room = discovered.get(locator);
789
- return room === undefined || !acquires(room);
790
- });
755
+ const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
791
756
  for (const locator of unreferenced) {
792
757
  await local.abandon(locator);
793
758
  transfers.delete(locator);
@@ -860,9 +825,7 @@ export function composeRuntime(options) {
860
825
  awaitPreparation(cover);
861
826
  if ('pending' in cover)
862
827
  return cover;
863
- return 'groups' in rows
864
- ? { groups: rows.groups, coverage: cover.coverage }
865
- : { documents: rows.documents, coverage: cover.coverage };
828
+ return { ...rows, coverage: cover.coverage };
866
829
  }
867
830
  async function indexConversation(conversation) {
868
831
  searchIndex.indexMessages(conversation, [], false);
@@ -1006,7 +969,7 @@ export function composeRuntime(options) {
1006
969
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
1007
970
  });
1008
971
  }
1009
- async function applyDiscovery(found, started) {
972
+ async function applyDiscovery(found, started, covered) {
1010
973
  for (const room of found) {
1011
974
  const fence = fences.get(room.locator);
1012
975
  if (fence !== undefined && fence.erased && fence.at < started)
@@ -1019,10 +982,14 @@ export function composeRuntime(options) {
1019
982
  const listedLocators = new Set(listed.map((room) => room.locator));
1020
983
  for (const locator of listedLocators)
1021
984
  scopedRefusals.delete(locator);
1022
- discovered.clear();
985
+ if (covered === undefined)
986
+ discovered.clear();
987
+ else
988
+ for (const locator of covered)
989
+ discovered.delete(locator);
1023
990
  for (const room of listed)
1024
991
  discovered.set(room.locator, room);
1025
- const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
992
+ const unlisted = unlistedHeld(new Set(found.map((room) => room.locator))).filter((locator) => covered === undefined || covered.includes(locator));
1026
993
  if (options.adapter.exhaustiveDiscovery === true)
1027
994
  await loseRooms(unlisted);
1028
995
  else
@@ -1280,8 +1247,9 @@ export function composeRuntime(options) {
1280
1247
  }
1281
1248
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
1282
1249
  const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
1283
- const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
1284
- const kept = queue.filter((batch) => !withdrawn.includes(batch));
1250
+ const { withdrawn, kept } = standing(confirmed, queue, withdrawable, localSaves);
1251
+ const founded = kept.filter((batch) => !queue.includes(batch));
1252
+ const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
1285
1253
  const unqueued = withdrawn.map((batch) => ({
1286
1254
  batch,
1287
1255
  returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
@@ -1305,9 +1273,10 @@ export function composeRuntime(options) {
1305
1273
  });
1306
1274
  }
1307
1275
  }
1308
- await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
1276
+ await local.keepAuthored({ locator, queue: kept, founded, roomDrafts: [...written.values()] });
1277
+ const staying = new Set(kept.map((batch) => batch.id));
1309
1278
  for (const batch of previousQueue) {
1310
- if (!kept.includes(batch))
1279
+ if (!staying.has(batch.id))
1311
1280
  pendings.delete(batch.id);
1312
1281
  }
1313
1282
  for (const record of written.values())
@@ -1328,7 +1297,7 @@ export function composeRuntime(options) {
1328
1297
  else
1329
1298
  queuesWithoutReplica.delete(locator);
1330
1299
  publishChangeSet(changeSet(locator, before ?? null, held, {
1331
- queue: kept.length !== previousQueue.length,
1300
+ queue: queueChanged,
1332
1301
  drafts: [...written.keys()],
1333
1302
  }));
1334
1303
  moveTo({ ...status, ...attention() });
@@ -1343,7 +1312,7 @@ export function composeRuntime(options) {
1343
1312
  replicas.set(locator, replicaOf(confirmed, kept, before));
1344
1313
  return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
1345
1314
  room: settling.room,
1346
- queue: kept.length !== previousQueue.length,
1315
+ queue: queueChanged,
1347
1316
  drafts: [...written.keys()],
1348
1317
  });
1349
1318
  };
@@ -1435,7 +1404,7 @@ export function composeRuntime(options) {
1435
1404
  return true;
1436
1405
  });
1437
1406
  }
1438
- async function saveRoomDrafts(locator, named, writtenBy) {
1407
+ async function savedRoom(locator) {
1439
1408
  const local = await openLocal();
1440
1409
  await referenced(locator);
1441
1410
  const replica = replicas.get(locator);
@@ -1443,6 +1412,10 @@ export function composeRuntime(options) {
1443
1412
  if (replica === undefined || room === undefined) {
1444
1413
  throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1445
1414
  }
1415
+ return { local, replica, room };
1416
+ }
1417
+ async function saveRoomDrafts(locator, named, writtenBy) {
1418
+ const target = await savedRoom(locator);
1446
1419
  const saved = [];
1447
1420
  for (const { path, revision } of named) {
1448
1421
  const record = roomDrafts.get(locator)?.get(path);
@@ -1452,6 +1425,38 @@ export function composeRuntime(options) {
1452
1425
  if (!saved.some((draft) => draft.path === path))
1453
1426
  saved.push({ path, parts: record.parts });
1454
1427
  }
1428
+ return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
1429
+ }
1430
+ async function saveParts(locator, named, writtenBy) {
1431
+ const read = named.map(({ path, parts }) => ({ path, parts: readParts(parts) }));
1432
+ if (read.some(({ path }, at) => path === '' ||
1433
+ normalize(path) !== path ||
1434
+ read.findIndex((one) => one.path === path) !== at)) {
1435
+ throw runtimeError('room-draft-malformed');
1436
+ }
1437
+ const target = await savedRoom(locator);
1438
+ const saved = [];
1439
+ for (const { path, parts } of read) {
1440
+ const file = parts.file === undefined
1441
+ ? undefined
1442
+ : await heldFile(parts.file.blob, binaryBase(target.replica.visible, path));
1443
+ saved.push({ path, parts: held(undefined, parts, file) });
1444
+ }
1445
+ return committed(locator, target, saved, new Set(), writtenBy);
1446
+ }
1447
+ function carriedDrafts(locator, saved, consumed) {
1448
+ const holding = roomDrafts.get(locator);
1449
+ return new Map(saved.flatMap(({ path, parts }) => {
1450
+ const record = holding?.get(path);
1451
+ if (parts.move === undefined || record === undefined || consumed.has(path))
1452
+ return [];
1453
+ const to = moveTarget(parts.move);
1454
+ if (to === path || holding?.has(to) === true)
1455
+ return [];
1456
+ return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
1457
+ }));
1458
+ }
1459
+ async function committed(locator, { local, replica, room }, saved, consumed, writtenBy) {
1455
1460
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
1456
1461
  if (changed.length > 0)
1457
1462
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
@@ -1484,6 +1489,7 @@ export function composeRuntime(options) {
1484
1489
  visible: replica.visible,
1485
1490
  changes,
1486
1491
  bases: parts.flatMap(({ bases }) => bases),
1492
+ queued: queuedDocuments(replica.queue),
1487
1493
  writtenBy,
1488
1494
  clean: {
1489
1495
  text: replica.confirmed.record.documents,
@@ -1491,7 +1497,9 @@ export function composeRuntime(options) {
1491
1497
  composition: compositionOf(replica.confirmed.record),
1492
1498
  },
1493
1499
  };
1494
- const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
1500
+ const checked = withdrawing
1501
+ ? buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
1502
+ : null;
1495
1503
  if (checked?.kind === 'refused')
1496
1504
  return { outcome: 'refused', ...checked.refusal };
1497
1505
  const owners = [...requeue.owners].flatMap((origin) => {
@@ -1514,6 +1522,7 @@ export function composeRuntime(options) {
1514
1522
  ...input,
1515
1523
  visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
1516
1524
  changes: requeue.changes,
1525
+ queued: queuedDocuments(requeue.queue),
1517
1526
  });
1518
1527
  if (built?.kind === 'refused')
1519
1528
  return { outcome: 'refused', ...built.refusal };
@@ -1548,13 +1557,16 @@ export function composeRuntime(options) {
1548
1557
  if (limited !== null)
1549
1558
  return { outcome: 'refused', ...limited };
1550
1559
  }
1551
- const consumed = saved.map(({ path }) => path);
1560
+ const carried = carriedDrafts(locator, saved, consumed);
1561
+ const left = [...consumed, ...carried.keys()];
1562
+ const arrived = [...carried.values()];
1552
1563
  try {
1553
1564
  await local.keepAuthored({
1554
1565
  locator,
1555
1566
  queue: batches,
1556
1567
  saved: drafted,
1557
- consumedRoomDrafts: consumed,
1568
+ consumedRoomDrafts: left,
1569
+ roomDrafts: arrived,
1558
1570
  });
1559
1571
  }
1560
1572
  catch {
@@ -1569,12 +1581,14 @@ export function composeRuntime(options) {
1569
1581
  }
1570
1582
  if (!localSaves)
1571
1583
  recorded = false;
1572
- for (const path of consumed)
1584
+ for (const path of left)
1573
1585
  forgetRoomDraft(locator, path);
1586
+ for (const record of arrived)
1587
+ rememberRoomDraft(record);
1574
1588
  replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
1575
1589
  publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
1576
1590
  queue: true,
1577
- drafts: consumed,
1591
+ drafts: [...left, ...arrived.map(({ path }) => path)],
1578
1592
  }));
1579
1593
  moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
1580
1594
  submitLater(locator);
@@ -1665,7 +1679,7 @@ export function composeRuntime(options) {
1665
1679
  const scope = options.adapter.referrerScope(request.referrer);
1666
1680
  return scope?.kind === 'room' ? scope.locator : null;
1667
1681
  }
1668
- async function readBinary(request) {
1682
+ async function transferBinary(request, transfer, answer) {
1669
1683
  if (closed || refusal !== null)
1670
1684
  return UNCERTAIN;
1671
1685
  try {
@@ -1681,40 +1695,41 @@ export function composeRuntime(options) {
1681
1695
  ? `transfer binary ${request.locator} ${request.path}`
1682
1696
  : `transfer binary ${request.fileRef}`);
1683
1697
  try {
1684
- const result = await options.adapter.binary(authorization, request);
1685
- journal.record(`outcome binary ${result.kind}`);
1698
+ const result = await transfer(authorization);
1699
+ const settled = result;
1700
+ journal.record(`outcome binary ${settled.kind}`);
1686
1701
  const locator = binaryRoom(request);
1687
1702
  if (locator !== null && fencedSince(locator, since))
1688
1703
  return failure({ kind: 'room-not-readable', locator });
1689
- if (result.kind === 'scoped-refusal') {
1690
- const room = replicas.get(result.locator)?.confirmed.record;
1704
+ if (settled.kind === 'scoped-refusal') {
1705
+ const room = replicas.get(settled.locator)?.confirmed.record;
1691
1706
  if (room !== undefined)
1692
- await scoped(result, room, await openLocal());
1707
+ await scoped(settled, room, await openLocal());
1693
1708
  }
1694
- if (result.kind === 'revoked-authorization')
1709
+ if (settled.kind === 'revoked-authorization')
1695
1710
  await refuse('revoked-authorization');
1696
- return binaryAnswer(result);
1711
+ return answer(result);
1697
1712
  }
1698
1713
  catch {
1699
1714
  journal.record('outcome binary transport-uncertainty');
1700
1715
  return UNCERTAIN;
1701
1716
  }
1702
1717
  }
1703
- async function readBinaryPath(locator, path, revision) {
1704
- await ready;
1718
+ function readBinary(request) {
1719
+ return transferBinary(request, (held) => options.adapter.binary(held, request), binaryAnswer);
1720
+ }
1721
+ function binaryPath(locator, path, revision) {
1705
1722
  if (revision !== undefined)
1706
- return readBinary({ kind: 'path', locator, path, revision });
1723
+ return { kind: 'path', locator, path, revision };
1707
1724
  const visible = replicas.get(locator)?.visible;
1708
1725
  const held = visible?.binaries[path];
1709
1726
  if (visible === undefined || held === undefined)
1710
- return absentBinary(path);
1711
- if (held.state === 'pending') {
1712
- const blob = queuedBlob(locator, path, held.localId);
1713
- return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1714
- }
1727
+ return null;
1728
+ if (held.state === 'pending')
1729
+ return { kind: 'pending', localId: held.localId };
1715
1730
  const record = binaryRecord(path, held, visible.revisions[path]);
1716
1731
  if (record.state !== 'accepted')
1717
- return absentBinary(path);
1732
+ return null;
1718
1733
  const replica = replicas.get(locator);
1719
1734
  const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
1720
1735
  const holds = (at) => {
@@ -1729,13 +1744,31 @@ export function composeRuntime(options) {
1729
1744
  if (change.kind === 'move' && change.to === confirmed)
1730
1745
  confirmed = change.path;
1731
1746
  }
1732
- return readBinary({
1747
+ return {
1733
1748
  kind: 'path',
1734
1749
  locator,
1735
1750
  path: confirmed,
1736
1751
  revision: record.revision,
1737
1752
  fileRef: record.fileRef,
1738
- });
1753
+ };
1754
+ }
1755
+ async function readBinaryPath(locator, path, revision) {
1756
+ await ready;
1757
+ const asked = binaryPath(locator, path, revision);
1758
+ if (asked === null)
1759
+ return absentBinary(path);
1760
+ if (asked.kind === 'pending') {
1761
+ const blob = queuedBlob(locator, path, asked.localId);
1762
+ return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1763
+ }
1764
+ return readBinary(asked);
1765
+ }
1766
+ async function linkBinaryPath(locator, path, revision) {
1767
+ await ready;
1768
+ const asked = binaryPath(locator, path, revision);
1769
+ if (asked === null || asked.kind === 'pending')
1770
+ return absentBinary(path);
1771
+ return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
1739
1772
  }
1740
1773
  async function readBinaryReference(reference) {
1741
1774
  await ready;
@@ -1761,7 +1794,7 @@ export function composeRuntime(options) {
1761
1794
  }
1762
1795
  return undefined;
1763
1796
  }
1764
- async function invokeAction(name, input) {
1797
+ async function invokeAction(name, input, dispatching) {
1765
1798
  await ready;
1766
1799
  if (closed || refusal !== null)
1767
1800
  return UNCERTAIN;
@@ -1775,6 +1808,7 @@ export function composeRuntime(options) {
1775
1808
  }
1776
1809
  if (closed || authorization === undefined || refusal !== null)
1777
1810
  return UNCERTAIN;
1811
+ await dispatching?.();
1778
1812
  journal.record(`transfer action ${name}`);
1779
1813
  try {
1780
1814
  const result = await options.adapter.action(authorization, { name, input });
@@ -1795,6 +1829,60 @@ export function composeRuntime(options) {
1795
1829
  return UNCERTAIN;
1796
1830
  }
1797
1831
  }
1832
+ function unsettled(result) {
1833
+ return result.kind !== 'action' && result.retryable;
1834
+ }
1835
+ async function recoveredAction(name, input) {
1836
+ const operation = operationOf(input);
1837
+ if (operation === null)
1838
+ return invokeAction(name, input);
1839
+ const kept = async () => (await openLocal()).keepCall({ operation, name, input });
1840
+ calling.add(operation);
1841
+ try {
1842
+ let result = await invokeAction(name, input, kept);
1843
+ if (unsettled(result)) {
1844
+ await recovery.attempt(`action ${operation}`, async () => {
1845
+ if (closed || refusal !== null)
1846
+ return 'done';
1847
+ if (!socketOpen)
1848
+ return 'again';
1849
+ result = await invokeAction(name, input, kept);
1850
+ return unsettled(result) ? 'again' : 'done';
1851
+ }, { again: true });
1852
+ }
1853
+ if (!unsettled(result))
1854
+ await (await openLocal()).dropCall(operation);
1855
+ return result;
1856
+ }
1857
+ finally {
1858
+ calling.delete(operation);
1859
+ }
1860
+ }
1861
+ async function resendCalls(signal) {
1862
+ if (!socketOpen || closed || stopping)
1863
+ return 'done';
1864
+ const local = await openLocal();
1865
+ for (const held of await local.calls()) {
1866
+ if (calling.has(held.operation))
1867
+ continue;
1868
+ if (closed || signal.aborted)
1869
+ return 'done';
1870
+ if (pastHorizon(held.operation)) {
1871
+ await local.dropCall(held.operation);
1872
+ continue;
1873
+ }
1874
+ calling.add(held.operation);
1875
+ try {
1876
+ if (unsettled(await invokeAction(held.name, held.input)))
1877
+ return 'again';
1878
+ await local.dropCall(held.operation);
1879
+ }
1880
+ finally {
1881
+ calling.delete(held.operation);
1882
+ }
1883
+ }
1884
+ return 'done';
1885
+ }
1798
1886
  function continuing() {
1799
1887
  return !closed && !discoveryPaused;
1800
1888
  }
@@ -1812,8 +1900,9 @@ export function composeRuntime(options) {
1812
1900
  async function pullRefresh(authorized, confirmed, live) {
1813
1901
  if (!live())
1814
1902
  return unconfirmed;
1903
+ const turn = generation;
1815
1904
  try {
1816
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1905
+ const result = await refreshRoom(reads, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1817
1906
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
1818
1907
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
1819
1908
  return result;
@@ -1822,6 +1911,9 @@ export function composeRuntime(options) {
1822
1911
  journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
1823
1912
  throw error;
1824
1913
  }
1914
+ finally {
1915
+ reads.finished(confirmed.record.locator, turn);
1916
+ }
1825
1917
  }
1826
1918
  function staging(local, live) {
1827
1919
  return {
@@ -1864,12 +1956,13 @@ export function composeRuntime(options) {
1864
1956
  async function pullBootstrap(authorized, room, local, live) {
1865
1957
  if (!live())
1866
1958
  return unconfirmed;
1959
+ const turn = generation;
1867
1960
  if (replicas.has(room.locator)) {
1868
1961
  catching.add(room.locator);
1869
1962
  readSide.recover();
1870
1963
  }
1871
1964
  try {
1872
- const result = await bootstrapRoom(options.adapter, authorized, room, {
1965
+ const result = await bootstrapRoom(reads, authorized, room, {
1873
1966
  collection: modelOf(room.locator),
1874
1967
  selection: modelOf(room.locator, 'typed-selection'),
1875
1968
  }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
@@ -1884,12 +1977,28 @@ export function composeRuntime(options) {
1884
1977
  caughtUpTo(room.locator);
1885
1978
  throw error;
1886
1979
  }
1980
+ finally {
1981
+ reads.finished(room.locator, turn);
1982
+ }
1887
1983
  }
1888
1984
  function caughtUpTo(locator) {
1889
1985
  if (catching.delete(locator))
1890
1986
  readSide.recover();
1891
1987
  }
1988
+ function behind(room) {
1989
+ const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1990
+ return (held !== undefined &&
1991
+ [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
1992
+ }
1892
1993
  async function advance(room, local, authorized, live) {
1994
+ const carried = reads.carried(room.locator);
1995
+ const withheld = await advanceOnce(room, local, authorized, live);
1996
+ if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
1997
+ return withheld;
1998
+ }
1999
+ return advanceOnce(room, local, authorized, live);
2000
+ }
2001
+ async function advanceOnce(room, local, authorized, live) {
1893
2002
  await referenced(room.locator);
1894
2003
  if (!live() || paused.has(room.locator))
1895
2004
  return unconfirmed;
@@ -2377,16 +2486,15 @@ export function composeRuntime(options) {
2377
2486
  });
2378
2487
  }
2379
2488
  async function synchronize(listed, local, progress, attempt, live) {
2380
- const acquired = listed.filter(acquires);
2381
- const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
2489
+ const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
2382
2490
  if (!live())
2383
2491
  return;
2384
- const ready = acquired.filter((_, index) => {
2492
+ const ready = listed.filter((_, index) => {
2385
2493
  const outcome = prepared[index];
2386
2494
  return outcome?.status === 'fulfilled' && outcome.value;
2387
2495
  });
2388
2496
  if (attempt === 'opened') {
2389
- options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
2497
+ options.adapter.roomStream?.hold(heldRooms(ready));
2390
2498
  handed = true;
2391
2499
  }
2392
2500
  discovering?.release();
@@ -2417,11 +2525,7 @@ export function composeRuntime(options) {
2417
2525
  return room.replica.admission === 'admitted';
2418
2526
  }
2419
2527
  async function caughtUp(room, local, progress, live) {
2420
- const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
2421
- if (held === undefined || authorization === undefined || !live())
2422
- return;
2423
- const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
2424
- if (!behind)
2528
+ if (authorization === undefined || !live() || !behind(room))
2425
2529
  return;
2426
2530
  const withheld = await advance(room, local, authorization, live);
2427
2531
  if (withheld !== null)
@@ -2465,7 +2569,7 @@ export function composeRuntime(options) {
2465
2569
  caughtUpTo(room.locator);
2466
2570
  }
2467
2571
  }
2468
- async function discoverRooms(local, progress, attempt, live) {
2572
+ async function discoverRooms(local, progress, attempt, live, scope) {
2469
2573
  if (closed || authorization === undefined) {
2470
2574
  notifyReady();
2471
2575
  return;
@@ -2475,7 +2579,7 @@ export function composeRuntime(options) {
2475
2579
  const started = discoveries;
2476
2580
  let result;
2477
2581
  try {
2478
- result = await options.adapter.discover(authorization);
2582
+ result = await options.adapter.discover(authorization, scope);
2479
2583
  }
2480
2584
  catch {
2481
2585
  result = { kind: 'transport-uncertainty' };
@@ -2552,7 +2656,7 @@ export function composeRuntime(options) {
2552
2656
  member = signedIn;
2553
2657
  emitMember();
2554
2658
  }
2555
- await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
2659
+ await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
2556
2660
  }
2557
2661
  async function ensureAuthorization() {
2558
2662
  if (authorization !== undefined)
@@ -2649,9 +2753,43 @@ export function composeRuntime(options) {
2649
2753
  options.adapter.roomStream?.reopen();
2650
2754
  });
2651
2755
  }
2652
- function closedSocket(revoked) {
2756
+ function rediscover(scope) {
2757
+ return task(OPENING_LANE, async (live) => {
2758
+ if (!live() || !socketOpen)
2759
+ return;
2760
+ const progress = { complete: true, reachable: true };
2761
+ const gate = discover();
2762
+ moveTo(transferring(status));
2763
+ try {
2764
+ const local = await openLocal();
2765
+ if (!live())
2766
+ return;
2767
+ await ensureAuthorization();
2768
+ if (!live())
2769
+ return;
2770
+ await discoverRooms(local, progress, 'authorized', live, scope);
2771
+ }
2772
+ catch {
2773
+ progress.complete = false;
2774
+ }
2775
+ finally {
2776
+ gate.release();
2777
+ notifyReady();
2778
+ }
2779
+ if (!live())
2780
+ return;
2781
+ if (!progress.complete)
2782
+ complete = false;
2783
+ await rest(progress, live);
2784
+ });
2785
+ }
2786
+ function closedSocket(revoked, lost) {
2653
2787
  const opened = socketOpen;
2654
2788
  socketOpen = false;
2789
+ if (lost) {
2790
+ reads.lost();
2791
+ conversations.lost();
2792
+ }
2655
2793
  if (revoked) {
2656
2794
  void refuse('revoked-authorization');
2657
2795
  return;
@@ -2668,9 +2806,10 @@ export function composeRuntime(options) {
2668
2806
  function applyOpening(expired) {
2669
2807
  const progress = underway?.generation === generation ? underway.progress : null;
2670
2808
  const cycle = progress ?? { complete: true, reachable: true };
2671
- const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
2672
2809
  if (opened === generation)
2673
2810
  opened = null;
2811
+ const reloading = progress === null ? [] : owedReloads(expired);
2812
+ const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
2674
2813
  return task(OPENING_LANE, async (live) => {
2675
2814
  underway = null;
2676
2815
  if (!live())
@@ -2680,24 +2819,17 @@ export function composeRuntime(options) {
2680
2819
  await rest(cycle, live);
2681
2820
  return;
2682
2821
  }
2683
- await roomsCarried(expired);
2822
+ await roomsCarried([...expired, ...reloading]);
2684
2823
  if (!live())
2685
2824
  return;
2686
- const replaced = new Set(owed);
2687
- owed.clear();
2688
2825
  try {
2689
2826
  const local = await openLocal();
2690
- const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
2691
- const reloading = held.filter((room) => !expired.includes(room.locator) &&
2692
- (replaced.has(room.locator) || revoked(room.locator)));
2693
- for (const room of reloading)
2694
- catching.add(room.locator);
2695
- readSide.recover();
2696
- const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
2697
- const reloaded = [...(await reloads), ...(await replacing)];
2827
+ const reloaded = await replacing;
2698
2828
  if (progress.reachable) {
2699
- for (const room of held) {
2700
- if (!expired.includes(room.locator) && !reloading.includes(room)) {
2829
+ for (const room of discovered.values()) {
2830
+ if (replicas.has(room.locator) &&
2831
+ !expired.includes(room.locator) &&
2832
+ !reloading.includes(room.locator)) {
2701
2833
  await confirmModels(room, local);
2702
2834
  }
2703
2835
  }
@@ -2714,6 +2846,18 @@ export function composeRuntime(options) {
2714
2846
  await finishOpening(progress, live);
2715
2847
  });
2716
2848
  }
2849
+ function owedReloads(expired) {
2850
+ const reloading = [...discovered.values()]
2851
+ .filter((room) => replicas.has(room.locator) &&
2852
+ !expired.includes(room.locator) &&
2853
+ (owed.has(room.locator) || revoked(room.locator)))
2854
+ .map((room) => room.locator);
2855
+ owed.clear();
2856
+ for (const locator of reloading)
2857
+ catching.add(locator);
2858
+ readSide.recover();
2859
+ return reloading;
2860
+ }
2717
2861
  async function reloadNow(replaced, progress) {
2718
2862
  try {
2719
2863
  const local = await openLocal();
@@ -2838,6 +2982,7 @@ export function composeRuntime(options) {
2838
2982
  progress.complete = false;
2839
2983
  return;
2840
2984
  }
2985
+ reads.applied();
2841
2986
  submitAll();
2842
2987
  for (const locator of stale)
2843
2988
  if (repairable.has(locator))
@@ -2846,10 +2991,11 @@ export function composeRuntime(options) {
2846
2991
  if (status.providerUnreachable)
2847
2992
  conversations.reconnected();
2848
2993
  const reading = { complete: true, reachable: true };
2849
- if (demand.conversations)
2850
- await conversations.catchUp(reading);
2994
+ await conversations.catchUp(reading);
2851
2995
  if (progress.reachable && reading.reachable)
2852
2996
  await conversations.resend(reading);
2997
+ if (progress.reachable)
2998
+ void recovery.attempt('calls', resendCalls);
2853
2999
  if (!reading.reachable) {
2854
3000
  progress.reachable = false;
2855
3001
  resendLater();
@@ -2910,7 +3056,7 @@ export function composeRuntime(options) {
2910
3056
  return;
2911
3057
  const room = discovered.get(locator);
2912
3058
  const held = replicas.get(locator);
2913
- if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
3059
+ if (room === undefined || room.replica.admission !== 'admitted')
2914
3060
  return;
2915
3061
  if (held !== undefined &&
2916
3062
  (held.confirmed.record.replica.selection !== room.replica.selection ||
@@ -2962,14 +3108,17 @@ export function composeRuntime(options) {
2962
3108
  if (event.kind === 'opened') {
2963
3109
  socketOpen = true;
2964
3110
  generation += 1;
3111
+ reads.opened();
2965
3112
  conversations.reopened();
2966
3113
  recovery.resume();
2967
3114
  void sync('opened');
2968
3115
  }
2969
3116
  else if (event.kind === 'closed')
2970
- closedSocket(event.revoked);
3117
+ closedSocket(event.revoked, event.lost);
2971
3118
  else if (event.kind === 'opening')
2972
3119
  void applyOpening(event.expired);
3120
+ else if (event.kind === 'authority-changed')
3121
+ void rediscover(event.scope);
2973
3122
  else
2974
3123
  void applyRoom(event.locator, event.roomVersion);
2975
3124
  }, receipts);
@@ -3024,7 +3173,7 @@ export function composeRuntime(options) {
3024
3173
  if (!live())
3025
3174
  return;
3026
3175
  await conversations.reconcile();
3027
- if (conversations.current() || !demand.conversations) {
3176
+ if (conversations.current()) {
3028
3177
  show();
3029
3178
  return;
3030
3179
  }
@@ -3042,8 +3191,6 @@ export function composeRuntime(options) {
3042
3191
  if (closed)
3043
3192
  return;
3044
3193
  closed = true;
3045
- if (narrowing !== null)
3046
- clearTimeout(narrowing);
3047
3194
  if (rechecking !== null)
3048
3195
  clearInterval(rechecking);
3049
3196
  readinessWatches.clear();
@@ -3091,7 +3238,11 @@ export function composeRuntime(options) {
3091
3238
  if (options.adapter.roomStream === undefined)
3092
3239
  void sync('unstreamed');
3093
3240
  function planned(input, reach) {
3094
- return planOf(input, roomFacts(reach), planReach(reach));
3241
+ const plan = planOf(input, roomFacts(reach), planReach(reach));
3242
+ const named = plan.paths !== null &&
3243
+ plan.rooms !== null &&
3244
+ plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
3245
+ return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
3095
3246
  }
3096
3247
  function watchPlan(plan, deliver) {
3097
3248
  let seeded = false;
@@ -3128,7 +3279,8 @@ export function composeRuntime(options) {
3128
3279
  };
3129
3280
  }
3130
3281
  async function searched(input, reach) {
3131
- const plan = searchPlanOf(input);
3282
+ const asked = searchPlanOf(input);
3283
+ const plan = { ...asked, limit: capped(asked.limit) };
3132
3284
  const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
3133
3285
  for (;;) {
3134
3286
  const cover = covered(filter);
@@ -3167,6 +3319,7 @@ export function composeRuntime(options) {
3167
3319
  if ('pending' in answered)
3168
3320
  continue;
3169
3321
  const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
3322
+ await readSide.settled();
3170
3323
  const seen = searchIndex.changes();
3171
3324
  await searchIndex.caughtUp();
3172
3325
  const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
@@ -3187,7 +3340,9 @@ export function composeRuntime(options) {
3187
3340
  index: searchIndex.generation(),
3188
3341
  overlay: answered.coverage.overlay,
3189
3342
  };
3190
- return complete ? { ...hits, coverage } : { pending, ...hits, coverage };
3343
+ return complete
3344
+ ? { ...hits, coverage }
3345
+ : { pending, documents: hits.documents, messages: hits.messages, coverage };
3191
3346
  }
3192
3347
  }
3193
3348
  const watch = (topic, input, deliver) => {
@@ -3372,27 +3527,31 @@ export function composeRuntime(options) {
3372
3527
  });
3373
3528
  },
3374
3529
  write(app, value) {
3375
- return exclusive(async () => {
3376
- const local = await openLocal();
3377
- const stored = structuredClone(value);
3378
- await local.present({ app, value: stored });
3379
- presentations.set(app, stored);
3380
- publishChangeSet(changeSet('', null, null, { presentation: app }));
3381
- });
3530
+ return keepPresentation(app, () => structuredClone(value));
3531
+ },
3532
+ edit(app, members) {
3533
+ return keepPresentation(app, (held) => Object.fromEntries([
3534
+ ...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(members, member)),
3535
+ ...Object.entries(structuredClone(members)).filter(([, value]) => value !== null),
3536
+ ]));
3382
3537
  },
3383
3538
  },
3384
3539
  save(room, named, writtenBy) {
3385
3540
  return exclusive(() => saveRoomDrafts(room, named, writtenBy));
3386
3541
  },
3542
+ saveParts(room, parts, writtenBy) {
3543
+ return exclusive(() => saveParts(room, parts, writtenBy));
3544
+ },
3387
3545
  history(locator, path, query) {
3388
3546
  return readHistory(locator, path, query);
3389
3547
  },
3390
3548
  binaryFile: {
3391
3549
  read: readBinaryPath,
3550
+ link: linkBinaryPath,
3392
3551
  readReference: readBinaryReference,
3393
3552
  },
3394
3553
  call(name, input) {
3395
- return invokeAction(name, input);
3554
+ return recoveredAction(name, input);
3396
3555
  },
3397
3556
  reached(locator, roomVersion, path) {
3398
3557
  return path === undefined
@@ -3444,7 +3603,6 @@ export function composeRuntime(options) {
3444
3603
  stop,
3445
3604
  lose: loseRooms,
3446
3605
  watch,
3447
- acquire,
3448
3606
  persisted(answer) {
3449
3607
  if (answer === persistence)
3450
3608
  return;