@agentshouse/mdruntime 0.1.0-alpha.5 → 0.1.0-alpha.50

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