@agentshouse/mdruntime 0.1.0-c3903a68dcbbf → 0.1.0-c56daf494ecb4

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