@agentshouse/mdruntime 0.1.0-ce531b51ff088 → 0.1.0-cff8ba8fa2542

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 (66) hide show
  1. package/AGENTS.md +32 -8
  2. package/README.md +10 -10
  3. package/dist/adapter.d.ts +50 -25
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/adapter.js +1 -9
  6. package/dist/attached-model.d.ts +11 -0
  7. package/dist/attached-model.d.ts.map +1 -0
  8. package/dist/attached-model.js +34 -0
  9. package/dist/bootstrap.d.ts +5 -3
  10. package/dist/bootstrap.d.ts.map +1 -1
  11. package/dist/bootstrap.js +2 -2
  12. package/dist/client.d.ts +3 -2
  13. package/dist/client.d.ts.map +1 -1
  14. package/dist/client.js +3 -10
  15. package/dist/connection.d.ts +9 -12
  16. package/dist/connection.d.ts.map +1 -1
  17. package/dist/connection.js +495 -511
  18. package/dist/conversation.d.ts +18 -6
  19. package/dist/conversation.d.ts.map +1 -1
  20. package/dist/conversation.js +179 -78
  21. package/dist/host.d.ts +7 -1
  22. package/dist/host.d.ts.map +1 -1
  23. package/dist/host.js +34 -31
  24. package/dist/index.d.ts +3 -3
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/memory.d.ts +11 -5
  27. package/dist/memory.d.ts.map +1 -1
  28. package/dist/memory.js +77 -26
  29. package/dist/model.d.ts +4 -11
  30. package/dist/model.d.ts.map +1 -1
  31. package/dist/model.js +4 -3
  32. package/dist/observation.d.ts +2 -0
  33. package/dist/observation.d.ts.map +1 -1
  34. package/dist/observation.js +24 -14
  35. package/dist/query.d.ts +7 -2
  36. package/dist/query.d.ts.map +1 -1
  37. package/dist/query.js +29 -20
  38. package/dist/reach.d.ts +40 -0
  39. package/dist/reach.d.ts.map +1 -0
  40. package/dist/reach.js +285 -0
  41. package/dist/react.d.ts +31 -0
  42. package/dist/react.d.ts.map +1 -0
  43. package/dist/react.js +62 -0
  44. package/dist/read-side.d.ts +4 -0
  45. package/dist/read-side.d.ts.map +1 -1
  46. package/dist/read-side.js +16 -10
  47. package/dist/refresh.d.ts +2 -1
  48. package/dist/refresh.d.ts.map +1 -1
  49. package/dist/refresh.js +7 -3
  50. package/dist/replica.d.ts +4 -4
  51. package/dist/replica.d.ts.map +1 -1
  52. package/dist/replica.js +12 -13
  53. package/dist/room-draft.d.ts +3 -0
  54. package/dist/room-draft.d.ts.map +1 -1
  55. package/dist/room-draft.js +6 -4
  56. package/dist/rows.d.ts.map +1 -1
  57. package/dist/rows.js +1 -4
  58. package/dist/save.d.ts +0 -2
  59. package/dist/save.d.ts.map +1 -1
  60. package/dist/save.js +3 -8
  61. package/dist/store.d.ts +0 -1
  62. package/dist/store.d.ts.map +1 -1
  63. package/dist/wire.d.ts +51 -16
  64. package/dist/wire.d.ts.map +1 -1
  65. package/dist/wire.js +50 -59
  66. package/package.json +14 -2
@@ -1,14 +1,13 @@
1
- import { verify } from '@agentshouse/mdmodel';
1
+ import { attachedModel } from './attached-model.js';
2
2
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
3
- import { opened } from './observation.js';
3
+ import { observing } from './observation.js';
4
4
  import { filterOf, identityOf, planOf, } from './query.js';
5
5
  import { createReadSide } from './read-side.js';
6
6
  import { createSearchIndex, searchPlanOf, } from './search.js';
7
- import { ROOM_CONTRACT_VERSION, VERSION_1_CAPABILITIES, } from './adapter.js';
8
7
  import { bootstrapRoom } from './bootstrap.js';
9
8
  import { changeSet, mergeChangeSets } from './change-set.js';
10
9
  import { createConversationStore, } from './conversation.js';
11
- import { MD_MODEL, parseCollection, validRoot } from './collection.js';
10
+ import { parseCollection, validRoot } from './collection.js';
12
11
  import { createLog } from './log.js';
13
12
  import { refreshRoom } from './refresh.js';
14
13
  import { buildBatch } from './save.js';
@@ -20,37 +19,21 @@ import { roomDraftRevision } from './identity.js';
20
19
  import { beneath, normalize } from './paths.js';
21
20
  import { openRuntimeStore, } from './store.js';
22
21
  import { unconfirmed } from './transfer.js';
23
- // Discovery repeats each minute so a Room the stream stopped serving stays unrefreshed no longer.
24
- const DEFAULT_DISCOVERY_INTERVAL_MS = 60_000;
25
- const EMPTY_SCHEMA = { verdict: 'accept', model: { types: {} } };
26
- const knownCapabilities = new Set(VERSION_1_CAPABILITIES);
22
+ import { governs, holds, narrowed, planReach, } from './reach.js';
27
23
  function within(root, path) {
28
24
  return root === '' || beneath(root, path);
29
25
  }
30
- function declaredIdentity(model) {
31
- if (model === undefined)
32
- return false;
33
- if (model === null)
34
- return true;
35
- return (typeof model.identity === 'string' &&
36
- model.identity.length > 0 &&
37
- typeof model.version === 'string' &&
38
- model.version.length > 0);
39
- }
40
26
  function declaredSelection(room) {
41
27
  const selection = room.typedSelection;
42
28
  if (selection === undefined)
43
29
  return true;
44
30
  if (!Array.isArray(selection.roots) || selection.roots.length === 0)
45
31
  return false;
46
- if (!declaredIdentity(selection.attachedModel))
47
- return false;
48
32
  return selection.roots.every((root, index) => validRoot(root) &&
49
33
  root !== '' &&
50
34
  selection.roots.every((other, at) => at === index || !within(other, root)));
51
35
  }
52
36
  export function composeRuntime(options) {
53
- const period = options.discoveryIntervalMs ?? DEFAULT_DISCOVERY_INTERVAL_MS;
54
37
  let authorization = options.authorization;
55
38
  let opening;
56
39
  let store;
@@ -58,20 +41,13 @@ export function composeRuntime(options) {
58
41
  const pendings = new Map();
59
42
  const roomDrafts = new Map();
60
43
  const presentations = new Map();
61
- const wanted = new Map();
62
44
  let limits = [];
63
45
  let capabilities = [];
64
- function rewriting() {
65
- return capabilities.some((capability) => capability.name === 'move-reference-rewrites')
66
- ? 'provider'
67
- : 'runtime';
68
- }
69
46
  const rooms = new Map();
70
47
  const unreadable = new Set();
71
48
  const models = new Map();
72
49
  const liveModels = new Map();
73
50
  const invalidModels = new Set();
74
- const modelEpoch = new Map();
75
51
  const discovered = new Map();
76
52
  let roomsListed = false;
77
53
  const scopedRefusals = new Map();
@@ -100,16 +76,19 @@ export function composeRuntime(options) {
100
76
  let status = STARTING;
101
77
  let shown = STARTING;
102
78
  let discoveryPaused = false;
103
- let timer;
79
+ let resetTimer;
104
80
  let closed = false;
105
81
  let stopping = false;
106
- let withdrawNotifications;
107
- let handed;
108
- const notices = new Map();
82
+ let withdrawStream;
83
+ const announced = new Map();
84
+ let underway = null;
85
+ const submitting = new Map();
86
+ const answering = new Map();
87
+ let complete = false;
88
+ let recorded = false;
89
+ const stale = new Set();
109
90
  let chain = Promise.resolve();
110
91
  let mutations = Promise.resolve();
111
- let inflight = false;
112
- let queued = false;
113
92
  const roomListeners = new Set();
114
93
  const refusalListeners = new Set();
115
94
  const storageLossListeners = new Set();
@@ -120,12 +99,13 @@ export function composeRuntime(options) {
120
99
  const readSide = createReadSide();
121
100
  let withdrawConversations;
122
101
  let currencyQueued = false;
102
+ let socketOpen = false;
123
103
  let serviceMode = 'open';
124
104
  let haltedRefusal = false;
125
105
  let openSignals = 0;
126
106
  const serviceModeListeners = new Set();
127
107
  const conversations = createConversationStore({
128
- cards: declaredCards,
108
+ agents: declaredAgents,
129
109
  documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
130
110
  call: (name, input) => invokeAction(name, input),
131
111
  durably,
@@ -234,11 +214,14 @@ export function composeRuntime(options) {
234
214
  function listed() {
235
215
  return structuredClone([...rooms.values()]);
236
216
  }
237
- function roomFacts() {
238
- const held = [...rooms.values()].map((room) => {
217
+ function roomFacts(reach = null) {
218
+ const held = [...rooms.values()]
219
+ .filter((room) => holds(reach, room.locator))
220
+ .map((room) => {
221
+ const governed = governs(reach, room);
239
222
  const attached = [
240
- modelOf(room.locator),
241
- ...(room.typedSelection === null ? [] : [modelOf(room.locator, 'typed-selection')]),
223
+ ...(governed.collection ? [modelOf(room.locator)] : []),
224
+ ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
242
225
  ];
243
226
  return {
244
227
  locator: room.locator,
@@ -316,23 +299,32 @@ export function composeRuntime(options) {
316
299
  for (const app of set.presentations)
317
300
  emitPresentation(app);
318
301
  conversations.changed(set);
319
- const cards = declaredCards();
320
- if (cards !== undefined && set.documents.some((change) => change.locator === cards.locator)) {
321
- scheduleCurrency();
322
- }
323
302
  }
324
303
  function stopTimer() {
325
- if (timer === undefined)
304
+ if (resetTimer === undefined)
326
305
  return;
327
- clearInterval(timer);
328
- timer = undefined;
306
+ clearTimeout(resetTimer);
307
+ resetTimer = undefined;
329
308
  }
330
- function startTimer() {
331
- if (timer !== undefined)
309
+ function armTimer() {
310
+ stopTimer();
311
+ if (closed || stopping)
312
+ return;
313
+ let earliest = null;
314
+ for (const { submission } of pendings.values()) {
315
+ if (submission.state !== 'reset')
316
+ continue;
317
+ const at = Date.parse(submission.reset);
318
+ if (earliest === null || at < earliest)
319
+ earliest = at;
320
+ }
321
+ if (earliest === null)
332
322
  return;
333
- timer = setInterval(() => {
334
- void enqueue();
335
- }, period);
323
+ // A Save House held back goes again only once the reset its answer named has passed.
324
+ resetTimer = setTimeout(() => {
325
+ resetTimer = undefined;
326
+ submitAll();
327
+ }, Math.max(0, earliest - Date.now()));
336
328
  }
337
329
  async function refuse(reason, provider = null) {
338
330
  stopTimer();
@@ -358,7 +350,6 @@ export function composeRuntime(options) {
358
350
  }
359
351
  for (const key of keys) {
360
352
  invalidModels.add(key);
361
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
362
353
  models.set(key, revoked);
363
354
  liveModels.delete(key);
364
355
  }
@@ -373,7 +364,7 @@ export function composeRuntime(options) {
373
364
  };
374
365
  const read = readConfirmed(record, before.confirmed);
375
366
  replicas.set(locator, read.kind === 'staged'
376
- ? replicaOf(read.confirmed, before.queue, rewriting(), before)
367
+ ? replicaOf(read.confirmed, before.queue, before)
377
368
  : { ...before, confirmed: { ...before.confirmed, record } });
378
369
  }
379
370
  rooms.set(locator, {
@@ -406,13 +397,11 @@ export function composeRuntime(options) {
406
397
  locator: room.locator,
407
398
  operations: room.operations,
408
399
  replica: room.replica,
409
- attachedModel: room.attachedModel,
410
400
  model: modelTypes(modelOf(room.locator)),
411
401
  typedSelection: selection === null
412
402
  ? null
413
403
  : {
414
404
  roots: selection.roots,
415
- attachedModel: selection.attachedModel,
416
405
  model: modelTypes(modelOf(room.locator, 'typed-selection')),
417
406
  },
418
407
  providerOwnedPaths: room.providerOwnedPaths,
@@ -429,9 +418,6 @@ export function composeRuntime(options) {
429
418
  discovered.clear();
430
419
  for (const room of listed)
431
420
  discovered.set(room.locator, room);
432
- for (const locator of wanted.keys())
433
- if (!listedLocators.has(locator))
434
- wanted.delete(locator);
435
421
  await dropUnlisted(listedLocators, local);
436
422
  for (const room of listed) {
437
423
  const held = replicas.get(room.locator)?.confirmed.record;
@@ -465,6 +451,7 @@ export function composeRuntime(options) {
465
451
  rooms.delete(locator);
466
452
  roomDrafts.delete(locator);
467
453
  unreadable.delete(locator);
454
+ stale.delete(locator);
468
455
  scopedRefusals.delete(locator);
469
456
  sourceRefusals.delete(locator);
470
457
  declarationRefusals.delete(locator);
@@ -473,7 +460,6 @@ export function composeRuntime(options) {
473
460
  models.delete(key);
474
461
  liveModels.delete(key);
475
462
  invalidModels.delete(key);
476
- modelEpoch.delete(key);
477
463
  }
478
464
  journal.record(`room unlisted ${locator}`);
479
465
  }
@@ -486,7 +472,6 @@ export function composeRuntime(options) {
486
472
  if (prior !== undefined &&
487
473
  prior.status !== 'unavailable' &&
488
474
  (persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
489
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
490
475
  invalidModels.add(key);
491
476
  }
492
477
  const model = persisted.status === 'unavailable' ||
@@ -541,7 +526,7 @@ export function composeRuntime(options) {
541
526
  record: retyped,
542
527
  collection: parseCollection(retyped.documents, compositionOf(retyped)),
543
528
  };
544
- replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], rewriting(), prior));
529
+ replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
545
530
  const existing = rooms.get(record.locator);
546
531
  if (existing === undefined)
547
532
  list(record, false);
@@ -552,13 +537,11 @@ export function composeRuntime(options) {
552
537
  attachments: record.attachments,
553
538
  operations: record.operations,
554
539
  replica: record.replica,
555
- attachedModel: record.attachedModel,
556
540
  model: modelTypes(model),
557
541
  typedSelection: record.typedSelection === null
558
542
  ? null
559
543
  : {
560
544
  roots: record.typedSelection.roots,
561
- attachedModel: record.typedSelection.attachedModel,
562
545
  model: modelTypes(selectionModel),
563
546
  },
564
547
  });
@@ -626,7 +609,7 @@ export function composeRuntime(options) {
626
609
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
627
610
  }
628
611
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
629
- const withdrawn = withdrawals(confirmed, queue, withdrawable, rewriting());
612
+ const withdrawn = withdrawals(confirmed, queue, withdrawable);
630
613
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
631
614
  const returning = [
632
615
  ...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
@@ -662,42 +645,73 @@ export function composeRuntime(options) {
662
645
  rememberRoomDraft(record);
663
646
  for (const batch of withdrawn)
664
647
  journal.record(`outcome ${locator} ${batch.id} withdrawn`);
665
- replicas.set(locator, replicaOf(confirmed, kept, rewriting(), before));
648
+ replicas.set(locator, replicaOf(confirmed, kept, before));
666
649
  return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
667
650
  room: settling.room,
668
651
  queue: kept.length !== (before?.queue.length ?? 0),
669
652
  drafts: [...written.keys()],
670
653
  });
671
654
  }
672
- function publish(local, room, confirmed) {
655
+ function adoptModels(locator, record) {
656
+ for (const [attached, model] of [
657
+ ['collection', record.model],
658
+ ['typed-selection', record.selectionModel],
659
+ ]) {
660
+ const key = modelKey(locator, attached);
661
+ models.set(key, model);
662
+ invalidModels.delete(key);
663
+ if (model.status === 'current')
664
+ liveModels.set(key, JSON.stringify(model.snapshot));
665
+ else if (model.status === 'unavailable')
666
+ liveModels.delete(key);
667
+ }
668
+ }
669
+ function remodel(locator, answers, held) {
670
+ const taken = (answer, previous, attached) => {
671
+ if (answer === undefined)
672
+ return previous;
673
+ const { state, refused } = attachedModel(answer, previous);
674
+ if (refused !== null) {
675
+ const where = refused.location === undefined ? '' : ` ${refused.location}`;
676
+ journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
677
+ }
678
+ models.set(modelKey(locator, attached), state);
679
+ return state;
680
+ };
681
+ return {
682
+ collection: taken(answers?.collection, held.collection, 'collection'),
683
+ selection: taken(answers?.selection, held.selection, 'typed-selection'),
684
+ };
685
+ }
686
+ function publish(local, room, confirmed, base) {
673
687
  return exclusive(async () => {
674
688
  if (!continuing())
675
- return;
689
+ return true;
676
690
  const prior = replicas.get(room.locator);
677
- const retained = readConfirmed({
678
- ...confirmed.record,
679
- model: modelOf(room.locator),
680
- selectionModel: modelOf(room.locator, 'typed-selection'),
681
- }, prior?.confirmed);
682
- if (retained.kind !== 'staged')
683
- throw new Error(`Cannot retype staged Room ${room.locator}`);
691
+ if (base !== undefined &&
692
+ (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
693
+ prior.confirmed.record.revisions !== base.record.revisions)) {
694
+ return false;
695
+ }
696
+ adoptModels(room.locator, confirmed.record);
684
697
  const queue = prior?.queue ?? [];
685
- const roomVersion = retained.confirmed.record.roomVersion;
698
+ const roomVersion = confirmed.record.roomVersion;
686
699
  const accepted = queue.filter((batch) => {
687
700
  const accepted = pendings.get(batch.id)?.accepted;
688
701
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
689
702
  });
690
- const set = await settle(local, room.locator, prior, retained.confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
691
- const target = wanted.get(room.locator);
692
- if (target !== undefined && options.adapter.reached(room.locator, roomVersion, target)) {
693
- wanted.delete(room.locator);
694
- }
695
- unreadable.delete(room.locator);
703
+ const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
704
+ for (const batch of accepted)
705
+ pendings.delete(batch.id);
706
+ stale.delete(room.locator);
707
+ if (unreadable.delete(room.locator))
708
+ scopedRefusals.delete(room.locator);
696
709
  sourceRefusals.delete(room.locator);
697
710
  declarationRefusals.delete(room.locator);
698
711
  list(room, true);
699
712
  publishChangeSet(set);
700
713
  moveTo({ ...status, ...attention() });
714
+ return true;
701
715
  });
702
716
  }
703
717
  async function saveRoomDrafts(locator, named, writtenBy) {
@@ -726,7 +740,6 @@ export function composeRuntime(options) {
726
740
  operations: room.operations,
727
741
  providerOwnedPaths: room.providerOwnedPaths,
728
742
  providerLimits: limits,
729
- rewriting: rewriting(),
730
743
  visible: replica.visible,
731
744
  changes: parts.flatMap(({ changes }) => changes),
732
745
  bases: parts.flatMap(({ bases }) => bases),
@@ -750,15 +763,16 @@ export function composeRuntime(options) {
750
763
  return { outcome: 'refused', rule: 'durable-write' };
751
764
  }
752
765
  pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
766
+ recorded = false;
753
767
  for (const path of consumed)
754
768
  forgetRoomDraft(locator, path);
755
- replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting(), replica));
769
+ replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
756
770
  publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
757
771
  queue: true,
758
772
  drafts: consumed,
759
773
  }));
760
774
  moveTo({ ...saving(status), ...attention() });
761
- void enqueue();
775
+ submitLater(locator);
762
776
  return { outcome: 'saved', identifier: built.batch.id };
763
777
  }
764
778
  async function mark(local, locator, identifier, submission, lost) {
@@ -823,10 +837,10 @@ export function composeRuntime(options) {
823
837
  return { kind: 'transport-uncertainty' };
824
838
  }
825
839
  }
826
- function declaredCards() {
840
+ function declaredAgents() {
827
841
  for (const capability of capabilities) {
828
- if (capability.conversationCards !== undefined)
829
- return capability.conversationCards;
842
+ if (capability.conversationAgents !== undefined)
843
+ return capability.conversationAgents;
830
844
  }
831
845
  return undefined;
832
846
  }
@@ -857,8 +871,6 @@ export function composeRuntime(options) {
857
871
  journal.record(`transfer action ${name}`);
858
872
  try {
859
873
  const result = await options.adapter.action(authorization, { name, input });
860
- if (result.kind === 'action' && result.at !== undefined)
861
- want(result.at);
862
874
  if (result.kind === 'scoped-refusal') {
863
875
  const room = replicas.get(result.locator)?.confirmed.record;
864
876
  if (room !== undefined)
@@ -890,32 +902,11 @@ export function composeRuntime(options) {
890
902
  options.adapter.reached(locator, written, roomVersion) &&
891
903
  !options.adapter.reached(locator, roomVersion, written));
892
904
  }
893
- function want({ locator, roomVersion }) {
894
- if (reachedBy(locator, roomVersion))
895
- return;
896
- const held = wanted.get(locator);
897
- if (held === undefined || options.adapter.reached(locator, roomVersion, held)) {
898
- wanted.set(locator, roomVersion);
899
- }
900
- }
901
- function reachOf(locator) {
902
- let target = wanted.get(locator);
903
- const versions = (replicas.get(locator)?.queue ?? []).flatMap((batch) => {
904
- const accepted = pendings.get(batch.id)?.accepted;
905
- return accepted === undefined ? [] : [accepted];
906
- });
907
- for (const version of versions) {
908
- if (target === undefined || options.adapter.reached(locator, version, target)) {
909
- target = version;
910
- }
911
- }
912
- return target === undefined || reachedBy(locator, target) ? undefined : target;
913
- }
914
- async function pullRefresh(authorized, confirmed, minPosition) {
905
+ async function pullRefresh(authorized, confirmed) {
915
906
  if (!continuing())
916
907
  return unconfirmed;
917
908
  try {
918
- const result = await refreshRoom(options.adapter, authorized, confirmed, continuing, minPosition);
909
+ const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
919
910
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
920
911
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
921
912
  return result;
@@ -932,7 +923,7 @@ export function composeRuntime(options) {
932
923
  const result = await bootstrapRoom(options.adapter, authorized, room, {
933
924
  collection: modelOf(room.locator),
934
925
  selection: modelOf(room.locator, 'typed-selection'),
935
- }, continuing);
926
+ }, (models, held) => remodel(room.locator, models, held), continuing);
936
927
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
937
928
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
938
929
  return result;
@@ -945,20 +936,28 @@ export function composeRuntime(options) {
945
936
  async function advance(room, local, authorized) {
946
937
  const held = replicas.get(room.locator);
947
938
  if (held !== undefined) {
948
- const refreshed = await pullRefresh(authorized, held.confirmed, reachOf(room.locator));
939
+ const refreshed = await pullRefresh(authorized, held.confirmed);
949
940
  if (!continuing())
950
941
  return unconfirmed;
951
942
  if (refreshed.kind === 'still-current') {
952
943
  if (sourceRefusals.has(room.locator))
953
944
  return unconfirmed;
945
+ stale.delete(room.locator);
954
946
  if (unreadable.delete(room.locator)) {
947
+ scopedRefusals.delete(room.locator);
955
948
  list(room, true);
956
949
  emitRooms();
957
950
  }
958
951
  return null;
959
952
  }
960
953
  if (refreshed.kind === 'advanced') {
961
- await publish(local, room, refreshed.confirmed);
954
+ if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
955
+ const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
956
+ const reached = now !== undefined &&
957
+ options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
958
+ return reached ? null : advance(room, local, authorized);
959
+ }
960
+ await retainModel(local, room, 'collection', modelOf(room.locator));
962
961
  return null;
963
962
  }
964
963
  if (refreshed.kind !== 'beyond-horizon')
@@ -973,7 +972,7 @@ export function composeRuntime(options) {
973
972
  return null;
974
973
  }
975
974
  async function withhold(withheld, room, local, progress) {
976
- progress.complete = false;
975
+ stale.add(room.locator);
977
976
  if (withheld.kind === 'scoped-refusal') {
978
977
  await scoped(withheld, room, local);
979
978
  }
@@ -1020,44 +1019,30 @@ export function composeRuntime(options) {
1020
1019
  (accepted.author === undefined ? '' : ` author ${accepted.author}`) +
1021
1020
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1022
1021
  }
1023
- async function recordAccepted(room, local, submitted, accepted) {
1024
- await exclusive(async () => {
1025
- if (!continuing())
1026
- return;
1027
- const current = replicas.get(room.locator);
1028
- if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1029
- return;
1030
- }
1031
- const state = pendings.get(submitted.id);
1032
- if (state === undefined)
1033
- return;
1034
- await local.accept(room.locator, submitted.id, accepted.roomVersion);
1035
- pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1036
- logAccepted(room.locator, submitted, accepted);
1037
- emitQueue(room.locator);
1038
- });
1039
- }
1040
- async function commitAccept(room, local, submitted, accepted, base, overlay) {
1022
+ async function adoptAccepted(room, local, submitted, accepted) {
1041
1023
  if (!continuing())
1042
1024
  return unconfirmed;
1043
1025
  const current = replicas.get(room.locator);
1044
1026
  if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1045
1027
  return null;
1046
1028
  }
1047
- const remaining = current.queue.filter((batch) => batch.id !== submitted.id);
1048
- const retainedBase = readConfirmed({
1049
- ...base.record,
1050
- model: modelOf(room.locator),
1051
- selectionModel: modelOf(room.locator, 'typed-selection'),
1052
- }, base);
1053
- const applied = overlay && retainedBase.kind === 'staged'
1054
- ? applyReturned(retainedBase.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion)
1055
- : retainedBase;
1056
- if (applied.kind !== 'staged')
1029
+ const held = current.confirmed.record.roomVersion;
1030
+ logAccepted(room.locator, submitted, accepted);
1031
+ const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1032
+ ? { kind: 'staged', confirmed: current.confirmed }
1033
+ : applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
1034
+ ? accepted.roomVersion
1035
+ : held, accepted.roomVersion);
1036
+ if (applied.kind !== 'staged') {
1037
+ const state = pendings.get(submitted.id);
1038
+ await local.accept(room.locator, submitted.id, accepted.roomVersion);
1039
+ if (state !== undefined)
1040
+ pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1041
+ emitQueue(room.locator);
1057
1042
  return applied;
1058
- const set = await settle(local, room.locator, current, applied.confirmed, remaining, {
1059
- room: true,
1060
- });
1043
+ }
1044
+ const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1045
+ pendings.delete(submitted.id);
1061
1046
  unreadable.delete(room.locator);
1062
1047
  sourceRefusals.delete(room.locator);
1063
1048
  declarationRefusals.delete(room.locator);
@@ -1066,17 +1051,8 @@ export function composeRuntime(options) {
1066
1051
  moveTo({ ...status, ...attention() });
1067
1052
  return null;
1068
1053
  }
1069
- async function accepting(room, local, submitted, accepted, pulled, fresh) {
1070
- const roomVersion = pulled.record.roomVersion;
1071
- if (options.adapter.reached(room.locator, roomVersion, accepted.roomVersion)) {
1072
- return committed(room, local, submitted, accepted, pulled, roomVersion === accepted.roomVersion);
1073
- }
1074
- if (fresh && continuing())
1075
- await publish(local, room, pulled);
1076
- return false;
1077
- }
1078
- async function committed(room, local, submitted, accepted, base, overlay) {
1079
- const failed = await exclusive(() => commitAccept(room, local, submitted, accepted, base, overlay));
1054
+ async function adopted(room, local, submitted, accepted) {
1055
+ const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
1080
1056
  if (failed === null)
1081
1057
  return true;
1082
1058
  if (failed.kind === 'refused' && continuing())
@@ -1089,10 +1065,8 @@ export function composeRuntime(options) {
1089
1065
  const head = replica?.queue[0];
1090
1066
  if (replica === undefined || head === undefined)
1091
1067
  return;
1092
- if (pendings.get(head.id)?.accepted !== undefined) {
1093
- progress.complete = false;
1068
+ if (pendings.get(head.id)?.accepted !== undefined)
1094
1069
  return;
1095
- }
1096
1070
  const state = pendings.get(head.id);
1097
1071
  const { submission, lost: earlier } = state ?? {
1098
1072
  submission: { state: 'waiting' },
@@ -1120,6 +1094,7 @@ export function composeRuntime(options) {
1120
1094
  return;
1121
1095
  }
1122
1096
  const opensBefore = openSignals;
1097
+ const answered = awaitingAnswer(room.locator);
1123
1098
  try {
1124
1099
  result = await options.adapter.mutate(authorized, {
1125
1100
  locator: room.locator,
@@ -1128,11 +1103,14 @@ export function composeRuntime(options) {
1128
1103
  }, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
1129
1104
  }
1130
1105
  catch {
1106
+ answered();
1131
1107
  progress.complete = false;
1132
1108
  if (continuing())
1133
1109
  progress.reachable = false;
1134
1110
  return;
1135
1111
  }
1112
+ if (result.kind !== 'accepted' || !continuing())
1113
+ answered();
1136
1114
  if (!continuing())
1137
1115
  return;
1138
1116
  if (result.kind === 'transport-uncertainty') {
@@ -1185,6 +1163,7 @@ export function composeRuntime(options) {
1185
1163
  const reset = result.reset;
1186
1164
  progress.complete = false;
1187
1165
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1166
+ armTimer();
1188
1167
  return;
1189
1168
  }
1190
1169
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
@@ -1211,95 +1190,33 @@ export function composeRuntime(options) {
1211
1190
  return withhold(withheld, room, local, progress);
1212
1191
  continue;
1213
1192
  }
1214
- await recordAccepted(room, local, head, result);
1215
- const from = replicas.get(room.locator)?.confirmed;
1216
- if (from === undefined)
1217
- return;
1218
- const pulled = await pullRefresh(authorized, from, reachOf(room.locator));
1219
- if (!continuing())
1220
- return;
1221
- if (pulled.kind === 'revoked-authorization') {
1222
- await refuse('revoked-authorization');
1223
- return;
1224
- }
1225
- if (pulled.kind === 'scoped-refusal') {
1226
- await scoped(pulled, room, local);
1227
- progress.complete = false;
1228
- return;
1229
- }
1230
- if (pulled.kind === 'still-current' || pulled.kind === 'advanced') {
1231
- const fresh = pulled.kind === 'advanced';
1232
- if (!(await accepting(room, local, head, result, fresh ? pulled.confirmed : from, fresh))) {
1233
- progress.complete = false;
1234
- return;
1235
- }
1236
- continue;
1237
- }
1238
- if (pulled.kind === 'beyond-horizon') {
1239
- const bootstrapped = await pullBootstrap(authorized, room);
1240
- if (!continuing())
1241
- return;
1242
- if (bootstrapped.kind === 'revoked-authorization') {
1243
- await refuse('revoked-authorization');
1244
- return;
1245
- }
1246
- if (bootstrapped.kind === 'scoped-refusal') {
1247
- await scoped(bootstrapped, room, local);
1248
- progress.complete = false;
1249
- return;
1250
- }
1251
- if (bootstrapped.kind === 'staged') {
1252
- if (!(await accepting(room, local, head, result, bootstrapped.confirmed, true))) {
1253
- progress.complete = false;
1254
- return;
1255
- }
1256
- continue;
1257
- }
1193
+ const kept = await adopted(room, local, head, result).finally(answered);
1194
+ if (!kept) {
1258
1195
  progress.complete = false;
1259
- if (bootstrapped.kind === 'transport-uncertainty') {
1260
- progress.reachable = false;
1261
- }
1262
- else if (bootstrapped.kind === 'room-not-readable') {
1263
- unreadable.add(room.locator);
1264
- list(room, false);
1265
- emitRooms();
1266
- }
1267
- else if (bootstrapped.kind === 'refused') {
1268
- refusedSource(room, bootstrapped);
1269
- }
1270
1196
  return;
1271
1197
  }
1272
- progress.complete = false;
1273
- if (pulled.kind === 'transport-uncertainty') {
1274
- progress.reachable = false;
1275
- }
1276
- else if (pulled.kind === 'room-not-readable') {
1277
- unreadable.add(room.locator);
1278
- list(room, false);
1279
- emitRooms();
1280
- }
1281
- else if (pulled.kind === 'retryable-refusal') {
1282
- journal.record(`refusal ${pulled.kind} ${pulled.code}`);
1283
- }
1284
- else if (pulled.kind === 'refused') {
1285
- refusedSource(room, pulled);
1286
- }
1287
- return;
1288
1198
  }
1289
- return;
1199
+ }
1200
+ function awaitingAnswer(locator) {
1201
+ let release = () => { };
1202
+ const awaited = new Promise((resolve) => {
1203
+ release = resolve;
1204
+ });
1205
+ answering.set(locator, awaited);
1206
+ return () => {
1207
+ if (answering.get(locator) === awaited)
1208
+ answering.delete(locator);
1209
+ release();
1210
+ };
1290
1211
  }
1291
1212
  async function retainModel(local, room, attached, next) {
1292
1213
  const key = modelKey(room.locator, attached);
1293
- if (next.status === 'unavailable')
1294
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
1295
- const epoch = modelEpoch.get(key) ?? 0;
1296
1214
  const previous = modelOf(room.locator, attached);
1297
1215
  const heldBefore = replicas.get(room.locator)?.confirmed.record;
1298
1216
  if (JSON.stringify(previous) === JSON.stringify(next) &&
1299
1217
  (heldBefore === undefined ||
1300
1218
  (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
1301
1219
  JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
1302
- JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
1303
1220
  JSON.stringify(heldBefore.typedSelection) ===
1304
1221
  JSON.stringify(room.typedSelection ?? null))))
1305
1222
  return;
@@ -1310,15 +1227,12 @@ export function composeRuntime(options) {
1310
1227
  await exclusive(async () => {
1311
1228
  if (!continuing())
1312
1229
  return;
1313
- if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
1314
- return;
1315
1230
  const held = replicas.get(room.locator);
1316
1231
  if (held !== undefined) {
1317
1232
  const declared = {
1318
1233
  ...held.confirmed.record,
1319
1234
  operations: room.operations,
1320
1235
  replica: room.replica,
1321
- attachedModel: room.attachedModel,
1322
1236
  typedSelection: room.typedSelection ?? null,
1323
1237
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1324
1238
  };
@@ -1338,7 +1252,7 @@ export function composeRuntime(options) {
1338
1252
  queue: held.queue,
1339
1253
  });
1340
1254
  if (read.kind === 'staged') {
1341
- replicas.set(room.locator, replicaOf(read.confirmed, held.queue, rewriting(), held));
1255
+ replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1342
1256
  if (declarationRefusals.delete(room.locator)) {
1343
1257
  sourceRefusals.delete(room.locator);
1344
1258
  unreadable.delete(room.locator);
@@ -1362,77 +1276,6 @@ export function composeRuntime(options) {
1362
1276
  publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
1363
1277
  });
1364
1278
  }
1365
- async function acquireModel(local, room, authorized, selection) {
1366
- const attached = selection === null ? 'collection' : 'typed-selection';
1367
- const key = modelKey(room.locator, attached);
1368
- const declared = selection === null ? room.attachedModel : selection.attachedModel;
1369
- let previous = modelOf(room.locator, attached);
1370
- if (previous.status !== 'unavailable' &&
1371
- (declared === null || !sameModel(previous.snapshot, declared))) {
1372
- previous = { status: 'unavailable', reason: 'version-changed' };
1373
- await retainModel(local, room, attached, previous);
1374
- if (discoveryPaused)
1375
- return;
1376
- }
1377
- const epoch = modelEpoch.get(key) ?? 0;
1378
- let answer;
1379
- try {
1380
- answer = await options.adapter.model(authorized, selection === null
1381
- ? { locator: room.locator, attached: 'collection', model: declared }
1382
- : {
1383
- locator: room.locator,
1384
- attached: 'typed-selection',
1385
- roots: selection.roots,
1386
- model: declared,
1387
- });
1388
- }
1389
- catch {
1390
- answer = { kind: 'transport-uncertainty' };
1391
- }
1392
- if (discoveryPaused || epoch !== (modelEpoch.get(key) ?? 0))
1393
- return;
1394
- if (answer.kind === 'revoked-authorization') {
1395
- await refuse('revoked-authorization');
1396
- return;
1397
- }
1398
- if (answer.kind === 'transport-uncertainty') {
1399
- await retainModel(local, room, attached, cached(previous));
1400
- return;
1401
- }
1402
- if (answer.kind === 'model-unavailable') {
1403
- await retainModel(local, room, attached, {
1404
- status: 'unavailable',
1405
- reason: answer.reason,
1406
- });
1407
- return;
1408
- }
1409
- if (declared === null || !sameModel(answer.snapshot, declared)) {
1410
- await retainModel(local, room, attached, {
1411
- status: 'unavailable',
1412
- reason: 'version-changed',
1413
- });
1414
- return;
1415
- }
1416
- if (previous.status !== 'unavailable' &&
1417
- sameModel(previous.snapshot, answer.snapshot) &&
1418
- previous.snapshot.schema !== answer.snapshot.schema) {
1419
- await retainModel(local, room, attached, { status: 'unavailable', reason: 'invalid' });
1420
- return;
1421
- }
1422
- const verified = answer.snapshot.schema === null ? EMPTY_SCHEMA : verify(answer.snapshot.schema);
1423
- if (verified.verdict === 'refuse') {
1424
- const where = verified.location === undefined ? '' : ` ${verified.location}`;
1425
- journal.record(`model invalid ${room.locator} ${attached} ${answer.snapshot.identity} ${verified.rule}${where}`);
1426
- }
1427
- await retainModel(local, room, attached, verified.verdict === 'accept'
1428
- ? {
1429
- status: 'current',
1430
- snapshot: structuredClone(answer.snapshot),
1431
- verifiedWith: MD_MODEL,
1432
- types: verified.model.types,
1433
- }
1434
- : { status: 'unavailable', reason: 'invalid' });
1435
- }
1436
1279
  async function scoped(result, room, local) {
1437
1280
  scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
1438
1281
  let scopedAway = room;
@@ -1460,14 +1303,34 @@ export function composeRuntime(options) {
1460
1303
  emitRooms();
1461
1304
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
1462
1305
  }
1463
- async function synchronize(discovered, local, progress) {
1464
- const prepared = await Promise.allSettled(discovered.map((room) => prepareRoom(room, local, progress)));
1465
- const ready = discovered.filter((_, index) => {
1306
+ function heldRooms(listed) {
1307
+ return listed.flatMap((room) => {
1308
+ const record = replicas.get(room.locator)?.confirmed.record;
1309
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1310
+ return [];
1311
+ return [
1312
+ {
1313
+ locator: room.locator,
1314
+ roomVersion: record.roomVersion,
1315
+ documents: record.documents,
1316
+ revisions: record.revisions,
1317
+ attachments: record.attachments,
1318
+ },
1319
+ ];
1320
+ });
1321
+ }
1322
+ async function synchronize(listed, local, progress, attempt) {
1323
+ const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1324
+ const ready = listed.filter((_, index) => {
1466
1325
  const outcome = prepared[index];
1467
1326
  return outcome?.status === 'fulfilled' && outcome.value;
1468
1327
  });
1469
- const rooms = await Promise.allSettled(ready.map((room) => synchronizeRoom(room, local, progress)));
1470
- const failed = [...prepared, ...rooms].find((room) => room.status === 'rejected');
1328
+ if (attempt === 'opened')
1329
+ options.adapter.roomStream?.hold(heldRooms(ready));
1330
+ const loaded = await Promise.allSettled(ready
1331
+ .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1332
+ .map((room) => loadRoom(room, local, progress)));
1333
+ const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1471
1334
  if (failed !== undefined)
1472
1335
  throw failed.reason;
1473
1336
  }
@@ -1484,11 +1347,8 @@ export function composeRuntime(options) {
1484
1347
  progress.complete = false;
1485
1348
  return false;
1486
1349
  }
1487
- const selection = room.typedSelection;
1488
- await Promise.all([
1489
- acquireModel(local, room, authorization, null),
1490
- ...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
1491
- ]);
1350
+ if (prior !== undefined)
1351
+ await retainModel(local, room, 'collection', modelOf(room.locator));
1492
1352
  if (discoveryPaused || authorization === undefined)
1493
1353
  return false;
1494
1354
  if (room.replica.admission !== 'admitted') {
@@ -1496,24 +1356,53 @@ export function composeRuntime(options) {
1496
1356
  progress.complete = false;
1497
1357
  return false;
1498
1358
  }
1499
- const held = replicas.get(room.locator);
1500
- if (held !== undefined && held.confirmed.record.replica.selection !== room.replica.selection) {
1501
- progress.complete = false;
1502
- return false;
1503
- }
1504
1359
  return true;
1505
1360
  }
1506
- async function synchronizeRoom(room, local, progress) {
1361
+ async function caughtUp(room, local, progress) {
1362
+ const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1363
+ if (held === undefined || authorization === undefined)
1364
+ return;
1365
+ const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1366
+ if (!behind)
1367
+ return;
1368
+ const withheld = await advance(room, local, authorization);
1369
+ if (withheld !== null)
1370
+ await withhold(withheld, room, local, progress);
1371
+ }
1372
+ function revoked(locator) {
1373
+ return ['collection', 'typed-selection'].some((attached) => {
1374
+ const model = modelOf(locator, attached);
1375
+ return model.status === 'unavailable' && model.reason === 'revoked';
1376
+ });
1377
+ }
1378
+ async function loadRoom(room, local, progress) {
1507
1379
  if (discoveryPaused || authorization === undefined)
1508
1380
  return;
1381
+ if (replicas.has(room.locator) && revoked(room.locator)) {
1382
+ await reload(room, local, progress);
1383
+ return;
1384
+ }
1509
1385
  const withheld = await advance(room, local, authorization);
1510
1386
  if (withheld !== null) {
1511
1387
  await withhold(withheld, room, local, progress);
1512
1388
  return;
1513
1389
  }
1514
- await submitRoom(room, local, authorization, progress);
1390
+ await caughtUp(room, local, progress);
1515
1391
  }
1516
- async function cycle(local, progress) {
1392
+ async function reload(room, local, progress) {
1393
+ if (discoveryPaused || authorization === undefined)
1394
+ return;
1395
+ const bootstrapped = await pullBootstrap(authorization, room);
1396
+ if (!continuing())
1397
+ return;
1398
+ if (bootstrapped.kind !== 'staged') {
1399
+ await withhold(bootstrapped, room, local, progress);
1400
+ return;
1401
+ }
1402
+ await publish(local, room, bootstrapped.confirmed);
1403
+ await caughtUp(room, local, progress);
1404
+ }
1405
+ async function discoverRooms(local, progress, attempt) {
1517
1406
  if (closed || authorization === undefined) {
1518
1407
  notifyReady();
1519
1408
  return;
@@ -1539,6 +1428,10 @@ export function composeRuntime(options) {
1539
1428
  }
1540
1429
  }
1541
1430
  progress.reachable = false;
1431
+ progress.complete = false;
1432
+ if (attempt === 'opened') {
1433
+ options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1434
+ }
1542
1435
  notifyReady();
1543
1436
  return;
1544
1437
  }
@@ -1553,17 +1446,6 @@ export function composeRuntime(options) {
1553
1446
  notifyReady();
1554
1447
  return;
1555
1448
  }
1556
- if (!result.supportedVersions.includes(ROOM_CONTRACT_VERSION)) {
1557
- await refuse('no-matching-version');
1558
- notifyReady();
1559
- return;
1560
- }
1561
- if (result.mdModel.identity !== MD_MODEL.identity ||
1562
- result.mdModel.version !== MD_MODEL.version) {
1563
- await refuse('md-model-version-mismatch');
1564
- notifyReady();
1565
- return;
1566
- }
1567
1449
  for (const room of result.rooms) {
1568
1450
  if (room.operations === undefined ||
1569
1451
  !['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
@@ -1571,8 +1453,7 @@ export function composeRuntime(options) {
1571
1453
  room.replica === undefined ||
1572
1454
  typeof room.replica.selection !== 'string' ||
1573
1455
  room.replica.selection.length === 0 ||
1574
- !['admitted', 'unavailable'].includes(room.replica.admission) ||
1575
- !declaredIdentity(room.attachedModel)) {
1456
+ !['admitted', 'unavailable'].includes(room.replica.admission)) {
1576
1457
  journal.record(`invalid model or replica declaration ${room.locator}`);
1577
1458
  await refuse('missing-required-guarantee');
1578
1459
  notifyReady();
@@ -1585,14 +1466,6 @@ export function composeRuntime(options) {
1585
1466
  return;
1586
1467
  }
1587
1468
  }
1588
- const declared = new Set(result.capabilities.map((capability) => capability.name));
1589
- for (const required of options.requiredGuarantees) {
1590
- if (!knownCapabilities.has(required) || !declared.has(required)) {
1591
- await refuse('missing-required-guarantee');
1592
- notifyReady();
1593
- return;
1594
- }
1595
- }
1596
1469
  if (refusal !== null) {
1597
1470
  refusal = null;
1598
1471
  emitRefusal();
@@ -1600,26 +1473,13 @@ export function composeRuntime(options) {
1600
1473
  const previousCapabilities = capabilities;
1601
1474
  capabilities = result.capabilities;
1602
1475
  notifyReady();
1603
- if (declared.has('room-version-notifications'))
1604
- observeRoomVersions();
1605
1476
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1606
1477
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
1607
1478
  limits = result.providerLimits;
1608
1479
  await local.declare(limits, capabilities);
1609
1480
  }
1610
1481
  await applyDiscovery(result.rooms, local);
1611
- handSelected(result.rooms
1612
- .filter((room) => room.replica.admission === 'admitted')
1613
- .map((room) => room.locator)
1614
- .sort());
1615
- progress.complete = true;
1616
- await synchronize(result.rooms, local, progress);
1617
- if (closed || discoveryPaused)
1618
- return;
1619
- await conversations.reconcile();
1620
- if (status.providerUnreachable)
1621
- conversations.reconnected();
1622
- await conversations.catchUp(progress);
1482
+ await synchronize(result.rooms, local, progress, attempt);
1623
1483
  }
1624
1484
  async function ensureAuthorization() {
1625
1485
  if (authorization !== undefined)
@@ -1633,64 +1493,246 @@ export function composeRuntime(options) {
1633
1493
  throw error;
1634
1494
  }
1635
1495
  }
1636
- async function connect() {
1637
- if (closed || stopping || discoveryPaused) {
1638
- notifyReady();
1496
+ function task(work) {
1497
+ if (stopping)
1498
+ return chain;
1499
+ chain = chain.then(work, work).catch(() => undefined);
1500
+ return chain;
1501
+ }
1502
+ function sync(attempt) {
1503
+ return task(async () => {
1504
+ if (closed || stopping || discoveryPaused) {
1505
+ notifyReady();
1506
+ return;
1507
+ }
1508
+ const progress = { complete: true, reachable: true };
1509
+ underway = attempt === 'opened' ? progress : null;
1510
+ complete = false;
1511
+ recorded = false;
1512
+ moveTo(transferring(status));
1513
+ try {
1514
+ const local = await openLocal();
1515
+ if (closed)
1516
+ return;
1517
+ await ensureAuthorization();
1518
+ if (closed)
1519
+ return;
1520
+ await discoverRooms(local, progress, attempt);
1521
+ }
1522
+ catch {
1523
+ progress.complete = false;
1524
+ if (attempt === 'opened') {
1525
+ options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1526
+ }
1527
+ }
1528
+ finally {
1529
+ notifyReady();
1530
+ }
1531
+ if (attempt === 'opened')
1532
+ return;
1533
+ await finishOpening(progress);
1534
+ });
1535
+ }
1536
+ function closedSocket(revoked) {
1537
+ const opened = socketOpen;
1538
+ socketOpen = false;
1539
+ if (revoked) {
1540
+ void refuse('revoked-authorization');
1639
1541
  return;
1640
1542
  }
1641
- startTimer();
1642
- const progress = { complete: false, reachable: true };
1643
- let completedAt = new Date().toISOString();
1644
- try {
1645
- const local = await openLocal();
1646
- if (closed)
1647
- return;
1648
- await ensureAuthorization();
1649
- if (closed)
1543
+ if (opened || status.providerUnreachable)
1544
+ return;
1545
+ moveTo({ ...status, providerUnreachable: true });
1546
+ }
1547
+ function applyOpening(expired) {
1548
+ return task(async () => {
1549
+ const progress = underway;
1550
+ underway = null;
1551
+ if (closed || stopping)
1650
1552
  return;
1651
- await cycle(local, progress);
1652
- if (closed)
1553
+ if (progress === null) {
1554
+ await reloadExpired(expired);
1653
1555
  return;
1654
- if (progress.complete) {
1655
- completedAt = new Date().toISOString();
1656
- await local.complete(completedAt);
1657
1556
  }
1557
+ try {
1558
+ const local = await openLocal();
1559
+ const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1560
+ const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1561
+ const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
1562
+ if (progress.reachable) {
1563
+ for (const room of held)
1564
+ if (!reloading.includes(room))
1565
+ await confirmModels(room, local);
1566
+ }
1567
+ const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1568
+ if (failed !== undefined)
1569
+ throw failed.reason;
1570
+ }
1571
+ catch {
1572
+ progress.complete = false;
1573
+ progress.reachable = false;
1574
+ }
1575
+ await finishOpening(progress);
1576
+ });
1577
+ }
1578
+ async function reloadExpired(expired) {
1579
+ const progress = { complete: true, reachable: true };
1580
+ try {
1581
+ const local = await openLocal();
1582
+ await Promise.allSettled([...discovered.values()]
1583
+ .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1584
+ .map((room) => reload(room, local, progress)));
1658
1585
  }
1659
1586
  catch {
1660
- progress.complete = false;
1661
- notifyReady();
1587
+ progress.reachable = false;
1588
+ }
1589
+ await rest(progress);
1590
+ }
1591
+ async function confirmModels(room, local) {
1592
+ for (const attached of ['collection', 'typed-selection']) {
1593
+ const model = modelOf(room.locator, attached);
1594
+ if (model.status === 'cached') {
1595
+ await retainModel(local, room, attached, { ...model, status: 'current' });
1596
+ }
1597
+ }
1598
+ }
1599
+ function submitLater(locator) {
1600
+ const running = submitting.get(locator);
1601
+ if (running !== undefined) {
1602
+ running.again = true;
1662
1603
  return;
1663
1604
  }
1664
- finally {
1665
- notifyReady();
1666
- moveTo(settled(status, progress, completedAt, pending(), attention()));
1605
+ const run = { again: false, done: Promise.resolve() };
1606
+ submitting.set(locator, run);
1607
+ run.done = (async () => {
1608
+ try {
1609
+ do {
1610
+ run.again = false;
1611
+ const room = discovered.get(locator);
1612
+ if (closed || stopping || discoveryPaused || room === undefined)
1613
+ return;
1614
+ const progress = { complete: true, reachable: true };
1615
+ try {
1616
+ await ensureAuthorization();
1617
+ if (authorization === undefined)
1618
+ return;
1619
+ await submitRoom(room, await openLocal(), authorization, progress);
1620
+ }
1621
+ catch {
1622
+ progress.reachable = false;
1623
+ }
1624
+ armTimer();
1625
+ await rest(progress);
1626
+ } while (run.again);
1627
+ }
1628
+ finally {
1629
+ submitting.delete(locator);
1630
+ }
1631
+ })();
1632
+ }
1633
+ function submitAll() {
1634
+ for (const [locator, replica] of replicas) {
1635
+ if (replica.queue.length > 0)
1636
+ submitLater(locator);
1667
1637
  }
1668
1638
  }
1669
- async function run() {
1639
+ async function finishOpening(progress) {
1670
1640
  try {
1671
- await connect();
1641
+ if (closed || discoveryPaused) {
1642
+ progress.complete = false;
1643
+ return;
1644
+ }
1645
+ submitAll();
1646
+ await conversations.reconcile();
1647
+ if (status.providerUnreachable)
1648
+ conversations.reconnected();
1649
+ const reading = { complete: true, reachable: true };
1650
+ await conversations.catchUp(reading);
1651
+ if (!reading.reachable)
1652
+ progress.reachable = false;
1653
+ }
1654
+ catch {
1655
+ progress.complete = false;
1672
1656
  }
1673
1657
  finally {
1674
- inflight = false;
1675
- if (queued && !closed) {
1676
- queued = false;
1677
- void enqueue();
1678
- }
1658
+ complete = progress.complete;
1659
+ await rest(progress);
1679
1660
  }
1680
1661
  }
1681
- function observeRoomVersions() {
1682
- const path = options.adapter.roomVersions;
1683
- if (path === undefined || withdrawNotifications !== undefined)
1662
+ async function rest(progress) {
1663
+ if (closed)
1684
1664
  return;
1685
- withdrawNotifications = path.observe((notification) => {
1686
- if (closed || stopping || discoveryPaused)
1665
+ let lastCompleted = status.lastCompleted;
1666
+ let synced = complete && stale.size === 0 && !pending();
1667
+ if (synced && !recorded) {
1668
+ const completedAt = new Date().toISOString();
1669
+ try {
1670
+ await (await openLocal()).complete(completedAt);
1671
+ recorded = true;
1672
+ lastCompleted = completedAt;
1673
+ }
1674
+ catch {
1675
+ synced = false;
1676
+ }
1677
+ }
1678
+ moveTo({
1679
+ state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
1680
+ lastCompleted,
1681
+ providerUnreachable: !progress.reachable,
1682
+ ...attention(),
1683
+ });
1684
+ }
1685
+ function applyRoom(locator, roomVersion) {
1686
+ announced.set(locator, roomVersion);
1687
+ return task(async () => {
1688
+ await answering.get(locator);
1689
+ if (closed || stopping || discoveryPaused || authorization === undefined)
1690
+ return;
1691
+ const room = discovered.get(locator);
1692
+ const held = replicas.get(locator);
1693
+ if (room === undefined || room.replica.admission !== 'admitted')
1687
1694
  return;
1688
- if (notification.modelChanged === true && handed?.includes(notification.locator) === true) {
1689
- void enqueue();
1695
+ if (held !== undefined &&
1696
+ (held.confirmed.record.replica.selection !== room.replica.selection ||
1697
+ options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
1698
+ return;
1699
+ }
1700
+ const during = underway;
1701
+ const progress = during ?? { complete: true, reachable: true };
1702
+ if (during === null)
1703
+ moveTo(transferring(status));
1704
+ try {
1705
+ const local = await openLocal();
1706
+ const withheld = await advance(room, local, authorization);
1707
+ if (withheld !== null)
1708
+ await withhold(withheld, room, local, progress);
1709
+ }
1710
+ catch {
1711
+ stale.add(locator);
1712
+ }
1713
+ finally {
1714
+ if (during === null)
1715
+ await rest(progress);
1716
+ }
1717
+ });
1718
+ }
1719
+ function observeStream() {
1720
+ const stream = options.adapter.roomStream;
1721
+ if (stream === undefined || withdrawStream !== undefined)
1722
+ return;
1723
+ withdrawStream = stream.observe((event) => {
1724
+ if (closed || stopping)
1690
1725
  return;
1726
+ if (event.kind === 'opened') {
1727
+ socketOpen = true;
1728
+ void sync('opened');
1691
1729
  }
1692
- notices.set(notification.locator, notification.roomVersion);
1693
- chain = chain.then(runNotified, runNotified);
1730
+ else if (event.kind === 'closed')
1731
+ closedSocket(event.revoked);
1732
+ else if (event.kind === 'opening')
1733
+ void applyOpening(event.expired);
1734
+ else
1735
+ void applyRoom(event.locator, event.roomVersion);
1694
1736
  });
1695
1737
  }
1696
1738
  function writesHeld() {
@@ -1711,14 +1753,17 @@ export function composeRuntime(options) {
1711
1753
  listener(mode);
1712
1754
  }
1713
1755
  if (releasing)
1714
- void enqueue();
1756
+ submitAll();
1715
1757
  }
1716
1758
  function observeConversations() {
1717
1759
  const stream = options.adapter.conversations;
1718
1760
  if (stream === undefined || withdrawConversations !== undefined)
1719
1761
  return;
1720
1762
  withdrawConversations = stream.observe((event) => {
1721
- conversations.receive(event);
1763
+ if (conversations.receive(event)) {
1764
+ void ready.then(scheduleCurrency);
1765
+ return;
1766
+ }
1722
1767
  if (event.kind !== 'gap' || conversations.current())
1723
1768
  return;
1724
1769
  show();
@@ -1729,7 +1774,7 @@ export function composeRuntime(options) {
1729
1774
  if (currencyQueued || closed || stopping)
1730
1775
  return;
1731
1776
  currencyQueued = true;
1732
- chain = chain.then(runCurrency, runCurrency);
1777
+ void task(runCurrency);
1733
1778
  }
1734
1779
  async function runCurrency() {
1735
1780
  currencyQueued = false;
@@ -1755,95 +1800,12 @@ export function composeRuntime(options) {
1755
1800
  }
1756
1801
  }
1757
1802
  }
1758
- function handSelected(locators) {
1759
- const path = options.adapter.roomVersions;
1760
- if (path === undefined || withdrawNotifications === undefined)
1761
- return;
1762
- if (handed !== undefined && JSON.stringify(locators) === JSON.stringify(handed))
1763
- return;
1764
- handed = locators;
1765
- path.select(locators);
1766
- }
1767
- async function runNotified() {
1768
- const announced = [...notices];
1769
- notices.clear();
1770
- if (announced.length === 0 || closed || stopping || discoveryPaused)
1771
- return;
1772
- const resting = status;
1773
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1774
- moveTo(transferring(resting));
1775
- try {
1776
- const local = await openLocal();
1777
- for (const [locator, roomVersion] of announced) {
1778
- if (closed || discoveryPaused || authorization === undefined)
1779
- return;
1780
- if (handed?.includes(locator) !== true)
1781
- continue;
1782
- const room = discovered.get(locator);
1783
- const held = replicas.get(locator);
1784
- if (room === undefined)
1785
- continue;
1786
- if (sourceRefusals.has(locator)) {
1787
- void enqueue();
1788
- continue;
1789
- }
1790
- if (held === undefined)
1791
- continue;
1792
- if (held.confirmed.record.roomVersion === roomVersion)
1793
- continue;
1794
- const withheld = await advance(room, local, authorization);
1795
- if (withheld === null)
1796
- continue;
1797
- if (withheld.kind === 'refused') {
1798
- void enqueue();
1799
- continue;
1800
- }
1801
- if (withheld.kind === 'scoped-refusal') {
1802
- await scoped(withheld, room, local);
1803
- continue;
1804
- }
1805
- if (withheld.kind === 'transport-uncertainty') {
1806
- progress.reachable = false;
1807
- continue;
1808
- }
1809
- if (withheld.kind === 'room-not-readable') {
1810
- unreadable.add(locator);
1811
- list(room, false);
1812
- emitRooms();
1813
- continue;
1814
- }
1815
- if (withheld.kind === 'revoked-authorization') {
1816
- await refuse('revoked-authorization');
1817
- return;
1818
- }
1819
- }
1820
- }
1821
- catch {
1822
- progress.reachable = false;
1823
- }
1824
- finally {
1825
- if (!closed) {
1826
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1827
- }
1828
- }
1829
- }
1830
- function enqueue() {
1831
- if (stopping)
1832
- return chain;
1833
- if (inflight) {
1834
- queued = true;
1835
- return chain;
1836
- }
1837
- inflight = true;
1838
- chain = chain.then(run, run);
1839
- return chain;
1840
- }
1841
1803
  function close() {
1842
1804
  if (closed)
1843
1805
  return;
1844
1806
  closed = true;
1845
- withdrawNotifications?.();
1846
- withdrawNotifications = undefined;
1807
+ withdrawStream?.();
1808
+ withdrawStream = undefined;
1847
1809
  withdrawConversations?.();
1848
1810
  withdrawConversations = undefined;
1849
1811
  withdrawServiceMode?.();
@@ -1867,6 +1829,7 @@ export function composeRuntime(options) {
1867
1829
  stopping = true;
1868
1830
  stopTimer();
1869
1831
  await chain.catch(() => undefined);
1832
+ await Promise.all([...submitting.values()].map((run) => run.done));
1870
1833
  await mutations;
1871
1834
  close();
1872
1835
  }
@@ -1875,11 +1838,29 @@ export function composeRuntime(options) {
1875
1838
  options.signal?.addEventListener('abort', close, { once: true });
1876
1839
  if (options.signal?.aborted)
1877
1840
  close();
1878
- void openLocal().catch(() => {
1841
+ observeStream();
1842
+ void openLocal().then(() => {
1843
+ if (capabilities.length > 0)
1844
+ notifyReady();
1845
+ }, () => {
1879
1846
  notifyReady();
1880
1847
  });
1881
- void enqueue();
1882
- function watch(topic, input, deliver) {
1848
+ if (options.adapter.roomStream === undefined)
1849
+ void sync('unstreamed');
1850
+ function planned(input, reach) {
1851
+ return planOf(input, roomFacts(reach), planReach(reach));
1852
+ }
1853
+ function watchPlan(plan, deliver) {
1854
+ return readSide.watch(plan, identityOf(plan), deliver);
1855
+ }
1856
+ async function searched(input, reach) {
1857
+ const plan = searchPlanOf(input);
1858
+ const selected = plan.documents === undefined && reach === null
1859
+ ? null
1860
+ : (await readSide.query(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
1861
+ return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
1862
+ }
1863
+ const watch = (topic, input, deliver) => {
1883
1864
  if (topic === 'room-drafts') {
1884
1865
  const listener = { notify: deliver, ready: false };
1885
1866
  roomDraftListeners.add(listener);
@@ -1900,6 +1881,8 @@ export function composeRuntime(options) {
1900
1881
  if (topic === 'conversation') {
1901
1882
  return conversations.watch(input[0], input[1], deliver);
1902
1883
  }
1884
+ if (topic === 'conversations')
1885
+ return conversations.watchList(deliver);
1903
1886
  if (topic === 'presentation') {
1904
1887
  const listener = {
1905
1888
  app: input[0],
@@ -1962,9 +1945,8 @@ export function composeRuntime(options) {
1962
1945
  queueSubscriptions.delete(queue);
1963
1946
  };
1964
1947
  }
1965
- const plan = planOf(input[0], roomFacts());
1966
- return readSide.watch(plan, identityOf(plan), deliver);
1967
- }
1948
+ return watchPlan(planned(input[0], null), deliver);
1949
+ };
1968
1950
  const md = {
1969
1951
  loaded() {
1970
1952
  return openLocal().then(() => undefined);
@@ -1972,15 +1954,8 @@ export function composeRuntime(options) {
1972
1954
  rooms() {
1973
1955
  return listed();
1974
1956
  },
1975
- query: ((input) => readSide.query(planOf(input, roomFacts()))),
1976
- async search(input) {
1977
- const plan = searchPlanOf(input);
1978
- const selected = plan.documents === undefined
1979
- ? null
1980
- : (await readSide.query(filterOf(plan.documents, roomFacts())))
1981
- .documents;
1982
- return searchIndex.search(plan, selected);
1983
- },
1957
+ query: ((input) => readSide.query(planned(input, null))),
1958
+ search: (input) => searched(input, null),
1984
1959
  types(locator) {
1985
1960
  return {
1986
1961
  collection: modelTypes(modelOf(locator)),
@@ -2087,9 +2062,7 @@ export function composeRuntime(options) {
2087
2062
  persistence() {
2088
2063
  return options.shell.persistentStorage;
2089
2064
  },
2090
- observe: ((topic, ...input) => opened((deliver) => watch(topic, input, (message) => {
2091
- deliver(structuredClone(message));
2092
- }), topic === 'query')),
2065
+ observe: observing(watch),
2093
2066
  async reauthorize() {
2094
2067
  if (refusal === null || !refusal.reauthorizationOffered)
2095
2068
  return;
@@ -2098,8 +2071,19 @@ export function composeRuntime(options) {
2098
2071
  await mutations;
2099
2072
  authorization = undefined;
2100
2073
  discoveryPaused = false;
2101
- await enqueue();
2074
+ if (options.adapter.roomStream === undefined)
2075
+ await sync('unstreamed');
2076
+ else
2077
+ await task(ensureAuthorization);
2102
2078
  },
2103
2079
  };
2104
- return { md, stop, watch };
2080
+ const reachable = {
2081
+ md,
2082
+ watch,
2083
+ plan: planned,
2084
+ query: (plan) => readSide.query(plan),
2085
+ watchPlan,
2086
+ search: searched,
2087
+ };
2088
+ return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2105
2089
  }