@agentshouse/mdruntime 0.1.0-cd50a4607cb7d → 0.1.0-cdb8840578a20

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 (65) hide show
  1. package/AGENTS.md +42 -38
  2. package/dist/acquisition.d.ts +14 -0
  3. package/dist/acquisition.d.ts.map +1 -0
  4. package/dist/acquisition.js +24 -0
  5. package/dist/adapter.d.ts +9 -7
  6. package/dist/adapter.d.ts.map +1 -1
  7. package/dist/chat/kit.d.ts +2 -1
  8. package/dist/chat/kit.d.ts.map +1 -1
  9. package/dist/chat/kit.js +2 -2
  10. package/dist/chat/list.d.ts +2 -1
  11. package/dist/chat/list.d.ts.map +1 -1
  12. package/dist/chat/list.js +4 -3
  13. package/dist/chat/messages.d.ts +1 -0
  14. package/dist/chat/messages.d.ts.map +1 -1
  15. package/dist/chat/messages.js +4 -0
  16. package/dist/chat/provider.d.ts +1 -1
  17. package/dist/chat/provider.d.ts.map +1 -1
  18. package/dist/chat/provider.js +26 -17
  19. package/dist/chat/ready.d.ts +1 -0
  20. package/dist/chat/ready.d.ts.map +1 -1
  21. package/dist/chat/ready.js +2 -2
  22. package/dist/client.d.ts +5 -1
  23. package/dist/client.d.ts.map +1 -1
  24. package/dist/client.js +2 -0
  25. package/dist/connection.d.ts +13 -2
  26. package/dist/connection.d.ts.map +1 -1
  27. package/dist/connection.js +424 -36
  28. package/dist/conversation.d.ts +2 -0
  29. package/dist/conversation.d.ts.map +1 -1
  30. package/dist/conversation.js +75 -12
  31. package/dist/durability.d.ts +7 -1
  32. package/dist/durability.d.ts.map +1 -1
  33. package/dist/host.d.ts +10 -2
  34. package/dist/host.d.ts.map +1 -1
  35. package/dist/host.js +35 -6
  36. package/dist/index.d.ts +3 -2
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/memory.d.ts +2 -1
  39. package/dist/memory.d.ts.map +1 -1
  40. package/dist/memory.js +6 -1
  41. package/dist/model.d.ts +3 -2
  42. package/dist/model.d.ts.map +1 -1
  43. package/dist/model.js +1 -1
  44. package/dist/query.d.ts +2 -1
  45. package/dist/query.d.ts.map +1 -1
  46. package/dist/reach.d.ts +6 -2
  47. package/dist/reach.d.ts.map +1 -1
  48. package/dist/reach.js +14 -2
  49. package/dist/react.d.ts +2 -0
  50. package/dist/react.d.ts.map +1 -1
  51. package/dist/readiness.d.ts +53 -0
  52. package/dist/readiness.d.ts.map +1 -0
  53. package/dist/readiness.js +33 -0
  54. package/dist/room-draft.d.ts +1 -1
  55. package/dist/room-draft.d.ts.map +1 -1
  56. package/dist/search.d.ts +6 -5
  57. package/dist/search.d.ts.map +1 -1
  58. package/dist/search.js +6 -8
  59. package/dist/store.d.ts +9 -1
  60. package/dist/store.d.ts.map +1 -1
  61. package/dist/store.js +35 -11
  62. package/dist/wire.d.ts +136 -32
  63. package/dist/wire.d.ts.map +1 -1
  64. package/dist/wire.js +48 -31
  65. package/package.json +1 -1
@@ -21,6 +21,8 @@ import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, re
21
21
  import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
22
22
  import { same, saving, STARTING, transferring, } from './status.js';
23
23
  import { detectStorageLoss, nextGeneration } from './durability.js';
24
+ import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
25
+ import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
24
26
  import { FencedMap } from './fence.js';
25
27
  import { tally } from './shared.js';
26
28
  import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
@@ -166,8 +168,18 @@ export function composeRuntime(options) {
166
168
  const memberListeners = new Set();
167
169
  const prepared = new Set();
168
170
  const projecting = new Set();
169
- const demanded = new Set();
171
+ const unpreparedDemand = new Set();
172
+ const unindexable = new Set();
170
173
  let preparing = false;
174
+ let demand = WHOLE_DEMAND;
175
+ let narrowing = null;
176
+ let targets = {};
177
+ let persistence = options.shell.persistence;
178
+ let capacity = null;
179
+ const paused = new Set();
180
+ let rechecking = null;
181
+ const readinessWatches = new Set();
182
+ let readinessQueued = false;
171
183
  const catching = new Set();
172
184
  const transfers = new FencedMap(isFenced);
173
185
  const conversations = createConversationStore({
@@ -325,15 +337,18 @@ export function composeRuntime(options) {
325
337
  }
326
338
  function emitRooms() {
327
339
  readSide.recover();
340
+ queueReadiness();
328
341
  const value = listed();
329
342
  for (const listener of roomListeners)
330
343
  listener(value);
331
344
  }
332
345
  function emitRefusal() {
346
+ queueReadiness();
333
347
  for (const listener of refusalListeners)
334
348
  listener(refusal);
335
349
  }
336
350
  function emitStorageLoss() {
351
+ queueReadiness();
337
352
  for (const listener of storageLossListeners)
338
353
  listener(storageLoss);
339
354
  }
@@ -350,7 +365,8 @@ export function composeRuntime(options) {
350
365
  function show() {
351
366
  if (closed)
352
367
  return;
353
- const next = status.state === 'synced' && !conversations.current()
368
+ queueReadiness();
369
+ const next = status.state === 'synced' && demand.conversations && !conversations.current()
354
370
  ? { ...status, state: 'syncing' }
355
371
  : status;
356
372
  if (same(shown, next))
@@ -427,13 +443,19 @@ export function composeRuntime(options) {
427
443
  }
428
444
  conversations.changed(mergeChangeSets(entered));
429
445
  readSide.recover();
446
+ queueReadiness();
430
447
  }
431
448
  function unprepared() {
432
- return [...demanded, ...replicas.keys()].find((locator) => !prepared.has(locator));
449
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
450
+ }
451
+ function unbuilt(locator, error) {
452
+ unindexable.add(locator);
453
+ journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
454
+ queueReadiness();
433
455
  }
434
456
  function prepare(locators = []) {
435
457
  for (const locator of locators)
436
- demanded.add(locator);
458
+ unpreparedDemand.add(locator);
437
459
  if (preparing)
438
460
  return;
439
461
  preparing = true;
@@ -443,8 +465,8 @@ export function composeRuntime(options) {
443
465
  await yielded();
444
466
  if (closed)
445
467
  return;
446
- demanded.delete(waiting);
447
- await prepareReplica(waiting);
468
+ unpreparedDemand.delete(waiting);
469
+ await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
448
470
  }
449
471
  }
450
472
  finally {
@@ -502,6 +524,292 @@ export function composeRuntime(options) {
502
524
  if (waiting.length > 0)
503
525
  prepare(waiting.map(({ room }) => room));
504
526
  }
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) {
579
+ if (reach === null)
580
+ return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
581
+ const named = [...new Set(reach.rooms)].sort();
582
+ if (roomsListed)
583
+ return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
584
+ return { listed: named.every((room) => replicas.has(room)), rooms: named };
585
+ }
586
+ function roomState(locator, search) {
587
+ if (sourceRefusals.has(locator))
588
+ return 'refused';
589
+ const listedRoom = discovered.get(locator);
590
+ if (roomsListed &&
591
+ (listedRoom === undefined ||
592
+ listedRoom.replica.admission !== 'admitted' ||
593
+ unreadable.has(locator)))
594
+ return 'unadmitted';
595
+ if (!replicas.has(locator))
596
+ return 'acquiring';
597
+ if (!prepared.has(locator))
598
+ return 'preparing';
599
+ if (search && !searchIndex.rooms().has(locator))
600
+ return 'indexing';
601
+ return 'complete';
602
+ }
603
+ function conversationCoverage(reach, search) {
604
+ const declared = reach === null || reach.conversations;
605
+ if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
606
+ return 'undeclared';
607
+ if (!conversations.known() || !conversations.current())
608
+ return 'acquiring';
609
+ if (!search)
610
+ return 'complete';
611
+ const unindexed = conversations
612
+ .searchable()
613
+ .filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
614
+ if (unindexed.length === 0)
615
+ return 'complete';
616
+ index([], unindexed);
617
+ return 'indexing';
618
+ }
619
+ function connectionOf() {
620
+ if (socketOpen)
621
+ return 'connected';
622
+ if (status.providerUnreachable)
623
+ return 'unreachable';
624
+ return options.adapter.roomStream === undefined && status.lastCompleted !== null
625
+ ? 'connected'
626
+ : 'connecting';
627
+ }
628
+ function refusedIn(locators) {
629
+ return locators.flatMap((room) => {
630
+ const held = sourceRefusals.get(room);
631
+ if (held === undefined)
632
+ return [];
633
+ const { file, location } = held.fields;
634
+ return [
635
+ {
636
+ room,
637
+ file: typeof file === 'string' ? file : '',
638
+ rule: held.code,
639
+ location: typeof location === 'string' ? location : null,
640
+ },
641
+ ];
642
+ });
643
+ }
644
+ function readinessFor(port, reach) {
645
+ const required = requiredRooms(reach);
646
+ const target = targets[port.target];
647
+ const search = target?.search === true;
648
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
649
+ const building = (state) => coverage
650
+ .filter((room) => room.state === state && !unindexable.has(room.room))
651
+ .map(({ room }) => room);
652
+ const preparing = building('preparing');
653
+ if (preparing.length > 0)
654
+ prepare(preparing);
655
+ const indexing = building('indexing');
656
+ if (indexing.length > 0)
657
+ index(indexing, []);
658
+ return readinessOf({
659
+ code: port.code,
660
+ coverage: {
661
+ listed: required.listed,
662
+ rooms: coverage,
663
+ conversations: conversationCoverage(reach, search),
664
+ search: search ? 'required' : 'unused',
665
+ },
666
+ persistence,
667
+ connection: connectionOf(),
668
+ house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
669
+ capacity: capacity !== null &&
670
+ coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
671
+ ? capacity
672
+ : port.capacity,
673
+ refused: refusedIn(required.rooms),
674
+ lost: storageLoss !== null,
675
+ established: target?.established === true,
676
+ });
677
+ }
678
+ function keepTarget(name, change) {
679
+ const held = targets[name] ?? { established: false, search: false };
680
+ const next = { ...held, ...change };
681
+ if (next.established === held.established && next.search === held.search)
682
+ return;
683
+ targets = { ...targets, [name]: next };
684
+ queueReadiness();
685
+ void durably((local) => local.keepTargets(targets)).catch(() => {
686
+ journal.record(`target record failed ${name}`);
687
+ });
688
+ }
689
+ function deliverReadiness(watch) {
690
+ if (!readinessWatches.has(watch))
691
+ return;
692
+ const readiness = readinessFor(watch.port, watch.reach);
693
+ if (readiness.ready && targets[watch.port.target]?.established !== true) {
694
+ keepTarget(watch.port.target, { established: true });
695
+ }
696
+ const shown = JSON.stringify(readiness);
697
+ if (shown === watch.last)
698
+ return;
699
+ watch.last = shown;
700
+ watch.deliver(structuredClone(readiness));
701
+ }
702
+ function queueReadiness() {
703
+ if (readinessQueued || closed || readinessWatches.size === 0)
704
+ return;
705
+ readinessQueued = true;
706
+ queueMicrotask(() => {
707
+ readinessQueued = false;
708
+ if (closed)
709
+ return;
710
+ for (const watch of [...readinessWatches])
711
+ deliverReadiness(watch);
712
+ });
713
+ }
714
+ function watchReadiness(port, reach, deliver) {
715
+ const watch = { port, reach, deliver, last: null };
716
+ readinessWatches.add(watch);
717
+ void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
718
+ return () => {
719
+ readinessWatches.delete(watch);
720
+ };
721
+ }
722
+ function searching(target) {
723
+ if (targets[target]?.search !== true)
724
+ keepTarget(target, { search: true });
725
+ }
726
+ function layerBytes(layer) {
727
+ let bytes = 0;
728
+ for (const [path, text] of Object.entries(layer.documents))
729
+ bytes += (path.length + text.length) * 2;
730
+ return bytes;
731
+ }
732
+ function grownBy(next, previous) {
733
+ if (previous === undefined)
734
+ return layerBytes(next);
735
+ const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
736
+ return layerBytes({ ...next, documents });
737
+ }
738
+ function exceeded(error) {
739
+ return (typeof error === 'object' &&
740
+ error !== null &&
741
+ error.name === 'QuotaExceededError');
742
+ }
743
+ async function estimated() {
744
+ const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
745
+ return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
746
+ }
747
+ function pause(locator, cause) {
748
+ paused.add(locator);
749
+ capacity = cause;
750
+ journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
751
+ queueReadiness();
752
+ if (rechecking !== null)
753
+ return;
754
+ // A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
755
+ rechecking = setInterval(() => {
756
+ void recheck();
757
+ }, 60_000);
758
+ }
759
+ async function recheck() {
760
+ const held = capacity;
761
+ if (held === null || closed)
762
+ return;
763
+ const available = await estimated().catch(() => null);
764
+ if (available === null || available < (held.needed ?? 0))
765
+ return;
766
+ if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
767
+ return;
768
+ resume();
769
+ }
770
+ function resume() {
771
+ if (rechecking !== null)
772
+ clearInterval(rechecking);
773
+ rechecking = null;
774
+ if (paused.size === 0 && capacity === null)
775
+ return;
776
+ paused.clear();
777
+ capacity = null;
778
+ journal.record('capacity resumed');
779
+ queueReadiness();
780
+ if (!closed && authorization !== undefined)
781
+ void sync('unstreamed');
782
+ }
783
+ async function reclaimed(local) {
784
+ const unreferenced = [...transfers.keys()].filter((locator) => {
785
+ const room = discovered.get(locator);
786
+ return room === undefined || !acquires(room);
787
+ });
788
+ for (const locator of unreferenced) {
789
+ await local.abandon(locator);
790
+ transfers.delete(locator);
791
+ }
792
+ if (unreferenced.length > 0)
793
+ journal.record(`capacity reclaimed ${unreferenced.length}`);
794
+ return unreferenced.length > 0;
795
+ }
796
+ async function admitted(local, locator, needed) {
797
+ if (paused.has(locator))
798
+ return false;
799
+ let available = await estimated().catch(() => null);
800
+ if (available === null || available >= needed)
801
+ return true;
802
+ if (await reclaimed(local))
803
+ available = await estimated().catch(() => null);
804
+ if (available === null || available >= needed)
805
+ return true;
806
+ pause(locator, { cause: 'insufficient-space', needed, available });
807
+ return false;
808
+ }
809
+ async function exceededOn(locator, bytes) {
810
+ const available = await estimated().catch(() => null);
811
+ pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
812
+ }
505
813
  async function rowsQuery(plan) {
506
814
  const before = covered(plan);
507
815
  awaitPreparation(before);
@@ -529,10 +837,12 @@ export function composeRuntime(options) {
529
837
  }
530
838
  }
531
839
  function indexNext() {
532
- const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator));
840
+ const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
533
841
  if (room !== undefined) {
534
842
  roomsToIndex.delete(room);
535
- return () => (prepared.has(room) ? searchIndex.indexRoom(room) : undefined);
843
+ return () => prepared.has(room)
844
+ ? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
845
+ : undefined;
536
846
  }
537
847
  const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
538
848
  if (conversation === undefined)
@@ -553,6 +863,7 @@ export function composeRuntime(options) {
553
863
  for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
554
864
  await yielded();
555
865
  await unit();
866
+ queueReadiness();
556
867
  }
557
868
  }
558
869
  finally {
@@ -726,6 +1037,8 @@ export function composeRuntime(options) {
726
1037
  unreadable.delete(locator);
727
1038
  stale.delete(locator);
728
1039
  repairable.delete(locator);
1040
+ unindexable.delete(locator);
1041
+ paused.delete(locator);
729
1042
  scopedRefusals.delete(locator);
730
1043
  sourceRefusals.delete(locator);
731
1044
  declarationRefusals.delete(locator);
@@ -779,7 +1092,7 @@ export function composeRuntime(options) {
779
1092
  return model;
780
1093
  }
781
1094
  async function hydrate(opened) {
782
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
1095
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
783
1096
  opened.all(),
784
1097
  opened.queues(),
785
1098
  opened.roomDrafts(),
@@ -791,7 +1104,9 @@ export function composeRuntime(options) {
791
1104
  opened.capabilities(),
792
1105
  opened.member(),
793
1106
  opened.transfers(),
1107
+ opened.targets(),
794
1108
  ]);
1109
+ targets = heldTargets;
795
1110
  transfers.clear();
796
1111
  for (const record of staged)
797
1112
  transfers.set(record.locator, record);
@@ -1037,7 +1352,18 @@ export function composeRuntime(options) {
1037
1352
  const accepted = pendings.get(batch.id)?.accepted;
1038
1353
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
1039
1354
  });
1040
- const set = (await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) }))();
1355
+ let settled;
1356
+ try {
1357
+ settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
1358
+ }
1359
+ catch (error) {
1360
+ if (!exceeded(error))
1361
+ throw error;
1362
+ await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
1363
+ caughtUpTo(room.locator);
1364
+ return true;
1365
+ }
1366
+ const set = settled();
1041
1367
  if (stored !== undefined) {
1042
1368
  transfers.delete(room.locator);
1043
1369
  catching.delete(room.locator);
@@ -1045,6 +1371,7 @@ export function composeRuntime(options) {
1045
1371
  for (const batch of accepted)
1046
1372
  pendings.delete(batch.id);
1047
1373
  stale.delete(room.locator);
1374
+ unindexable.delete(room.locator);
1048
1375
  if (unreadable.delete(room.locator))
1049
1376
  scopedRefusals.delete(room.locator);
1050
1377
  sourceRefusals.delete(room.locator);
@@ -1453,7 +1780,18 @@ export function composeRuntime(options) {
1453
1780
  stage: (record, page) => exclusive(async () => {
1454
1781
  if (!live())
1455
1782
  return false;
1456
- await local.stage(record, page);
1783
+ const bytes = layerBytes(page);
1784
+ if (!(await admitted(local, record.locator, bytes)))
1785
+ return false;
1786
+ try {
1787
+ await local.stage(record, page);
1788
+ }
1789
+ catch (error) {
1790
+ if (!exceeded(error))
1791
+ throw error;
1792
+ await exceededOn(record.locator, bytes);
1793
+ return false;
1794
+ }
1457
1795
  transfers.set(record.locator, record);
1458
1796
  readSide.recover();
1459
1797
  return true;
@@ -1503,7 +1841,7 @@ export function composeRuntime(options) {
1503
1841
  }
1504
1842
  async function advance(room, local, authorized, live) {
1505
1843
  await referenced(room.locator);
1506
- if (!live())
1844
+ if (!live() || paused.has(room.locator))
1507
1845
  return unconfirmed;
1508
1846
  const held = replicas.get(room.locator);
1509
1847
  if (held !== undefined) {
@@ -1984,15 +2322,16 @@ export function composeRuntime(options) {
1984
2322
  });
1985
2323
  }
1986
2324
  async function synchronize(listed, local, progress, attempt, live) {
1987
- const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
2325
+ const acquired = listed.filter(acquires);
2326
+ const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
1988
2327
  if (!live())
1989
2328
  return;
1990
- const ready = listed.filter((_, index) => {
2329
+ const ready = acquired.filter((_, index) => {
1991
2330
  const outcome = prepared[index];
1992
2331
  return outcome?.status === 'fulfilled' && outcome.value;
1993
2332
  });
1994
2333
  if (attempt === 'opened') {
1995
- options.adapter.roomStream?.hold(heldRooms(ready));
2334
+ options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
1996
2335
  handed = true;
1997
2336
  }
1998
2337
  discovering?.release();
@@ -2457,7 +2796,8 @@ export function composeRuntime(options) {
2457
2796
  if (status.providerUnreachable)
2458
2797
  conversations.reconnected();
2459
2798
  const reading = { complete: true, reachable: true };
2460
- await conversations.catchUp(reading);
2799
+ if (demand.conversations)
2800
+ await conversations.catchUp(reading);
2461
2801
  if (progress.reachable && reading.reachable)
2462
2802
  await conversations.resend(reading);
2463
2803
  if (!reading.reachable) {
@@ -2520,7 +2860,7 @@ export function composeRuntime(options) {
2520
2860
  return;
2521
2861
  const room = discovered.get(locator);
2522
2862
  const held = replicas.get(locator);
2523
- if (room === undefined || room.replica.admission !== 'admitted')
2863
+ if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
2524
2864
  return;
2525
2865
  if (held !== undefined &&
2526
2866
  (held.confirmed.record.replica.selection !== room.replica.selection ||
@@ -2597,6 +2937,7 @@ export function composeRuntime(options) {
2597
2937
  haltedRefusal = false;
2598
2938
  if (mode !== serviceMode) {
2599
2939
  serviceMode = mode;
2940
+ queueReadiness();
2600
2941
  journal.record(`service mode ${mode}`);
2601
2942
  for (const listener of serviceModeListeners)
2602
2943
  listener(mode);
@@ -2609,6 +2950,8 @@ export function composeRuntime(options) {
2609
2950
  if (stream === undefined || withdrawConversations !== undefined)
2610
2951
  return;
2611
2952
  withdrawConversations = stream.observe((event) => {
2953
+ if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
2954
+ queueReadiness();
2612
2955
  if (conversations.receive(event)) {
2613
2956
  void ready.then(scheduleCurrency);
2614
2957
  return;
@@ -2631,7 +2974,7 @@ export function composeRuntime(options) {
2631
2974
  if (!live())
2632
2975
  return;
2633
2976
  await conversations.reconcile();
2634
- if (conversations.current()) {
2977
+ if (conversations.current() || !demand.conversations) {
2635
2978
  show();
2636
2979
  return;
2637
2980
  }
@@ -2649,6 +2992,11 @@ export function composeRuntime(options) {
2649
2992
  if (closed)
2650
2993
  return;
2651
2994
  closed = true;
2995
+ if (narrowing !== null)
2996
+ clearTimeout(narrowing);
2997
+ if (rechecking !== null)
2998
+ clearInterval(rechecking);
2999
+ readinessWatches.clear();
2652
3000
  withdrawStream?.();
2653
3001
  withdrawStream = undefined;
2654
3002
  withdrawConversations?.();
@@ -2709,40 +3057,55 @@ export function composeRuntime(options) {
2709
3057
  const cover = covered(filter);
2710
3058
  awaitPreparation(cover);
2711
3059
  const pending = 'pending' in cover ? [...cover.pending] : [];
2712
- const covering = 'pending' in cover ? [] : cover.coverage.rooms;
3060
+ const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3061
+ const held = covered({
3062
+ ...filter,
3063
+ rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
3064
+ });
3065
+ const covering = 'pending' in held ? [] : held.coverage.rooms;
2713
3066
  const unindexed = covering
2714
3067
  .filter(({ room }) => !searchIndex.rooms().has(room))
2715
3068
  .map(({ room }) => room);
2716
3069
  for (const room of unindexed)
2717
3070
  pending.push({ room, awaits: 'index' });
2718
- const held = conversations.searchable();
3071
+ const listed = conversations.searchable();
2719
3072
  const named = reach !== null && !reach.conversations
2720
3073
  ? []
2721
- : (plan.conversations ?? held).filter((one) => held.includes(one) && conversations.acquired(one));
3074
+ : (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
2722
3075
  const waiting = named.filter((one) => !searchIndex.conversations().has(one));
2723
3076
  for (const conversation of waiting)
2724
3077
  pending.push({ conversation, awaits: 'index' });
2725
3078
  if (unindexed.length > 0 || waiting.length > 0)
2726
3079
  index(unindexed, waiting);
2727
- if (pending.length > 0 || 'pending' in cover)
2728
- return { pending };
3080
+ const complete = pending.length === 0 && !('pending' in cover);
3081
+ const answering = complete
3082
+ ? filter
3083
+ : {
3084
+ ...filter,
3085
+ rooms: covering
3086
+ .filter(({ room }) => searchIndex.rooms().has(room))
3087
+ .map(({ room }) => room),
3088
+ };
3089
+ const answered = covered(answering);
3090
+ if ('pending' in answered)
3091
+ continue;
3092
+ const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
2729
3093
  const seen = searchIndex.changes();
2730
3094
  await searchIndex.caughtUp();
2731
- const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(filter);
3095
+ const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(answering);
2732
3096
  if (selected !== null && 'pending' in selected)
2733
- return selected;
3097
+ continue;
2734
3098
  if (searchIndex.changes() !== seen ||
2735
- JSON.stringify(covered(filter)) !== JSON.stringify(cover))
3099
+ JSON.stringify(covered(answering)) !== JSON.stringify(answered))
2736
3100
  continue;
2737
- return {
2738
- ...searchIndex.search(plan, selected === null ? null : selected.documents, named),
2739
- coverage: {
2740
- rooms: covering,
2741
- conversations: named,
2742
- index: searchIndex.generation(),
2743
- overlay: cover.coverage.overlay,
2744
- },
3101
+ const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, complete ? null : new Set(answering.rooms));
3102
+ const coverage = {
3103
+ rooms: answered.coverage.rooms,
3104
+ conversations: searchedConversations,
3105
+ index: searchIndex.generation(),
3106
+ overlay: answered.coverage.overlay,
2745
3107
  };
3108
+ return complete ? { ...hits, coverage } : { pending, ...hits, coverage };
2746
3109
  }
2747
3110
  }
2748
3111
  const watch = (topic, input, deliver) => {
@@ -2789,6 +3152,9 @@ export function composeRuntime(options) {
2789
3152
  presentationListeners.delete(listener);
2790
3153
  };
2791
3154
  }
3155
+ if (topic === 'readiness') {
3156
+ return watchReadiness({ target: '', code: 'stored', capacity: null }, null, deliver);
3157
+ }
2792
3158
  if (topic === 'rooms') {
2793
3159
  roomListeners.add(deliver);
2794
3160
  deliver(listed());
@@ -2955,7 +3321,13 @@ export function composeRuntime(options) {
2955
3321
  return journal.export();
2956
3322
  },
2957
3323
  persistence() {
2958
- return options.shell.persistentStorage;
3324
+ return persistence === 'granted';
3325
+ },
3326
+ async retry() {
3327
+ journal.record('retry');
3328
+ unindexable.clear();
3329
+ queueReadiness();
3330
+ resume();
2959
3331
  },
2960
3332
  observe: observing(watch),
2961
3333
  async reauthorize() {
@@ -2982,6 +3354,22 @@ export function composeRuntime(options) {
2982
3354
  query: (plan) => rowsQuery(plan),
2983
3355
  watchPlan,
2984
3356
  search: searched,
3357
+ searching,
3358
+ watchReadiness,
3359
+ };
3360
+ return {
3361
+ md,
3362
+ stop,
3363
+ lose: loseRooms,
3364
+ watch,
3365
+ acquire,
3366
+ persisted(answer) {
3367
+ if (answer === persistence)
3368
+ return;
3369
+ persistence = answer;
3370
+ journal.record(`persistence ${answer}`);
3371
+ queueReadiness();
3372
+ },
3373
+ within: (reach, port) => narrowed(reachable, reach, port),
2985
3374
  };
2986
- return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
2987
3375
  }