@agentshouse/mdruntime 0.1.0-c5d3fa6fb67bc → 0.1.0-c6636685d0c4b

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 (135) hide show
  1. package/AGENTS.md +205 -205
  2. package/README.md +8 -8
  3. package/dist/adapter.d.ts +36 -9
  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/bootstrap.js +1 -1
  8. package/dist/change-set.d.ts +5 -2
  9. package/dist/change-set.d.ts.map +1 -1
  10. package/dist/change-set.js +6 -1
  11. package/dist/chat/earlier.d.ts +10 -0
  12. package/dist/chat/earlier.d.ts.map +1 -0
  13. package/dist/chat/earlier.js +54 -0
  14. package/dist/chat/kit.js +1 -1
  15. package/dist/chat/list.d.ts +2 -1
  16. package/dist/chat/list.d.ts.map +1 -1
  17. package/dist/chat/list.js +45 -5
  18. package/dist/chat/markdown.d.ts +1 -0
  19. package/dist/chat/markdown.d.ts.map +1 -1
  20. package/dist/chat/markdown.js +78 -20
  21. package/dist/chat/message.d.ts +1 -0
  22. package/dist/chat/message.d.ts.map +1 -1
  23. package/dist/chat/message.js +12 -2
  24. package/dist/chat/messages.d.ts +2 -0
  25. package/dist/chat/messages.d.ts.map +1 -1
  26. package/dist/chat/messages.js +52 -20
  27. package/dist/chat/overlay.d.ts +1 -0
  28. package/dist/chat/overlay.d.ts.map +1 -1
  29. package/dist/chat/overlay.js +10 -0
  30. package/dist/chat/pieces.d.ts +62 -0
  31. package/dist/chat/pieces.d.ts.map +1 -0
  32. package/dist/chat/pieces.js +1110 -0
  33. package/dist/chat/provider.d.ts +1 -0
  34. package/dist/chat/provider.d.ts.map +1 -1
  35. package/dist/chat/provider.js +23 -14
  36. package/dist/chat/question.d.ts +1 -1
  37. package/dist/chat/question.d.ts.map +1 -1
  38. package/dist/chat/question.js +7 -18
  39. package/dist/chat/ready.d.ts +1 -1
  40. package/dist/chat/ready.d.ts.map +1 -1
  41. package/dist/chat/ready.js +1 -1
  42. package/dist/chat/steady.d.ts +3 -0
  43. package/dist/chat/steady.d.ts.map +1 -0
  44. package/dist/chat/steady.js +26 -0
  45. package/dist/chat/style.js +1 -1
  46. package/dist/chat.d.ts +2 -2
  47. package/dist/chat.d.ts.map +1 -1
  48. package/dist/chat.js +2 -2
  49. package/dist/client.d.ts +6 -2
  50. package/dist/client.d.ts.map +1 -1
  51. package/dist/client.js +16 -5
  52. package/dist/collection.d.ts +4 -3
  53. package/dist/collection.d.ts.map +1 -1
  54. package/dist/collection.js +128 -63
  55. package/dist/connection.d.ts +12 -4
  56. package/dist/connection.d.ts.map +1 -1
  57. package/dist/connection.js +577 -217
  58. package/dist/conversation.d.ts +12 -0
  59. package/dist/conversation.d.ts.map +1 -1
  60. package/dist/conversation.js +196 -41
  61. package/dist/entry.d.ts.map +1 -1
  62. package/dist/entry.js +2 -1
  63. package/dist/host.d.ts +5 -4
  64. package/dist/host.d.ts.map +1 -1
  65. package/dist/host.js +45 -5
  66. package/dist/index.d.ts +5 -4
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +1 -0
  69. package/dist/memory.d.ts +6 -3
  70. package/dist/memory.d.ts.map +1 -1
  71. package/dist/memory.js +17 -11
  72. package/dist/observation.d.ts +2 -0
  73. package/dist/observation.d.ts.map +1 -1
  74. package/dist/observation.js +9 -3
  75. package/dist/outcome.d.ts +14 -2
  76. package/dist/outcome.d.ts.map +1 -1
  77. package/dist/outcome.js +7 -0
  78. package/dist/overlay.d.ts +4 -4
  79. package/dist/overlay.d.ts.map +1 -1
  80. package/dist/overlay.js +181 -140
  81. package/dist/query.d.ts +26 -2
  82. package/dist/query.d.ts.map +1 -1
  83. package/dist/query.js +34 -6
  84. package/dist/reach.d.ts +4 -1
  85. package/dist/reach.d.ts.map +1 -1
  86. package/dist/reach.js +90 -21
  87. package/dist/react.d.ts +3 -3
  88. package/dist/react.d.ts.map +1 -1
  89. package/dist/react.js +7 -17
  90. package/dist/read-side.d.ts +3 -1
  91. package/dist/read-side.d.ts.map +1 -1
  92. package/dist/read-side.js +271 -82
  93. package/dist/reads.d.ts +11 -0
  94. package/dist/reads.d.ts.map +1 -0
  95. package/dist/reads.js +98 -0
  96. package/dist/refresh.d.ts +4 -4
  97. package/dist/refresh.d.ts.map +1 -1
  98. package/dist/refresh.js +35 -16
  99. package/dist/replica.d.ts +11 -5
  100. package/dist/replica.d.ts.map +1 -1
  101. package/dist/replica.js +69 -40
  102. package/dist/room-draft.d.ts +12 -0
  103. package/dist/room-draft.d.ts.map +1 -1
  104. package/dist/room-draft.js +11 -12
  105. package/dist/rows.d.ts +1 -0
  106. package/dist/rows.d.ts.map +1 -1
  107. package/dist/rows.js +5 -0
  108. package/dist/save.d.ts +4 -2
  109. package/dist/save.d.ts.map +1 -1
  110. package/dist/save.js +195 -111
  111. package/dist/search.d.ts +17 -2
  112. package/dist/search.d.ts.map +1 -1
  113. package/dist/search.js +81 -7
  114. package/dist/seed.d.ts +16 -0
  115. package/dist/seed.d.ts.map +1 -0
  116. package/dist/seed.js +75 -0
  117. package/dist/shared.d.ts +4 -0
  118. package/dist/shared.d.ts.map +1 -1
  119. package/dist/shared.js +63 -33
  120. package/dist/store.d.ts +18 -2
  121. package/dist/store.d.ts.map +1 -1
  122. package/dist/store.js +398 -316
  123. package/dist/turn.d.ts +5 -3
  124. package/dist/turn.d.ts.map +1 -1
  125. package/dist/turn.js +26 -7
  126. package/dist/windows.d.ts +26 -0
  127. package/dist/windows.d.ts.map +1 -0
  128. package/dist/windows.js +93 -0
  129. package/dist/wire.d.ts +16 -0
  130. package/dist/wire.d.ts.map +1 -1
  131. package/dist/wire.js +4 -0
  132. package/package.json +2 -2
  133. package/dist/acquisition.d.ts +0 -14
  134. package/dist/acquisition.d.ts.map +0 -1
  135. package/dist/acquisition.js +0 -24
@@ -1,36 +1,47 @@
1
1
  import { attachedModel } from './attached-model.js';
2
2
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
3
- import { observing } from './observation.js';
4
- import { filterOf, identityOf, planOf, } from './query.js';
3
+ import { observing, whole } from './observation.js';
4
+ import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
5
5
  import { createReadSide } from './read-side.js';
6
+ import { seededResult } from './seed.js';
6
7
  import { markdownPath, roomOfKey } from './rows.js';
7
8
  import { createSearchIndex, searchPlanOf, } from './search.js';
9
+ import { capped, MAXIMUM_WINDOW } from './windows.js';
8
10
  import { bootstrapRoom } from './bootstrap.js';
9
- import { changeSet, entering, mergeChangeSets } from './change-set.js';
11
+ import { changeSet, changing, entering, mergeChangeSets } from './change-set.js';
10
12
  import { createConversationStore, } from './conversation.js';
11
13
  import { queuedFile } from './change.js';
12
14
  import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
13
15
  import { runtimeError } from './failure.js';
14
16
  import { createLog } from './log.js';
15
17
  import { binaryRecord } from './binary.js';
16
- import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
18
+ import { absentBinary, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, lockedRoom, undeclared, UNCERTAIN, } from './outcome.js';
17
19
  import { createRecovery } from './recovery.js';
20
+ import { doorReads } from './reads.js';
18
21
  import { refreshRoom } from './refresh.js';
19
- import { buildBatch, joined, limitedIn, requeued } from './save.js';
20
- import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
21
- 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, replicating, sameMembership, standing, } from './replica.js';
24
+ import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
22
25
  import { same, saving, STARTING, transferring, } from './status.js';
23
26
  import { detectStorageLoss, nextGeneration } from './durability.js';
24
- import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
25
27
  import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
26
28
  import { FencedMap } from './fence.js';
27
29
  import { tally } from './shared.js';
28
- import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
30
+ import { inChunks, ROW_WEIGHT, stepped, yielded } from './turn.js';
29
31
  import { identityTime, roomDraftRevision } from './identity.js';
30
32
  import { beneath, normalize } from './paths.js';
31
33
  import { openRuntimeStore, } from './store.js';
32
34
  import { unconfirmed } from './transfer.js';
33
35
  import { governs, holds, narrowed, planReach, } from './reach.js';
36
+ function roomsAlone(plan) {
37
+ return (plan.reach === null &&
38
+ plan.root.types === null &&
39
+ plan.root.where === null &&
40
+ plan.root.pathPrefix === null &&
41
+ plan.paths === null &&
42
+ plan.hop === null &&
43
+ plan.backlinks === null);
44
+ }
34
45
  function within(root, path) {
35
46
  return root === '' || beneath(root, path);
36
47
  }
@@ -48,6 +59,12 @@ const OPENING_LANE = 'opening';
48
59
  const CONVERSATION_LANE = 'conversations';
49
60
  const ROOM_READS = 256;
50
61
  const ROOM_ATTEMPTS = 16;
62
+ // 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.
63
+ const LOCK_GRACE_MS = 120_000;
64
+ function operationOf(input) {
65
+ const operation = input?.operation_id;
66
+ return typeof operation === 'string' ? operation : null;
67
+ }
51
68
  const outside = new Map();
52
69
  function outsideOf(roots) {
53
70
  const key = JSON.stringify(roots);
@@ -87,6 +104,8 @@ export function composeRuntime(options) {
87
104
  const replicas = new FencedMap(isFenced);
88
105
  const queuesWithoutReplica = new FencedMap(isFenced);
89
106
  const pendings = new Map();
107
+ const seeds = new Map();
108
+ const seedWatchers = new Map();
90
109
  const roomDrafts = new FencedMap(isFenced);
91
110
  const presentations = new Map();
92
111
  let limits = [];
@@ -99,6 +118,7 @@ export function composeRuntime(options) {
99
118
  const liveModels = new FencedMap(isModelFenced);
100
119
  const invalidModels = new Set();
101
120
  const discovered = new Map();
121
+ const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
102
122
  let roomsListed = false;
103
123
  const scopedRefusals = new Map();
104
124
  const sourceRefusals = new Map();
@@ -135,6 +155,7 @@ export function composeRuntime(options) {
135
155
  let handed = false;
136
156
  const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
137
157
  const repairing = new Set();
158
+ const calling = new Set();
138
159
  const repairable = new Map();
139
160
  let outcomeRetention = null;
140
161
  const answering = new Map();
@@ -145,6 +166,7 @@ export function composeRuntime(options) {
145
166
  let discovering = null;
146
167
  let opened = null;
147
168
  const owed = new Set();
169
+ const reloadsDue = new Map();
148
170
  const lanes = new Map();
149
171
  let roomReads = { generation, free: ROOM_READS, waiting: [] };
150
172
  const working = new Set();
@@ -160,6 +182,9 @@ export function composeRuntime(options) {
160
182
  let withdrawConversations;
161
183
  let currencyQueued = null;
162
184
  let socketOpen = false;
185
+ const confirmed = new Set();
186
+ let lockGrace = null;
187
+ const lockListeners = new Set();
163
188
  let serviceMode = 'open';
164
189
  let haltedRefusal = false;
165
190
  let openSignals = 0;
@@ -171,8 +196,6 @@ export function composeRuntime(options) {
171
196
  const unpreparedDemand = new Set();
172
197
  const unindexable = new Set();
173
198
  let preparing = false;
174
- let demand = WHOLE_DEMAND;
175
- let narrowing = null;
176
199
  let targets = {};
177
200
  let persistence = options.shell.persistence;
178
201
  let capacity = null;
@@ -189,7 +212,7 @@ export function composeRuntime(options) {
189
212
  durably,
190
213
  stream: () => options.adapter.conversations,
191
214
  publish: publishChangeSet,
192
- online: () => !status.providerUnreachable && refusal === null,
215
+ online: () => connectionOf() !== 'unreachable' && refusal === null,
193
216
  closed: () => closed,
194
217
  unanswered: resendLater,
195
218
  expired: (operation) => pastHorizon(operation),
@@ -255,6 +278,15 @@ export function composeRuntime(options) {
255
278
  const value = presentations.get(app);
256
279
  return value === undefined ? null : structuredClone(value);
257
280
  }
281
+ function keepPresentation(app, next) {
282
+ return exclusive(async () => {
283
+ const local = await openLocal();
284
+ const stored = next(presentations.get(app));
285
+ await local.present({ app, value: stored });
286
+ presentations.set(app, stored);
287
+ publishChangeSet(changeSet('', null, null, { presentation: app }));
288
+ });
289
+ }
258
290
  function emitPresentation(app) {
259
291
  if (closed)
260
292
  return;
@@ -315,9 +347,63 @@ export function composeRuntime(options) {
315
347
  return structuredClone([...rooms.values()].map((room) => {
316
348
  const documents = documentsIn(room.locator);
317
349
  counted.set(room.locator, documents);
318
- return { ...room, documents };
350
+ return locked(room.locator)
351
+ ? {
352
+ ...room,
353
+ model: modelTypes(NO_MODEL),
354
+ typedSelection: null,
355
+ documents: 0,
356
+ locked: true,
357
+ }
358
+ : { ...room, documents, locked: false };
319
359
  }));
320
360
  }
361
+ function locked(locator) {
362
+ return rooms.get(locator)?.replica.offline === 'locked' && !confirmed.has(locator);
363
+ }
364
+ function lockedRooms() {
365
+ return [...rooms.keys()].filter(locked).sort();
366
+ }
367
+ function relocked(before) {
368
+ const after = lockedRooms();
369
+ if (JSON.stringify(after) === JSON.stringify(before))
370
+ return;
371
+ journal.record(`locked ${after.length}`);
372
+ emitRooms();
373
+ for (const notify of [...lockListeners])
374
+ notify();
375
+ }
376
+ function confirmLocks(listed) {
377
+ if (!socketOpen)
378
+ return;
379
+ for (const room of listed) {
380
+ if (room.replica.admission === 'admitted')
381
+ confirmed.add(room.locator);
382
+ }
383
+ }
384
+ function connectionLost() {
385
+ if (lockGrace !== null || closed)
386
+ return;
387
+ lockGrace = setTimeout(() => {
388
+ lockGrace = null;
389
+ if (socketOpen || closed)
390
+ return;
391
+ const before = lockedRooms();
392
+ confirmed.clear();
393
+ relocked(before);
394
+ }, LOCK_GRACE_MS);
395
+ }
396
+ function connectionLive() {
397
+ if (lockGrace !== null)
398
+ clearTimeout(lockGrace);
399
+ lockGrace = null;
400
+ }
401
+ function watchLocks(notify) {
402
+ lockListeners.add(notify);
403
+ return () => {
404
+ lockListeners.delete(notify);
405
+ };
406
+ }
321
407
  function roomFacts(reach = null) {
322
408
  const held = [...rooms.values()]
323
409
  .filter((room) => holds(reach, room.locator))
@@ -366,7 +452,7 @@ export function composeRuntime(options) {
366
452
  if (closed)
367
453
  return;
368
454
  queueReadiness();
369
- const next = status.state === 'synced' && demand.conversations && !conversations.current()
455
+ const next = status.state === 'synced' && !conversations.current()
370
456
  ? { ...status, state: 'syncing' }
371
457
  : status;
372
458
  if (same(shown, next))
@@ -399,8 +485,12 @@ export function composeRuntime(options) {
399
485
  ...set,
400
486
  documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
401
487
  };
402
- readSide.apply(held);
403
- searchIndex.changed(held);
488
+ const applied = readSide.apply(held, () => searchIndex.changed(held));
489
+ if (applied instanceof Promise)
490
+ void applied.catch(unpublished);
491
+ }
492
+ function unpublished(error) {
493
+ journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
404
494
  }
405
495
  async function referenced(locator) {
406
496
  for (;;) {
@@ -431,8 +521,8 @@ export function composeRuntime(options) {
431
521
  if (current === undefined || closed)
432
522
  return;
433
523
  const set = entering(locator, current, chunk);
434
- readSide.apply(set);
435
524
  entered.push(set);
525
+ return readSide.apply(set);
436
526
  });
437
527
  if (!replicas.has(locator) || closed)
438
528
  return;
@@ -474,17 +564,22 @@ export function composeRuntime(options) {
474
564
  }
475
565
  })();
476
566
  }
567
+ function roomsOfReach(reach) {
568
+ return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
569
+ }
477
570
  function reachedRooms(plan) {
478
- const reach = plan.reach;
479
- const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
571
+ const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
480
572
  const named = plan.rooms ??
481
573
  (reached === null
482
574
  ? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
483
575
  : [...reached]);
484
- return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
576
+ return [...new Set(named)]
577
+ .filter((locator) => (reached === null || reached.has(locator)) && !plan.locked.includes(locator))
578
+ .sort();
485
579
  }
486
580
  function awaited(locator) {
487
- return rooms.has(locator) || !roomsListed;
581
+ return (!roomsListed ||
582
+ (rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
488
583
  }
489
584
  function covered(plan) {
490
585
  const locators = reachedRooms(plan);
@@ -524,61 +619,11 @@ export function composeRuntime(options) {
524
619
  if (waiting.length > 0)
525
620
  prepare(waiting.map(({ room }) => room));
526
621
  }
527
- function acquires(room) {
528
- return (demanded(demand, room) ||
529
- (replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
530
- queuesWithoutReplica.has(room.locator));
531
- }
532
- function acquiredLocators() {
533
- return [...discovered.values()]
534
- .filter(acquires)
535
- .map((room) => room.locator)
536
- .sort();
537
- }
538
- function heldDemand(acquired) {
539
- if (demand.whole)
540
- return null;
541
- return { rooms: acquired.map((room) => room.locator), conversations: demand.conversations };
542
- }
543
- function applyDemand(next) {
544
- const before = acquiredLocators();
545
- const conversed = demand.conversations;
546
- demand = next;
547
- const after = acquiredLocators();
548
- queueReadiness();
549
- if (!conversed && next.conversations)
550
- scheduleCurrency();
551
- if (JSON.stringify(before) === JSON.stringify(after) && conversed === next.conversations)
552
- return;
553
- journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
554
- if (options.adapter.roomStream === undefined)
555
- void sync('unstreamed');
556
- else if (socketOpen)
557
- options.adapter.roomStream.reopen();
558
- }
559
- function acquire(next) {
560
- if (closed)
561
- return;
562
- if (narrowing !== null)
563
- clearTimeout(narrowing);
564
- narrowing = null;
565
- const widening = wider(next, demand);
566
- if (!widening && !narrower(next, demand))
567
- return;
568
- if (widening || !roomsListed) {
569
- applyDemand(next);
570
- return;
571
- }
572
- // A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
573
- narrowing = setTimeout(() => {
574
- narrowing = null;
575
- applyDemand(next);
576
- }, 5_000);
577
- }
578
- function requiredRooms(reach) {
622
+ function requiredRooms(given) {
623
+ const reach = planReach(given);
579
624
  if (reach === null)
580
625
  return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
581
- const named = [...new Set(reach.rooms)].sort();
626
+ const named = [...roomsOfReach(reach)].sort();
582
627
  if (roomsListed)
583
628
  return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
584
629
  return { listed: named.every((room) => replicas.has(room)), rooms: named };
@@ -772,10 +817,7 @@ export function composeRuntime(options) {
772
817
  void sync('unstreamed');
773
818
  }
774
819
  async function reclaimed(local) {
775
- const unreferenced = [...transfers.keys()].filter((locator) => {
776
- const room = discovered.get(locator);
777
- return room === undefined || !acquires(room);
778
- });
820
+ const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
779
821
  for (const locator of unreferenced) {
780
822
  await local.abandon(locator);
781
823
  transfers.delete(locator);
@@ -801,18 +843,54 @@ export function composeRuntime(options) {
801
843
  const available = await estimated().catch(() => null);
802
844
  pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
803
845
  }
846
+ function seedRoomHeld(room, version) {
847
+ return prepared.has(room) && reachedBy(room, version);
848
+ }
849
+ function seedHeld(at) {
850
+ return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
851
+ }
852
+ function seedApplies(plan) {
853
+ const seed = seeds.get(plan.key);
854
+ if (seed === undefined)
855
+ return null;
856
+ if (!seedHeld(seed.at))
857
+ return seed;
858
+ seeds.delete(plan.key);
859
+ return null;
860
+ }
861
+ function seedBehind(plan, cover) {
862
+ const seed = seedApplies(plan);
863
+ if (seed === null || 'pending' in cover)
864
+ return cover;
865
+ return {
866
+ pending: Object.entries(seed.at)
867
+ .filter(([room, version]) => !seedRoomHeld(room, version))
868
+ .map(([room]) => ({ room, awaits: 'acquisition' })),
869
+ };
870
+ }
871
+ function seed(entry) {
872
+ if (seedHeld(entry.at))
873
+ return;
874
+ for (const { query, rows } of entry.queries) {
875
+ const key = queryKey(query);
876
+ seeds.set(key, { rows, at: entry.at });
877
+ for (const shown of seedWatchers.get(key) ?? [])
878
+ shown();
879
+ }
880
+ }
804
881
  async function rowsQuery(plan) {
805
882
  const before = covered(plan);
806
883
  awaitPreparation(before);
884
+ const seeded = seedApplies(plan);
885
+ if (seeded !== null)
886
+ return seededResult(seeded).result;
807
887
  if ('pending' in before)
808
888
  return before;
809
889
  const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
810
890
  awaitPreparation(cover);
811
891
  if ('pending' in cover)
812
892
  return cover;
813
- return 'groups' in rows
814
- ? { groups: rows.groups, coverage: cover.coverage }
815
- : { documents: rows.documents, coverage: cover.coverage };
893
+ return { ...rows, coverage: cover.coverage };
816
894
  }
817
895
  async function indexConversation(conversation) {
818
896
  searchIndex.indexMessages(conversation, [], false);
@@ -956,20 +1034,28 @@ export function composeRuntime(options) {
956
1034
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
957
1035
  });
958
1036
  }
959
- async function applyDiscovery(found, started) {
1037
+ async function applyDiscovery(found, started, covered) {
1038
+ const lockedBefore = lockedRooms();
960
1039
  for (const room of found) {
961
1040
  const fence = fences.get(room.locator);
962
1041
  if (fence !== undefined && fence.erased && fence.at < started)
963
1042
  fences.delete(room.locator);
964
1043
  }
1044
+ await loseRooms(found
1045
+ .filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
1046
+ .map((room) => room.locator));
965
1047
  const listed = found.filter((room) => !fences.has(room.locator));
966
1048
  const listedLocators = new Set(listed.map((room) => room.locator));
967
1049
  for (const locator of listedLocators)
968
1050
  scopedRefusals.delete(locator);
969
- discovered.clear();
1051
+ if (covered === undefined)
1052
+ discovered.clear();
1053
+ else
1054
+ for (const locator of covered)
1055
+ discovered.delete(locator);
970
1056
  for (const room of listed)
971
1057
  discovered.set(room.locator, room);
972
- const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
1058
+ const unlisted = unlistedHeld(new Set(found.map((room) => room.locator))).filter((locator) => covered === undefined || covered.includes(locator));
973
1059
  if (options.adapter.exhaustiveDiscovery === true)
974
1060
  await loseRooms(unlisted);
975
1061
  else
@@ -982,10 +1068,18 @@ export function composeRuntime(options) {
982
1068
  }
983
1069
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
984
1070
  }
1071
+ confirmLocks(listed);
985
1072
  roomsListed = true;
986
1073
  emitRooms();
1074
+ relocked(lockedBefore);
987
1075
  return listed;
988
1076
  }
1077
+ function heldLocally(locator) {
1078
+ return (replicas.has(locator) ||
1079
+ roomDrafts.has(locator) ||
1080
+ transfers.has(locator) ||
1081
+ queuesWithoutReplica.has(locator));
1082
+ }
989
1083
  function unlistedHeld(listed) {
990
1084
  return [
991
1085
  ...new Set([
@@ -1015,6 +1109,7 @@ export function composeRuntime(options) {
1015
1109
  for (const locator of fresh) {
1016
1110
  fences.set(locator, { at: discoveries, erased: false });
1017
1111
  lastFenced.set(locator, ++fencings);
1112
+ confirmed.delete(locator);
1018
1113
  for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1019
1114
  pendings.delete(batch.id);
1020
1115
  replicas.delete(locator);
@@ -1221,8 +1316,9 @@ export function composeRuntime(options) {
1221
1316
  }
1222
1317
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
1223
1318
  const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
1224
- const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
1225
- const kept = queue.filter((batch) => !withdrawn.includes(batch));
1319
+ const { withdrawn, kept } = await stepped(standing(confirmed, queue, withdrawable, localSaves));
1320
+ const founded = kept.filter((batch) => !queue.includes(batch));
1321
+ const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
1226
1322
  const unqueued = withdrawn.map((batch) => ({
1227
1323
  batch,
1228
1324
  returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
@@ -1246,9 +1342,10 @@ export function composeRuntime(options) {
1246
1342
  });
1247
1343
  }
1248
1344
  }
1249
- await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
1345
+ await local.keepAuthored({ locator, queue: kept, founded, roomDrafts: [...written.values()] });
1346
+ const staying = new Set(kept.map((batch) => batch.id));
1250
1347
  for (const batch of previousQueue) {
1251
- if (!kept.includes(batch))
1348
+ if (!staying.has(batch.id))
1252
1349
  pendings.delete(batch.id);
1253
1350
  }
1254
1351
  for (const record of written.values())
@@ -1261,34 +1358,38 @@ export function composeRuntime(options) {
1261
1358
  });
1262
1359
  }
1263
1360
  catch (error) {
1264
- const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
1361
+ const held = before === undefined ? null : await stepped(replicating(before.confirmed, kept, before));
1362
+ const set = await stepped(changing(locator, before ?? null, held, {
1363
+ queue: queueChanged,
1364
+ drafts: [...written.keys()],
1365
+ }));
1265
1366
  if (held !== null)
1266
1367
  replicas.set(locator, held);
1267
1368
  else if (kept.length > 0)
1268
1369
  queuesWithoutReplica.set(locator, kept);
1269
1370
  else
1270
1371
  queuesWithoutReplica.delete(locator);
1271
- publishChangeSet(changeSet(locator, before ?? null, held, {
1272
- queue: kept.length !== previousQueue.length,
1273
- drafts: [...written.keys()],
1274
- }));
1372
+ publishChangeSet(set);
1275
1373
  moveTo({ ...status, ...attention() });
1276
1374
  throw error;
1277
1375
  }
1376
+ const changed = { room: settling.room, queue: queueChanged, drafts: [...written.keys()] };
1377
+ const next = await stepped(replicating(confirmed, kept, before));
1378
+ const set = await stepped(changing(locator, before ?? null, next, changed));
1278
1379
  return () => {
1279
1380
  queuesWithoutReplica.delete(locator);
1280
1381
  for (const { batch, returned } of unqueued) {
1281
1382
  const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
1282
1383
  journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1283
1384
  }
1284
- replicas.set(locator, replicaOf(confirmed, kept, before));
1285
- return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
1286
- room: settling.room,
1287
- queue: kept.length !== previousQueue.length,
1288
- drafts: [...written.keys()],
1289
- });
1385
+ replicas.set(locator, next);
1386
+ return heldChange(locator, before ?? null, next, set, changed);
1290
1387
  };
1291
1388
  }
1389
+ function heldChange(locator, before, next, set, changed) {
1390
+ const held = replicas.get(locator) ?? null;
1391
+ return held === next ? set : changeSet(locator, before, held, changed);
1392
+ }
1292
1393
  function adoptModels(locator, record) {
1293
1394
  for (const [attached, model] of [
1294
1395
  ['collection', record.model],
@@ -1376,7 +1477,7 @@ export function composeRuntime(options) {
1376
1477
  return true;
1377
1478
  });
1378
1479
  }
1379
- async function saveRoomDrafts(locator, named, writtenBy) {
1480
+ async function savedRoom(locator) {
1380
1481
  const local = await openLocal();
1381
1482
  await referenced(locator);
1382
1483
  const replica = replicas.get(locator);
@@ -1384,6 +1485,10 @@ export function composeRuntime(options) {
1384
1485
  if (replica === undefined || room === undefined) {
1385
1486
  throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1386
1487
  }
1488
+ return { local, replica, room };
1489
+ }
1490
+ async function saveRoomDrafts(locator, named, writtenBy) {
1491
+ const target = await savedRoom(locator);
1387
1492
  const saved = [];
1388
1493
  for (const { path, revision } of named) {
1389
1494
  const record = roomDrafts.get(locator)?.get(path);
@@ -1393,6 +1498,38 @@ export function composeRuntime(options) {
1393
1498
  if (!saved.some((draft) => draft.path === path))
1394
1499
  saved.push({ path, parts: record.parts });
1395
1500
  }
1501
+ return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
1502
+ }
1503
+ async function saveParts(locator, named, writtenBy) {
1504
+ const read = named.map(({ path, parts }) => ({ path, parts: readParts(parts) }));
1505
+ if (read.some(({ path }, at) => path === '' ||
1506
+ normalize(path) !== path ||
1507
+ read.findIndex((one) => one.path === path) !== at)) {
1508
+ throw runtimeError('room-draft-malformed');
1509
+ }
1510
+ const target = await savedRoom(locator);
1511
+ const saved = [];
1512
+ for (const { path, parts } of read) {
1513
+ const file = parts.file === undefined
1514
+ ? undefined
1515
+ : await heldFile(parts.file.blob, binaryBase(target.replica.visible, path));
1516
+ saved.push({ path, parts: held(undefined, parts, file) });
1517
+ }
1518
+ return committed(locator, target, saved, new Set(), writtenBy);
1519
+ }
1520
+ function carriedDrafts(locator, saved, consumed) {
1521
+ const holding = roomDrafts.get(locator);
1522
+ return new Map(saved.flatMap(({ path, parts }) => {
1523
+ const record = holding?.get(path);
1524
+ if (parts.move === undefined || record === undefined || consumed.has(path))
1525
+ return [];
1526
+ const to = moveTarget(parts.move);
1527
+ if (to === path || holding?.has(to) === true)
1528
+ return [];
1529
+ return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
1530
+ }));
1531
+ }
1532
+ async function committed(locator, { local, replica, room }, saved, consumed, writtenBy) {
1396
1533
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
1397
1534
  if (changed.length > 0)
1398
1535
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
@@ -1425,6 +1562,7 @@ export function composeRuntime(options) {
1425
1562
  visible: replica.visible,
1426
1563
  changes,
1427
1564
  bases: parts.flatMap(({ bases }) => bases),
1565
+ queued: queuedDocuments(replica.queue),
1428
1566
  writtenBy,
1429
1567
  clean: {
1430
1568
  text: replica.confirmed.record.documents,
@@ -1432,7 +1570,9 @@ export function composeRuntime(options) {
1432
1570
  composition: compositionOf(replica.confirmed.record),
1433
1571
  },
1434
1572
  };
1435
- const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
1573
+ const checked = withdrawing
1574
+ ? await buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
1575
+ : null;
1436
1576
  if (checked?.kind === 'refused')
1437
1577
  return { outcome: 'refused', ...checked.refusal };
1438
1578
  const owners = [...requeue.owners].flatMap((origin) => {
@@ -1448,13 +1588,15 @@ export function composeRuntime(options) {
1448
1588
  };
1449
1589
  }
1450
1590
  const built = !withdrawing
1451
- ? buildBatch(input)
1591
+ ? await buildBatch(input)
1452
1592
  : requeue.changes.length === 0
1453
1593
  ? null
1454
- : buildBatch({
1594
+ : await buildBatch({
1455
1595
  ...input,
1456
- visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
1596
+ visible: (await stepped(replicating(replica.confirmed, requeue.queue, replica)))
1597
+ .visible,
1457
1598
  changes: requeue.changes,
1599
+ queued: queuedDocuments(requeue.queue),
1458
1600
  });
1459
1601
  if (built?.kind === 'refused')
1460
1602
  return { outcome: 'refused', ...built.refusal };
@@ -1489,18 +1631,26 @@ export function composeRuntime(options) {
1489
1631
  if (limited !== null)
1490
1632
  return { outcome: 'refused', ...limited };
1491
1633
  }
1492
- const consumed = saved.map(({ path }) => path);
1634
+ const carried = carriedDrafts(locator, saved, consumed);
1635
+ const left = [...consumed, ...carried.keys()];
1636
+ const arrived = [...carried.values()];
1493
1637
  try {
1494
1638
  await local.keepAuthored({
1495
1639
  locator,
1496
1640
  queue: batches,
1497
1641
  saved: drafted,
1498
- consumedRoomDrafts: consumed,
1642
+ consumedRoomDrafts: left,
1643
+ roomDrafts: arrived,
1499
1644
  });
1500
1645
  }
1501
1646
  catch {
1502
1647
  return { outcome: 'refused', code: 'durable-write', retryable: true };
1503
1648
  }
1649
+ const next = await stepped(replicating(replica.confirmed, batches, replica));
1650
+ const set = await stepped(changing(locator, replica, next, {
1651
+ queue: true,
1652
+ drafts: [...left, ...arrived.map(({ path }) => path)],
1653
+ }));
1504
1654
  if (fences.has(locator))
1505
1655
  throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1506
1656
  for (const origin of withdrawn.keys())
@@ -1510,13 +1660,12 @@ export function composeRuntime(options) {
1510
1660
  }
1511
1661
  if (!localSaves)
1512
1662
  recorded = false;
1513
- for (const path of consumed)
1663
+ for (const path of left)
1514
1664
  forgetRoomDraft(locator, path);
1515
- replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
1516
- publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
1517
- queue: true,
1518
- drafts: consumed,
1519
- }));
1665
+ for (const record of arrived)
1666
+ rememberRoomDraft(record);
1667
+ replicas.set(locator, next);
1668
+ publishChangeSet(set);
1520
1669
  moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
1521
1670
  submitLater(locator);
1522
1671
  const identifier = join?.batch.id ??
@@ -1606,7 +1755,7 @@ export function composeRuntime(options) {
1606
1755
  const scope = options.adapter.referrerScope(request.referrer);
1607
1756
  return scope?.kind === 'room' ? scope.locator : null;
1608
1757
  }
1609
- async function readBinary(request) {
1758
+ async function transferBinary(request, transfer, answer) {
1610
1759
  if (closed || refusal !== null)
1611
1760
  return UNCERTAIN;
1612
1761
  try {
@@ -1622,40 +1771,41 @@ export function composeRuntime(options) {
1622
1771
  ? `transfer binary ${request.locator} ${request.path}`
1623
1772
  : `transfer binary ${request.fileRef}`);
1624
1773
  try {
1625
- const result = await options.adapter.binary(authorization, request);
1626
- journal.record(`outcome binary ${result.kind}`);
1774
+ const result = await transfer(authorization);
1775
+ const settled = result;
1776
+ journal.record(`outcome binary ${settled.kind}`);
1627
1777
  const locator = binaryRoom(request);
1628
1778
  if (locator !== null && fencedSince(locator, since))
1629
1779
  return failure({ kind: 'room-not-readable', locator });
1630
- if (result.kind === 'scoped-refusal') {
1631
- const room = replicas.get(result.locator)?.confirmed.record;
1780
+ if (settled.kind === 'scoped-refusal') {
1781
+ const room = replicas.get(settled.locator)?.confirmed.record;
1632
1782
  if (room !== undefined)
1633
- await scoped(result, room, await openLocal());
1783
+ await scoped(settled, room, await openLocal());
1634
1784
  }
1635
- if (result.kind === 'revoked-authorization')
1785
+ if (settled.kind === 'revoked-authorization')
1636
1786
  await refuse('revoked-authorization');
1637
- return binaryAnswer(result);
1787
+ return answer(result);
1638
1788
  }
1639
1789
  catch {
1640
1790
  journal.record('outcome binary transport-uncertainty');
1641
1791
  return UNCERTAIN;
1642
1792
  }
1643
1793
  }
1644
- async function readBinaryPath(locator, path, revision) {
1645
- await ready;
1794
+ function readBinary(request) {
1795
+ return transferBinary(request, (held) => options.adapter.binary(held, request), binaryAnswer);
1796
+ }
1797
+ function binaryPath(locator, path, revision) {
1646
1798
  if (revision !== undefined)
1647
- return readBinary({ kind: 'path', locator, path, revision });
1799
+ return { kind: 'path', locator, path, revision };
1648
1800
  const visible = replicas.get(locator)?.visible;
1649
1801
  const held = visible?.binaries[path];
1650
1802
  if (visible === undefined || held === undefined)
1651
- return absentBinary(path);
1652
- if (held.state === 'pending') {
1653
- const blob = queuedBlob(locator, path, held.localId);
1654
- return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1655
- }
1803
+ return null;
1804
+ if (held.state === 'pending')
1805
+ return { kind: 'pending', localId: held.localId };
1656
1806
  const record = binaryRecord(path, held, visible.revisions[path]);
1657
1807
  if (record.state !== 'accepted')
1658
- return absentBinary(path);
1808
+ return null;
1659
1809
  const replica = replicas.get(locator);
1660
1810
  const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
1661
1811
  const holds = (at) => {
@@ -1670,13 +1820,31 @@ export function composeRuntime(options) {
1670
1820
  if (change.kind === 'move' && change.to === confirmed)
1671
1821
  confirmed = change.path;
1672
1822
  }
1673
- return readBinary({
1823
+ return {
1674
1824
  kind: 'path',
1675
1825
  locator,
1676
1826
  path: confirmed,
1677
1827
  revision: record.revision,
1678
1828
  fileRef: record.fileRef,
1679
- });
1829
+ };
1830
+ }
1831
+ async function readBinaryPath(locator, path, revision) {
1832
+ await ready;
1833
+ const asked = binaryPath(locator, path, revision);
1834
+ if (asked === null)
1835
+ return absentBinary(path);
1836
+ if (asked.kind === 'pending') {
1837
+ const blob = queuedBlob(locator, path, asked.localId);
1838
+ return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1839
+ }
1840
+ return readBinary(asked);
1841
+ }
1842
+ async function linkBinaryPath(locator, path, revision) {
1843
+ await ready;
1844
+ const asked = binaryPath(locator, path, revision);
1845
+ if (asked === null || asked.kind === 'pending')
1846
+ return absentBinary(path);
1847
+ return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
1680
1848
  }
1681
1849
  async function readBinaryReference(reference) {
1682
1850
  await ready;
@@ -1702,7 +1870,14 @@ export function composeRuntime(options) {
1702
1870
  }
1703
1871
  return undefined;
1704
1872
  }
1705
- async function invokeAction(name, input) {
1873
+ function calledRoom(name, input) {
1874
+ const member = declaredAction(name)?.room;
1875
+ if (member === undefined || input === null || typeof input !== 'object')
1876
+ return null;
1877
+ const room = input[member];
1878
+ return typeof room === 'string' ? room : null;
1879
+ }
1880
+ async function invokeAction(name, input, dispatching) {
1706
1881
  await ready;
1707
1882
  if (closed || refusal !== null)
1708
1883
  return UNCERTAIN;
@@ -1716,6 +1891,7 @@ export function composeRuntime(options) {
1716
1891
  }
1717
1892
  if (closed || authorization === undefined || refusal !== null)
1718
1893
  return UNCERTAIN;
1894
+ await dispatching?.();
1719
1895
  journal.record(`transfer action ${name}`);
1720
1896
  try {
1721
1897
  const result = await options.adapter.action(authorization, { name, input });
@@ -1736,6 +1912,60 @@ export function composeRuntime(options) {
1736
1912
  return UNCERTAIN;
1737
1913
  }
1738
1914
  }
1915
+ function unsettled(result) {
1916
+ return result.kind !== 'action' && result.retryable;
1917
+ }
1918
+ async function recoveredAction(name, input) {
1919
+ const operation = operationOf(input);
1920
+ if (operation === null)
1921
+ return invokeAction(name, input);
1922
+ const kept = async () => (await openLocal()).keepCall({ operation, name, input });
1923
+ calling.add(operation);
1924
+ try {
1925
+ let result = await invokeAction(name, input, kept);
1926
+ if (unsettled(result)) {
1927
+ await recovery.attempt(`action ${operation}`, async () => {
1928
+ if (closed || refusal !== null)
1929
+ return 'done';
1930
+ if (!socketOpen)
1931
+ return 'again';
1932
+ result = await invokeAction(name, input, kept);
1933
+ return unsettled(result) ? 'again' : 'done';
1934
+ }, { again: true });
1935
+ }
1936
+ if (!unsettled(result))
1937
+ await (await openLocal()).dropCall(operation);
1938
+ return result;
1939
+ }
1940
+ finally {
1941
+ calling.delete(operation);
1942
+ }
1943
+ }
1944
+ async function resendCalls(signal) {
1945
+ if (!socketOpen || closed || stopping)
1946
+ return 'done';
1947
+ const local = await openLocal();
1948
+ for (const held of await local.calls()) {
1949
+ if (calling.has(held.operation))
1950
+ continue;
1951
+ if (closed || signal.aborted)
1952
+ return 'done';
1953
+ if (pastHorizon(held.operation)) {
1954
+ await local.dropCall(held.operation);
1955
+ continue;
1956
+ }
1957
+ calling.add(held.operation);
1958
+ try {
1959
+ if (unsettled(await invokeAction(held.name, held.input)))
1960
+ return 'again';
1961
+ await local.dropCall(held.operation);
1962
+ }
1963
+ finally {
1964
+ calling.delete(held.operation);
1965
+ }
1966
+ }
1967
+ return 'done';
1968
+ }
1739
1969
  function continuing() {
1740
1970
  return !closed && !discoveryPaused;
1741
1971
  }
@@ -1753,8 +1983,9 @@ export function composeRuntime(options) {
1753
1983
  async function pullRefresh(authorized, confirmed, live) {
1754
1984
  if (!live())
1755
1985
  return unconfirmed;
1986
+ const turn = generation;
1756
1987
  try {
1757
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1988
+ const result = await refreshRoom(reads, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1758
1989
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
1759
1990
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
1760
1991
  return result;
@@ -1763,6 +1994,9 @@ export function composeRuntime(options) {
1763
1994
  journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
1764
1995
  throw error;
1765
1996
  }
1997
+ finally {
1998
+ reads.finished(confirmed.record.locator, turn);
1999
+ }
1766
2000
  }
1767
2001
  function staging(local, live) {
1768
2002
  return {
@@ -1805,12 +2039,13 @@ export function composeRuntime(options) {
1805
2039
  async function pullBootstrap(authorized, room, local, live) {
1806
2040
  if (!live())
1807
2041
  return unconfirmed;
2042
+ const turn = generation;
1808
2043
  if (replicas.has(room.locator)) {
1809
2044
  catching.add(room.locator);
1810
2045
  readSide.recover();
1811
2046
  }
1812
2047
  try {
1813
- const result = await bootstrapRoom(options.adapter, authorized, room, {
2048
+ const result = await bootstrapRoom(reads, authorized, room, {
1814
2049
  collection: modelOf(room.locator),
1815
2050
  selection: modelOf(room.locator, 'typed-selection'),
1816
2051
  }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
@@ -1825,12 +2060,28 @@ export function composeRuntime(options) {
1825
2060
  caughtUpTo(room.locator);
1826
2061
  throw error;
1827
2062
  }
2063
+ finally {
2064
+ reads.finished(room.locator, turn);
2065
+ }
1828
2066
  }
1829
2067
  function caughtUpTo(locator) {
1830
2068
  if (catching.delete(locator))
1831
2069
  readSide.recover();
1832
2070
  }
2071
+ function behind(room) {
2072
+ const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
2073
+ return (held !== undefined &&
2074
+ [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
2075
+ }
1833
2076
  async function advance(room, local, authorized, live) {
2077
+ const carried = reads.carried(room.locator);
2078
+ const withheld = await advanceOnce(room, local, authorized, live);
2079
+ if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
2080
+ return withheld;
2081
+ }
2082
+ return advanceOnce(room, local, authorized, live);
2083
+ }
2084
+ async function advanceOnce(room, local, authorized, live) {
1834
2085
  await referenced(room.locator);
1835
2086
  if (!live() || paused.has(room.locator))
1836
2087
  return unconfirmed;
@@ -1867,6 +2118,8 @@ export function composeRuntime(options) {
1867
2118
  readSide.recover();
1868
2119
  return unconfirmed;
1869
2120
  }
2121
+ if (reloadsDue.get(room.locator) === generation)
2122
+ return unconfirmed;
1870
2123
  }
1871
2124
  const bootstrapped = await pullBootstrap(authorized, room, local, live);
1872
2125
  if (!live())
@@ -1958,7 +2211,7 @@ export function composeRuntime(options) {
1958
2211
  logAccepted(room.locator, submitted, accepted);
1959
2212
  const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1960
2213
  ? { kind: 'staged', confirmed: current.confirmed }
1961
- : applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
2214
+ : await applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
1962
2215
  ? accepted.roomVersion
1963
2216
  : held, accepted.roomVersion);
1964
2217
  if (applied.kind !== 'staged') {
@@ -2220,6 +2473,7 @@ export function composeRuntime(options) {
2220
2473
  if (!continuing())
2221
2474
  return;
2222
2475
  const held = replicas.get(room.locator);
2476
+ let changed = null;
2223
2477
  if (held !== undefined) {
2224
2478
  const declared = {
2225
2479
  ...held.confirmed.record,
@@ -2230,7 +2484,7 @@ export function composeRuntime(options) {
2230
2484
  };
2231
2485
  const reclassified = sameMembership(declared, held.confirmed.record)
2232
2486
  ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
2233
- : readReclassified(declared, held.confirmed);
2487
+ : await readReclassified(declared, held.confirmed);
2234
2488
  const record = reclassified.kind === 'staged'
2235
2489
  ? declared
2236
2490
  : {
@@ -2241,15 +2495,19 @@ export function composeRuntime(options) {
2241
2495
  ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
2242
2496
  : reclassified;
2243
2497
  await local.write({ next: record, previous: held.confirmed.record });
2498
+ const replica = read.kind === 'staged'
2499
+ ? await stepped(replicating(read.confirmed, held.queue, held))
2500
+ : { ...held, confirmed: { ...held.confirmed, record } };
2501
+ const set = await stepped(changing(room.locator, held, replica, { room: true }));
2502
+ replicas.set(room.locator, replica);
2503
+ changed = heldChange(room.locator, held, replica, set, { room: true });
2244
2504
  if (read.kind === 'staged') {
2245
- replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
2246
2505
  if (declarationRefusals.delete(room.locator)) {
2247
2506
  sourceRefusals.delete(room.locator);
2248
2507
  unreadable.delete(room.locator);
2249
2508
  }
2250
2509
  }
2251
- else if (read.kind === 'refused') {
2252
- replicas.set(room.locator, { ...held, confirmed: { ...held.confirmed, record } });
2510
+ else {
2253
2511
  refusedSource(room, read);
2254
2512
  declarationRefusals.add(room.locator);
2255
2513
  }
@@ -2260,13 +2518,18 @@ export function composeRuntime(options) {
2260
2518
  invalidModels.delete(key);
2261
2519
  }
2262
2520
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
2263
- if (held === undefined)
2521
+ if (changed === null)
2264
2522
  emitRooms();
2265
2523
  else
2266
- publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
2524
+ publishChangeSet(changed);
2267
2525
  });
2268
2526
  }
2269
2527
  async function scoped(result, room, local) {
2528
+ if (result.scope === 'content' && !result.retryable) {
2529
+ journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
2530
+ await loseRooms([room.locator]);
2531
+ return;
2532
+ }
2270
2533
  scopedRefusals.set(room.locator, {
2271
2534
  code: result.code,
2272
2535
  fields: result.fields,
@@ -2280,15 +2543,7 @@ export function composeRuntime(options) {
2280
2543
  });
2281
2544
  else if (result.scope !== 'operation') {
2282
2545
  unreadable.add(room.locator);
2283
- scopedAway = {
2284
- ...room,
2285
- replica: { ...room.replica, admission: 'unavailable' },
2286
- operations: {
2287
- ...room.operations,
2288
- ...(result.scope === 'content' ? { read: [] } : {}),
2289
- ...(result.scope === 'sync' ? { sync: [] } : {}),
2290
- },
2291
- };
2546
+ scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
2292
2547
  await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
2293
2548
  }
2294
2549
  if (!continuing())
@@ -2313,16 +2568,15 @@ export function composeRuntime(options) {
2313
2568
  });
2314
2569
  }
2315
2570
  async function synchronize(listed, local, progress, attempt, live) {
2316
- const acquired = listed.filter(acquires);
2317
- const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
2571
+ const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
2318
2572
  if (!live())
2319
2573
  return;
2320
- const ready = acquired.filter((_, index) => {
2574
+ const ready = listed.filter((_, index) => {
2321
2575
  const outcome = prepared[index];
2322
2576
  return outcome?.status === 'fulfilled' && outcome.value;
2323
2577
  });
2324
2578
  if (attempt === 'opened') {
2325
- options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
2579
+ options.adapter.roomStream?.hold(heldRooms(ready));
2326
2580
  handed = true;
2327
2581
  }
2328
2582
  discovering?.release();
@@ -2350,19 +2604,10 @@ export function composeRuntime(options) {
2350
2604
  await retainModel(local, room, 'collection', modelOf(room.locator));
2351
2605
  if (discoveryPaused || authorization === undefined)
2352
2606
  return false;
2353
- if (room.replica.admission !== 'admitted') {
2354
- if (replicas.has(room.locator))
2355
- progress.complete = false;
2356
- return false;
2357
- }
2358
- return true;
2607
+ return room.replica.admission === 'admitted';
2359
2608
  }
2360
2609
  async function caughtUp(room, local, progress, live) {
2361
- const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
2362
- if (held === undefined || authorization === undefined || !live())
2363
- return;
2364
- const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
2365
- if (!behind)
2610
+ if (authorization === undefined || !live() || !behind(room))
2366
2611
  return;
2367
2612
  const withheld = await advance(room, local, authorization, live);
2368
2613
  if (withheld !== null)
@@ -2390,6 +2635,8 @@ export function composeRuntime(options) {
2390
2635
  }
2391
2636
  async function reload(room, local, progress, live) {
2392
2637
  try {
2638
+ if (live())
2639
+ reloadsDue.delete(room.locator);
2393
2640
  if (!live() || authorization === undefined)
2394
2641
  return;
2395
2642
  const bootstrapped = await pullBootstrap(authorization, room, local, live);
@@ -2406,7 +2653,7 @@ export function composeRuntime(options) {
2406
2653
  caughtUpTo(room.locator);
2407
2654
  }
2408
2655
  }
2409
- async function discoverRooms(local, progress, attempt, live) {
2656
+ async function discoverRooms(local, progress, attempt, live, scope) {
2410
2657
  if (closed || authorization === undefined) {
2411
2658
  notifyReady();
2412
2659
  return;
@@ -2416,7 +2663,7 @@ export function composeRuntime(options) {
2416
2663
  const started = discoveries;
2417
2664
  let result;
2418
2665
  try {
2419
- result = await options.adapter.discover(authorization);
2666
+ result = await options.adapter.discover(authorization, scope);
2420
2667
  }
2421
2668
  catch {
2422
2669
  result = { kind: 'transport-uncertainty' };
@@ -2456,12 +2703,13 @@ export function composeRuntime(options) {
2456
2703
  }
2457
2704
  for (const room of result.rooms) {
2458
2705
  if (room.operations === undefined ||
2459
- !['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
2706
+ !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
2460
2707
  room.operations[operation].every((scope) => typeof scope === 'string')) ||
2461
2708
  room.replica === undefined ||
2462
2709
  typeof room.replica.selection !== 'string' ||
2463
2710
  room.replica.selection.length === 0 ||
2464
- !['admitted', 'unavailable'].includes(room.replica.admission)) {
2711
+ !['admitted', 'unavailable'].includes(room.replica.admission) ||
2712
+ !['locked', 'open'].includes(room.replica.offline)) {
2465
2713
  journal.record(`invalid model or replica declaration ${room.locator}`);
2466
2714
  await refuse('missing-required-guarantee');
2467
2715
  notifyReady();
@@ -2493,7 +2741,7 @@ export function composeRuntime(options) {
2493
2741
  member = signedIn;
2494
2742
  emitMember();
2495
2743
  }
2496
- await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
2744
+ await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
2497
2745
  }
2498
2746
  async function ensureAuthorization() {
2499
2747
  if (authorization !== undefined)
@@ -2590,9 +2838,44 @@ export function composeRuntime(options) {
2590
2838
  options.adapter.roomStream?.reopen();
2591
2839
  });
2592
2840
  }
2593
- function closedSocket(revoked) {
2841
+ function rediscover(scope) {
2842
+ return task(OPENING_LANE, async (live) => {
2843
+ if (!live() || !socketOpen)
2844
+ return;
2845
+ const progress = { complete: true, reachable: true };
2846
+ const gate = discover();
2847
+ moveTo(transferring(status));
2848
+ try {
2849
+ const local = await openLocal();
2850
+ if (!live())
2851
+ return;
2852
+ await ensureAuthorization();
2853
+ if (!live())
2854
+ return;
2855
+ await discoverRooms(local, progress, 'authorized', live, scope);
2856
+ }
2857
+ catch {
2858
+ progress.complete = false;
2859
+ }
2860
+ finally {
2861
+ gate.release();
2862
+ notifyReady();
2863
+ }
2864
+ if (!live())
2865
+ return;
2866
+ if (!progress.complete)
2867
+ complete = false;
2868
+ await rest(progress, live);
2869
+ });
2870
+ }
2871
+ function closedSocket(revoked, lost) {
2594
2872
  const opened = socketOpen;
2595
2873
  socketOpen = false;
2874
+ connectionLost();
2875
+ if (lost) {
2876
+ reads.lost();
2877
+ conversations.lost();
2878
+ }
2596
2879
  if (revoked) {
2597
2880
  void refuse('revoked-authorization');
2598
2881
  return;
@@ -2609,9 +2892,10 @@ export function composeRuntime(options) {
2609
2892
  function applyOpening(expired) {
2610
2893
  const progress = underway?.generation === generation ? underway.progress : null;
2611
2894
  const cycle = progress ?? { complete: true, reachable: true };
2612
- const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
2613
2895
  if (opened === generation)
2614
2896
  opened = null;
2897
+ const reloading = progress === null ? [] : owedReloads(expired);
2898
+ const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
2615
2899
  return task(OPENING_LANE, async (live) => {
2616
2900
  underway = null;
2617
2901
  if (!live())
@@ -2621,24 +2905,17 @@ export function composeRuntime(options) {
2621
2905
  await rest(cycle, live);
2622
2906
  return;
2623
2907
  }
2624
- await roomsCarried(expired);
2908
+ await roomsCarried([...expired, ...reloading]);
2625
2909
  if (!live())
2626
2910
  return;
2627
- const replaced = new Set(owed);
2628
- owed.clear();
2629
2911
  try {
2630
2912
  const local = await openLocal();
2631
- const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
2632
- const reloading = held.filter((room) => !expired.includes(room.locator) &&
2633
- (replaced.has(room.locator) || revoked(room.locator)));
2634
- for (const room of reloading)
2635
- catching.add(room.locator);
2636
- readSide.recover();
2637
- const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
2638
- const reloaded = [...(await reloads), ...(await replacing)];
2913
+ const reloaded = await replacing;
2639
2914
  if (progress.reachable) {
2640
- for (const room of held) {
2641
- if (!expired.includes(room.locator) && !reloading.includes(room)) {
2915
+ for (const room of discovered.values()) {
2916
+ if (replicas.has(room.locator) &&
2917
+ !expired.includes(room.locator) &&
2918
+ !reloading.includes(room.locator)) {
2642
2919
  await confirmModels(room, local);
2643
2920
  }
2644
2921
  }
@@ -2655,14 +2932,29 @@ export function composeRuntime(options) {
2655
2932
  await finishOpening(progress, live);
2656
2933
  });
2657
2934
  }
2935
+ function owedReloads(expired) {
2936
+ const reloading = [...discovered.values()]
2937
+ .filter((room) => replicas.has(room.locator) &&
2938
+ !expired.includes(room.locator) &&
2939
+ (owed.has(room.locator) || revoked(room.locator)))
2940
+ .map((room) => room.locator);
2941
+ owed.clear();
2942
+ for (const locator of reloading)
2943
+ catching.add(locator);
2944
+ readSide.recover();
2945
+ return reloading;
2946
+ }
2658
2947
  async function reloadNow(replaced, progress) {
2948
+ const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
2949
+ for (const room of reloaded)
2950
+ reloadsDue.set(room.locator, generation);
2659
2951
  try {
2660
2952
  const local = await openLocal();
2661
- return await Promise.allSettled([...discovered.values()]
2662
- .filter((room) => replicas.has(room.locator) && replaced(room))
2663
- .map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
2953
+ return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
2664
2954
  }
2665
2955
  catch {
2956
+ for (const room of reloaded)
2957
+ reloadsDue.delete(room.locator);
2666
2958
  progress.reachable = false;
2667
2959
  return [];
2668
2960
  }
@@ -2779,6 +3071,7 @@ export function composeRuntime(options) {
2779
3071
  progress.complete = false;
2780
3072
  return;
2781
3073
  }
3074
+ reads.applied();
2782
3075
  submitAll();
2783
3076
  for (const locator of stale)
2784
3077
  if (repairable.has(locator))
@@ -2787,10 +3080,11 @@ export function composeRuntime(options) {
2787
3080
  if (status.providerUnreachable)
2788
3081
  conversations.reconnected();
2789
3082
  const reading = { complete: true, reachable: true };
2790
- if (demand.conversations)
2791
- await conversations.catchUp(reading);
3083
+ await conversations.catchUp(reading);
2792
3084
  if (progress.reachable && reading.reachable)
2793
3085
  await conversations.resend(reading);
3086
+ if (progress.reachable)
3087
+ void recovery.attempt('calls', resendCalls);
2794
3088
  if (!reading.reachable) {
2795
3089
  progress.reachable = false;
2796
3090
  resendLater();
@@ -2851,7 +3145,7 @@ export function composeRuntime(options) {
2851
3145
  return;
2852
3146
  const room = discovered.get(locator);
2853
3147
  const held = replicas.get(locator);
2854
- if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
3148
+ if (room === undefined || room.replica.admission !== 'admitted')
2855
3149
  return;
2856
3150
  if (held !== undefined &&
2857
3151
  (held.confirmed.record.replica.selection !== room.replica.selection ||
@@ -2902,15 +3196,19 @@ export function composeRuntime(options) {
2902
3196
  return;
2903
3197
  if (event.kind === 'opened') {
2904
3198
  socketOpen = true;
3199
+ connectionLive();
2905
3200
  generation += 1;
3201
+ reads.opened();
2906
3202
  conversations.reopened();
2907
3203
  recovery.resume();
2908
3204
  void sync('opened');
2909
3205
  }
2910
3206
  else if (event.kind === 'closed')
2911
- closedSocket(event.revoked);
3207
+ closedSocket(event.revoked, event.lost);
2912
3208
  else if (event.kind === 'opening')
2913
3209
  void applyOpening(event.expired);
3210
+ else if (event.kind === 'authority-changed')
3211
+ void rediscover(event.scope);
2914
3212
  else
2915
3213
  void applyRoom(event.locator, event.roomVersion);
2916
3214
  }, receipts);
@@ -2965,7 +3263,7 @@ export function composeRuntime(options) {
2965
3263
  if (!live())
2966
3264
  return;
2967
3265
  await conversations.reconcile();
2968
- if (conversations.current() || !demand.conversations) {
3266
+ if (conversations.current()) {
2969
3267
  show();
2970
3268
  return;
2971
3269
  }
@@ -2983,10 +3281,10 @@ export function composeRuntime(options) {
2983
3281
  if (closed)
2984
3282
  return;
2985
3283
  closed = true;
2986
- if (narrowing !== null)
2987
- clearTimeout(narrowing);
2988
3284
  if (rechecking !== null)
2989
3285
  clearInterval(rechecking);
3286
+ connectionLive();
3287
+ lockListeners.clear();
2990
3288
  readinessWatches.clear();
2991
3289
  withdrawStream?.();
2992
3290
  withdrawStream = undefined;
@@ -3032,22 +3330,63 @@ export function composeRuntime(options) {
3032
3330
  if (options.adapter.roomStream === undefined)
3033
3331
  void sync('unstreamed');
3034
3332
  function planned(input, reach) {
3035
- return planOf(input, roomFacts(reach), planReach(reach));
3333
+ const plan = planOf(input, roomFacts(reach), planReach(reach));
3334
+ const named = plan.paths !== null &&
3335
+ plan.rooms !== null &&
3336
+ plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
3337
+ return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
3036
3338
  }
3037
3339
  function watchPlan(plan, deliver) {
3038
- return readSide.watch(plan, identityOf(plan), () => {
3340
+ let seeded = false;
3341
+ let pending = true;
3342
+ const shown = () => {
3343
+ const seed = seeds.get(plan.key);
3344
+ if (seed === undefined || seeded || !pending)
3345
+ return;
3346
+ seeded = true;
3347
+ deliver(seededResult(seed));
3348
+ };
3349
+ const watchers = seedWatchers.get(plan.key) ?? new Set();
3350
+ seedWatchers.set(plan.key, watchers);
3351
+ watchers.add(shown);
3352
+ const stop = readSide.watch(plan, identityOf(plan), () => {
3039
3353
  const cover = covered(plan);
3040
3354
  awaitPreparation(cover);
3041
- return cover;
3042
- }, deliver);
3355
+ return seedBehind(plan, cover);
3356
+ }, (message) => {
3357
+ const answered = message;
3358
+ pending = whole(answered) && 'pending' in answered.result;
3359
+ if (pending && seeds.has(plan.key)) {
3360
+ shown();
3361
+ return;
3362
+ }
3363
+ seeded = false;
3364
+ deliver(message);
3365
+ });
3366
+ return () => {
3367
+ watchers.delete(shown);
3368
+ if (watchers.size === 0)
3369
+ seedWatchers.delete(plan.key);
3370
+ stop();
3371
+ };
3043
3372
  }
3044
- async function searched(input, reach) {
3045
- const plan = searchPlanOf(input);
3046
- const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
3373
+ async function searched(input, reach, locked) {
3374
+ const asked = searchPlanOf(input);
3375
+ const plan = { ...asked, limit: capped(asked.limit) };
3376
+ const filter = {
3377
+ ...filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)),
3378
+ locked,
3379
+ };
3380
+ const unlocking = (filter.rooms ?? [])
3381
+ .filter((room) => locked.includes(room))
3382
+ .map((room) => ({ room, awaits: 'unlock' }));
3047
3383
  for (;;) {
3048
3384
  const cover = covered(filter);
3049
3385
  awaitPreparation(cover);
3050
- const pending = 'pending' in cover ? [...cover.pending] : [];
3386
+ const pending = [
3387
+ ...unlocking,
3388
+ ...('pending' in cover ? cover.pending : []),
3389
+ ];
3051
3390
  const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3052
3391
  const held = covered({
3053
3392
  ...filter,
@@ -3081,22 +3420,34 @@ export function composeRuntime(options) {
3081
3420
  if ('pending' in answered)
3082
3421
  continue;
3083
3422
  const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
3423
+ await readSide.settled();
3084
3424
  const seen = searchIndex.changes();
3085
3425
  await searchIndex.caughtUp();
3086
- const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(answering);
3426
+ const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
3087
3427
  if (selected !== null && 'pending' in selected)
3088
3428
  continue;
3089
3429
  if (searchIndex.changes() !== seen ||
3090
3430
  JSON.stringify(covered(answering)) !== JSON.stringify(answered))
3091
3431
  continue;
3092
- const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, complete ? null : new Set(answering.rooms));
3432
+ const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms !== null
3433
+ ? new Set(answering.rooms)
3434
+ : locked.length === 0
3435
+ ? null
3436
+ : new Set(reachedRooms(answering)), plan.collection
3437
+ ? new Map(answered.coverage.rooms.map(({ room }) => [
3438
+ room,
3439
+ replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
3440
+ ]))
3441
+ : null);
3093
3442
  const coverage = {
3094
3443
  rooms: answered.coverage.rooms,
3095
3444
  conversations: searchedConversations,
3096
3445
  index: searchIndex.generation(),
3097
3446
  overlay: answered.coverage.overlay,
3098
3447
  };
3099
- return complete ? { ...hits, coverage } : { pending, ...hits, coverage };
3448
+ return complete
3449
+ ? { ...hits, coverage }
3450
+ : { pending, documents: hits.documents, messages: hits.messages, coverage };
3100
3451
  }
3101
3452
  }
3102
3453
  const watch = (topic, input, deliver) => {
@@ -3206,7 +3557,7 @@ export function composeRuntime(options) {
3206
3557
  return listed();
3207
3558
  },
3208
3559
  query: ((input) => rowsQuery(planned(input, null))),
3209
- search: (input) => searched(input, null),
3560
+ search: (input) => searched(input, null, []),
3210
3561
  types(locator) {
3211
3562
  return {
3212
3563
  collection: modelTypes(modelOf(locator)),
@@ -3281,27 +3632,33 @@ export function composeRuntime(options) {
3281
3632
  });
3282
3633
  },
3283
3634
  write(app, value) {
3284
- return exclusive(async () => {
3285
- const local = await openLocal();
3286
- const stored = structuredClone(value);
3287
- await local.present({ app, value: stored });
3288
- presentations.set(app, stored);
3289
- publishChangeSet(changeSet('', null, null, { presentation: app }));
3290
- });
3635
+ return keepPresentation(app, () => structuredClone(value));
3636
+ },
3637
+ edit(app, members) {
3638
+ return keepPresentation(app, (held) => Object.fromEntries([
3639
+ ...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(members, member)),
3640
+ ...Object.entries(structuredClone(members)).filter(([, value]) => value !== null),
3641
+ ]));
3291
3642
  },
3292
3643
  },
3293
3644
  save(room, named, writtenBy) {
3294
3645
  return exclusive(() => saveRoomDrafts(room, named, writtenBy));
3295
3646
  },
3647
+ saveParts(room, parts, writtenBy) {
3648
+ return exclusive(() => saveParts(room, parts, writtenBy));
3649
+ },
3296
3650
  history(locator, path, query) {
3297
3651
  return readHistory(locator, path, query);
3298
3652
  },
3299
3653
  binaryFile: {
3300
3654
  read: readBinaryPath,
3655
+ link: linkBinaryPath,
3301
3656
  readReference: readBinaryReference,
3302
3657
  },
3303
- call(name, input) {
3304
- return invokeAction(name, input);
3658
+ async call(name, input) {
3659
+ await ready;
3660
+ const room = calledRoom(name, input);
3661
+ return room !== null && locked(room) ? lockedRoom(room) : recoveredAction(name, input);
3305
3662
  },
3306
3663
  reached(locator, roomVersion, path) {
3307
3664
  return path === undefined
@@ -3346,13 +3703,16 @@ export function composeRuntime(options) {
3346
3703
  watchPlan,
3347
3704
  search: searched,
3348
3705
  watchReadiness,
3706
+ locked,
3707
+ lockedRooms,
3708
+ watchLocks,
3349
3709
  };
3350
3710
  return {
3351
3711
  md,
3712
+ seed,
3352
3713
  stop,
3353
3714
  lose: loseRooms,
3354
3715
  watch,
3355
- acquire,
3356
3716
  persisted(answer) {
3357
3717
  if (answer === persistence)
3358
3718
  return;