@agentshouse/mdruntime 0.1.0-alpha.4 → 0.1.0-alpha.41

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 (125) hide show
  1. package/AGENTS.md +191 -0
  2. package/README.md +44 -0
  3. package/dist/adapter.d.ts +174 -18
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/adapter.js +2 -1
  6. package/dist/adapter.js.map +1 -1
  7. package/dist/bootstrap.d.ts +7 -7
  8. package/dist/bootstrap.d.ts.map +1 -1
  9. package/dist/bootstrap.js +14 -9
  10. package/dist/bootstrap.js.map +1 -1
  11. package/dist/change-set.d.ts +33 -0
  12. package/dist/change-set.d.ts.map +1 -0
  13. package/dist/change-set.js +89 -0
  14. package/dist/change-set.js.map +1 -0
  15. package/dist/change.d.ts +10 -0
  16. package/dist/change.d.ts.map +1 -1
  17. package/dist/change.js.map +1 -1
  18. package/dist/client.d.ts +74 -0
  19. package/dist/client.d.ts.map +1 -0
  20. package/dist/client.js +232 -0
  21. package/dist/client.js.map +1 -0
  22. package/dist/collection.d.ts +25 -12
  23. package/dist/collection.d.ts.map +1 -1
  24. package/dist/collection.js +113 -31
  25. package/dist/collection.js.map +1 -1
  26. package/dist/connection.d.ts +84 -27
  27. package/dist/connection.d.ts.map +1 -1
  28. package/dist/connection.js +1685 -336
  29. package/dist/connection.js.map +1 -1
  30. package/dist/conversation.d.ts +79 -0
  31. package/dist/conversation.d.ts.map +1 -0
  32. package/dist/conversation.js +593 -0
  33. package/dist/conversation.js.map +1 -0
  34. package/dist/durability.d.ts +9 -0
  35. package/dist/durability.d.ts.map +1 -0
  36. package/dist/durability.js +11 -0
  37. package/dist/durability.js.map +1 -0
  38. package/dist/file.d.ts +20 -0
  39. package/dist/file.d.ts.map +1 -0
  40. package/dist/file.js +149 -0
  41. package/dist/file.js.map +1 -0
  42. package/dist/host.d.ts +14 -0
  43. package/dist/host.d.ts.map +1 -0
  44. package/dist/host.js +211 -0
  45. package/dist/host.js.map +1 -0
  46. package/dist/identity.d.ts +3 -0
  47. package/dist/identity.d.ts.map +1 -0
  48. package/dist/identity.js +23 -0
  49. package/dist/identity.js.map +1 -0
  50. package/dist/index.d.ts +15 -7
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +1 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/memory.d.ts +17 -2
  55. package/dist/memory.d.ts.map +1 -1
  56. package/dist/memory.js +201 -15
  57. package/dist/memory.js.map +1 -1
  58. package/dist/model.d.ts +55 -0
  59. package/dist/model.d.ts.map +1 -0
  60. package/dist/model.js +36 -0
  61. package/dist/model.js.map +1 -0
  62. package/dist/observation.d.ts +25 -0
  63. package/dist/observation.d.ts.map +1 -0
  64. package/dist/observation.js +92 -0
  65. package/dist/observation.js.map +1 -0
  66. package/dist/outcome.d.ts +22 -18
  67. package/dist/outcome.d.ts.map +1 -1
  68. package/dist/outcome.js +1 -11
  69. package/dist/outcome.js.map +1 -1
  70. package/dist/overlay.d.ts +2 -0
  71. package/dist/overlay.d.ts.map +1 -1
  72. package/dist/overlay.js +107 -1
  73. package/dist/overlay.js.map +1 -1
  74. package/dist/paths.d.ts +0 -1
  75. package/dist/paths.d.ts.map +1 -1
  76. package/dist/paths.js +0 -4
  77. package/dist/paths.js.map +1 -1
  78. package/dist/query.d.ts +209 -0
  79. package/dist/query.d.ts.map +1 -0
  80. package/dist/query.js +467 -0
  81. package/dist/query.js.map +1 -0
  82. package/dist/read-side.d.ts +14 -0
  83. package/dist/read-side.d.ts.map +1 -0
  84. package/dist/read-side.js +451 -0
  85. package/dist/read-side.js.map +1 -0
  86. package/dist/refresh.d.ts +3 -3
  87. package/dist/refresh.d.ts.map +1 -1
  88. package/dist/refresh.js +10 -7
  89. package/dist/refresh.js.map +1 -1
  90. package/dist/replica.d.ts +23 -4
  91. package/dist/replica.d.ts.map +1 -1
  92. package/dist/replica.js +224 -13
  93. package/dist/replica.js.map +1 -1
  94. package/dist/room-draft.d.ts +124 -0
  95. package/dist/room-draft.d.ts.map +1 -0
  96. package/dist/room-draft.js +406 -0
  97. package/dist/room-draft.js.map +1 -0
  98. package/dist/rows.d.ts +39 -0
  99. package/dist/rows.d.ts.map +1 -0
  100. package/dist/rows.js +100 -0
  101. package/dist/rows.js.map +1 -0
  102. package/dist/save.d.ts +11 -2
  103. package/dist/save.d.ts.map +1 -1
  104. package/dist/save.js +116 -128
  105. package/dist/save.js.map +1 -1
  106. package/dist/search.d.ts +61 -0
  107. package/dist/search.d.ts.map +1 -0
  108. package/dist/search.js +155 -0
  109. package/dist/search.js.map +1 -0
  110. package/dist/status.d.ts +9 -2
  111. package/dist/status.d.ts.map +1 -1
  112. package/dist/status.js +5 -4
  113. package/dist/status.js.map +1 -1
  114. package/dist/store.d.ts +89 -18
  115. package/dist/store.d.ts.map +1 -1
  116. package/dist/store.js +445 -57
  117. package/dist/store.js.map +1 -1
  118. package/dist/transfer.d.ts +5 -0
  119. package/dist/transfer.d.ts.map +1 -1
  120. package/dist/transfer.js.map +1 -1
  121. package/dist/wire.d.ts +64 -0
  122. package/dist/wire.d.ts.map +1 -0
  123. package/dist/wire.js +144 -0
  124. package/dist/wire.js.map +1 -0
  125. package/package.json +15 -13
@@ -1,50 +1,205 @@
1
- import { query as evaluate } from '@agentshouse/query';
1
+ import { verify } from '@agentshouse/mdmodel';
2
+ import { cached, modelTypes, NO_MODEL, sameModel } from './model.js';
3
+ import { opened } from './observation.js';
4
+ import { filterOf, identityOf, planOf } from './query.js';
5
+ import { createReadSide } from './read-side.js';
6
+ import { createSearchIndex, searchPlanOf } from './search.js';
2
7
  import { ROOM_CONTRACT_VERSION, VERSION_1_CAPABILITIES } from './adapter.js';
3
8
  import { bootstrapRoom } from './bootstrap.js';
4
- import { MD_MODEL, PRESENTED } from './collection.js';
9
+ import { changeSet, mergeChangeSets } from './change-set.js';
10
+ import { createConversationStore } from './conversation.js';
11
+ import { MD_MODEL, parseCollection, validRoot } from './collection.js';
5
12
  import { createLog } from './log.js';
6
- import { queuedOutcome, refused } from './outcome.js';
7
13
  import { refreshRoom } from './refresh.js';
8
14
  import { buildBatch } from './save.js';
9
- import { applyReturned, readConfirmed, replicaOf } from './replica.js';
15
+ import { applyReturned, compositionOf, landed, readConfirmed, replicaOf, withdrawals } from './replica.js';
16
+ import { changedParts, changesOf, held, readParts, returnedInto, underneath } from './room-draft.js';
10
17
  import { settled, same, saving, STARTING, transferring } from './status.js';
11
- import { openReplicaStore } from './store.js';
18
+ import { detectStorageLoss, nextGeneration } from './durability.js';
19
+ import { roomDraftRevision } from './identity.js';
20
+ import { beneath, normalize } from './paths.js';
21
+ import { openRuntimeStore } from './store.js';
12
22
  import { unconfirmed } from './transfer.js';
13
23
  const DEFAULT_DISCOVERY_INTERVAL_MS = 60_000;
24
+ const EMPTY_SCHEMA = {
25
+ verdict: 'accept',
26
+ model: {
27
+ types: {}
28
+ }
29
+ };
14
30
  const knownCapabilities = new Set(VERSION_1_CAPABILITIES);
31
+ function within(root, path) {
32
+ return root === '' || beneath(root, path);
33
+ }
34
+ function declaredIdentity(model) {
35
+ if (model === undefined) return false;
36
+ if (model === null) return true;
37
+ return typeof model.identity === 'string' && model.identity.length > 0 && typeof model.version === 'string' && model.version.length > 0;
38
+ }
39
+ function declaredSelection(room) {
40
+ const selection = room.typedSelection;
41
+ if (selection === undefined) return true;
42
+ if (!Array.isArray(selection.roots) || selection.roots.length === 0) return false;
43
+ if (!declaredIdentity(selection.attachedModel)) return false;
44
+ return selection.roots.every((root, index)=>validRoot(root) && root !== '' && !within(room.collectionRoot, root) && selection.roots.every((other, at)=>at === index || !within(other, root)));
45
+ }
15
46
  export function createMdRuntime(options) {
47
+ return composeRuntime(options).md;
48
+ }
49
+ export function composeRuntime(options) {
16
50
  const period = options.discoveryIntervalMs ?? DEFAULT_DISCOVERY_INTERVAL_MS;
17
51
  let authorization = options.authorization;
18
52
  let opening;
19
53
  let store;
20
54
  const replicas = new Map();
21
- const drafts = new Map();
55
+ const pendings = new Map();
56
+ const roomDrafts = new Map();
57
+ const presentations = new Map();
22
58
  const outcomes = new Map();
59
+ const outcomeRooms = new Map();
23
60
  let limits = [];
61
+ let slots = [];
24
62
  let capabilities = [];
63
+ function rewriting() {
64
+ return capabilities.some((capability)=>capability.name === 'move-reference-rewrites') ? 'provider' : 'runtime';
65
+ }
25
66
  const rooms = new Map();
26
67
  const unreadable = new Set();
68
+ const models = new Map();
69
+ const liveModels = new Map();
70
+ const invalidModels = new Set();
71
+ const modelEpoch = new Map();
72
+ const discovered = new Map();
73
+ let roomsListed = false;
74
+ const scopedRefusals = new Map();
75
+ const sourceRefusals = new Map();
76
+ const declarationRefusals = new Set();
77
+ function modelKey(locator, attached) {
78
+ return `${attached} ${locator}`;
79
+ }
80
+ function modelOf(locator, attached = 'collection') {
81
+ return models.get(modelKey(locator, attached)) ?? NO_MODEL;
82
+ }
83
+ function savedModel(locator, attached) {
84
+ return invalidModels.has(modelKey(locator, attached)) ? {
85
+ status: 'unavailable',
86
+ reason: 'revoked'
87
+ } : modelOf(locator, attached);
88
+ }
89
+ function compositionFor(record) {
90
+ return compositionOf({
91
+ ...record,
92
+ model: savedModel(record.locator, 'collection'),
93
+ selectionModel: savedModel(record.locator, 'typed-selection')
94
+ });
95
+ }
96
+ const mismatchedRoots = new Set();
27
97
  let refusal = null;
98
+ let storageLoss = null;
28
99
  let status = STARTING;
100
+ let shown = STARTING;
29
101
  let discoveryPaused = false;
30
102
  let timer;
31
103
  let closed = false;
104
+ let stopping = false;
105
+ let withdrawNotifications;
106
+ let handed;
107
+ const notices = new Map();
32
108
  let chain = Promise.resolve();
33
109
  let mutations = Promise.resolve();
34
110
  let inflight = false;
35
111
  let queued = false;
36
112
  const roomListeners = new Set();
37
113
  const refusalListeners = new Set();
114
+ const storageLossListeners = new Set();
38
115
  const statusListeners = new Set();
39
- const querySubscriptions = new Set();
40
116
  const queueSubscriptions = new Set();
117
+ const roomDraftListeners = new Set();
118
+ const presentationListeners = new Set();
119
+ const readSide = createReadSide();
120
+ let withdrawConversations;
121
+ let currencyQueued = false;
122
+ let serviceMode = 'open';
123
+ let haltedRefusal = false;
124
+ let openSignals = 0;
125
+ const serviceModeListeners = new Set();
126
+ const conversations = createConversationStore({
127
+ cards: declaredCards,
128
+ documents: (locator)=>readSide.query(planOf({
129
+ rooms: [
130
+ locator
131
+ ],
132
+ select: 'all'
133
+ }, roomFacts())),
134
+ call: (name, input)=>invokeAction(name, input),
135
+ durably,
136
+ stream: ()=>options.adapter.conversations,
137
+ publish: publishChangeSet,
138
+ closed: ()=>closed
139
+ });
140
+ const searchIndex = createSearchIndex({
141
+ documents: ()=>readSide.documents(),
142
+ document: (room, path)=>readSide.document(room, path),
143
+ messages: ()=>conversations.messages(),
144
+ message: (conversation, identity)=>conversations.message(conversation, identity)
145
+ });
41
146
  const journal = createLog();
42
147
  let notifyReady = ()=>{};
43
148
  const ready = new Promise((resolve)=>{
44
149
  notifyReady = resolve;
45
150
  });
46
- function at(locator, path) {
47
- return `${locator} ${path}`;
151
+ function rememberRoomDraft(record) {
152
+ const room = roomDrafts.get(record.locator) ?? new Map();
153
+ room.set(record.path, record);
154
+ roomDrafts.set(record.locator, room);
155
+ }
156
+ function forgetRoomDraft(locator, path) {
157
+ const room = roomDrafts.get(locator);
158
+ if (room === undefined) return;
159
+ room.delete(path);
160
+ if (room.size === 0) roomDrafts.delete(locator);
161
+ }
162
+ function roomDraftOf(record) {
163
+ return structuredClone({
164
+ room: record.locator,
165
+ path: record.path,
166
+ revision: record.revision,
167
+ parts: record.parts,
168
+ changedUnderneath: underneath(record.parts, record.path, replicas.get(record.locator)?.visible),
169
+ returned: record.returned
170
+ });
171
+ }
172
+ function snapshotRoomDrafts() {
173
+ const records = [
174
+ ...roomDrafts.values()
175
+ ].flatMap((room)=>[
176
+ ...room.values()
177
+ ]);
178
+ records.sort((left, right)=>{
179
+ if (left.locator !== right.locator) return left.locator < right.locator ? -1 : 1;
180
+ if (left.path !== right.path) return left.path < right.path ? -1 : 1;
181
+ return 0;
182
+ });
183
+ return records.map(roomDraftOf);
184
+ }
185
+ function publishRoomDrafts(waitingOnly) {
186
+ if (closed) return;
187
+ const value = snapshotRoomDrafts();
188
+ for (const listener of roomDraftListeners){
189
+ if (waitingOnly && listener.ready) continue;
190
+ listener.ready = true;
191
+ listener.notify(value);
192
+ }
193
+ }
194
+ function presentationOf(app) {
195
+ const value = presentations.get(app);
196
+ return value === undefined ? null : structuredClone(value);
197
+ }
198
+ function emitPresentation(app) {
199
+ if (closed) return;
200
+ for (const listener of presentationListeners){
201
+ if (listener.app === app && listener.ready) listener.notify(presentationOf(app));
202
+ }
48
203
  }
49
204
  function pending() {
50
205
  return [
@@ -52,41 +207,73 @@ export function createMdRuntime(options) {
52
207
  ].some((replica)=>replica.queue.length > 0);
53
208
  }
54
209
  function attention() {
55
- return [
56
- ...replicas.values()
57
- ].some((replica)=>{
58
- const head = replica.queue[0];
59
- return head !== undefined && refused(outcomes.get(head.id));
60
- });
61
- }
62
- function storedOutcomes(batches) {
63
- const stored = {};
64
- for (const batch of batches){
65
- const outcome = queuedOutcome(outcomes.get(batch.id));
66
- if (outcome.kind !== 'pending') stored[batch.id] = outcome;
210
+ const attentionRequired = [];
211
+ for (const record of [
212
+ ...roomDrafts.values()
213
+ ].flatMap((room)=>[
214
+ ...room.values()
215
+ ])){
216
+ if (record.returned === null) continue;
217
+ attentionRequired.push({
218
+ room: record.locator,
219
+ path: record.path,
220
+ returned: structuredClone(record.returned)
221
+ });
67
222
  }
68
- return stored;
69
- }
70
- async function persistQueue(local, locator, batches) {
71
- await local.writeQueue({
72
- locator,
73
- batches,
74
- outcomes: storedOutcomes(batches)
223
+ attentionRequired.sort((left, right)=>{
224
+ if (left.room !== right.room) return left.room < right.room ? -1 : 1;
225
+ if (left.path !== right.path) return left.path < right.path ? -1 : 1;
226
+ return 0;
75
227
  });
228
+ let reset = null;
229
+ for (const { submission } of pendings.values()){
230
+ if (submission.state !== 'reset') continue;
231
+ if (reset === null || submission.reset < reset) reset = submission.reset;
232
+ }
233
+ return {
234
+ attentionRequired,
235
+ reset
236
+ };
76
237
  }
77
238
  function listed() {
78
- return [
239
+ return structuredClone([
79
240
  ...rooms.values()
80
- ];
241
+ ]);
81
242
  }
82
- function queryRooms() {
83
- return [
84
- ...replicas.values()
85
- ].map((replica)=>({
86
- room: replica.confirmed.record.locator,
87
- roomVersion: replica.confirmed.record.roomVersion,
88
- documents: replica.visible.collection.documents
89
- }));
243
+ function slotted() {
244
+ return structuredClone(slots.map(({ slot, entries })=>({
245
+ slot,
246
+ entries: entries.flatMap(({ locator, segment })=>{
247
+ const room = rooms.get(locator);
248
+ return room === undefined ? [] : [
249
+ {
250
+ room,
251
+ segment
252
+ }
253
+ ];
254
+ })
255
+ })));
256
+ }
257
+ function roomFacts() {
258
+ const held = [
259
+ ...rooms.values()
260
+ ].map((room)=>{
261
+ const attached = [
262
+ modelOf(room.locator),
263
+ ...room.typedSelection === null ? [] : [
264
+ modelOf(room.locator, 'typed-selection')
265
+ ]
266
+ ];
267
+ return {
268
+ locator: room.locator,
269
+ available: attached.every((model)=>model.status !== 'unavailable'),
270
+ types: attached.flatMap((model)=>model.status === 'unavailable' ? [] : Object.values(model.types))
271
+ };
272
+ });
273
+ return {
274
+ listed: roomsListed,
275
+ rooms: held
276
+ };
90
277
  }
91
278
  function emitRooms() {
92
279
  const value = listed();
@@ -95,33 +282,35 @@ export function createMdRuntime(options) {
95
282
  function emitRefusal() {
96
283
  for (const listener of refusalListeners)listener(refusal);
97
284
  }
285
+ function emitStorageLoss() {
286
+ for (const listener of storageLossListeners)listener(storageLoss);
287
+ }
98
288
  function moveTo(next) {
99
- if (closed || same(status, next)) return;
289
+ if (closed) return;
100
290
  status = next;
101
- for (const listener of statusListeners)listener(status);
102
- }
103
- function visible(request) {
104
- return evaluate(queryRooms(), {
105
- ...request,
106
- selector: {
107
- $and: [
108
- PRESENTED,
109
- request.selector ?? {}
110
- ]
111
- }
112
- });
291
+ show();
113
292
  }
114
- function emitVisible() {
115
- for (const subscription of querySubscriptions){
116
- subscription.notify(visible(subscription.query));
117
- }
293
+ function show() {
294
+ if (closed) return;
295
+ const next = status.state === 'synced' && !conversations.current() ? {
296
+ ...status,
297
+ state: 'syncing'
298
+ } : status;
299
+ if (same(shown, next)) return;
300
+ shown = next;
301
+ for (const listener of statusListeners)listener(shown);
302
+ }
303
+ function submissionOf(batch) {
304
+ return pendings.get(batch.id)?.submission ?? {
305
+ state: 'waiting'
306
+ };
118
307
  }
119
308
  function entriesOf(locator) {
120
309
  const replica = replicas.get(locator);
121
310
  if (replica === undefined) return [];
122
311
  return replica.queue.map((batch)=>Object.freeze({
123
312
  batch,
124
- outcome: queuedOutcome(outcomes.get(batch.id))
313
+ ...submissionOf(batch)
125
314
  }));
126
315
  }
127
316
  function emitQueue(locator) {
@@ -130,6 +319,24 @@ export function createMdRuntime(options) {
130
319
  if (subscription.locator === locator) subscription.notify(entries);
131
320
  }
132
321
  }
322
+ function applyChangeSet(set) {
323
+ readSide.apply(set);
324
+ searchIndex.changed(set);
325
+ }
326
+ function publishChangeSet(set) {
327
+ applyChangeSet(set);
328
+ if (set.rooms.length > 0) emitRooms();
329
+ for (const locator of set.queues)emitQueue(locator);
330
+ if (set.drafts.length > 0 || set.documents.some((change)=>roomDrafts.get(change.locator)?.has(change.path) === true)) {
331
+ publishRoomDrafts(false);
332
+ }
333
+ for (const app of set.presentations)emitPresentation(app);
334
+ conversations.changed(set);
335
+ const cards = declaredCards();
336
+ if (cards !== undefined && set.documents.some((change)=>change.locator === cards.locator)) {
337
+ scheduleCurrency();
338
+ }
339
+ }
133
340
  function stopTimer() {
134
341
  if (timer === undefined) return;
135
342
  clearInterval(timer);
@@ -141,79 +348,311 @@ export function createMdRuntime(options) {
141
348
  void enqueue();
142
349
  }, period);
143
350
  }
144
- function refuse(reason) {
351
+ async function refuse(reason, provider = null) {
145
352
  stopTimer();
146
353
  discoveryPaused = true;
147
- refusal = {
148
- reason,
149
- reauthorizationOffered: reason === 'revoked-authorization'
150
- };
151
- journal.record(`connection refused ${reason}`);
152
- emitRefusal();
354
+ try {
355
+ if (reason === 'revoked-authorization') {
356
+ await exclusive(async ()=>{
357
+ const revoked = {
358
+ status: 'unavailable',
359
+ reason: 'revoked'
360
+ };
361
+ try {
362
+ const local = await openLocal();
363
+ await local.revokeModels([
364
+ ...replicas.values()
365
+ ].map((replica)=>({
366
+ record: replica.confirmed.record,
367
+ queue: replica.queue
368
+ })));
369
+ } catch {
370
+ journal.record('model revocation persistence failed');
371
+ }
372
+ const keys = new Set(models.keys());
373
+ for (const locator of rooms.keys()){
374
+ keys.add(modelKey(locator, 'collection'));
375
+ keys.add(modelKey(locator, 'typed-selection'));
376
+ }
377
+ for (const key of keys){
378
+ invalidModels.add(key);
379
+ modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
380
+ models.set(key, revoked);
381
+ liveModels.delete(key);
382
+ }
383
+ const changed = [];
384
+ for (const [locator, room] of rooms){
385
+ const before = replicas.get(locator) ?? null;
386
+ if (before !== null) {
387
+ const record = {
388
+ ...before.confirmed.record,
389
+ model: revoked,
390
+ selectionModel: revoked
391
+ };
392
+ const read = readConfirmed(record, before.confirmed);
393
+ replicas.set(locator, read.kind === 'staged' ? replicaOf(read.confirmed, before.queue, rewriting(), before) : {
394
+ ...before,
395
+ confirmed: {
396
+ ...before.confirmed,
397
+ record
398
+ }
399
+ });
400
+ }
401
+ rooms.set(locator, {
402
+ ...room,
403
+ model: modelTypes(revoked),
404
+ typedSelection: room.typedSelection === null ? null : {
405
+ ...room.typedSelection,
406
+ model: modelTypes(revoked)
407
+ }
408
+ });
409
+ changed.push(changeSet(locator, before, replicas.get(locator) ?? null, {
410
+ room: true
411
+ }));
412
+ }
413
+ publishChangeSet(mergeChangeSets(changed));
414
+ });
415
+ }
416
+ } finally{
417
+ refusal = {
418
+ reason,
419
+ reauthorizationOffered: reason === 'revoked-authorization',
420
+ refusal: provider
421
+ };
422
+ journal.record(`connection refused ${reason}`);
423
+ emitRefusal();
424
+ }
153
425
  }
154
426
  function list(room, synchronizationAvailable) {
427
+ const selection = room.typedSelection ?? null;
155
428
  rooms.set(room.locator, {
156
429
  name: room.name,
430
+ collectionRoot: room.collectionRoot,
157
431
  locator: room.locator,
158
- writeGrants: room.writeGrants,
432
+ operations: room.operations,
433
+ replica: room.replica,
434
+ attachedModel: room.attachedModel,
435
+ model: modelTypes(modelOf(room.locator)),
436
+ typedSelection: selection === null ? null : {
437
+ roots: selection.roots,
438
+ attachedModel: selection.attachedModel,
439
+ model: modelTypes(modelOf(room.locator, 'typed-selection'))
440
+ },
159
441
  providerOwnedPaths: room.providerOwnedPaths,
160
442
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
161
- synchronizationAvailable
443
+ synchronizationAvailable,
444
+ refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null
162
445
  });
163
446
  }
164
- function applyDiscovery(discovered) {
165
- const listedLocators = new Set(discovered.map((room)=>room.locator));
166
- for (const room of discovered){
167
- list(room, !unreadable.has(room.locator));
447
+ async function applyDiscovery(listed, local) {
448
+ const listedLocators = new Set(listed.map((room)=>room.locator));
449
+ for (const locator of listedLocators)scopedRefusals.delete(locator);
450
+ discovered.clear();
451
+ for (const room of listed)discovered.set(room.locator, room);
452
+ await dropUnlisted(listedLocators, local);
453
+ for (const room of listed){
454
+ const held = replicas.get(room.locator)?.confirmed.record;
455
+ if (held !== undefined && held.replica.selection !== room.replica.selection) {
456
+ list(held, false);
457
+ continue;
458
+ }
459
+ list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
168
460
  }
169
- for (const [locator, room] of rooms){
170
- if (listedLocators.has(locator)) continue;
171
- rooms.set(locator, {
172
- ...room,
173
- synchronizationAvailable: false
461
+ roomsListed = true;
462
+ emitRooms();
463
+ }
464
+ function unlistedHeld(listed) {
465
+ return [
466
+ ...new Set([
467
+ ...rooms.keys(),
468
+ ...roomDrafts.keys()
469
+ ])
470
+ ].filter((locator)=>!listed.has(locator));
471
+ }
472
+ async function dropUnlisted(listed, local) {
473
+ if (unlistedHeld(listed).length === 0) return;
474
+ await exclusive(async ()=>{
475
+ const unlisted = unlistedHeld(listed);
476
+ await local.dropRooms(unlisted);
477
+ for (const [batch, locator] of outcomeRooms){
478
+ if (listed.has(locator)) continue;
479
+ outcomeRooms.delete(batch);
480
+ outcomes.delete(batch);
481
+ }
482
+ const removed = unlisted.map((locator)=>changeSet(locator, replicas.get(locator) ?? null, null, {
483
+ room: true,
484
+ queue: true,
485
+ drafts: [
486
+ ...roomDrafts.get(locator)?.keys() ?? []
487
+ ]
488
+ }));
489
+ for (const locator of unlisted){
490
+ for (const batch of replicas.get(locator)?.queue ?? [])pendings.delete(batch.id);
491
+ replicas.delete(locator);
492
+ rooms.delete(locator);
493
+ roomDrafts.delete(locator);
494
+ unreadable.delete(locator);
495
+ scopedRefusals.delete(locator);
496
+ sourceRefusals.delete(locator);
497
+ declarationRefusals.delete(locator);
498
+ for (const attached of [
499
+ 'collection',
500
+ 'typed-selection'
501
+ ]){
502
+ const key = modelKey(locator, attached);
503
+ models.delete(key);
504
+ liveModels.delete(key);
505
+ invalidModels.delete(key);
506
+ modelEpoch.delete(key);
507
+ }
508
+ journal.record(`room unlisted ${locator}`);
509
+ }
510
+ publishChangeSet(mergeChangeSets(removed));
511
+ moveTo({
512
+ ...status,
513
+ ...attention()
174
514
  });
515
+ });
516
+ }
517
+ function restored(key, persisted) {
518
+ const prior = models.get(key);
519
+ if (prior !== undefined && prior.status !== 'unavailable' && (persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
520
+ modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
521
+ invalidModels.add(key);
175
522
  }
176
- emitRooms();
177
- return listedLocators;
523
+ const model = persisted.status === 'unavailable' || liveModels.get(key) === JSON.stringify(persisted.snapshot) ? persisted : cached(persisted);
524
+ models.set(key, model);
525
+ if (model.status === 'unavailable') {
526
+ liveModels.delete(key);
527
+ invalidModels.delete(key);
528
+ }
529
+ return model;
178
530
  }
179
- async function open() {
180
- const opened = await openReplicaStore();
181
- const [records, queues, kept, lastCompleted, declared] = await Promise.all([
531
+ async function hydrate(opened) {
532
+ const [records, queues, recordedOutcomes, kept, presented, held, chatDrafts, lastCompleted, declared, named, listedSlots] = await Promise.all([
182
533
  opened.all(),
183
534
  opened.queues(),
184
- opened.drafts(),
535
+ opened.outcomes(),
536
+ opened.roomDrafts(),
537
+ opened.presentation(),
538
+ opened.conversations(),
539
+ opened.chatDrafts(),
185
540
  opened.lastCompleted(),
186
- opened.providerLimits()
541
+ opened.providerLimits(),
542
+ opened.capabilities(),
543
+ opened.slots()
187
544
  ]);
188
- store = opened;
189
545
  limits = declared;
190
- for (const draft of kept)drafts.set(at(draft.locator, draft.path), draft.content);
191
- const queued = new Map(queues.map((queue)=>[
546
+ capabilities = named;
547
+ slots = listedSlots;
548
+ roomDrafts.clear();
549
+ for (const record of kept)rememberRoomDraft(record);
550
+ presentations.clear();
551
+ for (const record of presented)presentations.set(record.app, record.value);
552
+ conversations.hydrate(held, chatDrafts);
553
+ const queuedBatches = new Map(queues.map((queue)=>[
192
554
  queue.locator,
193
- queue.batches
555
+ queue.pending.map(({ batch })=>batch)
194
556
  ]));
557
+ pendings.clear();
195
558
  for (const queue of queues){
196
- for (const [id, outcome] of Object.entries(queue.outcomes)){
197
- outcomes.set(id, outcome);
198
- }
559
+ for (const { batch, state } of queue.pending)pendings.set(batch.id, state);
560
+ }
561
+ outcomes.clear();
562
+ outcomeRooms.clear();
563
+ for (const record of recordedOutcomes){
564
+ outcomes.set(record.id, record.outcome);
565
+ outcomeRooms.set(record.id, record.locator);
199
566
  }
567
+ const previousReplicas = new Map(replicas);
568
+ replicas.clear();
200
569
  for (const record of records){
201
- const confirmed = readConfirmed(record);
202
- if (confirmed === null) continue;
203
- replicas.set(record.locator, replicaOf(confirmed, queued.get(record.locator) ?? []));
204
- list(record, false);
570
+ const model = restored(modelKey(record.locator, 'collection'), record.model);
571
+ const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
572
+ const prior = previousReplicas.get(record.locator);
573
+ const retyped = {
574
+ ...record,
575
+ model,
576
+ selectionModel
577
+ };
578
+ const read = readConfirmed(retyped, prior?.confirmed);
579
+ if (read.kind === 'unconfirmed') continue;
580
+ const confirmed = read.kind === 'staged' ? read.confirmed : {
581
+ record: retyped,
582
+ collection: parseCollection(retyped.documents, compositionOf(retyped))
583
+ };
584
+ replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], rewriting(), prior));
585
+ const existing = rooms.get(record.locator);
586
+ if (existing === undefined) list(record, false);
587
+ else {
588
+ rooms.set(record.locator, {
589
+ ...existing,
590
+ roomVersion: record.roomVersion,
591
+ operations: record.operations,
592
+ replica: record.replica,
593
+ attachedModel: record.attachedModel,
594
+ model: modelTypes(model),
595
+ typedSelection: record.typedSelection === null ? null : {
596
+ roots: record.typedSelection.roots,
597
+ attachedModel: record.typedSelection.attachedModel,
598
+ model: modelTypes(selectionModel)
599
+ }
600
+ });
601
+ }
602
+ if (read.kind === 'refused') {
603
+ refusedSource(record, read);
604
+ declarationRefusals.add(record.locator);
605
+ }
606
+ }
607
+ return {
608
+ lastCompleted,
609
+ locators: new Set([
610
+ ...queuedBatches.keys(),
611
+ ...replicas.keys()
612
+ ]),
613
+ read: mergeChangeSets([
614
+ ...new Set([
615
+ ...previousReplicas.keys(),
616
+ ...replicas.keys()
617
+ ])
618
+ ].map((locator)=>changeSet(locator, previousReplicas.get(locator) ?? null, replicas.get(locator) ?? null)))
619
+ };
620
+ }
621
+ async function readState(opened) {
622
+ const { lastCompleted, locators, read } = await hydrate(opened);
623
+ applyChangeSet(read);
624
+ if (replicas.size > 0) emitRooms();
625
+ for (const locator of locators)emitQueue(locator);
626
+ publishRoomDrafts(false);
627
+ return lastCompleted;
628
+ }
629
+ async function open() {
630
+ const opened = await openRuntimeStore();
631
+ store = opened;
632
+ const kept = options.shell.generation;
633
+ const storedGeneration = await opened.generation();
634
+ const loss = detectStorageLoss(storedGeneration, kept);
635
+ const generation = nextGeneration(storedGeneration, kept);
636
+ if (generation !== storedGeneration) await opened.rememberGeneration(generation);
637
+ if (generation !== kept) await options.shell.keepGeneration(generation);
638
+ if (loss !== null) {
639
+ storageLoss = loss;
640
+ emitStorageLoss();
205
641
  }
642
+ const lastCompleted = await readState(opened);
206
643
  moveTo({
207
644
  ...status,
208
645
  lastCompleted,
209
- attentionRequired: attention()
646
+ ...attention()
210
647
  });
211
- if (replicas.size > 0) {
212
- emitRooms();
213
- emitVisible();
214
- }
215
648
  return opened;
216
649
  }
650
+ function durably(work) {
651
+ return exclusive(async ()=>{
652
+ const local = await openLocal();
653
+ return work(local);
654
+ });
655
+ }
217
656
  function openLocal() {
218
657
  opening ??= open().catch((error)=>{
219
658
  opening = undefined;
@@ -226,93 +665,228 @@ export function createMdRuntime(options) {
226
665
  mutations = next.then(()=>undefined, ()=>undefined);
227
666
  return next;
228
667
  }
668
+ function withdrawable(batch) {
669
+ const state = pendings.get(batch.id);
670
+ return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
671
+ }
672
+ async function settle(local, locator, before, confirmed, queue, settling = {}) {
673
+ const withdrawn = withdrawals(confirmed, queue, withdrawable, rewriting());
674
+ const kept = queue.filter((batch)=>!withdrawn.includes(batch));
675
+ const returning = [
676
+ ...withdrawn.map((batch)=>({
677
+ batch,
678
+ returned: {
679
+ kind: 'withdrawn'
680
+ }
681
+ })).reverse(),
682
+ ...settling.leaving === undefined ? [] : [
683
+ settling.leaving
684
+ ]
685
+ ];
686
+ const written = new Map();
687
+ for (const { batch, returned } of returning){
688
+ if (returned === null) continue;
689
+ for (const saved of pendings.get(batch.id)?.drafts ?? []){
690
+ const existing = written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path);
691
+ written.set(saved.path, {
692
+ locator,
693
+ path: saved.path,
694
+ revision: roomDraftRevision(),
695
+ parts: returnedInto(existing?.parts, saved.parts),
696
+ returned
697
+ });
698
+ }
699
+ }
700
+ const settledOutcomes = [
701
+ ...settling.accepted === undefined ? [] : [
702
+ settling.accepted
703
+ ],
704
+ ...settling.leaving === undefined ? [] : [
705
+ {
706
+ id: settling.leaving.batch.id,
707
+ outcome: settling.leaving.outcome
708
+ }
709
+ ],
710
+ ...withdrawn.map((batch)=>({
711
+ id: batch.id,
712
+ outcome: {
713
+ kind: 'withdrawn'
714
+ }
715
+ }))
716
+ ];
717
+ await local.write({
718
+ next: confirmed.record,
719
+ previous: before?.confirmed.record ?? null,
720
+ queue: kept,
721
+ previousQueue: before?.queue ?? [],
722
+ outcomes: settledOutcomes,
723
+ roomDrafts: [
724
+ ...written.values()
725
+ ]
726
+ });
727
+ for (const { id, outcome } of settledOutcomes){
728
+ outcomes.set(id, outcome);
729
+ outcomeRooms.set(id, locator);
730
+ pendings.delete(id);
731
+ }
732
+ for (const record of written.values())rememberRoomDraft(record);
733
+ for (const batch of withdrawn)journal.record(`outcome ${locator} ${batch.id} withdrawn`);
734
+ replicas.set(locator, replicaOf(confirmed, kept, rewriting(), before));
735
+ return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
736
+ room: settling.room,
737
+ queue: kept.length !== (before?.queue.length ?? 0),
738
+ drafts: [
739
+ ...written.keys()
740
+ ]
741
+ });
742
+ }
229
743
  function publish(local, room, confirmed) {
230
744
  return exclusive(async ()=>{
231
- await local.publish(confirmed.record);
745
+ if (!continuing()) return;
746
+ const prior = replicas.get(room.locator);
747
+ const retained = readConfirmed({
748
+ ...confirmed.record,
749
+ model: modelOf(room.locator),
750
+ selectionModel: modelOf(room.locator, 'typed-selection')
751
+ }, prior?.confirmed);
752
+ if (retained.kind !== 'staged') throw new Error(`Cannot retype staged Room ${room.locator}`);
753
+ const queue = prior?.queue ?? [];
754
+ const accepted = queue.filter((batch)=>outcomes.get(batch.id)?.kind === 'accepted');
755
+ const set = await settle(local, room.locator, prior, retained.confirmed, queue.filter((batch)=>!accepted.includes(batch)), {
756
+ room: true
757
+ });
758
+ for (const batch of accepted)pendings.delete(batch.id);
232
759
  unreadable.delete(room.locator);
233
- replicas.set(room.locator, replicaOf(confirmed, replicas.get(room.locator)?.queue ?? []));
760
+ sourceRefusals.delete(room.locator);
761
+ declarationRefusals.delete(room.locator);
234
762
  list(room, true);
235
- emitRooms();
236
- emitVisible();
763
+ publishChangeSet(set);
764
+ moveTo({
765
+ ...status,
766
+ ...attention()
767
+ });
237
768
  });
238
769
  }
239
- async function write(locator, changes, replacing) {
770
+ async function saveRoomDrafts(locator, named, writtenBy) {
771
+ const local = await openLocal();
240
772
  const replica = replicas.get(locator);
241
773
  const room = rooms.get(locator);
242
774
  if (replica === undefined || room === undefined) {
243
775
  throw new Error(`No Local Replica for ${locator}`);
244
776
  }
777
+ if (mismatchedRoots.has(locator)) return {
778
+ outcome: 'refused',
779
+ rule: 'collection-root-mismatch'
780
+ };
781
+ const saved = [];
782
+ for (const { path, revision } of named){
783
+ const record = roomDrafts.get(locator)?.get(path);
784
+ if (record === undefined || record.revision !== revision) {
785
+ return {
786
+ outcome: 'refused',
787
+ rule: 'stale',
788
+ file: path
789
+ };
790
+ }
791
+ if (!saved.some((draft)=>draft.path === path)) saved.push({
792
+ path,
793
+ parts: record.parts
794
+ });
795
+ }
796
+ const changed = saved.flatMap(({ path, parts })=>changedParts(path, underneath(parts, path, replica.visible)));
797
+ if (changed.length > 0) return {
798
+ outcome: 'refused',
799
+ rule: 'changed-underneath',
800
+ parts: changed
801
+ };
802
+ const parts = saved.map(({ path, parts })=>changesOf(path, parts));
245
803
  const built = buildBatch({
246
804
  baseRoomVersion: replica.confirmed.record.roomVersion,
247
- writeGrants: room.writeGrants,
805
+ composition: compositionFor(replica.confirmed.record),
806
+ operations: room.operations,
248
807
  providerOwnedPaths: room.providerOwnedPaths,
249
808
  providerLimits: limits,
809
+ rewriting: rewriting(),
250
810
  visible: replica.visible,
251
- changes
811
+ changes: parts.flatMap(({ changes })=>changes),
812
+ bases: parts.flatMap(({ bases })=>bases),
813
+ writtenBy
252
814
  });
253
815
  if (built.kind === 'refused') return {
254
816
  outcome: 'refused',
255
817
  ...built.refusal
256
818
  };
257
- const head = replica.queue[0];
258
- if (replacing !== undefined) {
259
- if (head === undefined || head.id !== replacing || !refused(outcomes.get(head.id))) {
260
- return {
261
- outcome: 'refused',
262
- rule: 'no-refused-head'
263
- };
264
- }
265
- }
266
- const local = await openLocal();
267
- const batches = replacing === undefined ? [
819
+ const batches = [
268
820
  ...replica.queue,
269
821
  built.batch
270
- ] : [
271
- built.batch,
272
- ...replica.queue.slice(1)
273
822
  ];
274
- if (replacing !== undefined) outcomes.delete(replacing);
275
- const consumed = [
276
- ...new Set(changes.map((change)=>change.path))
277
- ];
278
- await local.append({
279
- locator,
280
- batches,
281
- outcomes: storedOutcomes(batches)
282
- }, consumed);
283
- for (const path of consumed)drafts.delete(at(locator, path));
284
- replicas.set(locator, replicaOf(replica.confirmed, batches));
285
- emitQueue(locator);
286
- emitVisible();
823
+ const consumed = saved.map(({ path })=>path);
824
+ try {
825
+ await local.write({
826
+ next: replica.confirmed.record,
827
+ previous: replica.confirmed.record,
828
+ queue: batches,
829
+ previousQueue: replica.queue,
830
+ saved,
831
+ consumedRoomDrafts: consumed
832
+ });
833
+ } catch {
834
+ return {
835
+ outcome: 'refused',
836
+ rule: 'durable-write'
837
+ };
838
+ }
839
+ pendings.set(built.batch.id, {
840
+ drafts: saved,
841
+ submission: {
842
+ state: 'waiting'
843
+ },
844
+ lost: false
845
+ });
846
+ for (const path of consumed)forgetRoomDraft(locator, path);
847
+ replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting(), replica));
848
+ publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
849
+ queue: true,
850
+ drafts: consumed
851
+ }));
287
852
  moveTo({
288
853
  ...saving(status),
289
- attentionRequired: attention()
854
+ ...attention()
290
855
  });
291
856
  void enqueue();
292
857
  return {
293
858
  outcome: 'saved',
294
- id: built.batch.id
859
+ identifier: built.batch.id
295
860
  };
296
861
  }
297
- async function discardHead(locator, identifier) {
298
- const replica = replicas.get(locator);
299
- const head = replica?.queue[0];
300
- if (replica === undefined || head === undefined || head.id !== identifier || !refused(outcomes.get(head.id))) {
862
+ async function mark(local, locator, identifier, submission, lost) {
863
+ const state = pendings.get(identifier);
864
+ if (!continuing() || state === undefined || JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost) {
301
865
  return;
302
866
  }
303
- const local = await openLocal();
304
- const batches = replica.queue.slice(1);
305
- outcomes.delete(identifier);
306
- await persistQueue(local, locator, batches);
307
- replicas.set(locator, replicaOf(replica.confirmed, batches));
308
- emitQueue(locator);
309
- emitVisible();
867
+ await local.submit(locator, identifier, submission, lost);
868
+ pendings.set(identifier, {
869
+ ...state,
870
+ submission,
871
+ lost
872
+ });
873
+ const replica = replicas.get(locator);
874
+ if (replica !== undefined) publishChangeSet(changeSet(locator, replica, replica, {
875
+ queue: true
876
+ }));
310
877
  moveTo({
311
878
  ...status,
312
- state: pending() ? 'pending' : status.state,
313
- attentionRequired: attention()
879
+ ...attention()
314
880
  });
315
- void enqueue();
881
+ }
882
+ async function keepUpload(local, locator, identifier, upload) {
883
+ if (!continuing()) throw new Error('connection refused');
884
+ await local.keepUpload(locator, identifier, upload);
885
+ pendings.set(identifier, {
886
+ ...pendings.get(identifier),
887
+ upload
888
+ });
889
+ if (!continuing()) throw new Error('connection refused');
316
890
  }
317
891
  async function readHistory(locator, path, query = {}) {
318
892
  if (closed || refusal !== null) return {
@@ -351,7 +925,11 @@ export function createMdRuntime(options) {
351
925
  }
352
926
  });
353
927
  journal.record(`outcome history ${result.kind}`);
354
- if (result.kind === 'revoked-authorization') refuse('revoked-authorization');
928
+ if (result.kind === 'scoped-refusal') {
929
+ const room = replicas.get(result.locator)?.confirmed.record;
930
+ if (room !== undefined) await scoped(result, room, await openLocal());
931
+ }
932
+ if (result.kind === 'revoked-authorization') await refuse('revoked-authorization');
355
933
  return result;
356
934
  } catch {
357
935
  journal.record('outcome history transport-uncertainty');
@@ -360,6 +938,12 @@ export function createMdRuntime(options) {
360
938
  };
361
939
  }
362
940
  }
941
+ function declaredCards() {
942
+ for (const capability of capabilities){
943
+ if (capability.conversationCards !== undefined) return capability.conversationCards;
944
+ }
945
+ return undefined;
946
+ }
363
947
  function declaredAction(name) {
364
948
  for (const capability of capabilities){
365
949
  for (const action of capability.actions ?? []){
@@ -394,9 +978,13 @@ export function createMdRuntime(options) {
394
978
  name,
395
979
  input
396
980
  });
397
- if (result.kind === 'revoked-authorization') refuse('revoked-authorization');
981
+ if (result.kind === 'scoped-refusal') {
982
+ const room = replicas.get(result.locator)?.confirmed.record;
983
+ if (room !== undefined) await scoped(result, room, await openLocal());
984
+ }
985
+ if (result.kind === 'revoked-authorization') await refuse('revoked-authorization');
398
986
  journal.record(`outcome action ${result.kind}`);
399
- if (result.kind === 'rejected') journal.record(`refusal rejected ${result.text}`);
987
+ if (result.kind === 'rejected') journal.record(`refusal rejected ${result.code}`);
400
988
  return result;
401
989
  } catch {
402
990
  journal.record('outcome action transport-uncertainty');
@@ -405,9 +993,13 @@ export function createMdRuntime(options) {
405
993
  };
406
994
  }
407
995
  }
996
+ function continuing() {
997
+ return !closed && !discoveryPaused;
998
+ }
408
999
  async function pullRefresh(authorized, confirmed) {
1000
+ if (!continuing()) return unconfirmed;
409
1001
  try {
410
- const result = await refreshRoom(options.adapter, authorized, confirmed);
1002
+ const result = await refreshRoom(options.adapter, authorized, confirmed, continuing);
411
1003
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
412
1004
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
413
1005
  return result;
@@ -417,8 +1009,12 @@ export function createMdRuntime(options) {
417
1009
  }
418
1010
  }
419
1011
  async function pullBootstrap(authorized, room) {
1012
+ if (!continuing()) return unconfirmed;
420
1013
  try {
421
- const result = await bootstrapRoom(options.adapter, authorized, room);
1014
+ const result = await bootstrapRoom(options.adapter, authorized, room, {
1015
+ collection: modelOf(room.locator),
1016
+ selection: modelOf(room.locator, 'typed-selection')
1017
+ }, continuing);
422
1018
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
423
1019
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
424
1020
  return result;
@@ -431,7 +1027,9 @@ export function createMdRuntime(options) {
431
1027
  const held = replicas.get(room.locator);
432
1028
  if (held !== undefined) {
433
1029
  const refreshed = await pullRefresh(authorized, held.confirmed);
1030
+ if (!continuing()) return unconfirmed;
434
1031
  if (refreshed.kind === 'still-current') {
1032
+ if (sourceRefusals.has(room.locator)) return unconfirmed;
435
1033
  if (unreadable.delete(room.locator)) {
436
1034
  list(room, true);
437
1035
  emitRooms();
@@ -439,151 +1037,352 @@ export function createMdRuntime(options) {
439
1037
  return null;
440
1038
  }
441
1039
  if (refreshed.kind === 'advanced') {
442
- if (closed) return unconfirmed;
443
1040
  await publish(local, room, refreshed.confirmed);
444
1041
  return null;
445
1042
  }
446
1043
  if (refreshed.kind !== 'beyond-horizon') return refreshed;
447
1044
  }
448
1045
  const bootstrapped = await pullBootstrap(authorized, room);
1046
+ if (!continuing()) return unconfirmed;
449
1047
  if (bootstrapped.kind !== 'staged') return bootstrapped;
450
- if (closed) return unconfirmed;
451
1048
  await publish(local, room, bootstrapped.confirmed);
452
1049
  return null;
453
1050
  }
454
- function recordRefusal(result) {
455
- if (result.kind === 'expired') return {
456
- kind: 'expired'
1051
+ async function withhold(withheld, room, local, progress) {
1052
+ progress.complete = false;
1053
+ if (withheld.kind === 'scoped-refusal') {
1054
+ await scoped(withheld, room, local);
1055
+ } else if (withheld.kind === 'transport-uncertainty') {
1056
+ progress.reachable = false;
1057
+ } else if (withheld.kind === 'room-not-readable') {
1058
+ unreadable.add(room.locator);
1059
+ list(room, false);
1060
+ emitRooms();
1061
+ } else if (withheld.kind === 'refused') {
1062
+ refusedSource(room, withheld);
1063
+ } else if (withheld.kind === 'revoked-authorization') {
1064
+ await refuse('revoked-authorization');
1065
+ }
1066
+ }
1067
+ function refusedSource(room, { refusal }) {
1068
+ sourceRefusals.set(room.locator, {
1069
+ code: refusal.rule,
1070
+ fields: {
1071
+ file: refusal.file,
1072
+ ...refusal.location === undefined ? {} : {
1073
+ location: refusal.location
1074
+ },
1075
+ version: refusal.version,
1076
+ ...refusal.referrers === undefined ? {} : {
1077
+ referrers: refusal.referrers
1078
+ }
1079
+ }
1080
+ });
1081
+ declarationRefusals.delete(room.locator);
1082
+ unreadable.add(room.locator);
1083
+ list(room, false);
1084
+ emitRooms();
1085
+ journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
1086
+ }
1087
+ function leaving(batch, result) {
1088
+ if (result.kind === 'expired') {
1089
+ return {
1090
+ batch,
1091
+ outcome: {
1092
+ kind: 'expired'
1093
+ },
1094
+ returned: {
1095
+ kind: 'expired'
1096
+ }
1097
+ };
1098
+ }
1099
+ const refusal = {
1100
+ code: result.code,
1101
+ fields: result.fields
457
1102
  };
458
1103
  if (result.kind === 'conflicted') {
459
1104
  return {
460
- kind: 'conflicted',
461
- roomVersion: result.roomVersion,
462
- affected: result.affected,
463
- text: result.text
1105
+ batch,
1106
+ outcome: {
1107
+ kind: 'conflicted',
1108
+ ...result.roomVersion === undefined ? {} : {
1109
+ roomVersion: result.roomVersion
1110
+ },
1111
+ affected: result.affected,
1112
+ ...refusal
1113
+ },
1114
+ returned: {
1115
+ kind: 'conflicted',
1116
+ ...refusal
1117
+ }
464
1118
  };
465
1119
  }
466
1120
  return {
467
- kind: 'rejected',
468
- affected: result.affected,
469
- text: result.text
1121
+ batch,
1122
+ outcome: {
1123
+ kind: 'rejected',
1124
+ affected: result.affected,
1125
+ ...refusal
1126
+ },
1127
+ returned: {
1128
+ kind: 'rejected',
1129
+ ...refusal
1130
+ }
1131
+ };
1132
+ }
1133
+ function acceptedOf(accepted) {
1134
+ return {
1135
+ kind: 'accepted',
1136
+ roomVersion: accepted.roomVersion,
1137
+ ...accepted.author === undefined ? {} : {
1138
+ author: accepted.author
1139
+ },
1140
+ ...accepted.time === undefined ? {} : {
1141
+ time: accepted.time
1142
+ }
470
1143
  };
471
1144
  }
1145
+ function logAccepted(locator, submitted, accepted) {
1146
+ journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` + (accepted.author === undefined ? '' : ` author ${accepted.author}`) + (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1147
+ }
1148
+ async function holdAccepted(room, local, submitted, accepted, refused) {
1149
+ await exclusive(async ()=>{
1150
+ if (!continuing()) return;
1151
+ const current = replicas.get(room.locator);
1152
+ if (current === undefined || !current.queue.some((batch)=>batch.id === submitted.id)) {
1153
+ return;
1154
+ }
1155
+ const outcome = acceptedOf(accepted);
1156
+ await local.write({
1157
+ next: current.confirmed.record,
1158
+ previous: current.confirmed.record,
1159
+ queue: current.queue,
1160
+ previousQueue: current.queue,
1161
+ outcomes: [
1162
+ {
1163
+ id: submitted.id,
1164
+ outcome
1165
+ }
1166
+ ]
1167
+ });
1168
+ outcomes.set(submitted.id, outcome);
1169
+ outcomeRooms.set(submitted.id, room.locator);
1170
+ logAccepted(room.locator, submitted, accepted);
1171
+ });
1172
+ if (refused !== null && continuing()) refusedSource(room, refused);
1173
+ }
472
1174
  async function commitAccept(room, local, submitted, accepted, base, overlay) {
1175
+ if (!continuing()) return unconfirmed;
473
1176
  const current = replicas.get(room.locator);
474
1177
  if (current === undefined || !current.queue.some((batch)=>batch.id === submitted.id)) {
475
- return true;
1178
+ return null;
476
1179
  }
477
1180
  const remaining = current.queue.filter((batch)=>batch.id !== submitted.id);
478
- const applied = overlay ? applyReturned(base, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion) : base;
479
- if (applied === null) return false;
480
- await local.publish(applied.record);
481
- await persistQueue(local, room.locator, remaining);
482
- outcomes.set(submitted.id, {
483
- kind: 'accepted',
484
- roomVersion: accepted.roomVersion,
485
- author: accepted.author,
486
- time: accepted.time
1181
+ const retainedBase = readConfirmed({
1182
+ ...base.record,
1183
+ model: modelOf(room.locator),
1184
+ selectionModel: modelOf(room.locator, 'typed-selection')
1185
+ }, base);
1186
+ const applied = overlay && retainedBase.kind === 'staged' ? applyReturned(retainedBase.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion) : retainedBase;
1187
+ if (applied.kind !== 'staged') return applied;
1188
+ const set = await settle(local, room.locator, current, applied.confirmed, remaining, {
1189
+ accepted: {
1190
+ id: submitted.id,
1191
+ outcome: acceptedOf(accepted)
1192
+ },
1193
+ room: true
487
1194
  });
488
1195
  unreadable.delete(room.locator);
489
- replicas.set(room.locator, replicaOf(applied, remaining));
1196
+ sourceRefusals.delete(room.locator);
1197
+ declarationRefusals.delete(room.locator);
490
1198
  list(room, true);
491
- emitRooms();
492
- emitQueue(room.locator);
493
- emitVisible();
494
- journal.record(`outcome ${room.locator} ${submitted.id} accepted`);
495
- return true;
1199
+ publishChangeSet(set);
1200
+ moveTo({
1201
+ ...status,
1202
+ ...attention()
1203
+ });
1204
+ logAccepted(room.locator, submitted, accepted);
1205
+ return null;
1206
+ }
1207
+ async function committed(room, local, submitted, accepted, base, overlay) {
1208
+ const failed = await exclusive(()=>commitAccept(room, local, submitted, accepted, base, overlay));
1209
+ if (failed === null) return true;
1210
+ await holdAccepted(room, local, submitted, accepted, failed.kind === 'refused' ? failed : null);
1211
+ return false;
496
1212
  }
497
1213
  async function submitRoom(room, local, authorized, progress) {
498
- while(!closed && authorization !== undefined){
1214
+ while(continuing() && authorization !== undefined){
499
1215
  const replica = replicas.get(room.locator);
500
1216
  const head = replica?.queue[0];
501
- if (replica === undefined || head === undefined || refused(outcomes.get(head.id))) {
502
- return null;
1217
+ if (replica === undefined || head === undefined) return;
1218
+ if (outcomes.get(head.id)?.kind === 'accepted') {
1219
+ progress.complete = false;
1220
+ return;
503
1221
  }
1222
+ const state = pendings.get(head.id);
1223
+ const { submission, lost: earlier } = state ?? {
1224
+ submission: {
1225
+ state: 'waiting'
1226
+ },
1227
+ lost: false
1228
+ };
1229
+ if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
1230
+ progress.complete = false;
1231
+ return;
1232
+ }
1233
+ if (writesHeld()) {
1234
+ progress.complete = false;
1235
+ journal.record(`submission ${room.locator} ${head.id} held`);
1236
+ return;
1237
+ }
1238
+ const lost = earlier || submission.state === 'submitted';
504
1239
  let result;
505
1240
  journal.record(`submission ${room.locator} ${head.id}`);
1241
+ await exclusive(()=>mark(local, room.locator, head.id, {
1242
+ state: 'submitted'
1243
+ }, lost));
1244
+ if (discoveryPaused) return;
1245
+ if (writesHeld()) {
1246
+ await exclusive(()=>mark(local, room.locator, head.id, submission, earlier));
1247
+ progress.complete = false;
1248
+ journal.record(`submission ${room.locator} ${head.id} held`);
1249
+ return;
1250
+ }
1251
+ const opensBefore = openSignals;
506
1252
  try {
507
1253
  result = await options.adapter.mutate(authorized, {
508
1254
  locator: room.locator,
509
- changeBatch: head
510
- });
1255
+ changeBatch: head,
1256
+ ...state?.upload === undefined ? {} : {
1257
+ upload: state.upload
1258
+ }
1259
+ }, (upload)=>exclusive(()=>keepUpload(local, room.locator, head.id, upload)));
511
1260
  } catch {
512
1261
  progress.complete = false;
513
- progress.reachable = false;
514
- return null;
1262
+ if (continuing()) progress.reachable = false;
1263
+ return;
515
1264
  }
516
- if (closed) return null;
1265
+ if (!continuing()) return;
517
1266
  if (result.kind === 'transport-uncertainty') {
518
1267
  progress.complete = false;
519
1268
  progress.reachable = false;
520
- return null;
1269
+ return;
521
1270
  }
522
1271
  if (result.kind === 'room-not-readable') {
523
1272
  progress.complete = false;
524
1273
  unreadable.add(room.locator);
525
1274
  list(room, false);
526
1275
  emitRooms();
527
- return null;
1276
+ return;
528
1277
  }
529
1278
  if (result.kind === 'revoked-authorization') {
530
- refuse('revoked-authorization');
531
- return 'stop';
1279
+ await refuse('revoked-authorization');
1280
+ return;
1281
+ }
1282
+ if (result.kind === 'scoped-refusal') {
1283
+ await scoped(result, room, local);
1284
+ progress.complete = false;
1285
+ return;
1286
+ }
1287
+ if (result.kind === 'write-halted') {
1288
+ journal.record(`outcome ${room.locator} ${head.id} write-halted`);
1289
+ await exclusive(()=>mark(local, room.locator, head.id, {
1290
+ state: 'waiting'
1291
+ }, lost));
1292
+ if (serviceMode === 'open' && openSignals !== opensBefore) continue;
1293
+ haltedRefusal = true;
1294
+ progress.complete = false;
1295
+ return;
1296
+ }
1297
+ if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired' || result.kind === 'retryable-refusal') {
1298
+ if (result.kind === 'expired') {
1299
+ const withheld = await advance(room, local, authorized);
1300
+ if (withheld !== null) return withhold(withheld, room, local, progress);
1301
+ }
1302
+ journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` + (result.kind === 'conflicted' && result.roomVersion !== undefined ? ` ${result.roomVersion}` : ''));
1303
+ if (result.kind !== 'expired') journal.record(`refusal ${result.kind} ${result.code}`);
1304
+ }
1305
+ if (result.kind === 'retryable-refusal') {
1306
+ const reset = result.reset;
1307
+ progress.complete = false;
1308
+ await exclusive(()=>mark(local, room.locator, head.id, reset === undefined ? {
1309
+ state: 'waiting'
1310
+ } : {
1311
+ state: 'reset',
1312
+ reset
1313
+ }, lost));
1314
+ return;
532
1315
  }
533
1316
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
534
- journal.record(`outcome ${room.locator} ${head.id} ${result.kind}`);
535
- if (result.kind !== 'expired') journal.record(`refusal ${result.kind} ${result.text}`);
1317
+ const left = leaving(head, result);
536
1318
  await exclusive(async ()=>{
1319
+ if (!continuing()) return;
537
1320
  const current = replicas.get(room.locator);
538
1321
  if (current === undefined || !current.queue.some((batch)=>batch.id === head.id)) {
539
1322
  return;
540
1323
  }
541
- outcomes.set(head.id, recordRefusal(result));
542
- await persistQueue(local, room.locator, current.queue);
543
- emitQueue(room.locator);
1324
+ const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch)=>batch.id !== head.id), {
1325
+ leaving: result.kind === 'expired' && landed(head, current.confirmed) ? {
1326
+ ...left,
1327
+ returned: null
1328
+ } : left
1329
+ });
1330
+ publishChangeSet(set);
544
1331
  moveTo({
545
1332
  ...status,
546
- attentionRequired: true
1333
+ ...attention()
547
1334
  });
548
1335
  });
549
- return null;
1336
+ if (result.kind === 'expired') continue;
1337
+ const withheld = await advance(room, local, authorized);
1338
+ if (withheld !== null) return withhold(withheld, room, local, progress);
1339
+ continue;
550
1340
  }
551
1341
  const from = replicas.get(room.locator)?.confirmed;
552
- if (from === undefined) return null;
1342
+ if (from === undefined) return;
553
1343
  const pulled = await pullRefresh(authorized, from);
1344
+ if (!continuing()) return;
554
1345
  if (pulled.kind === 'revoked-authorization') {
555
- refuse('revoked-authorization');
556
- return 'stop';
1346
+ await refuse('revoked-authorization');
1347
+ return;
1348
+ }
1349
+ if (pulled.kind === 'scoped-refusal') {
1350
+ await scoped(pulled, room, local);
1351
+ progress.complete = false;
1352
+ return;
557
1353
  }
558
1354
  if (pulled.kind === 'still-current') {
559
- const committed = await exclusive(()=>commitAccept(room, local, head, result, from, true));
560
- if (!committed) {
1355
+ if (!await committed(room, local, head, result, from, true)) {
561
1356
  progress.complete = false;
562
- return null;
1357
+ return;
563
1358
  }
564
1359
  continue;
565
1360
  }
566
1361
  if (pulled.kind === 'advanced') {
567
1362
  const overlay = pulled.confirmed.record.roomVersion === result.roomVersion;
568
- const committed = await exclusive(()=>commitAccept(room, local, head, result, pulled.confirmed, overlay));
569
- if (!committed) {
1363
+ if (!await committed(room, local, head, result, pulled.confirmed, overlay)) {
570
1364
  progress.complete = false;
571
- return null;
1365
+ return;
572
1366
  }
573
1367
  continue;
574
1368
  }
575
1369
  if (pulled.kind === 'beyond-horizon') {
576
1370
  const bootstrapped = await pullBootstrap(authorized, room);
1371
+ if (!continuing()) return;
577
1372
  if (bootstrapped.kind === 'revoked-authorization') {
578
- refuse('revoked-authorization');
579
- return 'stop';
1373
+ await refuse('revoked-authorization');
1374
+ return;
1375
+ }
1376
+ if (bootstrapped.kind === 'scoped-refusal') {
1377
+ await scoped(bootstrapped, room, local);
1378
+ progress.complete = false;
1379
+ return;
580
1380
  }
581
1381
  if (bootstrapped.kind === 'staged') {
582
1382
  const overlay = bootstrapped.confirmed.record.roomVersion === result.roomVersion;
583
- const committed = await exclusive(()=>commitAccept(room, local, head, result, bootstrapped.confirmed, overlay));
584
- if (!committed) {
1383
+ if (!await committed(room, local, head, result, bootstrapped.confirmed, overlay)) {
585
1384
  progress.complete = false;
586
- return null;
1385
+ return;
587
1386
  }
588
1387
  continue;
589
1388
  }
@@ -594,8 +1393,10 @@ export function createMdRuntime(options) {
594
1393
  unreadable.add(room.locator);
595
1394
  list(room, false);
596
1395
  emitRooms();
1396
+ } else if (bootstrapped.kind === 'refused') {
1397
+ await holdAccepted(room, local, head, result, bootstrapped);
597
1398
  }
598
- return null;
1399
+ return;
599
1400
  }
600
1401
  progress.complete = false;
601
1402
  if (pulled.kind === 'transport-uncertainty') {
@@ -604,38 +1405,230 @@ export function createMdRuntime(options) {
604
1405
  unreadable.add(room.locator);
605
1406
  list(room, false);
606
1407
  emitRooms();
1408
+ } else if (pulled.kind === 'refused') {
1409
+ await holdAccepted(room, local, head, result, pulled);
607
1410
  }
608
- return null;
1411
+ return;
609
1412
  }
610
- return null;
1413
+ return;
611
1414
  }
612
- async function synchronize(discovered, local, progress) {
613
- for (const room of discovered){
614
- if (closed || authorization === undefined) {
615
- progress.complete = false;
616
- return;
617
- }
618
- const withheld = await advance(room, local, authorization);
619
- if (withheld !== null) {
620
- progress.complete = false;
621
- if (withheld.kind === 'transport-uncertainty') {
622
- progress.reachable = false;
623
- continue;
624
- }
625
- if (withheld.kind === 'room-not-readable') {
626
- unreadable.add(room.locator);
627
- list(room, false);
628
- emitRooms();
629
- continue;
630
- }
631
- if (withheld.kind === 'revoked-authorization') {
632
- refuse('revoked-authorization');
633
- return;
1415
+ async function retainModel(local, room, attached, next) {
1416
+ const key = modelKey(room.locator, attached);
1417
+ if (next.status === 'unavailable') modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
1418
+ const epoch = modelEpoch.get(key) ?? 0;
1419
+ const previous = modelOf(room.locator, attached);
1420
+ const heldBefore = replicas.get(room.locator)?.confirmed.record;
1421
+ 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;
1422
+ if (next.status === 'unavailable') {
1423
+ invalidModels.add(key);
1424
+ liveModels.delete(key);
1425
+ }
1426
+ await exclusive(async ()=>{
1427
+ if (!continuing()) return;
1428
+ if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0)) return;
1429
+ const held = replicas.get(room.locator);
1430
+ if (held !== undefined) {
1431
+ const record = {
1432
+ ...held.confirmed.record,
1433
+ operations: room.operations,
1434
+ replica: room.replica,
1435
+ attachedModel: room.attachedModel,
1436
+ typedSelection: room.typedSelection ?? null,
1437
+ ...attached === 'collection' ? {
1438
+ model: next
1439
+ } : {
1440
+ selectionModel: next
1441
+ }
1442
+ };
1443
+ await local.write({
1444
+ next: record,
1445
+ previous: held.confirmed.record,
1446
+ queue: held.queue
1447
+ });
1448
+ const read = readConfirmed(record, held.confirmed);
1449
+ if (read.kind === 'staged') {
1450
+ replicas.set(room.locator, replicaOf(read.confirmed, held.queue, rewriting(), held));
1451
+ if (declarationRefusals.delete(room.locator)) {
1452
+ sourceRefusals.delete(room.locator);
1453
+ unreadable.delete(room.locator);
1454
+ }
1455
+ } else if (read.kind === 'refused') {
1456
+ replicas.set(room.locator, {
1457
+ ...held,
1458
+ confirmed: {
1459
+ ...held.confirmed,
1460
+ record
1461
+ }
1462
+ });
1463
+ refusedSource(room, read);
1464
+ declarationRefusals.add(room.locator);
634
1465
  }
635
- continue;
636
1466
  }
637
- if (await submitRoom(room, local, authorization, progress) === 'stop') return;
1467
+ models.set(key, next);
1468
+ if (next.status === 'current') {
1469
+ liveModels.set(key, JSON.stringify(next.snapshot));
1470
+ invalidModels.delete(key);
1471
+ }
1472
+ list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
1473
+ if (held === undefined) emitRooms();
1474
+ else publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, {
1475
+ room: true
1476
+ }));
1477
+ });
1478
+ }
1479
+ async function acquireModel(local, room, authorized, selection) {
1480
+ const attached = selection === null ? 'collection' : 'typed-selection';
1481
+ const key = modelKey(room.locator, attached);
1482
+ const declared = selection === null ? room.attachedModel : selection.attachedModel;
1483
+ let previous = modelOf(room.locator, attached);
1484
+ if (previous.status !== 'unavailable' && (declared === null || !sameModel(previous.snapshot, declared))) {
1485
+ previous = {
1486
+ status: 'unavailable',
1487
+ reason: 'version-changed'
1488
+ };
1489
+ await retainModel(local, room, attached, previous);
1490
+ if (discoveryPaused) return;
1491
+ }
1492
+ const epoch = modelEpoch.get(key) ?? 0;
1493
+ let answer;
1494
+ try {
1495
+ answer = await options.adapter.model(authorized, selection === null ? {
1496
+ locator: room.locator,
1497
+ attached: 'collection',
1498
+ collectionRoot: room.collectionRoot,
1499
+ model: declared
1500
+ } : {
1501
+ locator: room.locator,
1502
+ attached: 'typed-selection',
1503
+ roots: selection.roots,
1504
+ model: declared
1505
+ });
1506
+ } catch {
1507
+ answer = {
1508
+ kind: 'transport-uncertainty'
1509
+ };
1510
+ }
1511
+ if (discoveryPaused || epoch !== (modelEpoch.get(key) ?? 0)) return;
1512
+ if (answer.kind === 'revoked-authorization') {
1513
+ await refuse('revoked-authorization');
1514
+ return;
1515
+ }
1516
+ if (answer.kind === 'transport-uncertainty') {
1517
+ await retainModel(local, room, attached, cached(previous));
1518
+ return;
1519
+ }
1520
+ if (answer.kind === 'model-unavailable') {
1521
+ await retainModel(local, room, attached, {
1522
+ status: 'unavailable',
1523
+ reason: answer.reason
1524
+ });
1525
+ return;
1526
+ }
1527
+ if (declared === null || !sameModel(answer.snapshot, declared)) {
1528
+ await retainModel(local, room, attached, {
1529
+ status: 'unavailable',
1530
+ reason: 'version-changed'
1531
+ });
1532
+ return;
1533
+ }
1534
+ if (previous.status !== 'unavailable' && sameModel(previous.snapshot, answer.snapshot) && previous.snapshot.schema !== answer.snapshot.schema) {
1535
+ await retainModel(local, room, attached, {
1536
+ status: 'unavailable',
1537
+ reason: 'invalid'
1538
+ });
1539
+ return;
1540
+ }
1541
+ const verified = answer.snapshot.schema === null ? EMPTY_SCHEMA : verify(answer.snapshot.schema);
1542
+ if (verified.verdict === 'refuse') {
1543
+ const where = verified.location === undefined ? '' : ` ${verified.location}`;
1544
+ journal.record(`model invalid ${room.locator} ${attached} ${answer.snapshot.identity} ${verified.rule}${where}`);
1545
+ }
1546
+ await retainModel(local, room, attached, verified.verdict === 'accept' ? {
1547
+ status: 'current',
1548
+ snapshot: structuredClone(answer.snapshot),
1549
+ verifiedWith: MD_MODEL,
1550
+ types: verified.model.types
1551
+ } : {
1552
+ status: 'unavailable',
1553
+ reason: 'invalid'
1554
+ });
1555
+ }
1556
+ async function scoped(result, room, local) {
1557
+ scopedRefusals.set(room.locator, {
1558
+ code: result.code,
1559
+ fields: result.fields
1560
+ });
1561
+ let scopedAway = room;
1562
+ if (result.scope === 'model') await retainModel(local, room, 'collection', {
1563
+ status: 'unavailable',
1564
+ reason: 'not-readable'
1565
+ });
1566
+ else if (result.scope !== 'operation') {
1567
+ unreadable.add(room.locator);
1568
+ scopedAway = {
1569
+ ...room,
1570
+ replica: {
1571
+ ...room.replica,
1572
+ admission: 'unavailable'
1573
+ },
1574
+ operations: {
1575
+ ...room.operations,
1576
+ ...result.scope === 'content' ? {
1577
+ read: []
1578
+ } : {},
1579
+ ...result.scope === 'sync' ? {
1580
+ sync: []
1581
+ } : {}
1582
+ }
1583
+ };
1584
+ await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
638
1585
  }
1586
+ if (!continuing()) return;
1587
+ list(scopedAway, scopedAway.replica.admission === 'admitted' && !unreadable.has(room.locator));
1588
+ emitRooms();
1589
+ journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
1590
+ }
1591
+ async function synchronize(discovered, local, progress) {
1592
+ const rooms = await Promise.allSettled(discovered.map((room)=>synchronizeRoom(room, local, progress)));
1593
+ const failed = rooms.find((room)=>room.status === 'rejected');
1594
+ if (failed !== undefined) throw failed.reason;
1595
+ }
1596
+ async function synchronizeRoom(room, local, progress) {
1597
+ if (closed || authorization === undefined) {
1598
+ progress.complete = false;
1599
+ return;
1600
+ }
1601
+ const prior = replicas.get(room.locator)?.confirmed.record;
1602
+ if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
1603
+ list(prior, false);
1604
+ emitRooms();
1605
+ journal.record(`selection changed ${room.locator}`);
1606
+ progress.complete = false;
1607
+ return;
1608
+ }
1609
+ const selection = room.typedSelection;
1610
+ await Promise.all([
1611
+ acquireModel(local, room, authorization, null),
1612
+ ...selection === undefined ? [] : [
1613
+ acquireModel(local, room, authorization, selection)
1614
+ ]
1615
+ ]);
1616
+ if (discoveryPaused || authorization === undefined) return;
1617
+ if (room.replica.admission !== 'admitted') {
1618
+ progress.complete = false;
1619
+ return;
1620
+ }
1621
+ const held = replicas.get(room.locator);
1622
+ if (held !== undefined && held.confirmed.record.replica.selection !== room.replica.selection) {
1623
+ progress.complete = false;
1624
+ return;
1625
+ }
1626
+ const withheld = await advance(room, local, authorization);
1627
+ if (withheld !== null) {
1628
+ await withhold(withheld, room, local, progress);
1629
+ return;
1630
+ }
1631
+ await submitRoom(room, local, authorization, progress);
639
1632
  }
640
1633
  async function cycle(local, progress) {
641
1634
  if (closed || authorization === undefined) {
@@ -646,10 +1639,10 @@ export function createMdRuntime(options) {
646
1639
  let result;
647
1640
  try {
648
1641
  result = await options.adapter.discover(authorization);
649
- } catch (error) {
650
- journal.record('connection discover transport-uncertainty');
651
- notifyReady();
652
- throw error;
1642
+ } catch {
1643
+ result = {
1644
+ kind: 'transport-uncertainty'
1645
+ };
653
1646
  }
654
1647
  if (closed) {
655
1648
  notifyReady();
@@ -657,29 +1650,78 @@ export function createMdRuntime(options) {
657
1650
  }
658
1651
  journal.record(`connection discover ${result.kind}`);
659
1652
  if (result.kind === 'transport-uncertainty') {
1653
+ for (const room of rooms.values()){
1654
+ await retainModel(local, room, 'collection', cached(modelOf(room.locator)));
1655
+ if (room.typedSelection !== null) {
1656
+ await retainModel(local, room, 'typed-selection', cached(modelOf(room.locator, 'typed-selection')));
1657
+ }
1658
+ }
660
1659
  progress.reachable = false;
661
1660
  notifyReady();
662
1661
  return;
663
1662
  }
1663
+ if (result.kind === 'contract-unavailable') {
1664
+ journal.record(`connection contract unavailable ${result.code}`);
1665
+ await refuse('missing-required-guarantee', {
1666
+ code: result.code,
1667
+ fields: result.fields
1668
+ });
1669
+ notifyReady();
1670
+ return;
1671
+ }
664
1672
  if (result.kind === 'revoked-authorization') {
665
- refuse('revoked-authorization');
1673
+ await refuse('revoked-authorization');
666
1674
  notifyReady();
667
1675
  return;
668
1676
  }
669
1677
  if (!result.supportedVersions.includes(ROOM_CONTRACT_VERSION)) {
670
- refuse('no-matching-version');
1678
+ await refuse('no-matching-version');
671
1679
  notifyReady();
672
1680
  return;
673
1681
  }
674
1682
  if (result.mdModel.identity !== MD_MODEL.identity || result.mdModel.version !== MD_MODEL.version) {
675
- refuse('md-model-version-mismatch');
1683
+ await refuse('md-model-version-mismatch');
1684
+ notifyReady();
1685
+ return;
1686
+ }
1687
+ mismatchedRoots.clear();
1688
+ for (const room of result.rooms){
1689
+ const held = replicas.get(room.locator)?.confirmed.record;
1690
+ const malformed = !validRoot(room.collectionRoot);
1691
+ if (room.operations === undefined || ![
1692
+ 'read',
1693
+ 'sync',
1694
+ 'write',
1695
+ 'delete'
1696
+ ].every((operation)=>Array.isArray(room.operations[operation]) && room.operations[operation].every((scope)=>typeof scope === 'string')) || room.replica === undefined || typeof room.replica.selection !== 'string' || room.replica.selection.length === 0 || ![
1697
+ 'admitted',
1698
+ 'unavailable'
1699
+ ].includes(room.replica.admission) || !declaredIdentity(room.attachedModel)) {
1700
+ journal.record(`invalid model or replica declaration ${room.locator}`);
1701
+ await refuse('missing-required-guarantee');
1702
+ notifyReady();
1703
+ return;
1704
+ }
1705
+ if (!declaredSelection(room)) {
1706
+ journal.record(`invalid typed selection declaration ${room.locator}`);
1707
+ await refuse('missing-required-guarantee');
1708
+ notifyReady();
1709
+ return;
1710
+ }
1711
+ if (malformed || held !== undefined && held.collectionRoot !== room.collectionRoot) {
1712
+ mismatchedRoots.add(room.locator);
1713
+ journal.record(`collection root ${malformed ? 'invalid' : 'changed'} ${room.locator}`);
1714
+ }
1715
+ }
1716
+ if (mismatchedRoots.size > 0) {
1717
+ await refuse('missing-required-guarantee');
676
1718
  notifyReady();
677
1719
  return;
678
1720
  }
679
1721
  const declared = new Set(result.capabilities.map((capability)=>capability.name));
680
1722
  for (const required of options.requiredGuarantees){
681
1723
  if (!knownCapabilities.has(required) || !declared.has(required)) {
682
- refuse('missing-required-guarantee');
1724
+ await refuse('missing-required-guarantee');
683
1725
  notifyReady();
684
1726
  return;
685
1727
  }
@@ -688,17 +1730,24 @@ export function createMdRuntime(options) {
688
1730
  refusal = null;
689
1731
  emitRefusal();
690
1732
  }
1733
+ const previousCapabilities = capabilities;
691
1734
  capabilities = result.capabilities;
692
1735
  notifyReady();
693
- if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits)) {
1736
+ if (declared.has('room-version-notifications')) observeRoomVersions();
1737
+ const discoveredSlots = result.slots ?? [];
1738
+ if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) || JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities) || JSON.stringify(discoveredSlots) !== JSON.stringify(slots)) {
694
1739
  limits = result.providerLimits;
695
- await local.declare(limits);
1740
+ slots = discoveredSlots;
1741
+ await local.declare(limits, capabilities, slots);
696
1742
  }
697
- const listedLocators = applyDiscovery(result.rooms);
698
- progress.complete = [
699
- ...replicas.keys()
700
- ].every((locator)=>listedLocators.has(locator));
1743
+ await applyDiscovery(result.rooms, local);
1744
+ handSelected(result.rooms.filter((room)=>room.replica.admission === 'admitted').map((room)=>room.locator).sort());
1745
+ progress.complete = true;
701
1746
  await synchronize(result.rooms, local, progress);
1747
+ if (closed || discoveryPaused) return;
1748
+ await conversations.reconcile();
1749
+ if (status.providerUnreachable) conversations.reconnected();
1750
+ await conversations.catchUp(progress);
702
1751
  }
703
1752
  async function ensureAuthorization() {
704
1753
  if (authorization !== undefined) return;
@@ -711,7 +1760,7 @@ export function createMdRuntime(options) {
711
1760
  }
712
1761
  }
713
1762
  async function connect() {
714
- if (closed || discoveryPaused) {
1763
+ if (closed || stopping || discoveryPaused) {
715
1764
  notifyReady();
716
1765
  return;
717
1766
  }
@@ -752,7 +1801,141 @@ export function createMdRuntime(options) {
752
1801
  }
753
1802
  }
754
1803
  }
1804
+ function observeRoomVersions() {
1805
+ const path = options.adapter.roomVersions;
1806
+ if (path === undefined || withdrawNotifications !== undefined) return;
1807
+ withdrawNotifications = path.observe((notification)=>{
1808
+ if (closed || stopping || discoveryPaused) return;
1809
+ if (notification.modelChanged === true && handed?.includes(notification.locator) === true) {
1810
+ void enqueue();
1811
+ return;
1812
+ }
1813
+ notices.set(notification.locator, notification.roomVersion);
1814
+ chain = chain.then(runNotified, runNotified);
1815
+ });
1816
+ }
1817
+ function writesHeld() {
1818
+ return serviceMode === 'write-halted' || haltedRefusal;
1819
+ }
1820
+ function signalled(mode) {
1821
+ if (closed) return;
1822
+ if (mode === 'open') openSignals += 1;
1823
+ const releasing = mode === 'open' && (haltedRefusal || serviceMode === 'write-halted');
1824
+ if (mode === 'open') haltedRefusal = false;
1825
+ if (mode !== serviceMode) {
1826
+ serviceMode = mode;
1827
+ journal.record(`service mode ${mode}`);
1828
+ for (const listener of serviceModeListeners)listener(mode);
1829
+ }
1830
+ if (releasing) void enqueue();
1831
+ }
1832
+ function observeConversations() {
1833
+ const stream = options.adapter.conversations;
1834
+ if (stream === undefined || withdrawConversations !== undefined) return;
1835
+ withdrawConversations = stream.observe((event)=>{
1836
+ conversations.receive(event);
1837
+ if (event.kind !== 'gap' || conversations.current()) return;
1838
+ show();
1839
+ scheduleCurrency();
1840
+ });
1841
+ }
1842
+ function scheduleCurrency() {
1843
+ if (currencyQueued || closed || stopping) return;
1844
+ currencyQueued = true;
1845
+ chain = chain.then(runCurrency, runCurrency);
1846
+ }
1847
+ async function runCurrency() {
1848
+ currencyQueued = false;
1849
+ if (closed || stopping || discoveryPaused) return;
1850
+ await conversations.reconcile();
1851
+ if (conversations.current()) {
1852
+ show();
1853
+ return;
1854
+ }
1855
+ const resting = status;
1856
+ const progress = {
1857
+ complete: false,
1858
+ reachable: !resting.providerUnreachable
1859
+ };
1860
+ moveTo(transferring(resting));
1861
+ try {
1862
+ await conversations.catchUp(progress);
1863
+ } catch {
1864
+ progress.reachable = false;
1865
+ } finally{
1866
+ if (!closed) {
1867
+ moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1868
+ }
1869
+ }
1870
+ }
1871
+ function handSelected(locators) {
1872
+ const path = options.adapter.roomVersions;
1873
+ if (path === undefined || withdrawNotifications === undefined) return;
1874
+ if (handed !== undefined && JSON.stringify(locators) === JSON.stringify(handed)) return;
1875
+ handed = locators;
1876
+ path.select(locators);
1877
+ }
1878
+ async function runNotified() {
1879
+ const announced = [
1880
+ ...notices
1881
+ ];
1882
+ notices.clear();
1883
+ if (announced.length === 0 || closed || stopping || discoveryPaused) return;
1884
+ const resting = status;
1885
+ const progress = {
1886
+ complete: false,
1887
+ reachable: !resting.providerUnreachable
1888
+ };
1889
+ moveTo(transferring(resting));
1890
+ try {
1891
+ const local = await openLocal();
1892
+ for (const [locator, roomVersion] of announced){
1893
+ if (closed || discoveryPaused || authorization === undefined) return;
1894
+ if (handed?.includes(locator) !== true) continue;
1895
+ const room = discovered.get(locator);
1896
+ const held = replicas.get(locator);
1897
+ if (room === undefined) continue;
1898
+ if (sourceRefusals.has(locator)) {
1899
+ void enqueue();
1900
+ continue;
1901
+ }
1902
+ if (held === undefined) continue;
1903
+ if (held.confirmed.record.roomVersion === roomVersion) continue;
1904
+ const withheld = await advance(room, local, authorization);
1905
+ if (withheld === null) continue;
1906
+ if (withheld.kind === 'refused') {
1907
+ void enqueue();
1908
+ continue;
1909
+ }
1910
+ if (withheld.kind === 'scoped-refusal') {
1911
+ await scoped(withheld, room, local);
1912
+ continue;
1913
+ }
1914
+ if (withheld.kind === 'transport-uncertainty') {
1915
+ progress.reachable = false;
1916
+ continue;
1917
+ }
1918
+ if (withheld.kind === 'room-not-readable') {
1919
+ unreadable.add(locator);
1920
+ list(room, false);
1921
+ emitRooms();
1922
+ continue;
1923
+ }
1924
+ if (withheld.kind === 'revoked-authorization') {
1925
+ await refuse('revoked-authorization');
1926
+ return;
1927
+ }
1928
+ }
1929
+ } catch {
1930
+ progress.reachable = false;
1931
+ } finally{
1932
+ if (!closed) {
1933
+ moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1934
+ }
1935
+ }
1936
+ }
755
1937
  function enqueue() {
1938
+ if (stopping) return chain;
756
1939
  if (inflight) {
757
1940
  queued = true;
758
1941
  return chain;
@@ -762,69 +1945,266 @@ export function createMdRuntime(options) {
762
1945
  return chain;
763
1946
  }
764
1947
  function close() {
1948
+ if (closed) return;
765
1949
  closed = true;
1950
+ withdrawNotifications?.();
1951
+ withdrawNotifications = undefined;
1952
+ withdrawConversations?.();
1953
+ withdrawConversations = undefined;
1954
+ withdrawServiceMode?.();
1955
+ conversations.close();
766
1956
  stopTimer();
767
1957
  store?.close();
1958
+ void opening?.then((opened)=>{
1959
+ opened.close();
1960
+ }, ()=>{});
768
1961
  roomListeners.clear();
769
1962
  refusalListeners.clear();
1963
+ storageLossListeners.clear();
770
1964
  statusListeners.clear();
771
- querySubscriptions.clear();
1965
+ serviceModeListeners.clear();
1966
+ readSide.close();
772
1967
  queueSubscriptions.clear();
1968
+ roomDraftListeners.clear();
1969
+ presentationListeners.clear();
773
1970
  }
1971
+ async function stop() {
1972
+ stopping = true;
1973
+ stopTimer();
1974
+ await chain.catch(()=>undefined);
1975
+ await mutations;
1976
+ close();
1977
+ }
1978
+ const withdrawServiceMode = options.adapter.serviceMode?.observe(signalled);
1979
+ observeConversations();
774
1980
  options.signal?.addEventListener('abort', close, {
775
1981
  once: true
776
1982
  });
777
1983
  if (options.signal?.aborted) close();
1984
+ void openLocal().catch(()=>{
1985
+ notifyReady();
1986
+ });
778
1987
  void enqueue();
779
- return {
1988
+ function watch(topic, input, deliver) {
1989
+ if (topic === 'room-drafts') {
1990
+ const listener = {
1991
+ notify: deliver,
1992
+ ready: false
1993
+ };
1994
+ roomDraftListeners.add(listener);
1995
+ void exclusive(async ()=>{
1996
+ await openLocal();
1997
+ if (closed || !roomDraftListeners.has(listener)) return;
1998
+ listener.ready = true;
1999
+ }).then(()=>{
2000
+ if (closed || !listener.ready || !roomDraftListeners.has(listener)) return;
2001
+ listener.notify(snapshotRoomDrafts());
2002
+ }, ()=>{});
2003
+ return ()=>{
2004
+ roomDraftListeners.delete(listener);
2005
+ };
2006
+ }
2007
+ if (topic === 'conversation') {
2008
+ return conversations.watch(input[0], input[1], deliver);
2009
+ }
2010
+ if (topic === 'presentation') {
2011
+ const listener = {
2012
+ app: input[0],
2013
+ notify: deliver,
2014
+ ready: false
2015
+ };
2016
+ presentationListeners.add(listener);
2017
+ void exclusive(async ()=>{
2018
+ await openLocal();
2019
+ }).then(()=>{
2020
+ if (closed || !presentationListeners.has(listener)) return;
2021
+ listener.ready = true;
2022
+ listener.notify(presentationOf(listener.app));
2023
+ }, ()=>{});
2024
+ return ()=>{
2025
+ presentationListeners.delete(listener);
2026
+ };
2027
+ }
2028
+ if (topic === 'rooms') {
2029
+ roomListeners.add(deliver);
2030
+ deliver(listed());
2031
+ return ()=>{
2032
+ roomListeners.delete(deliver);
2033
+ };
2034
+ }
2035
+ if (topic === 'connection-refusal') {
2036
+ refusalListeners.add(deliver);
2037
+ deliver(refusal);
2038
+ return ()=>{
2039
+ refusalListeners.delete(deliver);
2040
+ };
2041
+ }
2042
+ if (topic === 'storage-loss') {
2043
+ storageLossListeners.add(deliver);
2044
+ deliver(storageLoss);
2045
+ return ()=>{
2046
+ storageLossListeners.delete(deliver);
2047
+ };
2048
+ }
2049
+ if (topic === 'sync-status') {
2050
+ statusListeners.add(deliver);
2051
+ deliver(shown);
2052
+ return ()=>{
2053
+ statusListeners.delete(deliver);
2054
+ };
2055
+ }
2056
+ if (topic === 'service-mode') {
2057
+ serviceModeListeners.add(deliver);
2058
+ deliver(serviceMode);
2059
+ return ()=>{
2060
+ serviceModeListeners.delete(deliver);
2061
+ };
2062
+ }
2063
+ if (topic === 'queue') {
2064
+ const queue = {
2065
+ locator: input[0],
2066
+ notify: deliver
2067
+ };
2068
+ queueSubscriptions.add(queue);
2069
+ queue.notify(entriesOf(queue.locator));
2070
+ return ()=>{
2071
+ queueSubscriptions.delete(queue);
2072
+ };
2073
+ }
2074
+ const plan = planOf(input[0], roomFacts());
2075
+ return readSide.watch(plan, identityOf(plan), deliver);
2076
+ }
2077
+ const md = {
2078
+ loaded () {
2079
+ return openLocal().then(()=>undefined);
2080
+ },
780
2081
  rooms () {
781
2082
  return listed();
782
2083
  },
783
- query (request = {}) {
784
- return visible(request);
2084
+ slots () {
2085
+ return slotted();
2086
+ },
2087
+ query: (input)=>readSide.query(planOf(input, roomFacts())),
2088
+ async search (input) {
2089
+ const plan = searchPlanOf(input);
2090
+ const selected = plan.documents === undefined ? null : (await readSide.query(filterOf(plan.documents, roomFacts()))).documents;
2091
+ return searchIndex.search(plan, selected);
785
2092
  },
786
2093
  types (locator) {
787
- return replicas.get(locator)?.visible.collection.types ?? [];
2094
+ return {
2095
+ collection: modelTypes(modelOf(locator)),
2096
+ typedSelection: rooms.get(locator)?.typedSelection === undefined || rooms.get(locator)?.typedSelection === null ? null : modelTypes(modelOf(locator, 'typed-selection'))
2097
+ };
788
2098
  },
789
2099
  syncStatus () {
790
- return status;
2100
+ return shown;
2101
+ },
2102
+ serviceMode () {
2103
+ return serviceMode;
791
2104
  },
792
- draft: {
793
- read (locator, path) {
794
- return drafts.get(at(locator, path)) ?? null;
2105
+ roomDraft: {
2106
+ list () {
2107
+ return exclusive(async ()=>{
2108
+ await openLocal();
2109
+ return snapshotRoomDrafts();
2110
+ }).then((listed)=>{
2111
+ publishRoomDrafts(true);
2112
+ return listed;
2113
+ });
795
2114
  },
796
- async replace (locator, path, content) {
797
- const local = await openLocal();
798
- await local.keep({
799
- locator,
800
- path,
801
- content
2115
+ read (room, path) {
2116
+ const record = roomDrafts.get(room)?.get(path);
2117
+ return record === undefined ? null : roomDraftOf(record);
2118
+ },
2119
+ write (room, path, revision, input) {
2120
+ return exclusive(async ()=>{
2121
+ const parts = readParts(input);
2122
+ if (path === '' || normalize(path) !== path) throw new Error('room-draft-malformed');
2123
+ const local = await openLocal();
2124
+ const current = roomDrafts.get(room)?.get(path);
2125
+ if ((current?.revision ?? null) !== revision) {
2126
+ return {
2127
+ outcome: 'stale',
2128
+ current: current === undefined ? null : roomDraftOf(current)
2129
+ };
2130
+ }
2131
+ const record = {
2132
+ locator: room,
2133
+ path,
2134
+ revision: roomDraftRevision(),
2135
+ parts: held(current?.parts, parts),
2136
+ returned: current?.returned ?? null
2137
+ };
2138
+ await local.keepRoomDraft(record);
2139
+ rememberRoomDraft(record);
2140
+ publishChangeSet(changeSet(room, null, null, {
2141
+ drafts: [
2142
+ path
2143
+ ]
2144
+ }));
2145
+ return {
2146
+ outcome: 'written',
2147
+ roomDraft: roomDraftOf(record)
2148
+ };
802
2149
  });
803
- drafts.set(at(locator, path), content);
804
2150
  },
805
- async discard (locator, path) {
806
- const local = await openLocal();
807
- await local.discard(locator, path);
808
- drafts.delete(at(locator, path));
2151
+ discard (room, path, revision) {
2152
+ return exclusive(async ()=>{
2153
+ const local = await openLocal();
2154
+ const current = roomDrafts.get(room)?.get(path);
2155
+ if (current === undefined || current.revision !== revision) {
2156
+ return {
2157
+ outcome: 'stale',
2158
+ current: current === undefined ? null : roomDraftOf(current)
2159
+ };
2160
+ }
2161
+ await local.discardRoomDraft(room, path);
2162
+ forgetRoomDraft(room, path);
2163
+ publishChangeSet(changeSet(room, null, null, {
2164
+ drafts: [
2165
+ path
2166
+ ]
2167
+ }));
2168
+ moveTo({
2169
+ ...status,
2170
+ ...attention()
2171
+ });
2172
+ return {
2173
+ outcome: 'written',
2174
+ roomDraft: null
2175
+ };
2176
+ });
809
2177
  }
810
2178
  },
811
- save (locator, changes, replacing) {
812
- return exclusive(()=>write(locator, changes, replacing));
813
- },
814
- outcome (identifier) {
815
- const recorded = outcomes.get(identifier);
816
- if (recorded !== undefined) return recorded;
817
- for (const replica of replicas.values()){
818
- if (replica.queue.some((batch)=>batch.id === identifier)) return {
819
- kind: 'pending'
820
- };
2179
+ conversation: conversations.surface,
2180
+ chatDraft: conversations.chatDraft,
2181
+ presentation: {
2182
+ read (app) {
2183
+ return exclusive(async ()=>{
2184
+ await openLocal();
2185
+ return presentationOf(app);
2186
+ });
2187
+ },
2188
+ write (app, value) {
2189
+ return exclusive(async ()=>{
2190
+ const local = await openLocal();
2191
+ const stored = structuredClone(value);
2192
+ await local.present({
2193
+ app,
2194
+ value: stored
2195
+ });
2196
+ presentations.set(app, stored);
2197
+ publishChangeSet(changeSet('', null, null, {
2198
+ presentation: app
2199
+ }));
2200
+ });
821
2201
  }
822
- return null;
823
2202
  },
824
- queue: {
825
- discard (locator, identifier) {
826
- return exclusive(()=>discardHead(locator, identifier));
827
- }
2203
+ save (room, named, writtenBy) {
2204
+ return exclusive(()=>saveRoomDrafts(room, named, writtenBy));
2205
+ },
2206
+ outcome (identifier) {
2207
+ return outcomes.get(identifier) ?? null;
828
2208
  },
829
2209
  history (locator, path, query) {
830
2210
  return readHistory(locator, path, query);
@@ -835,61 +2215,30 @@ export function createMdRuntime(options) {
835
2215
  log () {
836
2216
  return journal.export();
837
2217
  },
838
- observe (topic, second, third) {
839
- if (topic === 'rooms') {
840
- const notify = second;
841
- roomListeners.add(notify);
842
- notify(listed());
843
- return ()=>{
844
- roomListeners.delete(notify);
845
- };
846
- }
847
- if (topic === 'connection-refusal') {
848
- const notify = second;
849
- refusalListeners.add(notify);
850
- notify(refusal);
851
- return ()=>{
852
- refusalListeners.delete(notify);
853
- };
854
- }
855
- if (topic === 'sync-status') {
856
- const notify = second;
857
- statusListeners.add(notify);
858
- notify(status);
859
- return ()=>{
860
- statusListeners.delete(notify);
861
- };
862
- }
863
- if (topic === 'queue') {
864
- const queue = {
865
- locator: second,
866
- notify: third
867
- };
868
- queueSubscriptions.add(queue);
869
- queue.notify(entriesOf(queue.locator));
870
- return ()=>{
871
- queueSubscriptions.delete(queue);
872
- };
873
- }
874
- const subscription = {
875
- query: second,
876
- notify: third
877
- };
878
- const first = visible(subscription.query);
879
- querySubscriptions.add(subscription);
880
- subscription.notify(first);
881
- return ()=>{
882
- querySubscriptions.delete(subscription);
883
- };
2218
+ storageLoss () {
2219
+ return storageLoss;
884
2220
  },
2221
+ persistence () {
2222
+ return options.shell.persistentStorage;
2223
+ },
2224
+ observe: (topic, ...input)=>opened((deliver)=>watch(topic, input, (message)=>{
2225
+ deliver(structuredClone(message));
2226
+ }), topic === 'query'),
885
2227
  async reauthorize () {
886
2228
  if (refusal === null || !refusal.reauthorizationOffered) return;
887
2229
  journal.record('connection reauthorize');
2230
+ await chain.catch(()=>undefined);
2231
+ await mutations;
888
2232
  authorization = undefined;
889
2233
  discoveryPaused = false;
890
2234
  await enqueue();
891
2235
  }
892
2236
  };
2237
+ return {
2238
+ md,
2239
+ stop,
2240
+ watch
2241
+ };
893
2242
  }
894
2243
 
895
2244
  //# sourceMappingURL=connection.js.map