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

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