@agentshouse/mdruntime 0.1.0-c3903a68dcbbf → 0.1.0-c7b0ab1aa40a3

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 (75) hide show
  1. package/AGENTS.md +51 -29
  2. package/README.md +11 -11
  3. package/dist/adapter.d.ts +78 -51
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/adapter.js +1 -9
  6. package/dist/attached-model.d.ts +11 -0
  7. package/dist/attached-model.d.ts.map +1 -0
  8. package/dist/attached-model.js +34 -0
  9. package/dist/bootstrap.d.ts +5 -3
  10. package/dist/bootstrap.d.ts.map +1 -1
  11. package/dist/bootstrap.js +2 -3
  12. package/dist/change-set.d.ts.map +1 -1
  13. package/dist/change-set.js +2 -6
  14. package/dist/client.d.ts +4 -9
  15. package/dist/client.d.ts.map +1 -1
  16. package/dist/client.js +3 -16
  17. package/dist/collection.d.ts +0 -2
  18. package/dist/collection.d.ts.map +1 -1
  19. package/dist/collection.js +2 -12
  20. package/dist/connection.d.ts +10 -28
  21. package/dist/connection.d.ts.map +1 -1
  22. package/dist/connection.js +516 -639
  23. package/dist/conversation.d.ts +22 -11
  24. package/dist/conversation.d.ts.map +1 -1
  25. package/dist/conversation.js +250 -105
  26. package/dist/host.d.ts +7 -2
  27. package/dist/host.d.ts.map +1 -1
  28. package/dist/host.js +37 -57
  29. package/dist/index.d.ts +4 -5
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +0 -1
  32. package/dist/memory.d.ts +11 -5
  33. package/dist/memory.d.ts.map +1 -1
  34. package/dist/memory.js +77 -27
  35. package/dist/model.d.ts +4 -11
  36. package/dist/model.d.ts.map +1 -1
  37. package/dist/model.js +4 -3
  38. package/dist/observation.d.ts +2 -0
  39. package/dist/observation.d.ts.map +1 -1
  40. package/dist/observation.js +24 -14
  41. package/dist/outcome.d.ts +5 -21
  42. package/dist/outcome.d.ts.map +1 -1
  43. package/dist/query.d.ts +7 -2
  44. package/dist/query.d.ts.map +1 -1
  45. package/dist/query.js +29 -20
  46. package/dist/reach.d.ts +40 -0
  47. package/dist/reach.d.ts.map +1 -0
  48. package/dist/reach.js +270 -0
  49. package/dist/react.d.ts +31 -0
  50. package/dist/react.d.ts.map +1 -0
  51. package/dist/react.js +62 -0
  52. package/dist/read-side.d.ts +4 -0
  53. package/dist/read-side.d.ts.map +1 -1
  54. package/dist/read-side.js +16 -10
  55. package/dist/refresh.d.ts +2 -1
  56. package/dist/refresh.d.ts.map +1 -1
  57. package/dist/refresh.js +7 -3
  58. package/dist/replica.d.ts +4 -4
  59. package/dist/replica.d.ts.map +1 -1
  60. package/dist/replica.js +14 -19
  61. package/dist/room-draft.d.ts +3 -0
  62. package/dist/room-draft.d.ts.map +1 -1
  63. package/dist/room-draft.js +6 -4
  64. package/dist/rows.d.ts.map +1 -1
  65. package/dist/rows.js +1 -4
  66. package/dist/save.d.ts +0 -2
  67. package/dist/save.d.ts.map +1 -1
  68. package/dist/save.js +5 -11
  69. package/dist/store.d.ts +5 -16
  70. package/dist/store.d.ts.map +1 -1
  71. package/dist/store.js +10 -32
  72. package/dist/wire.d.ts +51 -16
  73. package/dist/wire.d.ts.map +1 -1
  74. package/dist/wire.js +50 -65
  75. 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,52 +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
- function declaredUntypedFolders(room) {
53
- const roots = room.untypedFolders;
54
- if (roots === undefined)
55
- return true;
56
- if (!Array.isArray(roots) || roots.length === 0)
57
- return false;
58
- const selected = room.typedSelection?.roots ?? [];
59
- return roots.every((root, index) => validRoot(root) &&
60
- root !== '' &&
61
- roots.every((other, at) => at === index || !within(other, root)) &&
62
- selected.every((other) => !within(other, root) && !within(root, other)));
63
- }
64
- export function createMdRuntime(options) {
65
- return composeRuntime(options).md;
66
- }
67
36
  export function composeRuntime(options) {
68
- const period = options.discoveryIntervalMs ?? DEFAULT_DISCOVERY_INTERVAL_MS;
69
37
  let authorization = options.authorization;
70
38
  let opening;
71
39
  let store;
@@ -73,23 +41,13 @@ export function composeRuntime(options) {
73
41
  const pendings = new Map();
74
42
  const roomDrafts = new Map();
75
43
  const presentations = new Map();
76
- const outcomes = new Map();
77
- const outcomeRooms = new Map();
78
- const wanted = new Map();
79
44
  let limits = [];
80
- let slots = [];
81
45
  let capabilities = [];
82
- function rewriting() {
83
- return capabilities.some((capability) => capability.name === 'move-reference-rewrites')
84
- ? 'provider'
85
- : 'runtime';
86
- }
87
46
  const rooms = new Map();
88
47
  const unreadable = new Set();
89
48
  const models = new Map();
90
49
  const liveModels = new Map();
91
50
  const invalidModels = new Set();
92
- const modelEpoch = new Map();
93
51
  const discovered = new Map();
94
52
  let roomsListed = false;
95
53
  const scopedRefusals = new Map();
@@ -118,16 +76,19 @@ export function composeRuntime(options) {
118
76
  let status = STARTING;
119
77
  let shown = STARTING;
120
78
  let discoveryPaused = false;
121
- let timer;
79
+ let resetTimer;
122
80
  let closed = false;
123
81
  let stopping = false;
124
- let withdrawNotifications;
125
- let handed;
126
- 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();
127
90
  let chain = Promise.resolve();
128
91
  let mutations = Promise.resolve();
129
- let inflight = false;
130
- let queued = false;
131
92
  const roomListeners = new Set();
132
93
  const refusalListeners = new Set();
133
94
  const storageLossListeners = new Set();
@@ -138,12 +99,13 @@ export function composeRuntime(options) {
138
99
  const readSide = createReadSide();
139
100
  let withdrawConversations;
140
101
  let currencyQueued = false;
102
+ let socketOpen = false;
141
103
  let serviceMode = 'open';
142
104
  let haltedRefusal = false;
143
105
  let openSignals = 0;
144
106
  const serviceModeListeners = new Set();
145
107
  const conversations = createConversationStore({
146
- cards: declaredCards,
108
+ agents: declaredAgents,
147
109
  documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
148
110
  call: (name, input) => invokeAction(name, input),
149
111
  durably,
@@ -252,20 +214,14 @@ export function composeRuntime(options) {
252
214
  function listed() {
253
215
  return structuredClone([...rooms.values()]);
254
216
  }
255
- function slotted() {
256
- return structuredClone(slots.map(({ slot, entries }) => ({
257
- slot,
258
- entries: entries.flatMap(({ locator, segment }) => {
259
- const room = rooms.get(locator);
260
- return room === undefined ? [] : [{ room, segment }];
261
- }),
262
- })));
263
- }
264
- function roomFacts() {
265
- 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);
266
222
  const attached = [
267
- modelOf(room.locator),
268
- ...(room.typedSelection === null ? [] : [modelOf(room.locator, 'typed-selection')]),
223
+ ...(governed.collection ? [modelOf(room.locator)] : []),
224
+ ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
269
225
  ];
270
226
  return {
271
227
  locator: room.locator,
@@ -306,14 +262,18 @@ export function composeRuntime(options) {
306
262
  for (const listener of statusListeners)
307
263
  listener(shown);
308
264
  }
309
- function submissionOf(batch) {
310
- return pendings.get(batch.id)?.submission ?? { state: 'waiting' };
265
+ function entryOf(batch) {
266
+ const state = pendings.get(batch.id);
267
+ if (state?.accepted !== undefined) {
268
+ return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
269
+ }
270
+ return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
311
271
  }
312
272
  function entriesOf(locator) {
313
273
  const replica = replicas.get(locator);
314
274
  if (replica === undefined)
315
275
  return [];
316
- return replica.queue.map((batch) => Object.freeze({ batch, ...submissionOf(batch) }));
276
+ return replica.queue.map(entryOf);
317
277
  }
318
278
  function emitQueue(locator) {
319
279
  const entries = entriesOf(locator);
@@ -339,23 +299,32 @@ export function composeRuntime(options) {
339
299
  for (const app of set.presentations)
340
300
  emitPresentation(app);
341
301
  conversations.changed(set);
342
- const cards = declaredCards();
343
- if (cards !== undefined && set.documents.some((change) => change.locator === cards.locator)) {
344
- scheduleCurrency();
345
- }
346
302
  }
347
303
  function stopTimer() {
348
- if (timer === undefined)
304
+ if (resetTimer === undefined)
349
305
  return;
350
- clearInterval(timer);
351
- timer = undefined;
306
+ clearTimeout(resetTimer);
307
+ resetTimer = undefined;
352
308
  }
353
- function startTimer() {
354
- if (timer !== undefined)
309
+ function armTimer() {
310
+ stopTimer();
311
+ if (closed || stopping)
355
312
  return;
356
- timer = setInterval(() => {
357
- void enqueue();
358
- }, period);
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)
322
+ return;
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()));
359
328
  }
360
329
  async function refuse(reason, provider = null) {
361
330
  stopTimer();
@@ -381,7 +350,6 @@ export function composeRuntime(options) {
381
350
  }
382
351
  for (const key of keys) {
383
352
  invalidModels.add(key);
384
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
385
353
  models.set(key, revoked);
386
354
  liveModels.delete(key);
387
355
  }
@@ -396,7 +364,7 @@ export function composeRuntime(options) {
396
364
  };
397
365
  const read = readConfirmed(record, before.confirmed);
398
366
  replicas.set(locator, read.kind === 'staged'
399
- ? replicaOf(read.confirmed, before.queue, rewriting(), before)
367
+ ? replicaOf(read.confirmed, before.queue, before)
400
368
  : { ...before, confirmed: { ...before.confirmed, record } });
401
369
  }
402
370
  rooms.set(locator, {
@@ -429,16 +397,13 @@ export function composeRuntime(options) {
429
397
  locator: room.locator,
430
398
  operations: room.operations,
431
399
  replica: room.replica,
432
- attachedModel: room.attachedModel,
433
400
  model: modelTypes(modelOf(room.locator)),
434
401
  typedSelection: selection === null
435
402
  ? null
436
403
  : {
437
404
  roots: selection.roots,
438
- attachedModel: selection.attachedModel,
439
405
  model: modelTypes(modelOf(room.locator, 'typed-selection')),
440
406
  },
441
- untypedFolders: room.untypedFolders ?? [],
442
407
  providerOwnedPaths: room.providerOwnedPaths,
443
408
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
444
409
  attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
@@ -453,9 +418,6 @@ export function composeRuntime(options) {
453
418
  discovered.clear();
454
419
  for (const room of listed)
455
420
  discovered.set(room.locator, room);
456
- for (const locator of wanted.keys())
457
- if (!listedLocators.has(locator))
458
- wanted.delete(locator);
459
421
  await dropUnlisted(listedLocators, local);
460
422
  for (const room of listed) {
461
423
  const held = replicas.get(room.locator)?.confirmed.record;
@@ -477,12 +439,6 @@ export function composeRuntime(options) {
477
439
  await exclusive(async () => {
478
440
  const unlisted = unlistedHeld(listed);
479
441
  await local.dropRooms(unlisted);
480
- for (const [batch, locator] of outcomeRooms) {
481
- if (listed.has(locator))
482
- continue;
483
- outcomeRooms.delete(batch);
484
- outcomes.delete(batch);
485
- }
486
442
  const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
487
443
  room: true,
488
444
  queue: true,
@@ -495,6 +451,7 @@ export function composeRuntime(options) {
495
451
  rooms.delete(locator);
496
452
  roomDrafts.delete(locator);
497
453
  unreadable.delete(locator);
454
+ stale.delete(locator);
498
455
  scopedRefusals.delete(locator);
499
456
  sourceRefusals.delete(locator);
500
457
  declarationRefusals.delete(locator);
@@ -503,7 +460,6 @@ export function composeRuntime(options) {
503
460
  models.delete(key);
504
461
  liveModels.delete(key);
505
462
  invalidModels.delete(key);
506
- modelEpoch.delete(key);
507
463
  }
508
464
  journal.record(`room unlisted ${locator}`);
509
465
  }
@@ -516,7 +472,6 @@ export function composeRuntime(options) {
516
472
  if (prior !== undefined &&
517
473
  prior.status !== 'unavailable' &&
518
474
  (persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
519
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
520
475
  invalidModels.add(key);
521
476
  }
522
477
  const model = persisted.status === 'unavailable' ||
@@ -531,10 +486,9 @@ export function composeRuntime(options) {
531
486
  return model;
532
487
  }
533
488
  async function hydrate(opened) {
534
- const [records, queues, recordedOutcomes, kept, presented, held, chatDrafts, lastCompleted, declared, named, listedSlots,] = await Promise.all([
489
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
535
490
  opened.all(),
536
491
  opened.queues(),
537
- opened.outcomes(),
538
492
  opened.roomDrafts(),
539
493
  opened.presentation(),
540
494
  opened.conversations(),
@@ -542,11 +496,9 @@ export function composeRuntime(options) {
542
496
  opened.lastCompleted(),
543
497
  opened.providerLimits(),
544
498
  opened.capabilities(),
545
- opened.slots(),
546
499
  ]);
547
500
  limits = declared;
548
501
  capabilities = named;
549
- slots = listedSlots;
550
502
  roomDrafts.clear();
551
503
  for (const record of kept)
552
504
  rememberRoomDraft(record);
@@ -560,12 +512,6 @@ export function composeRuntime(options) {
560
512
  for (const { batch, state } of queue.pending)
561
513
  pendings.set(batch.id, state);
562
514
  }
563
- outcomes.clear();
564
- outcomeRooms.clear();
565
- for (const record of recordedOutcomes) {
566
- outcomes.set(record.id, record.outcome);
567
- outcomeRooms.set(record.id, record.locator);
568
- }
569
515
  const previousReplicas = new Map(replicas);
570
516
  replicas.clear();
571
517
  for (const record of records) {
@@ -580,7 +526,7 @@ export function composeRuntime(options) {
580
526
  record: retyped,
581
527
  collection: parseCollection(retyped.documents, compositionOf(retyped)),
582
528
  };
583
- replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], rewriting(), prior));
529
+ replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
584
530
  const existing = rooms.get(record.locator);
585
531
  if (existing === undefined)
586
532
  list(record, false);
@@ -591,16 +537,13 @@ export function composeRuntime(options) {
591
537
  attachments: record.attachments,
592
538
  operations: record.operations,
593
539
  replica: record.replica,
594
- attachedModel: record.attachedModel,
595
540
  model: modelTypes(model),
596
541
  typedSelection: record.typedSelection === null
597
542
  ? null
598
543
  : {
599
544
  roots: record.typedSelection.roots,
600
- attachedModel: record.typedSelection.attachedModel,
601
545
  model: modelTypes(selectionModel),
602
546
  },
603
- untypedFolders: record.untypedFolders,
604
547
  });
605
548
  }
606
549
  if (read.kind === 'refused') {
@@ -666,7 +609,7 @@ export function composeRuntime(options) {
666
609
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
667
610
  }
668
611
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
669
- const withdrawn = withdrawals(confirmed, queue, withdrawable, rewriting());
612
+ const withdrawn = withdrawals(confirmed, queue, withdrawable);
670
613
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
671
614
  const returning = [
672
615
  ...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
@@ -687,69 +630,88 @@ export function composeRuntime(options) {
687
630
  });
688
631
  }
689
632
  }
690
- const settledOutcomes = [
691
- ...(settling.accepted === undefined ? [] : [settling.accepted]),
692
- ...(settling.leaving === undefined
693
- ? []
694
- : [{ id: settling.leaving.batch.id, outcome: settling.leaving.outcome }]),
695
- ...withdrawn.map((batch) => ({ id: batch.id, outcome: { kind: 'withdrawn' } })),
696
- ];
697
633
  await local.write({
698
634
  next: confirmed.record,
699
635
  previous: before?.confirmed.record ?? null,
700
636
  queue: kept,
701
637
  previousQueue: before?.queue ?? [],
702
- outcomes: settledOutcomes,
703
638
  roomDrafts: [...written.values()],
704
639
  });
705
- for (const { id, outcome } of settledOutcomes) {
706
- outcomes.set(id, outcome);
707
- outcomeRooms.set(id, locator);
708
- pendings.delete(id);
640
+ for (const batch of before?.queue ?? []) {
641
+ if (!kept.includes(batch))
642
+ pendings.delete(batch.id);
709
643
  }
710
644
  for (const record of written.values())
711
645
  rememberRoomDraft(record);
712
646
  for (const batch of withdrawn)
713
647
  journal.record(`outcome ${locator} ${batch.id} withdrawn`);
714
- replicas.set(locator, replicaOf(confirmed, kept, rewriting(), before));
648
+ replicas.set(locator, replicaOf(confirmed, kept, before));
715
649
  return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
716
650
  room: settling.room,
717
651
  queue: kept.length !== (before?.queue.length ?? 0),
718
652
  drafts: [...written.keys()],
719
653
  });
720
654
  }
721
- function publish(local, room, confirmed) {
655
+ function adoptModels(locator, record) {
656
+ for (const [attached, model] of [
657
+ ['collection', record.model],
658
+ ['typed-selection', record.selectionModel],
659
+ ]) {
660
+ const key = modelKey(locator, attached);
661
+ models.set(key, model);
662
+ invalidModels.delete(key);
663
+ if (model.status === 'current')
664
+ liveModels.set(key, JSON.stringify(model.snapshot));
665
+ else if (model.status === 'unavailable')
666
+ liveModels.delete(key);
667
+ }
668
+ }
669
+ function remodel(locator, answers, held) {
670
+ const taken = (answer, previous, attached) => {
671
+ if (answer === undefined)
672
+ return previous;
673
+ const { state, refused } = attachedModel(answer, previous);
674
+ if (refused !== null) {
675
+ const where = refused.location === undefined ? '' : ` ${refused.location}`;
676
+ journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
677
+ }
678
+ models.set(modelKey(locator, attached), state);
679
+ return state;
680
+ };
681
+ return {
682
+ collection: taken(answers?.collection, held.collection, 'collection'),
683
+ selection: taken(answers?.selection, held.selection, 'typed-selection'),
684
+ };
685
+ }
686
+ function publish(local, room, confirmed, base) {
722
687
  return exclusive(async () => {
723
688
  if (!continuing())
724
- return;
689
+ return true;
725
690
  const prior = replicas.get(room.locator);
726
- const retained = readConfirmed({
727
- ...confirmed.record,
728
- model: modelOf(room.locator),
729
- selectionModel: modelOf(room.locator, 'typed-selection'),
730
- }, prior?.confirmed);
731
- if (retained.kind !== 'staged')
732
- throw new Error(`Cannot retype staged Room ${room.locator}`);
691
+ if (base !== undefined &&
692
+ (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
693
+ prior.confirmed.record.revisions !== base.record.revisions)) {
694
+ return false;
695
+ }
696
+ adoptModels(room.locator, confirmed.record);
733
697
  const queue = prior?.queue ?? [];
734
- const roomVersion = retained.confirmed.record.roomVersion;
698
+ const roomVersion = confirmed.record.roomVersion;
735
699
  const accepted = queue.filter((batch) => {
736
- const outcome = outcomes.get(batch.id);
737
- return (outcome?.kind === 'accepted' &&
738
- options.adapter.reached(room.locator, roomVersion, outcome.roomVersion));
700
+ const accepted = pendings.get(batch.id)?.accepted;
701
+ return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
739
702
  });
740
- const set = await settle(local, room.locator, prior, retained.confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
703
+ const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
741
704
  for (const batch of accepted)
742
705
  pendings.delete(batch.id);
743
- const target = wanted.get(room.locator);
744
- if (target !== undefined && options.adapter.reached(room.locator, roomVersion, target)) {
745
- wanted.delete(room.locator);
746
- }
747
- unreadable.delete(room.locator);
706
+ stale.delete(room.locator);
707
+ if (unreadable.delete(room.locator))
708
+ scopedRefusals.delete(room.locator);
748
709
  sourceRefusals.delete(room.locator);
749
710
  declarationRefusals.delete(room.locator);
750
711
  list(room, true);
751
712
  publishChangeSet(set);
752
713
  moveTo({ ...status, ...attention() });
714
+ return true;
753
715
  });
754
716
  }
755
717
  async function saveRoomDrafts(locator, named, writtenBy) {
@@ -778,7 +740,6 @@ export function composeRuntime(options) {
778
740
  operations: room.operations,
779
741
  providerOwnedPaths: room.providerOwnedPaths,
780
742
  providerLimits: limits,
781
- rewriting: rewriting(),
782
743
  visible: replica.visible,
783
744
  changes: parts.flatMap(({ changes }) => changes),
784
745
  bases: parts.flatMap(({ bases }) => bases),
@@ -802,15 +763,16 @@ export function composeRuntime(options) {
802
763
  return { outcome: 'refused', rule: 'durable-write' };
803
764
  }
804
765
  pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
766
+ recorded = false;
805
767
  for (const path of consumed)
806
768
  forgetRoomDraft(locator, path);
807
- replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting(), replica));
769
+ replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
808
770
  publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
809
771
  queue: true,
810
772
  drafts: consumed,
811
773
  }));
812
774
  moveTo({ ...saving(status), ...attention() });
813
- void enqueue();
775
+ submitLater(locator);
814
776
  return { outcome: 'saved', identifier: built.batch.id };
815
777
  }
816
778
  async function mark(local, locator, identifier, submission, lost) {
@@ -875,10 +837,10 @@ export function composeRuntime(options) {
875
837
  return { kind: 'transport-uncertainty' };
876
838
  }
877
839
  }
878
- function declaredCards() {
840
+ function declaredAgents() {
879
841
  for (const capability of capabilities) {
880
- if (capability.conversationCards !== undefined)
881
- return capability.conversationCards;
842
+ if (capability.conversationAgents !== undefined)
843
+ return capability.conversationAgents;
882
844
  }
883
845
  return undefined;
884
846
  }
@@ -909,8 +871,6 @@ export function composeRuntime(options) {
909
871
  journal.record(`transfer action ${name}`);
910
872
  try {
911
873
  const result = await options.adapter.action(authorization, { name, input });
912
- if (result.kind === 'action' && result.at !== undefined)
913
- want(result.at);
914
874
  if (result.kind === 'scoped-refusal') {
915
875
  const room = replicas.get(result.locator)?.confirmed.record;
916
876
  if (room !== undefined)
@@ -942,32 +902,11 @@ export function composeRuntime(options) {
942
902
  options.adapter.reached(locator, written, roomVersion) &&
943
903
  !options.adapter.reached(locator, roomVersion, written));
944
904
  }
945
- function want({ locator, roomVersion }) {
946
- if (reachedBy(locator, roomVersion))
947
- return;
948
- const held = wanted.get(locator);
949
- if (held === undefined || options.adapter.reached(locator, roomVersion, held)) {
950
- wanted.set(locator, roomVersion);
951
- }
952
- }
953
- function reachOf(locator) {
954
- let target = wanted.get(locator);
955
- const versions = (replicas.get(locator)?.queue ?? []).flatMap((batch) => {
956
- const outcome = outcomes.get(batch.id);
957
- return outcome?.kind === 'accepted' ? [outcome.roomVersion] : [];
958
- });
959
- for (const version of versions) {
960
- if (target === undefined || options.adapter.reached(locator, version, target)) {
961
- target = version;
962
- }
963
- }
964
- return target === undefined || reachedBy(locator, target) ? undefined : target;
965
- }
966
- async function pullRefresh(authorized, confirmed, minPosition) {
905
+ async function pullRefresh(authorized, confirmed) {
967
906
  if (!continuing())
968
907
  return unconfirmed;
969
908
  try {
970
- const result = await refreshRoom(options.adapter, authorized, confirmed, continuing, minPosition);
909
+ const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
971
910
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
972
911
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
973
912
  return result;
@@ -984,7 +923,7 @@ export function composeRuntime(options) {
984
923
  const result = await bootstrapRoom(options.adapter, authorized, room, {
985
924
  collection: modelOf(room.locator),
986
925
  selection: modelOf(room.locator, 'typed-selection'),
987
- }, continuing);
926
+ }, (models, held) => remodel(room.locator, models, held), continuing);
988
927
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
989
928
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
990
929
  return result;
@@ -997,20 +936,28 @@ export function composeRuntime(options) {
997
936
  async function advance(room, local, authorized) {
998
937
  const held = replicas.get(room.locator);
999
938
  if (held !== undefined) {
1000
- const refreshed = await pullRefresh(authorized, held.confirmed, reachOf(room.locator));
939
+ const refreshed = await pullRefresh(authorized, held.confirmed);
1001
940
  if (!continuing())
1002
941
  return unconfirmed;
1003
942
  if (refreshed.kind === 'still-current') {
943
+ stale.delete(room.locator);
1004
944
  if (sourceRefusals.has(room.locator))
1005
- return unconfirmed;
945
+ return null;
1006
946
  if (unreadable.delete(room.locator)) {
947
+ scopedRefusals.delete(room.locator);
1007
948
  list(room, true);
1008
949
  emitRooms();
1009
950
  }
1010
951
  return null;
1011
952
  }
1012
953
  if (refreshed.kind === 'advanced') {
1013
- await publish(local, room, refreshed.confirmed);
954
+ if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
955
+ const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
956
+ const reached = now !== undefined &&
957
+ options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
958
+ return reached ? null : advance(room, local, authorized);
959
+ }
960
+ await retainModel(local, room, 'collection', modelOf(room.locator));
1014
961
  return null;
1015
962
  }
1016
963
  if (refreshed.kind !== 'beyond-horizon')
@@ -1025,7 +972,10 @@ export function composeRuntime(options) {
1025
972
  return null;
1026
973
  }
1027
974
  async function withhold(withheld, room, local, progress) {
1028
- progress.complete = false;
975
+ if (withheld.kind === 'refused')
976
+ stale.delete(room.locator);
977
+ else
978
+ stale.add(room.locator);
1029
979
  if (withheld.kind === 'scoped-refusal') {
1030
980
  await scoped(withheld, room, local);
1031
981
  }
@@ -1063,84 +1013,39 @@ export function composeRuntime(options) {
1063
1013
  journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
1064
1014
  }
1065
1015
  function leaving(batch, result) {
1066
- if (result.kind === 'expired') {
1067
- return { batch, outcome: { kind: 'expired' }, returned: { kind: 'expired' } };
1068
- }
1069
- const refusal = { code: result.code, fields: result.fields };
1070
- if (result.kind === 'conflicted') {
1071
- return {
1072
- batch,
1073
- outcome: {
1074
- kind: 'conflicted',
1075
- ...(result.roomVersion === undefined ? {} : { roomVersion: result.roomVersion }),
1076
- affected: result.affected,
1077
- ...refusal,
1078
- },
1079
- returned: { kind: 'conflicted', ...refusal },
1080
- };
1081
- }
1082
- return {
1083
- batch,
1084
- outcome: { kind: 'rejected', affected: result.affected, ...refusal },
1085
- returned: { kind: 'rejected', ...refusal },
1086
- };
1087
- }
1088
- function acceptedOf(accepted) {
1089
- return {
1090
- kind: 'accepted',
1091
- roomVersion: accepted.roomVersion,
1092
- ...(accepted.author === undefined ? {} : { author: accepted.author }),
1093
- ...(accepted.time === undefined ? {} : { time: accepted.time }),
1094
- };
1016
+ if (result.kind === 'expired')
1017
+ return { batch, returned: { kind: 'expired' } };
1018
+ return { batch, returned: { kind: result.kind, code: result.code, fields: result.fields } };
1095
1019
  }
1096
1020
  function logAccepted(locator, submitted, accepted) {
1097
1021
  journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
1098
1022
  (accepted.author === undefined ? '' : ` author ${accepted.author}`) +
1099
1023
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1100
1024
  }
1101
- async function recordAccepted(room, local, submitted, accepted) {
1102
- await exclusive(async () => {
1103
- if (!continuing())
1104
- return;
1105
- const current = replicas.get(room.locator);
1106
- if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1107
- return;
1108
- }
1109
- const outcome = acceptedOf(accepted);
1110
- await local.write({
1111
- next: current.confirmed.record,
1112
- previous: current.confirmed.record,
1113
- queue: current.queue,
1114
- previousQueue: current.queue,
1115
- outcomes: [{ id: submitted.id, outcome }],
1116
- });
1117
- outcomes.set(submitted.id, outcome);
1118
- outcomeRooms.set(submitted.id, room.locator);
1119
- logAccepted(room.locator, submitted, accepted);
1120
- });
1121
- }
1122
- async function commitAccept(room, local, submitted, accepted, base, overlay) {
1025
+ async function adoptAccepted(room, local, submitted, accepted) {
1123
1026
  if (!continuing())
1124
1027
  return unconfirmed;
1125
1028
  const current = replicas.get(room.locator);
1126
1029
  if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1127
1030
  return null;
1128
1031
  }
1129
- const remaining = current.queue.filter((batch) => batch.id !== submitted.id);
1130
- const retainedBase = readConfirmed({
1131
- ...base.record,
1132
- model: modelOf(room.locator),
1133
- selectionModel: modelOf(room.locator, 'typed-selection'),
1134
- }, base);
1135
- const applied = overlay && retainedBase.kind === 'staged'
1136
- ? applyReturned(retainedBase.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion)
1137
- : retainedBase;
1138
- if (applied.kind !== 'staged')
1032
+ const held = current.confirmed.record.roomVersion;
1033
+ logAccepted(room.locator, submitted, accepted);
1034
+ const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1035
+ ? { kind: 'staged', confirmed: current.confirmed }
1036
+ : applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
1037
+ ? accepted.roomVersion
1038
+ : held, accepted.roomVersion);
1039
+ if (applied.kind !== 'staged') {
1040
+ const state = pendings.get(submitted.id);
1041
+ await local.accept(room.locator, submitted.id, accepted.roomVersion);
1042
+ if (state !== undefined)
1043
+ pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1044
+ emitQueue(room.locator);
1139
1045
  return applied;
1140
- const set = await settle(local, room.locator, current, applied.confirmed, remaining, {
1141
- accepted: { id: submitted.id, outcome: acceptedOf(accepted) },
1142
- room: true,
1143
- });
1046
+ }
1047
+ const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1048
+ pendings.delete(submitted.id);
1144
1049
  unreadable.delete(room.locator);
1145
1050
  sourceRefusals.delete(room.locator);
1146
1051
  declarationRefusals.delete(room.locator);
@@ -1149,17 +1054,8 @@ export function composeRuntime(options) {
1149
1054
  moveTo({ ...status, ...attention() });
1150
1055
  return null;
1151
1056
  }
1152
- async function accepting(room, local, submitted, accepted, pulled, fresh) {
1153
- const roomVersion = pulled.record.roomVersion;
1154
- if (options.adapter.reached(room.locator, roomVersion, accepted.roomVersion)) {
1155
- return committed(room, local, submitted, accepted, pulled, roomVersion === accepted.roomVersion);
1156
- }
1157
- if (fresh && continuing())
1158
- await publish(local, room, pulled);
1159
- return false;
1160
- }
1161
- async function committed(room, local, submitted, accepted, base, overlay) {
1162
- const failed = await exclusive(() => commitAccept(room, local, submitted, accepted, base, overlay));
1057
+ async function adopted(room, local, submitted, accepted) {
1058
+ const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
1163
1059
  if (failed === null)
1164
1060
  return true;
1165
1061
  if (failed.kind === 'refused' && continuing())
@@ -1172,10 +1068,8 @@ export function composeRuntime(options) {
1172
1068
  const head = replica?.queue[0];
1173
1069
  if (replica === undefined || head === undefined)
1174
1070
  return;
1175
- if (outcomes.get(head.id)?.kind === 'accepted') {
1176
- progress.complete = false;
1071
+ if (pendings.get(head.id)?.accepted !== undefined)
1177
1072
  return;
1178
- }
1179
1073
  const state = pendings.get(head.id);
1180
1074
  const { submission, lost: earlier } = state ?? {
1181
1075
  submission: { state: 'waiting' },
@@ -1203,6 +1097,7 @@ export function composeRuntime(options) {
1203
1097
  return;
1204
1098
  }
1205
1099
  const opensBefore = openSignals;
1100
+ const answered = awaitingAnswer(room.locator);
1206
1101
  try {
1207
1102
  result = await options.adapter.mutate(authorized, {
1208
1103
  locator: room.locator,
@@ -1211,11 +1106,14 @@ export function composeRuntime(options) {
1211
1106
  }, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
1212
1107
  }
1213
1108
  catch {
1109
+ answered();
1214
1110
  progress.complete = false;
1215
1111
  if (continuing())
1216
1112
  progress.reachable = false;
1217
1113
  return;
1218
1114
  }
1115
+ if (result.kind !== 'accepted' || !continuing())
1116
+ answered();
1219
1117
  if (!continuing())
1220
1118
  return;
1221
1119
  if (result.kind === 'transport-uncertainty') {
@@ -1268,6 +1166,7 @@ export function composeRuntime(options) {
1268
1166
  const reset = result.reset;
1269
1167
  progress.complete = false;
1270
1168
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1169
+ armTimer();
1271
1170
  return;
1272
1171
  }
1273
1172
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
@@ -1294,98 +1193,35 @@ export function composeRuntime(options) {
1294
1193
  return withhold(withheld, room, local, progress);
1295
1194
  continue;
1296
1195
  }
1297
- await recordAccepted(room, local, head, result);
1298
- const from = replicas.get(room.locator)?.confirmed;
1299
- if (from === undefined)
1300
- return;
1301
- const pulled = await pullRefresh(authorized, from, reachOf(room.locator));
1302
- if (!continuing())
1303
- return;
1304
- if (pulled.kind === 'revoked-authorization') {
1305
- await refuse('revoked-authorization');
1306
- return;
1307
- }
1308
- if (pulled.kind === 'scoped-refusal') {
1309
- await scoped(pulled, room, local);
1196
+ const kept = await adopted(room, local, head, result).finally(answered);
1197
+ if (!kept) {
1310
1198
  progress.complete = false;
1311
1199
  return;
1312
1200
  }
1313
- if (pulled.kind === 'still-current' || pulled.kind === 'advanced') {
1314
- const fresh = pulled.kind === 'advanced';
1315
- if (!(await accepting(room, local, head, result, fresh ? pulled.confirmed : from, fresh))) {
1316
- progress.complete = false;
1317
- return;
1318
- }
1319
- continue;
1320
- }
1321
- if (pulled.kind === 'beyond-horizon') {
1322
- const bootstrapped = await pullBootstrap(authorized, room);
1323
- if (!continuing())
1324
- return;
1325
- if (bootstrapped.kind === 'revoked-authorization') {
1326
- await refuse('revoked-authorization');
1327
- return;
1328
- }
1329
- if (bootstrapped.kind === 'scoped-refusal') {
1330
- await scoped(bootstrapped, room, local);
1331
- progress.complete = false;
1332
- return;
1333
- }
1334
- if (bootstrapped.kind === 'staged') {
1335
- if (!(await accepting(room, local, head, result, bootstrapped.confirmed, true))) {
1336
- progress.complete = false;
1337
- return;
1338
- }
1339
- continue;
1340
- }
1341
- progress.complete = false;
1342
- if (bootstrapped.kind === 'transport-uncertainty') {
1343
- progress.reachable = false;
1344
- }
1345
- else if (bootstrapped.kind === 'room-not-readable') {
1346
- unreadable.add(room.locator);
1347
- list(room, false);
1348
- emitRooms();
1349
- }
1350
- else if (bootstrapped.kind === 'refused') {
1351
- refusedSource(room, bootstrapped);
1352
- }
1353
- return;
1354
- }
1355
- progress.complete = false;
1356
- if (pulled.kind === 'transport-uncertainty') {
1357
- progress.reachable = false;
1358
- }
1359
- else if (pulled.kind === 'room-not-readable') {
1360
- unreadable.add(room.locator);
1361
- list(room, false);
1362
- emitRooms();
1363
- }
1364
- else if (pulled.kind === 'retryable-refusal') {
1365
- journal.record(`refusal ${pulled.kind} ${pulled.code}`);
1366
- }
1367
- else if (pulled.kind === 'refused') {
1368
- refusedSource(room, pulled);
1369
- }
1370
- return;
1371
1201
  }
1372
- return;
1202
+ }
1203
+ function awaitingAnswer(locator) {
1204
+ let release = () => { };
1205
+ const awaited = new Promise((resolve) => {
1206
+ release = resolve;
1207
+ });
1208
+ answering.set(locator, awaited);
1209
+ return () => {
1210
+ if (answering.get(locator) === awaited)
1211
+ answering.delete(locator);
1212
+ release();
1213
+ };
1373
1214
  }
1374
1215
  async function retainModel(local, room, attached, next) {
1375
1216
  const key = modelKey(room.locator, attached);
1376
- if (next.status === 'unavailable')
1377
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
1378
- const epoch = modelEpoch.get(key) ?? 0;
1379
1217
  const previous = modelOf(room.locator, attached);
1380
1218
  const heldBefore = replicas.get(room.locator)?.confirmed.record;
1381
1219
  if (JSON.stringify(previous) === JSON.stringify(next) &&
1382
1220
  (heldBefore === undefined ||
1383
1221
  (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
1384
1222
  JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
1385
- JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
1386
1223
  JSON.stringify(heldBefore.typedSelection) ===
1387
- JSON.stringify(room.typedSelection ?? null) &&
1388
- JSON.stringify(heldBefore.untypedFolders) === JSON.stringify(room.untypedFolders ?? []))))
1224
+ JSON.stringify(room.typedSelection ?? null))))
1389
1225
  return;
1390
1226
  if (next.status === 'unavailable') {
1391
1227
  invalidModels.add(key);
@@ -1394,17 +1230,13 @@ export function composeRuntime(options) {
1394
1230
  await exclusive(async () => {
1395
1231
  if (!continuing())
1396
1232
  return;
1397
- if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
1398
- return;
1399
1233
  const held = replicas.get(room.locator);
1400
1234
  if (held !== undefined) {
1401
1235
  const declared = {
1402
1236
  ...held.confirmed.record,
1403
1237
  operations: room.operations,
1404
1238
  replica: room.replica,
1405
- attachedModel: room.attachedModel,
1406
1239
  typedSelection: room.typedSelection ?? null,
1407
- untypedFolders: room.untypedFolders ?? [],
1408
1240
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1409
1241
  };
1410
1242
  const reclassified = sameMembership(declared, held.confirmed.record)
@@ -1415,7 +1247,6 @@ export function composeRuntime(options) {
1415
1247
  : {
1416
1248
  ...declared,
1417
1249
  typedSelection: held.confirmed.record.typedSelection,
1418
- untypedFolders: held.confirmed.record.untypedFolders,
1419
1250
  };
1420
1251
  const read = reclassified.kind === 'deferred' ? readConfirmed(record, held.confirmed) : reclassified;
1421
1252
  await local.write({
@@ -1424,7 +1255,7 @@ export function composeRuntime(options) {
1424
1255
  queue: held.queue,
1425
1256
  });
1426
1257
  if (read.kind === 'staged') {
1427
- replicas.set(room.locator, replicaOf(read.confirmed, held.queue, rewriting(), held));
1258
+ replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1428
1259
  if (declarationRefusals.delete(room.locator)) {
1429
1260
  sourceRefusals.delete(room.locator);
1430
1261
  unreadable.delete(room.locator);
@@ -1448,77 +1279,6 @@ export function composeRuntime(options) {
1448
1279
  publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
1449
1280
  });
1450
1281
  }
1451
- async function acquireModel(local, room, authorized, selection) {
1452
- const attached = selection === null ? 'collection' : 'typed-selection';
1453
- const key = modelKey(room.locator, attached);
1454
- const declared = selection === null ? room.attachedModel : selection.attachedModel;
1455
- let previous = modelOf(room.locator, attached);
1456
- if (previous.status !== 'unavailable' &&
1457
- (declared === null || !sameModel(previous.snapshot, declared))) {
1458
- previous = { status: 'unavailable', reason: 'version-changed' };
1459
- await retainModel(local, room, attached, previous);
1460
- if (discoveryPaused)
1461
- return;
1462
- }
1463
- const epoch = modelEpoch.get(key) ?? 0;
1464
- let answer;
1465
- try {
1466
- answer = await options.adapter.model(authorized, selection === null
1467
- ? { locator: room.locator, attached: 'collection', model: declared }
1468
- : {
1469
- locator: room.locator,
1470
- attached: 'typed-selection',
1471
- roots: selection.roots,
1472
- model: declared,
1473
- });
1474
- }
1475
- catch {
1476
- answer = { kind: 'transport-uncertainty' };
1477
- }
1478
- if (discoveryPaused || epoch !== (modelEpoch.get(key) ?? 0))
1479
- return;
1480
- if (answer.kind === 'revoked-authorization') {
1481
- await refuse('revoked-authorization');
1482
- return;
1483
- }
1484
- if (answer.kind === 'transport-uncertainty') {
1485
- await retainModel(local, room, attached, cached(previous));
1486
- return;
1487
- }
1488
- if (answer.kind === 'model-unavailable') {
1489
- await retainModel(local, room, attached, {
1490
- status: 'unavailable',
1491
- reason: answer.reason,
1492
- });
1493
- return;
1494
- }
1495
- if (declared === null || !sameModel(answer.snapshot, declared)) {
1496
- await retainModel(local, room, attached, {
1497
- status: 'unavailable',
1498
- reason: 'version-changed',
1499
- });
1500
- return;
1501
- }
1502
- if (previous.status !== 'unavailable' &&
1503
- sameModel(previous.snapshot, answer.snapshot) &&
1504
- previous.snapshot.schema !== answer.snapshot.schema) {
1505
- await retainModel(local, room, attached, { status: 'unavailable', reason: 'invalid' });
1506
- return;
1507
- }
1508
- const verified = answer.snapshot.schema === null ? EMPTY_SCHEMA : verify(answer.snapshot.schema);
1509
- if (verified.verdict === 'refuse') {
1510
- const where = verified.location === undefined ? '' : ` ${verified.location}`;
1511
- journal.record(`model invalid ${room.locator} ${attached} ${answer.snapshot.identity} ${verified.rule}${where}`);
1512
- }
1513
- await retainModel(local, room, attached, verified.verdict === 'accept'
1514
- ? {
1515
- status: 'current',
1516
- snapshot: structuredClone(answer.snapshot),
1517
- verifiedWith: MD_MODEL,
1518
- types: verified.model.types,
1519
- }
1520
- : { status: 'unavailable', reason: 'invalid' });
1521
- }
1522
1282
  async function scoped(result, room, local) {
1523
1283
  scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
1524
1284
  let scopedAway = room;
@@ -1546,14 +1306,34 @@ export function composeRuntime(options) {
1546
1306
  emitRooms();
1547
1307
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
1548
1308
  }
1549
- async function synchronize(discovered, local, progress) {
1550
- const prepared = await Promise.allSettled(discovered.map((room) => prepareRoom(room, local, progress)));
1551
- const ready = discovered.filter((_, index) => {
1309
+ function heldRooms(listed) {
1310
+ return listed.flatMap((room) => {
1311
+ const record = replicas.get(room.locator)?.confirmed.record;
1312
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1313
+ return [];
1314
+ return [
1315
+ {
1316
+ locator: room.locator,
1317
+ roomVersion: record.roomVersion,
1318
+ documents: record.documents,
1319
+ revisions: record.revisions,
1320
+ attachments: record.attachments,
1321
+ },
1322
+ ];
1323
+ });
1324
+ }
1325
+ async function synchronize(listed, local, progress, attempt) {
1326
+ const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1327
+ const ready = listed.filter((_, index) => {
1552
1328
  const outcome = prepared[index];
1553
1329
  return outcome?.status === 'fulfilled' && outcome.value;
1554
1330
  });
1555
- const rooms = await Promise.allSettled(ready.map((room) => synchronizeRoom(room, local, progress)));
1556
- const failed = [...prepared, ...rooms].find((room) => room.status === 'rejected');
1331
+ if (attempt === 'opened')
1332
+ options.adapter.roomStream?.hold(heldRooms(ready));
1333
+ const loaded = await Promise.allSettled(ready
1334
+ .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1335
+ .map((room) => loadRoom(room, local, progress)));
1336
+ const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1557
1337
  if (failed !== undefined)
1558
1338
  throw failed.reason;
1559
1339
  }
@@ -1570,11 +1350,8 @@ export function composeRuntime(options) {
1570
1350
  progress.complete = false;
1571
1351
  return false;
1572
1352
  }
1573
- const selection = room.typedSelection;
1574
- await Promise.all([
1575
- acquireModel(local, room, authorization, null),
1576
- ...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
1577
- ]);
1353
+ if (prior !== undefined)
1354
+ await retainModel(local, room, 'collection', modelOf(room.locator));
1578
1355
  if (discoveryPaused || authorization === undefined)
1579
1356
  return false;
1580
1357
  if (room.replica.admission !== 'admitted') {
@@ -1582,24 +1359,53 @@ export function composeRuntime(options) {
1582
1359
  progress.complete = false;
1583
1360
  return false;
1584
1361
  }
1585
- const held = replicas.get(room.locator);
1586
- if (held !== undefined && held.confirmed.record.replica.selection !== room.replica.selection) {
1587
- progress.complete = false;
1588
- return false;
1589
- }
1590
1362
  return true;
1591
1363
  }
1592
- async function synchronizeRoom(room, local, progress) {
1364
+ async function caughtUp(room, local, progress) {
1365
+ const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1366
+ if (held === undefined || authorization === undefined)
1367
+ return;
1368
+ const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1369
+ if (!behind)
1370
+ return;
1371
+ const withheld = await advance(room, local, authorization);
1372
+ if (withheld !== null)
1373
+ await withhold(withheld, room, local, progress);
1374
+ }
1375
+ function revoked(locator) {
1376
+ return ['collection', 'typed-selection'].some((attached) => {
1377
+ const model = modelOf(locator, attached);
1378
+ return model.status === 'unavailable' && model.reason === 'revoked';
1379
+ });
1380
+ }
1381
+ async function loadRoom(room, local, progress) {
1593
1382
  if (discoveryPaused || authorization === undefined)
1594
1383
  return;
1384
+ if (replicas.has(room.locator) && revoked(room.locator)) {
1385
+ await reload(room, local, progress);
1386
+ return;
1387
+ }
1595
1388
  const withheld = await advance(room, local, authorization);
1596
1389
  if (withheld !== null) {
1597
1390
  await withhold(withheld, room, local, progress);
1598
1391
  return;
1599
1392
  }
1600
- await submitRoom(room, local, authorization, progress);
1393
+ await caughtUp(room, local, progress);
1601
1394
  }
1602
- async function cycle(local, progress) {
1395
+ async function reload(room, local, progress) {
1396
+ if (discoveryPaused || authorization === undefined)
1397
+ return;
1398
+ const bootstrapped = await pullBootstrap(authorization, room);
1399
+ if (!continuing())
1400
+ return;
1401
+ if (bootstrapped.kind !== 'staged') {
1402
+ await withhold(bootstrapped, room, local, progress);
1403
+ return;
1404
+ }
1405
+ await publish(local, room, bootstrapped.confirmed);
1406
+ await caughtUp(room, local, progress);
1407
+ }
1408
+ async function discoverRooms(local, progress, attempt) {
1603
1409
  if (closed || authorization === undefined) {
1604
1410
  notifyReady();
1605
1411
  return;
@@ -1625,6 +1431,10 @@ export function composeRuntime(options) {
1625
1431
  }
1626
1432
  }
1627
1433
  progress.reachable = false;
1434
+ progress.complete = false;
1435
+ if (attempt === 'opened') {
1436
+ options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1437
+ }
1628
1438
  notifyReady();
1629
1439
  return;
1630
1440
  }
@@ -1639,17 +1449,6 @@ export function composeRuntime(options) {
1639
1449
  notifyReady();
1640
1450
  return;
1641
1451
  }
1642
- if (!result.supportedVersions.includes(ROOM_CONTRACT_VERSION)) {
1643
- await refuse('no-matching-version');
1644
- notifyReady();
1645
- return;
1646
- }
1647
- if (result.mdModel.identity !== MD_MODEL.identity ||
1648
- result.mdModel.version !== MD_MODEL.version) {
1649
- await refuse('md-model-version-mismatch');
1650
- notifyReady();
1651
- return;
1652
- }
1653
1452
  for (const room of result.rooms) {
1654
1453
  if (room.operations === undefined ||
1655
1454
  !['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
@@ -1657,8 +1456,7 @@ export function composeRuntime(options) {
1657
1456
  room.replica === undefined ||
1658
1457
  typeof room.replica.selection !== 'string' ||
1659
1458
  room.replica.selection.length === 0 ||
1660
- !['admitted', 'unavailable'].includes(room.replica.admission) ||
1661
- !declaredIdentity(room.attachedModel)) {
1459
+ !['admitted', 'unavailable'].includes(room.replica.admission)) {
1662
1460
  journal.record(`invalid model or replica declaration ${room.locator}`);
1663
1461
  await refuse('missing-required-guarantee');
1664
1462
  notifyReady();
@@ -1670,20 +1468,6 @@ export function composeRuntime(options) {
1670
1468
  notifyReady();
1671
1469
  return;
1672
1470
  }
1673
- if (!declaredUntypedFolders(room)) {
1674
- journal.record(`invalid untyped folder declaration ${room.locator}`);
1675
- await refuse('missing-required-guarantee');
1676
- notifyReady();
1677
- return;
1678
- }
1679
- }
1680
- const declared = new Set(result.capabilities.map((capability) => capability.name));
1681
- for (const required of options.requiredGuarantees) {
1682
- if (!knownCapabilities.has(required) || !declared.has(required)) {
1683
- await refuse('missing-required-guarantee');
1684
- notifyReady();
1685
- return;
1686
- }
1687
1471
  }
1688
1472
  if (refusal !== null) {
1689
1473
  refusal = null;
@@ -1692,29 +1476,13 @@ export function composeRuntime(options) {
1692
1476
  const previousCapabilities = capabilities;
1693
1477
  capabilities = result.capabilities;
1694
1478
  notifyReady();
1695
- if (declared.has('room-version-notifications'))
1696
- observeRoomVersions();
1697
- const discoveredSlots = result.slots ?? [];
1698
1479
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1699
- JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities) ||
1700
- JSON.stringify(discoveredSlots) !== JSON.stringify(slots)) {
1480
+ JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
1701
1481
  limits = result.providerLimits;
1702
- slots = discoveredSlots;
1703
- await local.declare(limits, capabilities, slots);
1482
+ await local.declare(limits, capabilities);
1704
1483
  }
1705
1484
  await applyDiscovery(result.rooms, local);
1706
- handSelected(result.rooms
1707
- .filter((room) => room.replica.admission === 'admitted')
1708
- .map((room) => room.locator)
1709
- .sort());
1710
- progress.complete = true;
1711
- await synchronize(result.rooms, local, progress);
1712
- if (closed || discoveryPaused)
1713
- return;
1714
- await conversations.reconcile();
1715
- if (status.providerUnreachable)
1716
- conversations.reconnected();
1717
- await conversations.catchUp(progress);
1485
+ await synchronize(result.rooms, local, progress, attempt);
1718
1486
  }
1719
1487
  async function ensureAuthorization() {
1720
1488
  if (authorization !== undefined)
@@ -1728,64 +1496,246 @@ export function composeRuntime(options) {
1728
1496
  throw error;
1729
1497
  }
1730
1498
  }
1731
- async function connect() {
1732
- if (closed || stopping || discoveryPaused) {
1733
- notifyReady();
1499
+ function task(work) {
1500
+ if (stopping)
1501
+ return chain;
1502
+ chain = chain.then(work, work).catch(() => undefined);
1503
+ return chain;
1504
+ }
1505
+ function sync(attempt) {
1506
+ return task(async () => {
1507
+ if (closed || stopping || discoveryPaused) {
1508
+ notifyReady();
1509
+ return;
1510
+ }
1511
+ const progress = { complete: true, reachable: true };
1512
+ underway = attempt === 'opened' ? progress : null;
1513
+ complete = false;
1514
+ recorded = false;
1515
+ moveTo(transferring(status));
1516
+ try {
1517
+ const local = await openLocal();
1518
+ if (closed)
1519
+ return;
1520
+ await ensureAuthorization();
1521
+ if (closed)
1522
+ return;
1523
+ await discoverRooms(local, progress, attempt);
1524
+ }
1525
+ catch {
1526
+ progress.complete = false;
1527
+ if (attempt === 'opened') {
1528
+ options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1529
+ }
1530
+ }
1531
+ finally {
1532
+ notifyReady();
1533
+ }
1534
+ if (attempt === 'opened')
1535
+ return;
1536
+ await finishOpening(progress);
1537
+ });
1538
+ }
1539
+ function closedSocket(revoked) {
1540
+ const opened = socketOpen;
1541
+ socketOpen = false;
1542
+ if (revoked) {
1543
+ void refuse('revoked-authorization');
1734
1544
  return;
1735
1545
  }
1736
- startTimer();
1737
- const progress = { complete: false, reachable: true };
1738
- let completedAt = new Date().toISOString();
1739
- try {
1740
- const local = await openLocal();
1741
- if (closed)
1742
- return;
1743
- await ensureAuthorization();
1744
- if (closed)
1546
+ if (opened || status.providerUnreachable)
1547
+ return;
1548
+ moveTo({ ...status, providerUnreachable: true });
1549
+ }
1550
+ function applyOpening(expired) {
1551
+ return task(async () => {
1552
+ const progress = underway;
1553
+ underway = null;
1554
+ if (closed || stopping)
1745
1555
  return;
1746
- await cycle(local, progress);
1747
- if (closed)
1556
+ if (progress === null) {
1557
+ await reloadExpired(expired);
1748
1558
  return;
1749
- if (progress.complete) {
1750
- completedAt = new Date().toISOString();
1751
- await local.complete(completedAt);
1752
1559
  }
1560
+ try {
1561
+ const local = await openLocal();
1562
+ const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1563
+ const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1564
+ const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
1565
+ if (progress.reachable) {
1566
+ for (const room of held)
1567
+ if (!reloading.includes(room))
1568
+ await confirmModels(room, local);
1569
+ }
1570
+ const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1571
+ if (failed !== undefined)
1572
+ throw failed.reason;
1573
+ }
1574
+ catch {
1575
+ progress.complete = false;
1576
+ progress.reachable = false;
1577
+ }
1578
+ await finishOpening(progress);
1579
+ });
1580
+ }
1581
+ async function reloadExpired(expired) {
1582
+ const progress = { complete: true, reachable: true };
1583
+ try {
1584
+ const local = await openLocal();
1585
+ await Promise.allSettled([...discovered.values()]
1586
+ .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1587
+ .map((room) => reload(room, local, progress)));
1753
1588
  }
1754
1589
  catch {
1755
- progress.complete = false;
1756
- notifyReady();
1590
+ progress.reachable = false;
1591
+ }
1592
+ await rest(progress);
1593
+ }
1594
+ async function confirmModels(room, local) {
1595
+ for (const attached of ['collection', 'typed-selection']) {
1596
+ const model = modelOf(room.locator, attached);
1597
+ if (model.status === 'cached') {
1598
+ await retainModel(local, room, attached, { ...model, status: 'current' });
1599
+ }
1600
+ }
1601
+ }
1602
+ function submitLater(locator) {
1603
+ const running = submitting.get(locator);
1604
+ if (running !== undefined) {
1605
+ running.again = true;
1757
1606
  return;
1758
1607
  }
1759
- finally {
1760
- notifyReady();
1761
- moveTo(settled(status, progress, completedAt, pending(), attention()));
1608
+ const run = { again: false, done: Promise.resolve() };
1609
+ submitting.set(locator, run);
1610
+ run.done = (async () => {
1611
+ try {
1612
+ do {
1613
+ run.again = false;
1614
+ const room = discovered.get(locator);
1615
+ if (closed || stopping || discoveryPaused || room === undefined)
1616
+ return;
1617
+ const progress = { complete: true, reachable: true };
1618
+ try {
1619
+ await ensureAuthorization();
1620
+ if (authorization === undefined)
1621
+ return;
1622
+ await submitRoom(room, await openLocal(), authorization, progress);
1623
+ }
1624
+ catch {
1625
+ progress.reachable = false;
1626
+ }
1627
+ armTimer();
1628
+ await rest(progress);
1629
+ } while (run.again);
1630
+ }
1631
+ finally {
1632
+ submitting.delete(locator);
1633
+ }
1634
+ })();
1635
+ }
1636
+ function submitAll() {
1637
+ for (const [locator, replica] of replicas) {
1638
+ if (replica.queue.length > 0)
1639
+ submitLater(locator);
1762
1640
  }
1763
1641
  }
1764
- async function run() {
1642
+ async function finishOpening(progress) {
1765
1643
  try {
1766
- await connect();
1644
+ if (closed || discoveryPaused) {
1645
+ progress.complete = false;
1646
+ return;
1647
+ }
1648
+ submitAll();
1649
+ await conversations.reconcile();
1650
+ if (status.providerUnreachable)
1651
+ conversations.reconnected();
1652
+ const reading = { complete: true, reachable: true };
1653
+ await conversations.catchUp(reading);
1654
+ if (!reading.reachable)
1655
+ progress.reachable = false;
1656
+ }
1657
+ catch {
1658
+ progress.complete = false;
1767
1659
  }
1768
1660
  finally {
1769
- inflight = false;
1770
- if (queued && !closed) {
1771
- queued = false;
1772
- void enqueue();
1773
- }
1661
+ complete = progress.complete;
1662
+ await rest(progress);
1774
1663
  }
1775
1664
  }
1776
- function observeRoomVersions() {
1777
- const path = options.adapter.roomVersions;
1778
- if (path === undefined || withdrawNotifications !== undefined)
1665
+ async function rest(progress) {
1666
+ if (closed)
1779
1667
  return;
1780
- withdrawNotifications = path.observe((notification) => {
1781
- if (closed || stopping || discoveryPaused)
1668
+ let lastCompleted = status.lastCompleted;
1669
+ let synced = complete && stale.size === 0 && !pending();
1670
+ if (synced && !recorded) {
1671
+ const completedAt = new Date().toISOString();
1672
+ try {
1673
+ await (await openLocal()).complete(completedAt);
1674
+ recorded = true;
1675
+ lastCompleted = completedAt;
1676
+ }
1677
+ catch {
1678
+ synced = false;
1679
+ }
1680
+ }
1681
+ moveTo({
1682
+ state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
1683
+ lastCompleted,
1684
+ providerUnreachable: !progress.reachable,
1685
+ ...attention(),
1686
+ });
1687
+ }
1688
+ function applyRoom(locator, roomVersion) {
1689
+ announced.set(locator, roomVersion);
1690
+ return task(async () => {
1691
+ await answering.get(locator);
1692
+ if (closed || stopping || discoveryPaused || authorization === undefined)
1693
+ return;
1694
+ const room = discovered.get(locator);
1695
+ const held = replicas.get(locator);
1696
+ if (room === undefined || room.replica.admission !== 'admitted')
1782
1697
  return;
1783
- if (notification.modelChanged === true && handed?.includes(notification.locator) === true) {
1784
- void enqueue();
1698
+ if (held !== undefined &&
1699
+ (held.confirmed.record.replica.selection !== room.replica.selection ||
1700
+ options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
1701
+ return;
1702
+ }
1703
+ const during = underway;
1704
+ const progress = during ?? { complete: true, reachable: true };
1705
+ if (during === null)
1706
+ moveTo(transferring(status));
1707
+ try {
1708
+ const local = await openLocal();
1709
+ const withheld = await advance(room, local, authorization);
1710
+ if (withheld !== null)
1711
+ await withhold(withheld, room, local, progress);
1712
+ }
1713
+ catch {
1714
+ stale.add(locator);
1715
+ }
1716
+ finally {
1717
+ if (during === null)
1718
+ await rest(progress);
1719
+ }
1720
+ });
1721
+ }
1722
+ function observeStream() {
1723
+ const stream = options.adapter.roomStream;
1724
+ if (stream === undefined || withdrawStream !== undefined)
1725
+ return;
1726
+ withdrawStream = stream.observe((event) => {
1727
+ if (closed || stopping)
1785
1728
  return;
1729
+ if (event.kind === 'opened') {
1730
+ socketOpen = true;
1731
+ void sync('opened');
1786
1732
  }
1787
- notices.set(notification.locator, notification.roomVersion);
1788
- chain = chain.then(runNotified, runNotified);
1733
+ else if (event.kind === 'closed')
1734
+ closedSocket(event.revoked);
1735
+ else if (event.kind === 'opening')
1736
+ void applyOpening(event.expired);
1737
+ else
1738
+ void applyRoom(event.locator, event.roomVersion);
1789
1739
  });
1790
1740
  }
1791
1741
  function writesHeld() {
@@ -1806,14 +1756,17 @@ export function composeRuntime(options) {
1806
1756
  listener(mode);
1807
1757
  }
1808
1758
  if (releasing)
1809
- void enqueue();
1759
+ submitAll();
1810
1760
  }
1811
1761
  function observeConversations() {
1812
1762
  const stream = options.adapter.conversations;
1813
1763
  if (stream === undefined || withdrawConversations !== undefined)
1814
1764
  return;
1815
1765
  withdrawConversations = stream.observe((event) => {
1816
- conversations.receive(event);
1766
+ if (conversations.receive(event)) {
1767
+ void ready.then(scheduleCurrency);
1768
+ return;
1769
+ }
1817
1770
  if (event.kind !== 'gap' || conversations.current())
1818
1771
  return;
1819
1772
  show();
@@ -1824,7 +1777,7 @@ export function composeRuntime(options) {
1824
1777
  if (currencyQueued || closed || stopping)
1825
1778
  return;
1826
1779
  currencyQueued = true;
1827
- chain = chain.then(runCurrency, runCurrency);
1780
+ void task(runCurrency);
1828
1781
  }
1829
1782
  async function runCurrency() {
1830
1783
  currencyQueued = false;
@@ -1850,95 +1803,12 @@ export function composeRuntime(options) {
1850
1803
  }
1851
1804
  }
1852
1805
  }
1853
- function handSelected(locators) {
1854
- const path = options.adapter.roomVersions;
1855
- if (path === undefined || withdrawNotifications === undefined)
1856
- return;
1857
- if (handed !== undefined && JSON.stringify(locators) === JSON.stringify(handed))
1858
- return;
1859
- handed = locators;
1860
- path.select(locators);
1861
- }
1862
- async function runNotified() {
1863
- const announced = [...notices];
1864
- notices.clear();
1865
- if (announced.length === 0 || closed || stopping || discoveryPaused)
1866
- return;
1867
- const resting = status;
1868
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1869
- moveTo(transferring(resting));
1870
- try {
1871
- const local = await openLocal();
1872
- for (const [locator, roomVersion] of announced) {
1873
- if (closed || discoveryPaused || authorization === undefined)
1874
- return;
1875
- if (handed?.includes(locator) !== true)
1876
- continue;
1877
- const room = discovered.get(locator);
1878
- const held = replicas.get(locator);
1879
- if (room === undefined)
1880
- continue;
1881
- if (sourceRefusals.has(locator)) {
1882
- void enqueue();
1883
- continue;
1884
- }
1885
- if (held === undefined)
1886
- continue;
1887
- if (held.confirmed.record.roomVersion === roomVersion)
1888
- continue;
1889
- const withheld = await advance(room, local, authorization);
1890
- if (withheld === null)
1891
- continue;
1892
- if (withheld.kind === 'refused') {
1893
- void enqueue();
1894
- continue;
1895
- }
1896
- if (withheld.kind === 'scoped-refusal') {
1897
- await scoped(withheld, room, local);
1898
- continue;
1899
- }
1900
- if (withheld.kind === 'transport-uncertainty') {
1901
- progress.reachable = false;
1902
- continue;
1903
- }
1904
- if (withheld.kind === 'room-not-readable') {
1905
- unreadable.add(locator);
1906
- list(room, false);
1907
- emitRooms();
1908
- continue;
1909
- }
1910
- if (withheld.kind === 'revoked-authorization') {
1911
- await refuse('revoked-authorization');
1912
- return;
1913
- }
1914
- }
1915
- }
1916
- catch {
1917
- progress.reachable = false;
1918
- }
1919
- finally {
1920
- if (!closed) {
1921
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1922
- }
1923
- }
1924
- }
1925
- function enqueue() {
1926
- if (stopping)
1927
- return chain;
1928
- if (inflight) {
1929
- queued = true;
1930
- return chain;
1931
- }
1932
- inflight = true;
1933
- chain = chain.then(run, run);
1934
- return chain;
1935
- }
1936
1806
  function close() {
1937
1807
  if (closed)
1938
1808
  return;
1939
1809
  closed = true;
1940
- withdrawNotifications?.();
1941
- withdrawNotifications = undefined;
1810
+ withdrawStream?.();
1811
+ withdrawStream = undefined;
1942
1812
  withdrawConversations?.();
1943
1813
  withdrawConversations = undefined;
1944
1814
  withdrawServiceMode?.();
@@ -1962,6 +1832,7 @@ export function composeRuntime(options) {
1962
1832
  stopping = true;
1963
1833
  stopTimer();
1964
1834
  await chain.catch(() => undefined);
1835
+ await Promise.all([...submitting.values()].map((run) => run.done));
1965
1836
  await mutations;
1966
1837
  close();
1967
1838
  }
@@ -1970,11 +1841,29 @@ export function composeRuntime(options) {
1970
1841
  options.signal?.addEventListener('abort', close, { once: true });
1971
1842
  if (options.signal?.aborted)
1972
1843
  close();
1973
- void openLocal().catch(() => {
1844
+ observeStream();
1845
+ void openLocal().then(() => {
1846
+ if (capabilities.length > 0)
1847
+ notifyReady();
1848
+ }, () => {
1974
1849
  notifyReady();
1975
1850
  });
1976
- void enqueue();
1977
- function watch(topic, input, deliver) {
1851
+ if (options.adapter.roomStream === undefined)
1852
+ void sync('unstreamed');
1853
+ function planned(input, reach) {
1854
+ return planOf(input, roomFacts(reach), planReach(reach));
1855
+ }
1856
+ function watchPlan(plan, deliver) {
1857
+ return readSide.watch(plan, identityOf(plan), deliver);
1858
+ }
1859
+ async function searched(input, reach) {
1860
+ const plan = searchPlanOf(input);
1861
+ const selected = plan.documents === undefined && reach === null
1862
+ ? null
1863
+ : (await readSide.query(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
1864
+ return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
1865
+ }
1866
+ const watch = (topic, input, deliver) => {
1978
1867
  if (topic === 'room-drafts') {
1979
1868
  const listener = { notify: deliver, ready: false };
1980
1869
  roomDraftListeners.add(listener);
@@ -1995,6 +1884,8 @@ export function composeRuntime(options) {
1995
1884
  if (topic === 'conversation') {
1996
1885
  return conversations.watch(input[0], input[1], deliver);
1997
1886
  }
1887
+ if (topic === 'conversations')
1888
+ return conversations.watchList(deliver);
1998
1889
  if (topic === 'presentation') {
1999
1890
  const listener = {
2000
1891
  app: input[0],
@@ -2057,9 +1948,8 @@ export function composeRuntime(options) {
2057
1948
  queueSubscriptions.delete(queue);
2058
1949
  };
2059
1950
  }
2060
- const plan = planOf(input[0], roomFacts());
2061
- return readSide.watch(plan, identityOf(plan), deliver);
2062
- }
1951
+ return watchPlan(planned(input[0], null), deliver);
1952
+ };
2063
1953
  const md = {
2064
1954
  loaded() {
2065
1955
  return openLocal().then(() => undefined);
@@ -2067,18 +1957,8 @@ export function composeRuntime(options) {
2067
1957
  rooms() {
2068
1958
  return listed();
2069
1959
  },
2070
- slots() {
2071
- return slotted();
2072
- },
2073
- query: ((input) => readSide.query(planOf(input, roomFacts()))),
2074
- async search(input) {
2075
- const plan = searchPlanOf(input);
2076
- const selected = plan.documents === undefined
2077
- ? null
2078
- : (await readSide.query(filterOf(plan.documents, roomFacts())))
2079
- .documents;
2080
- return searchIndex.search(plan, selected);
2081
- },
1960
+ query: ((input) => readSide.query(planned(input, null))),
1961
+ search: (input) => searched(input, null),
2082
1962
  types(locator) {
2083
1963
  return {
2084
1964
  collection: modelTypes(modelOf(locator)),
@@ -2088,12 +1968,6 @@ export function composeRuntime(options) {
2088
1968
  : modelTypes(modelOf(locator, 'typed-selection')),
2089
1969
  };
2090
1970
  },
2091
- syncStatus() {
2092
- return shown;
2093
- },
2094
- serviceMode() {
2095
- return serviceMode;
2096
- },
2097
1971
  roomDraft: {
2098
1972
  list() {
2099
1973
  return exclusive(async () => {
@@ -2174,9 +2048,6 @@ export function composeRuntime(options) {
2174
2048
  save(room, named, writtenBy) {
2175
2049
  return exclusive(() => saveRoomDrafts(room, named, writtenBy));
2176
2050
  },
2177
- outcome(identifier) {
2178
- return outcomes.get(identifier) ?? null;
2179
- },
2180
2051
  history(locator, path, query) {
2181
2052
  return readHistory(locator, path, query);
2182
2053
  },
@@ -2191,15 +2062,10 @@ export function composeRuntime(options) {
2191
2062
  log() {
2192
2063
  return journal.export();
2193
2064
  },
2194
- storageLoss() {
2195
- return storageLoss;
2196
- },
2197
2065
  persistence() {
2198
2066
  return options.shell.persistentStorage;
2199
2067
  },
2200
- observe: ((topic, ...input) => opened((deliver) => watch(topic, input, (message) => {
2201
- deliver(structuredClone(message));
2202
- }), topic === 'query')),
2068
+ observe: observing(watch),
2203
2069
  async reauthorize() {
2204
2070
  if (refusal === null || !refusal.reauthorizationOffered)
2205
2071
  return;
@@ -2208,8 +2074,19 @@ export function composeRuntime(options) {
2208
2074
  await mutations;
2209
2075
  authorization = undefined;
2210
2076
  discoveryPaused = false;
2211
- await enqueue();
2077
+ if (options.adapter.roomStream === undefined)
2078
+ await sync('unstreamed');
2079
+ else
2080
+ await task(ensureAuthorization);
2212
2081
  },
2213
2082
  };
2214
- return { md, stop, watch };
2083
+ const reachable = {
2084
+ md,
2085
+ watch,
2086
+ plan: planned,
2087
+ query: (plan) => readSide.query(plan),
2088
+ watchPlan,
2089
+ search: searched,
2090
+ };
2091
+ return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2215
2092
  }