@agentshouse/mdruntime 0.1.0-alpha.23 → 0.1.0-alpha.24

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 (55) hide show
  1. package/AGENTS.md +34 -18
  2. package/dist/change.d.ts +4 -0
  3. package/dist/change.d.ts.map +1 -1
  4. package/dist/change.js.map +1 -1
  5. package/dist/client.d.ts +9 -12
  6. package/dist/client.d.ts.map +1 -1
  7. package/dist/client.js +15 -20
  8. package/dist/client.js.map +1 -1
  9. package/dist/connection.d.ts +14 -18
  10. package/dist/connection.d.ts.map +1 -1
  11. package/dist/connection.js +390 -291
  12. package/dist/connection.js.map +1 -1
  13. package/dist/durability.js +1 -1
  14. package/dist/durability.js.map +1 -1
  15. package/dist/host.d.ts.map +1 -1
  16. package/dist/host.js +12 -14
  17. package/dist/host.js.map +1 -1
  18. package/dist/identity.d.ts +1 -0
  19. package/dist/identity.d.ts.map +1 -1
  20. package/dist/identity.js +7 -0
  21. package/dist/identity.js.map +1 -1
  22. package/dist/index.d.ts +3 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/outcome.d.ts +17 -20
  26. package/dist/outcome.d.ts.map +1 -1
  27. package/dist/outcome.js +1 -21
  28. package/dist/outcome.js.map +1 -1
  29. package/dist/overlay.d.ts.map +1 -1
  30. package/dist/overlay.js +1 -1
  31. package/dist/overlay.js.map +1 -1
  32. package/dist/replica.d.ts +1 -0
  33. package/dist/replica.d.ts.map +1 -1
  34. package/dist/replica.js +54 -0
  35. package/dist/replica.js.map +1 -1
  36. package/dist/room-draft.d.ts +124 -0
  37. package/dist/room-draft.d.ts.map +1 -0
  38. package/dist/room-draft.js +403 -0
  39. package/dist/room-draft.js.map +1 -0
  40. package/dist/save.d.ts +4 -1
  41. package/dist/save.d.ts.map +1 -1
  42. package/dist/save.js +34 -100
  43. package/dist/save.js.map +1 -1
  44. package/dist/status.d.ts +4 -6
  45. package/dist/status.d.ts.map +1 -1
  46. package/dist/status.js.map +1 -1
  47. package/dist/store.d.ts +18 -12
  48. package/dist/store.d.ts.map +1 -1
  49. package/dist/store.js +43 -34
  50. package/dist/store.js.map +1 -1
  51. package/dist/wire.d.ts +3 -3
  52. package/dist/wire.d.ts.map +1 -1
  53. package/dist/wire.js +6 -7
  54. package/dist/wire.js.map +1 -1
  55. package/package.json +1 -1
@@ -7,13 +7,14 @@ import { changeSet, mergeChangeSets } from './change-set.js';
7
7
  import { createConversationStore } from './conversation.js';
8
8
  import { MD_MODEL, PRESENTED, validRoot } from './collection.js';
9
9
  import { createLog } from './log.js';
10
- import { queuedOutcome, refusalOf, refused, withheldUntilReset } from './outcome.js';
11
10
  import { refreshRoom } from './refresh.js';
12
11
  import { buildBatch } from './save.js';
13
- import { applyReturned, compositionOf, readConfirmed, replicaOf } from './replica.js';
12
+ import { applyReturned, compositionOf, readConfirmed, replicaOf, withdrawals } from './replica.js';
13
+ import { changedParts, changesOf, held, readParts, returnedInto, underneath } from './room-draft.js';
14
14
  import { settled, same, saving, STARTING, transferring } from './status.js';
15
15
  import { detectStorageLoss, nextGeneration } from './durability.js';
16
- import { beneath } from './paths.js';
16
+ import { roomDraftRevision } from './identity.js';
17
+ import { beneath, normalize } from './paths.js';
17
18
  import { openRuntimeStore } from './store.js';
18
19
  import { unconfirmed } from './transfer.js';
19
20
  const DEFAULT_DISCOVERY_INTERVAL_MS = 60_000;
@@ -39,10 +40,11 @@ export function createMdRuntime(options) {
39
40
  let opening;
40
41
  let store;
41
42
  const replicas = new Map();
42
- const drafts = new Map();
43
+ const pendings = new Map();
44
+ const roomDrafts = new Map();
43
45
  const presentations = new Map();
44
46
  const outcomes = new Map();
45
- const acceptedIn = new Map();
47
+ const outcomeRooms = new Map();
46
48
  let limits = [];
47
49
  let slots = [];
48
50
  let capabilities = [];
@@ -97,7 +99,7 @@ export function createMdRuntime(options) {
97
99
  const statusListeners = new Set();
98
100
  const querySubscriptions = new Set();
99
101
  const queueSubscriptions = new Set();
100
- const draftListeners = new Set();
102
+ const roomDraftListeners = new Set();
101
103
  const presentationListeners = new Set();
102
104
  let withdrawConversations;
103
105
  let currencyQueued = false;
@@ -115,53 +117,49 @@ export function createMdRuntime(options) {
115
117
  const ready = new Promise((resolve)=>{
116
118
  notifyReady = resolve;
117
119
  });
118
- function draftContent(locator, path) {
119
- return drafts.get(locator)?.get(path);
120
+ function rememberRoomDraft(record) {
121
+ const room = roomDrafts.get(record.locator) ?? new Map();
122
+ room.set(record.path, record);
123
+ roomDrafts.set(record.locator, room);
120
124
  }
121
- function rememberDraft(locator, path, content) {
122
- const room = drafts.get(locator) ?? new Map();
123
- room.set(path, content);
124
- drafts.set(locator, room);
125
- }
126
- function forgetDraft(locator, path) {
127
- const room = drafts.get(locator);
125
+ function forgetRoomDraft(locator, path) {
126
+ const room = roomDrafts.get(locator);
128
127
  if (room === undefined) return;
129
128
  room.delete(path);
130
- if (room.size === 0) drafts.delete(locator);
129
+ if (room.size === 0) roomDrafts.delete(locator);
130
+ }
131
+ function roomDraftOf(record) {
132
+ return structuredClone({
133
+ room: record.locator,
134
+ path: record.path,
135
+ revision: record.revision,
136
+ parts: record.parts,
137
+ changedUnderneath: underneath(record.parts, record.path, replicas.get(record.locator)?.visible),
138
+ returned: record.returned
139
+ });
131
140
  }
132
- function snapshotDrafts() {
133
- const entries = [];
134
- for (const [locator, room] of drafts){
135
- for (const [path, content] of room){
136
- entries.push({
137
- locator,
138
- path,
139
- content: structuredClone(content)
140
- });
141
- }
142
- }
143
- entries.sort((left, right)=>{
141
+ function snapshotRoomDrafts() {
142
+ const records = [
143
+ ...roomDrafts.values()
144
+ ].flatMap((room)=>[
145
+ ...room.values()
146
+ ]);
147
+ records.sort((left, right)=>{
144
148
  if (left.locator !== right.locator) return left.locator < right.locator ? -1 : 1;
145
149
  if (left.path !== right.path) return left.path < right.path ? -1 : 1;
146
150
  return 0;
147
151
  });
148
- return entries;
152
+ return records.map(roomDraftOf);
149
153
  }
150
- function publishDrafts(waitingOnly) {
154
+ function publishRoomDrafts(waitingOnly) {
151
155
  if (closed) return;
152
- const value = snapshotDrafts();
153
- for (const listener of draftListeners){
156
+ const value = snapshotRoomDrafts();
157
+ for (const listener of roomDraftListeners){
154
158
  if (waitingOnly && listener.ready) continue;
155
159
  listener.ready = true;
156
160
  listener.notify(value);
157
161
  }
158
162
  }
159
- function emitDrafts() {
160
- publishDrafts(false);
161
- }
162
- function releaseWaitingDrafts() {
163
- publishDrafts(true);
164
- }
165
163
  function presentationOf(app) {
166
164
  const value = presentations.get(app);
167
165
  return value === undefined ? null : structuredClone(value);
@@ -172,17 +170,6 @@ export function createMdRuntime(options) {
172
170
  if (listener.app === app && listener.ready) listener.notify(presentationOf(app));
173
171
  }
174
172
  }
175
- function commitDraft(locator, path, work) {
176
- return exclusive(async ()=>{
177
- const local = await openLocal();
178
- await work(local);
179
- publishChangeSet(changeSet(locator, null, null, {
180
- drafts: [
181
- path
182
- ]
183
- }));
184
- });
185
- }
186
173
  function pending() {
187
174
  return [
188
175
  ...replicas.values()
@@ -190,42 +177,33 @@ export function createMdRuntime(options) {
190
177
  }
191
178
  function attention() {
192
179
  const attentionRequired = [];
193
- let reset = null;
194
- for (const [locator, replica] of replicas){
195
- const head = replica.queue[0];
196
- if (head === undefined) continue;
197
- const outcome = outcomes.get(head.id);
198
- if (outcome?.kind === 'retryable-refusal') {
199
- if (outcome.reset !== undefined && (reset === null || outcome.reset < reset)) {
200
- reset = outcome.reset;
201
- }
202
- continue;
203
- }
204
- if (!refused(outcome)) continue;
180
+ for (const record of [
181
+ ...roomDrafts.values()
182
+ ].flatMap((room)=>[
183
+ ...room.values()
184
+ ])){
185
+ if (record.returned === null) continue;
205
186
  attentionRequired.push({
206
- locator,
207
- batch: head.id,
208
- outcome: outcome.kind,
209
- refusal: refusalOf(outcome),
210
- ...outcome.kind === 'conflicted' && outcome.roomVersion !== undefined ? {
211
- roomVersion: outcome.roomVersion
212
- } : {}
187
+ room: record.locator,
188
+ path: record.path,
189
+ returned: structuredClone(record.returned)
213
190
  });
214
191
  }
192
+ attentionRequired.sort((left, right)=>{
193
+ if (left.room !== right.room) return left.room < right.room ? -1 : 1;
194
+ if (left.path !== right.path) return left.path < right.path ? -1 : 1;
195
+ return 0;
196
+ });
197
+ let reset = null;
198
+ for (const { submission } of pendings.values()){
199
+ if (submission.state !== 'reset') continue;
200
+ if (reset === null || submission.reset < reset) reset = submission.reset;
201
+ }
215
202
  return {
216
203
  attentionRequired,
217
204
  reset
218
205
  };
219
206
  }
220
- async function persistQueue(local, replica, batches, changedOutcomes = []) {
221
- await local.write({
222
- next: replica.confirmed.record,
223
- previous: replica.confirmed.record,
224
- queue: batches,
225
- previousQueue: replica.queue,
226
- outcomes: changedOutcomes
227
- });
228
- }
229
207
  function listed() {
230
208
  return structuredClone([
231
209
  ...rooms.values()
@@ -301,12 +279,17 @@ export function createMdRuntime(options) {
301
279
  subscription.notify(visible(subscription.query));
302
280
  }
303
281
  }
282
+ function submissionOf(batch) {
283
+ return pendings.get(batch.id)?.submission ?? {
284
+ state: 'waiting'
285
+ };
286
+ }
304
287
  function entriesOf(locator) {
305
288
  const replica = replicas.get(locator);
306
289
  if (replica === undefined) return [];
307
290
  return replica.queue.map((batch)=>Object.freeze({
308
291
  batch,
309
- outcome: queuedOutcome(outcomes.get(batch.id))
292
+ ...submissionOf(batch)
310
293
  }));
311
294
  }
312
295
  function emitQueue(locator) {
@@ -319,7 +302,9 @@ export function createMdRuntime(options) {
319
302
  if (set.rooms.length > 0) emitRooms();
320
303
  if (set.documents.length > 0 || set.query) emitVisible();
321
304
  for (const locator of set.queues)emitQueue(locator);
322
- if (set.drafts.length > 0) emitDrafts();
305
+ if (set.drafts.length > 0 || set.documents.some((change)=>roomDrafts.get(change.locator)?.has(change.path) === true)) {
306
+ publishRoomDrafts(false);
307
+ }
323
308
  for (const app of set.presentations)emitPresentation(app);
324
309
  conversations.changed(set);
325
310
  const cards = declaredCards();
@@ -450,7 +435,7 @@ export function createMdRuntime(options) {
450
435
  return [
451
436
  ...new Set([
452
437
  ...rooms.keys(),
453
- ...drafts.keys()
438
+ ...roomDrafts.keys()
454
439
  ])
455
440
  ].filter((locator)=>!listed.has(locator));
456
441
  }
@@ -459,9 +444,9 @@ export function createMdRuntime(options) {
459
444
  await exclusive(async ()=>{
460
445
  const unlisted = unlistedHeld(listed);
461
446
  await local.dropRooms(unlisted);
462
- for (const [batch, locator] of acceptedIn){
447
+ for (const [batch, locator] of outcomeRooms){
463
448
  if (listed.has(locator)) continue;
464
- acceptedIn.delete(batch);
449
+ outcomeRooms.delete(batch);
465
450
  outcomes.delete(batch);
466
451
  }
467
452
  const removed = unlisted.map((locator)=>changeSet(locator, replicas.get(locator) ?? null, null, {
@@ -469,14 +454,14 @@ export function createMdRuntime(options) {
469
454
  queue: true,
470
455
  query: true,
471
456
  drafts: [
472
- ...drafts.get(locator)?.keys() ?? []
457
+ ...roomDrafts.get(locator)?.keys() ?? []
473
458
  ]
474
459
  }));
475
460
  for (const locator of unlisted){
476
- for (const batch of replicas.get(locator)?.queue ?? [])outcomes.delete(batch.id);
461
+ for (const batch of replicas.get(locator)?.queue ?? [])pendings.delete(batch.id);
477
462
  replicas.delete(locator);
478
463
  rooms.delete(locator);
479
- drafts.delete(locator);
464
+ roomDrafts.delete(locator);
480
465
  unreadable.delete(locator);
481
466
  scopedRefusals.delete(locator);
482
467
  for (const attached of [
@@ -517,7 +502,7 @@ export function createMdRuntime(options) {
517
502
  opened.all(),
518
503
  opened.queues(),
519
504
  opened.outcomes(),
520
- opened.drafts(),
505
+ opened.roomDrafts(),
521
506
  opened.presentation(),
522
507
  opened.conversations(),
523
508
  opened.chatDrafts(),
@@ -529,20 +514,24 @@ export function createMdRuntime(options) {
529
514
  limits = declared;
530
515
  capabilities = named;
531
516
  slots = listedSlots;
532
- drafts.clear();
533
- for (const draft of kept)rememberDraft(draft.locator, draft.path, draft.content);
517
+ roomDrafts.clear();
518
+ for (const record of kept)rememberRoomDraft(record);
534
519
  presentations.clear();
535
520
  for (const record of presented)presentations.set(record.app, record.value);
536
521
  conversations.hydrate(held, chatDrafts);
537
522
  const queuedBatches = new Map(queues.map((queue)=>[
538
523
  queue.locator,
539
- queue.batches
524
+ queue.pending.map(({ batch })=>batch)
540
525
  ]));
526
+ pendings.clear();
527
+ for (const queue of queues){
528
+ for (const { batch, state } of queue.pending)pendings.set(batch.id, state);
529
+ }
541
530
  outcomes.clear();
542
- acceptedIn.clear();
531
+ outcomeRooms.clear();
543
532
  for (const record of recordedOutcomes){
544
533
  outcomes.set(record.id, record.outcome);
545
- if (record.outcome.kind === 'accepted') acceptedIn.set(record.id, record.locator);
534
+ outcomeRooms.set(record.id, record.locator);
546
535
  }
547
536
  const previousReplicas = new Map(replicas);
548
537
  replicas.clear();
@@ -590,7 +579,7 @@ export function createMdRuntime(options) {
590
579
  emitVisible();
591
580
  }
592
581
  for (const locator of locators)emitQueue(locator);
593
- emitDrafts();
582
+ publishRoomDrafts(false);
594
583
  return lastCompleted;
595
584
  }
596
585
  async function open() {
@@ -632,6 +621,81 @@ export function createMdRuntime(options) {
632
621
  mutations = next.then(()=>undefined, ()=>undefined);
633
622
  return next;
634
623
  }
624
+ function withdrawable(batch) {
625
+ const state = pendings.get(batch.id);
626
+ return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
627
+ }
628
+ async function settle(local, locator, before, confirmed, queue, settling = {}) {
629
+ const withdrawn = withdrawals(confirmed, queue, withdrawable, rewriting());
630
+ const kept = queue.filter((batch)=>!withdrawn.includes(batch));
631
+ const returning = [
632
+ ...withdrawn.map((batch)=>({
633
+ batch,
634
+ returned: {
635
+ kind: 'withdrawn'
636
+ }
637
+ })).reverse(),
638
+ ...settling.leaving === undefined ? [] : [
639
+ settling.leaving
640
+ ]
641
+ ];
642
+ const written = new Map();
643
+ for (const { batch, returned } of returning){
644
+ for (const saved of pendings.get(batch.id)?.drafts ?? []){
645
+ const existing = written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path);
646
+ written.set(saved.path, {
647
+ locator,
648
+ path: saved.path,
649
+ revision: roomDraftRevision(),
650
+ parts: returnedInto(existing?.parts, saved.parts),
651
+ returned
652
+ });
653
+ }
654
+ }
655
+ const settledOutcomes = [
656
+ ...settling.accepted === undefined ? [] : [
657
+ settling.accepted
658
+ ],
659
+ ...settling.leaving === undefined ? [] : [
660
+ {
661
+ id: settling.leaving.batch.id,
662
+ outcome: settling.leaving.outcome
663
+ }
664
+ ],
665
+ ...withdrawn.map((batch)=>({
666
+ id: batch.id,
667
+ outcome: {
668
+ kind: 'withdrawn'
669
+ }
670
+ }))
671
+ ];
672
+ await local.write({
673
+ next: confirmed.record,
674
+ previous: before?.confirmed.record ?? null,
675
+ queue: kept,
676
+ previousQueue: before?.queue ?? [],
677
+ outcomes: settledOutcomes,
678
+ roomDrafts: [
679
+ ...written.values()
680
+ ]
681
+ });
682
+ for (const { id, outcome } of settledOutcomes){
683
+ outcomes.set(id, outcome);
684
+ outcomeRooms.set(id, locator);
685
+ pendings.delete(id);
686
+ }
687
+ for (const record of written.values())rememberRoomDraft(record);
688
+ for (const batch of withdrawn)journal.record(`outcome ${locator} ${batch.id} withdrawn`);
689
+ replicas.set(locator, replicaOf(confirmed, kept, rewriting(), before));
690
+ return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
691
+ room: settling.room,
692
+ query: settling.query,
693
+ queue: kept.length !== (before?.queue.length ?? 0),
694
+ drafts: [
695
+ ...written.keys()
696
+ ]
697
+ });
698
+ }
635
699
  function publish(local, room, confirmed) {
636
700
  return exclusive(async ()=>{
637
701
  const prior = replicas.get(room.locator);
@@ -640,23 +704,28 @@ export function createMdRuntime(options) {
640
704
  model: modelOf(room.locator),
641
705
  selectionModel: modelOf(room.locator, 'typed-selection')
642
706
  }, prior?.confirmed);
643
- if (retained !== null) {
644
- await local.write({
645
- next: retained.record,
646
- previous: prior?.confirmed.record ?? null,
647
- queue: prior?.queue ?? []
648
- });
649
- unreadable.delete(room.locator);
650
- replicas.set(room.locator, replicaOf(retained, prior?.queue ?? [], rewriting(), prior));
707
+ if (retained === null) {
708
+ list(room, true);
709
+ publishChangeSet(changeSet(room.locator, prior ?? null, prior ?? null, {
710
+ room: true,
711
+ query: true
712
+ }));
713
+ return;
651
714
  }
652
- list(room, true);
653
- publishChangeSet(changeSet(room.locator, prior ?? null, replicas.get(room.locator) ?? null, {
715
+ const set = await settle(local, room.locator, prior, retained, prior?.queue ?? [], {
654
716
  room: true,
655
717
  query: true
656
- }));
718
+ });
719
+ unreadable.delete(room.locator);
720
+ list(room, true);
721
+ publishChangeSet(set);
722
+ moveTo({
723
+ ...status,
724
+ ...attention()
725
+ });
657
726
  });
658
727
  }
659
- async function write(locator, changes, replacing, writtenBy) {
728
+ async function saveRoomDrafts(locator, named, writtenBy) {
660
729
  const local = await openLocal();
661
730
  const replica = replicas.get(locator);
662
731
  const room = rooms.get(locator);
@@ -667,6 +736,28 @@ export function createMdRuntime(options) {
667
736
  outcome: 'refused',
668
737
  rule: 'collection-root-mismatch'
669
738
  };
739
+ const saved = [];
740
+ for (const { path, revision } of named){
741
+ const record = roomDrafts.get(locator)?.get(path);
742
+ if (record === undefined || record.revision !== revision) {
743
+ return {
744
+ outcome: 'refused',
745
+ rule: 'stale',
746
+ file: path
747
+ };
748
+ }
749
+ if (!saved.some((draft)=>draft.path === path)) saved.push({
750
+ path,
751
+ parts: record.parts
752
+ });
753
+ }
754
+ const changed = saved.flatMap(({ path, parts })=>changedParts(path, underneath(parts, path, replica.visible)));
755
+ if (changed.length > 0) return {
756
+ outcome: 'refused',
757
+ rule: 'changed-underneath',
758
+ parts: changed
759
+ };
760
+ const parts = saved.map(({ path, parts })=>changesOf(path, parts));
670
761
  const built = buildBatch({
671
762
  baseRoomVersion: replica.confirmed.record.roomVersion,
672
763
  composition: compositionFor(replica.confirmed.record),
@@ -675,45 +766,27 @@ export function createMdRuntime(options) {
675
766
  providerLimits: limits,
676
767
  rewriting: rewriting(),
677
768
  visible: replica.visible,
678
- changes,
769
+ changes: parts.flatMap(({ changes })=>changes),
770
+ bases: parts.flatMap(({ bases })=>bases),
679
771
  writtenBy
680
772
  });
681
773
  if (built.kind === 'refused') return {
682
774
  outcome: 'refused',
683
775
  ...built.refusal
684
776
  };
685
- const head = replica.queue[0];
686
- if (replacing !== undefined) {
687
- if (head === undefined || head.id !== replacing || !refused(outcomes.get(head.id))) {
688
- return {
689
- outcome: 'refused',
690
- rule: 'no-refused-head'
691
- };
692
- }
693
- }
694
- const batches = replacing === undefined ? [
777
+ const batches = [
695
778
  ...replica.queue,
696
779
  built.batch
697
- ] : [
698
- built.batch,
699
- ...replica.queue.slice(1)
700
- ];
701
- const consumed = [
702
- ...new Set(changes.map((change)=>change.path))
703
780
  ];
781
+ const consumed = saved.map(({ path })=>path);
704
782
  try {
705
783
  await local.write({
706
784
  next: replica.confirmed.record,
707
785
  previous: replica.confirmed.record,
708
786
  queue: batches,
709
787
  previousQueue: replica.queue,
710
- outcomes: replacing === undefined ? [] : [
711
- {
712
- id: replacing,
713
- outcome: null
714
- }
715
- ],
716
- consumedDrafts: consumed
788
+ saved,
789
+ consumedRoomDrafts: consumed
717
790
  });
718
791
  } catch {
719
792
  return {
@@ -721,8 +794,14 @@ export function createMdRuntime(options) {
721
794
  rule: 'durable-write'
722
795
  };
723
796
  }
724
- if (replacing !== undefined) outcomes.delete(replacing);
725
- for (const path of consumed)forgetDraft(locator, path);
797
+ pendings.set(built.batch.id, {
798
+ drafts: saved,
799
+ submission: {
800
+ state: 'waiting'
801
+ },
802
+ lost: false
803
+ });
804
+ for (const path of consumed)forgetRoomDraft(locator, path);
726
805
  replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting(), replica));
727
806
  publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
728
807
  queue: true,
@@ -735,34 +814,28 @@ export function createMdRuntime(options) {
735
814
  void enqueue();
736
815
  return {
737
816
  outcome: 'saved',
738
- id: built.batch.id
817
+ identifier: built.batch.id
739
818
  };
740
819
  }
741
- async function discardHead(locator, identifier) {
742
- const local = await openLocal();
743
- const replica = replicas.get(locator);
744
- const head = replica?.queue[0];
745
- if (replica === undefined || head === undefined || head.id !== identifier || !refused(outcomes.get(head.id))) {
820
+ async function mark(local, locator, identifier, submission, lost) {
821
+ const state = pendings.get(identifier);
822
+ if (state === undefined || JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost) {
746
823
  return;
747
824
  }
748
- const batches = replica.queue.slice(1);
749
- await persistQueue(local, replica, batches, [
750
- {
751
- id: identifier,
752
- outcome: null
753
- }
754
- ]);
755
- outcomes.delete(identifier);
756
- replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting(), replica));
757
- publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
825
+ await local.submit(locator, identifier, submission, lost);
826
+ pendings.set(identifier, {
827
+ ...state,
828
+ submission,
829
+ lost
830
+ });
831
+ const replica = replicas.get(locator);
832
+ if (replica !== undefined) publishChangeSet(changeSet(locator, replica, replica, {
758
833
  queue: true
759
834
  }));
760
835
  moveTo({
761
836
  ...status,
762
- state: pending() ? 'pending' : status.state,
763
837
  ...attention()
764
838
  });
765
- void enqueue();
766
839
  }
767
840
  async function readHistory(locator, path, query = {}) {
768
841
  if (closed || refusal !== null) return {
@@ -918,36 +991,66 @@ export function createMdRuntime(options) {
918
991
  await publish(local, room, bootstrapped.confirmed);
919
992
  return null;
920
993
  }
921
- function recordRefusal(result) {
922
- if (result.kind === 'expired') return {
923
- kind: 'expired'
924
- };
925
- if (result.kind === 'retryable-refusal') {
994
+ async function withhold(withheld, room, local, progress) {
995
+ progress.complete = false;
996
+ if (withheld.kind === 'scoped-refusal') {
997
+ await scoped(withheld, room, local);
998
+ } else if (withheld.kind === 'transport-uncertainty') {
999
+ progress.reachable = false;
1000
+ } else if (withheld.kind === 'room-not-readable') {
1001
+ unreadable.add(room.locator);
1002
+ list(room, false);
1003
+ emitRooms();
1004
+ } else if (withheld.kind === 'revoked-authorization') {
1005
+ await refuse('revoked-authorization');
1006
+ return 'stop';
1007
+ }
1008
+ return null;
1009
+ }
1010
+ function leaving(batch, result) {
1011
+ if (result.kind === 'expired') {
926
1012
  return {
927
- kind: 'retryable-refusal',
928
- code: result.code,
929
- fields: result.fields,
930
- ...result.reset === undefined ? {} : {
931
- reset: result.reset
1013
+ batch,
1014
+ outcome: {
1015
+ kind: 'expired'
1016
+ },
1017
+ returned: {
1018
+ kind: 'expired'
932
1019
  }
933
1020
  };
934
1021
  }
1022
+ const refusal = {
1023
+ code: result.code,
1024
+ fields: result.fields
1025
+ };
935
1026
  if (result.kind === 'conflicted') {
936
1027
  return {
937
- kind: 'conflicted',
938
- ...result.roomVersion === undefined ? {} : {
939
- roomVersion: result.roomVersion
1028
+ batch,
1029
+ outcome: {
1030
+ kind: 'conflicted',
1031
+ ...result.roomVersion === undefined ? {} : {
1032
+ roomVersion: result.roomVersion
1033
+ },
1034
+ affected: result.affected,
1035
+ ...refusal
940
1036
  },
941
- affected: result.affected,
942
- code: result.code,
943
- fields: result.fields
1037
+ returned: {
1038
+ kind: 'conflicted',
1039
+ ...refusal
1040
+ }
944
1041
  };
945
1042
  }
946
1043
  return {
947
- kind: 'rejected',
948
- affected: result.affected,
949
- code: result.code,
950
- fields: result.fields
1044
+ batch,
1045
+ outcome: {
1046
+ kind: 'rejected',
1047
+ affected: result.affected,
1048
+ ...refusal
1049
+ },
1050
+ returned: {
1051
+ kind: 'rejected',
1052
+ ...refusal
1053
+ }
951
1054
  };
952
1055
  }
953
1056
  async function commitAccept(room, local, submitted, accepted, base, overlay) {
@@ -974,28 +1077,21 @@ export function createMdRuntime(options) {
974
1077
  time: accepted.time
975
1078
  }
976
1079
  };
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
- ]
1080
+ const set = await settle(local, room.locator, current, applied, remaining, {
1081
+ accepted: {
1082
+ id: submitted.id,
1083
+ outcome: acceptedOutcome
1084
+ },
1085
+ room: true,
1086
+ query: true
988
1087
  });
989
- acceptedIn.set(submitted.id, room.locator);
990
- outcomes.set(submitted.id, acceptedOutcome);
991
1088
  unreadable.delete(room.locator);
992
- replicas.set(room.locator, replicaOf(applied, remaining, rewriting(), current));
993
1089
  list(room, true);
994
- publishChangeSet(changeSet(room.locator, current, replicas.get(room.locator) ?? null, {
995
- room: true,
996
- queue: true,
997
- query: true
998
- }));
1090
+ publishChangeSet(set);
1091
+ moveTo({
1092
+ ...status,
1093
+ ...attention()
1094
+ });
999
1095
  journal.record(`outcome ${room.locator} ${submitted.id} accepted ${accepted.roomVersion}` + (accepted.author === undefined ? '' : ` author ${accepted.author}`) + (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1000
1096
  return true;
1001
1097
  }
@@ -1004,39 +1100,22 @@ export function createMdRuntime(options) {
1004
1100
  const replica = replicas.get(room.locator);
1005
1101
  const head = replica?.queue[0];
1006
1102
  if (replica === undefined || head === undefined) return null;
1007
- const held = outcomes.get(head.id);
1008
- if (refused(held)) return null;
1009
- if (held?.kind === 'retryable-refusal') {
1010
- if (withheldUntilReset(held, Date.now())) {
1011
- progress.complete = false;
1012
- return null;
1013
- }
1014
- await exclusive(async ()=>{
1015
- const current = replicas.get(room.locator);
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
- ]);
1023
- outcomes.delete(head.id);
1024
- publishChangeSet(changeSet(room.locator, current, current, {
1025
- queue: true
1026
- }));
1027
- moveTo({
1028
- ...status,
1029
- ...attention()
1030
- });
1031
- });
1103
+ const { submission, lost: earlier } = pendings.get(head.id) ?? {
1104
+ submission: {
1105
+ state: 'waiting'
1106
+ },
1107
+ lost: false
1108
+ };
1109
+ if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
1110
+ progress.complete = false;
1111
+ return null;
1032
1112
  }
1113
+ const lost = earlier || submission.state === 'submitted';
1033
1114
  let result;
1034
1115
  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
- }
1116
+ await exclusive(()=>mark(local, room.locator, head.id, {
1117
+ state: 'submitted'
1118
+ }, lost));
1040
1119
  try {
1041
1120
  result = await options.adapter.mutate(authorized, {
1042
1121
  locator: room.locator,
@@ -1072,29 +1151,37 @@ export function createMdRuntime(options) {
1072
1151
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired' || result.kind === 'retryable-refusal') {
1073
1152
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` + (result.kind === 'conflicted' && result.roomVersion !== undefined ? ` ${result.roomVersion}` : ''));
1074
1153
  if (result.kind !== 'expired') journal.record(`refusal ${result.kind} ${result.code}`);
1075
- if (result.kind === 'retryable-refusal') progress.complete = false;
1154
+ }
1155
+ if (result.kind === 'retryable-refusal') {
1156
+ const reset = result.reset;
1157
+ progress.complete = false;
1158
+ await exclusive(()=>mark(local, room.locator, head.id, reset === undefined ? {
1159
+ state: 'waiting'
1160
+ } : {
1161
+ state: 'reset',
1162
+ reset
1163
+ }, lost));
1164
+ return null;
1165
+ }
1166
+ if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
1167
+ const left = leaving(head, result);
1076
1168
  await exclusive(async ()=>{
1077
1169
  const current = replicas.get(room.locator);
1078
1170
  if (current === undefined || !current.queue.some((batch)=>batch.id === head.id)) {
1079
1171
  return;
1080
1172
  }
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
- }));
1173
+ const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch)=>batch.id !== head.id), {
1174
+ leaving: left
1175
+ });
1176
+ publishChangeSet(set);
1092
1177
  moveTo({
1093
1178
  ...status,
1094
1179
  ...attention()
1095
1180
  });
1096
1181
  });
1097
- return null;
1182
+ const withheld = await advance(room, local, authorized);
1183
+ if (withheld !== null) return withhold(withheld, room, local, progress);
1184
+ continue;
1098
1185
  }
1099
1186
  const from = replicas.get(room.locator)?.confirmed;
1100
1187
  if (from === undefined) return null;
@@ -1356,25 +1443,7 @@ export function createMdRuntime(options) {
1356
1443
  }
1357
1444
  const withheld = await advance(room, local, authorization);
1358
1445
  if (withheld !== null) {
1359
- progress.complete = false;
1360
- if (withheld.kind === 'scoped-refusal') {
1361
- await scoped(withheld, room, local);
1362
- continue;
1363
- }
1364
- if (withheld.kind === 'transport-uncertainty') {
1365
- progress.reachable = false;
1366
- continue;
1367
- }
1368
- if (withheld.kind === 'room-not-readable') {
1369
- unreadable.add(room.locator);
1370
- list(room, false);
1371
- emitRooms();
1372
- continue;
1373
- }
1374
- if (withheld.kind === 'revoked-authorization') {
1375
- await refuse('revoked-authorization');
1376
- return;
1377
- }
1446
+ if (await withhold(withheld, room, local, progress) === 'stop') return;
1378
1447
  continue;
1379
1448
  }
1380
1449
  if (await submitRoom(room, local, authorization, progress) === 'stop') return;
@@ -1684,7 +1753,7 @@ export function createMdRuntime(options) {
1684
1753
  statusListeners.clear();
1685
1754
  querySubscriptions.clear();
1686
1755
  queueSubscriptions.clear();
1687
- draftListeners.clear();
1756
+ roomDraftListeners.clear();
1688
1757
  presentationListeners.clear();
1689
1758
  }
1690
1759
  options.signal?.addEventListener('abort', close, {
@@ -1717,35 +1786,77 @@ export function createMdRuntime(options) {
1717
1786
  syncStatus () {
1718
1787
  return shown;
1719
1788
  },
1720
- draft: {
1789
+ roomDraft: {
1721
1790
  list () {
1722
1791
  return exclusive(async ()=>{
1723
1792
  await openLocal();
1724
- return snapshotDrafts();
1725
- }).then((entries)=>{
1726
- releaseWaitingDrafts();
1727
- return entries;
1793
+ return snapshotRoomDrafts();
1794
+ }).then((listed)=>{
1795
+ publishRoomDrafts(true);
1796
+ return listed;
1728
1797
  });
1729
1798
  },
1730
- read (locator, path) {
1731
- const content = draftContent(locator, path);
1732
- return content === undefined ? null : structuredClone(content);
1799
+ read (room, path) {
1800
+ const record = roomDrafts.get(room)?.get(path);
1801
+ return record === undefined ? null : roomDraftOf(record);
1733
1802
  },
1734
- replace (locator, path, content) {
1735
- return commitDraft(locator, path, async (local)=>{
1736
- const stored = structuredClone(content);
1737
- await local.keep({
1738
- locator,
1803
+ write (room, path, revision, input) {
1804
+ return exclusive(async ()=>{
1805
+ const parts = readParts(input);
1806
+ if (path === '' || normalize(path) !== path) throw new Error('room-draft-malformed');
1807
+ const local = await openLocal();
1808
+ const current = roomDrafts.get(room)?.get(path);
1809
+ if ((current?.revision ?? null) !== revision) {
1810
+ return {
1811
+ outcome: 'stale',
1812
+ current: current === undefined ? null : roomDraftOf(current)
1813
+ };
1814
+ }
1815
+ const record = {
1816
+ locator: room,
1739
1817
  path,
1740
- content: stored
1741
- });
1742
- rememberDraft(locator, path, stored);
1818
+ revision: roomDraftRevision(),
1819
+ parts: held(current?.parts, parts),
1820
+ returned: current?.returned ?? null
1821
+ };
1822
+ await local.keepRoomDraft(record);
1823
+ rememberRoomDraft(record);
1824
+ publishChangeSet(changeSet(room, null, null, {
1825
+ drafts: [
1826
+ path
1827
+ ]
1828
+ }));
1829
+ return {
1830
+ outcome: 'written',
1831
+ roomDraft: roomDraftOf(record)
1832
+ };
1743
1833
  });
1744
1834
  },
1745
- discard (locator, path) {
1746
- return commitDraft(locator, path, async (local)=>{
1747
- await local.discard(locator, path);
1748
- forgetDraft(locator, path);
1835
+ discard (room, path, revision) {
1836
+ return exclusive(async ()=>{
1837
+ const local = await openLocal();
1838
+ const current = roomDrafts.get(room)?.get(path);
1839
+ if (current === undefined || current.revision !== revision) {
1840
+ return {
1841
+ outcome: 'stale',
1842
+ current: current === undefined ? null : roomDraftOf(current)
1843
+ };
1844
+ }
1845
+ await local.discardRoomDraft(room, path);
1846
+ forgetRoomDraft(room, path);
1847
+ publishChangeSet(changeSet(room, null, null, {
1848
+ drafts: [
1849
+ path
1850
+ ]
1851
+ }));
1852
+ moveTo({
1853
+ ...status,
1854
+ ...attention()
1855
+ });
1856
+ return {
1857
+ outcome: 'written',
1858
+ roomDraft: null
1859
+ };
1749
1860
  });
1750
1861
  }
1751
1862
  },
@@ -1773,23 +1884,11 @@ export function createMdRuntime(options) {
1773
1884
  });
1774
1885
  }
1775
1886
  },
1776
- save (locator, changes, replacing, writtenBy) {
1777
- return exclusive(()=>write(locator, changes, replacing, writtenBy));
1887
+ save (room, named, writtenBy) {
1888
+ return exclusive(()=>saveRoomDrafts(room, named, writtenBy));
1778
1889
  },
1779
1890
  outcome (identifier) {
1780
- const recorded = outcomes.get(identifier);
1781
- if (recorded !== undefined) return recorded;
1782
- for (const replica of replicas.values()){
1783
- if (replica.queue.some((batch)=>batch.id === identifier)) return {
1784
- kind: 'pending'
1785
- };
1786
- }
1787
- return null;
1788
- },
1789
- queue: {
1790
- discard (locator, identifier) {
1791
- return exclusive(()=>discardHead(locator, identifier));
1792
- }
1891
+ return outcomes.get(identifier) ?? null;
1793
1892
  },
1794
1893
  history (locator, path, query) {
1795
1894
  return readHistory(locator, path, query);
@@ -1807,23 +1906,23 @@ export function createMdRuntime(options) {
1807
1906
  return options.shell.persistentStorage;
1808
1907
  },
1809
1908
  observe (topic, second, third, fourth) {
1810
- if (topic === 'drafts') {
1909
+ if (topic === 'room-drafts') {
1811
1910
  const notify = second;
1812
1911
  const listener = {
1813
1912
  notify,
1814
1913
  ready: false
1815
1914
  };
1816
- draftListeners.add(listener);
1915
+ roomDraftListeners.add(listener);
1817
1916
  void exclusive(async ()=>{
1818
1917
  await openLocal();
1819
- if (closed || !draftListeners.has(listener)) return;
1918
+ if (closed || !roomDraftListeners.has(listener)) return;
1820
1919
  listener.ready = true;
1821
1920
  }).then(()=>{
1822
- if (closed || !listener.ready || !draftListeners.has(listener)) return;
1823
- listener.notify(snapshotDrafts());
1921
+ if (closed || !listener.ready || !roomDraftListeners.has(listener)) return;
1922
+ listener.notify(snapshotRoomDrafts());
1824
1923
  }, ()=>{});
1825
1924
  return ()=>{
1826
- draftListeners.delete(listener);
1925
+ roomDraftListeners.delete(listener);
1827
1926
  };
1828
1927
  }
1829
1928
  if (topic === 'conversation') {