@agentshouse/mdruntime 0.1.0-c3903a68dcbbf → 0.1.0-c6e4c19c13638

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 (79) hide show
  1. package/AGENTS.md +72 -41
  2. package/README.md +11 -11
  3. package/dist/adapter.d.ts +80 -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 +3 -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 +11 -28
  21. package/dist/connection.d.ts.map +1 -1
  22. package/dist/connection.js +518 -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 +78 -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/paths.d.ts +0 -1
  44. package/dist/paths.d.ts.map +1 -1
  45. package/dist/paths.js +0 -16
  46. package/dist/query.d.ts +8 -3
  47. package/dist/query.d.ts.map +1 -1
  48. package/dist/query.js +104 -36
  49. package/dist/reach.d.ts +40 -0
  50. package/dist/reach.d.ts.map +1 -0
  51. package/dist/reach.js +270 -0
  52. package/dist/react.d.ts +31 -0
  53. package/dist/react.d.ts.map +1 -0
  54. package/dist/react.js +62 -0
  55. package/dist/read-side.d.ts +4 -0
  56. package/dist/read-side.d.ts.map +1 -1
  57. package/dist/read-side.js +116 -54
  58. package/dist/refresh.d.ts +2 -1
  59. package/dist/refresh.d.ts.map +1 -1
  60. package/dist/refresh.js +7 -3
  61. package/dist/replica.d.ts +4 -4
  62. package/dist/replica.d.ts.map +1 -1
  63. package/dist/replica.js +14 -19
  64. package/dist/room-draft.d.ts +3 -0
  65. package/dist/room-draft.d.ts.map +1 -1
  66. package/dist/room-draft.js +6 -4
  67. package/dist/rows.d.ts +2 -1
  68. package/dist/rows.d.ts.map +1 -1
  69. package/dist/rows.js +75 -38
  70. package/dist/save.d.ts +1 -2
  71. package/dist/save.d.ts.map +1 -1
  72. package/dist/save.js +92 -51
  73. package/dist/store.d.ts +6 -16
  74. package/dist/store.d.ts.map +1 -1
  75. package/dist/store.js +10 -32
  76. package/dist/wire.d.ts +51 -16
  77. package/dist/wire.d.ts.map +1 -1
  78. package/dist/wire.js +50 -65
  79. 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, {
@@ -427,18 +395,16 @@ export function composeRuntime(options) {
427
395
  rooms.set(room.locator, {
428
396
  name: room.name,
429
397
  locator: room.locator,
398
+ handle: room.handle,
430
399
  operations: room.operations,
431
400
  replica: room.replica,
432
- attachedModel: room.attachedModel,
433
401
  model: modelTypes(modelOf(room.locator)),
434
402
  typedSelection: selection === null
435
403
  ? null
436
404
  : {
437
405
  roots: selection.roots,
438
- attachedModel: selection.attachedModel,
439
406
  model: modelTypes(modelOf(room.locator, 'typed-selection')),
440
407
  },
441
- untypedFolders: room.untypedFolders ?? [],
442
408
  providerOwnedPaths: room.providerOwnedPaths,
443
409
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
444
410
  attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
@@ -453,9 +419,6 @@ export function composeRuntime(options) {
453
419
  discovered.clear();
454
420
  for (const room of listed)
455
421
  discovered.set(room.locator, room);
456
- for (const locator of wanted.keys())
457
- if (!listedLocators.has(locator))
458
- wanted.delete(locator);
459
422
  await dropUnlisted(listedLocators, local);
460
423
  for (const room of listed) {
461
424
  const held = replicas.get(room.locator)?.confirmed.record;
@@ -477,12 +440,6 @@ export function composeRuntime(options) {
477
440
  await exclusive(async () => {
478
441
  const unlisted = unlistedHeld(listed);
479
442
  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
443
  const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
487
444
  room: true,
488
445
  queue: true,
@@ -495,6 +452,7 @@ export function composeRuntime(options) {
495
452
  rooms.delete(locator);
496
453
  roomDrafts.delete(locator);
497
454
  unreadable.delete(locator);
455
+ stale.delete(locator);
498
456
  scopedRefusals.delete(locator);
499
457
  sourceRefusals.delete(locator);
500
458
  declarationRefusals.delete(locator);
@@ -503,7 +461,6 @@ export function composeRuntime(options) {
503
461
  models.delete(key);
504
462
  liveModels.delete(key);
505
463
  invalidModels.delete(key);
506
- modelEpoch.delete(key);
507
464
  }
508
465
  journal.record(`room unlisted ${locator}`);
509
466
  }
@@ -516,7 +473,6 @@ export function composeRuntime(options) {
516
473
  if (prior !== undefined &&
517
474
  prior.status !== 'unavailable' &&
518
475
  (persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
519
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
520
476
  invalidModels.add(key);
521
477
  }
522
478
  const model = persisted.status === 'unavailable' ||
@@ -531,10 +487,9 @@ export function composeRuntime(options) {
531
487
  return model;
532
488
  }
533
489
  async function hydrate(opened) {
534
- const [records, queues, recordedOutcomes, kept, presented, held, chatDrafts, lastCompleted, declared, named, listedSlots,] = await Promise.all([
490
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
535
491
  opened.all(),
536
492
  opened.queues(),
537
- opened.outcomes(),
538
493
  opened.roomDrafts(),
539
494
  opened.presentation(),
540
495
  opened.conversations(),
@@ -542,11 +497,9 @@ export function composeRuntime(options) {
542
497
  opened.lastCompleted(),
543
498
  opened.providerLimits(),
544
499
  opened.capabilities(),
545
- opened.slots(),
546
500
  ]);
547
501
  limits = declared;
548
502
  capabilities = named;
549
- slots = listedSlots;
550
503
  roomDrafts.clear();
551
504
  for (const record of kept)
552
505
  rememberRoomDraft(record);
@@ -560,12 +513,6 @@ export function composeRuntime(options) {
560
513
  for (const { batch, state } of queue.pending)
561
514
  pendings.set(batch.id, state);
562
515
  }
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
516
  const previousReplicas = new Map(replicas);
570
517
  replicas.clear();
571
518
  for (const record of records) {
@@ -580,7 +527,7 @@ export function composeRuntime(options) {
580
527
  record: retyped,
581
528
  collection: parseCollection(retyped.documents, compositionOf(retyped)),
582
529
  };
583
- replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], rewriting(), prior));
530
+ replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
584
531
  const existing = rooms.get(record.locator);
585
532
  if (existing === undefined)
586
533
  list(record, false);
@@ -591,16 +538,13 @@ export function composeRuntime(options) {
591
538
  attachments: record.attachments,
592
539
  operations: record.operations,
593
540
  replica: record.replica,
594
- attachedModel: record.attachedModel,
595
541
  model: modelTypes(model),
596
542
  typedSelection: record.typedSelection === null
597
543
  ? null
598
544
  : {
599
545
  roots: record.typedSelection.roots,
600
- attachedModel: record.typedSelection.attachedModel,
601
546
  model: modelTypes(selectionModel),
602
547
  },
603
- untypedFolders: record.untypedFolders,
604
548
  });
605
549
  }
606
550
  if (read.kind === 'refused') {
@@ -666,7 +610,7 @@ export function composeRuntime(options) {
666
610
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
667
611
  }
668
612
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
669
- const withdrawn = withdrawals(confirmed, queue, withdrawable, rewriting());
613
+ const withdrawn = withdrawals(confirmed, queue, withdrawable);
670
614
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
671
615
  const returning = [
672
616
  ...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
@@ -687,69 +631,88 @@ export function composeRuntime(options) {
687
631
  });
688
632
  }
689
633
  }
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
634
  await local.write({
698
635
  next: confirmed.record,
699
636
  previous: before?.confirmed.record ?? null,
700
637
  queue: kept,
701
638
  previousQueue: before?.queue ?? [],
702
- outcomes: settledOutcomes,
703
639
  roomDrafts: [...written.values()],
704
640
  });
705
- for (const { id, outcome } of settledOutcomes) {
706
- outcomes.set(id, outcome);
707
- outcomeRooms.set(id, locator);
708
- pendings.delete(id);
641
+ for (const batch of before?.queue ?? []) {
642
+ if (!kept.includes(batch))
643
+ pendings.delete(batch.id);
709
644
  }
710
645
  for (const record of written.values())
711
646
  rememberRoomDraft(record);
712
647
  for (const batch of withdrawn)
713
648
  journal.record(`outcome ${locator} ${batch.id} withdrawn`);
714
- replicas.set(locator, replicaOf(confirmed, kept, rewriting(), before));
649
+ replicas.set(locator, replicaOf(confirmed, kept, before));
715
650
  return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
716
651
  room: settling.room,
717
652
  queue: kept.length !== (before?.queue.length ?? 0),
718
653
  drafts: [...written.keys()],
719
654
  });
720
655
  }
721
- function publish(local, room, confirmed) {
656
+ function adoptModels(locator, record) {
657
+ for (const [attached, model] of [
658
+ ['collection', record.model],
659
+ ['typed-selection', record.selectionModel],
660
+ ]) {
661
+ const key = modelKey(locator, attached);
662
+ models.set(key, model);
663
+ invalidModels.delete(key);
664
+ if (model.status === 'current')
665
+ liveModels.set(key, JSON.stringify(model.snapshot));
666
+ else if (model.status === 'unavailable')
667
+ liveModels.delete(key);
668
+ }
669
+ }
670
+ function remodel(locator, answers, held) {
671
+ const taken = (answer, previous, attached) => {
672
+ if (answer === undefined)
673
+ return previous;
674
+ const { state, refused } = attachedModel(answer, previous);
675
+ if (refused !== null) {
676
+ const where = refused.location === undefined ? '' : ` ${refused.location}`;
677
+ journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
678
+ }
679
+ models.set(modelKey(locator, attached), state);
680
+ return state;
681
+ };
682
+ return {
683
+ collection: taken(answers?.collection, held.collection, 'collection'),
684
+ selection: taken(answers?.selection, held.selection, 'typed-selection'),
685
+ };
686
+ }
687
+ function publish(local, room, confirmed, base) {
722
688
  return exclusive(async () => {
723
689
  if (!continuing())
724
- return;
690
+ return true;
725
691
  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}`);
692
+ if (base !== undefined &&
693
+ (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
694
+ prior.confirmed.record.revisions !== base.record.revisions)) {
695
+ return false;
696
+ }
697
+ adoptModels(room.locator, confirmed.record);
733
698
  const queue = prior?.queue ?? [];
734
- const roomVersion = retained.confirmed.record.roomVersion;
699
+ const roomVersion = confirmed.record.roomVersion;
735
700
  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));
701
+ const accepted = pendings.get(batch.id)?.accepted;
702
+ return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
739
703
  });
740
- const set = await settle(local, room.locator, prior, retained.confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
704
+ const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
741
705
  for (const batch of accepted)
742
706
  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);
707
+ stale.delete(room.locator);
708
+ if (unreadable.delete(room.locator))
709
+ scopedRefusals.delete(room.locator);
748
710
  sourceRefusals.delete(room.locator);
749
711
  declarationRefusals.delete(room.locator);
750
712
  list(room, true);
751
713
  publishChangeSet(set);
752
714
  moveTo({ ...status, ...attention() });
715
+ return true;
753
716
  });
754
717
  }
755
718
  async function saveRoomDrafts(locator, named, writtenBy) {
@@ -778,7 +741,7 @@ export function composeRuntime(options) {
778
741
  operations: room.operations,
779
742
  providerOwnedPaths: room.providerOwnedPaths,
780
743
  providerLimits: limits,
781
- rewriting: rewriting(),
744
+ handle: room.handle,
782
745
  visible: replica.visible,
783
746
  changes: parts.flatMap(({ changes }) => changes),
784
747
  bases: parts.flatMap(({ bases }) => bases),
@@ -802,15 +765,16 @@ export function composeRuntime(options) {
802
765
  return { outcome: 'refused', rule: 'durable-write' };
803
766
  }
804
767
  pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
768
+ recorded = false;
805
769
  for (const path of consumed)
806
770
  forgetRoomDraft(locator, path);
807
- replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting(), replica));
771
+ replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
808
772
  publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
809
773
  queue: true,
810
774
  drafts: consumed,
811
775
  }));
812
776
  moveTo({ ...saving(status), ...attention() });
813
- void enqueue();
777
+ submitLater(locator);
814
778
  return { outcome: 'saved', identifier: built.batch.id };
815
779
  }
816
780
  async function mark(local, locator, identifier, submission, lost) {
@@ -875,10 +839,10 @@ export function composeRuntime(options) {
875
839
  return { kind: 'transport-uncertainty' };
876
840
  }
877
841
  }
878
- function declaredCards() {
842
+ function declaredAgents() {
879
843
  for (const capability of capabilities) {
880
- if (capability.conversationCards !== undefined)
881
- return capability.conversationCards;
844
+ if (capability.conversationAgents !== undefined)
845
+ return capability.conversationAgents;
882
846
  }
883
847
  return undefined;
884
848
  }
@@ -909,8 +873,6 @@ export function composeRuntime(options) {
909
873
  journal.record(`transfer action ${name}`);
910
874
  try {
911
875
  const result = await options.adapter.action(authorization, { name, input });
912
- if (result.kind === 'action' && result.at !== undefined)
913
- want(result.at);
914
876
  if (result.kind === 'scoped-refusal') {
915
877
  const room = replicas.get(result.locator)?.confirmed.record;
916
878
  if (room !== undefined)
@@ -942,32 +904,11 @@ export function composeRuntime(options) {
942
904
  options.adapter.reached(locator, written, roomVersion) &&
943
905
  !options.adapter.reached(locator, roomVersion, written));
944
906
  }
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) {
907
+ async function pullRefresh(authorized, confirmed) {
967
908
  if (!continuing())
968
909
  return unconfirmed;
969
910
  try {
970
- const result = await refreshRoom(options.adapter, authorized, confirmed, continuing, minPosition);
911
+ const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
971
912
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
972
913
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
973
914
  return result;
@@ -984,7 +925,7 @@ export function composeRuntime(options) {
984
925
  const result = await bootstrapRoom(options.adapter, authorized, room, {
985
926
  collection: modelOf(room.locator),
986
927
  selection: modelOf(room.locator, 'typed-selection'),
987
- }, continuing);
928
+ }, (models, held) => remodel(room.locator, models, held), continuing);
988
929
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
989
930
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
990
931
  return result;
@@ -997,20 +938,28 @@ export function composeRuntime(options) {
997
938
  async function advance(room, local, authorized) {
998
939
  const held = replicas.get(room.locator);
999
940
  if (held !== undefined) {
1000
- const refreshed = await pullRefresh(authorized, held.confirmed, reachOf(room.locator));
941
+ const refreshed = await pullRefresh(authorized, held.confirmed);
1001
942
  if (!continuing())
1002
943
  return unconfirmed;
1003
944
  if (refreshed.kind === 'still-current') {
945
+ stale.delete(room.locator);
1004
946
  if (sourceRefusals.has(room.locator))
1005
- return unconfirmed;
947
+ return null;
1006
948
  if (unreadable.delete(room.locator)) {
949
+ scopedRefusals.delete(room.locator);
1007
950
  list(room, true);
1008
951
  emitRooms();
1009
952
  }
1010
953
  return null;
1011
954
  }
1012
955
  if (refreshed.kind === 'advanced') {
1013
- await publish(local, room, refreshed.confirmed);
956
+ if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
957
+ const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
958
+ const reached = now !== undefined &&
959
+ options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
960
+ return reached ? null : advance(room, local, authorized);
961
+ }
962
+ await retainModel(local, room, 'collection', modelOf(room.locator));
1014
963
  return null;
1015
964
  }
1016
965
  if (refreshed.kind !== 'beyond-horizon')
@@ -1025,7 +974,10 @@ export function composeRuntime(options) {
1025
974
  return null;
1026
975
  }
1027
976
  async function withhold(withheld, room, local, progress) {
1028
- progress.complete = false;
977
+ if (withheld.kind === 'refused')
978
+ stale.delete(room.locator);
979
+ else
980
+ stale.add(room.locator);
1029
981
  if (withheld.kind === 'scoped-refusal') {
1030
982
  await scoped(withheld, room, local);
1031
983
  }
@@ -1063,84 +1015,39 @@ export function composeRuntime(options) {
1063
1015
  journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
1064
1016
  }
1065
1017
  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
- };
1018
+ if (result.kind === 'expired')
1019
+ return { batch, returned: { kind: 'expired' } };
1020
+ return { batch, returned: { kind: result.kind, code: result.code, fields: result.fields } };
1095
1021
  }
1096
1022
  function logAccepted(locator, submitted, accepted) {
1097
1023
  journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
1098
1024
  (accepted.author === undefined ? '' : ` author ${accepted.author}`) +
1099
1025
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1100
1026
  }
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) {
1027
+ async function adoptAccepted(room, local, submitted, accepted) {
1123
1028
  if (!continuing())
1124
1029
  return unconfirmed;
1125
1030
  const current = replicas.get(room.locator);
1126
1031
  if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1127
1032
  return null;
1128
1033
  }
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')
1034
+ const held = current.confirmed.record.roomVersion;
1035
+ logAccepted(room.locator, submitted, accepted);
1036
+ const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1037
+ ? { kind: 'staged', confirmed: current.confirmed }
1038
+ : applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
1039
+ ? accepted.roomVersion
1040
+ : held, accepted.roomVersion);
1041
+ if (applied.kind !== 'staged') {
1042
+ const state = pendings.get(submitted.id);
1043
+ await local.accept(room.locator, submitted.id, accepted.roomVersion);
1044
+ if (state !== undefined)
1045
+ pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1046
+ emitQueue(room.locator);
1139
1047
  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
- });
1048
+ }
1049
+ const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1050
+ pendings.delete(submitted.id);
1144
1051
  unreadable.delete(room.locator);
1145
1052
  sourceRefusals.delete(room.locator);
1146
1053
  declarationRefusals.delete(room.locator);
@@ -1149,17 +1056,8 @@ export function composeRuntime(options) {
1149
1056
  moveTo({ ...status, ...attention() });
1150
1057
  return null;
1151
1058
  }
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));
1059
+ async function adopted(room, local, submitted, accepted) {
1060
+ const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
1163
1061
  if (failed === null)
1164
1062
  return true;
1165
1063
  if (failed.kind === 'refused' && continuing())
@@ -1172,10 +1070,8 @@ export function composeRuntime(options) {
1172
1070
  const head = replica?.queue[0];
1173
1071
  if (replica === undefined || head === undefined)
1174
1072
  return;
1175
- if (outcomes.get(head.id)?.kind === 'accepted') {
1176
- progress.complete = false;
1073
+ if (pendings.get(head.id)?.accepted !== undefined)
1177
1074
  return;
1178
- }
1179
1075
  const state = pendings.get(head.id);
1180
1076
  const { submission, lost: earlier } = state ?? {
1181
1077
  submission: { state: 'waiting' },
@@ -1203,6 +1099,7 @@ export function composeRuntime(options) {
1203
1099
  return;
1204
1100
  }
1205
1101
  const opensBefore = openSignals;
1102
+ const answered = awaitingAnswer(room.locator);
1206
1103
  try {
1207
1104
  result = await options.adapter.mutate(authorized, {
1208
1105
  locator: room.locator,
@@ -1211,11 +1108,14 @@ export function composeRuntime(options) {
1211
1108
  }, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
1212
1109
  }
1213
1110
  catch {
1111
+ answered();
1214
1112
  progress.complete = false;
1215
1113
  if (continuing())
1216
1114
  progress.reachable = false;
1217
1115
  return;
1218
1116
  }
1117
+ if (result.kind !== 'accepted' || !continuing())
1118
+ answered();
1219
1119
  if (!continuing())
1220
1120
  return;
1221
1121
  if (result.kind === 'transport-uncertainty') {
@@ -1268,6 +1168,7 @@ export function composeRuntime(options) {
1268
1168
  const reset = result.reset;
1269
1169
  progress.complete = false;
1270
1170
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1171
+ armTimer();
1271
1172
  return;
1272
1173
  }
1273
1174
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
@@ -1294,98 +1195,35 @@ export function composeRuntime(options) {
1294
1195
  return withhold(withheld, room, local, progress);
1295
1196
  continue;
1296
1197
  }
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);
1198
+ const kept = await adopted(room, local, head, result).finally(answered);
1199
+ if (!kept) {
1310
1200
  progress.complete = false;
1311
1201
  return;
1312
1202
  }
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
1203
  }
1372
- return;
1204
+ }
1205
+ function awaitingAnswer(locator) {
1206
+ let release = () => { };
1207
+ const awaited = new Promise((resolve) => {
1208
+ release = resolve;
1209
+ });
1210
+ answering.set(locator, awaited);
1211
+ return () => {
1212
+ if (answering.get(locator) === awaited)
1213
+ answering.delete(locator);
1214
+ release();
1215
+ };
1373
1216
  }
1374
1217
  async function retainModel(local, room, attached, next) {
1375
1218
  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
1219
  const previous = modelOf(room.locator, attached);
1380
1220
  const heldBefore = replicas.get(room.locator)?.confirmed.record;
1381
1221
  if (JSON.stringify(previous) === JSON.stringify(next) &&
1382
1222
  (heldBefore === undefined ||
1383
1223
  (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
1384
1224
  JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
1385
- JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
1386
1225
  JSON.stringify(heldBefore.typedSelection) ===
1387
- JSON.stringify(room.typedSelection ?? null) &&
1388
- JSON.stringify(heldBefore.untypedFolders) === JSON.stringify(room.untypedFolders ?? []))))
1226
+ JSON.stringify(room.typedSelection ?? null))))
1389
1227
  return;
1390
1228
  if (next.status === 'unavailable') {
1391
1229
  invalidModels.add(key);
@@ -1394,17 +1232,13 @@ export function composeRuntime(options) {
1394
1232
  await exclusive(async () => {
1395
1233
  if (!continuing())
1396
1234
  return;
1397
- if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
1398
- return;
1399
1235
  const held = replicas.get(room.locator);
1400
1236
  if (held !== undefined) {
1401
1237
  const declared = {
1402
1238
  ...held.confirmed.record,
1403
1239
  operations: room.operations,
1404
1240
  replica: room.replica,
1405
- attachedModel: room.attachedModel,
1406
1241
  typedSelection: room.typedSelection ?? null,
1407
- untypedFolders: room.untypedFolders ?? [],
1408
1242
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1409
1243
  };
1410
1244
  const reclassified = sameMembership(declared, held.confirmed.record)
@@ -1415,7 +1249,6 @@ export function composeRuntime(options) {
1415
1249
  : {
1416
1250
  ...declared,
1417
1251
  typedSelection: held.confirmed.record.typedSelection,
1418
- untypedFolders: held.confirmed.record.untypedFolders,
1419
1252
  };
1420
1253
  const read = reclassified.kind === 'deferred' ? readConfirmed(record, held.confirmed) : reclassified;
1421
1254
  await local.write({
@@ -1424,7 +1257,7 @@ export function composeRuntime(options) {
1424
1257
  queue: held.queue,
1425
1258
  });
1426
1259
  if (read.kind === 'staged') {
1427
- replicas.set(room.locator, replicaOf(read.confirmed, held.queue, rewriting(), held));
1260
+ replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1428
1261
  if (declarationRefusals.delete(room.locator)) {
1429
1262
  sourceRefusals.delete(room.locator);
1430
1263
  unreadable.delete(room.locator);
@@ -1448,77 +1281,6 @@ export function composeRuntime(options) {
1448
1281
  publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
1449
1282
  });
1450
1283
  }
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
1284
  async function scoped(result, room, local) {
1523
1285
  scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
1524
1286
  let scopedAway = room;
@@ -1546,14 +1308,34 @@ export function composeRuntime(options) {
1546
1308
  emitRooms();
1547
1309
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
1548
1310
  }
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) => {
1311
+ function heldRooms(listed) {
1312
+ return listed.flatMap((room) => {
1313
+ const record = replicas.get(room.locator)?.confirmed.record;
1314
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1315
+ return [];
1316
+ return [
1317
+ {
1318
+ locator: room.locator,
1319
+ roomVersion: record.roomVersion,
1320
+ documents: record.documents,
1321
+ revisions: record.revisions,
1322
+ attachments: record.attachments,
1323
+ },
1324
+ ];
1325
+ });
1326
+ }
1327
+ async function synchronize(listed, local, progress, attempt) {
1328
+ const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1329
+ const ready = listed.filter((_, index) => {
1552
1330
  const outcome = prepared[index];
1553
1331
  return outcome?.status === 'fulfilled' && outcome.value;
1554
1332
  });
1555
- const rooms = await Promise.allSettled(ready.map((room) => synchronizeRoom(room, local, progress)));
1556
- const failed = [...prepared, ...rooms].find((room) => room.status === 'rejected');
1333
+ if (attempt === 'opened')
1334
+ options.adapter.roomStream?.hold(heldRooms(ready));
1335
+ const loaded = await Promise.allSettled(ready
1336
+ .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1337
+ .map((room) => loadRoom(room, local, progress)));
1338
+ const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1557
1339
  if (failed !== undefined)
1558
1340
  throw failed.reason;
1559
1341
  }
@@ -1570,11 +1352,8 @@ export function composeRuntime(options) {
1570
1352
  progress.complete = false;
1571
1353
  return false;
1572
1354
  }
1573
- const selection = room.typedSelection;
1574
- await Promise.all([
1575
- acquireModel(local, room, authorization, null),
1576
- ...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
1577
- ]);
1355
+ if (prior !== undefined)
1356
+ await retainModel(local, room, 'collection', modelOf(room.locator));
1578
1357
  if (discoveryPaused || authorization === undefined)
1579
1358
  return false;
1580
1359
  if (room.replica.admission !== 'admitted') {
@@ -1582,24 +1361,53 @@ export function composeRuntime(options) {
1582
1361
  progress.complete = false;
1583
1362
  return false;
1584
1363
  }
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
1364
  return true;
1591
1365
  }
1592
- async function synchronizeRoom(room, local, progress) {
1366
+ async function caughtUp(room, local, progress) {
1367
+ const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1368
+ if (held === undefined || authorization === undefined)
1369
+ return;
1370
+ const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1371
+ if (!behind)
1372
+ return;
1373
+ const withheld = await advance(room, local, authorization);
1374
+ if (withheld !== null)
1375
+ await withhold(withheld, room, local, progress);
1376
+ }
1377
+ function revoked(locator) {
1378
+ return ['collection', 'typed-selection'].some((attached) => {
1379
+ const model = modelOf(locator, attached);
1380
+ return model.status === 'unavailable' && model.reason === 'revoked';
1381
+ });
1382
+ }
1383
+ async function loadRoom(room, local, progress) {
1593
1384
  if (discoveryPaused || authorization === undefined)
1594
1385
  return;
1386
+ if (replicas.has(room.locator) && revoked(room.locator)) {
1387
+ await reload(room, local, progress);
1388
+ return;
1389
+ }
1595
1390
  const withheld = await advance(room, local, authorization);
1596
1391
  if (withheld !== null) {
1597
1392
  await withhold(withheld, room, local, progress);
1598
1393
  return;
1599
1394
  }
1600
- await submitRoom(room, local, authorization, progress);
1395
+ await caughtUp(room, local, progress);
1601
1396
  }
1602
- async function cycle(local, progress) {
1397
+ async function reload(room, local, progress) {
1398
+ if (discoveryPaused || authorization === undefined)
1399
+ return;
1400
+ const bootstrapped = await pullBootstrap(authorization, room);
1401
+ if (!continuing())
1402
+ return;
1403
+ if (bootstrapped.kind !== 'staged') {
1404
+ await withhold(bootstrapped, room, local, progress);
1405
+ return;
1406
+ }
1407
+ await publish(local, room, bootstrapped.confirmed);
1408
+ await caughtUp(room, local, progress);
1409
+ }
1410
+ async function discoverRooms(local, progress, attempt) {
1603
1411
  if (closed || authorization === undefined) {
1604
1412
  notifyReady();
1605
1413
  return;
@@ -1625,6 +1433,10 @@ export function composeRuntime(options) {
1625
1433
  }
1626
1434
  }
1627
1435
  progress.reachable = false;
1436
+ progress.complete = false;
1437
+ if (attempt === 'opened') {
1438
+ options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1439
+ }
1628
1440
  notifyReady();
1629
1441
  return;
1630
1442
  }
@@ -1639,17 +1451,6 @@ export function composeRuntime(options) {
1639
1451
  notifyReady();
1640
1452
  return;
1641
1453
  }
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
1454
  for (const room of result.rooms) {
1654
1455
  if (room.operations === undefined ||
1655
1456
  !['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
@@ -1657,8 +1458,7 @@ export function composeRuntime(options) {
1657
1458
  room.replica === undefined ||
1658
1459
  typeof room.replica.selection !== 'string' ||
1659
1460
  room.replica.selection.length === 0 ||
1660
- !['admitted', 'unavailable'].includes(room.replica.admission) ||
1661
- !declaredIdentity(room.attachedModel)) {
1461
+ !['admitted', 'unavailable'].includes(room.replica.admission)) {
1662
1462
  journal.record(`invalid model or replica declaration ${room.locator}`);
1663
1463
  await refuse('missing-required-guarantee');
1664
1464
  notifyReady();
@@ -1670,20 +1470,6 @@ export function composeRuntime(options) {
1670
1470
  notifyReady();
1671
1471
  return;
1672
1472
  }
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
1473
  }
1688
1474
  if (refusal !== null) {
1689
1475
  refusal = null;
@@ -1692,29 +1478,13 @@ export function composeRuntime(options) {
1692
1478
  const previousCapabilities = capabilities;
1693
1479
  capabilities = result.capabilities;
1694
1480
  notifyReady();
1695
- if (declared.has('room-version-notifications'))
1696
- observeRoomVersions();
1697
- const discoveredSlots = result.slots ?? [];
1698
1481
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1699
- JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities) ||
1700
- JSON.stringify(discoveredSlots) !== JSON.stringify(slots)) {
1482
+ JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
1701
1483
  limits = result.providerLimits;
1702
- slots = discoveredSlots;
1703
- await local.declare(limits, capabilities, slots);
1484
+ await local.declare(limits, capabilities);
1704
1485
  }
1705
1486
  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);
1487
+ await synchronize(result.rooms, local, progress, attempt);
1718
1488
  }
1719
1489
  async function ensureAuthorization() {
1720
1490
  if (authorization !== undefined)
@@ -1728,64 +1498,246 @@ export function composeRuntime(options) {
1728
1498
  throw error;
1729
1499
  }
1730
1500
  }
1731
- async function connect() {
1732
- if (closed || stopping || discoveryPaused) {
1733
- notifyReady();
1501
+ function task(work) {
1502
+ if (stopping)
1503
+ return chain;
1504
+ chain = chain.then(work, work).catch(() => undefined);
1505
+ return chain;
1506
+ }
1507
+ function sync(attempt) {
1508
+ return task(async () => {
1509
+ if (closed || stopping || discoveryPaused) {
1510
+ notifyReady();
1511
+ return;
1512
+ }
1513
+ const progress = { complete: true, reachable: true };
1514
+ underway = attempt === 'opened' ? progress : null;
1515
+ complete = false;
1516
+ recorded = false;
1517
+ moveTo(transferring(status));
1518
+ try {
1519
+ const local = await openLocal();
1520
+ if (closed)
1521
+ return;
1522
+ await ensureAuthorization();
1523
+ if (closed)
1524
+ return;
1525
+ await discoverRooms(local, progress, attempt);
1526
+ }
1527
+ catch {
1528
+ progress.complete = false;
1529
+ if (attempt === 'opened') {
1530
+ options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1531
+ }
1532
+ }
1533
+ finally {
1534
+ notifyReady();
1535
+ }
1536
+ if (attempt === 'opened')
1537
+ return;
1538
+ await finishOpening(progress);
1539
+ });
1540
+ }
1541
+ function closedSocket(revoked) {
1542
+ const opened = socketOpen;
1543
+ socketOpen = false;
1544
+ if (revoked) {
1545
+ void refuse('revoked-authorization');
1734
1546
  return;
1735
1547
  }
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)
1548
+ if (opened || status.providerUnreachable)
1549
+ return;
1550
+ moveTo({ ...status, providerUnreachable: true });
1551
+ }
1552
+ function applyOpening(expired) {
1553
+ return task(async () => {
1554
+ const progress = underway;
1555
+ underway = null;
1556
+ if (closed || stopping)
1745
1557
  return;
1746
- await cycle(local, progress);
1747
- if (closed)
1558
+ if (progress === null) {
1559
+ await reloadExpired(expired);
1748
1560
  return;
1749
- if (progress.complete) {
1750
- completedAt = new Date().toISOString();
1751
- await local.complete(completedAt);
1752
1561
  }
1562
+ try {
1563
+ const local = await openLocal();
1564
+ const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1565
+ const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1566
+ const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
1567
+ if (progress.reachable) {
1568
+ for (const room of held)
1569
+ if (!reloading.includes(room))
1570
+ await confirmModels(room, local);
1571
+ }
1572
+ const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1573
+ if (failed !== undefined)
1574
+ throw failed.reason;
1575
+ }
1576
+ catch {
1577
+ progress.complete = false;
1578
+ progress.reachable = false;
1579
+ }
1580
+ await finishOpening(progress);
1581
+ });
1582
+ }
1583
+ async function reloadExpired(expired) {
1584
+ const progress = { complete: true, reachable: true };
1585
+ try {
1586
+ const local = await openLocal();
1587
+ await Promise.allSettled([...discovered.values()]
1588
+ .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1589
+ .map((room) => reload(room, local, progress)));
1753
1590
  }
1754
1591
  catch {
1755
- progress.complete = false;
1756
- notifyReady();
1592
+ progress.reachable = false;
1593
+ }
1594
+ await rest(progress);
1595
+ }
1596
+ async function confirmModels(room, local) {
1597
+ for (const attached of ['collection', 'typed-selection']) {
1598
+ const model = modelOf(room.locator, attached);
1599
+ if (model.status === 'cached') {
1600
+ await retainModel(local, room, attached, { ...model, status: 'current' });
1601
+ }
1602
+ }
1603
+ }
1604
+ function submitLater(locator) {
1605
+ const running = submitting.get(locator);
1606
+ if (running !== undefined) {
1607
+ running.again = true;
1757
1608
  return;
1758
1609
  }
1759
- finally {
1760
- notifyReady();
1761
- moveTo(settled(status, progress, completedAt, pending(), attention()));
1610
+ const run = { again: false, done: Promise.resolve() };
1611
+ submitting.set(locator, run);
1612
+ run.done = (async () => {
1613
+ try {
1614
+ do {
1615
+ run.again = false;
1616
+ const room = discovered.get(locator);
1617
+ if (closed || stopping || discoveryPaused || room === undefined)
1618
+ return;
1619
+ const progress = { complete: true, reachable: true };
1620
+ try {
1621
+ await ensureAuthorization();
1622
+ if (authorization === undefined)
1623
+ return;
1624
+ await submitRoom(room, await openLocal(), authorization, progress);
1625
+ }
1626
+ catch {
1627
+ progress.reachable = false;
1628
+ }
1629
+ armTimer();
1630
+ await rest(progress);
1631
+ } while (run.again);
1632
+ }
1633
+ finally {
1634
+ submitting.delete(locator);
1635
+ }
1636
+ })();
1637
+ }
1638
+ function submitAll() {
1639
+ for (const [locator, replica] of replicas) {
1640
+ if (replica.queue.length > 0)
1641
+ submitLater(locator);
1762
1642
  }
1763
1643
  }
1764
- async function run() {
1644
+ async function finishOpening(progress) {
1765
1645
  try {
1766
- await connect();
1646
+ if (closed || discoveryPaused) {
1647
+ progress.complete = false;
1648
+ return;
1649
+ }
1650
+ submitAll();
1651
+ await conversations.reconcile();
1652
+ if (status.providerUnreachable)
1653
+ conversations.reconnected();
1654
+ const reading = { complete: true, reachable: true };
1655
+ await conversations.catchUp(reading);
1656
+ if (!reading.reachable)
1657
+ progress.reachable = false;
1658
+ }
1659
+ catch {
1660
+ progress.complete = false;
1767
1661
  }
1768
1662
  finally {
1769
- inflight = false;
1770
- if (queued && !closed) {
1771
- queued = false;
1772
- void enqueue();
1773
- }
1663
+ complete = progress.complete;
1664
+ await rest(progress);
1774
1665
  }
1775
1666
  }
1776
- function observeRoomVersions() {
1777
- const path = options.adapter.roomVersions;
1778
- if (path === undefined || withdrawNotifications !== undefined)
1667
+ async function rest(progress) {
1668
+ if (closed)
1779
1669
  return;
1780
- withdrawNotifications = path.observe((notification) => {
1781
- if (closed || stopping || discoveryPaused)
1670
+ let lastCompleted = status.lastCompleted;
1671
+ let synced = complete && stale.size === 0 && !pending();
1672
+ if (synced && !recorded) {
1673
+ const completedAt = new Date().toISOString();
1674
+ try {
1675
+ await (await openLocal()).complete(completedAt);
1676
+ recorded = true;
1677
+ lastCompleted = completedAt;
1678
+ }
1679
+ catch {
1680
+ synced = false;
1681
+ }
1682
+ }
1683
+ moveTo({
1684
+ state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
1685
+ lastCompleted,
1686
+ providerUnreachable: !progress.reachable,
1687
+ ...attention(),
1688
+ });
1689
+ }
1690
+ function applyRoom(locator, roomVersion) {
1691
+ announced.set(locator, roomVersion);
1692
+ return task(async () => {
1693
+ await answering.get(locator);
1694
+ if (closed || stopping || discoveryPaused || authorization === undefined)
1695
+ return;
1696
+ const room = discovered.get(locator);
1697
+ const held = replicas.get(locator);
1698
+ if (room === undefined || room.replica.admission !== 'admitted')
1782
1699
  return;
1783
- if (notification.modelChanged === true && handed?.includes(notification.locator) === true) {
1784
- void enqueue();
1700
+ if (held !== undefined &&
1701
+ (held.confirmed.record.replica.selection !== room.replica.selection ||
1702
+ options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
1703
+ return;
1704
+ }
1705
+ const during = underway;
1706
+ const progress = during ?? { complete: true, reachable: true };
1707
+ if (during === null)
1708
+ moveTo(transferring(status));
1709
+ try {
1710
+ const local = await openLocal();
1711
+ const withheld = await advance(room, local, authorization);
1712
+ if (withheld !== null)
1713
+ await withhold(withheld, room, local, progress);
1714
+ }
1715
+ catch {
1716
+ stale.add(locator);
1717
+ }
1718
+ finally {
1719
+ if (during === null)
1720
+ await rest(progress);
1721
+ }
1722
+ });
1723
+ }
1724
+ function observeStream() {
1725
+ const stream = options.adapter.roomStream;
1726
+ if (stream === undefined || withdrawStream !== undefined)
1727
+ return;
1728
+ withdrawStream = stream.observe((event) => {
1729
+ if (closed || stopping)
1785
1730
  return;
1731
+ if (event.kind === 'opened') {
1732
+ socketOpen = true;
1733
+ void sync('opened');
1786
1734
  }
1787
- notices.set(notification.locator, notification.roomVersion);
1788
- chain = chain.then(runNotified, runNotified);
1735
+ else if (event.kind === 'closed')
1736
+ closedSocket(event.revoked);
1737
+ else if (event.kind === 'opening')
1738
+ void applyOpening(event.expired);
1739
+ else
1740
+ void applyRoom(event.locator, event.roomVersion);
1789
1741
  });
1790
1742
  }
1791
1743
  function writesHeld() {
@@ -1806,14 +1758,17 @@ export function composeRuntime(options) {
1806
1758
  listener(mode);
1807
1759
  }
1808
1760
  if (releasing)
1809
- void enqueue();
1761
+ submitAll();
1810
1762
  }
1811
1763
  function observeConversations() {
1812
1764
  const stream = options.adapter.conversations;
1813
1765
  if (stream === undefined || withdrawConversations !== undefined)
1814
1766
  return;
1815
1767
  withdrawConversations = stream.observe((event) => {
1816
- conversations.receive(event);
1768
+ if (conversations.receive(event)) {
1769
+ void ready.then(scheduleCurrency);
1770
+ return;
1771
+ }
1817
1772
  if (event.kind !== 'gap' || conversations.current())
1818
1773
  return;
1819
1774
  show();
@@ -1824,7 +1779,7 @@ export function composeRuntime(options) {
1824
1779
  if (currencyQueued || closed || stopping)
1825
1780
  return;
1826
1781
  currencyQueued = true;
1827
- chain = chain.then(runCurrency, runCurrency);
1782
+ void task(runCurrency);
1828
1783
  }
1829
1784
  async function runCurrency() {
1830
1785
  currencyQueued = false;
@@ -1850,95 +1805,12 @@ export function composeRuntime(options) {
1850
1805
  }
1851
1806
  }
1852
1807
  }
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
1808
  function close() {
1937
1809
  if (closed)
1938
1810
  return;
1939
1811
  closed = true;
1940
- withdrawNotifications?.();
1941
- withdrawNotifications = undefined;
1812
+ withdrawStream?.();
1813
+ withdrawStream = undefined;
1942
1814
  withdrawConversations?.();
1943
1815
  withdrawConversations = undefined;
1944
1816
  withdrawServiceMode?.();
@@ -1962,6 +1834,7 @@ export function composeRuntime(options) {
1962
1834
  stopping = true;
1963
1835
  stopTimer();
1964
1836
  await chain.catch(() => undefined);
1837
+ await Promise.all([...submitting.values()].map((run) => run.done));
1965
1838
  await mutations;
1966
1839
  close();
1967
1840
  }
@@ -1970,11 +1843,29 @@ export function composeRuntime(options) {
1970
1843
  options.signal?.addEventListener('abort', close, { once: true });
1971
1844
  if (options.signal?.aborted)
1972
1845
  close();
1973
- void openLocal().catch(() => {
1846
+ observeStream();
1847
+ void openLocal().then(() => {
1848
+ if (capabilities.length > 0)
1849
+ notifyReady();
1850
+ }, () => {
1974
1851
  notifyReady();
1975
1852
  });
1976
- void enqueue();
1977
- function watch(topic, input, deliver) {
1853
+ if (options.adapter.roomStream === undefined)
1854
+ void sync('unstreamed');
1855
+ function planned(input, reach) {
1856
+ return planOf(input, roomFacts(reach), planReach(reach));
1857
+ }
1858
+ function watchPlan(plan, deliver) {
1859
+ return readSide.watch(plan, identityOf(plan), deliver);
1860
+ }
1861
+ async function searched(input, reach) {
1862
+ const plan = searchPlanOf(input);
1863
+ const selected = plan.documents === undefined && reach === null
1864
+ ? null
1865
+ : (await readSide.query(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
1866
+ return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
1867
+ }
1868
+ const watch = (topic, input, deliver) => {
1978
1869
  if (topic === 'room-drafts') {
1979
1870
  const listener = { notify: deliver, ready: false };
1980
1871
  roomDraftListeners.add(listener);
@@ -1995,6 +1886,8 @@ export function composeRuntime(options) {
1995
1886
  if (topic === 'conversation') {
1996
1887
  return conversations.watch(input[0], input[1], deliver);
1997
1888
  }
1889
+ if (topic === 'conversations')
1890
+ return conversations.watchList(deliver);
1998
1891
  if (topic === 'presentation') {
1999
1892
  const listener = {
2000
1893
  app: input[0],
@@ -2057,9 +1950,8 @@ export function composeRuntime(options) {
2057
1950
  queueSubscriptions.delete(queue);
2058
1951
  };
2059
1952
  }
2060
- const plan = planOf(input[0], roomFacts());
2061
- return readSide.watch(plan, identityOf(plan), deliver);
2062
- }
1953
+ return watchPlan(planned(input[0], null), deliver);
1954
+ };
2063
1955
  const md = {
2064
1956
  loaded() {
2065
1957
  return openLocal().then(() => undefined);
@@ -2067,18 +1959,8 @@ export function composeRuntime(options) {
2067
1959
  rooms() {
2068
1960
  return listed();
2069
1961
  },
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
- },
1962
+ query: ((input) => readSide.query(planned(input, null))),
1963
+ search: (input) => searched(input, null),
2082
1964
  types(locator) {
2083
1965
  return {
2084
1966
  collection: modelTypes(modelOf(locator)),
@@ -2088,12 +1970,6 @@ export function composeRuntime(options) {
2088
1970
  : modelTypes(modelOf(locator, 'typed-selection')),
2089
1971
  };
2090
1972
  },
2091
- syncStatus() {
2092
- return shown;
2093
- },
2094
- serviceMode() {
2095
- return serviceMode;
2096
- },
2097
1973
  roomDraft: {
2098
1974
  list() {
2099
1975
  return exclusive(async () => {
@@ -2174,9 +2050,6 @@ export function composeRuntime(options) {
2174
2050
  save(room, named, writtenBy) {
2175
2051
  return exclusive(() => saveRoomDrafts(room, named, writtenBy));
2176
2052
  },
2177
- outcome(identifier) {
2178
- return outcomes.get(identifier) ?? null;
2179
- },
2180
2053
  history(locator, path, query) {
2181
2054
  return readHistory(locator, path, query);
2182
2055
  },
@@ -2191,15 +2064,10 @@ export function composeRuntime(options) {
2191
2064
  log() {
2192
2065
  return journal.export();
2193
2066
  },
2194
- storageLoss() {
2195
- return storageLoss;
2196
- },
2197
2067
  persistence() {
2198
2068
  return options.shell.persistentStorage;
2199
2069
  },
2200
- observe: ((topic, ...input) => opened((deliver) => watch(topic, input, (message) => {
2201
- deliver(structuredClone(message));
2202
- }), topic === 'query')),
2070
+ observe: observing(watch),
2203
2071
  async reauthorize() {
2204
2072
  if (refusal === null || !refusal.reauthorizationOffered)
2205
2073
  return;
@@ -2208,8 +2076,19 @@ export function composeRuntime(options) {
2208
2076
  await mutations;
2209
2077
  authorization = undefined;
2210
2078
  discoveryPaused = false;
2211
- await enqueue();
2079
+ if (options.adapter.roomStream === undefined)
2080
+ await sync('unstreamed');
2081
+ else
2082
+ await task(ensureAuthorization);
2212
2083
  },
2213
2084
  };
2214
- return { md, stop, watch };
2085
+ const reachable = {
2086
+ md,
2087
+ watch,
2088
+ plan: planned,
2089
+ query: (plan) => readSide.query(plan),
2090
+ watchPlan,
2091
+ search: searched,
2092
+ };
2093
+ return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2215
2094
  }