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

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