@agentshouse/mdruntime 0.1.0-c8c4b020a3ec7 → 0.1.0-ca1eff92bbe73

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 (70) hide show
  1. package/AGENTS.md +62 -26
  2. package/README.md +10 -10
  3. package/dist/adapter.d.ts +81 -43
  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 +47 -0
  9. package/dist/bootstrap.d.ts +5 -3
  10. package/dist/bootstrap.d.ts.map +1 -1
  11. package/dist/bootstrap.js +3 -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 +10 -12
  16. package/dist/connection.d.ts.map +1 -1
  17. package/dist/connection.js +518 -516
  18. package/dist/conversation.d.ts +22 -11
  19. package/dist/conversation.d.ts.map +1 -1
  20. package/dist/conversation.js +250 -105
  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 +84 -26
  29. package/dist/model.d.ts +11 -11
  30. package/dist/model.d.ts.map +1 -1
  31. package/dist/model.js +6 -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/paths.d.ts +0 -1
  36. package/dist/paths.d.ts.map +1 -1
  37. package/dist/paths.js +0 -16
  38. package/dist/query.d.ts +8 -3
  39. package/dist/query.d.ts.map +1 -1
  40. package/dist/query.js +104 -36
  41. package/dist/reach.d.ts +40 -0
  42. package/dist/reach.d.ts.map +1 -0
  43. package/dist/reach.js +270 -0
  44. package/dist/react.d.ts +32 -0
  45. package/dist/react.d.ts.map +1 -0
  46. package/dist/react.js +62 -0
  47. package/dist/read-side.d.ts +4 -0
  48. package/dist/read-side.d.ts.map +1 -1
  49. package/dist/read-side.js +116 -54
  50. package/dist/refresh.d.ts +2 -1
  51. package/dist/refresh.d.ts.map +1 -1
  52. package/dist/refresh.js +7 -3
  53. package/dist/replica.d.ts +5 -5
  54. package/dist/replica.d.ts.map +1 -1
  55. package/dist/replica.js +14 -15
  56. package/dist/room-draft.d.ts +3 -0
  57. package/dist/room-draft.d.ts.map +1 -1
  58. package/dist/room-draft.js +6 -4
  59. package/dist/rows.d.ts +2 -1
  60. package/dist/rows.d.ts.map +1 -1
  61. package/dist/rows.js +75 -38
  62. package/dist/save.d.ts +1 -2
  63. package/dist/save.d.ts.map +1 -1
  64. package/dist/save.js +90 -48
  65. package/dist/store.d.ts +1 -1
  66. package/dist/store.d.ts.map +1 -1
  67. package/dist/wire.d.ts +51 -16
  68. package/dist/wire.d.ts.map +1 -1
  69. package/dist/wire.js +50 -59
  70. 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,20 @@ 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
+ let handed = false;
86
+ const submitting = new Map();
87
+ const answering = new Map();
88
+ let complete = false;
89
+ let recorded = false;
90
+ const stale = new Set();
109
91
  let chain = Promise.resolve();
110
92
  let mutations = Promise.resolve();
111
- let inflight = false;
112
- let queued = false;
113
93
  const roomListeners = new Set();
114
94
  const refusalListeners = new Set();
115
95
  const storageLossListeners = new Set();
@@ -120,12 +100,13 @@ export function composeRuntime(options) {
120
100
  const readSide = createReadSide();
121
101
  let withdrawConversations;
122
102
  let currencyQueued = false;
103
+ let socketOpen = false;
123
104
  let serviceMode = 'open';
124
105
  let haltedRefusal = false;
125
106
  let openSignals = 0;
126
107
  const serviceModeListeners = new Set();
127
108
  const conversations = createConversationStore({
128
- cards: declaredCards,
109
+ agents: declaredAgents,
129
110
  documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
130
111
  call: (name, input) => invokeAction(name, input),
131
112
  durably,
@@ -234,11 +215,14 @@ export function composeRuntime(options) {
234
215
  function listed() {
235
216
  return structuredClone([...rooms.values()]);
236
217
  }
237
- function roomFacts() {
238
- const held = [...rooms.values()].map((room) => {
218
+ function roomFacts(reach = null) {
219
+ const held = [...rooms.values()]
220
+ .filter((room) => holds(reach, room.locator))
221
+ .map((room) => {
222
+ const governed = governs(reach, room);
239
223
  const attached = [
240
- modelOf(room.locator),
241
- ...(room.typedSelection === null ? [] : [modelOf(room.locator, 'typed-selection')]),
224
+ ...(governed.collection ? [modelOf(room.locator)] : []),
225
+ ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
242
226
  ];
243
227
  return {
244
228
  locator: room.locator,
@@ -316,23 +300,32 @@ export function composeRuntime(options) {
316
300
  for (const app of set.presentations)
317
301
  emitPresentation(app);
318
302
  conversations.changed(set);
319
- const cards = declaredCards();
320
- if (cards !== undefined && set.documents.some((change) => change.locator === cards.locator)) {
321
- scheduleCurrency();
322
- }
323
303
  }
324
304
  function stopTimer() {
325
- if (timer === undefined)
305
+ if (resetTimer === undefined)
326
306
  return;
327
- clearInterval(timer);
328
- timer = undefined;
307
+ clearTimeout(resetTimer);
308
+ resetTimer = undefined;
329
309
  }
330
- function startTimer() {
331
- if (timer !== undefined)
310
+ function armTimer() {
311
+ stopTimer();
312
+ if (closed || stopping)
313
+ return;
314
+ let earliest = null;
315
+ for (const { submission } of pendings.values()) {
316
+ if (submission.state !== 'reset')
317
+ continue;
318
+ const at = Date.parse(submission.reset);
319
+ if (earliest === null || at < earliest)
320
+ earliest = at;
321
+ }
322
+ if (earliest === null)
332
323
  return;
333
- timer = setInterval(() => {
334
- void enqueue();
335
- }, period);
324
+ // A Save House held back goes again only once the reset its answer named has passed.
325
+ resetTimer = setTimeout(() => {
326
+ resetTimer = undefined;
327
+ submitAll();
328
+ }, Math.max(0, earliest - Date.now()));
336
329
  }
337
330
  async function refuse(reason, provider = null) {
338
331
  stopTimer();
@@ -358,7 +351,6 @@ export function composeRuntime(options) {
358
351
  }
359
352
  for (const key of keys) {
360
353
  invalidModels.add(key);
361
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
362
354
  models.set(key, revoked);
363
355
  liveModels.delete(key);
364
356
  }
@@ -373,7 +365,7 @@ export function composeRuntime(options) {
373
365
  };
374
366
  const read = readConfirmed(record, before.confirmed);
375
367
  replicas.set(locator, read.kind === 'staged'
376
- ? replicaOf(read.confirmed, before.queue, rewriting(), before)
368
+ ? replicaOf(read.confirmed, before.queue, before)
377
369
  : { ...before, confirmed: { ...before.confirmed, record } });
378
370
  }
379
371
  rooms.set(locator, {
@@ -404,15 +396,14 @@ export function composeRuntime(options) {
404
396
  rooms.set(room.locator, {
405
397
  name: room.name,
406
398
  locator: room.locator,
399
+ handle: room.handle,
407
400
  operations: room.operations,
408
401
  replica: room.replica,
409
- attachedModel: room.attachedModel,
410
402
  model: modelTypes(modelOf(room.locator)),
411
403
  typedSelection: selection === null
412
404
  ? null
413
405
  : {
414
406
  roots: selection.roots,
415
- attachedModel: selection.attachedModel,
416
407
  model: modelTypes(modelOf(room.locator, 'typed-selection')),
417
408
  },
418
409
  providerOwnedPaths: room.providerOwnedPaths,
@@ -429,9 +420,6 @@ export function composeRuntime(options) {
429
420
  discovered.clear();
430
421
  for (const room of listed)
431
422
  discovered.set(room.locator, room);
432
- for (const locator of wanted.keys())
433
- if (!listedLocators.has(locator))
434
- wanted.delete(locator);
435
423
  await dropUnlisted(listedLocators, local);
436
424
  for (const room of listed) {
437
425
  const held = replicas.get(room.locator)?.confirmed.record;
@@ -465,6 +453,7 @@ export function composeRuntime(options) {
465
453
  rooms.delete(locator);
466
454
  roomDrafts.delete(locator);
467
455
  unreadable.delete(locator);
456
+ stale.delete(locator);
468
457
  scopedRefusals.delete(locator);
469
458
  sourceRefusals.delete(locator);
470
459
  declarationRefusals.delete(locator);
@@ -473,7 +462,6 @@ export function composeRuntime(options) {
473
462
  models.delete(key);
474
463
  liveModels.delete(key);
475
464
  invalidModels.delete(key);
476
- modelEpoch.delete(key);
477
465
  }
478
466
  journal.record(`room unlisted ${locator}`);
479
467
  }
@@ -486,7 +474,6 @@ export function composeRuntime(options) {
486
474
  if (prior !== undefined &&
487
475
  prior.status !== 'unavailable' &&
488
476
  (persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
489
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
490
477
  invalidModels.add(key);
491
478
  }
492
479
  const model = persisted.status === 'unavailable' ||
@@ -541,7 +528,7 @@ export function composeRuntime(options) {
541
528
  record: retyped,
542
529
  collection: parseCollection(retyped.documents, compositionOf(retyped)),
543
530
  };
544
- replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], rewriting(), prior));
531
+ replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
545
532
  const existing = rooms.get(record.locator);
546
533
  if (existing === undefined)
547
534
  list(record, false);
@@ -552,13 +539,11 @@ export function composeRuntime(options) {
552
539
  attachments: record.attachments,
553
540
  operations: record.operations,
554
541
  replica: record.replica,
555
- attachedModel: record.attachedModel,
556
542
  model: modelTypes(model),
557
543
  typedSelection: record.typedSelection === null
558
544
  ? null
559
545
  : {
560
546
  roots: record.typedSelection.roots,
561
- attachedModel: record.typedSelection.attachedModel,
562
547
  model: modelTypes(selectionModel),
563
548
  },
564
549
  });
@@ -626,10 +611,14 @@ export function composeRuntime(options) {
626
611
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
627
612
  }
628
613
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
629
- const withdrawn = withdrawals(confirmed, queue, withdrawable, rewriting());
614
+ const withdrawn = withdrawals(confirmed, queue, withdrawable);
630
615
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
616
+ const unqueued = withdrawn.map((batch) => ({
617
+ batch,
618
+ returned: landed(batch, confirmed) ? null : { kind: 'withdrawn' },
619
+ }));
631
620
  const returning = [
632
- ...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
621
+ ...[...unqueued].reverse(),
633
622
  ...(settling.leaving === undefined ? [] : [settling.leaving]),
634
623
  ];
635
624
  const written = new Map();
@@ -660,44 +649,76 @@ export function composeRuntime(options) {
660
649
  }
661
650
  for (const record of written.values())
662
651
  rememberRoomDraft(record);
663
- for (const batch of withdrawn)
664
- journal.record(`outcome ${locator} ${batch.id} withdrawn`);
665
- replicas.set(locator, replicaOf(confirmed, kept, rewriting(), before));
652
+ for (const { batch, returned } of unqueued) {
653
+ journal.record(`outcome ${locator} ${batch.id} ${returned === null ? 'landed' : 'withdrawn'}`);
654
+ }
655
+ replicas.set(locator, replicaOf(confirmed, kept, before));
666
656
  return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
667
657
  room: settling.room,
668
658
  queue: kept.length !== (before?.queue.length ?? 0),
669
659
  drafts: [...written.keys()],
670
660
  });
671
661
  }
672
- function publish(local, room, confirmed) {
662
+ function adoptModels(locator, record) {
663
+ for (const [attached, model] of [
664
+ ['collection', record.model],
665
+ ['typed-selection', record.selectionModel],
666
+ ]) {
667
+ const key = modelKey(locator, attached);
668
+ models.set(key, model);
669
+ invalidModels.delete(key);
670
+ if (model.status === 'current')
671
+ liveModels.set(key, JSON.stringify(model.snapshot));
672
+ else if (model.status === 'unavailable')
673
+ liveModels.delete(key);
674
+ }
675
+ }
676
+ function remodel(locator, answers, held) {
677
+ const taken = (answer, previous, attached) => {
678
+ if (answer === undefined)
679
+ return previous;
680
+ const { state, refused } = attachedModel(answer, previous);
681
+ if (refused !== null) {
682
+ const where = refused.location === undefined ? '' : ` ${refused.location}`;
683
+ journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
684
+ }
685
+ models.set(modelKey(locator, attached), state);
686
+ return state;
687
+ };
688
+ return {
689
+ collection: taken(answers?.collection, held.collection, 'collection'),
690
+ selection: taken(answers?.selection, held.selection, 'typed-selection'),
691
+ };
692
+ }
693
+ function publish(local, room, confirmed, base) {
673
694
  return exclusive(async () => {
674
695
  if (!continuing())
675
- return;
696
+ return true;
676
697
  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}`);
698
+ if (base !== undefined &&
699
+ (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
700
+ prior.confirmed.record.revisions !== base.record.revisions)) {
701
+ return false;
702
+ }
703
+ adoptModels(room.locator, confirmed.record);
684
704
  const queue = prior?.queue ?? [];
685
- const roomVersion = retained.confirmed.record.roomVersion;
705
+ const roomVersion = confirmed.record.roomVersion;
686
706
  const accepted = queue.filter((batch) => {
687
707
  const accepted = pendings.get(batch.id)?.accepted;
688
708
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
689
709
  });
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);
710
+ const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
711
+ for (const batch of accepted)
712
+ pendings.delete(batch.id);
713
+ stale.delete(room.locator);
714
+ if (unreadable.delete(room.locator))
715
+ scopedRefusals.delete(room.locator);
696
716
  sourceRefusals.delete(room.locator);
697
717
  declarationRefusals.delete(room.locator);
698
718
  list(room, true);
699
719
  publishChangeSet(set);
700
720
  moveTo({ ...status, ...attention() });
721
+ return true;
701
722
  });
702
723
  }
703
724
  async function saveRoomDrafts(locator, named, writtenBy) {
@@ -726,7 +747,7 @@ export function composeRuntime(options) {
726
747
  operations: room.operations,
727
748
  providerOwnedPaths: room.providerOwnedPaths,
728
749
  providerLimits: limits,
729
- rewriting: rewriting(),
750
+ handle: room.handle,
730
751
  visible: replica.visible,
731
752
  changes: parts.flatMap(({ changes }) => changes),
732
753
  bases: parts.flatMap(({ bases }) => bases),
@@ -750,15 +771,16 @@ export function composeRuntime(options) {
750
771
  return { outcome: 'refused', rule: 'durable-write' };
751
772
  }
752
773
  pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
774
+ recorded = false;
753
775
  for (const path of consumed)
754
776
  forgetRoomDraft(locator, path);
755
- replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting(), replica));
777
+ replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
756
778
  publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
757
779
  queue: true,
758
780
  drafts: consumed,
759
781
  }));
760
782
  moveTo({ ...saving(status), ...attention() });
761
- void enqueue();
783
+ submitLater(locator);
762
784
  return { outcome: 'saved', identifier: built.batch.id };
763
785
  }
764
786
  async function mark(local, locator, identifier, submission, lost) {
@@ -823,10 +845,10 @@ export function composeRuntime(options) {
823
845
  return { kind: 'transport-uncertainty' };
824
846
  }
825
847
  }
826
- function declaredCards() {
848
+ function declaredAgents() {
827
849
  for (const capability of capabilities) {
828
- if (capability.conversationCards !== undefined)
829
- return capability.conversationCards;
850
+ if (capability.conversationAgents !== undefined)
851
+ return capability.conversationAgents;
830
852
  }
831
853
  return undefined;
832
854
  }
@@ -857,8 +879,6 @@ export function composeRuntime(options) {
857
879
  journal.record(`transfer action ${name}`);
858
880
  try {
859
881
  const result = await options.adapter.action(authorization, { name, input });
860
- if (result.kind === 'action' && result.at !== undefined)
861
- want(result.at);
862
882
  if (result.kind === 'scoped-refusal') {
863
883
  const room = replicas.get(result.locator)?.confirmed.record;
864
884
  if (room !== undefined)
@@ -890,32 +910,11 @@ export function composeRuntime(options) {
890
910
  options.adapter.reached(locator, written, roomVersion) &&
891
911
  !options.adapter.reached(locator, roomVersion, written));
892
912
  }
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) {
913
+ async function pullRefresh(authorized, confirmed) {
915
914
  if (!continuing())
916
915
  return unconfirmed;
917
916
  try {
918
- const result = await refreshRoom(options.adapter, authorized, confirmed, continuing, minPosition);
917
+ const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
919
918
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
920
919
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
921
920
  return result;
@@ -932,7 +931,7 @@ export function composeRuntime(options) {
932
931
  const result = await bootstrapRoom(options.adapter, authorized, room, {
933
932
  collection: modelOf(room.locator),
934
933
  selection: modelOf(room.locator, 'typed-selection'),
935
- }, continuing);
934
+ }, (models, held) => remodel(room.locator, models, held), continuing);
936
935
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
937
936
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
938
937
  return result;
@@ -945,20 +944,28 @@ export function composeRuntime(options) {
945
944
  async function advance(room, local, authorized) {
946
945
  const held = replicas.get(room.locator);
947
946
  if (held !== undefined) {
948
- const refreshed = await pullRefresh(authorized, held.confirmed, reachOf(room.locator));
947
+ const refreshed = await pullRefresh(authorized, held.confirmed);
949
948
  if (!continuing())
950
949
  return unconfirmed;
951
950
  if (refreshed.kind === 'still-current') {
951
+ stale.delete(room.locator);
952
952
  if (sourceRefusals.has(room.locator))
953
- return unconfirmed;
953
+ return null;
954
954
  if (unreadable.delete(room.locator)) {
955
+ scopedRefusals.delete(room.locator);
955
956
  list(room, true);
956
957
  emitRooms();
957
958
  }
958
959
  return null;
959
960
  }
960
961
  if (refreshed.kind === 'advanced') {
961
- await publish(local, room, refreshed.confirmed);
962
+ if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
963
+ const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
964
+ const reached = now !== undefined &&
965
+ options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
966
+ return reached ? null : advance(room, local, authorized);
967
+ }
968
+ await retainModel(local, room, 'collection', modelOf(room.locator));
962
969
  return null;
963
970
  }
964
971
  if (refreshed.kind !== 'beyond-horizon')
@@ -973,7 +980,10 @@ export function composeRuntime(options) {
973
980
  return null;
974
981
  }
975
982
  async function withhold(withheld, room, local, progress) {
976
- progress.complete = false;
983
+ if (withheld.kind === 'refused')
984
+ stale.delete(room.locator);
985
+ else
986
+ stale.add(room.locator);
977
987
  if (withheld.kind === 'scoped-refusal') {
978
988
  await scoped(withheld, room, local);
979
989
  }
@@ -1020,44 +1030,30 @@ export function composeRuntime(options) {
1020
1030
  (accepted.author === undefined ? '' : ` author ${accepted.author}`) +
1021
1031
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1022
1032
  }
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) {
1033
+ async function adoptAccepted(room, local, submitted, accepted) {
1041
1034
  if (!continuing())
1042
1035
  return unconfirmed;
1043
1036
  const current = replicas.get(room.locator);
1044
1037
  if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1045
1038
  return null;
1046
1039
  }
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')
1040
+ const held = current.confirmed.record.roomVersion;
1041
+ logAccepted(room.locator, submitted, accepted);
1042
+ const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1043
+ ? { kind: 'staged', confirmed: current.confirmed }
1044
+ : applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
1045
+ ? accepted.roomVersion
1046
+ : held, accepted.roomVersion);
1047
+ if (applied.kind !== 'staged') {
1048
+ const state = pendings.get(submitted.id);
1049
+ await local.accept(room.locator, submitted.id, accepted.roomVersion);
1050
+ if (state !== undefined)
1051
+ pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1052
+ emitQueue(room.locator);
1057
1053
  return applied;
1058
- const set = await settle(local, room.locator, current, applied.confirmed, remaining, {
1059
- room: true,
1060
- });
1054
+ }
1055
+ const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1056
+ pendings.delete(submitted.id);
1061
1057
  unreadable.delete(room.locator);
1062
1058
  sourceRefusals.delete(room.locator);
1063
1059
  declarationRefusals.delete(room.locator);
@@ -1066,17 +1062,8 @@ export function composeRuntime(options) {
1066
1062
  moveTo({ ...status, ...attention() });
1067
1063
  return null;
1068
1064
  }
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));
1065
+ async function adopted(room, local, submitted, accepted) {
1066
+ const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
1080
1067
  if (failed === null)
1081
1068
  return true;
1082
1069
  if (failed.kind === 'refused' && continuing())
@@ -1089,10 +1076,8 @@ export function composeRuntime(options) {
1089
1076
  const head = replica?.queue[0];
1090
1077
  if (replica === undefined || head === undefined)
1091
1078
  return;
1092
- if (pendings.get(head.id)?.accepted !== undefined) {
1093
- progress.complete = false;
1079
+ if (pendings.get(head.id)?.accepted !== undefined)
1094
1080
  return;
1095
- }
1096
1081
  const state = pendings.get(head.id);
1097
1082
  const { submission, lost: earlier } = state ?? {
1098
1083
  submission: { state: 'waiting' },
@@ -1120,6 +1105,7 @@ export function composeRuntime(options) {
1120
1105
  return;
1121
1106
  }
1122
1107
  const opensBefore = openSignals;
1108
+ const answered = awaitingAnswer(room.locator);
1123
1109
  try {
1124
1110
  result = await options.adapter.mutate(authorized, {
1125
1111
  locator: room.locator,
@@ -1128,11 +1114,14 @@ export function composeRuntime(options) {
1128
1114
  }, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
1129
1115
  }
1130
1116
  catch {
1117
+ answered();
1131
1118
  progress.complete = false;
1132
1119
  if (continuing())
1133
1120
  progress.reachable = false;
1134
1121
  return;
1135
1122
  }
1123
+ if (result.kind !== 'accepted' || !continuing())
1124
+ answered();
1136
1125
  if (!continuing())
1137
1126
  return;
1138
1127
  if (result.kind === 'transport-uncertainty') {
@@ -1185,6 +1174,7 @@ export function composeRuntime(options) {
1185
1174
  const reset = result.reset;
1186
1175
  progress.complete = false;
1187
1176
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1177
+ armTimer();
1188
1178
  return;
1189
1179
  }
1190
1180
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
@@ -1197,7 +1187,8 @@ export function composeRuntime(options) {
1197
1187
  return;
1198
1188
  }
1199
1189
  const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1200
- leaving: result.kind === 'expired' && landed(head, current.confirmed)
1190
+ leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1191
+ (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1201
1192
  ? { ...left, returned: null }
1202
1193
  : left,
1203
1194
  });
@@ -1211,95 +1202,33 @@ export function composeRuntime(options) {
1211
1202
  return withhold(withheld, room, local, progress);
1212
1203
  continue;
1213
1204
  }
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
- }
1205
+ const kept = await adopted(room, local, head, result).finally(answered);
1206
+ if (!kept) {
1258
1207
  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
1208
  return;
1271
1209
  }
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
1210
  }
1289
- return;
1211
+ }
1212
+ function awaitingAnswer(locator) {
1213
+ let release = () => { };
1214
+ const awaited = new Promise((resolve) => {
1215
+ release = resolve;
1216
+ });
1217
+ answering.set(locator, awaited);
1218
+ return () => {
1219
+ if (answering.get(locator) === awaited)
1220
+ answering.delete(locator);
1221
+ release();
1222
+ };
1290
1223
  }
1291
1224
  async function retainModel(local, room, attached, next) {
1292
1225
  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
1226
  const previous = modelOf(room.locator, attached);
1297
1227
  const heldBefore = replicas.get(room.locator)?.confirmed.record;
1298
1228
  if (JSON.stringify(previous) === JSON.stringify(next) &&
1299
1229
  (heldBefore === undefined ||
1300
1230
  (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
1301
1231
  JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
1302
- JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
1303
1232
  JSON.stringify(heldBefore.typedSelection) ===
1304
1233
  JSON.stringify(room.typedSelection ?? null))))
1305
1234
  return;
@@ -1310,15 +1239,12 @@ export function composeRuntime(options) {
1310
1239
  await exclusive(async () => {
1311
1240
  if (!continuing())
1312
1241
  return;
1313
- if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
1314
- return;
1315
1242
  const held = replicas.get(room.locator);
1316
1243
  if (held !== undefined) {
1317
1244
  const declared = {
1318
1245
  ...held.confirmed.record,
1319
1246
  operations: room.operations,
1320
1247
  replica: room.replica,
1321
- attachedModel: room.attachedModel,
1322
1248
  typedSelection: room.typedSelection ?? null,
1323
1249
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1324
1250
  };
@@ -1338,7 +1264,7 @@ export function composeRuntime(options) {
1338
1264
  queue: held.queue,
1339
1265
  });
1340
1266
  if (read.kind === 'staged') {
1341
- replicas.set(room.locator, replicaOf(read.confirmed, held.queue, rewriting(), held));
1267
+ replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1342
1268
  if (declarationRefusals.delete(room.locator)) {
1343
1269
  sourceRefusals.delete(room.locator);
1344
1270
  unreadable.delete(room.locator);
@@ -1362,77 +1288,6 @@ export function composeRuntime(options) {
1362
1288
  publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
1363
1289
  });
1364
1290
  }
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
1291
  async function scoped(result, room, local) {
1437
1292
  scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
1438
1293
  let scopedAway = room;
@@ -1460,14 +1315,36 @@ export function composeRuntime(options) {
1460
1315
  emitRooms();
1461
1316
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
1462
1317
  }
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) => {
1318
+ function heldRooms(listed) {
1319
+ return listed.flatMap((room) => {
1320
+ const record = replicas.get(room.locator)?.confirmed.record;
1321
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1322
+ return [];
1323
+ return [
1324
+ {
1325
+ locator: room.locator,
1326
+ roomVersion: record.roomVersion,
1327
+ documents: record.documents,
1328
+ revisions: record.revisions,
1329
+ attachments: record.attachments,
1330
+ },
1331
+ ];
1332
+ });
1333
+ }
1334
+ async function synchronize(listed, local, progress, attempt) {
1335
+ const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1336
+ const ready = listed.filter((_, index) => {
1466
1337
  const outcome = prepared[index];
1467
1338
  return outcome?.status === 'fulfilled' && outcome.value;
1468
1339
  });
1469
- const rooms = await Promise.allSettled(ready.map((room) => synchronizeRoom(room, local, progress)));
1470
- const failed = [...prepared, ...rooms].find((room) => room.status === 'rejected');
1340
+ if (attempt === 'opened') {
1341
+ options.adapter.roomStream?.hold(heldRooms(ready));
1342
+ handed = true;
1343
+ }
1344
+ const loaded = await Promise.allSettled(ready
1345
+ .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1346
+ .map((room) => loadRoom(room, local, progress)));
1347
+ const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1471
1348
  if (failed !== undefined)
1472
1349
  throw failed.reason;
1473
1350
  }
@@ -1484,11 +1361,8 @@ export function composeRuntime(options) {
1484
1361
  progress.complete = false;
1485
1362
  return false;
1486
1363
  }
1487
- const selection = room.typedSelection;
1488
- await Promise.all([
1489
- acquireModel(local, room, authorization, null),
1490
- ...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
1491
- ]);
1364
+ if (prior !== undefined)
1365
+ await retainModel(local, room, 'collection', modelOf(room.locator));
1492
1366
  if (discoveryPaused || authorization === undefined)
1493
1367
  return false;
1494
1368
  if (room.replica.admission !== 'admitted') {
@@ -1496,24 +1370,53 @@ export function composeRuntime(options) {
1496
1370
  progress.complete = false;
1497
1371
  return false;
1498
1372
  }
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
1373
  return true;
1505
1374
  }
1506
- async function synchronizeRoom(room, local, progress) {
1375
+ async function caughtUp(room, local, progress) {
1376
+ const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1377
+ if (held === undefined || authorization === undefined)
1378
+ return;
1379
+ const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1380
+ if (!behind)
1381
+ return;
1382
+ const withheld = await advance(room, local, authorization);
1383
+ if (withheld !== null)
1384
+ await withhold(withheld, room, local, progress);
1385
+ }
1386
+ function revoked(locator) {
1387
+ return ['collection', 'typed-selection'].some((attached) => {
1388
+ const model = modelOf(locator, attached);
1389
+ return model.status === 'unavailable' && model.reason === 'revoked';
1390
+ });
1391
+ }
1392
+ async function loadRoom(room, local, progress) {
1507
1393
  if (discoveryPaused || authorization === undefined)
1508
1394
  return;
1395
+ if (replicas.has(room.locator) && revoked(room.locator)) {
1396
+ await reload(room, local, progress);
1397
+ return;
1398
+ }
1509
1399
  const withheld = await advance(room, local, authorization);
1510
1400
  if (withheld !== null) {
1511
1401
  await withhold(withheld, room, local, progress);
1512
1402
  return;
1513
1403
  }
1514
- await submitRoom(room, local, authorization, progress);
1404
+ await caughtUp(room, local, progress);
1405
+ }
1406
+ async function reload(room, local, progress) {
1407
+ if (discoveryPaused || authorization === undefined)
1408
+ return;
1409
+ const bootstrapped = await pullBootstrap(authorization, room);
1410
+ if (!continuing())
1411
+ return;
1412
+ if (bootstrapped.kind !== 'staged') {
1413
+ await withhold(bootstrapped, room, local, progress);
1414
+ return;
1415
+ }
1416
+ await publish(local, room, bootstrapped.confirmed);
1417
+ await caughtUp(room, local, progress);
1515
1418
  }
1516
- async function cycle(local, progress) {
1419
+ async function discoverRooms(local, progress, attempt) {
1517
1420
  if (closed || authorization === undefined) {
1518
1421
  notifyReady();
1519
1422
  return;
@@ -1539,6 +1442,7 @@ export function composeRuntime(options) {
1539
1442
  }
1540
1443
  }
1541
1444
  progress.reachable = false;
1445
+ progress.complete = false;
1542
1446
  notifyReady();
1543
1447
  return;
1544
1448
  }
@@ -1553,17 +1457,6 @@ export function composeRuntime(options) {
1553
1457
  notifyReady();
1554
1458
  return;
1555
1459
  }
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
1460
  for (const room of result.rooms) {
1568
1461
  if (room.operations === undefined ||
1569
1462
  !['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
@@ -1571,8 +1464,7 @@ export function composeRuntime(options) {
1571
1464
  room.replica === undefined ||
1572
1465
  typeof room.replica.selection !== 'string' ||
1573
1466
  room.replica.selection.length === 0 ||
1574
- !['admitted', 'unavailable'].includes(room.replica.admission) ||
1575
- !declaredIdentity(room.attachedModel)) {
1467
+ !['admitted', 'unavailable'].includes(room.replica.admission)) {
1576
1468
  journal.record(`invalid model or replica declaration ${room.locator}`);
1577
1469
  await refuse('missing-required-guarantee');
1578
1470
  notifyReady();
@@ -1585,14 +1477,6 @@ export function composeRuntime(options) {
1585
1477
  return;
1586
1478
  }
1587
1479
  }
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
1480
  if (refusal !== null) {
1597
1481
  refusal = null;
1598
1482
  emitRefusal();
@@ -1600,26 +1484,13 @@ export function composeRuntime(options) {
1600
1484
  const previousCapabilities = capabilities;
1601
1485
  capabilities = result.capabilities;
1602
1486
  notifyReady();
1603
- if (declared.has('room-version-notifications'))
1604
- observeRoomVersions();
1605
1487
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1606
1488
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
1607
1489
  limits = result.providerLimits;
1608
1490
  await local.declare(limits, capabilities);
1609
1491
  }
1610
1492
  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);
1493
+ await synchronize(result.rooms, local, progress, attempt);
1623
1494
  }
1624
1495
  async function ensureAuthorization() {
1625
1496
  if (authorization !== undefined)
@@ -1633,64 +1504,253 @@ export function composeRuntime(options) {
1633
1504
  throw error;
1634
1505
  }
1635
1506
  }
1636
- async function connect() {
1637
- if (closed || stopping || discoveryPaused) {
1638
- notifyReady();
1507
+ function task(work) {
1508
+ if (stopping)
1509
+ return chain;
1510
+ chain = chain.then(work, work).catch(() => undefined);
1511
+ return chain;
1512
+ }
1513
+ function sync(attempt) {
1514
+ return task(async () => {
1515
+ if (closed || stopping || discoveryPaused) {
1516
+ notifyReady();
1517
+ return;
1518
+ }
1519
+ const progress = { complete: true, reachable: true };
1520
+ underway = attempt === 'opened' ? progress : null;
1521
+ complete = false;
1522
+ recorded = false;
1523
+ moveTo(transferring(status));
1524
+ handed = false;
1525
+ try {
1526
+ const local = await openLocal();
1527
+ if (closed)
1528
+ return;
1529
+ await ensureAuthorization();
1530
+ if (closed)
1531
+ return;
1532
+ await discoverRooms(local, progress, attempt);
1533
+ }
1534
+ catch {
1535
+ progress.complete = false;
1536
+ if (attempt === 'opened' && !handed)
1537
+ progress.reachable = false;
1538
+ }
1539
+ finally {
1540
+ notifyReady();
1541
+ }
1542
+ if (attempt !== 'opened') {
1543
+ await finishOpening(progress);
1544
+ return;
1545
+ }
1546
+ if (handed || closed || progress.reachable)
1547
+ return;
1548
+ underway = null;
1549
+ journal.record('connection reopen');
1550
+ await rest(progress);
1551
+ options.adapter.roomStream?.reopen();
1552
+ });
1553
+ }
1554
+ function closedSocket(revoked) {
1555
+ const opened = socketOpen;
1556
+ socketOpen = false;
1557
+ if (revoked) {
1558
+ void refuse('revoked-authorization');
1639
1559
  return;
1640
1560
  }
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)
1561
+ if (opened || status.providerUnreachable)
1562
+ return;
1563
+ moveTo({ ...status, providerUnreachable: true });
1564
+ }
1565
+ function applyOpening(expired) {
1566
+ return task(async () => {
1567
+ const progress = underway;
1568
+ underway = null;
1569
+ if (closed || stopping)
1650
1570
  return;
1651
- await cycle(local, progress);
1652
- if (closed)
1571
+ if (progress === null) {
1572
+ await reloadExpired(expired);
1653
1573
  return;
1654
- if (progress.complete) {
1655
- completedAt = new Date().toISOString();
1656
- await local.complete(completedAt);
1657
1574
  }
1575
+ try {
1576
+ const local = await openLocal();
1577
+ const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1578
+ const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1579
+ const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
1580
+ if (progress.reachable) {
1581
+ for (const room of held)
1582
+ if (!reloading.includes(room))
1583
+ await confirmModels(room, local);
1584
+ }
1585
+ const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1586
+ if (failed !== undefined)
1587
+ throw failed.reason;
1588
+ }
1589
+ catch {
1590
+ progress.complete = false;
1591
+ progress.reachable = false;
1592
+ }
1593
+ await finishOpening(progress);
1594
+ });
1595
+ }
1596
+ async function reloadExpired(expired) {
1597
+ const progress = { complete: true, reachable: true };
1598
+ try {
1599
+ const local = await openLocal();
1600
+ await Promise.allSettled([...discovered.values()]
1601
+ .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1602
+ .map((room) => reload(room, local, progress)));
1658
1603
  }
1659
1604
  catch {
1660
- progress.complete = false;
1661
- notifyReady();
1605
+ progress.reachable = false;
1606
+ }
1607
+ await rest(progress);
1608
+ }
1609
+ async function confirmModels(room, local) {
1610
+ for (const attached of ['collection', 'typed-selection']) {
1611
+ const model = modelOf(room.locator, attached);
1612
+ if (model.status === 'cached') {
1613
+ await retainModel(local, room, attached, { ...model, status: 'current' });
1614
+ }
1615
+ }
1616
+ }
1617
+ function submitLater(locator) {
1618
+ const running = submitting.get(locator);
1619
+ if (running !== undefined) {
1620
+ running.again = true;
1662
1621
  return;
1663
1622
  }
1664
- finally {
1665
- notifyReady();
1666
- moveTo(settled(status, progress, completedAt, pending(), attention()));
1623
+ const run = { again: false, done: Promise.resolve() };
1624
+ submitting.set(locator, run);
1625
+ run.done = (async () => {
1626
+ try {
1627
+ do {
1628
+ run.again = false;
1629
+ const room = discovered.get(locator);
1630
+ if (closed || stopping || discoveryPaused || room === undefined)
1631
+ return;
1632
+ const progress = { complete: true, reachable: true };
1633
+ try {
1634
+ await ensureAuthorization();
1635
+ if (authorization === undefined)
1636
+ return;
1637
+ await submitRoom(room, await openLocal(), authorization, progress);
1638
+ }
1639
+ catch {
1640
+ progress.reachable = false;
1641
+ }
1642
+ armTimer();
1643
+ await rest(progress);
1644
+ } while (run.again);
1645
+ }
1646
+ finally {
1647
+ submitting.delete(locator);
1648
+ }
1649
+ })();
1650
+ }
1651
+ function submitAll() {
1652
+ for (const [locator, replica] of replicas) {
1653
+ if (replica.queue.length > 0)
1654
+ submitLater(locator);
1667
1655
  }
1668
1656
  }
1669
- async function run() {
1657
+ async function finishOpening(progress) {
1670
1658
  try {
1671
- await connect();
1659
+ if (closed || discoveryPaused) {
1660
+ progress.complete = false;
1661
+ return;
1662
+ }
1663
+ submitAll();
1664
+ await conversations.reconcile();
1665
+ if (status.providerUnreachable)
1666
+ conversations.reconnected();
1667
+ const reading = { complete: true, reachable: true };
1668
+ await conversations.catchUp(reading);
1669
+ if (!reading.reachable)
1670
+ progress.reachable = false;
1671
+ }
1672
+ catch {
1673
+ progress.complete = false;
1672
1674
  }
1673
1675
  finally {
1674
- inflight = false;
1675
- if (queued && !closed) {
1676
- queued = false;
1677
- void enqueue();
1678
- }
1676
+ complete = progress.complete;
1677
+ await rest(progress);
1679
1678
  }
1680
1679
  }
1681
- function observeRoomVersions() {
1682
- const path = options.adapter.roomVersions;
1683
- if (path === undefined || withdrawNotifications !== undefined)
1680
+ async function rest(progress) {
1681
+ if (closed)
1684
1682
  return;
1685
- withdrawNotifications = path.observe((notification) => {
1686
- if (closed || stopping || discoveryPaused)
1683
+ let lastCompleted = status.lastCompleted;
1684
+ let synced = complete && stale.size === 0 && !pending();
1685
+ if (synced && !recorded) {
1686
+ const completedAt = new Date().toISOString();
1687
+ try {
1688
+ await (await openLocal()).complete(completedAt);
1689
+ recorded = true;
1690
+ lastCompleted = completedAt;
1691
+ }
1692
+ catch {
1693
+ synced = false;
1694
+ }
1695
+ }
1696
+ moveTo({
1697
+ state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
1698
+ lastCompleted,
1699
+ providerUnreachable: !progress.reachable,
1700
+ ...attention(),
1701
+ });
1702
+ }
1703
+ function applyRoom(locator, roomVersion) {
1704
+ announced.set(locator, roomVersion);
1705
+ return task(async () => {
1706
+ await answering.get(locator);
1707
+ if (closed || stopping || discoveryPaused || authorization === undefined)
1687
1708
  return;
1688
- if (notification.modelChanged === true && handed?.includes(notification.locator) === true) {
1689
- void enqueue();
1709
+ const room = discovered.get(locator);
1710
+ const held = replicas.get(locator);
1711
+ if (room === undefined || room.replica.admission !== 'admitted')
1712
+ return;
1713
+ if (held !== undefined &&
1714
+ (held.confirmed.record.replica.selection !== room.replica.selection ||
1715
+ options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
1716
+ return;
1717
+ }
1718
+ const during = underway;
1719
+ const progress = during ?? { complete: true, reachable: true };
1720
+ if (during === null)
1721
+ moveTo(transferring(status));
1722
+ try {
1723
+ const local = await openLocal();
1724
+ const withheld = await advance(room, local, authorization);
1725
+ if (withheld !== null)
1726
+ await withhold(withheld, room, local, progress);
1727
+ }
1728
+ catch {
1729
+ stale.add(locator);
1730
+ }
1731
+ finally {
1732
+ if (during === null)
1733
+ await rest(progress);
1734
+ }
1735
+ });
1736
+ }
1737
+ function observeStream() {
1738
+ const stream = options.adapter.roomStream;
1739
+ if (stream === undefined || withdrawStream !== undefined)
1740
+ return;
1741
+ withdrawStream = stream.observe((event) => {
1742
+ if (closed || stopping)
1690
1743
  return;
1744
+ if (event.kind === 'opened') {
1745
+ socketOpen = true;
1746
+ void sync('opened');
1691
1747
  }
1692
- notices.set(notification.locator, notification.roomVersion);
1693
- chain = chain.then(runNotified, runNotified);
1748
+ else if (event.kind === 'closed')
1749
+ closedSocket(event.revoked);
1750
+ else if (event.kind === 'opening')
1751
+ void applyOpening(event.expired);
1752
+ else
1753
+ void applyRoom(event.locator, event.roomVersion);
1694
1754
  });
1695
1755
  }
1696
1756
  function writesHeld() {
@@ -1711,14 +1771,17 @@ export function composeRuntime(options) {
1711
1771
  listener(mode);
1712
1772
  }
1713
1773
  if (releasing)
1714
- void enqueue();
1774
+ submitAll();
1715
1775
  }
1716
1776
  function observeConversations() {
1717
1777
  const stream = options.adapter.conversations;
1718
1778
  if (stream === undefined || withdrawConversations !== undefined)
1719
1779
  return;
1720
1780
  withdrawConversations = stream.observe((event) => {
1721
- conversations.receive(event);
1781
+ if (conversations.receive(event)) {
1782
+ void ready.then(scheduleCurrency);
1783
+ return;
1784
+ }
1722
1785
  if (event.kind !== 'gap' || conversations.current())
1723
1786
  return;
1724
1787
  show();
@@ -1729,7 +1792,7 @@ export function composeRuntime(options) {
1729
1792
  if (currencyQueued || closed || stopping)
1730
1793
  return;
1731
1794
  currencyQueued = true;
1732
- chain = chain.then(runCurrency, runCurrency);
1795
+ void task(runCurrency);
1733
1796
  }
1734
1797
  async function runCurrency() {
1735
1798
  currencyQueued = false;
@@ -1755,95 +1818,12 @@ export function composeRuntime(options) {
1755
1818
  }
1756
1819
  }
1757
1820
  }
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
1821
  function close() {
1842
1822
  if (closed)
1843
1823
  return;
1844
1824
  closed = true;
1845
- withdrawNotifications?.();
1846
- withdrawNotifications = undefined;
1825
+ withdrawStream?.();
1826
+ withdrawStream = undefined;
1847
1827
  withdrawConversations?.();
1848
1828
  withdrawConversations = undefined;
1849
1829
  withdrawServiceMode?.();
@@ -1867,6 +1847,7 @@ export function composeRuntime(options) {
1867
1847
  stopping = true;
1868
1848
  stopTimer();
1869
1849
  await chain.catch(() => undefined);
1850
+ await Promise.all([...submitting.values()].map((run) => run.done));
1870
1851
  await mutations;
1871
1852
  close();
1872
1853
  }
@@ -1875,11 +1856,29 @@ export function composeRuntime(options) {
1875
1856
  options.signal?.addEventListener('abort', close, { once: true });
1876
1857
  if (options.signal?.aborted)
1877
1858
  close();
1878
- void openLocal().catch(() => {
1859
+ observeStream();
1860
+ void openLocal().then(() => {
1861
+ if (capabilities.length > 0)
1862
+ notifyReady();
1863
+ }, () => {
1879
1864
  notifyReady();
1880
1865
  });
1881
- void enqueue();
1882
- function watch(topic, input, deliver) {
1866
+ if (options.adapter.roomStream === undefined)
1867
+ void sync('unstreamed');
1868
+ function planned(input, reach) {
1869
+ return planOf(input, roomFacts(reach), planReach(reach));
1870
+ }
1871
+ function watchPlan(plan, deliver) {
1872
+ return readSide.watch(plan, identityOf(plan), deliver);
1873
+ }
1874
+ async function searched(input, reach) {
1875
+ const plan = searchPlanOf(input);
1876
+ const selected = plan.documents === undefined && reach === null
1877
+ ? null
1878
+ : (await readSide.query(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
1879
+ return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
1880
+ }
1881
+ const watch = (topic, input, deliver) => {
1883
1882
  if (topic === 'room-drafts') {
1884
1883
  const listener = { notify: deliver, ready: false };
1885
1884
  roomDraftListeners.add(listener);
@@ -1900,6 +1899,8 @@ export function composeRuntime(options) {
1900
1899
  if (topic === 'conversation') {
1901
1900
  return conversations.watch(input[0], input[1], deliver);
1902
1901
  }
1902
+ if (topic === 'conversations')
1903
+ return conversations.watchList(deliver);
1903
1904
  if (topic === 'presentation') {
1904
1905
  const listener = {
1905
1906
  app: input[0],
@@ -1962,9 +1963,8 @@ export function composeRuntime(options) {
1962
1963
  queueSubscriptions.delete(queue);
1963
1964
  };
1964
1965
  }
1965
- const plan = planOf(input[0], roomFacts());
1966
- return readSide.watch(plan, identityOf(plan), deliver);
1967
- }
1966
+ return watchPlan(planned(input[0], null), deliver);
1967
+ };
1968
1968
  const md = {
1969
1969
  loaded() {
1970
1970
  return openLocal().then(() => undefined);
@@ -1972,15 +1972,8 @@ export function composeRuntime(options) {
1972
1972
  rooms() {
1973
1973
  return listed();
1974
1974
  },
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
- },
1975
+ query: ((input) => readSide.query(planned(input, null))),
1976
+ search: (input) => searched(input, null),
1984
1977
  types(locator) {
1985
1978
  return {
1986
1979
  collection: modelTypes(modelOf(locator)),
@@ -2087,9 +2080,7 @@ export function composeRuntime(options) {
2087
2080
  persistence() {
2088
2081
  return options.shell.persistentStorage;
2089
2082
  },
2090
- observe: ((topic, ...input) => opened((deliver) => watch(topic, input, (message) => {
2091
- deliver(structuredClone(message));
2092
- }), topic === 'query')),
2083
+ observe: observing(watch),
2093
2084
  async reauthorize() {
2094
2085
  if (refusal === null || !refusal.reauthorizationOffered)
2095
2086
  return;
@@ -2098,8 +2089,19 @@ export function composeRuntime(options) {
2098
2089
  await mutations;
2099
2090
  authorization = undefined;
2100
2091
  discoveryPaused = false;
2101
- await enqueue();
2092
+ if (options.adapter.roomStream === undefined)
2093
+ await sync('unstreamed');
2094
+ else
2095
+ await task(ensureAuthorization);
2102
2096
  },
2103
2097
  };
2104
- return { md, stop, watch };
2098
+ const reachable = {
2099
+ md,
2100
+ watch,
2101
+ plan: planned,
2102
+ query: (plan) => readSide.query(plan),
2103
+ watchPlan,
2104
+ search: searched,
2105
+ };
2106
+ return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2105
2107
  }