@agentshouse/mdruntime 0.1.0-c8c4b020a3ec7 → 0.1.0-ca7c26c6bc27e

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 (52) hide show
  1. package/AGENTS.md +13 -6
  2. package/dist/adapter.d.ts +56 -37
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/attached-model.d.ts +11 -0
  5. package/dist/attached-model.d.ts.map +1 -0
  6. package/dist/attached-model.js +34 -0
  7. package/dist/bootstrap.d.ts +5 -3
  8. package/dist/bootstrap.d.ts.map +1 -1
  9. package/dist/bootstrap.js +2 -2
  10. package/dist/connection.d.ts +3 -6
  11. package/dist/connection.d.ts.map +1 -1
  12. package/dist/connection.js +478 -468
  13. package/dist/conversation.d.ts +6 -6
  14. package/dist/conversation.d.ts.map +1 -1
  15. package/dist/conversation.js +93 -37
  16. package/dist/host.d.ts +7 -1
  17. package/dist/host.d.ts.map +1 -1
  18. package/dist/host.js +32 -21
  19. package/dist/index.d.ts +2 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/memory.d.ts +10 -3
  22. package/dist/memory.d.ts.map +1 -1
  23. package/dist/memory.js +66 -20
  24. package/dist/model.d.ts +1 -5
  25. package/dist/model.d.ts.map +1 -1
  26. package/dist/observation.d.ts +2 -0
  27. package/dist/observation.d.ts.map +1 -1
  28. package/dist/observation.js +24 -14
  29. package/dist/query.d.ts +7 -2
  30. package/dist/query.d.ts.map +1 -1
  31. package/dist/query.js +4 -3
  32. package/dist/reach.d.ts +40 -0
  33. package/dist/reach.d.ts.map +1 -0
  34. package/dist/reach.js +277 -0
  35. package/dist/read-side.d.ts +4 -0
  36. package/dist/read-side.d.ts.map +1 -1
  37. package/dist/read-side.js +16 -10
  38. package/dist/refresh.d.ts +2 -1
  39. package/dist/refresh.d.ts.map +1 -1
  40. package/dist/refresh.js +7 -3
  41. package/dist/replica.d.ts +2 -1
  42. package/dist/replica.d.ts.map +1 -1
  43. package/dist/replica.js +7 -3
  44. package/dist/room-draft.d.ts +3 -0
  45. package/dist/room-draft.d.ts.map +1 -1
  46. package/dist/room-draft.js +6 -4
  47. package/dist/store.d.ts +0 -1
  48. package/dist/store.d.ts.map +1 -1
  49. package/dist/wire.d.ts +50 -4
  50. package/dist/wire.d.ts.map +1 -1
  51. package/dist/wire.js +50 -45
  52. package/package.json +2 -2
@@ -1,6 +1,6 @@
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';
@@ -20,37 +20,22 @@ import { roomDraftRevision } from './identity.js';
20
20
  import { beneath, normalize } from './paths.js';
21
21
  import { openRuntimeStore, } from './store.js';
22
22
  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: {} } };
23
+ import { governs, holds, narrowed, planReach, } from './reach.js';
26
24
  const knownCapabilities = new Set(VERSION_1_CAPABILITIES);
27
25
  function within(root, path) {
28
26
  return root === '' || beneath(root, path);
29
27
  }
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
28
  function declaredSelection(room) {
41
29
  const selection = room.typedSelection;
42
30
  if (selection === undefined)
43
31
  return true;
44
32
  if (!Array.isArray(selection.roots) || selection.roots.length === 0)
45
33
  return false;
46
- if (!declaredIdentity(selection.attachedModel))
47
- return false;
48
34
  return selection.roots.every((root, index) => validRoot(root) &&
49
35
  root !== '' &&
50
36
  selection.roots.every((other, at) => at === index || !within(other, root)));
51
37
  }
52
38
  export function composeRuntime(options) {
53
- const period = options.discoveryIntervalMs ?? DEFAULT_DISCOVERY_INTERVAL_MS;
54
39
  let authorization = options.authorization;
55
40
  let opening;
56
41
  let store;
@@ -58,7 +43,6 @@ export function composeRuntime(options) {
58
43
  const pendings = new Map();
59
44
  const roomDrafts = new Map();
60
45
  const presentations = new Map();
61
- const wanted = new Map();
62
46
  let limits = [];
63
47
  let capabilities = [];
64
48
  function rewriting() {
@@ -71,7 +55,6 @@ export function composeRuntime(options) {
71
55
  const models = new Map();
72
56
  const liveModels = new Map();
73
57
  const invalidModels = new Set();
74
- const modelEpoch = new Map();
75
58
  const discovered = new Map();
76
59
  let roomsListed = false;
77
60
  const scopedRefusals = new Map();
@@ -100,16 +83,19 @@ export function composeRuntime(options) {
100
83
  let status = STARTING;
101
84
  let shown = STARTING;
102
85
  let discoveryPaused = false;
103
- let timer;
86
+ let resetTimer;
104
87
  let closed = false;
105
88
  let stopping = false;
106
- let withdrawNotifications;
107
- let handed;
108
- const notices = new Map();
89
+ let withdrawStream;
90
+ const announced = new Map();
91
+ let underway = null;
92
+ const submitting = new Map();
93
+ const answering = new Map();
94
+ let complete = false;
95
+ let recorded = false;
96
+ const stale = new Set();
109
97
  let chain = Promise.resolve();
110
98
  let mutations = Promise.resolve();
111
- let inflight = false;
112
- let queued = false;
113
99
  const roomListeners = new Set();
114
100
  const refusalListeners = new Set();
115
101
  const storageLossListeners = new Set();
@@ -120,6 +106,7 @@ export function composeRuntime(options) {
120
106
  const readSide = createReadSide();
121
107
  let withdrawConversations;
122
108
  let currencyQueued = false;
109
+ let socketOpen = false;
123
110
  let serviceMode = 'open';
124
111
  let haltedRefusal = false;
125
112
  let openSignals = 0;
@@ -234,11 +221,14 @@ export function composeRuntime(options) {
234
221
  function listed() {
235
222
  return structuredClone([...rooms.values()]);
236
223
  }
237
- function roomFacts() {
238
- const held = [...rooms.values()].map((room) => {
224
+ function roomFacts(reach = null) {
225
+ const held = [...rooms.values()]
226
+ .filter((room) => holds(reach, room.locator))
227
+ .map((room) => {
228
+ const governed = governs(reach, room);
239
229
  const attached = [
240
- modelOf(room.locator),
241
- ...(room.typedSelection === null ? [] : [modelOf(room.locator, 'typed-selection')]),
230
+ ...(governed.collection ? [modelOf(room.locator)] : []),
231
+ ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
242
232
  ];
243
233
  return {
244
234
  locator: room.locator,
@@ -322,17 +312,30 @@ export function composeRuntime(options) {
322
312
  }
323
313
  }
324
314
  function stopTimer() {
325
- if (timer === undefined)
315
+ if (resetTimer === undefined)
326
316
  return;
327
- clearInterval(timer);
328
- timer = undefined;
317
+ clearTimeout(resetTimer);
318
+ resetTimer = undefined;
329
319
  }
330
- function startTimer() {
331
- if (timer !== undefined)
320
+ function armTimer() {
321
+ stopTimer();
322
+ if (closed || stopping)
323
+ return;
324
+ let earliest = null;
325
+ for (const { submission } of pendings.values()) {
326
+ if (submission.state !== 'reset')
327
+ continue;
328
+ const at = Date.parse(submission.reset);
329
+ if (earliest === null || at < earliest)
330
+ earliest = at;
331
+ }
332
+ if (earliest === null)
332
333
  return;
333
- timer = setInterval(() => {
334
- void enqueue();
335
- }, period);
334
+ // A Save House held back goes again only once the reset its answer named has passed.
335
+ resetTimer = setTimeout(() => {
336
+ resetTimer = undefined;
337
+ submitAll();
338
+ }, Math.max(0, earliest - Date.now()));
336
339
  }
337
340
  async function refuse(reason, provider = null) {
338
341
  stopTimer();
@@ -358,7 +361,6 @@ export function composeRuntime(options) {
358
361
  }
359
362
  for (const key of keys) {
360
363
  invalidModels.add(key);
361
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
362
364
  models.set(key, revoked);
363
365
  liveModels.delete(key);
364
366
  }
@@ -406,13 +408,11 @@ export function composeRuntime(options) {
406
408
  locator: room.locator,
407
409
  operations: room.operations,
408
410
  replica: room.replica,
409
- attachedModel: room.attachedModel,
410
411
  model: modelTypes(modelOf(room.locator)),
411
412
  typedSelection: selection === null
412
413
  ? null
413
414
  : {
414
415
  roots: selection.roots,
415
- attachedModel: selection.attachedModel,
416
416
  model: modelTypes(modelOf(room.locator, 'typed-selection')),
417
417
  },
418
418
  providerOwnedPaths: room.providerOwnedPaths,
@@ -429,9 +429,6 @@ export function composeRuntime(options) {
429
429
  discovered.clear();
430
430
  for (const room of listed)
431
431
  discovered.set(room.locator, room);
432
- for (const locator of wanted.keys())
433
- if (!listedLocators.has(locator))
434
- wanted.delete(locator);
435
432
  await dropUnlisted(listedLocators, local);
436
433
  for (const room of listed) {
437
434
  const held = replicas.get(room.locator)?.confirmed.record;
@@ -465,6 +462,7 @@ export function composeRuntime(options) {
465
462
  rooms.delete(locator);
466
463
  roomDrafts.delete(locator);
467
464
  unreadable.delete(locator);
465
+ stale.delete(locator);
468
466
  scopedRefusals.delete(locator);
469
467
  sourceRefusals.delete(locator);
470
468
  declarationRefusals.delete(locator);
@@ -473,7 +471,6 @@ export function composeRuntime(options) {
473
471
  models.delete(key);
474
472
  liveModels.delete(key);
475
473
  invalidModels.delete(key);
476
- modelEpoch.delete(key);
477
474
  }
478
475
  journal.record(`room unlisted ${locator}`);
479
476
  }
@@ -486,7 +483,6 @@ export function composeRuntime(options) {
486
483
  if (prior !== undefined &&
487
484
  prior.status !== 'unavailable' &&
488
485
  (persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
489
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
490
486
  invalidModels.add(key);
491
487
  }
492
488
  const model = persisted.status === 'unavailable' ||
@@ -552,13 +548,11 @@ export function composeRuntime(options) {
552
548
  attachments: record.attachments,
553
549
  operations: record.operations,
554
550
  replica: record.replica,
555
- attachedModel: record.attachedModel,
556
551
  model: modelTypes(model),
557
552
  typedSelection: record.typedSelection === null
558
553
  ? null
559
554
  : {
560
555
  roots: record.typedSelection.roots,
561
- attachedModel: record.typedSelection.attachedModel,
562
556
  model: modelTypes(selectionModel),
563
557
  },
564
558
  });
@@ -669,35 +663,66 @@ export function composeRuntime(options) {
669
663
  drafts: [...written.keys()],
670
664
  });
671
665
  }
672
- function publish(local, room, confirmed) {
666
+ function adoptModels(locator, record) {
667
+ for (const [attached, model] of [
668
+ ['collection', record.model],
669
+ ['typed-selection', record.selectionModel],
670
+ ]) {
671
+ const key = modelKey(locator, attached);
672
+ models.set(key, model);
673
+ invalidModels.delete(key);
674
+ if (model.status === 'current')
675
+ liveModels.set(key, JSON.stringify(model.snapshot));
676
+ else if (model.status === 'unavailable')
677
+ liveModels.delete(key);
678
+ }
679
+ }
680
+ function remodel(locator, answers, held) {
681
+ const taken = (answer, previous, attached) => {
682
+ if (answer === undefined)
683
+ return previous;
684
+ const { state, refused } = attachedModel(answer, previous);
685
+ if (refused !== null) {
686
+ const where = refused.location === undefined ? '' : ` ${refused.location}`;
687
+ journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
688
+ }
689
+ models.set(modelKey(locator, attached), state);
690
+ return state;
691
+ };
692
+ return {
693
+ collection: taken(answers?.collection, held.collection, 'collection'),
694
+ selection: taken(answers?.selection, held.selection, 'typed-selection'),
695
+ };
696
+ }
697
+ function publish(local, room, confirmed, base) {
673
698
  return exclusive(async () => {
674
699
  if (!continuing())
675
- return;
700
+ return true;
676
701
  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}`);
702
+ if (base !== undefined &&
703
+ (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
704
+ prior.confirmed.record.revisions !== base.record.revisions)) {
705
+ return false;
706
+ }
707
+ adoptModels(room.locator, confirmed.record);
684
708
  const queue = prior?.queue ?? [];
685
- const roomVersion = retained.confirmed.record.roomVersion;
709
+ const roomVersion = confirmed.record.roomVersion;
686
710
  const accepted = queue.filter((batch) => {
687
711
  const accepted = pendings.get(batch.id)?.accepted;
688
712
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
689
713
  });
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);
714
+ const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
715
+ for (const batch of accepted)
716
+ pendings.delete(batch.id);
717
+ stale.delete(room.locator);
718
+ if (unreadable.delete(room.locator))
719
+ scopedRefusals.delete(room.locator);
696
720
  sourceRefusals.delete(room.locator);
697
721
  declarationRefusals.delete(room.locator);
698
722
  list(room, true);
699
723
  publishChangeSet(set);
700
724
  moveTo({ ...status, ...attention() });
725
+ return true;
701
726
  });
702
727
  }
703
728
  async function saveRoomDrafts(locator, named, writtenBy) {
@@ -750,6 +775,7 @@ export function composeRuntime(options) {
750
775
  return { outcome: 'refused', rule: 'durable-write' };
751
776
  }
752
777
  pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
778
+ recorded = false;
753
779
  for (const path of consumed)
754
780
  forgetRoomDraft(locator, path);
755
781
  replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting(), replica));
@@ -758,7 +784,7 @@ export function composeRuntime(options) {
758
784
  drafts: consumed,
759
785
  }));
760
786
  moveTo({ ...saving(status), ...attention() });
761
- void enqueue();
787
+ submitLater(locator);
762
788
  return { outcome: 'saved', identifier: built.batch.id };
763
789
  }
764
790
  async function mark(local, locator, identifier, submission, lost) {
@@ -857,8 +883,6 @@ export function composeRuntime(options) {
857
883
  journal.record(`transfer action ${name}`);
858
884
  try {
859
885
  const result = await options.adapter.action(authorization, { name, input });
860
- if (result.kind === 'action' && result.at !== undefined)
861
- want(result.at);
862
886
  if (result.kind === 'scoped-refusal') {
863
887
  const room = replicas.get(result.locator)?.confirmed.record;
864
888
  if (room !== undefined)
@@ -890,32 +914,11 @@ export function composeRuntime(options) {
890
914
  options.adapter.reached(locator, written, roomVersion) &&
891
915
  !options.adapter.reached(locator, roomVersion, written));
892
916
  }
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) {
917
+ async function pullRefresh(authorized, confirmed) {
915
918
  if (!continuing())
916
919
  return unconfirmed;
917
920
  try {
918
- const result = await refreshRoom(options.adapter, authorized, confirmed, continuing, minPosition);
921
+ const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
919
922
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
920
923
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
921
924
  return result;
@@ -932,7 +935,7 @@ export function composeRuntime(options) {
932
935
  const result = await bootstrapRoom(options.adapter, authorized, room, {
933
936
  collection: modelOf(room.locator),
934
937
  selection: modelOf(room.locator, 'typed-selection'),
935
- }, continuing);
938
+ }, (models, held) => remodel(room.locator, models, held), continuing);
936
939
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
937
940
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
938
941
  return result;
@@ -945,20 +948,28 @@ export function composeRuntime(options) {
945
948
  async function advance(room, local, authorized) {
946
949
  const held = replicas.get(room.locator);
947
950
  if (held !== undefined) {
948
- const refreshed = await pullRefresh(authorized, held.confirmed, reachOf(room.locator));
951
+ const refreshed = await pullRefresh(authorized, held.confirmed);
949
952
  if (!continuing())
950
953
  return unconfirmed;
951
954
  if (refreshed.kind === 'still-current') {
952
955
  if (sourceRefusals.has(room.locator))
953
956
  return unconfirmed;
957
+ stale.delete(room.locator);
954
958
  if (unreadable.delete(room.locator)) {
959
+ scopedRefusals.delete(room.locator);
955
960
  list(room, true);
956
961
  emitRooms();
957
962
  }
958
963
  return null;
959
964
  }
960
965
  if (refreshed.kind === 'advanced') {
961
- await publish(local, room, refreshed.confirmed);
966
+ if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
967
+ const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
968
+ const reached = now !== undefined &&
969
+ options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
970
+ return reached ? null : advance(room, local, authorized);
971
+ }
972
+ await retainModel(local, room, 'collection', modelOf(room.locator));
962
973
  return null;
963
974
  }
964
975
  if (refreshed.kind !== 'beyond-horizon')
@@ -973,7 +984,7 @@ export function composeRuntime(options) {
973
984
  return null;
974
985
  }
975
986
  async function withhold(withheld, room, local, progress) {
976
- progress.complete = false;
987
+ stale.add(room.locator);
977
988
  if (withheld.kind === 'scoped-refusal') {
978
989
  await scoped(withheld, room, local);
979
990
  }
@@ -1020,44 +1031,30 @@ export function composeRuntime(options) {
1020
1031
  (accepted.author === undefined ? '' : ` author ${accepted.author}`) +
1021
1032
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1022
1033
  }
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) {
1034
+ async function adoptAccepted(room, local, submitted, accepted) {
1041
1035
  if (!continuing())
1042
1036
  return unconfirmed;
1043
1037
  const current = replicas.get(room.locator);
1044
1038
  if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1045
1039
  return null;
1046
1040
  }
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')
1041
+ const held = current.confirmed.record.roomVersion;
1042
+ logAccepted(room.locator, submitted, accepted);
1043
+ const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1044
+ ? { kind: 'staged', confirmed: current.confirmed }
1045
+ : applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
1046
+ ? accepted.roomVersion
1047
+ : held, accepted.roomVersion);
1048
+ if (applied.kind !== 'staged') {
1049
+ const state = pendings.get(submitted.id);
1050
+ await local.accept(room.locator, submitted.id, accepted.roomVersion);
1051
+ if (state !== undefined)
1052
+ pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1053
+ emitQueue(room.locator);
1057
1054
  return applied;
1058
- const set = await settle(local, room.locator, current, applied.confirmed, remaining, {
1059
- room: true,
1060
- });
1055
+ }
1056
+ const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1057
+ pendings.delete(submitted.id);
1061
1058
  unreadable.delete(room.locator);
1062
1059
  sourceRefusals.delete(room.locator);
1063
1060
  declarationRefusals.delete(room.locator);
@@ -1066,17 +1063,8 @@ export function composeRuntime(options) {
1066
1063
  moveTo({ ...status, ...attention() });
1067
1064
  return null;
1068
1065
  }
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));
1066
+ async function adopted(room, local, submitted, accepted) {
1067
+ const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
1080
1068
  if (failed === null)
1081
1069
  return true;
1082
1070
  if (failed.kind === 'refused' && continuing())
@@ -1089,10 +1077,8 @@ export function composeRuntime(options) {
1089
1077
  const head = replica?.queue[0];
1090
1078
  if (replica === undefined || head === undefined)
1091
1079
  return;
1092
- if (pendings.get(head.id)?.accepted !== undefined) {
1093
- progress.complete = false;
1080
+ if (pendings.get(head.id)?.accepted !== undefined)
1094
1081
  return;
1095
- }
1096
1082
  const state = pendings.get(head.id);
1097
1083
  const { submission, lost: earlier } = state ?? {
1098
1084
  submission: { state: 'waiting' },
@@ -1120,6 +1106,7 @@ export function composeRuntime(options) {
1120
1106
  return;
1121
1107
  }
1122
1108
  const opensBefore = openSignals;
1109
+ const answered = awaitingAnswer(room.locator);
1123
1110
  try {
1124
1111
  result = await options.adapter.mutate(authorized, {
1125
1112
  locator: room.locator,
@@ -1128,11 +1115,14 @@ export function composeRuntime(options) {
1128
1115
  }, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
1129
1116
  }
1130
1117
  catch {
1118
+ answered();
1131
1119
  progress.complete = false;
1132
1120
  if (continuing())
1133
1121
  progress.reachable = false;
1134
1122
  return;
1135
1123
  }
1124
+ if (result.kind !== 'accepted' || !continuing())
1125
+ answered();
1136
1126
  if (!continuing())
1137
1127
  return;
1138
1128
  if (result.kind === 'transport-uncertainty') {
@@ -1185,6 +1175,7 @@ export function composeRuntime(options) {
1185
1175
  const reset = result.reset;
1186
1176
  progress.complete = false;
1187
1177
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1178
+ armTimer();
1188
1179
  return;
1189
1180
  }
1190
1181
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
@@ -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);
1205
+ const kept = await adopted(room, local, head, result).finally(answered);
1206
+ if (!kept) {
1227
1207
  progress.complete = false;
1228
1208
  return;
1229
1209
  }
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
- }
1258
- 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
- return;
1271
- }
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
  };
@@ -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,34 @@ 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
+ const loaded = await Promise.allSettled(ready
1343
+ .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1344
+ .map((room) => loadRoom(room, local, progress)));
1345
+ const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1471
1346
  if (failed !== undefined)
1472
1347
  throw failed.reason;
1473
1348
  }
@@ -1484,11 +1359,8 @@ export function composeRuntime(options) {
1484
1359
  progress.complete = false;
1485
1360
  return false;
1486
1361
  }
1487
- const selection = room.typedSelection;
1488
- await Promise.all([
1489
- acquireModel(local, room, authorization, null),
1490
- ...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
1491
- ]);
1362
+ if (prior !== undefined)
1363
+ await retainModel(local, room, 'collection', modelOf(room.locator));
1492
1364
  if (discoveryPaused || authorization === undefined)
1493
1365
  return false;
1494
1366
  if (room.replica.admission !== 'admitted') {
@@ -1496,24 +1368,53 @@ export function composeRuntime(options) {
1496
1368
  progress.complete = false;
1497
1369
  return false;
1498
1370
  }
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
1371
  return true;
1505
1372
  }
1506
- async function synchronizeRoom(room, local, progress) {
1373
+ async function caughtUp(room, local, progress) {
1374
+ const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1375
+ if (held === undefined || authorization === undefined)
1376
+ return;
1377
+ const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1378
+ if (!behind)
1379
+ return;
1380
+ const withheld = await advance(room, local, authorization);
1381
+ if (withheld !== null)
1382
+ await withhold(withheld, room, local, progress);
1383
+ }
1384
+ function revoked(locator) {
1385
+ return ['collection', 'typed-selection'].some((attached) => {
1386
+ const model = modelOf(locator, attached);
1387
+ return model.status === 'unavailable' && model.reason === 'revoked';
1388
+ });
1389
+ }
1390
+ async function loadRoom(room, local, progress) {
1507
1391
  if (discoveryPaused || authorization === undefined)
1508
1392
  return;
1393
+ if (replicas.has(room.locator) && revoked(room.locator)) {
1394
+ await reload(room, local, progress);
1395
+ return;
1396
+ }
1509
1397
  const withheld = await advance(room, local, authorization);
1510
1398
  if (withheld !== null) {
1511
1399
  await withhold(withheld, room, local, progress);
1512
1400
  return;
1513
1401
  }
1514
- await submitRoom(room, local, authorization, progress);
1402
+ await caughtUp(room, local, progress);
1515
1403
  }
1516
- async function cycle(local, progress) {
1404
+ async function reload(room, local, progress) {
1405
+ if (discoveryPaused || authorization === undefined)
1406
+ return;
1407
+ const bootstrapped = await pullBootstrap(authorization, room);
1408
+ if (!continuing())
1409
+ return;
1410
+ if (bootstrapped.kind !== 'staged') {
1411
+ await withhold(bootstrapped, room, local, progress);
1412
+ return;
1413
+ }
1414
+ await publish(local, room, bootstrapped.confirmed);
1415
+ await caughtUp(room, local, progress);
1416
+ }
1417
+ async function discoverRooms(local, progress, attempt) {
1517
1418
  if (closed || authorization === undefined) {
1518
1419
  notifyReady();
1519
1420
  return;
@@ -1539,6 +1440,10 @@ export function composeRuntime(options) {
1539
1440
  }
1540
1441
  }
1541
1442
  progress.reachable = false;
1443
+ progress.complete = false;
1444
+ if (attempt === 'opened') {
1445
+ options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1446
+ }
1542
1447
  notifyReady();
1543
1448
  return;
1544
1449
  }
@@ -1571,8 +1476,7 @@ export function composeRuntime(options) {
1571
1476
  room.replica === undefined ||
1572
1477
  typeof room.replica.selection !== 'string' ||
1573
1478
  room.replica.selection.length === 0 ||
1574
- !['admitted', 'unavailable'].includes(room.replica.admission) ||
1575
- !declaredIdentity(room.attachedModel)) {
1479
+ !['admitted', 'unavailable'].includes(room.replica.admission)) {
1576
1480
  journal.record(`invalid model or replica declaration ${room.locator}`);
1577
1481
  await refuse('missing-required-guarantee');
1578
1482
  notifyReady();
@@ -1600,26 +1504,13 @@ export function composeRuntime(options) {
1600
1504
  const previousCapabilities = capabilities;
1601
1505
  capabilities = result.capabilities;
1602
1506
  notifyReady();
1603
- if (declared.has('room-version-notifications'))
1604
- observeRoomVersions();
1605
1507
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1606
1508
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
1607
1509
  limits = result.providerLimits;
1608
1510
  await local.declare(limits, capabilities);
1609
1511
  }
1610
1512
  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);
1513
+ await synchronize(result.rooms, local, progress, attempt);
1623
1514
  }
1624
1515
  async function ensureAuthorization() {
1625
1516
  if (authorization !== undefined)
@@ -1633,64 +1524,246 @@ export function composeRuntime(options) {
1633
1524
  throw error;
1634
1525
  }
1635
1526
  }
1636
- async function connect() {
1637
- if (closed || stopping || discoveryPaused) {
1638
- notifyReady();
1527
+ function task(work) {
1528
+ if (stopping)
1529
+ return chain;
1530
+ chain = chain.then(work, work).catch(() => undefined);
1531
+ return chain;
1532
+ }
1533
+ function sync(attempt) {
1534
+ return task(async () => {
1535
+ if (closed || stopping || discoveryPaused) {
1536
+ notifyReady();
1537
+ return;
1538
+ }
1539
+ const progress = { complete: true, reachable: true };
1540
+ underway = attempt === 'opened' ? progress : null;
1541
+ complete = false;
1542
+ recorded = false;
1543
+ moveTo(transferring(status));
1544
+ try {
1545
+ const local = await openLocal();
1546
+ if (closed)
1547
+ return;
1548
+ await ensureAuthorization();
1549
+ if (closed)
1550
+ return;
1551
+ await discoverRooms(local, progress, attempt);
1552
+ }
1553
+ catch {
1554
+ progress.complete = false;
1555
+ if (attempt === 'opened') {
1556
+ options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1557
+ }
1558
+ }
1559
+ finally {
1560
+ notifyReady();
1561
+ }
1562
+ if (attempt === 'opened')
1563
+ return;
1564
+ await finishOpening(progress);
1565
+ });
1566
+ }
1567
+ function closedSocket(revoked) {
1568
+ const opened = socketOpen;
1569
+ socketOpen = false;
1570
+ if (revoked) {
1571
+ void refuse('revoked-authorization');
1639
1572
  return;
1640
1573
  }
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)
1574
+ if (opened || status.providerUnreachable)
1575
+ return;
1576
+ moveTo({ ...status, providerUnreachable: true });
1577
+ }
1578
+ function applyOpening(expired) {
1579
+ return task(async () => {
1580
+ const progress = underway;
1581
+ underway = null;
1582
+ if (closed || stopping)
1650
1583
  return;
1651
- await cycle(local, progress);
1652
- if (closed)
1584
+ if (progress === null) {
1585
+ await reloadExpired(expired);
1653
1586
  return;
1654
- if (progress.complete) {
1655
- completedAt = new Date().toISOString();
1656
- await local.complete(completedAt);
1657
1587
  }
1588
+ try {
1589
+ const local = await openLocal();
1590
+ const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1591
+ const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1592
+ const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
1593
+ if (progress.reachable) {
1594
+ for (const room of held)
1595
+ if (!reloading.includes(room))
1596
+ await confirmModels(room, local);
1597
+ }
1598
+ const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1599
+ if (failed !== undefined)
1600
+ throw failed.reason;
1601
+ }
1602
+ catch {
1603
+ progress.complete = false;
1604
+ progress.reachable = false;
1605
+ }
1606
+ await finishOpening(progress);
1607
+ });
1608
+ }
1609
+ async function reloadExpired(expired) {
1610
+ const progress = { complete: true, reachable: true };
1611
+ try {
1612
+ const local = await openLocal();
1613
+ await Promise.allSettled([...discovered.values()]
1614
+ .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1615
+ .map((room) => reload(room, local, progress)));
1658
1616
  }
1659
1617
  catch {
1660
- progress.complete = false;
1661
- notifyReady();
1618
+ progress.reachable = false;
1619
+ }
1620
+ await rest(progress);
1621
+ }
1622
+ async function confirmModels(room, local) {
1623
+ for (const attached of ['collection', 'typed-selection']) {
1624
+ const model = modelOf(room.locator, attached);
1625
+ if (model.status === 'cached') {
1626
+ await retainModel(local, room, attached, { ...model, status: 'current' });
1627
+ }
1628
+ }
1629
+ }
1630
+ function submitLater(locator) {
1631
+ const running = submitting.get(locator);
1632
+ if (running !== undefined) {
1633
+ running.again = true;
1662
1634
  return;
1663
1635
  }
1664
- finally {
1665
- notifyReady();
1666
- moveTo(settled(status, progress, completedAt, pending(), attention()));
1636
+ const run = { again: false, done: Promise.resolve() };
1637
+ submitting.set(locator, run);
1638
+ run.done = (async () => {
1639
+ try {
1640
+ do {
1641
+ run.again = false;
1642
+ const room = discovered.get(locator);
1643
+ if (closed || stopping || discoveryPaused || room === undefined)
1644
+ return;
1645
+ const progress = { complete: true, reachable: true };
1646
+ try {
1647
+ await ensureAuthorization();
1648
+ if (authorization === undefined)
1649
+ return;
1650
+ await submitRoom(room, await openLocal(), authorization, progress);
1651
+ }
1652
+ catch {
1653
+ progress.reachable = false;
1654
+ }
1655
+ armTimer();
1656
+ await rest(progress);
1657
+ } while (run.again);
1658
+ }
1659
+ finally {
1660
+ submitting.delete(locator);
1661
+ }
1662
+ })();
1663
+ }
1664
+ function submitAll() {
1665
+ for (const [locator, replica] of replicas) {
1666
+ if (replica.queue.length > 0)
1667
+ submitLater(locator);
1667
1668
  }
1668
1669
  }
1669
- async function run() {
1670
+ async function finishOpening(progress) {
1670
1671
  try {
1671
- await connect();
1672
+ if (closed || discoveryPaused) {
1673
+ progress.complete = false;
1674
+ return;
1675
+ }
1676
+ submitAll();
1677
+ await conversations.reconcile();
1678
+ if (status.providerUnreachable)
1679
+ conversations.reconnected();
1680
+ const reading = { complete: true, reachable: true };
1681
+ await conversations.catchUp(reading);
1682
+ if (!reading.reachable)
1683
+ progress.reachable = false;
1684
+ }
1685
+ catch {
1686
+ progress.complete = false;
1672
1687
  }
1673
1688
  finally {
1674
- inflight = false;
1675
- if (queued && !closed) {
1676
- queued = false;
1677
- void enqueue();
1678
- }
1689
+ complete = progress.complete;
1690
+ await rest(progress);
1679
1691
  }
1680
1692
  }
1681
- function observeRoomVersions() {
1682
- const path = options.adapter.roomVersions;
1683
- if (path === undefined || withdrawNotifications !== undefined)
1693
+ async function rest(progress) {
1694
+ if (closed)
1684
1695
  return;
1685
- withdrawNotifications = path.observe((notification) => {
1686
- if (closed || stopping || discoveryPaused)
1696
+ let lastCompleted = status.lastCompleted;
1697
+ let synced = complete && stale.size === 0 && !pending();
1698
+ if (synced && !recorded) {
1699
+ const completedAt = new Date().toISOString();
1700
+ try {
1701
+ await (await openLocal()).complete(completedAt);
1702
+ recorded = true;
1703
+ lastCompleted = completedAt;
1704
+ }
1705
+ catch {
1706
+ synced = false;
1707
+ }
1708
+ }
1709
+ moveTo({
1710
+ state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
1711
+ lastCompleted,
1712
+ providerUnreachable: !progress.reachable,
1713
+ ...attention(),
1714
+ });
1715
+ }
1716
+ function applyRoom(locator, roomVersion) {
1717
+ announced.set(locator, roomVersion);
1718
+ return task(async () => {
1719
+ await answering.get(locator);
1720
+ if (closed || stopping || discoveryPaused || authorization === undefined)
1721
+ return;
1722
+ const room = discovered.get(locator);
1723
+ const held = replicas.get(locator);
1724
+ if (room === undefined || room.replica.admission !== 'admitted')
1687
1725
  return;
1688
- if (notification.modelChanged === true && handed?.includes(notification.locator) === true) {
1689
- void enqueue();
1726
+ if (held !== undefined &&
1727
+ (held.confirmed.record.replica.selection !== room.replica.selection ||
1728
+ options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
1690
1729
  return;
1691
1730
  }
1692
- notices.set(notification.locator, notification.roomVersion);
1693
- chain = chain.then(runNotified, runNotified);
1731
+ const during = underway;
1732
+ const progress = during ?? { complete: true, reachable: true };
1733
+ if (during === null)
1734
+ moveTo(transferring(status));
1735
+ try {
1736
+ const local = await openLocal();
1737
+ const withheld = await advance(room, local, authorization);
1738
+ if (withheld !== null)
1739
+ await withhold(withheld, room, local, progress);
1740
+ }
1741
+ catch {
1742
+ stale.add(locator);
1743
+ }
1744
+ finally {
1745
+ if (during === null)
1746
+ await rest(progress);
1747
+ }
1748
+ });
1749
+ }
1750
+ function observeStream() {
1751
+ const stream = options.adapter.roomStream;
1752
+ if (stream === undefined || withdrawStream !== undefined)
1753
+ return;
1754
+ withdrawStream = stream.observe((event) => {
1755
+ if (closed || stopping)
1756
+ return;
1757
+ if (event.kind === 'opened') {
1758
+ socketOpen = true;
1759
+ void sync('opened');
1760
+ }
1761
+ else if (event.kind === 'closed')
1762
+ closedSocket(event.revoked);
1763
+ else if (event.kind === 'opening')
1764
+ void applyOpening(event.expired);
1765
+ else
1766
+ void applyRoom(event.locator, event.roomVersion);
1694
1767
  });
1695
1768
  }
1696
1769
  function writesHeld() {
@@ -1711,7 +1784,7 @@ export function composeRuntime(options) {
1711
1784
  listener(mode);
1712
1785
  }
1713
1786
  if (releasing)
1714
- void enqueue();
1787
+ submitAll();
1715
1788
  }
1716
1789
  function observeConversations() {
1717
1790
  const stream = options.adapter.conversations;
@@ -1729,7 +1802,7 @@ export function composeRuntime(options) {
1729
1802
  if (currencyQueued || closed || stopping)
1730
1803
  return;
1731
1804
  currencyQueued = true;
1732
- chain = chain.then(runCurrency, runCurrency);
1805
+ void task(runCurrency);
1733
1806
  }
1734
1807
  async function runCurrency() {
1735
1808
  currencyQueued = false;
@@ -1755,95 +1828,12 @@ export function composeRuntime(options) {
1755
1828
  }
1756
1829
  }
1757
1830
  }
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
1831
  function close() {
1842
1832
  if (closed)
1843
1833
  return;
1844
1834
  closed = true;
1845
- withdrawNotifications?.();
1846
- withdrawNotifications = undefined;
1835
+ withdrawStream?.();
1836
+ withdrawStream = undefined;
1847
1837
  withdrawConversations?.();
1848
1838
  withdrawConversations = undefined;
1849
1839
  withdrawServiceMode?.();
@@ -1867,6 +1857,7 @@ export function composeRuntime(options) {
1867
1857
  stopping = true;
1868
1858
  stopTimer();
1869
1859
  await chain.catch(() => undefined);
1860
+ await Promise.all([...submitting.values()].map((run) => run.done));
1870
1861
  await mutations;
1871
1862
  close();
1872
1863
  }
@@ -1875,11 +1866,29 @@ export function composeRuntime(options) {
1875
1866
  options.signal?.addEventListener('abort', close, { once: true });
1876
1867
  if (options.signal?.aborted)
1877
1868
  close();
1878
- void openLocal().catch(() => {
1869
+ observeStream();
1870
+ void openLocal().then(() => {
1871
+ if (capabilities.length > 0)
1872
+ notifyReady();
1873
+ }, () => {
1879
1874
  notifyReady();
1880
1875
  });
1881
- void enqueue();
1882
- function watch(topic, input, deliver) {
1876
+ if (options.adapter.roomStream === undefined)
1877
+ void sync('unstreamed');
1878
+ function planned(input, reach) {
1879
+ return planOf(input, roomFacts(reach), planReach(reach));
1880
+ }
1881
+ function watchPlan(plan, deliver) {
1882
+ return readSide.watch(plan, identityOf(plan), deliver);
1883
+ }
1884
+ async function searched(input, reach) {
1885
+ const plan = searchPlanOf(input);
1886
+ const selected = plan.documents === undefined && reach === null
1887
+ ? null
1888
+ : (await readSide.query(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
1889
+ return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
1890
+ }
1891
+ const watch = (topic, input, deliver) => {
1883
1892
  if (topic === 'room-drafts') {
1884
1893
  const listener = { notify: deliver, ready: false };
1885
1894
  roomDraftListeners.add(listener);
@@ -1962,9 +1971,8 @@ export function composeRuntime(options) {
1962
1971
  queueSubscriptions.delete(queue);
1963
1972
  };
1964
1973
  }
1965
- const plan = planOf(input[0], roomFacts());
1966
- return readSide.watch(plan, identityOf(plan), deliver);
1967
- }
1974
+ return watchPlan(planned(input[0], null), deliver);
1975
+ };
1968
1976
  const md = {
1969
1977
  loaded() {
1970
1978
  return openLocal().then(() => undefined);
@@ -1972,15 +1980,8 @@ export function composeRuntime(options) {
1972
1980
  rooms() {
1973
1981
  return listed();
1974
1982
  },
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
- },
1983
+ query: ((input) => readSide.query(planned(input, null))),
1984
+ search: (input) => searched(input, null),
1984
1985
  types(locator) {
1985
1986
  return {
1986
1987
  collection: modelTypes(modelOf(locator)),
@@ -2087,9 +2088,7 @@ export function composeRuntime(options) {
2087
2088
  persistence() {
2088
2089
  return options.shell.persistentStorage;
2089
2090
  },
2090
- observe: ((topic, ...input) => opened((deliver) => watch(topic, input, (message) => {
2091
- deliver(structuredClone(message));
2092
- }), topic === 'query')),
2091
+ observe: observing(watch),
2093
2092
  async reauthorize() {
2094
2093
  if (refusal === null || !refusal.reauthorizationOffered)
2095
2094
  return;
@@ -2098,8 +2097,19 @@ export function composeRuntime(options) {
2098
2097
  await mutations;
2099
2098
  authorization = undefined;
2100
2099
  discoveryPaused = false;
2101
- await enqueue();
2100
+ if (options.adapter.roomStream === undefined)
2101
+ await sync('unstreamed');
2102
+ else
2103
+ await task(ensureAuthorization);
2102
2104
  },
2103
2105
  };
2104
- return { md, stop, watch };
2106
+ const reachable = {
2107
+ md,
2108
+ watch,
2109
+ plan: planned,
2110
+ query: (plan) => readSide.query(plan),
2111
+ watchPlan,
2112
+ search: searched,
2113
+ };
2114
+ return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2105
2115
  }