@agentshouse/mdruntime 0.1.0-cf28918c2bf91 → 0.1.0-cfbf80d8d4644

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 (69) hide show
  1. package/AGENTS.md +33 -8
  2. package/README.md +10 -10
  3. package/dist/adapter.d.ts +51 -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 +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 +501 -512
  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 +78 -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/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 +7 -2
  39. package/dist/query.d.ts.map +1 -1
  40. package/dist/query.js +29 -20
  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 +31 -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 +16 -10
  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 +4 -4
  54. package/dist/replica.d.ts.map +1 -1
  55. package/dist/replica.js +12 -13
  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.map +1 -1
  60. package/dist/rows.js +1 -4
  61. package/dist/save.d.ts +1 -2
  62. package/dist/save.d.ts.map +1 -1
  63. package/dist/save.js +90 -46
  64. package/dist/store.d.ts +1 -1
  65. package/dist/store.d.ts.map +1 -1
  66. package/dist/wire.d.ts +51 -16
  67. package/dist/wire.d.ts.map +1 -1
  68. package/dist/wire.js +50 -59
  69. 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, {
@@ -404,15 +395,14 @@ export function composeRuntime(options) {
404
395
  rooms.set(room.locator, {
405
396
  name: room.name,
406
397
  locator: room.locator,
398
+ handle: room.handle,
407
399
  operations: room.operations,
408
400
  replica: room.replica,
409
- attachedModel: room.attachedModel,
410
401
  model: modelTypes(modelOf(room.locator)),
411
402
  typedSelection: selection === null
412
403
  ? null
413
404
  : {
414
405
  roots: selection.roots,
415
- attachedModel: selection.attachedModel,
416
406
  model: modelTypes(modelOf(room.locator, 'typed-selection')),
417
407
  },
418
408
  providerOwnedPaths: room.providerOwnedPaths,
@@ -429,9 +419,6 @@ export function composeRuntime(options) {
429
419
  discovered.clear();
430
420
  for (const room of listed)
431
421
  discovered.set(room.locator, room);
432
- for (const locator of wanted.keys())
433
- if (!listedLocators.has(locator))
434
- wanted.delete(locator);
435
422
  await dropUnlisted(listedLocators, local);
436
423
  for (const room of listed) {
437
424
  const held = replicas.get(room.locator)?.confirmed.record;
@@ -465,6 +452,7 @@ export function composeRuntime(options) {
465
452
  rooms.delete(locator);
466
453
  roomDrafts.delete(locator);
467
454
  unreadable.delete(locator);
455
+ stale.delete(locator);
468
456
  scopedRefusals.delete(locator);
469
457
  sourceRefusals.delete(locator);
470
458
  declarationRefusals.delete(locator);
@@ -473,7 +461,6 @@ export function composeRuntime(options) {
473
461
  models.delete(key);
474
462
  liveModels.delete(key);
475
463
  invalidModels.delete(key);
476
- modelEpoch.delete(key);
477
464
  }
478
465
  journal.record(`room unlisted ${locator}`);
479
466
  }
@@ -486,7 +473,6 @@ export function composeRuntime(options) {
486
473
  if (prior !== undefined &&
487
474
  prior.status !== 'unavailable' &&
488
475
  (persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
489
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
490
476
  invalidModels.add(key);
491
477
  }
492
478
  const model = persisted.status === 'unavailable' ||
@@ -541,7 +527,7 @@ export function composeRuntime(options) {
541
527
  record: retyped,
542
528
  collection: parseCollection(retyped.documents, compositionOf(retyped)),
543
529
  };
544
- replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], rewriting(), prior));
530
+ replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
545
531
  const existing = rooms.get(record.locator);
546
532
  if (existing === undefined)
547
533
  list(record, false);
@@ -552,13 +538,11 @@ export function composeRuntime(options) {
552
538
  attachments: record.attachments,
553
539
  operations: record.operations,
554
540
  replica: record.replica,
555
- attachedModel: record.attachedModel,
556
541
  model: modelTypes(model),
557
542
  typedSelection: record.typedSelection === null
558
543
  ? null
559
544
  : {
560
545
  roots: record.typedSelection.roots,
561
- attachedModel: record.typedSelection.attachedModel,
562
546
  model: modelTypes(selectionModel),
563
547
  },
564
548
  });
@@ -626,7 +610,7 @@ export function composeRuntime(options) {
626
610
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
627
611
  }
628
612
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
629
- const withdrawn = withdrawals(confirmed, queue, withdrawable, rewriting());
613
+ const withdrawn = withdrawals(confirmed, queue, withdrawable);
630
614
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
631
615
  const returning = [
632
616
  ...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
@@ -662,42 +646,73 @@ export function composeRuntime(options) {
662
646
  rememberRoomDraft(record);
663
647
  for (const batch of withdrawn)
664
648
  journal.record(`outcome ${locator} ${batch.id} withdrawn`);
665
- replicas.set(locator, replicaOf(confirmed, kept, rewriting(), before));
649
+ replicas.set(locator, replicaOf(confirmed, kept, before));
666
650
  return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
667
651
  room: settling.room,
668
652
  queue: kept.length !== (before?.queue.length ?? 0),
669
653
  drafts: [...written.keys()],
670
654
  });
671
655
  }
672
- function publish(local, room, confirmed) {
656
+ function adoptModels(locator, record) {
657
+ for (const [attached, model] of [
658
+ ['collection', record.model],
659
+ ['typed-selection', record.selectionModel],
660
+ ]) {
661
+ const key = modelKey(locator, attached);
662
+ models.set(key, model);
663
+ invalidModels.delete(key);
664
+ if (model.status === 'current')
665
+ liveModels.set(key, JSON.stringify(model.snapshot));
666
+ else if (model.status === 'unavailable')
667
+ liveModels.delete(key);
668
+ }
669
+ }
670
+ function remodel(locator, answers, held) {
671
+ const taken = (answer, previous, attached) => {
672
+ if (answer === undefined)
673
+ return previous;
674
+ const { state, refused } = attachedModel(answer, previous);
675
+ if (refused !== null) {
676
+ const where = refused.location === undefined ? '' : ` ${refused.location}`;
677
+ journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
678
+ }
679
+ models.set(modelKey(locator, attached), state);
680
+ return state;
681
+ };
682
+ return {
683
+ collection: taken(answers?.collection, held.collection, 'collection'),
684
+ selection: taken(answers?.selection, held.selection, 'typed-selection'),
685
+ };
686
+ }
687
+ function publish(local, room, confirmed, base) {
673
688
  return exclusive(async () => {
674
689
  if (!continuing())
675
- return;
690
+ return true;
676
691
  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}`);
692
+ if (base !== undefined &&
693
+ (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
694
+ prior.confirmed.record.revisions !== base.record.revisions)) {
695
+ return false;
696
+ }
697
+ adoptModels(room.locator, confirmed.record);
684
698
  const queue = prior?.queue ?? [];
685
- const roomVersion = retained.confirmed.record.roomVersion;
699
+ const roomVersion = confirmed.record.roomVersion;
686
700
  const accepted = queue.filter((batch) => {
687
701
  const accepted = pendings.get(batch.id)?.accepted;
688
702
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
689
703
  });
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);
704
+ const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
705
+ for (const batch of accepted)
706
+ pendings.delete(batch.id);
707
+ stale.delete(room.locator);
708
+ if (unreadable.delete(room.locator))
709
+ scopedRefusals.delete(room.locator);
696
710
  sourceRefusals.delete(room.locator);
697
711
  declarationRefusals.delete(room.locator);
698
712
  list(room, true);
699
713
  publishChangeSet(set);
700
714
  moveTo({ ...status, ...attention() });
715
+ return true;
701
716
  });
702
717
  }
703
718
  async function saveRoomDrafts(locator, named, writtenBy) {
@@ -726,7 +741,7 @@ export function composeRuntime(options) {
726
741
  operations: room.operations,
727
742
  providerOwnedPaths: room.providerOwnedPaths,
728
743
  providerLimits: limits,
729
- rewriting: rewriting(),
744
+ handle: room.handle,
730
745
  visible: replica.visible,
731
746
  changes: parts.flatMap(({ changes }) => changes),
732
747
  bases: parts.flatMap(({ bases }) => bases),
@@ -750,15 +765,16 @@ export function composeRuntime(options) {
750
765
  return { outcome: 'refused', rule: 'durable-write' };
751
766
  }
752
767
  pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
768
+ recorded = false;
753
769
  for (const path of consumed)
754
770
  forgetRoomDraft(locator, path);
755
- replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting(), replica));
771
+ replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
756
772
  publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
757
773
  queue: true,
758
774
  drafts: consumed,
759
775
  }));
760
776
  moveTo({ ...saving(status), ...attention() });
761
- void enqueue();
777
+ submitLater(locator);
762
778
  return { outcome: 'saved', identifier: built.batch.id };
763
779
  }
764
780
  async function mark(local, locator, identifier, submission, lost) {
@@ -823,10 +839,10 @@ export function composeRuntime(options) {
823
839
  return { kind: 'transport-uncertainty' };
824
840
  }
825
841
  }
826
- function declaredCards() {
842
+ function declaredAgents() {
827
843
  for (const capability of capabilities) {
828
- if (capability.conversationCards !== undefined)
829
- return capability.conversationCards;
844
+ if (capability.conversationAgents !== undefined)
845
+ return capability.conversationAgents;
830
846
  }
831
847
  return undefined;
832
848
  }
@@ -857,8 +873,6 @@ export function composeRuntime(options) {
857
873
  journal.record(`transfer action ${name}`);
858
874
  try {
859
875
  const result = await options.adapter.action(authorization, { name, input });
860
- if (result.kind === 'action' && result.at !== undefined)
861
- want(result.at);
862
876
  if (result.kind === 'scoped-refusal') {
863
877
  const room = replicas.get(result.locator)?.confirmed.record;
864
878
  if (room !== undefined)
@@ -890,32 +904,11 @@ export function composeRuntime(options) {
890
904
  options.adapter.reached(locator, written, roomVersion) &&
891
905
  !options.adapter.reached(locator, roomVersion, written));
892
906
  }
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) {
907
+ async function pullRefresh(authorized, confirmed) {
915
908
  if (!continuing())
916
909
  return unconfirmed;
917
910
  try {
918
- const result = await refreshRoom(options.adapter, authorized, confirmed, continuing, minPosition);
911
+ const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
919
912
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
920
913
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
921
914
  return result;
@@ -932,7 +925,7 @@ export function composeRuntime(options) {
932
925
  const result = await bootstrapRoom(options.adapter, authorized, room, {
933
926
  collection: modelOf(room.locator),
934
927
  selection: modelOf(room.locator, 'typed-selection'),
935
- }, continuing);
928
+ }, (models, held) => remodel(room.locator, models, held), continuing);
936
929
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
937
930
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
938
931
  return result;
@@ -945,20 +938,28 @@ export function composeRuntime(options) {
945
938
  async function advance(room, local, authorized) {
946
939
  const held = replicas.get(room.locator);
947
940
  if (held !== undefined) {
948
- const refreshed = await pullRefresh(authorized, held.confirmed, reachOf(room.locator));
941
+ const refreshed = await pullRefresh(authorized, held.confirmed);
949
942
  if (!continuing())
950
943
  return unconfirmed;
951
944
  if (refreshed.kind === 'still-current') {
945
+ stale.delete(room.locator);
952
946
  if (sourceRefusals.has(room.locator))
953
- return unconfirmed;
947
+ return null;
954
948
  if (unreadable.delete(room.locator)) {
949
+ scopedRefusals.delete(room.locator);
955
950
  list(room, true);
956
951
  emitRooms();
957
952
  }
958
953
  return null;
959
954
  }
960
955
  if (refreshed.kind === 'advanced') {
961
- await publish(local, room, refreshed.confirmed);
956
+ if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
957
+ const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
958
+ const reached = now !== undefined &&
959
+ options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
960
+ return reached ? null : advance(room, local, authorized);
961
+ }
962
+ await retainModel(local, room, 'collection', modelOf(room.locator));
962
963
  return null;
963
964
  }
964
965
  if (refreshed.kind !== 'beyond-horizon')
@@ -973,7 +974,10 @@ export function composeRuntime(options) {
973
974
  return null;
974
975
  }
975
976
  async function withhold(withheld, room, local, progress) {
976
- progress.complete = false;
977
+ if (withheld.kind === 'refused')
978
+ stale.delete(room.locator);
979
+ else
980
+ stale.add(room.locator);
977
981
  if (withheld.kind === 'scoped-refusal') {
978
982
  await scoped(withheld, room, local);
979
983
  }
@@ -1020,44 +1024,30 @@ export function composeRuntime(options) {
1020
1024
  (accepted.author === undefined ? '' : ` author ${accepted.author}`) +
1021
1025
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1022
1026
  }
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) {
1027
+ async function adoptAccepted(room, local, submitted, accepted) {
1041
1028
  if (!continuing())
1042
1029
  return unconfirmed;
1043
1030
  const current = replicas.get(room.locator);
1044
1031
  if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1045
1032
  return null;
1046
1033
  }
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')
1034
+ const held = current.confirmed.record.roomVersion;
1035
+ logAccepted(room.locator, submitted, accepted);
1036
+ const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1037
+ ? { kind: 'staged', confirmed: current.confirmed }
1038
+ : applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
1039
+ ? accepted.roomVersion
1040
+ : held, accepted.roomVersion);
1041
+ if (applied.kind !== 'staged') {
1042
+ const state = pendings.get(submitted.id);
1043
+ await local.accept(room.locator, submitted.id, accepted.roomVersion);
1044
+ if (state !== undefined)
1045
+ pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1046
+ emitQueue(room.locator);
1057
1047
  return applied;
1058
- const set = await settle(local, room.locator, current, applied.confirmed, remaining, {
1059
- room: true,
1060
- });
1048
+ }
1049
+ const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1050
+ pendings.delete(submitted.id);
1061
1051
  unreadable.delete(room.locator);
1062
1052
  sourceRefusals.delete(room.locator);
1063
1053
  declarationRefusals.delete(room.locator);
@@ -1066,17 +1056,8 @@ export function composeRuntime(options) {
1066
1056
  moveTo({ ...status, ...attention() });
1067
1057
  return null;
1068
1058
  }
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));
1059
+ async function adopted(room, local, submitted, accepted) {
1060
+ const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
1080
1061
  if (failed === null)
1081
1062
  return true;
1082
1063
  if (failed.kind === 'refused' && continuing())
@@ -1089,10 +1070,8 @@ export function composeRuntime(options) {
1089
1070
  const head = replica?.queue[0];
1090
1071
  if (replica === undefined || head === undefined)
1091
1072
  return;
1092
- if (pendings.get(head.id)?.accepted !== undefined) {
1093
- progress.complete = false;
1073
+ if (pendings.get(head.id)?.accepted !== undefined)
1094
1074
  return;
1095
- }
1096
1075
  const state = pendings.get(head.id);
1097
1076
  const { submission, lost: earlier } = state ?? {
1098
1077
  submission: { state: 'waiting' },
@@ -1120,6 +1099,7 @@ export function composeRuntime(options) {
1120
1099
  return;
1121
1100
  }
1122
1101
  const opensBefore = openSignals;
1102
+ const answered = awaitingAnswer(room.locator);
1123
1103
  try {
1124
1104
  result = await options.adapter.mutate(authorized, {
1125
1105
  locator: room.locator,
@@ -1128,11 +1108,14 @@ export function composeRuntime(options) {
1128
1108
  }, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
1129
1109
  }
1130
1110
  catch {
1111
+ answered();
1131
1112
  progress.complete = false;
1132
1113
  if (continuing())
1133
1114
  progress.reachable = false;
1134
1115
  return;
1135
1116
  }
1117
+ if (result.kind !== 'accepted' || !continuing())
1118
+ answered();
1136
1119
  if (!continuing())
1137
1120
  return;
1138
1121
  if (result.kind === 'transport-uncertainty') {
@@ -1185,6 +1168,7 @@ export function composeRuntime(options) {
1185
1168
  const reset = result.reset;
1186
1169
  progress.complete = false;
1187
1170
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1171
+ armTimer();
1188
1172
  return;
1189
1173
  }
1190
1174
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
@@ -1211,95 +1195,33 @@ export function composeRuntime(options) {
1211
1195
  return withhold(withheld, room, local, progress);
1212
1196
  continue;
1213
1197
  }
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
- }
1198
+ const kept = await adopted(room, local, head, result).finally(answered);
1199
+ if (!kept) {
1258
1200
  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
1201
  return;
1271
1202
  }
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
1203
  }
1289
- return;
1204
+ }
1205
+ function awaitingAnswer(locator) {
1206
+ let release = () => { };
1207
+ const awaited = new Promise((resolve) => {
1208
+ release = resolve;
1209
+ });
1210
+ answering.set(locator, awaited);
1211
+ return () => {
1212
+ if (answering.get(locator) === awaited)
1213
+ answering.delete(locator);
1214
+ release();
1215
+ };
1290
1216
  }
1291
1217
  async function retainModel(local, room, attached, next) {
1292
1218
  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
1219
  const previous = modelOf(room.locator, attached);
1297
1220
  const heldBefore = replicas.get(room.locator)?.confirmed.record;
1298
1221
  if (JSON.stringify(previous) === JSON.stringify(next) &&
1299
1222
  (heldBefore === undefined ||
1300
1223
  (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
1301
1224
  JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
1302
- JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
1303
1225
  JSON.stringify(heldBefore.typedSelection) ===
1304
1226
  JSON.stringify(room.typedSelection ?? null))))
1305
1227
  return;
@@ -1310,15 +1232,12 @@ export function composeRuntime(options) {
1310
1232
  await exclusive(async () => {
1311
1233
  if (!continuing())
1312
1234
  return;
1313
- if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
1314
- return;
1315
1235
  const held = replicas.get(room.locator);
1316
1236
  if (held !== undefined) {
1317
1237
  const declared = {
1318
1238
  ...held.confirmed.record,
1319
1239
  operations: room.operations,
1320
1240
  replica: room.replica,
1321
- attachedModel: room.attachedModel,
1322
1241
  typedSelection: room.typedSelection ?? null,
1323
1242
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1324
1243
  };
@@ -1338,7 +1257,7 @@ export function composeRuntime(options) {
1338
1257
  queue: held.queue,
1339
1258
  });
1340
1259
  if (read.kind === 'staged') {
1341
- replicas.set(room.locator, replicaOf(read.confirmed, held.queue, rewriting(), held));
1260
+ replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1342
1261
  if (declarationRefusals.delete(room.locator)) {
1343
1262
  sourceRefusals.delete(room.locator);
1344
1263
  unreadable.delete(room.locator);
@@ -1362,77 +1281,6 @@ export function composeRuntime(options) {
1362
1281
  publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
1363
1282
  });
1364
1283
  }
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
1284
  async function scoped(result, room, local) {
1437
1285
  scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
1438
1286
  let scopedAway = room;
@@ -1460,14 +1308,34 @@ export function composeRuntime(options) {
1460
1308
  emitRooms();
1461
1309
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
1462
1310
  }
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) => {
1311
+ function heldRooms(listed) {
1312
+ return listed.flatMap((room) => {
1313
+ const record = replicas.get(room.locator)?.confirmed.record;
1314
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1315
+ return [];
1316
+ return [
1317
+ {
1318
+ locator: room.locator,
1319
+ roomVersion: record.roomVersion,
1320
+ documents: record.documents,
1321
+ revisions: record.revisions,
1322
+ attachments: record.attachments,
1323
+ },
1324
+ ];
1325
+ });
1326
+ }
1327
+ async function synchronize(listed, local, progress, attempt) {
1328
+ const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1329
+ const ready = listed.filter((_, index) => {
1466
1330
  const outcome = prepared[index];
1467
1331
  return outcome?.status === 'fulfilled' && outcome.value;
1468
1332
  });
1469
- const rooms = await Promise.allSettled(ready.map((room) => synchronizeRoom(room, local, progress)));
1470
- const failed = [...prepared, ...rooms].find((room) => room.status === 'rejected');
1333
+ if (attempt === 'opened')
1334
+ options.adapter.roomStream?.hold(heldRooms(ready));
1335
+ const loaded = await Promise.allSettled(ready
1336
+ .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1337
+ .map((room) => loadRoom(room, local, progress)));
1338
+ const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1471
1339
  if (failed !== undefined)
1472
1340
  throw failed.reason;
1473
1341
  }
@@ -1484,11 +1352,8 @@ export function composeRuntime(options) {
1484
1352
  progress.complete = false;
1485
1353
  return false;
1486
1354
  }
1487
- const selection = room.typedSelection;
1488
- await Promise.all([
1489
- acquireModel(local, room, authorization, null),
1490
- ...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
1491
- ]);
1355
+ if (prior !== undefined)
1356
+ await retainModel(local, room, 'collection', modelOf(room.locator));
1492
1357
  if (discoveryPaused || authorization === undefined)
1493
1358
  return false;
1494
1359
  if (room.replica.admission !== 'admitted') {
@@ -1496,24 +1361,53 @@ export function composeRuntime(options) {
1496
1361
  progress.complete = false;
1497
1362
  return false;
1498
1363
  }
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
1364
  return true;
1505
1365
  }
1506
- async function synchronizeRoom(room, local, progress) {
1366
+ async function caughtUp(room, local, progress) {
1367
+ const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1368
+ if (held === undefined || authorization === undefined)
1369
+ return;
1370
+ const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1371
+ if (!behind)
1372
+ return;
1373
+ const withheld = await advance(room, local, authorization);
1374
+ if (withheld !== null)
1375
+ await withhold(withheld, room, local, progress);
1376
+ }
1377
+ function revoked(locator) {
1378
+ return ['collection', 'typed-selection'].some((attached) => {
1379
+ const model = modelOf(locator, attached);
1380
+ return model.status === 'unavailable' && model.reason === 'revoked';
1381
+ });
1382
+ }
1383
+ async function loadRoom(room, local, progress) {
1507
1384
  if (discoveryPaused || authorization === undefined)
1508
1385
  return;
1386
+ if (replicas.has(room.locator) && revoked(room.locator)) {
1387
+ await reload(room, local, progress);
1388
+ return;
1389
+ }
1509
1390
  const withheld = await advance(room, local, authorization);
1510
1391
  if (withheld !== null) {
1511
1392
  await withhold(withheld, room, local, progress);
1512
1393
  return;
1513
1394
  }
1514
- await submitRoom(room, local, authorization, progress);
1395
+ await caughtUp(room, local, progress);
1515
1396
  }
1516
- async function cycle(local, progress) {
1397
+ async function reload(room, local, progress) {
1398
+ if (discoveryPaused || authorization === undefined)
1399
+ return;
1400
+ const bootstrapped = await pullBootstrap(authorization, room);
1401
+ if (!continuing())
1402
+ return;
1403
+ if (bootstrapped.kind !== 'staged') {
1404
+ await withhold(bootstrapped, room, local, progress);
1405
+ return;
1406
+ }
1407
+ await publish(local, room, bootstrapped.confirmed);
1408
+ await caughtUp(room, local, progress);
1409
+ }
1410
+ async function discoverRooms(local, progress, attempt) {
1517
1411
  if (closed || authorization === undefined) {
1518
1412
  notifyReady();
1519
1413
  return;
@@ -1539,6 +1433,10 @@ export function composeRuntime(options) {
1539
1433
  }
1540
1434
  }
1541
1435
  progress.reachable = false;
1436
+ progress.complete = false;
1437
+ if (attempt === 'opened') {
1438
+ options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1439
+ }
1542
1440
  notifyReady();
1543
1441
  return;
1544
1442
  }
@@ -1553,17 +1451,6 @@ export function composeRuntime(options) {
1553
1451
  notifyReady();
1554
1452
  return;
1555
1453
  }
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
1454
  for (const room of result.rooms) {
1568
1455
  if (room.operations === undefined ||
1569
1456
  !['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
@@ -1571,8 +1458,7 @@ export function composeRuntime(options) {
1571
1458
  room.replica === undefined ||
1572
1459
  typeof room.replica.selection !== 'string' ||
1573
1460
  room.replica.selection.length === 0 ||
1574
- !['admitted', 'unavailable'].includes(room.replica.admission) ||
1575
- !declaredIdentity(room.attachedModel)) {
1461
+ !['admitted', 'unavailable'].includes(room.replica.admission)) {
1576
1462
  journal.record(`invalid model or replica declaration ${room.locator}`);
1577
1463
  await refuse('missing-required-guarantee');
1578
1464
  notifyReady();
@@ -1585,14 +1471,6 @@ export function composeRuntime(options) {
1585
1471
  return;
1586
1472
  }
1587
1473
  }
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
1474
  if (refusal !== null) {
1597
1475
  refusal = null;
1598
1476
  emitRefusal();
@@ -1600,26 +1478,13 @@ export function composeRuntime(options) {
1600
1478
  const previousCapabilities = capabilities;
1601
1479
  capabilities = result.capabilities;
1602
1480
  notifyReady();
1603
- if (declared.has('room-version-notifications'))
1604
- observeRoomVersions();
1605
1481
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1606
1482
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
1607
1483
  limits = result.providerLimits;
1608
1484
  await local.declare(limits, capabilities);
1609
1485
  }
1610
1486
  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);
1487
+ await synchronize(result.rooms, local, progress, attempt);
1623
1488
  }
1624
1489
  async function ensureAuthorization() {
1625
1490
  if (authorization !== undefined)
@@ -1633,64 +1498,246 @@ export function composeRuntime(options) {
1633
1498
  throw error;
1634
1499
  }
1635
1500
  }
1636
- async function connect() {
1637
- if (closed || stopping || discoveryPaused) {
1638
- notifyReady();
1501
+ function task(work) {
1502
+ if (stopping)
1503
+ return chain;
1504
+ chain = chain.then(work, work).catch(() => undefined);
1505
+ return chain;
1506
+ }
1507
+ function sync(attempt) {
1508
+ return task(async () => {
1509
+ if (closed || stopping || discoveryPaused) {
1510
+ notifyReady();
1511
+ return;
1512
+ }
1513
+ const progress = { complete: true, reachable: true };
1514
+ underway = attempt === 'opened' ? progress : null;
1515
+ complete = false;
1516
+ recorded = false;
1517
+ moveTo(transferring(status));
1518
+ try {
1519
+ const local = await openLocal();
1520
+ if (closed)
1521
+ return;
1522
+ await ensureAuthorization();
1523
+ if (closed)
1524
+ return;
1525
+ await discoverRooms(local, progress, attempt);
1526
+ }
1527
+ catch {
1528
+ progress.complete = false;
1529
+ if (attempt === 'opened') {
1530
+ options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1531
+ }
1532
+ }
1533
+ finally {
1534
+ notifyReady();
1535
+ }
1536
+ if (attempt === 'opened')
1537
+ return;
1538
+ await finishOpening(progress);
1539
+ });
1540
+ }
1541
+ function closedSocket(revoked) {
1542
+ const opened = socketOpen;
1543
+ socketOpen = false;
1544
+ if (revoked) {
1545
+ void refuse('revoked-authorization');
1639
1546
  return;
1640
1547
  }
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)
1548
+ if (opened || status.providerUnreachable)
1549
+ return;
1550
+ moveTo({ ...status, providerUnreachable: true });
1551
+ }
1552
+ function applyOpening(expired) {
1553
+ return task(async () => {
1554
+ const progress = underway;
1555
+ underway = null;
1556
+ if (closed || stopping)
1650
1557
  return;
1651
- await cycle(local, progress);
1652
- if (closed)
1558
+ if (progress === null) {
1559
+ await reloadExpired(expired);
1653
1560
  return;
1654
- if (progress.complete) {
1655
- completedAt = new Date().toISOString();
1656
- await local.complete(completedAt);
1657
1561
  }
1562
+ try {
1563
+ const local = await openLocal();
1564
+ const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1565
+ const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1566
+ const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
1567
+ if (progress.reachable) {
1568
+ for (const room of held)
1569
+ if (!reloading.includes(room))
1570
+ await confirmModels(room, local);
1571
+ }
1572
+ const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1573
+ if (failed !== undefined)
1574
+ throw failed.reason;
1575
+ }
1576
+ catch {
1577
+ progress.complete = false;
1578
+ progress.reachable = false;
1579
+ }
1580
+ await finishOpening(progress);
1581
+ });
1582
+ }
1583
+ async function reloadExpired(expired) {
1584
+ const progress = { complete: true, reachable: true };
1585
+ try {
1586
+ const local = await openLocal();
1587
+ await Promise.allSettled([...discovered.values()]
1588
+ .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1589
+ .map((room) => reload(room, local, progress)));
1658
1590
  }
1659
1591
  catch {
1660
- progress.complete = false;
1661
- notifyReady();
1592
+ progress.reachable = false;
1593
+ }
1594
+ await rest(progress);
1595
+ }
1596
+ async function confirmModels(room, local) {
1597
+ for (const attached of ['collection', 'typed-selection']) {
1598
+ const model = modelOf(room.locator, attached);
1599
+ if (model.status === 'cached') {
1600
+ await retainModel(local, room, attached, { ...model, status: 'current' });
1601
+ }
1602
+ }
1603
+ }
1604
+ function submitLater(locator) {
1605
+ const running = submitting.get(locator);
1606
+ if (running !== undefined) {
1607
+ running.again = true;
1662
1608
  return;
1663
1609
  }
1664
- finally {
1665
- notifyReady();
1666
- moveTo(settled(status, progress, completedAt, pending(), attention()));
1610
+ const run = { again: false, done: Promise.resolve() };
1611
+ submitting.set(locator, run);
1612
+ run.done = (async () => {
1613
+ try {
1614
+ do {
1615
+ run.again = false;
1616
+ const room = discovered.get(locator);
1617
+ if (closed || stopping || discoveryPaused || room === undefined)
1618
+ return;
1619
+ const progress = { complete: true, reachable: true };
1620
+ try {
1621
+ await ensureAuthorization();
1622
+ if (authorization === undefined)
1623
+ return;
1624
+ await submitRoom(room, await openLocal(), authorization, progress);
1625
+ }
1626
+ catch {
1627
+ progress.reachable = false;
1628
+ }
1629
+ armTimer();
1630
+ await rest(progress);
1631
+ } while (run.again);
1632
+ }
1633
+ finally {
1634
+ submitting.delete(locator);
1635
+ }
1636
+ })();
1637
+ }
1638
+ function submitAll() {
1639
+ for (const [locator, replica] of replicas) {
1640
+ if (replica.queue.length > 0)
1641
+ submitLater(locator);
1667
1642
  }
1668
1643
  }
1669
- async function run() {
1644
+ async function finishOpening(progress) {
1670
1645
  try {
1671
- await connect();
1646
+ if (closed || discoveryPaused) {
1647
+ progress.complete = false;
1648
+ return;
1649
+ }
1650
+ submitAll();
1651
+ await conversations.reconcile();
1652
+ if (status.providerUnreachable)
1653
+ conversations.reconnected();
1654
+ const reading = { complete: true, reachable: true };
1655
+ await conversations.catchUp(reading);
1656
+ if (!reading.reachable)
1657
+ progress.reachable = false;
1658
+ }
1659
+ catch {
1660
+ progress.complete = false;
1672
1661
  }
1673
1662
  finally {
1674
- inflight = false;
1675
- if (queued && !closed) {
1676
- queued = false;
1677
- void enqueue();
1678
- }
1663
+ complete = progress.complete;
1664
+ await rest(progress);
1679
1665
  }
1680
1666
  }
1681
- function observeRoomVersions() {
1682
- const path = options.adapter.roomVersions;
1683
- if (path === undefined || withdrawNotifications !== undefined)
1667
+ async function rest(progress) {
1668
+ if (closed)
1684
1669
  return;
1685
- withdrawNotifications = path.observe((notification) => {
1686
- if (closed || stopping || discoveryPaused)
1670
+ let lastCompleted = status.lastCompleted;
1671
+ let synced = complete && stale.size === 0 && !pending();
1672
+ if (synced && !recorded) {
1673
+ const completedAt = new Date().toISOString();
1674
+ try {
1675
+ await (await openLocal()).complete(completedAt);
1676
+ recorded = true;
1677
+ lastCompleted = completedAt;
1678
+ }
1679
+ catch {
1680
+ synced = false;
1681
+ }
1682
+ }
1683
+ moveTo({
1684
+ state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
1685
+ lastCompleted,
1686
+ providerUnreachable: !progress.reachable,
1687
+ ...attention(),
1688
+ });
1689
+ }
1690
+ function applyRoom(locator, roomVersion) {
1691
+ announced.set(locator, roomVersion);
1692
+ return task(async () => {
1693
+ await answering.get(locator);
1694
+ if (closed || stopping || discoveryPaused || authorization === undefined)
1695
+ return;
1696
+ const room = discovered.get(locator);
1697
+ const held = replicas.get(locator);
1698
+ if (room === undefined || room.replica.admission !== 'admitted')
1687
1699
  return;
1688
- if (notification.modelChanged === true && handed?.includes(notification.locator) === true) {
1689
- void enqueue();
1700
+ if (held !== undefined &&
1701
+ (held.confirmed.record.replica.selection !== room.replica.selection ||
1702
+ options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
1703
+ return;
1704
+ }
1705
+ const during = underway;
1706
+ const progress = during ?? { complete: true, reachable: true };
1707
+ if (during === null)
1708
+ moveTo(transferring(status));
1709
+ try {
1710
+ const local = await openLocal();
1711
+ const withheld = await advance(room, local, authorization);
1712
+ if (withheld !== null)
1713
+ await withhold(withheld, room, local, progress);
1714
+ }
1715
+ catch {
1716
+ stale.add(locator);
1717
+ }
1718
+ finally {
1719
+ if (during === null)
1720
+ await rest(progress);
1721
+ }
1722
+ });
1723
+ }
1724
+ function observeStream() {
1725
+ const stream = options.adapter.roomStream;
1726
+ if (stream === undefined || withdrawStream !== undefined)
1727
+ return;
1728
+ withdrawStream = stream.observe((event) => {
1729
+ if (closed || stopping)
1690
1730
  return;
1731
+ if (event.kind === 'opened') {
1732
+ socketOpen = true;
1733
+ void sync('opened');
1691
1734
  }
1692
- notices.set(notification.locator, notification.roomVersion);
1693
- chain = chain.then(runNotified, runNotified);
1735
+ else if (event.kind === 'closed')
1736
+ closedSocket(event.revoked);
1737
+ else if (event.kind === 'opening')
1738
+ void applyOpening(event.expired);
1739
+ else
1740
+ void applyRoom(event.locator, event.roomVersion);
1694
1741
  });
1695
1742
  }
1696
1743
  function writesHeld() {
@@ -1711,14 +1758,17 @@ export function composeRuntime(options) {
1711
1758
  listener(mode);
1712
1759
  }
1713
1760
  if (releasing)
1714
- void enqueue();
1761
+ submitAll();
1715
1762
  }
1716
1763
  function observeConversations() {
1717
1764
  const stream = options.adapter.conversations;
1718
1765
  if (stream === undefined || withdrawConversations !== undefined)
1719
1766
  return;
1720
1767
  withdrawConversations = stream.observe((event) => {
1721
- conversations.receive(event);
1768
+ if (conversations.receive(event)) {
1769
+ void ready.then(scheduleCurrency);
1770
+ return;
1771
+ }
1722
1772
  if (event.kind !== 'gap' || conversations.current())
1723
1773
  return;
1724
1774
  show();
@@ -1729,7 +1779,7 @@ export function composeRuntime(options) {
1729
1779
  if (currencyQueued || closed || stopping)
1730
1780
  return;
1731
1781
  currencyQueued = true;
1732
- chain = chain.then(runCurrency, runCurrency);
1782
+ void task(runCurrency);
1733
1783
  }
1734
1784
  async function runCurrency() {
1735
1785
  currencyQueued = false;
@@ -1755,95 +1805,12 @@ export function composeRuntime(options) {
1755
1805
  }
1756
1806
  }
1757
1807
  }
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
1808
  function close() {
1842
1809
  if (closed)
1843
1810
  return;
1844
1811
  closed = true;
1845
- withdrawNotifications?.();
1846
- withdrawNotifications = undefined;
1812
+ withdrawStream?.();
1813
+ withdrawStream = undefined;
1847
1814
  withdrawConversations?.();
1848
1815
  withdrawConversations = undefined;
1849
1816
  withdrawServiceMode?.();
@@ -1867,6 +1834,7 @@ export function composeRuntime(options) {
1867
1834
  stopping = true;
1868
1835
  stopTimer();
1869
1836
  await chain.catch(() => undefined);
1837
+ await Promise.all([...submitting.values()].map((run) => run.done));
1870
1838
  await mutations;
1871
1839
  close();
1872
1840
  }
@@ -1875,11 +1843,29 @@ export function composeRuntime(options) {
1875
1843
  options.signal?.addEventListener('abort', close, { once: true });
1876
1844
  if (options.signal?.aborted)
1877
1845
  close();
1878
- void openLocal().catch(() => {
1846
+ observeStream();
1847
+ void openLocal().then(() => {
1848
+ if (capabilities.length > 0)
1849
+ notifyReady();
1850
+ }, () => {
1879
1851
  notifyReady();
1880
1852
  });
1881
- void enqueue();
1882
- function watch(topic, input, deliver) {
1853
+ if (options.adapter.roomStream === undefined)
1854
+ void sync('unstreamed');
1855
+ function planned(input, reach) {
1856
+ return planOf(input, roomFacts(reach), planReach(reach));
1857
+ }
1858
+ function watchPlan(plan, deliver) {
1859
+ return readSide.watch(plan, identityOf(plan), deliver);
1860
+ }
1861
+ async function searched(input, reach) {
1862
+ const plan = searchPlanOf(input);
1863
+ const selected = plan.documents === undefined && reach === null
1864
+ ? null
1865
+ : (await readSide.query(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
1866
+ return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
1867
+ }
1868
+ const watch = (topic, input, deliver) => {
1883
1869
  if (topic === 'room-drafts') {
1884
1870
  const listener = { notify: deliver, ready: false };
1885
1871
  roomDraftListeners.add(listener);
@@ -1900,6 +1886,8 @@ export function composeRuntime(options) {
1900
1886
  if (topic === 'conversation') {
1901
1887
  return conversations.watch(input[0], input[1], deliver);
1902
1888
  }
1889
+ if (topic === 'conversations')
1890
+ return conversations.watchList(deliver);
1903
1891
  if (topic === 'presentation') {
1904
1892
  const listener = {
1905
1893
  app: input[0],
@@ -1962,9 +1950,8 @@ export function composeRuntime(options) {
1962
1950
  queueSubscriptions.delete(queue);
1963
1951
  };
1964
1952
  }
1965
- const plan = planOf(input[0], roomFacts());
1966
- return readSide.watch(plan, identityOf(plan), deliver);
1967
- }
1953
+ return watchPlan(planned(input[0], null), deliver);
1954
+ };
1968
1955
  const md = {
1969
1956
  loaded() {
1970
1957
  return openLocal().then(() => undefined);
@@ -1972,15 +1959,8 @@ export function composeRuntime(options) {
1972
1959
  rooms() {
1973
1960
  return listed();
1974
1961
  },
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
- },
1962
+ query: ((input) => readSide.query(planned(input, null))),
1963
+ search: (input) => searched(input, null),
1984
1964
  types(locator) {
1985
1965
  return {
1986
1966
  collection: modelTypes(modelOf(locator)),
@@ -2087,9 +2067,7 @@ export function composeRuntime(options) {
2087
2067
  persistence() {
2088
2068
  return options.shell.persistentStorage;
2089
2069
  },
2090
- observe: ((topic, ...input) => opened((deliver) => watch(topic, input, (message) => {
2091
- deliver(structuredClone(message));
2092
- }), topic === 'query')),
2070
+ observe: observing(watch),
2093
2071
  async reauthorize() {
2094
2072
  if (refusal === null || !refusal.reauthorizationOffered)
2095
2073
  return;
@@ -2098,8 +2076,19 @@ export function composeRuntime(options) {
2098
2076
  await mutations;
2099
2077
  authorization = undefined;
2100
2078
  discoveryPaused = false;
2101
- await enqueue();
2079
+ if (options.adapter.roomStream === undefined)
2080
+ await sync('unstreamed');
2081
+ else
2082
+ await task(ensureAuthorization);
2102
2083
  },
2103
2084
  };
2104
- return { md, stop, watch };
2085
+ const reachable = {
2086
+ md,
2087
+ watch,
2088
+ plan: planned,
2089
+ query: (plan) => readSide.query(plan),
2090
+ watchPlan,
2091
+ search: searched,
2092
+ };
2093
+ return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2105
2094
  }