@agentshouse/mdruntime 0.1.0-cf28918c2bf91 → 0.1.0-cf6d5f0a5c9c8

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 +68 -22
  2. package/README.md +10 -10
  3. package/dist/adapter.d.ts +58 -25
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/adapter.js +1 -9
  6. package/dist/attached-model.d.ts +11 -0
  7. package/dist/attached-model.d.ts.map +1 -0
  8. package/dist/attached-model.js +47 -0
  9. package/dist/bootstrap.d.ts +20 -6
  10. package/dist/bootstrap.d.ts.map +1 -1
  11. package/dist/bootstrap.js +89 -35
  12. package/dist/change-set.d.ts.map +1 -1
  13. package/dist/change-set.js +13 -7
  14. package/dist/client.d.ts +6 -3
  15. package/dist/client.d.ts.map +1 -1
  16. package/dist/client.js +38 -10
  17. package/dist/collection.d.ts +6 -11
  18. package/dist/collection.d.ts.map +1 -1
  19. package/dist/collection.js +56 -19
  20. package/dist/connection.d.ts +13 -13
  21. package/dist/connection.d.ts.map +1 -1
  22. package/dist/connection.js +643 -542
  23. package/dist/conversation.d.ts +18 -6
  24. package/dist/conversation.d.ts.map +1 -1
  25. package/dist/conversation.js +179 -78
  26. package/dist/host.d.ts +8 -1
  27. package/dist/host.d.ts.map +1 -1
  28. package/dist/host.js +35 -31
  29. package/dist/identity.d.ts +1 -0
  30. package/dist/identity.d.ts.map +1 -1
  31. package/dist/identity.js +7 -1
  32. package/dist/index.d.ts +3 -3
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/memory.d.ts +12 -5
  35. package/dist/memory.d.ts.map +1 -1
  36. package/dist/memory.js +92 -31
  37. package/dist/model.d.ts +11 -11
  38. package/dist/model.d.ts.map +1 -1
  39. package/dist/model.js +6 -3
  40. package/dist/observation.d.ts +2 -0
  41. package/dist/observation.d.ts.map +1 -1
  42. package/dist/observation.js +24 -14
  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 +39 -0
  53. package/dist/react.d.ts.map +1 -0
  54. package/dist/react.js +63 -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 +12 -1
  59. package/dist/refresh.d.ts.map +1 -1
  60. package/dist/refresh.js +35 -22
  61. package/dist/replica.d.ts +6 -6
  62. package/dist/replica.d.ts.map +1 -1
  63. package/dist/replica.js +41 -36
  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 +7 -5
  67. package/dist/rows.d.ts +4 -1
  68. package/dist/rows.d.ts.map +1 -1
  69. package/dist/rows.js +82 -39
  70. package/dist/save.d.ts +1 -2
  71. package/dist/save.d.ts.map +1 -1
  72. package/dist/save.js +106 -58
  73. package/dist/store.d.ts +25 -2
  74. package/dist/store.d.ts.map +1 -1
  75. package/dist/store.js +206 -90
  76. package/dist/wire.d.ts +52 -16
  77. package/dist/wire.d.ts.map +1 -1
  78. package/dist/wire.js +51 -59
  79. package/package.json +14 -2
@@ -1,18 +1,18 @@
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
+ import { markdownPath, roomOfKey } from './rows.js';
6
7
  import { createSearchIndex, searchPlanOf, } from './search.js';
7
- import { ROOM_CONTRACT_VERSION, VERSION_1_CAPABILITIES, } from './adapter.js';
8
8
  import { bootstrapRoom } from './bootstrap.js';
9
9
  import { changeSet, mergeChangeSets } from './change-set.js';
10
10
  import { createConversationStore, } from './conversation.js';
11
- import { MD_MODEL, parseCollection, validRoot } from './collection.js';
11
+ import { validRoot } from './collection.js';
12
12
  import { createLog } from './log.js';
13
13
  import { refreshRoom } from './refresh.js';
14
14
  import { buildBatch } from './save.js';
15
- import { applyReturned, compositionOf, landed, readConfirmed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
15
+ import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
16
16
  import { changedParts, changesOf, held, readParts, returnedInto, underneath, } from './room-draft.js';
17
17
  import { settled, same, saving, STARTING, transferring, } from './status.js';
18
18
  import { detectStorageLoss, nextGeneration } from './durability.js';
@@ -20,37 +20,21 @@ import { roomDraftRevision } from './identity.js';
20
20
  import { beneath, normalize } from './paths.js';
21
21
  import { openRuntimeStore, } from './store.js';
22
22
  import { unconfirmed } from './transfer.js';
23
- // Discovery repeats each minute so a Room the stream stopped serving stays unrefreshed no longer.
24
- const DEFAULT_DISCOVERY_INTERVAL_MS = 60_000;
25
- const EMPTY_SCHEMA = { verdict: 'accept', model: { types: {} } };
26
- const knownCapabilities = new Set(VERSION_1_CAPABILITIES);
23
+ import { governs, holds, narrowed, planReach, } from './reach.js';
27
24
  function within(root, path) {
28
25
  return root === '' || beneath(root, path);
29
26
  }
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
27
  function declaredSelection(room) {
41
28
  const selection = room.typedSelection;
42
29
  if (selection === undefined)
43
30
  return true;
44
31
  if (!Array.isArray(selection.roots) || selection.roots.length === 0)
45
32
  return false;
46
- if (!declaredIdentity(selection.attachedModel))
47
- return false;
48
33
  return selection.roots.every((root, index) => validRoot(root) &&
49
34
  root !== '' &&
50
35
  selection.roots.every((other, at) => at === index || !within(other, root)));
51
36
  }
52
37
  export function composeRuntime(options) {
53
- const period = options.discoveryIntervalMs ?? DEFAULT_DISCOVERY_INTERVAL_MS;
54
38
  let authorization = options.authorization;
55
39
  let opening;
56
40
  let store;
@@ -58,20 +42,14 @@ export function composeRuntime(options) {
58
42
  const pendings = new Map();
59
43
  const roomDrafts = new Map();
60
44
  const presentations = new Map();
61
- const wanted = new Map();
62
45
  let limits = [];
63
46
  let capabilities = [];
64
- function rewriting() {
65
- return capabilities.some((capability) => capability.name === 'move-reference-rewrites')
66
- ? 'provider'
67
- : 'runtime';
68
- }
69
47
  const rooms = new Map();
48
+ const counted = new Map();
70
49
  const unreadable = new Set();
71
50
  const models = new Map();
72
51
  const liveModels = new Map();
73
52
  const invalidModels = new Set();
74
- const modelEpoch = new Map();
75
53
  const discovered = new Map();
76
54
  let roomsListed = false;
77
55
  const scopedRefusals = new Map();
@@ -100,16 +78,20 @@ export function composeRuntime(options) {
100
78
  let status = STARTING;
101
79
  let shown = STARTING;
102
80
  let discoveryPaused = false;
103
- let timer;
81
+ let resetTimer;
104
82
  let closed = false;
105
83
  let stopping = false;
106
- let withdrawNotifications;
107
- let handed;
108
- const notices = new Map();
84
+ let withdrawStream;
85
+ const announced = new Map();
86
+ let underway = null;
87
+ let handed = false;
88
+ const submitting = new Map();
89
+ const answering = new Map();
90
+ let complete = false;
91
+ let recorded = false;
92
+ const stale = new Set();
109
93
  let chain = Promise.resolve();
110
94
  let mutations = Promise.resolve();
111
- let inflight = false;
112
- let queued = false;
113
95
  const roomListeners = new Set();
114
96
  const refusalListeners = new Set();
115
97
  const storageLossListeners = new Set();
@@ -120,13 +102,19 @@ export function composeRuntime(options) {
120
102
  const readSide = createReadSide();
121
103
  let withdrawConversations;
122
104
  let currencyQueued = false;
105
+ let socketOpen = false;
123
106
  let serviceMode = 'open';
124
107
  let haltedRefusal = false;
125
108
  let openSignals = 0;
126
109
  const serviceModeListeners = new Set();
110
+ let member = null;
111
+ const memberListeners = new Set();
112
+ const materialized = new Set();
113
+ let everyRoom = false;
114
+ const transfers = new Map();
127
115
  const conversations = createConversationStore({
128
- cards: declaredCards,
129
- documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
116
+ agents: declaredAgents,
117
+ documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
130
118
  call: (name, input) => invokeAction(name, input),
131
119
  durably,
132
120
  stream: () => options.adapter.conversations,
@@ -231,14 +219,33 @@ export function composeRuntime(options) {
231
219
  }
232
220
  return { attentionRequired, reset };
233
221
  }
222
+ function documentsIn(locator) {
223
+ const replica = replicas.get(locator);
224
+ if (replica === undefined)
225
+ return 0;
226
+ const roots = replica.confirmed.record.typedSelection?.roots ?? [];
227
+ let count = 0;
228
+ for (const path of Object.keys(replica.visible.documents)) {
229
+ if (markdownPath(path) && !roots.some((root) => beneath(root, path)))
230
+ count += 1;
231
+ }
232
+ return count;
233
+ }
234
234
  function listed() {
235
- return structuredClone([...rooms.values()]);
235
+ return structuredClone([...rooms.values()].map((room) => {
236
+ const documents = documentsIn(room.locator);
237
+ counted.set(room.locator, documents);
238
+ return { ...room, documents };
239
+ }));
236
240
  }
237
- function roomFacts() {
238
- const held = [...rooms.values()].map((room) => {
241
+ function roomFacts(reach = null) {
242
+ const held = [...rooms.values()]
243
+ .filter((room) => holds(reach, room.locator))
244
+ .map((room) => {
245
+ const governed = governs(reach, room);
239
246
  const attached = [
240
- modelOf(room.locator),
241
- ...(room.typedSelection === null ? [] : [modelOf(room.locator, 'typed-selection')]),
247
+ ...(governed.collection ? [modelOf(room.locator)] : []),
248
+ ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
242
249
  ];
243
250
  return {
244
251
  locator: room.locator,
@@ -261,6 +268,10 @@ export function composeRuntime(options) {
261
268
  for (const listener of storageLossListeners)
262
269
  listener(storageLoss);
263
270
  }
271
+ function emitMember() {
272
+ for (const listener of memberListeners)
273
+ listener(member);
274
+ }
264
275
  function moveTo(next) {
265
276
  if (closed)
266
277
  return;
@@ -300,12 +311,47 @@ export function composeRuntime(options) {
300
311
  }
301
312
  }
302
313
  function applyChangeSet(set) {
314
+ const held = everyRoom
315
+ ? set
316
+ : { ...set, documents: set.documents.filter(({ locator }) => materialized.has(locator)) };
317
+ readSide.apply(held);
318
+ searchIndex.changed(held);
319
+ }
320
+ function materialize(locators) {
321
+ if (everyRoom)
322
+ return;
323
+ if (locators === null)
324
+ everyRoom = true;
325
+ const sets = [];
326
+ for (const locator of locators ?? [...replicas.keys()]) {
327
+ if (materialized.has(locator))
328
+ continue;
329
+ materialized.add(locator);
330
+ const replica = replicas.get(locator);
331
+ if (replica !== undefined)
332
+ sets.push(changeSet(locator, null, replica));
333
+ }
334
+ if (sets.length === 0)
335
+ return;
336
+ const set = mergeChangeSets(sets);
303
337
  readSide.apply(set);
304
338
  searchIndex.changed(set);
305
339
  }
340
+ function reachedRooms(plan) {
341
+ if (plan.rooms !== null)
342
+ return plan.rooms;
343
+ if (plan.reach === null)
344
+ return null;
345
+ return [...plan.reach.rooms, ...plan.reach.documents.map(roomOfKey)];
346
+ }
347
+ function rowsQuery(plan) {
348
+ materialize(reachedRooms(plan));
349
+ return readSide.query(plan);
350
+ }
306
351
  function publishChangeSet(set) {
307
352
  applyChangeSet(set);
308
- if (set.rooms.length > 0)
353
+ if (set.rooms.length > 0 ||
354
+ [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
309
355
  emitRooms();
310
356
  for (const locator of set.queues)
311
357
  emitQueue(locator);
@@ -316,23 +362,32 @@ export function composeRuntime(options) {
316
362
  for (const app of set.presentations)
317
363
  emitPresentation(app);
318
364
  conversations.changed(set);
319
- const cards = declaredCards();
320
- if (cards !== undefined && set.documents.some((change) => change.locator === cards.locator)) {
321
- scheduleCurrency();
322
- }
323
365
  }
324
366
  function stopTimer() {
325
- if (timer === undefined)
367
+ if (resetTimer === undefined)
326
368
  return;
327
- clearInterval(timer);
328
- timer = undefined;
369
+ clearTimeout(resetTimer);
370
+ resetTimer = undefined;
329
371
  }
330
- function startTimer() {
331
- if (timer !== undefined)
372
+ function armTimer() {
373
+ stopTimer();
374
+ if (closed || stopping)
332
375
  return;
333
- timer = setInterval(() => {
334
- void enqueue();
335
- }, period);
376
+ let earliest = null;
377
+ for (const { submission } of pendings.values()) {
378
+ if (submission.state !== 'reset')
379
+ continue;
380
+ const at = Date.parse(submission.reset);
381
+ if (earliest === null || at < earliest)
382
+ earliest = at;
383
+ }
384
+ if (earliest === null)
385
+ return;
386
+ // A Save House held back goes again only once the reset its answer named has passed.
387
+ resetTimer = setTimeout(() => {
388
+ resetTimer = undefined;
389
+ submitAll();
390
+ }, Math.max(0, earliest - Date.now()));
336
391
  }
337
392
  async function refuse(reason, provider = null) {
338
393
  stopTimer();
@@ -358,7 +413,6 @@ export function composeRuntime(options) {
358
413
  }
359
414
  for (const key of keys) {
360
415
  invalidModels.add(key);
361
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
362
416
  models.set(key, revoked);
363
417
  liveModels.delete(key);
364
418
  }
@@ -371,10 +425,7 @@ export function composeRuntime(options) {
371
425
  model: revoked,
372
426
  selectionModel: revoked,
373
427
  };
374
- const read = readConfirmed(record, before.confirmed);
375
- replicas.set(locator, read.kind === 'staged'
376
- ? replicaOf(read.confirmed, before.queue, rewriting(), before)
377
- : { ...before, confirmed: { ...before.confirmed, record } });
428
+ replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
378
429
  }
379
430
  rooms.set(locator, {
380
431
  ...room,
@@ -404,15 +455,14 @@ export function composeRuntime(options) {
404
455
  rooms.set(room.locator, {
405
456
  name: room.name,
406
457
  locator: room.locator,
458
+ handle: room.handle,
407
459
  operations: room.operations,
408
460
  replica: room.replica,
409
- attachedModel: room.attachedModel,
410
461
  model: modelTypes(modelOf(room.locator)),
411
462
  typedSelection: selection === null
412
463
  ? null
413
464
  : {
414
465
  roots: selection.roots,
415
- attachedModel: selection.attachedModel,
416
466
  model: modelTypes(modelOf(room.locator, 'typed-selection')),
417
467
  },
418
468
  providerOwnedPaths: room.providerOwnedPaths,
@@ -429,9 +479,6 @@ export function composeRuntime(options) {
429
479
  discovered.clear();
430
480
  for (const room of listed)
431
481
  discovered.set(room.locator, room);
432
- for (const locator of wanted.keys())
433
- if (!listedLocators.has(locator))
434
- wanted.delete(locator);
435
482
  await dropUnlisted(listedLocators, local);
436
483
  for (const room of listed) {
437
484
  const held = replicas.get(room.locator)?.confirmed.record;
@@ -445,7 +492,7 @@ export function composeRuntime(options) {
445
492
  emitRooms();
446
493
  }
447
494
  function unlistedHeld(listed) {
448
- return [...new Set([...rooms.keys(), ...roomDrafts.keys()])].filter((locator) => !listed.has(locator));
495
+ return [...new Set([...rooms.keys(), ...roomDrafts.keys(), ...transfers.keys()])].filter((locator) => !listed.has(locator));
449
496
  }
450
497
  async function dropUnlisted(listed, local) {
451
498
  if (unlistedHeld(listed).length === 0)
@@ -464,7 +511,9 @@ export function composeRuntime(options) {
464
511
  replicas.delete(locator);
465
512
  rooms.delete(locator);
466
513
  roomDrafts.delete(locator);
514
+ transfers.delete(locator);
467
515
  unreadable.delete(locator);
516
+ stale.delete(locator);
468
517
  scopedRefusals.delete(locator);
469
518
  sourceRefusals.delete(locator);
470
519
  declarationRefusals.delete(locator);
@@ -473,7 +522,6 @@ export function composeRuntime(options) {
473
522
  models.delete(key);
474
523
  liveModels.delete(key);
475
524
  invalidModels.delete(key);
476
- modelEpoch.delete(key);
477
525
  }
478
526
  journal.record(`room unlisted ${locator}`);
479
527
  }
@@ -486,7 +534,6 @@ export function composeRuntime(options) {
486
534
  if (prior !== undefined &&
487
535
  prior.status !== 'unavailable' &&
488
536
  (persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
489
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
490
537
  invalidModels.add(key);
491
538
  }
492
539
  const model = persisted.status === 'unavailable' ||
@@ -501,7 +548,7 @@ export function composeRuntime(options) {
501
548
  return model;
502
549
  }
503
550
  async function hydrate(opened) {
504
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
551
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
505
552
  opened.all(),
506
553
  opened.queues(),
507
554
  opened.roomDrafts(),
@@ -511,9 +558,15 @@ export function composeRuntime(options) {
511
558
  opened.lastCompleted(),
512
559
  opened.providerLimits(),
513
560
  opened.capabilities(),
561
+ opened.member(),
562
+ opened.transfers(),
514
563
  ]);
564
+ transfers.clear();
565
+ for (const record of staged)
566
+ transfers.set(record.locator, record);
515
567
  limits = declared;
516
568
  capabilities = named;
569
+ member = known;
517
570
  roomDrafts.clear();
518
571
  for (const record of kept)
519
572
  rememberRoomDraft(record);
@@ -533,15 +586,8 @@ export function composeRuntime(options) {
533
586
  const model = restored(modelKey(record.locator, 'collection'), record.model);
534
587
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
535
588
  const prior = previousReplicas.get(record.locator);
536
- const retyped = { ...record, model, selectionModel };
537
- const read = readConfirmed(retyped, prior?.confirmed);
538
- const confirmed = read.kind === 'staged'
539
- ? read.confirmed
540
- : {
541
- record: retyped,
542
- collection: parseCollection(retyped.documents, compositionOf(retyped)),
543
- };
544
- replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], rewriting(), prior));
589
+ const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
590
+ replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
545
591
  const existing = rooms.get(record.locator);
546
592
  if (existing === undefined)
547
593
  list(record, false);
@@ -552,21 +598,15 @@ export function composeRuntime(options) {
552
598
  attachments: record.attachments,
553
599
  operations: record.operations,
554
600
  replica: record.replica,
555
- attachedModel: record.attachedModel,
556
601
  model: modelTypes(model),
557
602
  typedSelection: record.typedSelection === null
558
603
  ? null
559
604
  : {
560
605
  roots: record.typedSelection.roots,
561
- attachedModel: record.typedSelection.attachedModel,
562
606
  model: modelTypes(selectionModel),
563
607
  },
564
608
  });
565
609
  }
566
- if (read.kind === 'refused') {
567
- refusedSource(record, read);
568
- declarationRefusals.add(record.locator);
569
- }
570
610
  }
571
611
  return {
572
612
  lastCompleted,
@@ -579,6 +619,8 @@ export function composeRuntime(options) {
579
619
  applyChangeSet(read);
580
620
  if (replicas.size > 0)
581
621
  emitRooms();
622
+ if (member !== null)
623
+ emitMember();
582
624
  for (const locator of locators)
583
625
  emitQueue(locator);
584
626
  publishRoomDrafts(false);
@@ -626,10 +668,14 @@ export function composeRuntime(options) {
626
668
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
627
669
  }
628
670
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
629
- const withdrawn = withdrawals(confirmed, queue, withdrawable, rewriting());
671
+ const withdrawn = withdrawals(confirmed, queue, withdrawable);
630
672
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
673
+ const unqueued = withdrawn.map((batch) => ({
674
+ batch,
675
+ returned: landed(batch, confirmed) ? null : { kind: 'withdrawn' },
676
+ }));
631
677
  const returning = [
632
- ...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
678
+ ...[...unqueued].reverse(),
633
679
  ...(settling.leaving === undefined ? [] : [settling.leaving]),
634
680
  ];
635
681
  const written = new Map();
@@ -650,6 +696,7 @@ export function composeRuntime(options) {
650
696
  await local.write({
651
697
  next: confirmed.record,
652
698
  previous: before?.confirmed.record ?? null,
699
+ ...(settling.stored === undefined ? {} : { stored: settling.stored }),
653
700
  queue: kept,
654
701
  previousQueue: before?.queue ?? [],
655
702
  roomDrafts: [...written.values()],
@@ -660,44 +707,79 @@ export function composeRuntime(options) {
660
707
  }
661
708
  for (const record of written.values())
662
709
  rememberRoomDraft(record);
663
- for (const batch of withdrawn)
664
- journal.record(`outcome ${locator} ${batch.id} withdrawn`);
665
- replicas.set(locator, replicaOf(confirmed, kept, rewriting(), before));
710
+ for (const { batch, returned } of unqueued) {
711
+ journal.record(`outcome ${locator} ${batch.id} ${returned === null ? 'landed' : 'withdrawn'}`);
712
+ }
713
+ replicas.set(locator, replicaOf(confirmed, kept, before));
666
714
  return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
667
715
  room: settling.room,
668
716
  queue: kept.length !== (before?.queue.length ?? 0),
669
717
  drafts: [...written.keys()],
670
718
  });
671
719
  }
672
- function publish(local, room, confirmed) {
720
+ function adoptModels(locator, record) {
721
+ for (const [attached, model] of [
722
+ ['collection', record.model],
723
+ ['typed-selection', record.selectionModel],
724
+ ]) {
725
+ const key = modelKey(locator, attached);
726
+ models.set(key, model);
727
+ invalidModels.delete(key);
728
+ if (model.status === 'current')
729
+ liveModels.set(key, JSON.stringify(model.snapshot));
730
+ else if (model.status === 'unavailable')
731
+ liveModels.delete(key);
732
+ }
733
+ }
734
+ function remodel(locator, answers, held) {
735
+ const taken = (answer, previous, attached) => {
736
+ if (answer === undefined)
737
+ return previous;
738
+ const { state, refused } = attachedModel(answer, previous);
739
+ if (refused !== null) {
740
+ const where = refused.location === undefined ? '' : ` ${refused.location}`;
741
+ journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
742
+ }
743
+ models.set(modelKey(locator, attached), state);
744
+ return state;
745
+ };
746
+ return {
747
+ collection: taken(answers?.collection, held.collection, 'collection'),
748
+ selection: taken(answers?.selection, held.selection, 'typed-selection'),
749
+ };
750
+ }
751
+ function publish(local, room, confirmed, landing = {}) {
752
+ const { base, stored } = landing;
673
753
  return exclusive(async () => {
674
754
  if (!continuing())
675
- return;
755
+ return true;
676
756
  const prior = replicas.get(room.locator);
677
- const retained = readConfirmed({
678
- ...confirmed.record,
679
- model: modelOf(room.locator),
680
- selectionModel: modelOf(room.locator, 'typed-selection'),
681
- }, prior?.confirmed);
682
- if (retained.kind !== 'staged')
683
- throw new Error(`Cannot retype staged Room ${room.locator}`);
757
+ if (base !== undefined &&
758
+ (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
759
+ prior.confirmed.record.revisions !== base.record.revisions)) {
760
+ return false;
761
+ }
762
+ adoptModels(room.locator, confirmed.record);
684
763
  const queue = prior?.queue ?? [];
685
- const roomVersion = retained.confirmed.record.roomVersion;
764
+ const roomVersion = confirmed.record.roomVersion;
686
765
  const accepted = queue.filter((batch) => {
687
766
  const accepted = pendings.get(batch.id)?.accepted;
688
767
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
689
768
  });
690
- const set = await settle(local, room.locator, prior, retained.confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
691
- const target = wanted.get(room.locator);
692
- if (target !== undefined && options.adapter.reached(room.locator, roomVersion, target)) {
693
- wanted.delete(room.locator);
694
- }
695
- unreadable.delete(room.locator);
769
+ const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
770
+ if (stored !== undefined)
771
+ transfers.delete(room.locator);
772
+ for (const batch of accepted)
773
+ pendings.delete(batch.id);
774
+ stale.delete(room.locator);
775
+ if (unreadable.delete(room.locator))
776
+ scopedRefusals.delete(room.locator);
696
777
  sourceRefusals.delete(room.locator);
697
778
  declarationRefusals.delete(room.locator);
698
779
  list(room, true);
699
780
  publishChangeSet(set);
700
781
  moveTo({ ...status, ...attention() });
782
+ return true;
701
783
  });
702
784
  }
703
785
  async function saveRoomDrafts(locator, named, writtenBy) {
@@ -726,7 +808,7 @@ export function composeRuntime(options) {
726
808
  operations: room.operations,
727
809
  providerOwnedPaths: room.providerOwnedPaths,
728
810
  providerLimits: limits,
729
- rewriting: rewriting(),
811
+ handle: room.handle,
730
812
  visible: replica.visible,
731
813
  changes: parts.flatMap(({ changes }) => changes),
732
814
  bases: parts.flatMap(({ bases }) => bases),
@@ -750,15 +832,16 @@ export function composeRuntime(options) {
750
832
  return { outcome: 'refused', rule: 'durable-write' };
751
833
  }
752
834
  pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
835
+ recorded = false;
753
836
  for (const path of consumed)
754
837
  forgetRoomDraft(locator, path);
755
- replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting(), replica));
838
+ replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
756
839
  publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
757
840
  queue: true,
758
841
  drafts: consumed,
759
842
  }));
760
843
  moveTo({ ...saving(status), ...attention() });
761
- void enqueue();
844
+ submitLater(locator);
762
845
  return { outcome: 'saved', identifier: built.batch.id };
763
846
  }
764
847
  async function mark(local, locator, identifier, submission, lost) {
@@ -823,10 +906,10 @@ export function composeRuntime(options) {
823
906
  return { kind: 'transport-uncertainty' };
824
907
  }
825
908
  }
826
- function declaredCards() {
909
+ function declaredAgents() {
827
910
  for (const capability of capabilities) {
828
- if (capability.conversationCards !== undefined)
829
- return capability.conversationCards;
911
+ if (capability.conversationAgents !== undefined)
912
+ return capability.conversationAgents;
830
913
  }
831
914
  return undefined;
832
915
  }
@@ -857,8 +940,6 @@ export function composeRuntime(options) {
857
940
  journal.record(`transfer action ${name}`);
858
941
  try {
859
942
  const result = await options.adapter.action(authorization, { name, input });
860
- if (result.kind === 'action' && result.at !== undefined)
861
- want(result.at);
862
943
  if (result.kind === 'scoped-refusal') {
863
944
  const room = replicas.get(result.locator)?.confirmed.record;
864
945
  if (room !== undefined)
@@ -890,32 +971,11 @@ export function composeRuntime(options) {
890
971
  options.adapter.reached(locator, written, roomVersion) &&
891
972
  !options.adapter.reached(locator, roomVersion, written));
892
973
  }
893
- function want({ locator, roomVersion }) {
894
- if (reachedBy(locator, roomVersion))
895
- return;
896
- const held = wanted.get(locator);
897
- if (held === undefined || options.adapter.reached(locator, roomVersion, held)) {
898
- wanted.set(locator, roomVersion);
899
- }
900
- }
901
- function reachOf(locator) {
902
- let target = wanted.get(locator);
903
- const versions = (replicas.get(locator)?.queue ?? []).flatMap((batch) => {
904
- const accepted = pendings.get(batch.id)?.accepted;
905
- return accepted === undefined ? [] : [accepted];
906
- });
907
- for (const version of versions) {
908
- if (target === undefined || options.adapter.reached(locator, version, target)) {
909
- target = version;
910
- }
911
- }
912
- return target === undefined || reachedBy(locator, target) ? undefined : target;
913
- }
914
- async function pullRefresh(authorized, confirmed, minPosition) {
974
+ async function pullRefresh(authorized, confirmed) {
915
975
  if (!continuing())
916
976
  return unconfirmed;
917
977
  try {
918
- const result = await refreshRoom(options.adapter, authorized, confirmed, continuing, minPosition);
978
+ const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
919
979
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
920
980
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
921
981
  return result;
@@ -925,14 +985,33 @@ export function composeRuntime(options) {
925
985
  throw error;
926
986
  }
927
987
  }
928
- async function pullBootstrap(authorized, room) {
988
+ function staging(local) {
989
+ return {
990
+ held: (locator) => transfers.get(locator) ?? null,
991
+ staged: (record) => local.staged(record),
992
+ stage: (record, page) => exclusive(async () => {
993
+ if (!continuing())
994
+ return false;
995
+ await local.stage(record, page);
996
+ transfers.set(record.locator, record);
997
+ return true;
998
+ }),
999
+ abandon: (locator) => exclusive(async () => {
1000
+ if (!continuing())
1001
+ return;
1002
+ await local.abandon(locator);
1003
+ transfers.delete(locator);
1004
+ }),
1005
+ };
1006
+ }
1007
+ async function pullBootstrap(authorized, room, local) {
929
1008
  if (!continuing())
930
1009
  return unconfirmed;
931
1010
  try {
932
1011
  const result = await bootstrapRoom(options.adapter, authorized, room, {
933
1012
  collection: modelOf(room.locator),
934
1013
  selection: modelOf(room.locator, 'typed-selection'),
935
- }, continuing);
1014
+ }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
936
1015
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
937
1016
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
938
1017
  return result;
@@ -945,35 +1024,46 @@ export function composeRuntime(options) {
945
1024
  async function advance(room, local, authorized) {
946
1025
  const held = replicas.get(room.locator);
947
1026
  if (held !== undefined) {
948
- const refreshed = await pullRefresh(authorized, held.confirmed, reachOf(room.locator));
1027
+ const refreshed = await pullRefresh(authorized, held.confirmed);
949
1028
  if (!continuing())
950
1029
  return unconfirmed;
951
1030
  if (refreshed.kind === 'still-current') {
1031
+ stale.delete(room.locator);
952
1032
  if (sourceRefusals.has(room.locator))
953
- return unconfirmed;
1033
+ return null;
954
1034
  if (unreadable.delete(room.locator)) {
1035
+ scopedRefusals.delete(room.locator);
955
1036
  list(room, true);
956
1037
  emitRooms();
957
1038
  }
958
1039
  return null;
959
1040
  }
960
1041
  if (refreshed.kind === 'advanced') {
961
- await publish(local, room, refreshed.confirmed);
1042
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
1043
+ const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
1044
+ const reached = now !== undefined &&
1045
+ options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
1046
+ return reached ? null : advance(room, local, authorized);
1047
+ }
1048
+ await retainModel(local, room, 'collection', modelOf(room.locator));
962
1049
  return null;
963
1050
  }
964
1051
  if (refreshed.kind !== 'beyond-horizon')
965
1052
  return refreshed;
966
1053
  }
967
- const bootstrapped = await pullBootstrap(authorized, room);
1054
+ const bootstrapped = await pullBootstrap(authorized, room, local);
968
1055
  if (!continuing())
969
1056
  return unconfirmed;
970
1057
  if (bootstrapped.kind !== 'staged')
971
1058
  return bootstrapped;
972
- await publish(local, room, bootstrapped.confirmed);
1059
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
973
1060
  return null;
974
1061
  }
975
1062
  async function withhold(withheld, room, local, progress) {
976
- progress.complete = false;
1063
+ if (withheld.kind === 'refused')
1064
+ stale.delete(room.locator);
1065
+ else
1066
+ stale.add(room.locator);
977
1067
  if (withheld.kind === 'scoped-refusal') {
978
1068
  await scoped(withheld, room, local);
979
1069
  }
@@ -1020,44 +1110,30 @@ export function composeRuntime(options) {
1020
1110
  (accepted.author === undefined ? '' : ` author ${accepted.author}`) +
1021
1111
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1022
1112
  }
1023
- async function recordAccepted(room, local, submitted, accepted) {
1024
- await exclusive(async () => {
1025
- if (!continuing())
1026
- return;
1027
- const current = replicas.get(room.locator);
1028
- if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1029
- return;
1030
- }
1031
- const state = pendings.get(submitted.id);
1032
- if (state === undefined)
1033
- return;
1034
- await local.accept(room.locator, submitted.id, accepted.roomVersion);
1035
- pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1036
- logAccepted(room.locator, submitted, accepted);
1037
- emitQueue(room.locator);
1038
- });
1039
- }
1040
- async function commitAccept(room, local, submitted, accepted, base, overlay) {
1113
+ async function adoptAccepted(room, local, submitted, accepted) {
1041
1114
  if (!continuing())
1042
1115
  return unconfirmed;
1043
1116
  const current = replicas.get(room.locator);
1044
1117
  if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1045
1118
  return null;
1046
1119
  }
1047
- const remaining = current.queue.filter((batch) => batch.id !== submitted.id);
1048
- const retainedBase = readConfirmed({
1049
- ...base.record,
1050
- model: modelOf(room.locator),
1051
- selectionModel: modelOf(room.locator, 'typed-selection'),
1052
- }, base);
1053
- const applied = overlay && retainedBase.kind === 'staged'
1054
- ? applyReturned(retainedBase.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion)
1055
- : retainedBase;
1056
- if (applied.kind !== 'staged')
1120
+ const held = current.confirmed.record.roomVersion;
1121
+ logAccepted(room.locator, submitted, accepted);
1122
+ const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1123
+ ? { kind: 'staged', confirmed: current.confirmed }
1124
+ : applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.changes, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
1125
+ ? accepted.roomVersion
1126
+ : held, accepted.roomVersion);
1127
+ if (applied.kind !== 'staged') {
1128
+ const state = pendings.get(submitted.id);
1129
+ await local.accept(room.locator, submitted.id, accepted.roomVersion);
1130
+ if (state !== undefined)
1131
+ pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1132
+ emitQueue(room.locator);
1057
1133
  return applied;
1058
- const set = await settle(local, room.locator, current, applied.confirmed, remaining, {
1059
- room: true,
1060
- });
1134
+ }
1135
+ const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1136
+ pendings.delete(submitted.id);
1061
1137
  unreadable.delete(room.locator);
1062
1138
  sourceRefusals.delete(room.locator);
1063
1139
  declarationRefusals.delete(room.locator);
@@ -1066,17 +1142,8 @@ export function composeRuntime(options) {
1066
1142
  moveTo({ ...status, ...attention() });
1067
1143
  return null;
1068
1144
  }
1069
- async function accepting(room, local, submitted, accepted, pulled, fresh) {
1070
- const roomVersion = pulled.record.roomVersion;
1071
- if (options.adapter.reached(room.locator, roomVersion, accepted.roomVersion)) {
1072
- return committed(room, local, submitted, accepted, pulled, roomVersion === accepted.roomVersion);
1073
- }
1074
- if (fresh && continuing())
1075
- await publish(local, room, pulled);
1076
- return false;
1077
- }
1078
- async function committed(room, local, submitted, accepted, base, overlay) {
1079
- const failed = await exclusive(() => commitAccept(room, local, submitted, accepted, base, overlay));
1145
+ async function adopted(room, local, submitted, accepted) {
1146
+ const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
1080
1147
  if (failed === null)
1081
1148
  return true;
1082
1149
  if (failed.kind === 'refused' && continuing())
@@ -1089,10 +1156,8 @@ export function composeRuntime(options) {
1089
1156
  const head = replica?.queue[0];
1090
1157
  if (replica === undefined || head === undefined)
1091
1158
  return;
1092
- if (pendings.get(head.id)?.accepted !== undefined) {
1093
- progress.complete = false;
1159
+ if (pendings.get(head.id)?.accepted !== undefined)
1094
1160
  return;
1095
- }
1096
1161
  const state = pendings.get(head.id);
1097
1162
  const { submission, lost: earlier } = state ?? {
1098
1163
  submission: { state: 'waiting' },
@@ -1120,6 +1185,7 @@ export function composeRuntime(options) {
1120
1185
  return;
1121
1186
  }
1122
1187
  const opensBefore = openSignals;
1188
+ const answered = awaitingAnswer(room.locator);
1123
1189
  try {
1124
1190
  result = await options.adapter.mutate(authorized, {
1125
1191
  locator: room.locator,
@@ -1128,11 +1194,14 @@ export function composeRuntime(options) {
1128
1194
  }, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
1129
1195
  }
1130
1196
  catch {
1197
+ answered();
1131
1198
  progress.complete = false;
1132
1199
  if (continuing())
1133
1200
  progress.reachable = false;
1134
1201
  return;
1135
1202
  }
1203
+ if (result.kind !== 'accepted' || !continuing())
1204
+ answered();
1136
1205
  if (!continuing())
1137
1206
  return;
1138
1207
  if (result.kind === 'transport-uncertainty') {
@@ -1185,6 +1254,7 @@ export function composeRuntime(options) {
1185
1254
  const reset = result.reset;
1186
1255
  progress.complete = false;
1187
1256
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1257
+ armTimer();
1188
1258
  return;
1189
1259
  }
1190
1260
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
@@ -1197,7 +1267,8 @@ export function composeRuntime(options) {
1197
1267
  return;
1198
1268
  }
1199
1269
  const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1200
- leaving: result.kind === 'expired' && landed(head, current.confirmed)
1270
+ leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1271
+ (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1201
1272
  ? { ...left, returned: null }
1202
1273
  : left,
1203
1274
  });
@@ -1211,95 +1282,33 @@ export function composeRuntime(options) {
1211
1282
  return withhold(withheld, room, local, progress);
1212
1283
  continue;
1213
1284
  }
1214
- await recordAccepted(room, local, head, result);
1215
- const from = replicas.get(room.locator)?.confirmed;
1216
- if (from === undefined)
1217
- return;
1218
- const pulled = await pullRefresh(authorized, from, reachOf(room.locator));
1219
- if (!continuing())
1220
- return;
1221
- if (pulled.kind === 'revoked-authorization') {
1222
- await refuse('revoked-authorization');
1223
- return;
1224
- }
1225
- if (pulled.kind === 'scoped-refusal') {
1226
- await scoped(pulled, room, local);
1227
- progress.complete = false;
1228
- return;
1229
- }
1230
- if (pulled.kind === 'still-current' || pulled.kind === 'advanced') {
1231
- const fresh = pulled.kind === 'advanced';
1232
- if (!(await accepting(room, local, head, result, fresh ? pulled.confirmed : from, fresh))) {
1233
- progress.complete = false;
1234
- return;
1235
- }
1236
- continue;
1237
- }
1238
- if (pulled.kind === 'beyond-horizon') {
1239
- const bootstrapped = await pullBootstrap(authorized, room);
1240
- if (!continuing())
1241
- return;
1242
- if (bootstrapped.kind === 'revoked-authorization') {
1243
- await refuse('revoked-authorization');
1244
- return;
1245
- }
1246
- if (bootstrapped.kind === 'scoped-refusal') {
1247
- await scoped(bootstrapped, room, local);
1248
- progress.complete = false;
1249
- return;
1250
- }
1251
- if (bootstrapped.kind === 'staged') {
1252
- if (!(await accepting(room, local, head, result, bootstrapped.confirmed, true))) {
1253
- progress.complete = false;
1254
- return;
1255
- }
1256
- continue;
1257
- }
1285
+ const kept = await adopted(room, local, head, result).finally(answered);
1286
+ if (!kept) {
1258
1287
  progress.complete = false;
1259
- if (bootstrapped.kind === 'transport-uncertainty') {
1260
- progress.reachable = false;
1261
- }
1262
- else if (bootstrapped.kind === 'room-not-readable') {
1263
- unreadable.add(room.locator);
1264
- list(room, false);
1265
- emitRooms();
1266
- }
1267
- else if (bootstrapped.kind === 'refused') {
1268
- refusedSource(room, bootstrapped);
1269
- }
1270
1288
  return;
1271
1289
  }
1272
- progress.complete = false;
1273
- if (pulled.kind === 'transport-uncertainty') {
1274
- progress.reachable = false;
1275
- }
1276
- else if (pulled.kind === 'room-not-readable') {
1277
- unreadable.add(room.locator);
1278
- list(room, false);
1279
- emitRooms();
1280
- }
1281
- else if (pulled.kind === 'retryable-refusal') {
1282
- journal.record(`refusal ${pulled.kind} ${pulled.code}`);
1283
- }
1284
- else if (pulled.kind === 'refused') {
1285
- refusedSource(room, pulled);
1286
- }
1287
- return;
1288
1290
  }
1289
- return;
1291
+ }
1292
+ function awaitingAnswer(locator) {
1293
+ let release = () => { };
1294
+ const awaited = new Promise((resolve) => {
1295
+ release = resolve;
1296
+ });
1297
+ answering.set(locator, awaited);
1298
+ return () => {
1299
+ if (answering.get(locator) === awaited)
1300
+ answering.delete(locator);
1301
+ release();
1302
+ };
1290
1303
  }
1291
1304
  async function retainModel(local, room, attached, next) {
1292
1305
  const key = modelKey(room.locator, attached);
1293
- if (next.status === 'unavailable')
1294
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
1295
- const epoch = modelEpoch.get(key) ?? 0;
1296
1306
  const previous = modelOf(room.locator, attached);
1297
1307
  const heldBefore = replicas.get(room.locator)?.confirmed.record;
1298
1308
  if (JSON.stringify(previous) === JSON.stringify(next) &&
1299
1309
  (heldBefore === undefined ||
1300
1310
  (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
1301
1311
  JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
1302
- JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
1303
1312
  JSON.stringify(heldBefore.typedSelection) ===
1304
1313
  JSON.stringify(room.typedSelection ?? null))))
1305
1314
  return;
@@ -1310,20 +1319,17 @@ export function composeRuntime(options) {
1310
1319
  await exclusive(async () => {
1311
1320
  if (!continuing())
1312
1321
  return;
1313
- if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
1314
- return;
1315
1322
  const held = replicas.get(room.locator);
1316
1323
  if (held !== undefined) {
1317
1324
  const declared = {
1318
1325
  ...held.confirmed.record,
1319
1326
  operations: room.operations,
1320
1327
  replica: room.replica,
1321
- attachedModel: room.attachedModel,
1322
1328
  typedSelection: room.typedSelection ?? null,
1323
1329
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1324
1330
  };
1325
1331
  const reclassified = sameMembership(declared, held.confirmed.record)
1326
- ? readConfirmed(declared, held.confirmed)
1332
+ ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
1327
1333
  : readReclassified(declared, held.confirmed);
1328
1334
  const record = reclassified.kind === 'staged'
1329
1335
  ? declared
@@ -1331,14 +1337,16 @@ export function composeRuntime(options) {
1331
1337
  ...declared,
1332
1338
  typedSelection: held.confirmed.record.typedSelection,
1333
1339
  };
1334
- const read = reclassified.kind === 'deferred' ? readConfirmed(record, held.confirmed) : reclassified;
1340
+ const read = reclassified.kind === 'deferred'
1341
+ ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
1342
+ : reclassified;
1335
1343
  await local.write({
1336
1344
  next: record,
1337
1345
  previous: held.confirmed.record,
1338
1346
  queue: held.queue,
1339
1347
  });
1340
1348
  if (read.kind === 'staged') {
1341
- replicas.set(room.locator, replicaOf(read.confirmed, held.queue, rewriting(), held));
1349
+ replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1342
1350
  if (declarationRefusals.delete(room.locator)) {
1343
1351
  sourceRefusals.delete(room.locator);
1344
1352
  unreadable.delete(room.locator);
@@ -1362,77 +1370,6 @@ export function composeRuntime(options) {
1362
1370
  publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
1363
1371
  });
1364
1372
  }
1365
- async function acquireModel(local, room, authorized, selection) {
1366
- const attached = selection === null ? 'collection' : 'typed-selection';
1367
- const key = modelKey(room.locator, attached);
1368
- const declared = selection === null ? room.attachedModel : selection.attachedModel;
1369
- let previous = modelOf(room.locator, attached);
1370
- if (previous.status !== 'unavailable' &&
1371
- (declared === null || !sameModel(previous.snapshot, declared))) {
1372
- previous = { status: 'unavailable', reason: 'version-changed' };
1373
- await retainModel(local, room, attached, previous);
1374
- if (discoveryPaused)
1375
- return;
1376
- }
1377
- const epoch = modelEpoch.get(key) ?? 0;
1378
- let answer;
1379
- try {
1380
- answer = await options.adapter.model(authorized, selection === null
1381
- ? { locator: room.locator, attached: 'collection', model: declared }
1382
- : {
1383
- locator: room.locator,
1384
- attached: 'typed-selection',
1385
- roots: selection.roots,
1386
- model: declared,
1387
- });
1388
- }
1389
- catch {
1390
- answer = { kind: 'transport-uncertainty' };
1391
- }
1392
- if (discoveryPaused || epoch !== (modelEpoch.get(key) ?? 0))
1393
- return;
1394
- if (answer.kind === 'revoked-authorization') {
1395
- await refuse('revoked-authorization');
1396
- return;
1397
- }
1398
- if (answer.kind === 'transport-uncertainty') {
1399
- await retainModel(local, room, attached, cached(previous));
1400
- return;
1401
- }
1402
- if (answer.kind === 'model-unavailable') {
1403
- await retainModel(local, room, attached, {
1404
- status: 'unavailable',
1405
- reason: answer.reason,
1406
- });
1407
- return;
1408
- }
1409
- if (declared === null || !sameModel(answer.snapshot, declared)) {
1410
- await retainModel(local, room, attached, {
1411
- status: 'unavailable',
1412
- reason: 'version-changed',
1413
- });
1414
- return;
1415
- }
1416
- if (previous.status !== 'unavailable' &&
1417
- sameModel(previous.snapshot, answer.snapshot) &&
1418
- previous.snapshot.schema !== answer.snapshot.schema) {
1419
- await retainModel(local, room, attached, { status: 'unavailable', reason: 'invalid' });
1420
- return;
1421
- }
1422
- const verified = answer.snapshot.schema === null ? EMPTY_SCHEMA : verify(answer.snapshot.schema);
1423
- if (verified.verdict === 'refuse') {
1424
- const where = verified.location === undefined ? '' : ` ${verified.location}`;
1425
- journal.record(`model invalid ${room.locator} ${attached} ${answer.snapshot.identity} ${verified.rule}${where}`);
1426
- }
1427
- await retainModel(local, room, attached, verified.verdict === 'accept'
1428
- ? {
1429
- status: 'current',
1430
- snapshot: structuredClone(answer.snapshot),
1431
- verifiedWith: MD_MODEL,
1432
- types: verified.model.types,
1433
- }
1434
- : { status: 'unavailable', reason: 'invalid' });
1435
- }
1436
1373
  async function scoped(result, room, local) {
1437
1374
  scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
1438
1375
  let scopedAway = room;
@@ -1460,14 +1397,36 @@ export function composeRuntime(options) {
1460
1397
  emitRooms();
1461
1398
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
1462
1399
  }
1463
- async function synchronize(discovered, local, progress) {
1464
- const prepared = await Promise.allSettled(discovered.map((room) => prepareRoom(room, local, progress)));
1465
- const ready = discovered.filter((_, index) => {
1400
+ function heldRooms(listed) {
1401
+ return listed.flatMap((room) => {
1402
+ const record = replicas.get(room.locator)?.confirmed.record;
1403
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1404
+ return [];
1405
+ return [
1406
+ {
1407
+ locator: room.locator,
1408
+ roomVersion: record.roomVersion,
1409
+ documents: record.documents,
1410
+ revisions: record.revisions,
1411
+ attachments: record.attachments,
1412
+ },
1413
+ ];
1414
+ });
1415
+ }
1416
+ async function synchronize(listed, local, progress, attempt) {
1417
+ const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1418
+ const ready = listed.filter((_, index) => {
1466
1419
  const outcome = prepared[index];
1467
1420
  return outcome?.status === 'fulfilled' && outcome.value;
1468
1421
  });
1469
- const rooms = await Promise.allSettled(ready.map((room) => synchronizeRoom(room, local, progress)));
1470
- const failed = [...prepared, ...rooms].find((room) => room.status === 'rejected');
1422
+ if (attempt === 'opened') {
1423
+ options.adapter.roomStream?.hold(heldRooms(ready));
1424
+ handed = true;
1425
+ }
1426
+ const loaded = await Promise.allSettled(ready
1427
+ .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1428
+ .map((room) => loadRoom(room, local, progress)));
1429
+ const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1471
1430
  if (failed !== undefined)
1472
1431
  throw failed.reason;
1473
1432
  }
@@ -1484,11 +1443,8 @@ export function composeRuntime(options) {
1484
1443
  progress.complete = false;
1485
1444
  return false;
1486
1445
  }
1487
- const selection = room.typedSelection;
1488
- await Promise.all([
1489
- acquireModel(local, room, authorization, null),
1490
- ...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
1491
- ]);
1446
+ if (prior !== undefined)
1447
+ await retainModel(local, room, 'collection', modelOf(room.locator));
1492
1448
  if (discoveryPaused || authorization === undefined)
1493
1449
  return false;
1494
1450
  if (room.replica.admission !== 'admitted') {
@@ -1496,24 +1452,53 @@ export function composeRuntime(options) {
1496
1452
  progress.complete = false;
1497
1453
  return false;
1498
1454
  }
1499
- const held = replicas.get(room.locator);
1500
- if (held !== undefined && held.confirmed.record.replica.selection !== room.replica.selection) {
1501
- progress.complete = false;
1502
- return false;
1503
- }
1504
1455
  return true;
1505
1456
  }
1506
- async function synchronizeRoom(room, local, progress) {
1457
+ async function caughtUp(room, local, progress) {
1458
+ const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1459
+ if (held === undefined || authorization === undefined)
1460
+ return;
1461
+ const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1462
+ if (!behind)
1463
+ return;
1464
+ const withheld = await advance(room, local, authorization);
1465
+ if (withheld !== null)
1466
+ await withhold(withheld, room, local, progress);
1467
+ }
1468
+ function revoked(locator) {
1469
+ return ['collection', 'typed-selection'].some((attached) => {
1470
+ const model = modelOf(locator, attached);
1471
+ return model.status === 'unavailable' && model.reason === 'revoked';
1472
+ });
1473
+ }
1474
+ async function loadRoom(room, local, progress) {
1507
1475
  if (discoveryPaused || authorization === undefined)
1508
1476
  return;
1477
+ if (replicas.has(room.locator) && revoked(room.locator)) {
1478
+ await reload(room, local, progress);
1479
+ return;
1480
+ }
1509
1481
  const withheld = await advance(room, local, authorization);
1510
1482
  if (withheld !== null) {
1511
1483
  await withhold(withheld, room, local, progress);
1512
1484
  return;
1513
1485
  }
1514
- await submitRoom(room, local, authorization, progress);
1486
+ await caughtUp(room, local, progress);
1515
1487
  }
1516
- async function cycle(local, progress) {
1488
+ async function reload(room, local, progress) {
1489
+ if (discoveryPaused || authorization === undefined)
1490
+ return;
1491
+ const bootstrapped = await pullBootstrap(authorization, room, local);
1492
+ if (!continuing())
1493
+ return;
1494
+ if (bootstrapped.kind !== 'staged') {
1495
+ await withhold(bootstrapped, room, local, progress);
1496
+ return;
1497
+ }
1498
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1499
+ await caughtUp(room, local, progress);
1500
+ }
1501
+ async function discoverRooms(local, progress, attempt) {
1517
1502
  if (closed || authorization === undefined) {
1518
1503
  notifyReady();
1519
1504
  return;
@@ -1539,6 +1524,7 @@ export function composeRuntime(options) {
1539
1524
  }
1540
1525
  }
1541
1526
  progress.reachable = false;
1527
+ progress.complete = false;
1542
1528
  notifyReady();
1543
1529
  return;
1544
1530
  }
@@ -1553,17 +1539,6 @@ export function composeRuntime(options) {
1553
1539
  notifyReady();
1554
1540
  return;
1555
1541
  }
1556
- if (!result.supportedVersions.includes(ROOM_CONTRACT_VERSION)) {
1557
- await refuse('no-matching-version');
1558
- notifyReady();
1559
- return;
1560
- }
1561
- if (result.mdModel.identity !== MD_MODEL.identity ||
1562
- result.mdModel.version !== MD_MODEL.version) {
1563
- await refuse('md-model-version-mismatch');
1564
- notifyReady();
1565
- return;
1566
- }
1567
1542
  for (const room of result.rooms) {
1568
1543
  if (room.operations === undefined ||
1569
1544
  !['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
@@ -1571,8 +1546,7 @@ export function composeRuntime(options) {
1571
1546
  room.replica === undefined ||
1572
1547
  typeof room.replica.selection !== 'string' ||
1573
1548
  room.replica.selection.length === 0 ||
1574
- !['admitted', 'unavailable'].includes(room.replica.admission) ||
1575
- !declaredIdentity(room.attachedModel)) {
1549
+ !['admitted', 'unavailable'].includes(room.replica.admission)) {
1576
1550
  journal.record(`invalid model or replica declaration ${room.locator}`);
1577
1551
  await refuse('missing-required-guarantee');
1578
1552
  notifyReady();
@@ -1585,14 +1559,6 @@ export function composeRuntime(options) {
1585
1559
  return;
1586
1560
  }
1587
1561
  }
1588
- const declared = new Set(result.capabilities.map((capability) => capability.name));
1589
- for (const required of options.requiredGuarantees) {
1590
- if (!knownCapabilities.has(required) || !declared.has(required)) {
1591
- await refuse('missing-required-guarantee');
1592
- notifyReady();
1593
- return;
1594
- }
1595
- }
1596
1562
  if (refusal !== null) {
1597
1563
  refusal = null;
1598
1564
  emitRefusal();
@@ -1600,26 +1566,19 @@ export function composeRuntime(options) {
1600
1566
  const previousCapabilities = capabilities;
1601
1567
  capabilities = result.capabilities;
1602
1568
  notifyReady();
1603
- if (declared.has('room-version-notifications'))
1604
- observeRoomVersions();
1605
1569
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1606
1570
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
1607
1571
  limits = result.providerLimits;
1608
1572
  await local.declare(limits, capabilities);
1609
1573
  }
1574
+ const signedIn = result.member ?? null;
1575
+ if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
1576
+ await local.keepMember(signedIn);
1577
+ member = signedIn;
1578
+ emitMember();
1579
+ }
1610
1580
  await applyDiscovery(result.rooms, local);
1611
- handSelected(result.rooms
1612
- .filter((room) => room.replica.admission === 'admitted')
1613
- .map((room) => room.locator)
1614
- .sort());
1615
- progress.complete = true;
1616
- await synchronize(result.rooms, local, progress);
1617
- if (closed || discoveryPaused)
1618
- return;
1619
- await conversations.reconcile();
1620
- if (status.providerUnreachable)
1621
- conversations.reconnected();
1622
- await conversations.catchUp(progress);
1581
+ await synchronize(result.rooms, local, progress, attempt);
1623
1582
  }
1624
1583
  async function ensureAuthorization() {
1625
1584
  if (authorization !== undefined)
@@ -1633,64 +1592,253 @@ export function composeRuntime(options) {
1633
1592
  throw error;
1634
1593
  }
1635
1594
  }
1636
- async function connect() {
1637
- if (closed || stopping || discoveryPaused) {
1638
- notifyReady();
1595
+ function task(work) {
1596
+ if (stopping)
1597
+ return chain;
1598
+ chain = chain.then(work, work).catch(() => undefined);
1599
+ return chain;
1600
+ }
1601
+ function sync(attempt) {
1602
+ return task(async () => {
1603
+ if (closed || stopping || discoveryPaused) {
1604
+ notifyReady();
1605
+ return;
1606
+ }
1607
+ const progress = { complete: true, reachable: true };
1608
+ underway = attempt === 'opened' ? progress : null;
1609
+ complete = false;
1610
+ recorded = false;
1611
+ moveTo(transferring(status));
1612
+ handed = false;
1613
+ try {
1614
+ const local = await openLocal();
1615
+ if (closed)
1616
+ return;
1617
+ await ensureAuthorization();
1618
+ if (closed)
1619
+ return;
1620
+ await discoverRooms(local, progress, attempt);
1621
+ }
1622
+ catch {
1623
+ progress.complete = false;
1624
+ if (attempt === 'opened' && !handed)
1625
+ progress.reachable = false;
1626
+ }
1627
+ finally {
1628
+ notifyReady();
1629
+ }
1630
+ if (attempt !== 'opened') {
1631
+ await finishOpening(progress);
1632
+ return;
1633
+ }
1634
+ if (handed || closed || progress.reachable)
1635
+ return;
1636
+ underway = null;
1637
+ journal.record('connection reopen');
1638
+ await rest(progress);
1639
+ options.adapter.roomStream?.reopen();
1640
+ });
1641
+ }
1642
+ function closedSocket(revoked) {
1643
+ const opened = socketOpen;
1644
+ socketOpen = false;
1645
+ if (revoked) {
1646
+ void refuse('revoked-authorization');
1639
1647
  return;
1640
1648
  }
1641
- startTimer();
1642
- const progress = { complete: false, reachable: true };
1643
- let completedAt = new Date().toISOString();
1644
- try {
1645
- const local = await openLocal();
1646
- if (closed)
1647
- return;
1648
- await ensureAuthorization();
1649
- if (closed)
1649
+ if (opened || status.providerUnreachable)
1650
+ return;
1651
+ moveTo({ ...status, providerUnreachable: true });
1652
+ }
1653
+ function applyOpening(expired) {
1654
+ return task(async () => {
1655
+ const progress = underway;
1656
+ underway = null;
1657
+ if (closed || stopping)
1650
1658
  return;
1651
- await cycle(local, progress);
1652
- if (closed)
1659
+ if (progress === null) {
1660
+ await reloadExpired(expired);
1653
1661
  return;
1654
- if (progress.complete) {
1655
- completedAt = new Date().toISOString();
1656
- await local.complete(completedAt);
1657
1662
  }
1663
+ try {
1664
+ const local = await openLocal();
1665
+ const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1666
+ const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1667
+ const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
1668
+ if (progress.reachable) {
1669
+ for (const room of held)
1670
+ if (!reloading.includes(room))
1671
+ await confirmModels(room, local);
1672
+ }
1673
+ const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1674
+ if (failed !== undefined)
1675
+ throw failed.reason;
1676
+ }
1677
+ catch {
1678
+ progress.complete = false;
1679
+ progress.reachable = false;
1680
+ }
1681
+ await finishOpening(progress);
1682
+ });
1683
+ }
1684
+ async function reloadExpired(expired) {
1685
+ const progress = { complete: true, reachable: true };
1686
+ try {
1687
+ const local = await openLocal();
1688
+ await Promise.allSettled([...discovered.values()]
1689
+ .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1690
+ .map((room) => reload(room, local, progress)));
1658
1691
  }
1659
1692
  catch {
1660
- progress.complete = false;
1661
- notifyReady();
1693
+ progress.reachable = false;
1694
+ }
1695
+ await rest(progress);
1696
+ }
1697
+ async function confirmModels(room, local) {
1698
+ for (const attached of ['collection', 'typed-selection']) {
1699
+ const model = modelOf(room.locator, attached);
1700
+ if (model.status === 'cached') {
1701
+ await retainModel(local, room, attached, { ...model, status: 'current' });
1702
+ }
1703
+ }
1704
+ }
1705
+ function submitLater(locator) {
1706
+ const running = submitting.get(locator);
1707
+ if (running !== undefined) {
1708
+ running.again = true;
1662
1709
  return;
1663
1710
  }
1664
- finally {
1665
- notifyReady();
1666
- moveTo(settled(status, progress, completedAt, pending(), attention()));
1711
+ const run = { again: false, done: Promise.resolve() };
1712
+ submitting.set(locator, run);
1713
+ run.done = (async () => {
1714
+ try {
1715
+ do {
1716
+ run.again = false;
1717
+ const room = discovered.get(locator);
1718
+ if (closed || stopping || discoveryPaused || room === undefined)
1719
+ return;
1720
+ const progress = { complete: true, reachable: true };
1721
+ try {
1722
+ await ensureAuthorization();
1723
+ if (authorization === undefined)
1724
+ return;
1725
+ await submitRoom(room, await openLocal(), authorization, progress);
1726
+ }
1727
+ catch {
1728
+ progress.reachable = false;
1729
+ }
1730
+ armTimer();
1731
+ await rest(progress);
1732
+ } while (run.again);
1733
+ }
1734
+ finally {
1735
+ submitting.delete(locator);
1736
+ }
1737
+ })();
1738
+ }
1739
+ function submitAll() {
1740
+ for (const [locator, replica] of replicas) {
1741
+ if (replica.queue.length > 0)
1742
+ submitLater(locator);
1667
1743
  }
1668
1744
  }
1669
- async function run() {
1745
+ async function finishOpening(progress) {
1670
1746
  try {
1671
- await connect();
1747
+ if (closed || discoveryPaused) {
1748
+ progress.complete = false;
1749
+ return;
1750
+ }
1751
+ submitAll();
1752
+ await conversations.reconcile();
1753
+ if (status.providerUnreachable)
1754
+ conversations.reconnected();
1755
+ const reading = { complete: true, reachable: true };
1756
+ await conversations.catchUp(reading);
1757
+ if (!reading.reachable)
1758
+ progress.reachable = false;
1759
+ }
1760
+ catch {
1761
+ progress.complete = false;
1672
1762
  }
1673
1763
  finally {
1674
- inflight = false;
1675
- if (queued && !closed) {
1676
- queued = false;
1677
- void enqueue();
1678
- }
1764
+ complete = progress.complete;
1765
+ await rest(progress);
1679
1766
  }
1680
1767
  }
1681
- function observeRoomVersions() {
1682
- const path = options.adapter.roomVersions;
1683
- if (path === undefined || withdrawNotifications !== undefined)
1768
+ async function rest(progress) {
1769
+ if (closed)
1684
1770
  return;
1685
- withdrawNotifications = path.observe((notification) => {
1686
- if (closed || stopping || discoveryPaused)
1771
+ let lastCompleted = status.lastCompleted;
1772
+ let synced = complete && stale.size === 0 && !pending();
1773
+ if (synced && !recorded) {
1774
+ const completedAt = new Date().toISOString();
1775
+ try {
1776
+ await (await openLocal()).complete(completedAt);
1777
+ recorded = true;
1778
+ lastCompleted = completedAt;
1779
+ }
1780
+ catch {
1781
+ synced = false;
1782
+ }
1783
+ }
1784
+ moveTo({
1785
+ state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
1786
+ lastCompleted,
1787
+ providerUnreachable: !progress.reachable,
1788
+ ...attention(),
1789
+ });
1790
+ }
1791
+ function applyRoom(locator, roomVersion) {
1792
+ announced.set(locator, roomVersion);
1793
+ return task(async () => {
1794
+ await answering.get(locator);
1795
+ if (closed || stopping || discoveryPaused || authorization === undefined)
1796
+ return;
1797
+ const room = discovered.get(locator);
1798
+ const held = replicas.get(locator);
1799
+ if (room === undefined || room.replica.admission !== 'admitted')
1687
1800
  return;
1688
- if (notification.modelChanged === true && handed?.includes(notification.locator) === true) {
1689
- void enqueue();
1801
+ if (held !== undefined &&
1802
+ (held.confirmed.record.replica.selection !== room.replica.selection ||
1803
+ options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
1804
+ return;
1805
+ }
1806
+ const during = underway;
1807
+ const progress = during ?? { complete: true, reachable: true };
1808
+ if (during === null)
1809
+ moveTo(transferring(status));
1810
+ try {
1811
+ const local = await openLocal();
1812
+ const withheld = await advance(room, local, authorization);
1813
+ if (withheld !== null)
1814
+ await withhold(withheld, room, local, progress);
1815
+ }
1816
+ catch {
1817
+ stale.add(locator);
1818
+ }
1819
+ finally {
1820
+ if (during === null)
1821
+ await rest(progress);
1822
+ }
1823
+ });
1824
+ }
1825
+ function observeStream() {
1826
+ const stream = options.adapter.roomStream;
1827
+ if (stream === undefined || withdrawStream !== undefined)
1828
+ return;
1829
+ withdrawStream = stream.observe((event) => {
1830
+ if (closed || stopping)
1690
1831
  return;
1832
+ if (event.kind === 'opened') {
1833
+ socketOpen = true;
1834
+ void sync('opened');
1691
1835
  }
1692
- notices.set(notification.locator, notification.roomVersion);
1693
- chain = chain.then(runNotified, runNotified);
1836
+ else if (event.kind === 'closed')
1837
+ closedSocket(event.revoked);
1838
+ else if (event.kind === 'opening')
1839
+ void applyOpening(event.expired);
1840
+ else
1841
+ void applyRoom(event.locator, event.roomVersion);
1694
1842
  });
1695
1843
  }
1696
1844
  function writesHeld() {
@@ -1711,14 +1859,17 @@ export function composeRuntime(options) {
1711
1859
  listener(mode);
1712
1860
  }
1713
1861
  if (releasing)
1714
- void enqueue();
1862
+ submitAll();
1715
1863
  }
1716
1864
  function observeConversations() {
1717
1865
  const stream = options.adapter.conversations;
1718
1866
  if (stream === undefined || withdrawConversations !== undefined)
1719
1867
  return;
1720
1868
  withdrawConversations = stream.observe((event) => {
1721
- conversations.receive(event);
1869
+ if (conversations.receive(event)) {
1870
+ void ready.then(scheduleCurrency);
1871
+ return;
1872
+ }
1722
1873
  if (event.kind !== 'gap' || conversations.current())
1723
1874
  return;
1724
1875
  show();
@@ -1729,7 +1880,7 @@ export function composeRuntime(options) {
1729
1880
  if (currencyQueued || closed || stopping)
1730
1881
  return;
1731
1882
  currencyQueued = true;
1732
- chain = chain.then(runCurrency, runCurrency);
1883
+ void task(runCurrency);
1733
1884
  }
1734
1885
  async function runCurrency() {
1735
1886
  currencyQueued = false;
@@ -1755,95 +1906,12 @@ export function composeRuntime(options) {
1755
1906
  }
1756
1907
  }
1757
1908
  }
1758
- function handSelected(locators) {
1759
- const path = options.adapter.roomVersions;
1760
- if (path === undefined || withdrawNotifications === undefined)
1761
- return;
1762
- if (handed !== undefined && JSON.stringify(locators) === JSON.stringify(handed))
1763
- return;
1764
- handed = locators;
1765
- path.select(locators);
1766
- }
1767
- async function runNotified() {
1768
- const announced = [...notices];
1769
- notices.clear();
1770
- if (announced.length === 0 || closed || stopping || discoveryPaused)
1771
- return;
1772
- const resting = status;
1773
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1774
- moveTo(transferring(resting));
1775
- try {
1776
- const local = await openLocal();
1777
- for (const [locator, roomVersion] of announced) {
1778
- if (closed || discoveryPaused || authorization === undefined)
1779
- return;
1780
- if (handed?.includes(locator) !== true)
1781
- continue;
1782
- const room = discovered.get(locator);
1783
- const held = replicas.get(locator);
1784
- if (room === undefined)
1785
- continue;
1786
- if (sourceRefusals.has(locator)) {
1787
- void enqueue();
1788
- continue;
1789
- }
1790
- if (held === undefined)
1791
- continue;
1792
- if (held.confirmed.record.roomVersion === roomVersion)
1793
- continue;
1794
- const withheld = await advance(room, local, authorization);
1795
- if (withheld === null)
1796
- continue;
1797
- if (withheld.kind === 'refused') {
1798
- void enqueue();
1799
- continue;
1800
- }
1801
- if (withheld.kind === 'scoped-refusal') {
1802
- await scoped(withheld, room, local);
1803
- continue;
1804
- }
1805
- if (withheld.kind === 'transport-uncertainty') {
1806
- progress.reachable = false;
1807
- continue;
1808
- }
1809
- if (withheld.kind === 'room-not-readable') {
1810
- unreadable.add(locator);
1811
- list(room, false);
1812
- emitRooms();
1813
- continue;
1814
- }
1815
- if (withheld.kind === 'revoked-authorization') {
1816
- await refuse('revoked-authorization');
1817
- return;
1818
- }
1819
- }
1820
- }
1821
- catch {
1822
- progress.reachable = false;
1823
- }
1824
- finally {
1825
- if (!closed) {
1826
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1827
- }
1828
- }
1829
- }
1830
- function enqueue() {
1831
- if (stopping)
1832
- return chain;
1833
- if (inflight) {
1834
- queued = true;
1835
- return chain;
1836
- }
1837
- inflight = true;
1838
- chain = chain.then(run, run);
1839
- return chain;
1840
- }
1841
1909
  function close() {
1842
1910
  if (closed)
1843
1911
  return;
1844
1912
  closed = true;
1845
- withdrawNotifications?.();
1846
- withdrawNotifications = undefined;
1913
+ withdrawStream?.();
1914
+ withdrawStream = undefined;
1847
1915
  withdrawConversations?.();
1848
1916
  withdrawConversations = undefined;
1849
1917
  withdrawServiceMode?.();
@@ -1858,6 +1926,7 @@ export function composeRuntime(options) {
1858
1926
  storageLossListeners.clear();
1859
1927
  statusListeners.clear();
1860
1928
  serviceModeListeners.clear();
1929
+ memberListeners.clear();
1861
1930
  readSide.close();
1862
1931
  queueSubscriptions.clear();
1863
1932
  roomDraftListeners.clear();
@@ -1867,6 +1936,7 @@ export function composeRuntime(options) {
1867
1936
  stopping = true;
1868
1937
  stopTimer();
1869
1938
  await chain.catch(() => undefined);
1939
+ await Promise.all([...submitting.values()].map((run) => run.done));
1870
1940
  await mutations;
1871
1941
  close();
1872
1942
  }
@@ -1875,11 +1945,32 @@ export function composeRuntime(options) {
1875
1945
  options.signal?.addEventListener('abort', close, { once: true });
1876
1946
  if (options.signal?.aborted)
1877
1947
  close();
1878
- void openLocal().catch(() => {
1948
+ observeStream();
1949
+ void openLocal().then(() => {
1950
+ if (capabilities.length > 0)
1951
+ notifyReady();
1952
+ }, () => {
1879
1953
  notifyReady();
1880
1954
  });
1881
- void enqueue();
1882
- function watch(topic, input, deliver) {
1955
+ if (options.adapter.roomStream === undefined)
1956
+ void sync('unstreamed');
1957
+ function planned(input, reach) {
1958
+ return planOf(input, roomFacts(reach), planReach(reach));
1959
+ }
1960
+ function watchPlan(plan, deliver) {
1961
+ materialize(reachedRooms(plan));
1962
+ return readSide.watch(plan, identityOf(plan), deliver);
1963
+ }
1964
+ async function searched(input, reach) {
1965
+ const plan = searchPlanOf(input);
1966
+ const selected = plan.documents === undefined && reach === null
1967
+ ? null
1968
+ : (await rowsQuery(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
1969
+ if (selected === null)
1970
+ materialize(null);
1971
+ return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
1972
+ }
1973
+ const watch = (topic, input, deliver) => {
1883
1974
  if (topic === 'room-drafts') {
1884
1975
  const listener = { notify: deliver, ready: false };
1885
1976
  roomDraftListeners.add(listener);
@@ -1900,6 +1991,8 @@ export function composeRuntime(options) {
1900
1991
  if (topic === 'conversation') {
1901
1992
  return conversations.watch(input[0], input[1], deliver);
1902
1993
  }
1994
+ if (topic === 'conversations')
1995
+ return conversations.watchList(deliver);
1903
1996
  if (topic === 'presentation') {
1904
1997
  const listener = {
1905
1998
  app: input[0],
@@ -1954,6 +2047,13 @@ export function composeRuntime(options) {
1954
2047
  serviceModeListeners.delete(deliver);
1955
2048
  };
1956
2049
  }
2050
+ if (topic === 'member') {
2051
+ memberListeners.add(deliver);
2052
+ deliver(member);
2053
+ return () => {
2054
+ memberListeners.delete(deliver);
2055
+ };
2056
+ }
1957
2057
  if (topic === 'queue') {
1958
2058
  const queue = { locator: input[0], notify: deliver };
1959
2059
  queueSubscriptions.add(queue);
@@ -1962,9 +2062,8 @@ export function composeRuntime(options) {
1962
2062
  queueSubscriptions.delete(queue);
1963
2063
  };
1964
2064
  }
1965
- const plan = planOf(input[0], roomFacts());
1966
- return readSide.watch(plan, identityOf(plan), deliver);
1967
- }
2065
+ return watchPlan(planned(input[0], null), deliver);
2066
+ };
1968
2067
  const md = {
1969
2068
  loaded() {
1970
2069
  return openLocal().then(() => undefined);
@@ -1972,15 +2071,8 @@ export function composeRuntime(options) {
1972
2071
  rooms() {
1973
2072
  return listed();
1974
2073
  },
1975
- query: ((input) => readSide.query(planOf(input, roomFacts()))),
1976
- async search(input) {
1977
- const plan = searchPlanOf(input);
1978
- const selected = plan.documents === undefined
1979
- ? null
1980
- : (await readSide.query(filterOf(plan.documents, roomFacts())))
1981
- .documents;
1982
- return searchIndex.search(plan, selected);
1983
- },
2074
+ query: ((input) => rowsQuery(planned(input, null))),
2075
+ search: (input) => searched(input, null),
1984
2076
  types(locator) {
1985
2077
  return {
1986
2078
  collection: modelTypes(modelOf(locator)),
@@ -2087,9 +2179,7 @@ export function composeRuntime(options) {
2087
2179
  persistence() {
2088
2180
  return options.shell.persistentStorage;
2089
2181
  },
2090
- observe: ((topic, ...input) => opened((deliver) => watch(topic, input, (message) => {
2091
- deliver(structuredClone(message));
2092
- }), topic === 'query')),
2182
+ observe: observing(watch),
2093
2183
  async reauthorize() {
2094
2184
  if (refusal === null || !refusal.reauthorizationOffered)
2095
2185
  return;
@@ -2098,8 +2188,19 @@ export function composeRuntime(options) {
2098
2188
  await mutations;
2099
2189
  authorization = undefined;
2100
2190
  discoveryPaused = false;
2101
- await enqueue();
2191
+ if (options.adapter.roomStream === undefined)
2192
+ await sync('unstreamed');
2193
+ else
2194
+ await task(ensureAuthorization);
2102
2195
  },
2103
2196
  };
2104
- return { md, stop, watch };
2197
+ const reachable = {
2198
+ md,
2199
+ watch,
2200
+ plan: planned,
2201
+ query: (plan) => rowsQuery(plan),
2202
+ watchPlan,
2203
+ search: searched,
2204
+ };
2205
+ return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2105
2206
  }