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

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