@agentshouse/mdruntime 0.1.0-alpha.22 → 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 (71) hide show
  1. package/AGENTS.md +34 -18
  2. package/dist/bootstrap.d.ts +1 -2
  3. package/dist/bootstrap.d.ts.map +1 -1
  4. package/dist/bootstrap.js +2 -2
  5. package/dist/bootstrap.js.map +1 -1
  6. package/dist/change.d.ts +4 -0
  7. package/dist/change.d.ts.map +1 -1
  8. package/dist/change.js.map +1 -1
  9. package/dist/client.d.ts +9 -12
  10. package/dist/client.d.ts.map +1 -1
  11. package/dist/client.js +15 -20
  12. package/dist/client.js.map +1 -1
  13. package/dist/collection.d.ts +2 -11
  14. package/dist/collection.d.ts.map +1 -1
  15. package/dist/collection.js +11 -16
  16. package/dist/collection.js.map +1 -1
  17. package/dist/connection.d.ts +15 -20
  18. package/dist/connection.d.ts.map +1 -1
  19. package/dist/connection.js +399 -299
  20. package/dist/connection.js.map +1 -1
  21. package/dist/durability.js +1 -1
  22. package/dist/durability.js.map +1 -1
  23. package/dist/host.d.ts.map +1 -1
  24. package/dist/host.js +12 -14
  25. package/dist/host.js.map +1 -1
  26. package/dist/identity.d.ts +1 -0
  27. package/dist/identity.d.ts.map +1 -1
  28. package/dist/identity.js +7 -0
  29. package/dist/identity.js.map +1 -1
  30. package/dist/index.d.ts +4 -4
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/model.d.ts +10 -24
  34. package/dist/model.d.ts.map +1 -1
  35. package/dist/model.js +10 -1
  36. package/dist/model.js.map +1 -1
  37. package/dist/outcome.d.ts +17 -20
  38. package/dist/outcome.d.ts.map +1 -1
  39. package/dist/outcome.js +1 -21
  40. package/dist/outcome.js.map +1 -1
  41. package/dist/overlay.d.ts.map +1 -1
  42. package/dist/overlay.js +1 -1
  43. package/dist/overlay.js.map +1 -1
  44. package/dist/refresh.d.ts +1 -2
  45. package/dist/refresh.d.ts.map +1 -1
  46. package/dist/refresh.js +2 -2
  47. package/dist/refresh.js.map +1 -1
  48. package/dist/replica.d.ts +3 -3
  49. package/dist/replica.d.ts.map +1 -1
  50. package/dist/replica.js +59 -5
  51. package/dist/replica.js.map +1 -1
  52. package/dist/room-draft.d.ts +124 -0
  53. package/dist/room-draft.d.ts.map +1 -0
  54. package/dist/room-draft.js +403 -0
  55. package/dist/room-draft.js.map +1 -0
  56. package/dist/save.d.ts +4 -4
  57. package/dist/save.d.ts.map +1 -1
  58. package/dist/save.js +41 -130
  59. package/dist/save.js.map +1 -1
  60. package/dist/status.d.ts +4 -6
  61. package/dist/status.d.ts.map +1 -1
  62. package/dist/status.js.map +1 -1
  63. package/dist/store.d.ts +18 -12
  64. package/dist/store.d.ts.map +1 -1
  65. package/dist/store.js +43 -34
  66. package/dist/store.js.map +1 -1
  67. package/dist/wire.d.ts +3 -3
  68. package/dist/wire.d.ts.map +1 -1
  69. package/dist/wire.js +6 -7
  70. package/dist/wire.js.map +1 -1
  71. package/package.json +3 -3
@@ -1,4 +1,5 @@
1
1
  import { rawQuery } from './query.js';
2
+ import { verify } from '@agentshouse/mdmodel';
2
3
  import { cached, modelTypes, NO_MODEL, sameModel } from './model.js';
3
4
  import { ROOM_CONTRACT_VERSION, VERSION_1_CAPABILITIES } from './adapter.js';
4
5
  import { bootstrapRoom } from './bootstrap.js';
@@ -6,13 +7,14 @@ import { changeSet, mergeChangeSets } from './change-set.js';
6
7
  import { createConversationStore } from './conversation.js';
7
8
  import { MD_MODEL, PRESENTED, validRoot } from './collection.js';
8
9
  import { createLog } from './log.js';
9
- import { queuedOutcome, refusalOf, refused, withheldUntilReset } from './outcome.js';
10
10
  import { refreshRoom } from './refresh.js';
11
11
  import { buildBatch } from './save.js';
12
- 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';
13
14
  import { settled, same, saving, STARTING, transferring } from './status.js';
14
15
  import { detectStorageLoss, nextGeneration } from './durability.js';
15
- import { beneath } from './paths.js';
16
+ import { roomDraftRevision } from './identity.js';
17
+ import { beneath, normalize } from './paths.js';
16
18
  import { openRuntimeStore } from './store.js';
17
19
  import { unconfirmed } from './transfer.js';
18
20
  const DEFAULT_DISCOVERY_INTERVAL_MS = 60_000;
@@ -38,10 +40,11 @@ export function createMdRuntime(options) {
38
40
  let opening;
39
41
  let store;
40
42
  const replicas = new Map();
41
- const drafts = new Map();
43
+ const pendings = new Map();
44
+ const roomDrafts = new Map();
42
45
  const presentations = new Map();
43
46
  const outcomes = new Map();
44
- const acceptedIn = new Map();
47
+ const outcomeRooms = new Map();
45
48
  let limits = [];
46
49
  let slots = [];
47
50
  let capabilities = [];
@@ -96,7 +99,7 @@ export function createMdRuntime(options) {
96
99
  const statusListeners = new Set();
97
100
  const querySubscriptions = new Set();
98
101
  const queueSubscriptions = new Set();
99
- const draftListeners = new Set();
102
+ const roomDraftListeners = new Set();
100
103
  const presentationListeners = new Set();
101
104
  let withdrawConversations;
102
105
  let currencyQueued = false;
@@ -114,53 +117,49 @@ export function createMdRuntime(options) {
114
117
  const ready = new Promise((resolve)=>{
115
118
  notifyReady = resolve;
116
119
  });
117
- function draftContent(locator, path) {
118
- 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);
119
124
  }
120
- function rememberDraft(locator, path, content) {
121
- const room = drafts.get(locator) ?? new Map();
122
- room.set(path, content);
123
- drafts.set(locator, room);
124
- }
125
- function forgetDraft(locator, path) {
126
- const room = drafts.get(locator);
125
+ function forgetRoomDraft(locator, path) {
126
+ const room = roomDrafts.get(locator);
127
127
  if (room === undefined) return;
128
128
  room.delete(path);
129
- 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
+ });
130
140
  }
131
- function snapshotDrafts() {
132
- const entries = [];
133
- for (const [locator, room] of drafts){
134
- for (const [path, content] of room){
135
- entries.push({
136
- locator,
137
- path,
138
- content: structuredClone(content)
139
- });
140
- }
141
- }
142
- 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)=>{
143
148
  if (left.locator !== right.locator) return left.locator < right.locator ? -1 : 1;
144
149
  if (left.path !== right.path) return left.path < right.path ? -1 : 1;
145
150
  return 0;
146
151
  });
147
- return entries;
152
+ return records.map(roomDraftOf);
148
153
  }
149
- function publishDrafts(waitingOnly) {
154
+ function publishRoomDrafts(waitingOnly) {
150
155
  if (closed) return;
151
- const value = snapshotDrafts();
152
- for (const listener of draftListeners){
156
+ const value = snapshotRoomDrafts();
157
+ for (const listener of roomDraftListeners){
153
158
  if (waitingOnly && listener.ready) continue;
154
159
  listener.ready = true;
155
160
  listener.notify(value);
156
161
  }
157
162
  }
158
- function emitDrafts() {
159
- publishDrafts(false);
160
- }
161
- function releaseWaitingDrafts() {
162
- publishDrafts(true);
163
- }
164
163
  function presentationOf(app) {
165
164
  const value = presentations.get(app);
166
165
  return value === undefined ? null : structuredClone(value);
@@ -171,17 +170,6 @@ export function createMdRuntime(options) {
171
170
  if (listener.app === app && listener.ready) listener.notify(presentationOf(app));
172
171
  }
173
172
  }
174
- function commitDraft(locator, path, work) {
175
- return exclusive(async ()=>{
176
- const local = await openLocal();
177
- await work(local);
178
- publishChangeSet(changeSet(locator, null, null, {
179
- drafts: [
180
- path
181
- ]
182
- }));
183
- });
184
- }
185
173
  function pending() {
186
174
  return [
187
175
  ...replicas.values()
@@ -189,42 +177,33 @@ export function createMdRuntime(options) {
189
177
  }
190
178
  function attention() {
191
179
  const attentionRequired = [];
192
- let reset = null;
193
- for (const [locator, replica] of replicas){
194
- const head = replica.queue[0];
195
- if (head === undefined) continue;
196
- const outcome = outcomes.get(head.id);
197
- if (outcome?.kind === 'retryable-refusal') {
198
- if (outcome.reset !== undefined && (reset === null || outcome.reset < reset)) {
199
- reset = outcome.reset;
200
- }
201
- continue;
202
- }
203
- 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;
204
186
  attentionRequired.push({
205
- locator,
206
- batch: head.id,
207
- outcome: outcome.kind,
208
- refusal: refusalOf(outcome),
209
- ...outcome.kind === 'conflicted' && outcome.roomVersion !== undefined ? {
210
- roomVersion: outcome.roomVersion
211
- } : {}
187
+ room: record.locator,
188
+ path: record.path,
189
+ returned: structuredClone(record.returned)
212
190
  });
213
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
+ }
214
202
  return {
215
203
  attentionRequired,
216
204
  reset
217
205
  };
218
206
  }
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
226
- });
227
- }
228
207
  function listed() {
229
208
  return structuredClone([
230
209
  ...rooms.values()
@@ -300,12 +279,17 @@ export function createMdRuntime(options) {
300
279
  subscription.notify(visible(subscription.query));
301
280
  }
302
281
  }
282
+ function submissionOf(batch) {
283
+ return pendings.get(batch.id)?.submission ?? {
284
+ state: 'waiting'
285
+ };
286
+ }
303
287
  function entriesOf(locator) {
304
288
  const replica = replicas.get(locator);
305
289
  if (replica === undefined) return [];
306
290
  return replica.queue.map((batch)=>Object.freeze({
307
291
  batch,
308
- outcome: queuedOutcome(outcomes.get(batch.id))
292
+ ...submissionOf(batch)
309
293
  }));
310
294
  }
311
295
  function emitQueue(locator) {
@@ -318,7 +302,9 @@ export function createMdRuntime(options) {
318
302
  if (set.rooms.length > 0) emitRooms();
319
303
  if (set.documents.length > 0 || set.query) emitVisible();
320
304
  for (const locator of set.queues)emitQueue(locator);
321
- 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
+ }
322
308
  for (const app of set.presentations)emitPresentation(app);
323
309
  conversations.changed(set);
324
310
  const cards = declaredCards();
@@ -449,7 +435,7 @@ export function createMdRuntime(options) {
449
435
  return [
450
436
  ...new Set([
451
437
  ...rooms.keys(),
452
- ...drafts.keys()
438
+ ...roomDrafts.keys()
453
439
  ])
454
440
  ].filter((locator)=>!listed.has(locator));
455
441
  }
@@ -458,9 +444,9 @@ export function createMdRuntime(options) {
458
444
  await exclusive(async ()=>{
459
445
  const unlisted = unlistedHeld(listed);
460
446
  await local.dropRooms(unlisted);
461
- for (const [batch, locator] of acceptedIn){
447
+ for (const [batch, locator] of outcomeRooms){
462
448
  if (listed.has(locator)) continue;
463
- acceptedIn.delete(batch);
449
+ outcomeRooms.delete(batch);
464
450
  outcomes.delete(batch);
465
451
  }
466
452
  const removed = unlisted.map((locator)=>changeSet(locator, replicas.get(locator) ?? null, null, {
@@ -468,14 +454,14 @@ export function createMdRuntime(options) {
468
454
  queue: true,
469
455
  query: true,
470
456
  drafts: [
471
- ...drafts.get(locator)?.keys() ?? []
457
+ ...roomDrafts.get(locator)?.keys() ?? []
472
458
  ]
473
459
  }));
474
460
  for (const locator of unlisted){
475
- 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);
476
462
  replicas.delete(locator);
477
463
  rooms.delete(locator);
478
- drafts.delete(locator);
464
+ roomDrafts.delete(locator);
479
465
  unreadable.delete(locator);
480
466
  scopedRefusals.delete(locator);
481
467
  for (const attached of [
@@ -516,7 +502,7 @@ export function createMdRuntime(options) {
516
502
  opened.all(),
517
503
  opened.queues(),
518
504
  opened.outcomes(),
519
- opened.drafts(),
505
+ opened.roomDrafts(),
520
506
  opened.presentation(),
521
507
  opened.conversations(),
522
508
  opened.chatDrafts(),
@@ -528,20 +514,24 @@ export function createMdRuntime(options) {
528
514
  limits = declared;
529
515
  capabilities = named;
530
516
  slots = listedSlots;
531
- drafts.clear();
532
- for (const draft of kept)rememberDraft(draft.locator, draft.path, draft.content);
517
+ roomDrafts.clear();
518
+ for (const record of kept)rememberRoomDraft(record);
533
519
  presentations.clear();
534
520
  for (const record of presented)presentations.set(record.app, record.value);
535
521
  conversations.hydrate(held, chatDrafts);
536
522
  const queuedBatches = new Map(queues.map((queue)=>[
537
523
  queue.locator,
538
- queue.batches
524
+ queue.pending.map(({ batch })=>batch)
539
525
  ]));
526
+ pendings.clear();
527
+ for (const queue of queues){
528
+ for (const { batch, state } of queue.pending)pendings.set(batch.id, state);
529
+ }
540
530
  outcomes.clear();
541
- acceptedIn.clear();
531
+ outcomeRooms.clear();
542
532
  for (const record of recordedOutcomes){
543
533
  outcomes.set(record.id, record.outcome);
544
- if (record.outcome.kind === 'accepted') acceptedIn.set(record.id, record.locator);
534
+ outcomeRooms.set(record.id, record.locator);
545
535
  }
546
536
  const previousReplicas = new Map(replicas);
547
537
  replicas.clear();
@@ -589,7 +579,7 @@ export function createMdRuntime(options) {
589
579
  emitVisible();
590
580
  }
591
581
  for (const locator of locators)emitQueue(locator);
592
- emitDrafts();
582
+ publishRoomDrafts(false);
593
583
  return lastCompleted;
594
584
  }
595
585
  async function open() {
@@ -631,6 +621,81 @@ export function createMdRuntime(options) {
631
621
  mutations = next.then(()=>undefined, ()=>undefined);
632
622
  return next;
633
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
+ }
634
699
  function publish(local, room, confirmed) {
635
700
  return exclusive(async ()=>{
636
701
  const prior = replicas.get(room.locator);
@@ -639,23 +704,28 @@ export function createMdRuntime(options) {
639
704
  model: modelOf(room.locator),
640
705
  selectionModel: modelOf(room.locator, 'typed-selection')
641
706
  }, prior?.confirmed);
642
- if (retained !== null) {
643
- await local.write({
644
- next: retained.record,
645
- previous: prior?.confirmed.record ?? null,
646
- queue: prior?.queue ?? []
647
- });
648
- unreadable.delete(room.locator);
649
- 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;
650
714
  }
651
- list(room, true);
652
- publishChangeSet(changeSet(room.locator, prior ?? null, replicas.get(room.locator) ?? null, {
715
+ const set = await settle(local, room.locator, prior, retained, prior?.queue ?? [], {
653
716
  room: true,
654
717
  query: true
655
- }));
718
+ });
719
+ unreadable.delete(room.locator);
720
+ list(room, true);
721
+ publishChangeSet(set);
722
+ moveTo({
723
+ ...status,
724
+ ...attention()
725
+ });
656
726
  });
657
727
  }
658
- async function write(locator, changes, replacing, writtenBy) {
728
+ async function saveRoomDrafts(locator, named, writtenBy) {
659
729
  const local = await openLocal();
660
730
  const replica = replicas.get(locator);
661
731
  const room = rooms.get(locator);
@@ -666,54 +736,57 @@ export function createMdRuntime(options) {
666
736
  outcome: 'refused',
667
737
  rule: 'collection-root-mismatch'
668
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));
669
761
  const built = buildBatch({
670
762
  baseRoomVersion: replica.confirmed.record.roomVersion,
671
763
  composition: compositionFor(replica.confirmed.record),
672
764
  operations: room.operations,
673
- binding: options.modelBinding,
674
765
  providerOwnedPaths: room.providerOwnedPaths,
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 {
@@ -871,7 +944,7 @@ export function createMdRuntime(options) {
871
944
  }
872
945
  async function pullRefresh(authorized, confirmed) {
873
946
  try {
874
- const result = await refreshRoom(options.adapter, authorized, confirmed, options.modelBinding);
947
+ const result = await refreshRoom(options.adapter, authorized, confirmed);
875
948
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
876
949
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
877
950
  return result;
@@ -885,7 +958,7 @@ export function createMdRuntime(options) {
885
958
  const result = await bootstrapRoom(options.adapter, authorized, room, {
886
959
  collection: modelOf(room.locator),
887
960
  selection: modelOf(room.locator, 'typed-selection')
888
- }, options.modelBinding);
961
+ });
889
962
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
890
963
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
891
964
  return result;
@@ -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) {
@@ -962,7 +1065,7 @@ export function createMdRuntime(options) {
962
1065
  selectionModel: modelOf(room.locator, 'typed-selection')
963
1066
  }, base);
964
1067
  if (retainedBase === null) return false;
965
- const applied = overlay ? applyReturned(retainedBase, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion, options.modelBinding) : retainedBase;
1068
+ const applied = overlay ? applyReturned(retainedBase, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion) : retainedBase;
966
1069
  if (applied === null) return false;
967
1070
  const acceptedOutcome = {
968
1071
  kind: 'accepted',
@@ -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;
@@ -1279,15 +1366,16 @@ export function createMdRuntime(options) {
1279
1366
  });
1280
1367
  return;
1281
1368
  }
1282
- const verified = options.modelBinding?.verify(answer.snapshot);
1283
- await retainModel(local, room, attached, verified?.kind === 'verified' ? {
1369
+ const verified = verify(answer.snapshot.definitions);
1370
+ if (verified.verdict === 'refuse') journal.record(`model invalid ${room.locator} ${attached} ${verified.rule} ${verified.file}`);
1371
+ await retainModel(local, room, attached, verified.verdict === 'accept' ? {
1284
1372
  status: 'current',
1285
1373
  snapshot: structuredClone(answer.snapshot),
1286
1374
  verifiedWith: MD_MODEL,
1287
- types: verified.types
1375
+ types: verified.model.types
1288
1376
  } : {
1289
1377
  status: 'unavailable',
1290
- reason: verified === undefined ? 'binding-unavailable' : 'invalid'
1378
+ reason: 'invalid'
1291
1379
  });
1292
1380
  }
1293
1381
  async function scoped(result, room, local) {
@@ -1355,25 +1443,7 @@ export function createMdRuntime(options) {
1355
1443
  }
1356
1444
  const withheld = await advance(room, local, authorization);
1357
1445
  if (withheld !== null) {
1358
- progress.complete = false;
1359
- if (withheld.kind === 'scoped-refusal') {
1360
- await scoped(withheld, room, local);
1361
- continue;
1362
- }
1363
- if (withheld.kind === 'transport-uncertainty') {
1364
- progress.reachable = false;
1365
- continue;
1366
- }
1367
- if (withheld.kind === 'room-not-readable') {
1368
- unreadable.add(room.locator);
1369
- list(room, false);
1370
- emitRooms();
1371
- continue;
1372
- }
1373
- if (withheld.kind === 'revoked-authorization') {
1374
- await refuse('revoked-authorization');
1375
- return;
1376
- }
1446
+ if (await withhold(withheld, room, local, progress) === 'stop') return;
1377
1447
  continue;
1378
1448
  }
1379
1449
  if (await submitRoom(room, local, authorization, progress) === 'stop') return;
@@ -1683,7 +1753,7 @@ export function createMdRuntime(options) {
1683
1753
  statusListeners.clear();
1684
1754
  querySubscriptions.clear();
1685
1755
  queueSubscriptions.clear();
1686
- draftListeners.clear();
1756
+ roomDraftListeners.clear();
1687
1757
  presentationListeners.clear();
1688
1758
  }
1689
1759
  options.signal?.addEventListener('abort', close, {
@@ -1716,35 +1786,77 @@ export function createMdRuntime(options) {
1716
1786
  syncStatus () {
1717
1787
  return shown;
1718
1788
  },
1719
- draft: {
1789
+ roomDraft: {
1720
1790
  list () {
1721
1791
  return exclusive(async ()=>{
1722
1792
  await openLocal();
1723
- return snapshotDrafts();
1724
- }).then((entries)=>{
1725
- releaseWaitingDrafts();
1726
- return entries;
1793
+ return snapshotRoomDrafts();
1794
+ }).then((listed)=>{
1795
+ publishRoomDrafts(true);
1796
+ return listed;
1727
1797
  });
1728
1798
  },
1729
- read (locator, path) {
1730
- const content = draftContent(locator, path);
1731
- 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);
1732
1802
  },
1733
- replace (locator, path, content) {
1734
- return commitDraft(locator, path, async (local)=>{
1735
- const stored = structuredClone(content);
1736
- await local.keep({
1737
- 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,
1738
1817
  path,
1739
- content: stored
1740
- });
1741
- 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
+ };
1742
1833
  });
1743
1834
  },
1744
- discard (locator, path) {
1745
- return commitDraft(locator, path, async (local)=>{
1746
- await local.discard(locator, path);
1747
- 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
+ };
1748
1860
  });
1749
1861
  }
1750
1862
  },
@@ -1772,23 +1884,11 @@ export function createMdRuntime(options) {
1772
1884
  });
1773
1885
  }
1774
1886
  },
1775
- save (locator, changes, replacing, writtenBy) {
1776
- return exclusive(()=>write(locator, changes, replacing, writtenBy));
1887
+ save (room, named, writtenBy) {
1888
+ return exclusive(()=>saveRoomDrafts(room, named, writtenBy));
1777
1889
  },
1778
1890
  outcome (identifier) {
1779
- const recorded = outcomes.get(identifier);
1780
- if (recorded !== undefined) return recorded;
1781
- for (const replica of replicas.values()){
1782
- if (replica.queue.some((batch)=>batch.id === identifier)) return {
1783
- kind: 'pending'
1784
- };
1785
- }
1786
- return null;
1787
- },
1788
- queue: {
1789
- discard (locator, identifier) {
1790
- return exclusive(()=>discardHead(locator, identifier));
1791
- }
1891
+ return outcomes.get(identifier) ?? null;
1792
1892
  },
1793
1893
  history (locator, path, query) {
1794
1894
  return readHistory(locator, path, query);
@@ -1806,23 +1906,23 @@ export function createMdRuntime(options) {
1806
1906
  return options.shell.persistentStorage;
1807
1907
  },
1808
1908
  observe (topic, second, third, fourth) {
1809
- if (topic === 'drafts') {
1909
+ if (topic === 'room-drafts') {
1810
1910
  const notify = second;
1811
1911
  const listener = {
1812
1912
  notify,
1813
1913
  ready: false
1814
1914
  };
1815
- draftListeners.add(listener);
1915
+ roomDraftListeners.add(listener);
1816
1916
  void exclusive(async ()=>{
1817
1917
  await openLocal();
1818
- if (closed || !draftListeners.has(listener)) return;
1918
+ if (closed || !roomDraftListeners.has(listener)) return;
1819
1919
  listener.ready = true;
1820
1920
  }).then(()=>{
1821
- if (closed || !listener.ready || !draftListeners.has(listener)) return;
1822
- listener.notify(snapshotDrafts());
1921
+ if (closed || !listener.ready || !roomDraftListeners.has(listener)) return;
1922
+ listener.notify(snapshotRoomDrafts());
1823
1923
  }, ()=>{});
1824
1924
  return ()=>{
1825
- draftListeners.delete(listener);
1925
+ roomDraftListeners.delete(listener);
1826
1926
  };
1827
1927
  }
1828
1928
  if (topic === 'conversation') {