@agentshouse/mdruntime 0.1.0-alpha.3 → 0.1.0-alpha.31

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