@agentshouse/mdruntime 0.1.0-alpha.8 → 0.1.0-c009335e08279

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