@agentshouse/mdruntime 0.1.0-c3903a68dcbbf → 0.1.0-c5b50992a4193

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 +75 -41
  2. package/README.md +11 -11
  3. package/dist/adapter.d.ts +81 -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 +47 -0
  9. package/dist/bootstrap.d.ts +5 -3
  10. package/dist/bootstrap.d.ts.map +1 -1
  11. package/dist/bootstrap.js +3 -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 +535 -643
  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 +84 -27
  35. package/dist/model.d.ts +11 -11
  36. package/dist/model.d.ts.map +1 -1
  37. package/dist/model.js +6 -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 +75 -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 +32 -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 +5 -5
  62. package/dist/replica.d.ts.map +1 -1
  63. package/dist/replica.js +16 -21
  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,20 @@ 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
+ let handed = false;
86
+ const submitting = new Map();
87
+ const answering = new Map();
88
+ let complete = false;
89
+ let recorded = false;
90
+ const stale = new Set();
127
91
  let chain = Promise.resolve();
128
92
  let mutations = Promise.resolve();
129
- let inflight = false;
130
- let queued = false;
131
93
  const roomListeners = new Set();
132
94
  const refusalListeners = new Set();
133
95
  const storageLossListeners = new Set();
@@ -138,12 +100,13 @@ export function composeRuntime(options) {
138
100
  const readSide = createReadSide();
139
101
  let withdrawConversations;
140
102
  let currencyQueued = false;
103
+ let socketOpen = false;
141
104
  let serviceMode = 'open';
142
105
  let haltedRefusal = false;
143
106
  let openSignals = 0;
144
107
  const serviceModeListeners = new Set();
145
108
  const conversations = createConversationStore({
146
- cards: declaredCards,
109
+ agents: declaredAgents,
147
110
  documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
148
111
  call: (name, input) => invokeAction(name, input),
149
112
  durably,
@@ -252,20 +215,14 @@ export function composeRuntime(options) {
252
215
  function listed() {
253
216
  return structuredClone([...rooms.values()]);
254
217
  }
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) => {
218
+ function roomFacts(reach = null) {
219
+ const held = [...rooms.values()]
220
+ .filter((room) => holds(reach, room.locator))
221
+ .map((room) => {
222
+ const governed = governs(reach, room);
266
223
  const attached = [
267
- modelOf(room.locator),
268
- ...(room.typedSelection === null ? [] : [modelOf(room.locator, 'typed-selection')]),
224
+ ...(governed.collection ? [modelOf(room.locator)] : []),
225
+ ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
269
226
  ];
270
227
  return {
271
228
  locator: room.locator,
@@ -306,14 +263,18 @@ export function composeRuntime(options) {
306
263
  for (const listener of statusListeners)
307
264
  listener(shown);
308
265
  }
309
- function submissionOf(batch) {
310
- return pendings.get(batch.id)?.submission ?? { state: 'waiting' };
266
+ function entryOf(batch) {
267
+ const state = pendings.get(batch.id);
268
+ if (state?.accepted !== undefined) {
269
+ return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
270
+ }
271
+ return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
311
272
  }
312
273
  function entriesOf(locator) {
313
274
  const replica = replicas.get(locator);
314
275
  if (replica === undefined)
315
276
  return [];
316
- return replica.queue.map((batch) => Object.freeze({ batch, ...submissionOf(batch) }));
277
+ return replica.queue.map(entryOf);
317
278
  }
318
279
  function emitQueue(locator) {
319
280
  const entries = entriesOf(locator);
@@ -339,23 +300,32 @@ export function composeRuntime(options) {
339
300
  for (const app of set.presentations)
340
301
  emitPresentation(app);
341
302
  conversations.changed(set);
342
- const cards = declaredCards();
343
- if (cards !== undefined && set.documents.some((change) => change.locator === cards.locator)) {
344
- scheduleCurrency();
345
- }
346
303
  }
347
304
  function stopTimer() {
348
- if (timer === undefined)
305
+ if (resetTimer === undefined)
349
306
  return;
350
- clearInterval(timer);
351
- timer = undefined;
307
+ clearTimeout(resetTimer);
308
+ resetTimer = undefined;
352
309
  }
353
- function startTimer() {
354
- if (timer !== undefined)
310
+ function armTimer() {
311
+ stopTimer();
312
+ if (closed || stopping)
355
313
  return;
356
- timer = setInterval(() => {
357
- void enqueue();
358
- }, period);
314
+ let earliest = null;
315
+ for (const { submission } of pendings.values()) {
316
+ if (submission.state !== 'reset')
317
+ continue;
318
+ const at = Date.parse(submission.reset);
319
+ if (earliest === null || at < earliest)
320
+ earliest = at;
321
+ }
322
+ if (earliest === null)
323
+ return;
324
+ // A Save House held back goes again only once the reset its answer named has passed.
325
+ resetTimer = setTimeout(() => {
326
+ resetTimer = undefined;
327
+ submitAll();
328
+ }, Math.max(0, earliest - Date.now()));
359
329
  }
360
330
  async function refuse(reason, provider = null) {
361
331
  stopTimer();
@@ -381,7 +351,6 @@ export function composeRuntime(options) {
381
351
  }
382
352
  for (const key of keys) {
383
353
  invalidModels.add(key);
384
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
385
354
  models.set(key, revoked);
386
355
  liveModels.delete(key);
387
356
  }
@@ -396,7 +365,7 @@ export function composeRuntime(options) {
396
365
  };
397
366
  const read = readConfirmed(record, before.confirmed);
398
367
  replicas.set(locator, read.kind === 'staged'
399
- ? replicaOf(read.confirmed, before.queue, rewriting(), before)
368
+ ? replicaOf(read.confirmed, before.queue, before)
400
369
  : { ...before, confirmed: { ...before.confirmed, record } });
401
370
  }
402
371
  rooms.set(locator, {
@@ -427,18 +396,16 @@ export function composeRuntime(options) {
427
396
  rooms.set(room.locator, {
428
397
  name: room.name,
429
398
  locator: room.locator,
399
+ handle: room.handle,
430
400
  operations: room.operations,
431
401
  replica: room.replica,
432
- attachedModel: room.attachedModel,
433
402
  model: modelTypes(modelOf(room.locator)),
434
403
  typedSelection: selection === null
435
404
  ? null
436
405
  : {
437
406
  roots: selection.roots,
438
- attachedModel: selection.attachedModel,
439
407
  model: modelTypes(modelOf(room.locator, 'typed-selection')),
440
408
  },
441
- untypedFolders: room.untypedFolders ?? [],
442
409
  providerOwnedPaths: room.providerOwnedPaths,
443
410
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
444
411
  attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
@@ -453,9 +420,6 @@ export function composeRuntime(options) {
453
420
  discovered.clear();
454
421
  for (const room of listed)
455
422
  discovered.set(room.locator, room);
456
- for (const locator of wanted.keys())
457
- if (!listedLocators.has(locator))
458
- wanted.delete(locator);
459
423
  await dropUnlisted(listedLocators, local);
460
424
  for (const room of listed) {
461
425
  const held = replicas.get(room.locator)?.confirmed.record;
@@ -477,12 +441,6 @@ export function composeRuntime(options) {
477
441
  await exclusive(async () => {
478
442
  const unlisted = unlistedHeld(listed);
479
443
  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
444
  const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
487
445
  room: true,
488
446
  queue: true,
@@ -495,6 +453,7 @@ export function composeRuntime(options) {
495
453
  rooms.delete(locator);
496
454
  roomDrafts.delete(locator);
497
455
  unreadable.delete(locator);
456
+ stale.delete(locator);
498
457
  scopedRefusals.delete(locator);
499
458
  sourceRefusals.delete(locator);
500
459
  declarationRefusals.delete(locator);
@@ -503,7 +462,6 @@ export function composeRuntime(options) {
503
462
  models.delete(key);
504
463
  liveModels.delete(key);
505
464
  invalidModels.delete(key);
506
- modelEpoch.delete(key);
507
465
  }
508
466
  journal.record(`room unlisted ${locator}`);
509
467
  }
@@ -516,7 +474,6 @@ export function composeRuntime(options) {
516
474
  if (prior !== undefined &&
517
475
  prior.status !== 'unavailable' &&
518
476
  (persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
519
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
520
477
  invalidModels.add(key);
521
478
  }
522
479
  const model = persisted.status === 'unavailable' ||
@@ -531,10 +488,9 @@ export function composeRuntime(options) {
531
488
  return model;
532
489
  }
533
490
  async function hydrate(opened) {
534
- const [records, queues, recordedOutcomes, kept, presented, held, chatDrafts, lastCompleted, declared, named, listedSlots,] = await Promise.all([
491
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
535
492
  opened.all(),
536
493
  opened.queues(),
537
- opened.outcomes(),
538
494
  opened.roomDrafts(),
539
495
  opened.presentation(),
540
496
  opened.conversations(),
@@ -542,11 +498,9 @@ export function composeRuntime(options) {
542
498
  opened.lastCompleted(),
543
499
  opened.providerLimits(),
544
500
  opened.capabilities(),
545
- opened.slots(),
546
501
  ]);
547
502
  limits = declared;
548
503
  capabilities = named;
549
- slots = listedSlots;
550
504
  roomDrafts.clear();
551
505
  for (const record of kept)
552
506
  rememberRoomDraft(record);
@@ -560,12 +514,6 @@ export function composeRuntime(options) {
560
514
  for (const { batch, state } of queue.pending)
561
515
  pendings.set(batch.id, state);
562
516
  }
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
517
  const previousReplicas = new Map(replicas);
570
518
  replicas.clear();
571
519
  for (const record of records) {
@@ -580,7 +528,7 @@ export function composeRuntime(options) {
580
528
  record: retyped,
581
529
  collection: parseCollection(retyped.documents, compositionOf(retyped)),
582
530
  };
583
- replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], rewriting(), prior));
531
+ replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
584
532
  const existing = rooms.get(record.locator);
585
533
  if (existing === undefined)
586
534
  list(record, false);
@@ -591,16 +539,13 @@ export function composeRuntime(options) {
591
539
  attachments: record.attachments,
592
540
  operations: record.operations,
593
541
  replica: record.replica,
594
- attachedModel: record.attachedModel,
595
542
  model: modelTypes(model),
596
543
  typedSelection: record.typedSelection === null
597
544
  ? null
598
545
  : {
599
546
  roots: record.typedSelection.roots,
600
- attachedModel: record.typedSelection.attachedModel,
601
547
  model: modelTypes(selectionModel),
602
548
  },
603
- untypedFolders: record.untypedFolders,
604
549
  });
605
550
  }
606
551
  if (read.kind === 'refused') {
@@ -666,10 +611,14 @@ export function composeRuntime(options) {
666
611
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
667
612
  }
668
613
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
669
- const withdrawn = withdrawals(confirmed, queue, withdrawable, rewriting());
614
+ const withdrawn = withdrawals(confirmed, queue, withdrawable);
670
615
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
616
+ const unqueued = withdrawn.map((batch) => ({
617
+ batch,
618
+ returned: landed(batch, confirmed) ? null : { kind: 'withdrawn' },
619
+ }));
671
620
  const returning = [
672
- ...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
621
+ ...[...unqueued].reverse(),
673
622
  ...(settling.leaving === undefined ? [] : [settling.leaving]),
674
623
  ];
675
624
  const written = new Map();
@@ -687,69 +636,89 @@ export function composeRuntime(options) {
687
636
  });
688
637
  }
689
638
  }
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
639
  await local.write({
698
640
  next: confirmed.record,
699
641
  previous: before?.confirmed.record ?? null,
700
642
  queue: kept,
701
643
  previousQueue: before?.queue ?? [],
702
- outcomes: settledOutcomes,
703
644
  roomDrafts: [...written.values()],
704
645
  });
705
- for (const { id, outcome } of settledOutcomes) {
706
- outcomes.set(id, outcome);
707
- outcomeRooms.set(id, locator);
708
- pendings.delete(id);
646
+ for (const batch of before?.queue ?? []) {
647
+ if (!kept.includes(batch))
648
+ pendings.delete(batch.id);
709
649
  }
710
650
  for (const record of written.values())
711
651
  rememberRoomDraft(record);
712
- for (const batch of withdrawn)
713
- journal.record(`outcome ${locator} ${batch.id} withdrawn`);
714
- replicas.set(locator, replicaOf(confirmed, kept, rewriting(), before));
652
+ for (const { batch, returned } of unqueued) {
653
+ journal.record(`outcome ${locator} ${batch.id} ${returned === null ? 'landed' : 'withdrawn'}`);
654
+ }
655
+ replicas.set(locator, replicaOf(confirmed, kept, before));
715
656
  return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
716
657
  room: settling.room,
717
658
  queue: kept.length !== (before?.queue.length ?? 0),
718
659
  drafts: [...written.keys()],
719
660
  });
720
661
  }
721
- function publish(local, room, confirmed) {
662
+ function adoptModels(locator, record) {
663
+ for (const [attached, model] of [
664
+ ['collection', record.model],
665
+ ['typed-selection', record.selectionModel],
666
+ ]) {
667
+ const key = modelKey(locator, attached);
668
+ models.set(key, model);
669
+ invalidModels.delete(key);
670
+ if (model.status === 'current')
671
+ liveModels.set(key, JSON.stringify(model.snapshot));
672
+ else if (model.status === 'unavailable')
673
+ liveModels.delete(key);
674
+ }
675
+ }
676
+ function remodel(locator, answers, held) {
677
+ const taken = (answer, previous, attached) => {
678
+ if (answer === undefined)
679
+ return previous;
680
+ const { state, refused } = attachedModel(answer, previous);
681
+ if (refused !== null) {
682
+ const where = refused.location === undefined ? '' : ` ${refused.location}`;
683
+ journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
684
+ }
685
+ models.set(modelKey(locator, attached), state);
686
+ return state;
687
+ };
688
+ return {
689
+ collection: taken(answers?.collection, held.collection, 'collection'),
690
+ selection: taken(answers?.selection, held.selection, 'typed-selection'),
691
+ };
692
+ }
693
+ function publish(local, room, confirmed, base) {
722
694
  return exclusive(async () => {
723
695
  if (!continuing())
724
- return;
696
+ return true;
725
697
  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}`);
698
+ if (base !== undefined &&
699
+ (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
700
+ prior.confirmed.record.revisions !== base.record.revisions)) {
701
+ return false;
702
+ }
703
+ adoptModels(room.locator, confirmed.record);
733
704
  const queue = prior?.queue ?? [];
734
- const roomVersion = retained.confirmed.record.roomVersion;
705
+ const roomVersion = confirmed.record.roomVersion;
735
706
  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));
707
+ const accepted = pendings.get(batch.id)?.accepted;
708
+ return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
739
709
  });
740
- const set = await settle(local, room.locator, prior, retained.confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
710
+ const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
741
711
  for (const batch of accepted)
742
712
  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);
713
+ stale.delete(room.locator);
714
+ if (unreadable.delete(room.locator))
715
+ scopedRefusals.delete(room.locator);
748
716
  sourceRefusals.delete(room.locator);
749
717
  declarationRefusals.delete(room.locator);
750
718
  list(room, true);
751
719
  publishChangeSet(set);
752
720
  moveTo({ ...status, ...attention() });
721
+ return true;
753
722
  });
754
723
  }
755
724
  async function saveRoomDrafts(locator, named, writtenBy) {
@@ -778,7 +747,7 @@ export function composeRuntime(options) {
778
747
  operations: room.operations,
779
748
  providerOwnedPaths: room.providerOwnedPaths,
780
749
  providerLimits: limits,
781
- rewriting: rewriting(),
750
+ handle: room.handle,
782
751
  visible: replica.visible,
783
752
  changes: parts.flatMap(({ changes }) => changes),
784
753
  bases: parts.flatMap(({ bases }) => bases),
@@ -802,15 +771,16 @@ export function composeRuntime(options) {
802
771
  return { outcome: 'refused', rule: 'durable-write' };
803
772
  }
804
773
  pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
774
+ recorded = false;
805
775
  for (const path of consumed)
806
776
  forgetRoomDraft(locator, path);
807
- replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting(), replica));
777
+ replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
808
778
  publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
809
779
  queue: true,
810
780
  drafts: consumed,
811
781
  }));
812
782
  moveTo({ ...saving(status), ...attention() });
813
- void enqueue();
783
+ submitLater(locator);
814
784
  return { outcome: 'saved', identifier: built.batch.id };
815
785
  }
816
786
  async function mark(local, locator, identifier, submission, lost) {
@@ -875,10 +845,10 @@ export function composeRuntime(options) {
875
845
  return { kind: 'transport-uncertainty' };
876
846
  }
877
847
  }
878
- function declaredCards() {
848
+ function declaredAgents() {
879
849
  for (const capability of capabilities) {
880
- if (capability.conversationCards !== undefined)
881
- return capability.conversationCards;
850
+ if (capability.conversationAgents !== undefined)
851
+ return capability.conversationAgents;
882
852
  }
883
853
  return undefined;
884
854
  }
@@ -909,8 +879,6 @@ export function composeRuntime(options) {
909
879
  journal.record(`transfer action ${name}`);
910
880
  try {
911
881
  const result = await options.adapter.action(authorization, { name, input });
912
- if (result.kind === 'action' && result.at !== undefined)
913
- want(result.at);
914
882
  if (result.kind === 'scoped-refusal') {
915
883
  const room = replicas.get(result.locator)?.confirmed.record;
916
884
  if (room !== undefined)
@@ -942,32 +910,11 @@ export function composeRuntime(options) {
942
910
  options.adapter.reached(locator, written, roomVersion) &&
943
911
  !options.adapter.reached(locator, roomVersion, written));
944
912
  }
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) {
913
+ async function pullRefresh(authorized, confirmed) {
967
914
  if (!continuing())
968
915
  return unconfirmed;
969
916
  try {
970
- const result = await refreshRoom(options.adapter, authorized, confirmed, continuing, minPosition);
917
+ const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
971
918
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
972
919
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
973
920
  return result;
@@ -984,7 +931,7 @@ export function composeRuntime(options) {
984
931
  const result = await bootstrapRoom(options.adapter, authorized, room, {
985
932
  collection: modelOf(room.locator),
986
933
  selection: modelOf(room.locator, 'typed-selection'),
987
- }, continuing);
934
+ }, (models, held) => remodel(room.locator, models, held), continuing);
988
935
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
989
936
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
990
937
  return result;
@@ -997,20 +944,28 @@ export function composeRuntime(options) {
997
944
  async function advance(room, local, authorized) {
998
945
  const held = replicas.get(room.locator);
999
946
  if (held !== undefined) {
1000
- const refreshed = await pullRefresh(authorized, held.confirmed, reachOf(room.locator));
947
+ const refreshed = await pullRefresh(authorized, held.confirmed);
1001
948
  if (!continuing())
1002
949
  return unconfirmed;
1003
950
  if (refreshed.kind === 'still-current') {
951
+ stale.delete(room.locator);
1004
952
  if (sourceRefusals.has(room.locator))
1005
- return unconfirmed;
953
+ return null;
1006
954
  if (unreadable.delete(room.locator)) {
955
+ scopedRefusals.delete(room.locator);
1007
956
  list(room, true);
1008
957
  emitRooms();
1009
958
  }
1010
959
  return null;
1011
960
  }
1012
961
  if (refreshed.kind === 'advanced') {
1013
- await publish(local, room, refreshed.confirmed);
962
+ if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
963
+ const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
964
+ const reached = now !== undefined &&
965
+ options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
966
+ return reached ? null : advance(room, local, authorized);
967
+ }
968
+ await retainModel(local, room, 'collection', modelOf(room.locator));
1014
969
  return null;
1015
970
  }
1016
971
  if (refreshed.kind !== 'beyond-horizon')
@@ -1025,7 +980,10 @@ export function composeRuntime(options) {
1025
980
  return null;
1026
981
  }
1027
982
  async function withhold(withheld, room, local, progress) {
1028
- progress.complete = false;
983
+ if (withheld.kind === 'refused')
984
+ stale.delete(room.locator);
985
+ else
986
+ stale.add(room.locator);
1029
987
  if (withheld.kind === 'scoped-refusal') {
1030
988
  await scoped(withheld, room, local);
1031
989
  }
@@ -1063,84 +1021,39 @@ export function composeRuntime(options) {
1063
1021
  journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
1064
1022
  }
1065
1023
  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
- };
1024
+ if (result.kind === 'expired')
1025
+ return { batch, returned: { kind: 'expired' } };
1026
+ return { batch, returned: { kind: result.kind, code: result.code, fields: result.fields } };
1095
1027
  }
1096
1028
  function logAccepted(locator, submitted, accepted) {
1097
1029
  journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
1098
1030
  (accepted.author === undefined ? '' : ` author ${accepted.author}`) +
1099
1031
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1100
1032
  }
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) {
1033
+ async function adoptAccepted(room, local, submitted, accepted) {
1123
1034
  if (!continuing())
1124
1035
  return unconfirmed;
1125
1036
  const current = replicas.get(room.locator);
1126
1037
  if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1127
1038
  return null;
1128
1039
  }
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')
1040
+ const held = current.confirmed.record.roomVersion;
1041
+ logAccepted(room.locator, submitted, accepted);
1042
+ const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1043
+ ? { kind: 'staged', confirmed: current.confirmed }
1044
+ : applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
1045
+ ? accepted.roomVersion
1046
+ : held, accepted.roomVersion);
1047
+ if (applied.kind !== 'staged') {
1048
+ const state = pendings.get(submitted.id);
1049
+ await local.accept(room.locator, submitted.id, accepted.roomVersion);
1050
+ if (state !== undefined)
1051
+ pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1052
+ emitQueue(room.locator);
1139
1053
  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
- });
1054
+ }
1055
+ const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1056
+ pendings.delete(submitted.id);
1144
1057
  unreadable.delete(room.locator);
1145
1058
  sourceRefusals.delete(room.locator);
1146
1059
  declarationRefusals.delete(room.locator);
@@ -1149,17 +1062,8 @@ export function composeRuntime(options) {
1149
1062
  moveTo({ ...status, ...attention() });
1150
1063
  return null;
1151
1064
  }
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));
1065
+ async function adopted(room, local, submitted, accepted) {
1066
+ const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
1163
1067
  if (failed === null)
1164
1068
  return true;
1165
1069
  if (failed.kind === 'refused' && continuing())
@@ -1172,10 +1076,8 @@ export function composeRuntime(options) {
1172
1076
  const head = replica?.queue[0];
1173
1077
  if (replica === undefined || head === undefined)
1174
1078
  return;
1175
- if (outcomes.get(head.id)?.kind === 'accepted') {
1176
- progress.complete = false;
1079
+ if (pendings.get(head.id)?.accepted !== undefined)
1177
1080
  return;
1178
- }
1179
1081
  const state = pendings.get(head.id);
1180
1082
  const { submission, lost: earlier } = state ?? {
1181
1083
  submission: { state: 'waiting' },
@@ -1203,6 +1105,7 @@ export function composeRuntime(options) {
1203
1105
  return;
1204
1106
  }
1205
1107
  const opensBefore = openSignals;
1108
+ const answered = awaitingAnswer(room.locator);
1206
1109
  try {
1207
1110
  result = await options.adapter.mutate(authorized, {
1208
1111
  locator: room.locator,
@@ -1211,11 +1114,14 @@ export function composeRuntime(options) {
1211
1114
  }, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
1212
1115
  }
1213
1116
  catch {
1117
+ answered();
1214
1118
  progress.complete = false;
1215
1119
  if (continuing())
1216
1120
  progress.reachable = false;
1217
1121
  return;
1218
1122
  }
1123
+ if (result.kind !== 'accepted' || !continuing())
1124
+ answered();
1219
1125
  if (!continuing())
1220
1126
  return;
1221
1127
  if (result.kind === 'transport-uncertainty') {
@@ -1268,6 +1174,7 @@ export function composeRuntime(options) {
1268
1174
  const reset = result.reset;
1269
1175
  progress.complete = false;
1270
1176
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1177
+ armTimer();
1271
1178
  return;
1272
1179
  }
1273
1180
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
@@ -1280,7 +1187,8 @@ export function composeRuntime(options) {
1280
1187
  return;
1281
1188
  }
1282
1189
  const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1283
- leaving: result.kind === 'expired' && landed(head, current.confirmed)
1190
+ leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1191
+ (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1284
1192
  ? { ...left, returned: null }
1285
1193
  : left,
1286
1194
  });
@@ -1294,98 +1202,35 @@ export function composeRuntime(options) {
1294
1202
  return withhold(withheld, room, local, progress);
1295
1203
  continue;
1296
1204
  }
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);
1310
- progress.complete = false;
1311
- return;
1312
- }
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
- }
1205
+ const kept = await adopted(room, local, head, result).finally(answered);
1206
+ if (!kept) {
1341
1207
  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
1208
  return;
1354
1209
  }
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
1210
  }
1372
- return;
1211
+ }
1212
+ function awaitingAnswer(locator) {
1213
+ let release = () => { };
1214
+ const awaited = new Promise((resolve) => {
1215
+ release = resolve;
1216
+ });
1217
+ answering.set(locator, awaited);
1218
+ return () => {
1219
+ if (answering.get(locator) === awaited)
1220
+ answering.delete(locator);
1221
+ release();
1222
+ };
1373
1223
  }
1374
1224
  async function retainModel(local, room, attached, next) {
1375
1225
  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
1226
  const previous = modelOf(room.locator, attached);
1380
1227
  const heldBefore = replicas.get(room.locator)?.confirmed.record;
1381
1228
  if (JSON.stringify(previous) === JSON.stringify(next) &&
1382
1229
  (heldBefore === undefined ||
1383
1230
  (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
1384
1231
  JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
1385
- JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
1386
1232
  JSON.stringify(heldBefore.typedSelection) ===
1387
- JSON.stringify(room.typedSelection ?? null) &&
1388
- JSON.stringify(heldBefore.untypedFolders) === JSON.stringify(room.untypedFolders ?? []))))
1233
+ JSON.stringify(room.typedSelection ?? null))))
1389
1234
  return;
1390
1235
  if (next.status === 'unavailable') {
1391
1236
  invalidModels.add(key);
@@ -1394,17 +1239,13 @@ export function composeRuntime(options) {
1394
1239
  await exclusive(async () => {
1395
1240
  if (!continuing())
1396
1241
  return;
1397
- if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
1398
- return;
1399
1242
  const held = replicas.get(room.locator);
1400
1243
  if (held !== undefined) {
1401
1244
  const declared = {
1402
1245
  ...held.confirmed.record,
1403
1246
  operations: room.operations,
1404
1247
  replica: room.replica,
1405
- attachedModel: room.attachedModel,
1406
1248
  typedSelection: room.typedSelection ?? null,
1407
- untypedFolders: room.untypedFolders ?? [],
1408
1249
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1409
1250
  };
1410
1251
  const reclassified = sameMembership(declared, held.confirmed.record)
@@ -1415,7 +1256,6 @@ export function composeRuntime(options) {
1415
1256
  : {
1416
1257
  ...declared,
1417
1258
  typedSelection: held.confirmed.record.typedSelection,
1418
- untypedFolders: held.confirmed.record.untypedFolders,
1419
1259
  };
1420
1260
  const read = reclassified.kind === 'deferred' ? readConfirmed(record, held.confirmed) : reclassified;
1421
1261
  await local.write({
@@ -1424,7 +1264,7 @@ export function composeRuntime(options) {
1424
1264
  queue: held.queue,
1425
1265
  });
1426
1266
  if (read.kind === 'staged') {
1427
- replicas.set(room.locator, replicaOf(read.confirmed, held.queue, rewriting(), held));
1267
+ replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1428
1268
  if (declarationRefusals.delete(room.locator)) {
1429
1269
  sourceRefusals.delete(room.locator);
1430
1270
  unreadable.delete(room.locator);
@@ -1448,77 +1288,6 @@ export function composeRuntime(options) {
1448
1288
  publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
1449
1289
  });
1450
1290
  }
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
1291
  async function scoped(result, room, local) {
1523
1292
  scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
1524
1293
  let scopedAway = room;
@@ -1546,14 +1315,36 @@ export function composeRuntime(options) {
1546
1315
  emitRooms();
1547
1316
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
1548
1317
  }
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) => {
1318
+ function heldRooms(listed) {
1319
+ return listed.flatMap((room) => {
1320
+ const record = replicas.get(room.locator)?.confirmed.record;
1321
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1322
+ return [];
1323
+ return [
1324
+ {
1325
+ locator: room.locator,
1326
+ roomVersion: record.roomVersion,
1327
+ documents: record.documents,
1328
+ revisions: record.revisions,
1329
+ attachments: record.attachments,
1330
+ },
1331
+ ];
1332
+ });
1333
+ }
1334
+ async function synchronize(listed, local, progress, attempt) {
1335
+ const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1336
+ const ready = listed.filter((_, index) => {
1552
1337
  const outcome = prepared[index];
1553
1338
  return outcome?.status === 'fulfilled' && outcome.value;
1554
1339
  });
1555
- const rooms = await Promise.allSettled(ready.map((room) => synchronizeRoom(room, local, progress)));
1556
- const failed = [...prepared, ...rooms].find((room) => room.status === 'rejected');
1340
+ if (attempt === 'opened') {
1341
+ options.adapter.roomStream?.hold(heldRooms(ready));
1342
+ handed = true;
1343
+ }
1344
+ const loaded = await Promise.allSettled(ready
1345
+ .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1346
+ .map((room) => loadRoom(room, local, progress)));
1347
+ const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1557
1348
  if (failed !== undefined)
1558
1349
  throw failed.reason;
1559
1350
  }
@@ -1570,11 +1361,8 @@ export function composeRuntime(options) {
1570
1361
  progress.complete = false;
1571
1362
  return false;
1572
1363
  }
1573
- const selection = room.typedSelection;
1574
- await Promise.all([
1575
- acquireModel(local, room, authorization, null),
1576
- ...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
1577
- ]);
1364
+ if (prior !== undefined)
1365
+ await retainModel(local, room, 'collection', modelOf(room.locator));
1578
1366
  if (discoveryPaused || authorization === undefined)
1579
1367
  return false;
1580
1368
  if (room.replica.admission !== 'admitted') {
@@ -1582,24 +1370,53 @@ export function composeRuntime(options) {
1582
1370
  progress.complete = false;
1583
1371
  return false;
1584
1372
  }
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
1373
  return true;
1591
1374
  }
1592
- async function synchronizeRoom(room, local, progress) {
1375
+ async function caughtUp(room, local, progress) {
1376
+ const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1377
+ if (held === undefined || authorization === undefined)
1378
+ return;
1379
+ const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1380
+ if (!behind)
1381
+ return;
1382
+ const withheld = await advance(room, local, authorization);
1383
+ if (withheld !== null)
1384
+ await withhold(withheld, room, local, progress);
1385
+ }
1386
+ function revoked(locator) {
1387
+ return ['collection', 'typed-selection'].some((attached) => {
1388
+ const model = modelOf(locator, attached);
1389
+ return model.status === 'unavailable' && model.reason === 'revoked';
1390
+ });
1391
+ }
1392
+ async function loadRoom(room, local, progress) {
1593
1393
  if (discoveryPaused || authorization === undefined)
1594
1394
  return;
1395
+ if (replicas.has(room.locator) && revoked(room.locator)) {
1396
+ await reload(room, local, progress);
1397
+ return;
1398
+ }
1595
1399
  const withheld = await advance(room, local, authorization);
1596
1400
  if (withheld !== null) {
1597
1401
  await withhold(withheld, room, local, progress);
1598
1402
  return;
1599
1403
  }
1600
- await submitRoom(room, local, authorization, progress);
1404
+ await caughtUp(room, local, progress);
1405
+ }
1406
+ async function reload(room, local, progress) {
1407
+ if (discoveryPaused || authorization === undefined)
1408
+ return;
1409
+ const bootstrapped = await pullBootstrap(authorization, room);
1410
+ if (!continuing())
1411
+ return;
1412
+ if (bootstrapped.kind !== 'staged') {
1413
+ await withhold(bootstrapped, room, local, progress);
1414
+ return;
1415
+ }
1416
+ await publish(local, room, bootstrapped.confirmed);
1417
+ await caughtUp(room, local, progress);
1601
1418
  }
1602
- async function cycle(local, progress) {
1419
+ async function discoverRooms(local, progress, attempt) {
1603
1420
  if (closed || authorization === undefined) {
1604
1421
  notifyReady();
1605
1422
  return;
@@ -1625,6 +1442,7 @@ export function composeRuntime(options) {
1625
1442
  }
1626
1443
  }
1627
1444
  progress.reachable = false;
1445
+ progress.complete = false;
1628
1446
  notifyReady();
1629
1447
  return;
1630
1448
  }
@@ -1639,17 +1457,6 @@ export function composeRuntime(options) {
1639
1457
  notifyReady();
1640
1458
  return;
1641
1459
  }
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
1460
  for (const room of result.rooms) {
1654
1461
  if (room.operations === undefined ||
1655
1462
  !['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
@@ -1657,8 +1464,7 @@ export function composeRuntime(options) {
1657
1464
  room.replica === undefined ||
1658
1465
  typeof room.replica.selection !== 'string' ||
1659
1466
  room.replica.selection.length === 0 ||
1660
- !['admitted', 'unavailable'].includes(room.replica.admission) ||
1661
- !declaredIdentity(room.attachedModel)) {
1467
+ !['admitted', 'unavailable'].includes(room.replica.admission)) {
1662
1468
  journal.record(`invalid model or replica declaration ${room.locator}`);
1663
1469
  await refuse('missing-required-guarantee');
1664
1470
  notifyReady();
@@ -1670,20 +1476,6 @@ export function composeRuntime(options) {
1670
1476
  notifyReady();
1671
1477
  return;
1672
1478
  }
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
1479
  }
1688
1480
  if (refusal !== null) {
1689
1481
  refusal = null;
@@ -1692,29 +1484,13 @@ export function composeRuntime(options) {
1692
1484
  const previousCapabilities = capabilities;
1693
1485
  capabilities = result.capabilities;
1694
1486
  notifyReady();
1695
- if (declared.has('room-version-notifications'))
1696
- observeRoomVersions();
1697
- const discoveredSlots = result.slots ?? [];
1698
1487
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1699
- JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities) ||
1700
- JSON.stringify(discoveredSlots) !== JSON.stringify(slots)) {
1488
+ JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
1701
1489
  limits = result.providerLimits;
1702
- slots = discoveredSlots;
1703
- await local.declare(limits, capabilities, slots);
1490
+ await local.declare(limits, capabilities);
1704
1491
  }
1705
1492
  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);
1493
+ await synchronize(result.rooms, local, progress, attempt);
1718
1494
  }
1719
1495
  async function ensureAuthorization() {
1720
1496
  if (authorization !== undefined)
@@ -1728,64 +1504,253 @@ export function composeRuntime(options) {
1728
1504
  throw error;
1729
1505
  }
1730
1506
  }
1731
- async function connect() {
1732
- if (closed || stopping || discoveryPaused) {
1733
- notifyReady();
1507
+ function task(work) {
1508
+ if (stopping)
1509
+ return chain;
1510
+ chain = chain.then(work, work).catch(() => undefined);
1511
+ return chain;
1512
+ }
1513
+ function sync(attempt) {
1514
+ return task(async () => {
1515
+ if (closed || stopping || discoveryPaused) {
1516
+ notifyReady();
1517
+ return;
1518
+ }
1519
+ const progress = { complete: true, reachable: true };
1520
+ underway = attempt === 'opened' ? progress : null;
1521
+ complete = false;
1522
+ recorded = false;
1523
+ moveTo(transferring(status));
1524
+ handed = false;
1525
+ try {
1526
+ const local = await openLocal();
1527
+ if (closed)
1528
+ return;
1529
+ await ensureAuthorization();
1530
+ if (closed)
1531
+ return;
1532
+ await discoverRooms(local, progress, attempt);
1533
+ }
1534
+ catch {
1535
+ progress.complete = false;
1536
+ if (attempt === 'opened' && !handed)
1537
+ progress.reachable = false;
1538
+ }
1539
+ finally {
1540
+ notifyReady();
1541
+ }
1542
+ if (attempt !== 'opened') {
1543
+ await finishOpening(progress);
1544
+ return;
1545
+ }
1546
+ if (handed || closed || progress.reachable)
1547
+ return;
1548
+ underway = null;
1549
+ journal.record('connection reopen');
1550
+ await rest(progress);
1551
+ options.adapter.roomStream?.reopen();
1552
+ });
1553
+ }
1554
+ function closedSocket(revoked) {
1555
+ const opened = socketOpen;
1556
+ socketOpen = false;
1557
+ if (revoked) {
1558
+ void refuse('revoked-authorization');
1734
1559
  return;
1735
1560
  }
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)
1561
+ if (opened || status.providerUnreachable)
1562
+ return;
1563
+ moveTo({ ...status, providerUnreachable: true });
1564
+ }
1565
+ function applyOpening(expired) {
1566
+ return task(async () => {
1567
+ const progress = underway;
1568
+ underway = null;
1569
+ if (closed || stopping)
1745
1570
  return;
1746
- await cycle(local, progress);
1747
- if (closed)
1571
+ if (progress === null) {
1572
+ await reloadExpired(expired);
1748
1573
  return;
1749
- if (progress.complete) {
1750
- completedAt = new Date().toISOString();
1751
- await local.complete(completedAt);
1752
1574
  }
1575
+ try {
1576
+ const local = await openLocal();
1577
+ const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1578
+ const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1579
+ const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
1580
+ if (progress.reachable) {
1581
+ for (const room of held)
1582
+ if (!reloading.includes(room))
1583
+ await confirmModels(room, local);
1584
+ }
1585
+ const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1586
+ if (failed !== undefined)
1587
+ throw failed.reason;
1588
+ }
1589
+ catch {
1590
+ progress.complete = false;
1591
+ progress.reachable = false;
1592
+ }
1593
+ await finishOpening(progress);
1594
+ });
1595
+ }
1596
+ async function reloadExpired(expired) {
1597
+ const progress = { complete: true, reachable: true };
1598
+ try {
1599
+ const local = await openLocal();
1600
+ await Promise.allSettled([...discovered.values()]
1601
+ .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1602
+ .map((room) => reload(room, local, progress)));
1753
1603
  }
1754
1604
  catch {
1755
- progress.complete = false;
1756
- notifyReady();
1605
+ progress.reachable = false;
1606
+ }
1607
+ await rest(progress);
1608
+ }
1609
+ async function confirmModels(room, local) {
1610
+ for (const attached of ['collection', 'typed-selection']) {
1611
+ const model = modelOf(room.locator, attached);
1612
+ if (model.status === 'cached') {
1613
+ await retainModel(local, room, attached, { ...model, status: 'current' });
1614
+ }
1615
+ }
1616
+ }
1617
+ function submitLater(locator) {
1618
+ const running = submitting.get(locator);
1619
+ if (running !== undefined) {
1620
+ running.again = true;
1757
1621
  return;
1758
1622
  }
1759
- finally {
1760
- notifyReady();
1761
- moveTo(settled(status, progress, completedAt, pending(), attention()));
1623
+ const run = { again: false, done: Promise.resolve() };
1624
+ submitting.set(locator, run);
1625
+ run.done = (async () => {
1626
+ try {
1627
+ do {
1628
+ run.again = false;
1629
+ const room = discovered.get(locator);
1630
+ if (closed || stopping || discoveryPaused || room === undefined)
1631
+ return;
1632
+ const progress = { complete: true, reachable: true };
1633
+ try {
1634
+ await ensureAuthorization();
1635
+ if (authorization === undefined)
1636
+ return;
1637
+ await submitRoom(room, await openLocal(), authorization, progress);
1638
+ }
1639
+ catch {
1640
+ progress.reachable = false;
1641
+ }
1642
+ armTimer();
1643
+ await rest(progress);
1644
+ } while (run.again);
1645
+ }
1646
+ finally {
1647
+ submitting.delete(locator);
1648
+ }
1649
+ })();
1650
+ }
1651
+ function submitAll() {
1652
+ for (const [locator, replica] of replicas) {
1653
+ if (replica.queue.length > 0)
1654
+ submitLater(locator);
1762
1655
  }
1763
1656
  }
1764
- async function run() {
1657
+ async function finishOpening(progress) {
1765
1658
  try {
1766
- await connect();
1659
+ if (closed || discoveryPaused) {
1660
+ progress.complete = false;
1661
+ return;
1662
+ }
1663
+ submitAll();
1664
+ await conversations.reconcile();
1665
+ if (status.providerUnreachable)
1666
+ conversations.reconnected();
1667
+ const reading = { complete: true, reachable: true };
1668
+ await conversations.catchUp(reading);
1669
+ if (!reading.reachable)
1670
+ progress.reachable = false;
1671
+ }
1672
+ catch {
1673
+ progress.complete = false;
1767
1674
  }
1768
1675
  finally {
1769
- inflight = false;
1770
- if (queued && !closed) {
1771
- queued = false;
1772
- void enqueue();
1773
- }
1676
+ complete = progress.complete;
1677
+ await rest(progress);
1774
1678
  }
1775
1679
  }
1776
- function observeRoomVersions() {
1777
- const path = options.adapter.roomVersions;
1778
- if (path === undefined || withdrawNotifications !== undefined)
1680
+ async function rest(progress) {
1681
+ if (closed)
1779
1682
  return;
1780
- withdrawNotifications = path.observe((notification) => {
1781
- if (closed || stopping || discoveryPaused)
1683
+ let lastCompleted = status.lastCompleted;
1684
+ let synced = complete && stale.size === 0 && !pending();
1685
+ if (synced && !recorded) {
1686
+ const completedAt = new Date().toISOString();
1687
+ try {
1688
+ await (await openLocal()).complete(completedAt);
1689
+ recorded = true;
1690
+ lastCompleted = completedAt;
1691
+ }
1692
+ catch {
1693
+ synced = false;
1694
+ }
1695
+ }
1696
+ moveTo({
1697
+ state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
1698
+ lastCompleted,
1699
+ providerUnreachable: !progress.reachable,
1700
+ ...attention(),
1701
+ });
1702
+ }
1703
+ function applyRoom(locator, roomVersion) {
1704
+ announced.set(locator, roomVersion);
1705
+ return task(async () => {
1706
+ await answering.get(locator);
1707
+ if (closed || stopping || discoveryPaused || authorization === undefined)
1782
1708
  return;
1783
- if (notification.modelChanged === true && handed?.includes(notification.locator) === true) {
1784
- void enqueue();
1709
+ const room = discovered.get(locator);
1710
+ const held = replicas.get(locator);
1711
+ if (room === undefined || room.replica.admission !== 'admitted')
1712
+ return;
1713
+ if (held !== undefined &&
1714
+ (held.confirmed.record.replica.selection !== room.replica.selection ||
1715
+ options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
1716
+ return;
1717
+ }
1718
+ const during = underway;
1719
+ const progress = during ?? { complete: true, reachable: true };
1720
+ if (during === null)
1721
+ moveTo(transferring(status));
1722
+ try {
1723
+ const local = await openLocal();
1724
+ const withheld = await advance(room, local, authorization);
1725
+ if (withheld !== null)
1726
+ await withhold(withheld, room, local, progress);
1727
+ }
1728
+ catch {
1729
+ stale.add(locator);
1730
+ }
1731
+ finally {
1732
+ if (during === null)
1733
+ await rest(progress);
1734
+ }
1735
+ });
1736
+ }
1737
+ function observeStream() {
1738
+ const stream = options.adapter.roomStream;
1739
+ if (stream === undefined || withdrawStream !== undefined)
1740
+ return;
1741
+ withdrawStream = stream.observe((event) => {
1742
+ if (closed || stopping)
1785
1743
  return;
1744
+ if (event.kind === 'opened') {
1745
+ socketOpen = true;
1746
+ void sync('opened');
1786
1747
  }
1787
- notices.set(notification.locator, notification.roomVersion);
1788
- chain = chain.then(runNotified, runNotified);
1748
+ else if (event.kind === 'closed')
1749
+ closedSocket(event.revoked);
1750
+ else if (event.kind === 'opening')
1751
+ void applyOpening(event.expired);
1752
+ else
1753
+ void applyRoom(event.locator, event.roomVersion);
1789
1754
  });
1790
1755
  }
1791
1756
  function writesHeld() {
@@ -1806,14 +1771,17 @@ export function composeRuntime(options) {
1806
1771
  listener(mode);
1807
1772
  }
1808
1773
  if (releasing)
1809
- void enqueue();
1774
+ submitAll();
1810
1775
  }
1811
1776
  function observeConversations() {
1812
1777
  const stream = options.adapter.conversations;
1813
1778
  if (stream === undefined || withdrawConversations !== undefined)
1814
1779
  return;
1815
1780
  withdrawConversations = stream.observe((event) => {
1816
- conversations.receive(event);
1781
+ if (conversations.receive(event)) {
1782
+ void ready.then(scheduleCurrency);
1783
+ return;
1784
+ }
1817
1785
  if (event.kind !== 'gap' || conversations.current())
1818
1786
  return;
1819
1787
  show();
@@ -1824,7 +1792,7 @@ export function composeRuntime(options) {
1824
1792
  if (currencyQueued || closed || stopping)
1825
1793
  return;
1826
1794
  currencyQueued = true;
1827
- chain = chain.then(runCurrency, runCurrency);
1795
+ void task(runCurrency);
1828
1796
  }
1829
1797
  async function runCurrency() {
1830
1798
  currencyQueued = false;
@@ -1850,95 +1818,12 @@ export function composeRuntime(options) {
1850
1818
  }
1851
1819
  }
1852
1820
  }
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
1821
  function close() {
1937
1822
  if (closed)
1938
1823
  return;
1939
1824
  closed = true;
1940
- withdrawNotifications?.();
1941
- withdrawNotifications = undefined;
1825
+ withdrawStream?.();
1826
+ withdrawStream = undefined;
1942
1827
  withdrawConversations?.();
1943
1828
  withdrawConversations = undefined;
1944
1829
  withdrawServiceMode?.();
@@ -1962,6 +1847,7 @@ export function composeRuntime(options) {
1962
1847
  stopping = true;
1963
1848
  stopTimer();
1964
1849
  await chain.catch(() => undefined);
1850
+ await Promise.all([...submitting.values()].map((run) => run.done));
1965
1851
  await mutations;
1966
1852
  close();
1967
1853
  }
@@ -1970,11 +1856,29 @@ export function composeRuntime(options) {
1970
1856
  options.signal?.addEventListener('abort', close, { once: true });
1971
1857
  if (options.signal?.aborted)
1972
1858
  close();
1973
- void openLocal().catch(() => {
1859
+ observeStream();
1860
+ void openLocal().then(() => {
1861
+ if (capabilities.length > 0)
1862
+ notifyReady();
1863
+ }, () => {
1974
1864
  notifyReady();
1975
1865
  });
1976
- void enqueue();
1977
- function watch(topic, input, deliver) {
1866
+ if (options.adapter.roomStream === undefined)
1867
+ void sync('unstreamed');
1868
+ function planned(input, reach) {
1869
+ return planOf(input, roomFacts(reach), planReach(reach));
1870
+ }
1871
+ function watchPlan(plan, deliver) {
1872
+ return readSide.watch(plan, identityOf(plan), deliver);
1873
+ }
1874
+ async function searched(input, reach) {
1875
+ const plan = searchPlanOf(input);
1876
+ const selected = plan.documents === undefined && reach === null
1877
+ ? null
1878
+ : (await readSide.query(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
1879
+ return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
1880
+ }
1881
+ const watch = (topic, input, deliver) => {
1978
1882
  if (topic === 'room-drafts') {
1979
1883
  const listener = { notify: deliver, ready: false };
1980
1884
  roomDraftListeners.add(listener);
@@ -1995,6 +1899,8 @@ export function composeRuntime(options) {
1995
1899
  if (topic === 'conversation') {
1996
1900
  return conversations.watch(input[0], input[1], deliver);
1997
1901
  }
1902
+ if (topic === 'conversations')
1903
+ return conversations.watchList(deliver);
1998
1904
  if (topic === 'presentation') {
1999
1905
  const listener = {
2000
1906
  app: input[0],
@@ -2057,9 +1963,8 @@ export function composeRuntime(options) {
2057
1963
  queueSubscriptions.delete(queue);
2058
1964
  };
2059
1965
  }
2060
- const plan = planOf(input[0], roomFacts());
2061
- return readSide.watch(plan, identityOf(plan), deliver);
2062
- }
1966
+ return watchPlan(planned(input[0], null), deliver);
1967
+ };
2063
1968
  const md = {
2064
1969
  loaded() {
2065
1970
  return openLocal().then(() => undefined);
@@ -2067,18 +1972,8 @@ export function composeRuntime(options) {
2067
1972
  rooms() {
2068
1973
  return listed();
2069
1974
  },
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
- },
1975
+ query: ((input) => readSide.query(planned(input, null))),
1976
+ search: (input) => searched(input, null),
2082
1977
  types(locator) {
2083
1978
  return {
2084
1979
  collection: modelTypes(modelOf(locator)),
@@ -2088,12 +1983,6 @@ export function composeRuntime(options) {
2088
1983
  : modelTypes(modelOf(locator, 'typed-selection')),
2089
1984
  };
2090
1985
  },
2091
- syncStatus() {
2092
- return shown;
2093
- },
2094
- serviceMode() {
2095
- return serviceMode;
2096
- },
2097
1986
  roomDraft: {
2098
1987
  list() {
2099
1988
  return exclusive(async () => {
@@ -2174,9 +2063,6 @@ export function composeRuntime(options) {
2174
2063
  save(room, named, writtenBy) {
2175
2064
  return exclusive(() => saveRoomDrafts(room, named, writtenBy));
2176
2065
  },
2177
- outcome(identifier) {
2178
- return outcomes.get(identifier) ?? null;
2179
- },
2180
2066
  history(locator, path, query) {
2181
2067
  return readHistory(locator, path, query);
2182
2068
  },
@@ -2191,15 +2077,10 @@ export function composeRuntime(options) {
2191
2077
  log() {
2192
2078
  return journal.export();
2193
2079
  },
2194
- storageLoss() {
2195
- return storageLoss;
2196
- },
2197
2080
  persistence() {
2198
2081
  return options.shell.persistentStorage;
2199
2082
  },
2200
- observe: ((topic, ...input) => opened((deliver) => watch(topic, input, (message) => {
2201
- deliver(structuredClone(message));
2202
- }), topic === 'query')),
2083
+ observe: observing(watch),
2203
2084
  async reauthorize() {
2204
2085
  if (refusal === null || !refusal.reauthorizationOffered)
2205
2086
  return;
@@ -2208,8 +2089,19 @@ export function composeRuntime(options) {
2208
2089
  await mutations;
2209
2090
  authorization = undefined;
2210
2091
  discoveryPaused = false;
2211
- await enqueue();
2092
+ if (options.adapter.roomStream === undefined)
2093
+ await sync('unstreamed');
2094
+ else
2095
+ await task(ensureAuthorization);
2212
2096
  },
2213
2097
  };
2214
- return { md, stop, watch };
2098
+ const reachable = {
2099
+ md,
2100
+ watch,
2101
+ plan: planned,
2102
+ query: (plan) => readSide.query(plan),
2103
+ watchPlan,
2104
+ search: searched,
2105
+ };
2106
+ return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2215
2107
  }