@agentshouse/mdruntime 0.1.0-alpha.20 → 0.1.0-alpha.22

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 (57) hide show
  1. package/AGENTS.md +9 -1
  2. package/dist/adapter.d.ts +20 -3
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/adapter.js.map +1 -1
  5. package/dist/change-set.d.ts +40 -0
  6. package/dist/change-set.d.ts.map +1 -0
  7. package/dist/change-set.js +94 -0
  8. package/dist/change-set.js.map +1 -0
  9. package/dist/client.d.ts +7 -6
  10. package/dist/client.d.ts.map +1 -1
  11. package/dist/client.js +19 -13
  12. package/dist/client.js.map +1 -1
  13. package/dist/collection.d.ts +8 -2
  14. package/dist/collection.d.ts.map +1 -1
  15. package/dist/collection.js +8 -3
  16. package/dist/collection.js.map +1 -1
  17. package/dist/connection.d.ts +4 -2
  18. package/dist/connection.d.ts.map +1 -1
  19. package/dist/connection.js +308 -168
  20. package/dist/connection.js.map +1 -1
  21. package/dist/conversation.d.ts +28 -16
  22. package/dist/conversation.d.ts.map +1 -1
  23. package/dist/conversation.js +371 -269
  24. package/dist/conversation.js.map +1 -1
  25. package/dist/file.d.ts +2 -2
  26. package/dist/file.d.ts.map +1 -1
  27. package/dist/file.js.map +1 -1
  28. package/dist/host.d.ts.map +1 -1
  29. package/dist/host.js +7 -8
  30. package/dist/host.js.map +1 -1
  31. package/dist/index.d.ts +3 -3
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/memory.d.ts +3 -2
  35. package/dist/memory.d.ts.map +1 -1
  36. package/dist/memory.js +24 -20
  37. package/dist/memory.js.map +1 -1
  38. package/dist/overlay.d.ts +2 -0
  39. package/dist/overlay.d.ts.map +1 -1
  40. package/dist/overlay.js +106 -0
  41. package/dist/overlay.js.map +1 -1
  42. package/dist/refresh.d.ts.map +1 -1
  43. package/dist/refresh.js +1 -1
  44. package/dist/refresh.js.map +1 -1
  45. package/dist/replica.d.ts +12 -3
  46. package/dist/replica.d.ts.map +1 -1
  47. package/dist/replica.js +61 -26
  48. package/dist/replica.js.map +1 -1
  49. package/dist/store.d.ts +43 -19
  50. package/dist/store.d.ts.map +1 -1
  51. package/dist/store.js +371 -106
  52. package/dist/store.js.map +1 -1
  53. package/dist/wire.d.ts +2 -2
  54. package/dist/wire.d.ts.map +1 -1
  55. package/dist/wire.js +4 -5
  56. package/dist/wire.js.map +1 -1
  57. package/package.json +1 -1
@@ -2,6 +2,7 @@ import { rawQuery } from './query.js';
2
2
  import { cached, modelTypes, NO_MODEL, sameModel } from './model.js';
3
3
  import { ROOM_CONTRACT_VERSION, VERSION_1_CAPABILITIES } from './adapter.js';
4
4
  import { bootstrapRoom } from './bootstrap.js';
5
+ import { changeSet, mergeChangeSets } from './change-set.js';
5
6
  import { createConversationStore } from './conversation.js';
6
7
  import { MD_MODEL, PRESENTED, validRoot } from './collection.js';
7
8
  import { createLog } from './log.js';
@@ -78,6 +79,7 @@ export function createMdRuntime(options) {
78
79
  let refusal = null;
79
80
  let storageLoss = null;
80
81
  let status = STARTING;
82
+ let shown = STARTING;
81
83
  let discoveryPaused = false;
82
84
  let timer;
83
85
  let closed = false;
@@ -97,12 +99,14 @@ export function createMdRuntime(options) {
97
99
  const draftListeners = new Set();
98
100
  const presentationListeners = new Set();
99
101
  let withdrawConversations;
102
+ let currencyQueued = false;
100
103
  const conversations = createConversationStore({
101
104
  cards: declaredCards,
102
- documents: ()=>visible({}),
105
+ documents: ()=>visible({}).documents,
103
106
  call: (name, input)=>invokeAction(name, input),
104
107
  durably,
105
- path: ()=>options.adapter.conversations,
108
+ stream: ()=>options.adapter.conversations,
109
+ publish: publishChangeSet,
106
110
  closed: ()=>closed
107
111
  });
108
112
  const journal = createLog();
@@ -142,10 +146,6 @@ export function createMdRuntime(options) {
142
146
  });
143
147
  return entries;
144
148
  }
145
- async function readDrafts(opened) {
146
- await hydrate(opened);
147
- return snapshotDrafts();
148
- }
149
149
  function publishDrafts(waitingOnly) {
150
150
  if (closed) return;
151
151
  const value = snapshotDrafts();
@@ -171,13 +171,15 @@ export function createMdRuntime(options) {
171
171
  if (listener.app === app && listener.ready) listener.notify(presentationOf(app));
172
172
  }
173
173
  }
174
- function commitDraft(work) {
174
+ function commitDraft(locator, path, work) {
175
175
  return exclusive(async ()=>{
176
176
  const local = await openLocal();
177
- await hydrate(local);
178
177
  await work(local);
179
- }).then(()=>{
180
- emitDrafts();
178
+ publishChangeSet(changeSet(locator, null, null, {
179
+ drafts: [
180
+ path
181
+ ]
182
+ }));
181
183
  });
182
184
  }
183
185
  function pending() {
@@ -214,19 +216,13 @@ export function createMdRuntime(options) {
214
216
  reset
215
217
  };
216
218
  }
217
- function storedOutcomes(batches) {
218
- const stored = {};
219
- for (const batch of batches){
220
- const outcome = queuedOutcome(outcomes.get(batch.id));
221
- if (outcome.kind !== 'pending') stored[batch.id] = outcome;
222
- }
223
- return stored;
224
- }
225
- async function persistQueue(local, locator, batches) {
226
- await local.writeQueue({
227
- locator,
228
- batches,
229
- outcomes: storedOutcomes(batches)
219
+ async function persistQueue(local, replica, batches, changedOutcomes = []) {
220
+ await local.write({
221
+ next: replica.confirmed.record,
222
+ previous: replica.confirmed.record,
223
+ queue: batches,
224
+ previousQueue: replica.queue,
225
+ outcomes: changedOutcomes
230
226
  });
231
227
  }
232
228
  function listed() {
@@ -270,9 +266,19 @@ export function createMdRuntime(options) {
270
266
  for (const listener of storageLossListeners)listener(storageLoss);
271
267
  }
272
268
  function moveTo(next) {
273
- if (closed || same(status, next)) return;
269
+ if (closed) return;
274
270
  status = next;
275
- for (const listener of statusListeners)listener(status);
271
+ show();
272
+ }
273
+ function show() {
274
+ if (closed) return;
275
+ const next = status.state === 'synced' && !conversations.current() ? {
276
+ ...status,
277
+ state: 'syncing'
278
+ } : status;
279
+ if (same(shown, next)) return;
280
+ shown = next;
281
+ for (const listener of statusListeners)listener(shown);
276
282
  }
277
283
  function visible(request) {
278
284
  if (options.queryBinding === null) {
@@ -308,6 +314,18 @@ export function createMdRuntime(options) {
308
314
  if (subscription.locator === locator) subscription.notify(entries);
309
315
  }
310
316
  }
317
+ function publishChangeSet(set) {
318
+ if (set.rooms.length > 0) emitRooms();
319
+ if (set.documents.length > 0 || set.query) emitVisible();
320
+ for (const locator of set.queues)emitQueue(locator);
321
+ if (set.drafts.length > 0) emitDrafts();
322
+ for (const app of set.presentations)emitPresentation(app);
323
+ conversations.changed(set);
324
+ const cards = declaredCards();
325
+ if (cards !== undefined && set.documents.some((change)=>change.locator === cards.locator)) {
326
+ scheduleCurrency();
327
+ }
328
+ }
311
329
  function stopTimer() {
312
330
  if (timer === undefined) return;
313
331
  clearInterval(timer);
@@ -319,55 +337,76 @@ export function createMdRuntime(options) {
319
337
  void enqueue();
320
338
  }, period);
321
339
  }
322
- function refuse(reason, provider = null) {
323
- if (reason === 'revoked-authorization') {
324
- const revoked = {
325
- status: 'unavailable',
326
- reason: 'revoked'
327
- };
328
- const attachments = [
329
- 'collection',
330
- 'typed-selection'
331
- ];
332
- const keys = new Set(models.keys());
333
- for (const locator of rooms.keys()){
334
- for (const attached of attachments)keys.add(modelKey(locator, attached));
335
- }
336
- for (const key of keys){
337
- invalidModels.add(key);
338
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
339
- models.set(key, revoked);
340
- liveModels.delete(key);
341
- }
342
- for (const [locator, room] of rooms)rooms.set(locator, {
343
- ...room,
344
- model: modelTypes(revoked),
345
- typedSelection: room.typedSelection === null ? null : {
346
- ...room.typedSelection,
347
- model: modelTypes(revoked)
348
- }
349
- });
350
- emitRooms();
351
- void exclusive(async ()=>{
352
- const local = await openLocal();
353
- for (const record of (await local.all()))await local.publish({
354
- ...record,
355
- model: revoked,
356
- selectionModel: revoked
357
- });
358
- }).catch(()=>{
359
- journal.record('model invalidation durable-write failure');
360
- });
361
- }
340
+ async function refuse(reason, provider = null) {
362
341
  stopTimer();
363
342
  discoveryPaused = true;
364
- refusal = {
365
- reason,
366
- reauthorizationOffered: reason === 'revoked-authorization',
367
- refusal: provider
368
- };
369
- journal.record(`connection refused ${reason}`);
370
- emitRefusal();
343
+ try {
344
+ if (reason === 'revoked-authorization') {
345
+ await exclusive(async ()=>{
346
+ const revoked = {
347
+ status: 'unavailable',
348
+ reason: 'revoked'
349
+ };
350
+ try {
351
+ const local = await openLocal();
352
+ await local.revokeModels([
353
+ ...replicas.values()
354
+ ].map((replica)=>({
355
+ record: replica.confirmed.record,
356
+ queue: replica.queue
357
+ })));
358
+ } catch {
359
+ journal.record('model revocation persistence failed');
360
+ }
361
+ const keys = new Set(models.keys());
362
+ for (const locator of rooms.keys()){
363
+ keys.add(modelKey(locator, 'collection'));
364
+ keys.add(modelKey(locator, 'typed-selection'));
365
+ }
366
+ for (const key of keys){
367
+ invalidModels.add(key);
368
+ modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
369
+ models.set(key, revoked);
370
+ liveModels.delete(key);
371
+ }
372
+ const changed = [];
373
+ for (const [locator, room] of rooms){
374
+ const before = replicas.get(locator) ?? null;
375
+ if (before !== null) {
376
+ const record = {
377
+ ...before.confirmed.record,
378
+ model: revoked,
379
+ selectionModel: revoked
380
+ };
381
+ const confirmed = readConfirmed(record, before.confirmed);
382
+ if (confirmed === null) throw new Error(`Cannot retype revoked Room ${locator}`);
383
+ replicas.set(locator, replicaOf(confirmed, before.queue, rewriting(), before));
384
+ }
385
+ rooms.set(locator, {
386
+ ...room,
387
+ model: modelTypes(revoked),
388
+ typedSelection: room.typedSelection === null ? null : {
389
+ ...room.typedSelection,
390
+ model: modelTypes(revoked)
391
+ }
392
+ });
393
+ changed.push(changeSet(locator, before, replicas.get(locator) ?? null, {
394
+ room: true,
395
+ query: true
396
+ }));
397
+ }
398
+ publishChangeSet(mergeChangeSets(changed));
399
+ });
400
+ }
401
+ } finally{
402
+ refusal = {
403
+ reason,
404
+ reauthorizationOffered: reason === 'revoked-authorization',
405
+ refusal: provider
406
+ };
407
+ journal.record(`connection refused ${reason}`);
408
+ emitRefusal();
409
+ }
371
410
  }
372
411
  function list(room, synchronizationAvailable) {
373
412
  const selection = room.typedSelection ?? null;
@@ -417,7 +456,6 @@ export function createMdRuntime(options) {
417
456
  async function dropUnlisted(listed, local) {
418
457
  if (unlistedHeld(listed).length === 0) return;
419
458
  await exclusive(async ()=>{
420
- await hydrate(local);
421
459
  const unlisted = unlistedHeld(listed);
422
460
  await local.dropRooms(unlisted);
423
461
  for (const [batch, locator] of acceptedIn){
@@ -425,6 +463,14 @@ export function createMdRuntime(options) {
425
463
  acceptedIn.delete(batch);
426
464
  outcomes.delete(batch);
427
465
  }
466
+ const removed = unlisted.map((locator)=>changeSet(locator, replicas.get(locator) ?? null, null, {
467
+ room: true,
468
+ queue: true,
469
+ query: true,
470
+ drafts: [
471
+ ...drafts.get(locator)?.keys() ?? []
472
+ ]
473
+ }));
428
474
  for (const locator of unlisted){
429
475
  for (const batch of replicas.get(locator)?.queue ?? [])outcomes.delete(batch.id);
430
476
  replicas.delete(locator);
@@ -444,9 +490,7 @@ export function createMdRuntime(options) {
444
490
  }
445
491
  journal.record(`room unlisted ${locator}`);
446
492
  }
447
- emitVisible();
448
- for (const locator of unlisted)emitQueue(locator);
449
- emitDrafts();
493
+ publishChangeSet(mergeChangeSets(removed));
450
494
  moveTo({
451
495
  ...status,
452
496
  ...attention()
@@ -468,13 +512,14 @@ export function createMdRuntime(options) {
468
512
  return model;
469
513
  }
470
514
  async function hydrate(opened) {
471
- const [records, queues, kept, presented, cached, unsent, lastCompleted, declared, named, listedSlots] = await Promise.all([
515
+ const [records, queues, recordedOutcomes, kept, presented, held, chatDrafts, lastCompleted, declared, named, listedSlots] = await Promise.all([
472
516
  opened.all(),
473
517
  opened.queues(),
518
+ opened.outcomes(),
474
519
  opened.drafts(),
475
520
  opened.presentation(),
476
521
  opened.conversations(),
477
- opened.instructions(),
522
+ opened.chatDrafts(),
478
523
  opened.lastCompleted(),
479
524
  opened.providerLimits(),
480
525
  opened.capabilities(),
@@ -487,27 +532,30 @@ export function createMdRuntime(options) {
487
532
  for (const draft of kept)rememberDraft(draft.locator, draft.path, draft.content);
488
533
  presentations.clear();
489
534
  for (const record of presented)presentations.set(record.app, record.value);
490
- conversations.hydrate(cached, unsent);
535
+ conversations.hydrate(held, chatDrafts);
491
536
  const queuedBatches = new Map(queues.map((queue)=>[
492
537
  queue.locator,
493
538
  queue.batches
494
539
  ]));
495
- for (const queue of queues){
496
- for (const [id, outcome] of Object.entries(queue.outcomes)){
497
- outcomes.set(id, outcome);
498
- }
540
+ outcomes.clear();
541
+ acceptedIn.clear();
542
+ for (const record of recordedOutcomes){
543
+ outcomes.set(record.id, record.outcome);
544
+ if (record.outcome.kind === 'accepted') acceptedIn.set(record.id, record.locator);
499
545
  }
546
+ const previousReplicas = new Map(replicas);
500
547
  replicas.clear();
501
548
  for (const record of records){
502
549
  const model = restored(modelKey(record.locator, 'collection'), record.model);
503
550
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
551
+ const prior = previousReplicas.get(record.locator);
504
552
  const confirmed = readConfirmed({
505
553
  ...record,
506
554
  model,
507
555
  selectionModel
508
- });
556
+ }, prior?.confirmed);
509
557
  if (confirmed === null) continue;
510
- replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], rewriting()));
558
+ replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], rewriting(), prior));
511
559
  const existing = rooms.get(record.locator);
512
560
  if (existing === undefined) list(record, false);
513
561
  else {
@@ -568,7 +616,6 @@ export function createMdRuntime(options) {
568
616
  function durably(work) {
569
617
  return exclusive(async ()=>{
570
618
  const local = await openLocal();
571
- await hydrate(local);
572
619
  return work(local);
573
620
  });
574
621
  }
@@ -586,25 +633,30 @@ export function createMdRuntime(options) {
586
633
  }
587
634
  function publish(local, room, confirmed) {
588
635
  return exclusive(async ()=>{
589
- await hydrate(local);
636
+ const prior = replicas.get(room.locator);
590
637
  const retained = readConfirmed({
591
638
  ...confirmed.record,
592
639
  model: modelOf(room.locator),
593
640
  selectionModel: modelOf(room.locator, 'typed-selection')
594
- });
641
+ }, prior?.confirmed);
595
642
  if (retained !== null) {
596
- await local.publish(retained.record);
643
+ await local.write({
644
+ next: retained.record,
645
+ previous: prior?.confirmed.record ?? null,
646
+ queue: prior?.queue ?? []
647
+ });
597
648
  unreadable.delete(room.locator);
598
- replicas.set(room.locator, replicaOf(retained, replicas.get(room.locator)?.queue ?? [], rewriting()));
649
+ replicas.set(room.locator, replicaOf(retained, prior?.queue ?? [], rewriting(), prior));
599
650
  }
600
651
  list(room, true);
601
- emitRooms();
602
- emitVisible();
652
+ publishChangeSet(changeSet(room.locator, prior ?? null, replicas.get(room.locator) ?? null, {
653
+ room: true,
654
+ query: true
655
+ }));
603
656
  });
604
657
  }
605
658
  async function write(locator, changes, replacing, writtenBy) {
606
659
  const local = await openLocal();
607
- await hydrate(local);
608
660
  const replica = replicas.get(locator);
609
661
  const room = rooms.get(locator);
610
662
  if (replica === undefined || room === undefined) {
@@ -650,11 +702,19 @@ export function createMdRuntime(options) {
650
702
  ...new Set(changes.map((change)=>change.path))
651
703
  ];
652
704
  try {
653
- await local.append({
654
- locator,
655
- batches,
656
- outcomes: storedOutcomes(batches)
657
- }, consumed);
705
+ await local.write({
706
+ next: replica.confirmed.record,
707
+ previous: replica.confirmed.record,
708
+ queue: batches,
709
+ previousQueue: replica.queue,
710
+ outcomes: replacing === undefined ? [] : [
711
+ {
712
+ id: replacing,
713
+ outcome: null
714
+ }
715
+ ],
716
+ consumedDrafts: consumed
717
+ });
658
718
  } catch {
659
719
  return {
660
720
  outcome: 'refused',
@@ -663,10 +723,11 @@ export function createMdRuntime(options) {
663
723
  }
664
724
  if (replacing !== undefined) outcomes.delete(replacing);
665
725
  for (const path of consumed)forgetDraft(locator, path);
666
- replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting()));
667
- emitQueue(locator);
668
- emitVisible();
669
- emitDrafts();
726
+ replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting(), replica));
727
+ publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
728
+ queue: true,
729
+ drafts: consumed
730
+ }));
670
731
  moveTo({
671
732
  ...saving(status),
672
733
  ...attention()
@@ -679,18 +740,23 @@ export function createMdRuntime(options) {
679
740
  }
680
741
  async function discardHead(locator, identifier) {
681
742
  const local = await openLocal();
682
- await hydrate(local);
683
743
  const replica = replicas.get(locator);
684
744
  const head = replica?.queue[0];
685
745
  if (replica === undefined || head === undefined || head.id !== identifier || !refused(outcomes.get(head.id))) {
686
746
  return;
687
747
  }
688
748
  const batches = replica.queue.slice(1);
749
+ await persistQueue(local, replica, batches, [
750
+ {
751
+ id: identifier,
752
+ outcome: null
753
+ }
754
+ ]);
689
755
  outcomes.delete(identifier);
690
- await persistQueue(local, locator, batches);
691
- replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting()));
692
- emitQueue(locator);
693
- emitVisible();
756
+ replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting(), replica));
757
+ publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
758
+ queue: true
759
+ }));
694
760
  moveTo({
695
761
  ...status,
696
762
  state: pending() ? 'pending' : status.state,
@@ -739,7 +805,7 @@ export function createMdRuntime(options) {
739
805
  const room = replicas.get(result.locator)?.confirmed.record;
740
806
  if (room !== undefined) await scoped(result, room, await openLocal());
741
807
  }
742
- if (result.kind === 'revoked-authorization') refuse('revoked-authorization');
808
+ if (result.kind === 'revoked-authorization') await refuse('revoked-authorization');
743
809
  return result;
744
810
  } catch {
745
811
  journal.record('outcome history transport-uncertainty');
@@ -792,7 +858,7 @@ export function createMdRuntime(options) {
792
858
  const room = replicas.get(result.locator)?.confirmed.record;
793
859
  if (room !== undefined) await scoped(result, room, await openLocal());
794
860
  }
795
- if (result.kind === 'revoked-authorization') refuse('revoked-authorization');
861
+ if (result.kind === 'revoked-authorization') await refuse('revoked-authorization');
796
862
  journal.record(`outcome action ${result.kind}`);
797
863
  if (result.kind === 'rejected') journal.record(`refusal rejected ${result.code}`);
798
864
  return result;
@@ -885,7 +951,6 @@ export function createMdRuntime(options) {
885
951
  };
886
952
  }
887
953
  async function commitAccept(room, local, submitted, accepted, base, overlay) {
888
- await hydrate(local);
889
954
  const current = replicas.get(room.locator);
890
955
  if (current === undefined || !current.queue.some((batch)=>batch.id === submitted.id)) {
891
956
  return true;
@@ -895,14 +960,11 @@ export function createMdRuntime(options) {
895
960
  ...base.record,
896
961
  model: modelOf(room.locator),
897
962
  selectionModel: modelOf(room.locator, 'typed-selection')
898
- });
963
+ }, base);
899
964
  if (retainedBase === null) return false;
900
965
  const applied = overlay ? applyReturned(retainedBase, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion, options.modelBinding) : retainedBase;
901
966
  if (applied === null) return false;
902
- await local.publish(applied.record);
903
- await persistQueue(local, room.locator, remaining);
904
- acceptedIn.set(submitted.id, room.locator);
905
- outcomes.set(submitted.id, {
967
+ const acceptedOutcome = {
906
968
  kind: 'accepted',
907
969
  roomVersion: accepted.roomVersion,
908
970
  ...accepted.author === undefined ? {} : {
@@ -911,13 +973,29 @@ export function createMdRuntime(options) {
911
973
  ...accepted.time === undefined ? {} : {
912
974
  time: accepted.time
913
975
  }
976
+ };
977
+ await local.write({
978
+ next: applied.record,
979
+ previous: current.confirmed.record,
980
+ queue: remaining,
981
+ previousQueue: current.queue,
982
+ outcomes: [
983
+ {
984
+ id: submitted.id,
985
+ outcome: acceptedOutcome
986
+ }
987
+ ]
914
988
  });
989
+ acceptedIn.set(submitted.id, room.locator);
990
+ outcomes.set(submitted.id, acceptedOutcome);
915
991
  unreadable.delete(room.locator);
916
- replicas.set(room.locator, replicaOf(applied, remaining, rewriting()));
992
+ replicas.set(room.locator, replicaOf(applied, remaining, rewriting(), current));
917
993
  list(room, true);
918
- emitRooms();
919
- emitQueue(room.locator);
920
- emitVisible();
994
+ publishChangeSet(changeSet(room.locator, current, replicas.get(room.locator) ?? null, {
995
+ room: true,
996
+ queue: true,
997
+ query: true
998
+ }));
921
999
  journal.record(`outcome ${room.locator} ${submitted.id} accepted ${accepted.roomVersion}` + (accepted.author === undefined ? '' : ` author ${accepted.author}`) + (accepted.time === undefined ? '' : ` time ${accepted.time}`));
922
1000
  return true;
923
1001
  }
@@ -934,12 +1012,18 @@ export function createMdRuntime(options) {
934
1012
  return null;
935
1013
  }
936
1014
  await exclusive(async ()=>{
937
- await hydrate(local);
938
1015
  const current = replicas.get(room.locator);
939
1016
  if (current === undefined || current.queue[0]?.id !== head.id) return;
1017
+ await persistQueue(local, current, current.queue, [
1018
+ {
1019
+ id: head.id,
1020
+ outcome: null
1021
+ }
1022
+ ]);
940
1023
  outcomes.delete(head.id);
941
- await persistQueue(local, room.locator, current.queue);
942
- emitQueue(room.locator);
1024
+ publishChangeSet(changeSet(room.locator, current, current, {
1025
+ queue: true
1026
+ }));
943
1027
  moveTo({
944
1028
  ...status,
945
1029
  ...attention()
@@ -948,6 +1032,11 @@ export function createMdRuntime(options) {
948
1032
  }
949
1033
  let result;
950
1034
  journal.record(`submission ${room.locator} ${head.id}`);
1035
+ if (await local.markSubmitted(room.locator, head.id)) {
1036
+ publishChangeSet(changeSet(room.locator, replica, replica, {
1037
+ queue: true
1038
+ }));
1039
+ }
951
1040
  try {
952
1041
  result = await options.adapter.mutate(authorized, {
953
1042
  locator: room.locator,
@@ -972,7 +1061,7 @@ export function createMdRuntime(options) {
972
1061
  return null;
973
1062
  }
974
1063
  if (result.kind === 'revoked-authorization') {
975
- refuse('revoked-authorization');
1064
+ await refuse('revoked-authorization');
976
1065
  return 'stop';
977
1066
  }
978
1067
  if (result.kind === 'scoped-refusal') {
@@ -985,14 +1074,21 @@ export function createMdRuntime(options) {
985
1074
  if (result.kind !== 'expired') journal.record(`refusal ${result.kind} ${result.code}`);
986
1075
  if (result.kind === 'retryable-refusal') progress.complete = false;
987
1076
  await exclusive(async ()=>{
988
- await hydrate(local);
989
1077
  const current = replicas.get(room.locator);
990
1078
  if (current === undefined || !current.queue.some((batch)=>batch.id === head.id)) {
991
1079
  return;
992
1080
  }
993
- outcomes.set(head.id, recordRefusal(result));
994
- await persistQueue(local, room.locator, current.queue);
995
- emitQueue(room.locator);
1081
+ const recorded = recordRefusal(result);
1082
+ await persistQueue(local, current, current.queue, [
1083
+ {
1084
+ id: head.id,
1085
+ outcome: recorded
1086
+ }
1087
+ ]);
1088
+ outcomes.set(head.id, recorded);
1089
+ publishChangeSet(changeSet(room.locator, current, current, {
1090
+ queue: true
1091
+ }));
996
1092
  moveTo({
997
1093
  ...status,
998
1094
  ...attention()
@@ -1004,7 +1100,7 @@ export function createMdRuntime(options) {
1004
1100
  if (from === undefined) return null;
1005
1101
  const pulled = await pullRefresh(authorized, from);
1006
1102
  if (pulled.kind === 'revoked-authorization') {
1007
- refuse('revoked-authorization');
1103
+ await refuse('revoked-authorization');
1008
1104
  return 'stop';
1009
1105
  }
1010
1106
  if (pulled.kind === 'scoped-refusal') {
@@ -1032,7 +1128,7 @@ export function createMdRuntime(options) {
1032
1128
  if (pulled.kind === 'beyond-horizon') {
1033
1129
  const bootstrapped = await pullBootstrap(authorized, room);
1034
1130
  if (bootstrapped.kind === 'revoked-authorization') {
1035
- refuse('revoked-authorization');
1131
+ await refuse('revoked-authorization');
1036
1132
  return 'stop';
1037
1133
  }
1038
1134
  if (bootstrapped.kind === 'scoped-refusal') {
@@ -1084,7 +1180,6 @@ export function createMdRuntime(options) {
1084
1180
  liveModels.delete(key);
1085
1181
  }
1086
1182
  await exclusive(async ()=>{
1087
- await hydrate(local);
1088
1183
  if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0)) return;
1089
1184
  const held = replicas.get(room.locator);
1090
1185
  if (held !== undefined) {
@@ -1100,9 +1195,15 @@ export function createMdRuntime(options) {
1100
1195
  selectionModel: next
1101
1196
  }
1102
1197
  };
1103
- await local.publish(record);
1104
- const confirmed = readConfirmed(record);
1105
- if (confirmed !== null) replicas.set(room.locator, replicaOf(confirmed, held.queue, rewriting()));
1198
+ await local.write({
1199
+ next: record,
1200
+ previous: held.confirmed.record,
1201
+ queue: held.queue
1202
+ });
1203
+ const confirmed = readConfirmed(record, held.confirmed);
1204
+ if (confirmed !== null) {
1205
+ replicas.set(room.locator, replicaOf(confirmed, held.queue, rewriting(), held));
1206
+ }
1106
1207
  }
1107
1208
  models.set(key, next);
1108
1209
  if (next.status === 'current') {
@@ -1110,8 +1211,11 @@ export function createMdRuntime(options) {
1110
1211
  invalidModels.delete(key);
1111
1212
  }
1112
1213
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
1113
- emitRooms();
1114
- if (queryChanged && held !== undefined) emitVisible();
1214
+ if (held === undefined) emitRooms();
1215
+ else publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, {
1216
+ room: true,
1217
+ query: queryChanged
1218
+ }));
1115
1219
  });
1116
1220
  }
1117
1221
  async function acquireModel(local, room, authorized, selection) {
@@ -1147,11 +1251,7 @@ export function createMdRuntime(options) {
1147
1251
  }
1148
1252
  if (epoch !== (modelEpoch.get(key) ?? 0)) return;
1149
1253
  if (answer.kind === 'revoked-authorization') {
1150
- await retainModel(local, room, attached, {
1151
- status: 'unavailable',
1152
- reason: 'revoked'
1153
- });
1154
- refuse('revoked-authorization');
1254
+ await refuse('revoked-authorization');
1155
1255
  return;
1156
1256
  }
1157
1257
  if (answer.kind === 'transport-uncertainty') {
@@ -1271,7 +1371,7 @@ export function createMdRuntime(options) {
1271
1371
  continue;
1272
1372
  }
1273
1373
  if (withheld.kind === 'revoked-authorization') {
1274
- refuse('revoked-authorization');
1374
+ await refuse('revoked-authorization');
1275
1375
  return;
1276
1376
  }
1277
1377
  continue;
@@ -1311,7 +1411,7 @@ export function createMdRuntime(options) {
1311
1411
  }
1312
1412
  if (result.kind === 'contract-unavailable') {
1313
1413
  journal.record(`connection contract unavailable ${result.code}`);
1314
- refuse('missing-required-guarantee', {
1414
+ await refuse('missing-required-guarantee', {
1315
1415
  code: result.code,
1316
1416
  fields: result.fields
1317
1417
  });
@@ -1319,17 +1419,17 @@ export function createMdRuntime(options) {
1319
1419
  return;
1320
1420
  }
1321
1421
  if (result.kind === 'revoked-authorization') {
1322
- refuse('revoked-authorization');
1422
+ await refuse('revoked-authorization');
1323
1423
  notifyReady();
1324
1424
  return;
1325
1425
  }
1326
1426
  if (!result.supportedVersions.includes(ROOM_CONTRACT_VERSION)) {
1327
- refuse('no-matching-version');
1427
+ await refuse('no-matching-version');
1328
1428
  notifyReady();
1329
1429
  return;
1330
1430
  }
1331
1431
  if (result.mdModel.identity !== MD_MODEL.identity || result.mdModel.version !== MD_MODEL.version) {
1332
- refuse('md-model-version-mismatch');
1432
+ await refuse('md-model-version-mismatch');
1333
1433
  notifyReady();
1334
1434
  return;
1335
1435
  }
@@ -1347,13 +1447,13 @@ export function createMdRuntime(options) {
1347
1447
  'unavailable'
1348
1448
  ].includes(room.replica.admission) || !declaredIdentity(room.attachedModel)) {
1349
1449
  journal.record(`invalid model or replica declaration ${room.locator}`);
1350
- refuse('missing-required-guarantee');
1450
+ await refuse('missing-required-guarantee');
1351
1451
  notifyReady();
1352
1452
  return;
1353
1453
  }
1354
1454
  if (!declaredSelection(room)) {
1355
1455
  journal.record(`invalid typed selection declaration ${room.locator}`);
1356
- refuse('missing-required-guarantee');
1456
+ await refuse('missing-required-guarantee');
1357
1457
  notifyReady();
1358
1458
  return;
1359
1459
  }
@@ -1363,14 +1463,14 @@ export function createMdRuntime(options) {
1363
1463
  }
1364
1464
  }
1365
1465
  if (mismatchedRoots.size > 0) {
1366
- refuse('missing-required-guarantee');
1466
+ await refuse('missing-required-guarantee');
1367
1467
  notifyReady();
1368
1468
  return;
1369
1469
  }
1370
1470
  const declared = new Set(result.capabilities.map((capability)=>capability.name));
1371
1471
  for (const required of options.requiredGuarantees){
1372
1472
  if (!knownCapabilities.has(required) || !declared.has(required)) {
1373
- refuse('missing-required-guarantee');
1473
+ await refuse('missing-required-guarantee');
1374
1474
  notifyReady();
1375
1475
  return;
1376
1476
  }
@@ -1394,6 +1494,10 @@ export function createMdRuntime(options) {
1394
1494
  handSelected(result.rooms.filter((room)=>room.replica.admission === 'admitted').map((room)=>room.locator).sort());
1395
1495
  progress.complete = true;
1396
1496
  await synchronize(result.rooms, local, progress);
1497
+ if (closed || discoveryPaused) return;
1498
+ await conversations.reconcile();
1499
+ if (status.providerUnreachable) conversations.reconnected();
1500
+ await conversations.catchUp(progress);
1397
1501
  }
1398
1502
  async function ensureAuthorization() {
1399
1503
  if (authorization !== undefined) return;
@@ -1457,12 +1561,44 @@ export function createMdRuntime(options) {
1457
1561
  });
1458
1562
  }
1459
1563
  function observeConversations() {
1460
- const path = options.adapter.conversations;
1461
- if (path === undefined || withdrawConversations !== undefined) return;
1462
- withdrawConversations = path.observe((event)=>{
1564
+ const stream = options.adapter.conversations;
1565
+ if (stream === undefined || withdrawConversations !== undefined) return;
1566
+ withdrawConversations = stream.observe((event)=>{
1463
1567
  conversations.receive(event);
1568
+ if (event.kind !== 'gap' || conversations.current()) return;
1569
+ show();
1570
+ scheduleCurrency();
1464
1571
  });
1465
1572
  }
1573
+ function scheduleCurrency() {
1574
+ if (currencyQueued || closed) return;
1575
+ currencyQueued = true;
1576
+ chain = chain.then(runCurrency, runCurrency);
1577
+ }
1578
+ async function runCurrency() {
1579
+ currencyQueued = false;
1580
+ if (closed || discoveryPaused) return;
1581
+ await conversations.reconcile();
1582
+ if (conversations.current()) {
1583
+ show();
1584
+ return;
1585
+ }
1586
+ const resting = status;
1587
+ const progress = {
1588
+ complete: false,
1589
+ reachable: !resting.providerUnreachable
1590
+ };
1591
+ moveTo(transferring(resting));
1592
+ try {
1593
+ await conversations.catchUp(progress);
1594
+ } catch {
1595
+ progress.reachable = false;
1596
+ } finally{
1597
+ if (!closed) {
1598
+ moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1599
+ }
1600
+ }
1601
+ }
1466
1602
  function handSelected(locators) {
1467
1603
  const path = options.adapter.roomVersions;
1468
1604
  if (path === undefined || withdrawNotifications === undefined) return;
@@ -1508,7 +1644,7 @@ export function createMdRuntime(options) {
1508
1644
  continue;
1509
1645
  }
1510
1646
  if (withheld.kind === 'revoked-authorization') {
1511
- refuse('revoked-authorization');
1647
+ await refuse('revoked-authorization');
1512
1648
  return;
1513
1649
  }
1514
1650
  }
@@ -1559,6 +1695,9 @@ export function createMdRuntime(options) {
1559
1695
  });
1560
1696
  void enqueue();
1561
1697
  return {
1698
+ loaded () {
1699
+ return openLocal().then(()=>undefined);
1700
+ },
1562
1701
  rooms () {
1563
1702
  return listed();
1564
1703
  },
@@ -1575,13 +1714,13 @@ export function createMdRuntime(options) {
1575
1714
  };
1576
1715
  },
1577
1716
  syncStatus () {
1578
- return status;
1717
+ return shown;
1579
1718
  },
1580
1719
  draft: {
1581
1720
  list () {
1582
1721
  return exclusive(async ()=>{
1583
- const local = await openLocal();
1584
- return readDrafts(local);
1722
+ await openLocal();
1723
+ return snapshotDrafts();
1585
1724
  }).then((entries)=>{
1586
1725
  releaseWaitingDrafts();
1587
1726
  return entries;
@@ -1592,7 +1731,7 @@ export function createMdRuntime(options) {
1592
1731
  return content === undefined ? null : structuredClone(content);
1593
1732
  },
1594
1733
  replace (locator, path, content) {
1595
- return commitDraft(async (local)=>{
1734
+ return commitDraft(locator, path, async (local)=>{
1596
1735
  const stored = structuredClone(content);
1597
1736
  await local.keep({
1598
1737
  locator,
@@ -1603,32 +1742,33 @@ export function createMdRuntime(options) {
1603
1742
  });
1604
1743
  },
1605
1744
  discard (locator, path) {
1606
- return commitDraft(async (local)=>{
1745
+ return commitDraft(locator, path, async (local)=>{
1607
1746
  await local.discard(locator, path);
1608
1747
  forgetDraft(locator, path);
1609
1748
  });
1610
1749
  }
1611
1750
  },
1612
1751
  conversation: conversations.surface,
1752
+ chatDraft: conversations.chatDraft,
1613
1753
  presentation: {
1614
1754
  read (app) {
1615
1755
  return exclusive(async ()=>{
1616
- await hydrate(await openLocal());
1756
+ await openLocal();
1617
1757
  return presentationOf(app);
1618
1758
  });
1619
1759
  },
1620
1760
  write (app, value) {
1621
1761
  return exclusive(async ()=>{
1622
1762
  const local = await openLocal();
1623
- await hydrate(local);
1624
1763
  const stored = structuredClone(value);
1625
1764
  await local.present({
1626
1765
  app,
1627
1766
  value: stored
1628
1767
  });
1629
1768
  presentations.set(app, stored);
1630
- }).then(()=>{
1631
- emitPresentation(app);
1769
+ publishChangeSet(changeSet('', null, null, {
1770
+ presentation: app
1771
+ }));
1632
1772
  });
1633
1773
  }
1634
1774
  },
@@ -1665,7 +1805,7 @@ export function createMdRuntime(options) {
1665
1805
  persistence () {
1666
1806
  return options.shell.persistentStorage;
1667
1807
  },
1668
- observe (topic, second, third) {
1808
+ observe (topic, second, third, fourth) {
1669
1809
  if (topic === 'drafts') {
1670
1810
  const notify = second;
1671
1811
  const listener = {
@@ -1674,8 +1814,7 @@ export function createMdRuntime(options) {
1674
1814
  };
1675
1815
  draftListeners.add(listener);
1676
1816
  void exclusive(async ()=>{
1677
- const local = await openLocal();
1678
- await readDrafts(local);
1817
+ await openLocal();
1679
1818
  if (closed || !draftListeners.has(listener)) return;
1680
1819
  listener.ready = true;
1681
1820
  }).then(()=>{
@@ -1687,7 +1826,7 @@ export function createMdRuntime(options) {
1687
1826
  };
1688
1827
  }
1689
1828
  if (topic === 'conversation') {
1690
- return conversations.observe(second, third);
1829
+ return conversations.observe(second, third, fourth);
1691
1830
  }
1692
1831
  if (topic === 'presentation') {
1693
1832
  const listener = {
@@ -1697,7 +1836,7 @@ export function createMdRuntime(options) {
1697
1836
  };
1698
1837
  presentationListeners.add(listener);
1699
1838
  void exclusive(async ()=>{
1700
- await hydrate(await openLocal());
1839
+ await openLocal();
1701
1840
  }).then(()=>{
1702
1841
  if (closed || !presentationListeners.has(listener)) return;
1703
1842
  listener.ready = true;
@@ -1734,7 +1873,7 @@ export function createMdRuntime(options) {
1734
1873
  if (topic === 'sync-status') {
1735
1874
  const notify = second;
1736
1875
  statusListeners.add(notify);
1737
- notify(status);
1876
+ notify(shown);
1738
1877
  return ()=>{
1739
1878
  statusListeners.delete(notify);
1740
1879
  };
@@ -1764,6 +1903,7 @@ export function createMdRuntime(options) {
1764
1903
  async reauthorize () {
1765
1904
  if (refusal === null || !refusal.reauthorizationOffered) return;
1766
1905
  journal.record('connection reauthorize');
1906
+ await mutations;
1767
1907
  authorization = undefined;
1768
1908
  discoveryPaused = false;
1769
1909
  await enqueue();