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

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 (83) hide show
  1. package/AGENTS.md +70 -18
  2. package/dist/change-set.d.ts +4 -11
  3. package/dist/change-set.d.ts.map +1 -1
  4. package/dist/change-set.js +8 -16
  5. package/dist/change-set.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 +16 -15
  10. package/dist/client.d.ts.map +1 -1
  11. package/dist/client.js +17 -22
  12. package/dist/client.js.map +1 -1
  13. package/dist/collection.d.ts +0 -2
  14. package/dist/collection.d.ts.map +1 -1
  15. package/dist/collection.js +0 -5
  16. package/dist/collection.js.map +1 -1
  17. package/dist/connection.d.ts +21 -23
  18. package/dist/connection.d.ts.map +1 -1
  19. package/dist/connection.js +435 -352
  20. package/dist/connection.js.map +1 -1
  21. package/dist/conversation.d.ts +4 -4
  22. package/dist/conversation.d.ts.map +1 -1
  23. package/dist/conversation.js +12 -6
  24. package/dist/conversation.js.map +1 -1
  25. package/dist/durability.js +1 -1
  26. package/dist/durability.js.map +1 -1
  27. package/dist/host.d.ts.map +1 -1
  28. package/dist/host.js +12 -14
  29. package/dist/host.js.map +1 -1
  30. package/dist/identity.d.ts +1 -0
  31. package/dist/identity.d.ts.map +1 -1
  32. package/dist/identity.js +7 -0
  33. package/dist/identity.js.map +1 -1
  34. package/dist/index.d.ts +4 -4
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.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/paths.d.ts +1 -0
  45. package/dist/paths.d.ts.map +1 -1
  46. package/dist/paths.js +8 -0
  47. package/dist/paths.js.map +1 -1
  48. package/dist/query.d.ts +199 -10
  49. package/dist/query.d.ts.map +1 -1
  50. package/dist/query.js +450 -14
  51. package/dist/query.js.map +1 -1
  52. package/dist/read-side.d.ts +10 -0
  53. package/dist/read-side.d.ts.map +1 -0
  54. package/dist/read-side.js +374 -0
  55. package/dist/read-side.js.map +1 -0
  56. package/dist/replica.d.ts +1 -0
  57. package/dist/replica.d.ts.map +1 -1
  58. package/dist/replica.js +54 -0
  59. package/dist/replica.js.map +1 -1
  60. package/dist/room-draft.d.ts +124 -0
  61. package/dist/room-draft.d.ts.map +1 -0
  62. package/dist/room-draft.js +403 -0
  63. package/dist/room-draft.js.map +1 -0
  64. package/dist/rows.d.ts +39 -0
  65. package/dist/rows.d.ts.map +1 -0
  66. package/dist/rows.js +85 -0
  67. package/dist/rows.js.map +1 -0
  68. package/dist/save.d.ts +4 -1
  69. package/dist/save.d.ts.map +1 -1
  70. package/dist/save.js +34 -100
  71. package/dist/save.js.map +1 -1
  72. package/dist/status.d.ts +4 -6
  73. package/dist/status.d.ts.map +1 -1
  74. package/dist/status.js.map +1 -1
  75. package/dist/store.d.ts +18 -12
  76. package/dist/store.d.ts.map +1 -1
  77. package/dist/store.js +43 -34
  78. package/dist/store.js.map +1 -1
  79. package/dist/wire.d.ts +3 -3
  80. package/dist/wire.d.ts.map +1 -1
  81. package/dist/wire.js +6 -7
  82. package/dist/wire.js.map +1 -1
  83. package/package.json +2 -2
@@ -1,19 +1,21 @@
1
- import { rawQuery } from './query.js';
2
1
  import { verify } from '@agentshouse/mdmodel';
3
2
  import { cached, modelTypes, NO_MODEL, sameModel } from './model.js';
3
+ import { identityOf, planOf } from './query.js';
4
+ import { createReadSide } from './read-side.js';
4
5
  import { ROOM_CONTRACT_VERSION, VERSION_1_CAPABILITIES } from './adapter.js';
5
6
  import { bootstrapRoom } from './bootstrap.js';
6
7
  import { changeSet, mergeChangeSets } from './change-set.js';
7
8
  import { createConversationStore } from './conversation.js';
8
- import { MD_MODEL, PRESENTED, validRoot } from './collection.js';
9
+ import { MD_MODEL, validRoot } from './collection.js';
9
10
  import { createLog } from './log.js';
10
- import { queuedOutcome, refusalOf, refused, withheldUntilReset } from './outcome.js';
11
11
  import { refreshRoom } from './refresh.js';
12
12
  import { buildBatch } from './save.js';
13
- import { applyReturned, compositionOf, readConfirmed, replicaOf } from './replica.js';
13
+ import { applyReturned, compositionOf, readConfirmed, replicaOf, withdrawals } from './replica.js';
14
+ import { changedParts, changesOf, held, readParts, returnedInto, underneath } from './room-draft.js';
14
15
  import { settled, same, saving, STARTING, transferring } from './status.js';
15
16
  import { detectStorageLoss, nextGeneration } from './durability.js';
16
- import { beneath } from './paths.js';
17
+ import { roomDraftRevision } from './identity.js';
18
+ import { beneath, normalize } from './paths.js';
17
19
  import { openRuntimeStore } from './store.js';
18
20
  import { unconfirmed } from './transfer.js';
19
21
  const DEFAULT_DISCOVERY_INTERVAL_MS = 60_000;
@@ -39,10 +41,11 @@ export function createMdRuntime(options) {
39
41
  let opening;
40
42
  let store;
41
43
  const replicas = new Map();
42
- const drafts = new Map();
44
+ const pendings = new Map();
45
+ const roomDrafts = new Map();
43
46
  const presentations = new Map();
44
47
  const outcomes = new Map();
45
- const acceptedIn = new Map();
48
+ const outcomeRooms = new Map();
46
49
  let limits = [];
47
50
  let slots = [];
48
51
  let capabilities = [];
@@ -56,6 +59,7 @@ export function createMdRuntime(options) {
56
59
  const invalidModels = new Set();
57
60
  const modelEpoch = new Map();
58
61
  const discovered = new Map();
62
+ let roomsListed = false;
59
63
  const scopedRefusals = new Map();
60
64
  function modelKey(locator, attached) {
61
65
  return `${attached} ${locator}`;
@@ -95,15 +99,20 @@ export function createMdRuntime(options) {
95
99
  const refusalListeners = new Set();
96
100
  const storageLossListeners = new Set();
97
101
  const statusListeners = new Set();
98
- const querySubscriptions = new Set();
99
102
  const queueSubscriptions = new Set();
100
- const draftListeners = new Set();
103
+ const roomDraftListeners = new Set();
101
104
  const presentationListeners = new Set();
105
+ const readSide = createReadSide();
102
106
  let withdrawConversations;
103
107
  let currencyQueued = false;
104
108
  const conversations = createConversationStore({
105
109
  cards: declaredCards,
106
- documents: ()=>visible({}).documents,
110
+ documents: (locator)=>readSide.query(planOf({
111
+ rooms: [
112
+ locator
113
+ ],
114
+ select: 'all'
115
+ }, roomFacts())),
107
116
  call: (name, input)=>invokeAction(name, input),
108
117
  durably,
109
118
  stream: ()=>options.adapter.conversations,
@@ -115,53 +124,49 @@ export function createMdRuntime(options) {
115
124
  const ready = new Promise((resolve)=>{
116
125
  notifyReady = resolve;
117
126
  });
118
- function draftContent(locator, path) {
119
- return drafts.get(locator)?.get(path);
120
- }
121
- function rememberDraft(locator, path, content) {
122
- const room = drafts.get(locator) ?? new Map();
123
- room.set(path, content);
124
- drafts.set(locator, room);
127
+ function rememberRoomDraft(record) {
128
+ const room = roomDrafts.get(record.locator) ?? new Map();
129
+ room.set(record.path, record);
130
+ roomDrafts.set(record.locator, room);
125
131
  }
126
- function forgetDraft(locator, path) {
127
- const room = drafts.get(locator);
132
+ function forgetRoomDraft(locator, path) {
133
+ const room = roomDrafts.get(locator);
128
134
  if (room === undefined) return;
129
135
  room.delete(path);
130
- if (room.size === 0) drafts.delete(locator);
136
+ if (room.size === 0) roomDrafts.delete(locator);
137
+ }
138
+ function roomDraftOf(record) {
139
+ return structuredClone({
140
+ room: record.locator,
141
+ path: record.path,
142
+ revision: record.revision,
143
+ parts: record.parts,
144
+ changedUnderneath: underneath(record.parts, record.path, replicas.get(record.locator)?.visible),
145
+ returned: record.returned
146
+ });
131
147
  }
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)=>{
148
+ function snapshotRoomDrafts() {
149
+ const records = [
150
+ ...roomDrafts.values()
151
+ ].flatMap((room)=>[
152
+ ...room.values()
153
+ ]);
154
+ records.sort((left, right)=>{
144
155
  if (left.locator !== right.locator) return left.locator < right.locator ? -1 : 1;
145
156
  if (left.path !== right.path) return left.path < right.path ? -1 : 1;
146
157
  return 0;
147
158
  });
148
- return entries;
159
+ return records.map(roomDraftOf);
149
160
  }
150
- function publishDrafts(waitingOnly) {
161
+ function publishRoomDrafts(waitingOnly) {
151
162
  if (closed) return;
152
- const value = snapshotDrafts();
153
- for (const listener of draftListeners){
163
+ const value = snapshotRoomDrafts();
164
+ for (const listener of roomDraftListeners){
154
165
  if (waitingOnly && listener.ready) continue;
155
166
  listener.ready = true;
156
167
  listener.notify(value);
157
168
  }
158
169
  }
159
- function emitDrafts() {
160
- publishDrafts(false);
161
- }
162
- function releaseWaitingDrafts() {
163
- publishDrafts(true);
164
- }
165
170
  function presentationOf(app) {
166
171
  const value = presentations.get(app);
167
172
  return value === undefined ? null : structuredClone(value);
@@ -172,17 +177,6 @@ export function createMdRuntime(options) {
172
177
  if (listener.app === app && listener.ready) listener.notify(presentationOf(app));
173
178
  }
174
179
  }
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
180
  function pending() {
187
181
  return [
188
182
  ...replicas.values()
@@ -190,42 +184,33 @@ export function createMdRuntime(options) {
190
184
  }
191
185
  function attention() {
192
186
  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;
187
+ for (const record of [
188
+ ...roomDrafts.values()
189
+ ].flatMap((room)=>[
190
+ ...room.values()
191
+ ])){
192
+ if (record.returned === null) continue;
205
193
  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
- } : {}
194
+ room: record.locator,
195
+ path: record.path,
196
+ returned: structuredClone(record.returned)
213
197
  });
214
198
  }
199
+ attentionRequired.sort((left, right)=>{
200
+ if (left.room !== right.room) return left.room < right.room ? -1 : 1;
201
+ if (left.path !== right.path) return left.path < right.path ? -1 : 1;
202
+ return 0;
203
+ });
204
+ let reset = null;
205
+ for (const { submission } of pendings.values()){
206
+ if (submission.state !== 'reset') continue;
207
+ if (reset === null || submission.reset < reset) reset = submission.reset;
208
+ }
215
209
  return {
216
210
  attentionRequired,
217
211
  reset
218
212
  };
219
213
  }
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
214
  function listed() {
230
215
  return structuredClone([
231
216
  ...rooms.values()
@@ -245,16 +230,26 @@ export function createMdRuntime(options) {
245
230
  })
246
231
  })));
247
232
  }
248
- function queryRooms() {
249
- return [
250
- ...replicas.values()
251
- ].map((replica)=>({
252
- room: replica.confirmed.record.locator,
253
- roomVersion: replica.confirmed.record.roomVersion,
254
- documents: replica.visible.collection.documents,
255
- model: modelTypes(modelOf(replica.confirmed.record.locator)),
256
- typedSelection: replica.confirmed.record.typedSelection === null ? null : modelTypes(modelOf(replica.confirmed.record.locator, 'typed-selection'))
257
- }));
233
+ function roomFacts() {
234
+ const held = [
235
+ ...rooms.values()
236
+ ].map((room)=>{
237
+ const attached = [
238
+ modelOf(room.locator),
239
+ ...room.typedSelection === null ? [] : [
240
+ modelOf(room.locator, 'typed-selection')
241
+ ]
242
+ ];
243
+ return {
244
+ locator: room.locator,
245
+ available: attached.every((model)=>model.status !== 'unavailable'),
246
+ types: attached.flatMap((model)=>model.status === 'unavailable' ? [] : Object.values(model.types))
247
+ };
248
+ });
249
+ return {
250
+ listed: roomsListed,
251
+ rooms: held
252
+ };
258
253
  }
259
254
  function emitRooms() {
260
255
  const value = listed();
@@ -281,32 +276,17 @@ export function createMdRuntime(options) {
281
276
  shown = next;
282
277
  for (const listener of statusListeners)listener(shown);
283
278
  }
284
- function visible(request) {
285
- if (options.queryBinding === null) {
286
- if (Object.keys(request).length > 0) throw new Error('query-binding-unavailable');
287
- return rawQuery(queryRooms());
288
- }
289
- return options.queryBinding(queryRooms(), {
290
- ...request,
291
- selector: {
292
- $and: [
293
- PRESENTED,
294
- request.selector ?? {}
295
- ]
296
- }
297
- });
298
- }
299
- function emitVisible() {
300
- for (const subscription of querySubscriptions){
301
- subscription.notify(visible(subscription.query));
302
- }
279
+ function submissionOf(batch) {
280
+ return pendings.get(batch.id)?.submission ?? {
281
+ state: 'waiting'
282
+ };
303
283
  }
304
284
  function entriesOf(locator) {
305
285
  const replica = replicas.get(locator);
306
286
  if (replica === undefined) return [];
307
287
  return replica.queue.map((batch)=>Object.freeze({
308
288
  batch,
309
- outcome: queuedOutcome(outcomes.get(batch.id))
289
+ ...submissionOf(batch)
310
290
  }));
311
291
  }
312
292
  function emitQueue(locator) {
@@ -316,10 +296,12 @@ export function createMdRuntime(options) {
316
296
  }
317
297
  }
318
298
  function publishChangeSet(set) {
299
+ readSide.apply(set);
319
300
  if (set.rooms.length > 0) emitRooms();
320
- if (set.documents.length > 0 || set.query) emitVisible();
321
301
  for (const locator of set.queues)emitQueue(locator);
322
- if (set.drafts.length > 0) emitDrafts();
302
+ if (set.drafts.length > 0 || set.documents.some((change)=>roomDrafts.get(change.locator)?.has(change.path) === true)) {
303
+ publishRoomDrafts(false);
304
+ }
323
305
  for (const app of set.presentations)emitPresentation(app);
324
306
  conversations.changed(set);
325
307
  const cards = declaredCards();
@@ -392,8 +374,7 @@ export function createMdRuntime(options) {
392
374
  }
393
375
  });
394
376
  changed.push(changeSet(locator, before, replicas.get(locator) ?? null, {
395
- room: true,
396
- query: true
377
+ room: true
397
378
  }));
398
379
  }
399
380
  publishChangeSet(mergeChangeSets(changed));
@@ -444,13 +425,14 @@ export function createMdRuntime(options) {
444
425
  }
445
426
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
446
427
  }
428
+ roomsListed = true;
447
429
  emitRooms();
448
430
  }
449
431
  function unlistedHeld(listed) {
450
432
  return [
451
433
  ...new Set([
452
434
  ...rooms.keys(),
453
- ...drafts.keys()
435
+ ...roomDrafts.keys()
454
436
  ])
455
437
  ].filter((locator)=>!listed.has(locator));
456
438
  }
@@ -459,24 +441,23 @@ export function createMdRuntime(options) {
459
441
  await exclusive(async ()=>{
460
442
  const unlisted = unlistedHeld(listed);
461
443
  await local.dropRooms(unlisted);
462
- for (const [batch, locator] of acceptedIn){
444
+ for (const [batch, locator] of outcomeRooms){
463
445
  if (listed.has(locator)) continue;
464
- acceptedIn.delete(batch);
446
+ outcomeRooms.delete(batch);
465
447
  outcomes.delete(batch);
466
448
  }
467
449
  const removed = unlisted.map((locator)=>changeSet(locator, replicas.get(locator) ?? null, null, {
468
450
  room: true,
469
451
  queue: true,
470
- query: true,
471
452
  drafts: [
472
- ...drafts.get(locator)?.keys() ?? []
453
+ ...roomDrafts.get(locator)?.keys() ?? []
473
454
  ]
474
455
  }));
475
456
  for (const locator of unlisted){
476
- for (const batch of replicas.get(locator)?.queue ?? [])outcomes.delete(batch.id);
457
+ for (const batch of replicas.get(locator)?.queue ?? [])pendings.delete(batch.id);
477
458
  replicas.delete(locator);
478
459
  rooms.delete(locator);
479
- drafts.delete(locator);
460
+ roomDrafts.delete(locator);
480
461
  unreadable.delete(locator);
481
462
  scopedRefusals.delete(locator);
482
463
  for (const attached of [
@@ -517,7 +498,7 @@ export function createMdRuntime(options) {
517
498
  opened.all(),
518
499
  opened.queues(),
519
500
  opened.outcomes(),
520
- opened.drafts(),
501
+ opened.roomDrafts(),
521
502
  opened.presentation(),
522
503
  opened.conversations(),
523
504
  opened.chatDrafts(),
@@ -529,20 +510,24 @@ export function createMdRuntime(options) {
529
510
  limits = declared;
530
511
  capabilities = named;
531
512
  slots = listedSlots;
532
- drafts.clear();
533
- for (const draft of kept)rememberDraft(draft.locator, draft.path, draft.content);
513
+ roomDrafts.clear();
514
+ for (const record of kept)rememberRoomDraft(record);
534
515
  presentations.clear();
535
516
  for (const record of presented)presentations.set(record.app, record.value);
536
517
  conversations.hydrate(held, chatDrafts);
537
518
  const queuedBatches = new Map(queues.map((queue)=>[
538
519
  queue.locator,
539
- queue.batches
520
+ queue.pending.map(({ batch })=>batch)
540
521
  ]));
522
+ pendings.clear();
523
+ for (const queue of queues){
524
+ for (const { batch, state } of queue.pending)pendings.set(batch.id, state);
525
+ }
541
526
  outcomes.clear();
542
- acceptedIn.clear();
527
+ outcomeRooms.clear();
543
528
  for (const record of recordedOutcomes){
544
529
  outcomes.set(record.id, record.outcome);
545
- if (record.outcome.kind === 'accepted') acceptedIn.set(record.id, record.locator);
530
+ outcomeRooms.set(record.id, record.locator);
546
531
  }
547
532
  const previousReplicas = new Map(replicas);
548
533
  replicas.clear();
@@ -580,17 +565,21 @@ export function createMdRuntime(options) {
580
565
  locators: new Set([
581
566
  ...queuedBatches.keys(),
582
567
  ...replicas.keys()
583
- ])
568
+ ]),
569
+ read: mergeChangeSets([
570
+ ...new Set([
571
+ ...previousReplicas.keys(),
572
+ ...replicas.keys()
573
+ ])
574
+ ].map((locator)=>changeSet(locator, previousReplicas.get(locator) ?? null, replicas.get(locator) ?? null)))
584
575
  };
585
576
  }
586
577
  async function readState(opened) {
587
- const { lastCompleted, locators } = await hydrate(opened);
588
- if (replicas.size > 0) {
589
- emitRooms();
590
- emitVisible();
591
- }
578
+ const { lastCompleted, locators, read } = await hydrate(opened);
579
+ readSide.apply(read);
580
+ if (replicas.size > 0) emitRooms();
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,80 @@ 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
+ queue: kept.length !== (before?.queue.length ?? 0),
693
+ drafts: [
694
+ ...written.keys()
695
+ ]
696
+ });
697
+ }
635
698
  function publish(local, room, confirmed) {
636
699
  return exclusive(async ()=>{
637
700
  const prior = replicas.get(room.locator);
@@ -640,23 +703,26 @@ export function createMdRuntime(options) {
640
703
  model: modelOf(room.locator),
641
704
  selectionModel: modelOf(room.locator, 'typed-selection')
642
705
  }, 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));
706
+ if (retained === null) {
707
+ list(room, true);
708
+ publishChangeSet(changeSet(room.locator, prior ?? null, prior ?? null, {
709
+ room: true
710
+ }));
711
+ return;
651
712
  }
713
+ const set = await settle(local, room.locator, prior, retained, prior?.queue ?? [], {
714
+ room: true
715
+ });
716
+ unreadable.delete(room.locator);
652
717
  list(room, true);
653
- publishChangeSet(changeSet(room.locator, prior ?? null, replicas.get(room.locator) ?? null, {
654
- room: true,
655
- query: true
656
- }));
718
+ publishChangeSet(set);
719
+ moveTo({
720
+ ...status,
721
+ ...attention()
722
+ });
657
723
  });
658
724
  }
659
- async function write(locator, changes, replacing, writtenBy) {
725
+ async function saveRoomDrafts(locator, named, writtenBy) {
660
726
  const local = await openLocal();
661
727
  const replica = replicas.get(locator);
662
728
  const room = rooms.get(locator);
@@ -667,6 +733,28 @@ export function createMdRuntime(options) {
667
733
  outcome: 'refused',
668
734
  rule: 'collection-root-mismatch'
669
735
  };
736
+ const saved = [];
737
+ for (const { path, revision } of named){
738
+ const record = roomDrafts.get(locator)?.get(path);
739
+ if (record === undefined || record.revision !== revision) {
740
+ return {
741
+ outcome: 'refused',
742
+ rule: 'stale',
743
+ file: path
744
+ };
745
+ }
746
+ if (!saved.some((draft)=>draft.path === path)) saved.push({
747
+ path,
748
+ parts: record.parts
749
+ });
750
+ }
751
+ const changed = saved.flatMap(({ path, parts })=>changedParts(path, underneath(parts, path, replica.visible)));
752
+ if (changed.length > 0) return {
753
+ outcome: 'refused',
754
+ rule: 'changed-underneath',
755
+ parts: changed
756
+ };
757
+ const parts = saved.map(({ path, parts })=>changesOf(path, parts));
670
758
  const built = buildBatch({
671
759
  baseRoomVersion: replica.confirmed.record.roomVersion,
672
760
  composition: compositionFor(replica.confirmed.record),
@@ -675,45 +763,27 @@ export function createMdRuntime(options) {
675
763
  providerLimits: limits,
676
764
  rewriting: rewriting(),
677
765
  visible: replica.visible,
678
- changes,
766
+ changes: parts.flatMap(({ changes })=>changes),
767
+ bases: parts.flatMap(({ bases })=>bases),
679
768
  writtenBy
680
769
  });
681
770
  if (built.kind === 'refused') return {
682
771
  outcome: 'refused',
683
772
  ...built.refusal
684
773
  };
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 ? [
774
+ const batches = [
695
775
  ...replica.queue,
696
776
  built.batch
697
- ] : [
698
- built.batch,
699
- ...replica.queue.slice(1)
700
- ];
701
- const consumed = [
702
- ...new Set(changes.map((change)=>change.path))
703
777
  ];
778
+ const consumed = saved.map(({ path })=>path);
704
779
  try {
705
780
  await local.write({
706
781
  next: replica.confirmed.record,
707
782
  previous: replica.confirmed.record,
708
783
  queue: batches,
709
784
  previousQueue: replica.queue,
710
- outcomes: replacing === undefined ? [] : [
711
- {
712
- id: replacing,
713
- outcome: null
714
- }
715
- ],
716
- consumedDrafts: consumed
785
+ saved,
786
+ consumedRoomDrafts: consumed
717
787
  });
718
788
  } catch {
719
789
  return {
@@ -721,8 +791,14 @@ export function createMdRuntime(options) {
721
791
  rule: 'durable-write'
722
792
  };
723
793
  }
724
- if (replacing !== undefined) outcomes.delete(replacing);
725
- for (const path of consumed)forgetDraft(locator, path);
794
+ pendings.set(built.batch.id, {
795
+ drafts: saved,
796
+ submission: {
797
+ state: 'waiting'
798
+ },
799
+ lost: false
800
+ });
801
+ for (const path of consumed)forgetRoomDraft(locator, path);
726
802
  replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting(), replica));
727
803
  publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
728
804
  queue: true,
@@ -735,34 +811,28 @@ export function createMdRuntime(options) {
735
811
  void enqueue();
736
812
  return {
737
813
  outcome: 'saved',
738
- id: built.batch.id
814
+ identifier: built.batch.id
739
815
  };
740
816
  }
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))) {
817
+ async function mark(local, locator, identifier, submission, lost) {
818
+ const state = pendings.get(identifier);
819
+ if (state === undefined || JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost) {
746
820
  return;
747
821
  }
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, {
822
+ await local.submit(locator, identifier, submission, lost);
823
+ pendings.set(identifier, {
824
+ ...state,
825
+ submission,
826
+ lost
827
+ });
828
+ const replica = replicas.get(locator);
829
+ if (replica !== undefined) publishChangeSet(changeSet(locator, replica, replica, {
758
830
  queue: true
759
831
  }));
760
832
  moveTo({
761
833
  ...status,
762
- state: pending() ? 'pending' : status.state,
763
834
  ...attention()
764
835
  });
765
- void enqueue();
766
836
  }
767
837
  async function readHistory(locator, path, query = {}) {
768
838
  if (closed || refusal !== null) return {
@@ -918,36 +988,66 @@ export function createMdRuntime(options) {
918
988
  await publish(local, room, bootstrapped.confirmed);
919
989
  return null;
920
990
  }
921
- function recordRefusal(result) {
922
- if (result.kind === 'expired') return {
923
- kind: 'expired'
924
- };
925
- if (result.kind === 'retryable-refusal') {
991
+ async function withhold(withheld, room, local, progress) {
992
+ progress.complete = false;
993
+ if (withheld.kind === 'scoped-refusal') {
994
+ await scoped(withheld, room, local);
995
+ } else if (withheld.kind === 'transport-uncertainty') {
996
+ progress.reachable = false;
997
+ } else if (withheld.kind === 'room-not-readable') {
998
+ unreadable.add(room.locator);
999
+ list(room, false);
1000
+ emitRooms();
1001
+ } else if (withheld.kind === 'revoked-authorization') {
1002
+ await refuse('revoked-authorization');
1003
+ return 'stop';
1004
+ }
1005
+ return null;
1006
+ }
1007
+ function leaving(batch, result) {
1008
+ if (result.kind === 'expired') {
926
1009
  return {
927
- kind: 'retryable-refusal',
928
- code: result.code,
929
- fields: result.fields,
930
- ...result.reset === undefined ? {} : {
931
- reset: result.reset
1010
+ batch,
1011
+ outcome: {
1012
+ kind: 'expired'
1013
+ },
1014
+ returned: {
1015
+ kind: 'expired'
932
1016
  }
933
1017
  };
934
1018
  }
1019
+ const refusal = {
1020
+ code: result.code,
1021
+ fields: result.fields
1022
+ };
935
1023
  if (result.kind === 'conflicted') {
936
1024
  return {
937
- kind: 'conflicted',
938
- ...result.roomVersion === undefined ? {} : {
939
- roomVersion: result.roomVersion
1025
+ batch,
1026
+ outcome: {
1027
+ kind: 'conflicted',
1028
+ ...result.roomVersion === undefined ? {} : {
1029
+ roomVersion: result.roomVersion
1030
+ },
1031
+ affected: result.affected,
1032
+ ...refusal
940
1033
  },
941
- affected: result.affected,
942
- code: result.code,
943
- fields: result.fields
1034
+ returned: {
1035
+ kind: 'conflicted',
1036
+ ...refusal
1037
+ }
944
1038
  };
945
1039
  }
946
1040
  return {
947
- kind: 'rejected',
948
- affected: result.affected,
949
- code: result.code,
950
- fields: result.fields
1041
+ batch,
1042
+ outcome: {
1043
+ kind: 'rejected',
1044
+ affected: result.affected,
1045
+ ...refusal
1046
+ },
1047
+ returned: {
1048
+ kind: 'rejected',
1049
+ ...refusal
1050
+ }
951
1051
  };
952
1052
  }
953
1053
  async function commitAccept(room, local, submitted, accepted, base, overlay) {
@@ -974,28 +1074,20 @@ export function createMdRuntime(options) {
974
1074
  time: accepted.time
975
1075
  }
976
1076
  };
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
- ]
1077
+ const set = await settle(local, room.locator, current, applied, remaining, {
1078
+ accepted: {
1079
+ id: submitted.id,
1080
+ outcome: acceptedOutcome
1081
+ },
1082
+ room: true
988
1083
  });
989
- acceptedIn.set(submitted.id, room.locator);
990
- outcomes.set(submitted.id, acceptedOutcome);
991
1084
  unreadable.delete(room.locator);
992
- replicas.set(room.locator, replicaOf(applied, remaining, rewriting(), current));
993
1085
  list(room, true);
994
- publishChangeSet(changeSet(room.locator, current, replicas.get(room.locator) ?? null, {
995
- room: true,
996
- queue: true,
997
- query: true
998
- }));
1086
+ publishChangeSet(set);
1087
+ moveTo({
1088
+ ...status,
1089
+ ...attention()
1090
+ });
999
1091
  journal.record(`outcome ${room.locator} ${submitted.id} accepted ${accepted.roomVersion}` + (accepted.author === undefined ? '' : ` author ${accepted.author}`) + (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1000
1092
  return true;
1001
1093
  }
@@ -1004,39 +1096,22 @@ export function createMdRuntime(options) {
1004
1096
  const replica = replicas.get(room.locator);
1005
1097
  const head = replica?.queue[0];
1006
1098
  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
- });
1099
+ const { submission, lost: earlier } = pendings.get(head.id) ?? {
1100
+ submission: {
1101
+ state: 'waiting'
1102
+ },
1103
+ lost: false
1104
+ };
1105
+ if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
1106
+ progress.complete = false;
1107
+ return null;
1032
1108
  }
1109
+ const lost = earlier || submission.state === 'submitted';
1033
1110
  let result;
1034
1111
  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
- }
1112
+ await exclusive(()=>mark(local, room.locator, head.id, {
1113
+ state: 'submitted'
1114
+ }, lost));
1040
1115
  try {
1041
1116
  result = await options.adapter.mutate(authorized, {
1042
1117
  locator: room.locator,
@@ -1072,29 +1147,37 @@ export function createMdRuntime(options) {
1072
1147
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired' || result.kind === 'retryable-refusal') {
1073
1148
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` + (result.kind === 'conflicted' && result.roomVersion !== undefined ? ` ${result.roomVersion}` : ''));
1074
1149
  if (result.kind !== 'expired') journal.record(`refusal ${result.kind} ${result.code}`);
1075
- if (result.kind === 'retryable-refusal') progress.complete = false;
1150
+ }
1151
+ if (result.kind === 'retryable-refusal') {
1152
+ const reset = result.reset;
1153
+ progress.complete = false;
1154
+ await exclusive(()=>mark(local, room.locator, head.id, reset === undefined ? {
1155
+ state: 'waiting'
1156
+ } : {
1157
+ state: 'reset',
1158
+ reset
1159
+ }, lost));
1160
+ return null;
1161
+ }
1162
+ if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
1163
+ const left = leaving(head, result);
1076
1164
  await exclusive(async ()=>{
1077
1165
  const current = replicas.get(room.locator);
1078
1166
  if (current === undefined || !current.queue.some((batch)=>batch.id === head.id)) {
1079
1167
  return;
1080
1168
  }
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
- }));
1169
+ const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch)=>batch.id !== head.id), {
1170
+ leaving: left
1171
+ });
1172
+ publishChangeSet(set);
1092
1173
  moveTo({
1093
1174
  ...status,
1094
1175
  ...attention()
1095
1176
  });
1096
1177
  });
1097
- return null;
1178
+ const withheld = await advance(room, local, authorized);
1179
+ if (withheld !== null) return withhold(withheld, room, local, progress);
1180
+ continue;
1098
1181
  }
1099
1182
  const from = replicas.get(room.locator)?.confirmed;
1100
1183
  if (from === undefined) return null;
@@ -1174,7 +1257,6 @@ export function createMdRuntime(options) {
1174
1257
  const previous = modelOf(room.locator, attached);
1175
1258
  const heldBefore = replicas.get(room.locator)?.confirmed.record;
1176
1259
  if (JSON.stringify(previous) === JSON.stringify(next) && (heldBefore === undefined || JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) && JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) && JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) && JSON.stringify(heldBefore.typedSelection) === JSON.stringify(room.typedSelection ?? null))) return;
1177
- const queryChanged = previous.status === 'unavailable' || next.status === 'unavailable' ? previous.status !== next.status : JSON.stringify(previous.types) !== JSON.stringify(next.types);
1178
1260
  if (next.status === 'unavailable') {
1179
1261
  invalidModels.add(key);
1180
1262
  liveModels.delete(key);
@@ -1213,8 +1295,7 @@ export function createMdRuntime(options) {
1213
1295
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
1214
1296
  if (held === undefined) emitRooms();
1215
1297
  else publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, {
1216
- room: true,
1217
- query: queryChanged
1298
+ room: true
1218
1299
  }));
1219
1300
  });
1220
1301
  }
@@ -1356,25 +1437,7 @@ export function createMdRuntime(options) {
1356
1437
  }
1357
1438
  const withheld = await advance(room, local, authorization);
1358
1439
  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
- }
1440
+ if (await withhold(withheld, room, local, progress) === 'stop') return;
1378
1441
  continue;
1379
1442
  }
1380
1443
  if (await submitRoom(room, local, authorization, progress) === 'stop') return;
@@ -1682,9 +1745,9 @@ export function createMdRuntime(options) {
1682
1745
  refusalListeners.clear();
1683
1746
  storageLossListeners.clear();
1684
1747
  statusListeners.clear();
1685
- querySubscriptions.clear();
1748
+ readSide.close();
1686
1749
  queueSubscriptions.clear();
1687
- draftListeners.clear();
1750
+ roomDraftListeners.clear();
1688
1751
  presentationListeners.clear();
1689
1752
  }
1690
1753
  options.signal?.addEventListener('abort', close, {
@@ -1705,9 +1768,7 @@ export function createMdRuntime(options) {
1705
1768
  slots () {
1706
1769
  return slotted();
1707
1770
  },
1708
- query (request = {}) {
1709
- return visible(request);
1710
- },
1771
+ query: (input)=>readSide.query(planOf(input, roomFacts())),
1711
1772
  types (locator) {
1712
1773
  return {
1713
1774
  collection: modelTypes(modelOf(locator)),
@@ -1717,35 +1778,77 @@ export function createMdRuntime(options) {
1717
1778
  syncStatus () {
1718
1779
  return shown;
1719
1780
  },
1720
- draft: {
1781
+ roomDraft: {
1721
1782
  list () {
1722
1783
  return exclusive(async ()=>{
1723
1784
  await openLocal();
1724
- return snapshotDrafts();
1725
- }).then((entries)=>{
1726
- releaseWaitingDrafts();
1727
- return entries;
1785
+ return snapshotRoomDrafts();
1786
+ }).then((listed)=>{
1787
+ publishRoomDrafts(true);
1788
+ return listed;
1728
1789
  });
1729
1790
  },
1730
- read (locator, path) {
1731
- const content = draftContent(locator, path);
1732
- return content === undefined ? null : structuredClone(content);
1791
+ read (room, path) {
1792
+ const record = roomDrafts.get(room)?.get(path);
1793
+ return record === undefined ? null : roomDraftOf(record);
1733
1794
  },
1734
- replace (locator, path, content) {
1735
- return commitDraft(locator, path, async (local)=>{
1736
- const stored = structuredClone(content);
1737
- await local.keep({
1738
- locator,
1795
+ write (room, path, revision, input) {
1796
+ return exclusive(async ()=>{
1797
+ const parts = readParts(input);
1798
+ if (path === '' || normalize(path) !== path) throw new Error('room-draft-malformed');
1799
+ const local = await openLocal();
1800
+ const current = roomDrafts.get(room)?.get(path);
1801
+ if ((current?.revision ?? null) !== revision) {
1802
+ return {
1803
+ outcome: 'stale',
1804
+ current: current === undefined ? null : roomDraftOf(current)
1805
+ };
1806
+ }
1807
+ const record = {
1808
+ locator: room,
1739
1809
  path,
1740
- content: stored
1741
- });
1742
- rememberDraft(locator, path, stored);
1810
+ revision: roomDraftRevision(),
1811
+ parts: held(current?.parts, parts),
1812
+ returned: current?.returned ?? null
1813
+ };
1814
+ await local.keepRoomDraft(record);
1815
+ rememberRoomDraft(record);
1816
+ publishChangeSet(changeSet(room, null, null, {
1817
+ drafts: [
1818
+ path
1819
+ ]
1820
+ }));
1821
+ return {
1822
+ outcome: 'written',
1823
+ roomDraft: roomDraftOf(record)
1824
+ };
1743
1825
  });
1744
1826
  },
1745
- discard (locator, path) {
1746
- return commitDraft(locator, path, async (local)=>{
1747
- await local.discard(locator, path);
1748
- forgetDraft(locator, path);
1827
+ discard (room, path, revision) {
1828
+ return exclusive(async ()=>{
1829
+ const local = await openLocal();
1830
+ const current = roomDrafts.get(room)?.get(path);
1831
+ if (current === undefined || current.revision !== revision) {
1832
+ return {
1833
+ outcome: 'stale',
1834
+ current: current === undefined ? null : roomDraftOf(current)
1835
+ };
1836
+ }
1837
+ await local.discardRoomDraft(room, path);
1838
+ forgetRoomDraft(room, path);
1839
+ publishChangeSet(changeSet(room, null, null, {
1840
+ drafts: [
1841
+ path
1842
+ ]
1843
+ }));
1844
+ moveTo({
1845
+ ...status,
1846
+ ...attention()
1847
+ });
1848
+ return {
1849
+ outcome: 'written',
1850
+ roomDraft: null
1851
+ };
1749
1852
  });
1750
1853
  }
1751
1854
  },
@@ -1773,23 +1876,11 @@ export function createMdRuntime(options) {
1773
1876
  });
1774
1877
  }
1775
1878
  },
1776
- save (locator, changes, replacing, writtenBy) {
1777
- return exclusive(()=>write(locator, changes, replacing, writtenBy));
1879
+ save (room, named, writtenBy) {
1880
+ return exclusive(()=>saveRoomDrafts(room, named, writtenBy));
1778
1881
  },
1779
1882
  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
- }
1883
+ return outcomes.get(identifier) ?? null;
1793
1884
  },
1794
1885
  history (locator, path, query) {
1795
1886
  return readHistory(locator, path, query);
@@ -1807,23 +1898,23 @@ export function createMdRuntime(options) {
1807
1898
  return options.shell.persistentStorage;
1808
1899
  },
1809
1900
  observe (topic, second, third, fourth) {
1810
- if (topic === 'drafts') {
1901
+ if (topic === 'room-drafts') {
1811
1902
  const notify = second;
1812
1903
  const listener = {
1813
1904
  notify,
1814
1905
  ready: false
1815
1906
  };
1816
- draftListeners.add(listener);
1907
+ roomDraftListeners.add(listener);
1817
1908
  void exclusive(async ()=>{
1818
1909
  await openLocal();
1819
- if (closed || !draftListeners.has(listener)) return;
1910
+ if (closed || !roomDraftListeners.has(listener)) return;
1820
1911
  listener.ready = true;
1821
1912
  }).then(()=>{
1822
- if (closed || !listener.ready || !draftListeners.has(listener)) return;
1823
- listener.notify(snapshotDrafts());
1913
+ if (closed || !listener.ready || !roomDraftListeners.has(listener)) return;
1914
+ listener.notify(snapshotRoomDrafts());
1824
1915
  }, ()=>{});
1825
1916
  return ()=>{
1826
- draftListeners.delete(listener);
1917
+ roomDraftListeners.delete(listener);
1827
1918
  };
1828
1919
  }
1829
1920
  if (topic === 'conversation') {
@@ -1890,16 +1981,8 @@ export function createMdRuntime(options) {
1890
1981
  queueSubscriptions.delete(queue);
1891
1982
  };
1892
1983
  }
1893
- const subscription = {
1894
- query: second,
1895
- notify: third
1896
- };
1897
- const first = visible(subscription.query);
1898
- querySubscriptions.add(subscription);
1899
- subscription.notify(first);
1900
- return ()=>{
1901
- querySubscriptions.delete(subscription);
1902
- };
1984
+ const plan = planOf(second, roomFacts());
1985
+ return readSide.observe(plan, identityOf(plan), third);
1903
1986
  },
1904
1987
  async reauthorize () {
1905
1988
  if (refusal === null || !refusal.reauthorizationOffered) return;