@agentshouse/mdruntime 0.1.0-alpha.9 → 0.1.0-c3903a68dcbbf

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