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

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