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

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.
@@ -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';
@@ -142,10 +143,6 @@ export function createMdRuntime(options) {
142
143
  });
143
144
  return entries;
144
145
  }
145
- async function readDrafts(opened) {
146
- await hydrate(opened);
147
- return snapshotDrafts();
148
- }
149
146
  function publishDrafts(waitingOnly) {
150
147
  if (closed) return;
151
148
  const value = snapshotDrafts();
@@ -171,13 +168,15 @@ export function createMdRuntime(options) {
171
168
  if (listener.app === app && listener.ready) listener.notify(presentationOf(app));
172
169
  }
173
170
  }
174
- function commitDraft(work) {
171
+ function commitDraft(locator, path, work) {
175
172
  return exclusive(async ()=>{
176
173
  const local = await openLocal();
177
- await hydrate(local);
178
174
  await work(local);
179
- }).then(()=>{
180
- emitDrafts();
175
+ publishChangeSet(changeSet(locator, null, null, {
176
+ drafts: [
177
+ path
178
+ ]
179
+ }));
181
180
  });
182
181
  }
183
182
  function pending() {
@@ -214,19 +213,13 @@ export function createMdRuntime(options) {
214
213
  reset
215
214
  };
216
215
  }
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)
216
+ async function persistQueue(local, replica, batches, changedOutcomes = []) {
217
+ await local.write({
218
+ next: replica.confirmed.record,
219
+ previous: replica.confirmed.record,
220
+ queue: batches,
221
+ previousQueue: replica.queue,
222
+ outcomes: changedOutcomes
230
223
  });
231
224
  }
232
225
  function listed() {
@@ -308,6 +301,13 @@ export function createMdRuntime(options) {
308
301
  if (subscription.locator === locator) subscription.notify(entries);
309
302
  }
310
303
  }
304
+ function publishChangeSet(set) {
305
+ if (set.rooms.length > 0) emitRooms();
306
+ if (set.documents.length > 0 || set.query) emitVisible();
307
+ for (const locator of set.queues)emitQueue(locator);
308
+ if (set.drafts.length > 0) emitDrafts();
309
+ for (const app of set.presentations)emitPresentation(app);
310
+ }
311
311
  function stopTimer() {
312
312
  if (timer === undefined) return;
313
313
  clearInterval(timer);
@@ -319,55 +319,76 @@ export function createMdRuntime(options) {
319
319
  void enqueue();
320
320
  }, period);
321
321
  }
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
- }
322
+ async function refuse(reason, provider = null) {
362
323
  stopTimer();
363
324
  discoveryPaused = true;
364
- refusal = {
365
- reason,
366
- reauthorizationOffered: reason === 'revoked-authorization',
367
- refusal: provider
368
- };
369
- journal.record(`connection refused ${reason}`);
370
- emitRefusal();
325
+ try {
326
+ if (reason === 'revoked-authorization') {
327
+ await exclusive(async ()=>{
328
+ const revoked = {
329
+ status: 'unavailable',
330
+ reason: 'revoked'
331
+ };
332
+ try {
333
+ const local = await openLocal();
334
+ await local.revokeModels([
335
+ ...replicas.values()
336
+ ].map((replica)=>({
337
+ record: replica.confirmed.record,
338
+ queue: replica.queue
339
+ })));
340
+ } catch {
341
+ journal.record('model revocation persistence failed');
342
+ }
343
+ const keys = new Set(models.keys());
344
+ for (const locator of rooms.keys()){
345
+ keys.add(modelKey(locator, 'collection'));
346
+ keys.add(modelKey(locator, 'typed-selection'));
347
+ }
348
+ for (const key of keys){
349
+ invalidModels.add(key);
350
+ modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
351
+ models.set(key, revoked);
352
+ liveModels.delete(key);
353
+ }
354
+ const changed = [];
355
+ for (const [locator, room] of rooms){
356
+ const before = replicas.get(locator) ?? null;
357
+ if (before !== null) {
358
+ const record = {
359
+ ...before.confirmed.record,
360
+ model: revoked,
361
+ selectionModel: revoked
362
+ };
363
+ const confirmed = readConfirmed(record, before.confirmed);
364
+ if (confirmed === null) throw new Error(`Cannot retype revoked Room ${locator}`);
365
+ replicas.set(locator, replicaOf(confirmed, before.queue, rewriting(), before));
366
+ }
367
+ rooms.set(locator, {
368
+ ...room,
369
+ model: modelTypes(revoked),
370
+ typedSelection: room.typedSelection === null ? null : {
371
+ ...room.typedSelection,
372
+ model: modelTypes(revoked)
373
+ }
374
+ });
375
+ changed.push(changeSet(locator, before, replicas.get(locator) ?? null, {
376
+ room: true,
377
+ query: true
378
+ }));
379
+ }
380
+ publishChangeSet(mergeChangeSets(changed));
381
+ });
382
+ }
383
+ } finally{
384
+ refusal = {
385
+ reason,
386
+ reauthorizationOffered: reason === 'revoked-authorization',
387
+ refusal: provider
388
+ };
389
+ journal.record(`connection refused ${reason}`);
390
+ emitRefusal();
391
+ }
371
392
  }
372
393
  function list(room, synchronizationAvailable) {
373
394
  const selection = room.typedSelection ?? null;
@@ -417,7 +438,6 @@ export function createMdRuntime(options) {
417
438
  async function dropUnlisted(listed, local) {
418
439
  if (unlistedHeld(listed).length === 0) return;
419
440
  await exclusive(async ()=>{
420
- await hydrate(local);
421
441
  const unlisted = unlistedHeld(listed);
422
442
  await local.dropRooms(unlisted);
423
443
  for (const [batch, locator] of acceptedIn){
@@ -425,6 +445,14 @@ export function createMdRuntime(options) {
425
445
  acceptedIn.delete(batch);
426
446
  outcomes.delete(batch);
427
447
  }
448
+ const removed = unlisted.map((locator)=>changeSet(locator, replicas.get(locator) ?? null, null, {
449
+ room: true,
450
+ queue: true,
451
+ query: true,
452
+ drafts: [
453
+ ...drafts.get(locator)?.keys() ?? []
454
+ ]
455
+ }));
428
456
  for (const locator of unlisted){
429
457
  for (const batch of replicas.get(locator)?.queue ?? [])outcomes.delete(batch.id);
430
458
  replicas.delete(locator);
@@ -444,9 +472,7 @@ export function createMdRuntime(options) {
444
472
  }
445
473
  journal.record(`room unlisted ${locator}`);
446
474
  }
447
- emitVisible();
448
- for (const locator of unlisted)emitQueue(locator);
449
- emitDrafts();
475
+ publishChangeSet(mergeChangeSets(removed));
450
476
  moveTo({
451
477
  ...status,
452
478
  ...attention()
@@ -468,9 +494,10 @@ export function createMdRuntime(options) {
468
494
  return model;
469
495
  }
470
496
  async function hydrate(opened) {
471
- const [records, queues, kept, presented, cached, unsent, lastCompleted, declared, named, listedSlots] = await Promise.all([
497
+ const [records, queues, recordedOutcomes, kept, presented, cached, unsent, lastCompleted, declared, named, listedSlots] = await Promise.all([
472
498
  opened.all(),
473
499
  opened.queues(),
500
+ opened.outcomes(),
474
501
  opened.drafts(),
475
502
  opened.presentation(),
476
503
  opened.conversations(),
@@ -492,22 +519,25 @@ export function createMdRuntime(options) {
492
519
  queue.locator,
493
520
  queue.batches
494
521
  ]));
495
- for (const queue of queues){
496
- for (const [id, outcome] of Object.entries(queue.outcomes)){
497
- outcomes.set(id, outcome);
498
- }
522
+ outcomes.clear();
523
+ acceptedIn.clear();
524
+ for (const record of recordedOutcomes){
525
+ outcomes.set(record.id, record.outcome);
526
+ if (record.outcome.kind === 'accepted') acceptedIn.set(record.id, record.locator);
499
527
  }
528
+ const previousReplicas = new Map(replicas);
500
529
  replicas.clear();
501
530
  for (const record of records){
502
531
  const model = restored(modelKey(record.locator, 'collection'), record.model);
503
532
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
533
+ const prior = previousReplicas.get(record.locator);
504
534
  const confirmed = readConfirmed({
505
535
  ...record,
506
536
  model,
507
537
  selectionModel
508
- });
538
+ }, prior?.confirmed);
509
539
  if (confirmed === null) continue;
510
- replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], rewriting()));
540
+ replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], rewriting(), prior));
511
541
  const existing = rooms.get(record.locator);
512
542
  if (existing === undefined) list(record, false);
513
543
  else {
@@ -568,7 +598,6 @@ export function createMdRuntime(options) {
568
598
  function durably(work) {
569
599
  return exclusive(async ()=>{
570
600
  const local = await openLocal();
571
- await hydrate(local);
572
601
  return work(local);
573
602
  });
574
603
  }
@@ -586,25 +615,30 @@ export function createMdRuntime(options) {
586
615
  }
587
616
  function publish(local, room, confirmed) {
588
617
  return exclusive(async ()=>{
589
- await hydrate(local);
618
+ const prior = replicas.get(room.locator);
590
619
  const retained = readConfirmed({
591
620
  ...confirmed.record,
592
621
  model: modelOf(room.locator),
593
622
  selectionModel: modelOf(room.locator, 'typed-selection')
594
- });
623
+ }, prior?.confirmed);
595
624
  if (retained !== null) {
596
- await local.publish(retained.record);
625
+ await local.write({
626
+ next: retained.record,
627
+ previous: prior?.confirmed.record ?? null,
628
+ queue: prior?.queue ?? []
629
+ });
597
630
  unreadable.delete(room.locator);
598
- replicas.set(room.locator, replicaOf(retained, replicas.get(room.locator)?.queue ?? [], rewriting()));
631
+ replicas.set(room.locator, replicaOf(retained, prior?.queue ?? [], rewriting(), prior));
599
632
  }
600
633
  list(room, true);
601
- emitRooms();
602
- emitVisible();
634
+ publishChangeSet(changeSet(room.locator, prior ?? null, replicas.get(room.locator) ?? null, {
635
+ room: true,
636
+ query: true
637
+ }));
603
638
  });
604
639
  }
605
640
  async function write(locator, changes, replacing, writtenBy) {
606
641
  const local = await openLocal();
607
- await hydrate(local);
608
642
  const replica = replicas.get(locator);
609
643
  const room = rooms.get(locator);
610
644
  if (replica === undefined || room === undefined) {
@@ -650,11 +684,19 @@ export function createMdRuntime(options) {
650
684
  ...new Set(changes.map((change)=>change.path))
651
685
  ];
652
686
  try {
653
- await local.append({
654
- locator,
655
- batches,
656
- outcomes: storedOutcomes(batches)
657
- }, consumed);
687
+ await local.write({
688
+ next: replica.confirmed.record,
689
+ previous: replica.confirmed.record,
690
+ queue: batches,
691
+ previousQueue: replica.queue,
692
+ outcomes: replacing === undefined ? [] : [
693
+ {
694
+ id: replacing,
695
+ outcome: null
696
+ }
697
+ ],
698
+ consumedDrafts: consumed
699
+ });
658
700
  } catch {
659
701
  return {
660
702
  outcome: 'refused',
@@ -663,10 +705,11 @@ export function createMdRuntime(options) {
663
705
  }
664
706
  if (replacing !== undefined) outcomes.delete(replacing);
665
707
  for (const path of consumed)forgetDraft(locator, path);
666
- replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting()));
667
- emitQueue(locator);
668
- emitVisible();
669
- emitDrafts();
708
+ replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting(), replica));
709
+ publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
710
+ queue: true,
711
+ drafts: consumed
712
+ }));
670
713
  moveTo({
671
714
  ...saving(status),
672
715
  ...attention()
@@ -679,18 +722,23 @@ export function createMdRuntime(options) {
679
722
  }
680
723
  async function discardHead(locator, identifier) {
681
724
  const local = await openLocal();
682
- await hydrate(local);
683
725
  const replica = replicas.get(locator);
684
726
  const head = replica?.queue[0];
685
727
  if (replica === undefined || head === undefined || head.id !== identifier || !refused(outcomes.get(head.id))) {
686
728
  return;
687
729
  }
688
730
  const batches = replica.queue.slice(1);
731
+ await persistQueue(local, replica, batches, [
732
+ {
733
+ id: identifier,
734
+ outcome: null
735
+ }
736
+ ]);
689
737
  outcomes.delete(identifier);
690
- await persistQueue(local, locator, batches);
691
- replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting()));
692
- emitQueue(locator);
693
- emitVisible();
738
+ replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting(), replica));
739
+ publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
740
+ queue: true
741
+ }));
694
742
  moveTo({
695
743
  ...status,
696
744
  state: pending() ? 'pending' : status.state,
@@ -739,7 +787,7 @@ export function createMdRuntime(options) {
739
787
  const room = replicas.get(result.locator)?.confirmed.record;
740
788
  if (room !== undefined) await scoped(result, room, await openLocal());
741
789
  }
742
- if (result.kind === 'revoked-authorization') refuse('revoked-authorization');
790
+ if (result.kind === 'revoked-authorization') await refuse('revoked-authorization');
743
791
  return result;
744
792
  } catch {
745
793
  journal.record('outcome history transport-uncertainty');
@@ -792,7 +840,7 @@ export function createMdRuntime(options) {
792
840
  const room = replicas.get(result.locator)?.confirmed.record;
793
841
  if (room !== undefined) await scoped(result, room, await openLocal());
794
842
  }
795
- if (result.kind === 'revoked-authorization') refuse('revoked-authorization');
843
+ if (result.kind === 'revoked-authorization') await refuse('revoked-authorization');
796
844
  journal.record(`outcome action ${result.kind}`);
797
845
  if (result.kind === 'rejected') journal.record(`refusal rejected ${result.code}`);
798
846
  return result;
@@ -885,7 +933,6 @@ export function createMdRuntime(options) {
885
933
  };
886
934
  }
887
935
  async function commitAccept(room, local, submitted, accepted, base, overlay) {
888
- await hydrate(local);
889
936
  const current = replicas.get(room.locator);
890
937
  if (current === undefined || !current.queue.some((batch)=>batch.id === submitted.id)) {
891
938
  return true;
@@ -895,14 +942,11 @@ export function createMdRuntime(options) {
895
942
  ...base.record,
896
943
  model: modelOf(room.locator),
897
944
  selectionModel: modelOf(room.locator, 'typed-selection')
898
- });
945
+ }, base);
899
946
  if (retainedBase === null) return false;
900
947
  const applied = overlay ? applyReturned(retainedBase, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion, options.modelBinding) : retainedBase;
901
948
  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, {
949
+ const acceptedOutcome = {
906
950
  kind: 'accepted',
907
951
  roomVersion: accepted.roomVersion,
908
952
  ...accepted.author === undefined ? {} : {
@@ -911,13 +955,29 @@ export function createMdRuntime(options) {
911
955
  ...accepted.time === undefined ? {} : {
912
956
  time: accepted.time
913
957
  }
958
+ };
959
+ await local.write({
960
+ next: applied.record,
961
+ previous: current.confirmed.record,
962
+ queue: remaining,
963
+ previousQueue: current.queue,
964
+ outcomes: [
965
+ {
966
+ id: submitted.id,
967
+ outcome: acceptedOutcome
968
+ }
969
+ ]
914
970
  });
971
+ acceptedIn.set(submitted.id, room.locator);
972
+ outcomes.set(submitted.id, acceptedOutcome);
915
973
  unreadable.delete(room.locator);
916
- replicas.set(room.locator, replicaOf(applied, remaining, rewriting()));
974
+ replicas.set(room.locator, replicaOf(applied, remaining, rewriting(), current));
917
975
  list(room, true);
918
- emitRooms();
919
- emitQueue(room.locator);
920
- emitVisible();
976
+ publishChangeSet(changeSet(room.locator, current, replicas.get(room.locator) ?? null, {
977
+ room: true,
978
+ queue: true,
979
+ query: true
980
+ }));
921
981
  journal.record(`outcome ${room.locator} ${submitted.id} accepted ${accepted.roomVersion}` + (accepted.author === undefined ? '' : ` author ${accepted.author}`) + (accepted.time === undefined ? '' : ` time ${accepted.time}`));
922
982
  return true;
923
983
  }
@@ -934,12 +994,18 @@ export function createMdRuntime(options) {
934
994
  return null;
935
995
  }
936
996
  await exclusive(async ()=>{
937
- await hydrate(local);
938
997
  const current = replicas.get(room.locator);
939
998
  if (current === undefined || current.queue[0]?.id !== head.id) return;
999
+ await persistQueue(local, current, current.queue, [
1000
+ {
1001
+ id: head.id,
1002
+ outcome: null
1003
+ }
1004
+ ]);
940
1005
  outcomes.delete(head.id);
941
- await persistQueue(local, room.locator, current.queue);
942
- emitQueue(room.locator);
1006
+ publishChangeSet(changeSet(room.locator, current, current, {
1007
+ queue: true
1008
+ }));
943
1009
  moveTo({
944
1010
  ...status,
945
1011
  ...attention()
@@ -948,6 +1014,11 @@ export function createMdRuntime(options) {
948
1014
  }
949
1015
  let result;
950
1016
  journal.record(`submission ${room.locator} ${head.id}`);
1017
+ if (await local.markSubmitted(room.locator, head.id)) {
1018
+ publishChangeSet(changeSet(room.locator, replica, replica, {
1019
+ queue: true
1020
+ }));
1021
+ }
951
1022
  try {
952
1023
  result = await options.adapter.mutate(authorized, {
953
1024
  locator: room.locator,
@@ -972,7 +1043,7 @@ export function createMdRuntime(options) {
972
1043
  return null;
973
1044
  }
974
1045
  if (result.kind === 'revoked-authorization') {
975
- refuse('revoked-authorization');
1046
+ await refuse('revoked-authorization');
976
1047
  return 'stop';
977
1048
  }
978
1049
  if (result.kind === 'scoped-refusal') {
@@ -985,14 +1056,21 @@ export function createMdRuntime(options) {
985
1056
  if (result.kind !== 'expired') journal.record(`refusal ${result.kind} ${result.code}`);
986
1057
  if (result.kind === 'retryable-refusal') progress.complete = false;
987
1058
  await exclusive(async ()=>{
988
- await hydrate(local);
989
1059
  const current = replicas.get(room.locator);
990
1060
  if (current === undefined || !current.queue.some((batch)=>batch.id === head.id)) {
991
1061
  return;
992
1062
  }
993
- outcomes.set(head.id, recordRefusal(result));
994
- await persistQueue(local, room.locator, current.queue);
995
- emitQueue(room.locator);
1063
+ const recorded = recordRefusal(result);
1064
+ await persistQueue(local, current, current.queue, [
1065
+ {
1066
+ id: head.id,
1067
+ outcome: recorded
1068
+ }
1069
+ ]);
1070
+ outcomes.set(head.id, recorded);
1071
+ publishChangeSet(changeSet(room.locator, current, current, {
1072
+ queue: true
1073
+ }));
996
1074
  moveTo({
997
1075
  ...status,
998
1076
  ...attention()
@@ -1004,7 +1082,7 @@ export function createMdRuntime(options) {
1004
1082
  if (from === undefined) return null;
1005
1083
  const pulled = await pullRefresh(authorized, from);
1006
1084
  if (pulled.kind === 'revoked-authorization') {
1007
- refuse('revoked-authorization');
1085
+ await refuse('revoked-authorization');
1008
1086
  return 'stop';
1009
1087
  }
1010
1088
  if (pulled.kind === 'scoped-refusal') {
@@ -1032,7 +1110,7 @@ export function createMdRuntime(options) {
1032
1110
  if (pulled.kind === 'beyond-horizon') {
1033
1111
  const bootstrapped = await pullBootstrap(authorized, room);
1034
1112
  if (bootstrapped.kind === 'revoked-authorization') {
1035
- refuse('revoked-authorization');
1113
+ await refuse('revoked-authorization');
1036
1114
  return 'stop';
1037
1115
  }
1038
1116
  if (bootstrapped.kind === 'scoped-refusal') {
@@ -1084,7 +1162,6 @@ export function createMdRuntime(options) {
1084
1162
  liveModels.delete(key);
1085
1163
  }
1086
1164
  await exclusive(async ()=>{
1087
- await hydrate(local);
1088
1165
  if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0)) return;
1089
1166
  const held = replicas.get(room.locator);
1090
1167
  if (held !== undefined) {
@@ -1100,9 +1177,15 @@ export function createMdRuntime(options) {
1100
1177
  selectionModel: next
1101
1178
  }
1102
1179
  };
1103
- await local.publish(record);
1104
- const confirmed = readConfirmed(record);
1105
- if (confirmed !== null) replicas.set(room.locator, replicaOf(confirmed, held.queue, rewriting()));
1180
+ await local.write({
1181
+ next: record,
1182
+ previous: held.confirmed.record,
1183
+ queue: held.queue
1184
+ });
1185
+ const confirmed = readConfirmed(record, held.confirmed);
1186
+ if (confirmed !== null) {
1187
+ replicas.set(room.locator, replicaOf(confirmed, held.queue, rewriting(), held));
1188
+ }
1106
1189
  }
1107
1190
  models.set(key, next);
1108
1191
  if (next.status === 'current') {
@@ -1110,8 +1193,11 @@ export function createMdRuntime(options) {
1110
1193
  invalidModels.delete(key);
1111
1194
  }
1112
1195
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
1113
- emitRooms();
1114
- if (queryChanged && held !== undefined) emitVisible();
1196
+ if (held === undefined) emitRooms();
1197
+ else publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, {
1198
+ room: true,
1199
+ query: queryChanged
1200
+ }));
1115
1201
  });
1116
1202
  }
1117
1203
  async function acquireModel(local, room, authorized, selection) {
@@ -1147,11 +1233,7 @@ export function createMdRuntime(options) {
1147
1233
  }
1148
1234
  if (epoch !== (modelEpoch.get(key) ?? 0)) return;
1149
1235
  if (answer.kind === 'revoked-authorization') {
1150
- await retainModel(local, room, attached, {
1151
- status: 'unavailable',
1152
- reason: 'revoked'
1153
- });
1154
- refuse('revoked-authorization');
1236
+ await refuse('revoked-authorization');
1155
1237
  return;
1156
1238
  }
1157
1239
  if (answer.kind === 'transport-uncertainty') {
@@ -1271,7 +1353,7 @@ export function createMdRuntime(options) {
1271
1353
  continue;
1272
1354
  }
1273
1355
  if (withheld.kind === 'revoked-authorization') {
1274
- refuse('revoked-authorization');
1356
+ await refuse('revoked-authorization');
1275
1357
  return;
1276
1358
  }
1277
1359
  continue;
@@ -1311,7 +1393,7 @@ export function createMdRuntime(options) {
1311
1393
  }
1312
1394
  if (result.kind === 'contract-unavailable') {
1313
1395
  journal.record(`connection contract unavailable ${result.code}`);
1314
- refuse('missing-required-guarantee', {
1396
+ await refuse('missing-required-guarantee', {
1315
1397
  code: result.code,
1316
1398
  fields: result.fields
1317
1399
  });
@@ -1319,17 +1401,17 @@ export function createMdRuntime(options) {
1319
1401
  return;
1320
1402
  }
1321
1403
  if (result.kind === 'revoked-authorization') {
1322
- refuse('revoked-authorization');
1404
+ await refuse('revoked-authorization');
1323
1405
  notifyReady();
1324
1406
  return;
1325
1407
  }
1326
1408
  if (!result.supportedVersions.includes(ROOM_CONTRACT_VERSION)) {
1327
- refuse('no-matching-version');
1409
+ await refuse('no-matching-version');
1328
1410
  notifyReady();
1329
1411
  return;
1330
1412
  }
1331
1413
  if (result.mdModel.identity !== MD_MODEL.identity || result.mdModel.version !== MD_MODEL.version) {
1332
- refuse('md-model-version-mismatch');
1414
+ await refuse('md-model-version-mismatch');
1333
1415
  notifyReady();
1334
1416
  return;
1335
1417
  }
@@ -1347,13 +1429,13 @@ export function createMdRuntime(options) {
1347
1429
  'unavailable'
1348
1430
  ].includes(room.replica.admission) || !declaredIdentity(room.attachedModel)) {
1349
1431
  journal.record(`invalid model or replica declaration ${room.locator}`);
1350
- refuse('missing-required-guarantee');
1432
+ await refuse('missing-required-guarantee');
1351
1433
  notifyReady();
1352
1434
  return;
1353
1435
  }
1354
1436
  if (!declaredSelection(room)) {
1355
1437
  journal.record(`invalid typed selection declaration ${room.locator}`);
1356
- refuse('missing-required-guarantee');
1438
+ await refuse('missing-required-guarantee');
1357
1439
  notifyReady();
1358
1440
  return;
1359
1441
  }
@@ -1363,14 +1445,14 @@ export function createMdRuntime(options) {
1363
1445
  }
1364
1446
  }
1365
1447
  if (mismatchedRoots.size > 0) {
1366
- refuse('missing-required-guarantee');
1448
+ await refuse('missing-required-guarantee');
1367
1449
  notifyReady();
1368
1450
  return;
1369
1451
  }
1370
1452
  const declared = new Set(result.capabilities.map((capability)=>capability.name));
1371
1453
  for (const required of options.requiredGuarantees){
1372
1454
  if (!knownCapabilities.has(required) || !declared.has(required)) {
1373
- refuse('missing-required-guarantee');
1455
+ await refuse('missing-required-guarantee');
1374
1456
  notifyReady();
1375
1457
  return;
1376
1458
  }
@@ -1508,7 +1590,7 @@ export function createMdRuntime(options) {
1508
1590
  continue;
1509
1591
  }
1510
1592
  if (withheld.kind === 'revoked-authorization') {
1511
- refuse('revoked-authorization');
1593
+ await refuse('revoked-authorization');
1512
1594
  return;
1513
1595
  }
1514
1596
  }
@@ -1559,6 +1641,9 @@ export function createMdRuntime(options) {
1559
1641
  });
1560
1642
  void enqueue();
1561
1643
  return {
1644
+ loaded () {
1645
+ return openLocal().then(()=>undefined);
1646
+ },
1562
1647
  rooms () {
1563
1648
  return listed();
1564
1649
  },
@@ -1580,8 +1665,8 @@ export function createMdRuntime(options) {
1580
1665
  draft: {
1581
1666
  list () {
1582
1667
  return exclusive(async ()=>{
1583
- const local = await openLocal();
1584
- return readDrafts(local);
1668
+ await openLocal();
1669
+ return snapshotDrafts();
1585
1670
  }).then((entries)=>{
1586
1671
  releaseWaitingDrafts();
1587
1672
  return entries;
@@ -1592,7 +1677,7 @@ export function createMdRuntime(options) {
1592
1677
  return content === undefined ? null : structuredClone(content);
1593
1678
  },
1594
1679
  replace (locator, path, content) {
1595
- return commitDraft(async (local)=>{
1680
+ return commitDraft(locator, path, async (local)=>{
1596
1681
  const stored = structuredClone(content);
1597
1682
  await local.keep({
1598
1683
  locator,
@@ -1603,7 +1688,7 @@ export function createMdRuntime(options) {
1603
1688
  });
1604
1689
  },
1605
1690
  discard (locator, path) {
1606
- return commitDraft(async (local)=>{
1691
+ return commitDraft(locator, path, async (local)=>{
1607
1692
  await local.discard(locator, path);
1608
1693
  forgetDraft(locator, path);
1609
1694
  });
@@ -1613,22 +1698,22 @@ export function createMdRuntime(options) {
1613
1698
  presentation: {
1614
1699
  read (app) {
1615
1700
  return exclusive(async ()=>{
1616
- await hydrate(await openLocal());
1701
+ await openLocal();
1617
1702
  return presentationOf(app);
1618
1703
  });
1619
1704
  },
1620
1705
  write (app, value) {
1621
1706
  return exclusive(async ()=>{
1622
1707
  const local = await openLocal();
1623
- await hydrate(local);
1624
1708
  const stored = structuredClone(value);
1625
1709
  await local.present({
1626
1710
  app,
1627
1711
  value: stored
1628
1712
  });
1629
1713
  presentations.set(app, stored);
1630
- }).then(()=>{
1631
- emitPresentation(app);
1714
+ publishChangeSet(changeSet('', null, null, {
1715
+ presentation: app
1716
+ }));
1632
1717
  });
1633
1718
  }
1634
1719
  },
@@ -1674,8 +1759,7 @@ export function createMdRuntime(options) {
1674
1759
  };
1675
1760
  draftListeners.add(listener);
1676
1761
  void exclusive(async ()=>{
1677
- const local = await openLocal();
1678
- await readDrafts(local);
1762
+ await openLocal();
1679
1763
  if (closed || !draftListeners.has(listener)) return;
1680
1764
  listener.ready = true;
1681
1765
  }).then(()=>{
@@ -1697,7 +1781,7 @@ export function createMdRuntime(options) {
1697
1781
  };
1698
1782
  presentationListeners.add(listener);
1699
1783
  void exclusive(async ()=>{
1700
- await hydrate(await openLocal());
1784
+ await openLocal();
1701
1785
  }).then(()=>{
1702
1786
  if (closed || !presentationListeners.has(listener)) return;
1703
1787
  listener.ready = true;
@@ -1764,6 +1848,7 @@ export function createMdRuntime(options) {
1764
1848
  async reauthorize () {
1765
1849
  if (refusal === null || !refusal.reauthorizationOffered) return;
1766
1850
  journal.record('connection reauthorize');
1851
+ await mutations;
1767
1852
  authorization = undefined;
1768
1853
  discoveryPaused = false;
1769
1854
  await enqueue();