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

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