@agentshouse/mdruntime 0.1.0-c8c4b020a3ec7 → 0.1.0-c8ca66ac9e99d

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 (175) hide show
  1. package/AGENTS.md +175 -84
  2. package/README.md +21 -11
  3. package/dist/adapter.d.ts +125 -55
  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 +89 -38
  18. package/dist/change-set.d.ts.map +1 -1
  19. package/dist/change-set.js +19 -12
  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 -7
  87. package/dist/client.d.ts.map +1 -1
  88. package/dist/client.js +68 -25
  89. package/dist/collection.d.ts +6 -11
  90. package/dist/collection.d.ts.map +1 -1
  91. package/dist/collection.js +56 -19
  92. package/dist/connection.d.ts +28 -18
  93. package/dist/connection.d.ts.map +1 -1
  94. package/dist/connection.js +942 -626
  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/expectation.d.ts +21 -0
  99. package/dist/expectation.d.ts.map +1 -0
  100. package/dist/expectation.js +70 -0
  101. package/dist/failure.d.ts +13 -0
  102. package/dist/failure.d.ts.map +1 -0
  103. package/dist/failure.js +19 -0
  104. package/dist/host.d.ts +9 -2
  105. package/dist/host.d.ts.map +1 -1
  106. package/dist/host.js +46 -42
  107. package/dist/identity.d.ts +3 -0
  108. package/dist/identity.d.ts.map +1 -1
  109. package/dist/identity.js +13 -1
  110. package/dist/index.d.ts +9 -4
  111. package/dist/index.d.ts.map +1 -1
  112. package/dist/index.js +3 -0
  113. package/dist/memory.d.ts +14 -7
  114. package/dist/memory.d.ts.map +1 -1
  115. package/dist/memory.js +137 -45
  116. package/dist/model.d.ts +13 -12
  117. package/dist/model.d.ts.map +1 -1
  118. package/dist/model.js +6 -3
  119. package/dist/observation.d.ts +2 -0
  120. package/dist/observation.d.ts.map +1 -1
  121. package/dist/observation.js +24 -14
  122. package/dist/open.d.ts +2 -0
  123. package/dist/open.d.ts.map +1 -0
  124. package/dist/open.js +1 -0
  125. package/dist/outcome.d.ts +28 -3
  126. package/dist/outcome.d.ts.map +1 -1
  127. package/dist/outcome.js +41 -1
  128. package/dist/overlay.d.ts +14 -3
  129. package/dist/overlay.d.ts.map +1 -1
  130. package/dist/overlay.js +75 -37
  131. package/dist/paths.d.ts +0 -1
  132. package/dist/paths.d.ts.map +1 -1
  133. package/dist/paths.js +0 -16
  134. package/dist/query.d.ts +12 -3
  135. package/dist/query.d.ts.map +1 -1
  136. package/dist/query.js +78 -36
  137. package/dist/reach.d.ts +42 -0
  138. package/dist/reach.d.ts.map +1 -0
  139. package/dist/reach.js +294 -0
  140. package/dist/react.d.ts +41 -0
  141. package/dist/react.d.ts.map +1 -0
  142. package/dist/react.js +64 -0
  143. package/dist/read-side.d.ts +4 -0
  144. package/dist/read-side.d.ts.map +1 -1
  145. package/dist/read-side.js +125 -56
  146. package/dist/refresh.d.ts +12 -1
  147. package/dist/refresh.d.ts.map +1 -1
  148. package/dist/refresh.js +44 -36
  149. package/dist/replica.d.ts +9 -6
  150. package/dist/replica.d.ts.map +1 -1
  151. package/dist/replica.js +81 -62
  152. package/dist/room-draft.d.ts +29 -9
  153. package/dist/room-draft.d.ts.map +1 -1
  154. package/dist/room-draft.js +137 -26
  155. package/dist/rows.d.ts +7 -2
  156. package/dist/rows.d.ts.map +1 -1
  157. package/dist/rows.js +85 -40
  158. package/dist/save.d.ts +28 -3
  159. package/dist/save.d.ts.map +1 -1
  160. package/dist/save.js +429 -80
  161. package/dist/search.d.ts +3 -2
  162. package/dist/search.d.ts.map +1 -1
  163. package/dist/status.d.ts +2 -2
  164. package/dist/status.d.ts.map +1 -1
  165. package/dist/status.js +0 -8
  166. package/dist/store.d.ts +37 -11
  167. package/dist/store.d.ts.map +1 -1
  168. package/dist/store.js +310 -164
  169. package/dist/wire.d.ts +58 -19
  170. package/dist/wire.d.ts.map +1 -1
  171. package/dist/wire.js +65 -61
  172. package/package.json +27 -7
  173. package/dist/attachment.d.ts +0 -3
  174. package/dist/attachment.d.ts.map +0 -1
  175. package/dist/attachment.js +0 -8
@@ -1,77 +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
41
  export function composeRuntime(options) {
53
- const period = options.discoveryIntervalMs ?? DEFAULT_DISCOVERY_INTERVAL_MS;
42
+ const localSaves = options.adapter.localSaves === true;
54
43
  let authorization = options.authorization;
55
44
  let opening;
56
45
  let store;
57
46
  const replicas = new Map();
47
+ const queuesWithoutReplica = new Map();
58
48
  const pendings = new Map();
59
49
  const roomDrafts = new Map();
60
50
  const presentations = new Map();
61
- const wanted = new Map();
62
51
  let limits = [];
63
52
  let capabilities = [];
64
- function rewriting() {
65
- return capabilities.some((capability) => capability.name === 'move-reference-rewrites')
66
- ? 'provider'
67
- : 'runtime';
68
- }
69
53
  const rooms = new Map();
54
+ const counted = new Map();
70
55
  const unreadable = new Set();
71
56
  const models = new Map();
72
57
  const liveModels = new Map();
73
58
  const invalidModels = new Set();
74
- const modelEpoch = new Map();
75
59
  const discovered = new Map();
76
60
  let roomsListed = false;
77
61
  const scopedRefusals = new Map();
@@ -100,16 +84,21 @@ export function composeRuntime(options) {
100
84
  let status = STARTING;
101
85
  let shown = STARTING;
102
86
  let discoveryPaused = false;
103
- let timer;
87
+ let resetTimer;
104
88
  let closed = false;
105
89
  let stopping = false;
106
- let withdrawNotifications;
107
- let handed;
108
- 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();
109
100
  let chain = Promise.resolve();
110
101
  let mutations = Promise.resolve();
111
- let inflight = false;
112
- let queued = false;
113
102
  const roomListeners = new Set();
114
103
  const refusalListeners = new Set();
115
104
  const storageLossListeners = new Set();
@@ -120,13 +109,19 @@ export function composeRuntime(options) {
120
109
  const readSide = createReadSide();
121
110
  let withdrawConversations;
122
111
  let currencyQueued = false;
112
+ let socketOpen = false;
123
113
  let serviceMode = 'open';
124
114
  let haltedRefusal = false;
125
115
  let openSignals = 0;
126
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();
127
122
  const conversations = createConversationStore({
128
- cards: declaredCards,
129
- documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
123
+ agents: declaredAgents,
124
+ documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
130
125
  call: (name, input) => invokeAction(name, input),
131
126
  durably,
132
127
  stream: () => options.adapter.conversations,
@@ -202,7 +197,9 @@ export function composeRuntime(options) {
202
197
  }
203
198
  }
204
199
  function pending() {
205
- 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)));
206
203
  }
207
204
  function attention() {
208
205
  const attentionRequired = [];
@@ -231,14 +228,32 @@ export function composeRuntime(options) {
231
228
  }
232
229
  return { attentionRequired, reset };
233
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
+ }
234
242
  function listed() {
235
- return structuredClone([...rooms.values()]);
243
+ return structuredClone([...rooms.values()].map((room) => {
244
+ const documents = documentsIn(room.locator);
245
+ counted.set(room.locator, documents);
246
+ return { ...room, documents };
247
+ }));
236
248
  }
237
- function roomFacts() {
238
- 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);
239
254
  const attached = [
240
- modelOf(room.locator),
241
- ...(room.typedSelection === null ? [] : [modelOf(room.locator, 'typed-selection')]),
255
+ ...(governed.collection ? [modelOf(room.locator)] : []),
256
+ ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
242
257
  ];
243
258
  return {
244
259
  locator: room.locator,
@@ -261,6 +276,10 @@ export function composeRuntime(options) {
261
276
  for (const listener of storageLossListeners)
262
277
  listener(storageLoss);
263
278
  }
279
+ function emitMember() {
280
+ for (const listener of memberListeners)
281
+ listener(member);
282
+ }
264
283
  function moveTo(next) {
265
284
  if (closed)
266
285
  return;
@@ -280,6 +299,8 @@ export function composeRuntime(options) {
280
299
  listener(shown);
281
300
  }
282
301
  function entryOf(batch) {
302
+ if (localSaves)
303
+ return Object.freeze({ batch, state: 'local' });
283
304
  const state = pendings.get(batch.id);
284
305
  if (state?.accepted !== undefined) {
285
306
  return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
@@ -287,10 +308,7 @@ export function composeRuntime(options) {
287
308
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
288
309
  }
289
310
  function entriesOf(locator) {
290
- const replica = replicas.get(locator);
291
- if (replica === undefined)
292
- return [];
293
- return replica.queue.map(entryOf);
311
+ return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
294
312
  }
295
313
  function emitQueue(locator) {
296
314
  const entries = entriesOf(locator);
@@ -300,12 +318,47 @@ export function composeRuntime(options) {
300
318
  }
301
319
  }
302
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);
303
344
  readSide.apply(set);
304
345
  searchIndex.changed(set);
305
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
+ }
306
358
  function publishChangeSet(set) {
307
359
  applyChangeSet(set);
308
- 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)))
309
362
  emitRooms();
310
363
  for (const locator of set.queues)
311
364
  emitQueue(locator);
@@ -316,23 +369,32 @@ export function composeRuntime(options) {
316
369
  for (const app of set.presentations)
317
370
  emitPresentation(app);
318
371
  conversations.changed(set);
319
- const cards = declaredCards();
320
- if (cards !== undefined && set.documents.some((change) => change.locator === cards.locator)) {
321
- scheduleCurrency();
322
- }
323
372
  }
324
373
  function stopTimer() {
325
- if (timer === undefined)
374
+ if (resetTimer === undefined)
326
375
  return;
327
- clearInterval(timer);
328
- timer = undefined;
376
+ clearTimeout(resetTimer);
377
+ resetTimer = undefined;
329
378
  }
330
- function startTimer() {
331
- if (timer !== undefined)
379
+ function armTimer() {
380
+ stopTimer();
381
+ if (closed || stopping)
332
382
  return;
333
- timer = setInterval(() => {
334
- void enqueue();
335
- }, period);
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)
392
+ return;
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()));
336
398
  }
337
399
  async function refuse(reason, provider = null) {
338
400
  stopTimer();
@@ -343,10 +405,7 @@ export function composeRuntime(options) {
343
405
  const revoked = { status: 'unavailable', reason: 'revoked' };
344
406
  try {
345
407
  const local = await openLocal();
346
- await local.revokeModels([...replicas.values()].map((replica) => ({
347
- record: replica.confirmed.record,
348
- queue: replica.queue,
349
- })));
408
+ await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
350
409
  }
351
410
  catch {
352
411
  journal.record('model revocation persistence failed');
@@ -358,7 +417,6 @@ export function composeRuntime(options) {
358
417
  }
359
418
  for (const key of keys) {
360
419
  invalidModels.add(key);
361
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
362
420
  models.set(key, revoked);
363
421
  liveModels.delete(key);
364
422
  }
@@ -371,10 +429,7 @@ export function composeRuntime(options) {
371
429
  model: revoked,
372
430
  selectionModel: revoked,
373
431
  };
374
- const read = readConfirmed(record, before.confirmed);
375
- replicas.set(locator, read.kind === 'staged'
376
- ? replicaOf(read.confirmed, before.queue, rewriting(), before)
377
- : { ...before, confirmed: { ...before.confirmed, record } });
432
+ replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
378
433
  }
379
434
  rooms.set(locator, {
380
435
  ...room,
@@ -391,7 +446,8 @@ export function composeRuntime(options) {
391
446
  }
392
447
  finally {
393
448
  refusal = {
394
- reason,
449
+ code: reason,
450
+ retryable: false,
395
451
  reauthorizationOffered: reason === 'revoked-authorization',
396
452
  refusal: provider,
397
453
  };
@@ -404,20 +460,18 @@ export function composeRuntime(options) {
404
460
  rooms.set(room.locator, {
405
461
  name: room.name,
406
462
  locator: room.locator,
463
+ handle: room.handle,
407
464
  operations: room.operations,
408
465
  replica: room.replica,
409
- attachedModel: room.attachedModel,
410
466
  model: modelTypes(modelOf(room.locator)),
411
467
  typedSelection: selection === null
412
468
  ? null
413
469
  : {
414
470
  roots: selection.roots,
415
- attachedModel: selection.attachedModel,
416
471
  model: modelTypes(modelOf(room.locator, 'typed-selection')),
417
472
  },
418
473
  providerOwnedPaths: room.providerOwnedPaths,
419
474
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
420
- attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
421
475
  synchronizationAvailable,
422
476
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
423
477
  });
@@ -429,9 +483,6 @@ export function composeRuntime(options) {
429
483
  discovered.clear();
430
484
  for (const room of listed)
431
485
  discovered.set(room.locator, room);
432
- for (const locator of wanted.keys())
433
- if (!listedLocators.has(locator))
434
- wanted.delete(locator);
435
486
  await dropUnlisted(listedLocators, local);
436
487
  for (const room of listed) {
437
488
  const held = replicas.get(room.locator)?.confirmed.record;
@@ -445,7 +496,14 @@ export function composeRuntime(options) {
445
496
  emitRooms();
446
497
  }
447
498
  function unlistedHeld(listed) {
448
- 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));
449
507
  }
450
508
  async function dropUnlisted(listed, local) {
451
509
  if (unlistedHeld(listed).length === 0)
@@ -459,12 +517,15 @@ export function composeRuntime(options) {
459
517
  drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
460
518
  }));
461
519
  for (const locator of unlisted) {
462
- for (const batch of replicas.get(locator)?.queue ?? [])
520
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
463
521
  pendings.delete(batch.id);
464
522
  replicas.delete(locator);
523
+ queuesWithoutReplica.delete(locator);
465
524
  rooms.delete(locator);
466
525
  roomDrafts.delete(locator);
526
+ transfers.delete(locator);
467
527
  unreadable.delete(locator);
528
+ stale.delete(locator);
468
529
  scopedRefusals.delete(locator);
469
530
  sourceRefusals.delete(locator);
470
531
  declarationRefusals.delete(locator);
@@ -473,7 +534,6 @@ export function composeRuntime(options) {
473
534
  models.delete(key);
474
535
  liveModels.delete(key);
475
536
  invalidModels.delete(key);
476
- modelEpoch.delete(key);
477
537
  }
478
538
  journal.record(`room unlisted ${locator}`);
479
539
  }
@@ -486,7 +546,6 @@ export function composeRuntime(options) {
486
546
  if (prior !== undefined &&
487
547
  prior.status !== 'unavailable' &&
488
548
  (persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
489
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
490
549
  invalidModels.add(key);
491
550
  }
492
551
  const model = persisted.status === 'unavailable' ||
@@ -501,7 +560,7 @@ export function composeRuntime(options) {
501
560
  return model;
502
561
  }
503
562
  async function hydrate(opened) {
504
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
563
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
505
564
  opened.all(),
506
565
  opened.queues(),
507
566
  opened.roomDrafts(),
@@ -511,9 +570,15 @@ export function composeRuntime(options) {
511
570
  opened.lastCompleted(),
512
571
  opened.providerLimits(),
513
572
  opened.capabilities(),
573
+ opened.member(),
574
+ opened.transfers(),
514
575
  ]);
576
+ transfers.clear();
577
+ for (const record of staged)
578
+ transfers.set(record.locator, record);
515
579
  limits = declared;
516
580
  capabilities = named;
581
+ member = known;
517
582
  roomDrafts.clear();
518
583
  for (const record of kept)
519
584
  rememberRoomDraft(record);
@@ -529,19 +594,18 @@ export function composeRuntime(options) {
529
594
  }
530
595
  const previousReplicas = new Map(replicas);
531
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
+ }
532
603
  for (const record of records) {
533
604
  const model = restored(modelKey(record.locator, 'collection'), record.model);
534
605
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
535
606
  const prior = previousReplicas.get(record.locator);
536
- const retyped = { ...record, model, selectionModel };
537
- const read = readConfirmed(retyped, prior?.confirmed);
538
- const confirmed = read.kind === 'staged'
539
- ? read.confirmed
540
- : {
541
- record: retyped,
542
- collection: parseCollection(retyped.documents, compositionOf(retyped)),
543
- };
544
- 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));
545
609
  const existing = rooms.get(record.locator);
546
610
  if (existing === undefined)
547
611
  list(record, false);
@@ -549,24 +613,17 @@ export function composeRuntime(options) {
549
613
  rooms.set(record.locator, {
550
614
  ...existing,
551
615
  roomVersion: record.roomVersion,
552
- attachments: record.attachments,
553
616
  operations: record.operations,
554
617
  replica: record.replica,
555
- attachedModel: record.attachedModel,
556
618
  model: modelTypes(model),
557
619
  typedSelection: record.typedSelection === null
558
620
  ? null
559
621
  : {
560
622
  roots: record.typedSelection.roots,
561
- attachedModel: record.typedSelection.attachedModel,
562
623
  model: modelTypes(selectionModel),
563
624
  },
564
625
  });
565
626
  }
566
- if (read.kind === 'refused') {
567
- refusedSource(record, read);
568
- declarationRefusals.add(record.locator);
569
- }
570
627
  }
571
628
  return {
572
629
  lastCompleted,
@@ -579,6 +636,8 @@ export function composeRuntime(options) {
579
636
  applyChangeSet(read);
580
637
  if (replicas.size > 0)
581
638
  emitRooms();
639
+ if (member !== null)
640
+ emitMember();
582
641
  for (const locator of locators)
583
642
  emitQueue(locator);
584
643
  publishRoomDrafts(false);
@@ -626,10 +685,15 @@ export function composeRuntime(options) {
626
685
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
627
686
  }
628
687
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
629
- const withdrawn = withdrawals(confirmed, queue, withdrawable, rewriting());
688
+ const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
689
+ const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
630
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
+ }));
631
695
  const returning = [
632
- ...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
696
+ ...[...unqueued].reverse(),
633
697
  ...(settling.leaving === undefined ? [] : [settling.leaving]),
634
698
  ];
635
699
  const written = new Map();
@@ -647,57 +711,110 @@ export function composeRuntime(options) {
647
711
  });
648
712
  }
649
713
  }
650
- await local.write({
651
- next: confirmed.record,
652
- previous: before?.confirmed.record ?? null,
653
- queue: kept,
654
- previousQueue: before?.queue ?? [],
655
- roomDrafts: [...written.values()],
656
- });
657
- for (const batch of before?.queue ?? []) {
714
+ await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
715
+ for (const batch of previousQueue) {
658
716
  if (!kept.includes(batch))
659
717
  pendings.delete(batch.id);
660
718
  }
661
719
  for (const record of written.values())
662
720
  rememberRoomDraft(record);
663
- for (const batch of withdrawn)
664
- journal.record(`outcome ${locator} ${batch.id} withdrawn`);
665
- 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));
666
749
  return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
667
750
  room: settling.room,
668
- queue: kept.length !== (before?.queue.length ?? 0),
751
+ queue: kept.length !== previousQueue.length,
669
752
  drafts: [...written.keys()],
670
753
  });
671
754
  }
672
- 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;
673
788
  return exclusive(async () => {
674
789
  if (!continuing())
675
- return;
790
+ return true;
676
791
  const prior = replicas.get(room.locator);
677
- const retained = readConfirmed({
678
- ...confirmed.record,
679
- model: modelOf(room.locator),
680
- selectionModel: modelOf(room.locator, 'typed-selection'),
681
- }, prior?.confirmed);
682
- if (retained.kind !== 'staged')
683
- throw new Error(`Cannot retype staged Room ${room.locator}`);
684
- const queue = prior?.queue ?? [];
685
- 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;
686
800
  const accepted = queue.filter((batch) => {
687
801
  const accepted = pendings.get(batch.id)?.accepted;
688
802
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
689
803
  });
690
- const set = await settle(local, room.locator, prior, retained.confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
691
- const target = wanted.get(room.locator);
692
- if (target !== undefined && options.adapter.reached(room.locator, roomVersion, target)) {
693
- wanted.delete(room.locator);
694
- }
695
- unreadable.delete(room.locator);
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);
807
+ for (const batch of accepted)
808
+ pendings.delete(batch.id);
809
+ stale.delete(room.locator);
810
+ if (unreadable.delete(room.locator))
811
+ scopedRefusals.delete(room.locator);
696
812
  sourceRefusals.delete(room.locator);
697
813
  declarationRefusals.delete(room.locator);
698
814
  list(room, true);
699
815
  publishChangeSet(set);
700
816
  moveTo({ ...status, ...attention() });
817
+ return true;
701
818
  });
702
819
  }
703
820
  async function saveRoomDrafts(locator, named, writtenBy) {
@@ -705,61 +822,140 @@ export function composeRuntime(options) {
705
822
  const replica = replicas.get(locator);
706
823
  const room = rooms.get(locator);
707
824
  if (replica === undefined || room === undefined) {
708
- throw new Error(`No Local Replica for ${locator}`);
825
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
709
826
  }
710
827
  const saved = [];
711
828
  for (const { path, revision } of named) {
712
829
  const record = roomDrafts.get(locator)?.get(path);
713
830
  if (record === undefined || record.revision !== revision) {
714
- return { outcome: 'refused', rule: 'stale', file: path };
831
+ return { outcome: 'refused', code: 'stale', retryable: false, file: path };
715
832
  }
716
833
  if (!saved.some((draft) => draft.path === path))
717
834
  saved.push({ path, parts: record.parts });
718
835
  }
719
836
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
720
837
  if (changed.length > 0)
721
- return { outcome: 'refused', rule: 'changed-underneath', parts: changed };
838
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
722
839
  const parts = saved.map(({ path, parts }) => changesOf(path, parts));
723
- 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,
724
859
  baseRoomVersion: replica.confirmed.record.roomVersion,
725
860
  composition: compositionFor(replica.confirmed.record),
726
861
  operations: room.operations,
727
862
  providerOwnedPaths: room.providerOwnedPaths,
728
863
  providerLimits: limits,
729
- rewriting: rewriting(),
864
+ handle: room.handle,
730
865
  visible: replica.visible,
731
- changes: parts.flatMap(({ changes }) => changes),
866
+ changes,
732
867
  bases: parts.flatMap(({ bases }) => bases),
733
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 }];
734
876
  });
735
- 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')
736
895
  return { outcome: 'refused', ...built.refusal };
737
- 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
+ }
738
927
  const consumed = saved.map(({ path }) => path);
739
928
  try {
740
- await local.write({
741
- next: replica.confirmed.record,
742
- previous: replica.confirmed.record,
929
+ await local.keepAuthored({
930
+ locator,
743
931
  queue: batches,
744
- previousQueue: replica.queue,
745
- saved,
932
+ saved: drafted,
746
933
  consumedRoomDrafts: consumed,
747
934
  });
748
935
  }
749
936
  catch {
750
- 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 });
751
943
  }
752
- pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
944
+ if (!localSaves)
945
+ recorded = false;
753
946
  for (const path of consumed)
754
947
  forgetRoomDraft(locator, path);
755
- replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting(), replica));
948
+ replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
756
949
  publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
757
950
  queue: true,
758
951
  drafts: consumed,
759
952
  }));
760
- moveTo({ ...saving(status), ...attention() });
761
- void enqueue();
762
- 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 };
763
959
  }
764
960
  async function mark(local, locator, identifier, submission, lost) {
765
961
  const state = pendings.get(identifier);
@@ -785,16 +981,15 @@ export function composeRuntime(options) {
785
981
  }
786
982
  async function readHistory(locator, path, query = {}) {
787
983
  if (closed || refusal !== null)
788
- return { kind: 'transport-uncertainty' };
984
+ return UNCERTAIN;
789
985
  try {
790
986
  await ensureAuthorization();
791
987
  }
792
988
  catch {
793
- return { kind: 'transport-uncertainty' };
794
- }
795
- if (closed || authorization === undefined || refusal !== null) {
796
- return { kind: 'transport-uncertainty' };
989
+ return UNCERTAIN;
797
990
  }
991
+ if (closed || authorization === undefined || refusal !== null)
992
+ return UNCERTAIN;
798
993
  const transfer = ['transfer', 'history', locator, path];
799
994
  if (query.revision !== undefined)
800
995
  transfer.push(query.revision);
@@ -816,17 +1011,101 @@ export function composeRuntime(options) {
816
1011
  }
817
1012
  if (result.kind === 'revoked-authorization')
818
1013
  await refuse('revoked-authorization');
819
- return result;
1014
+ return historyAnswer(result);
820
1015
  }
821
1016
  catch {
822
1017
  journal.record('outcome history transport-uncertainty');
823
- return { kind: 'transport-uncertainty' };
1018
+ return UNCERTAIN;
1019
+ }
1020
+ }
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;
824
1058
  }
825
1059
  }
826
- function declaredCards() {
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() {
827
1106
  for (const capability of capabilities) {
828
- if (capability.conversationCards !== undefined)
829
- return capability.conversationCards;
1107
+ if (capability.conversationAgents !== undefined)
1108
+ return capability.conversationAgents;
830
1109
  }
831
1110
  return undefined;
832
1111
  }
@@ -842,23 +1121,20 @@ export function composeRuntime(options) {
842
1121
  async function invokeAction(name, input) {
843
1122
  await ready;
844
1123
  if (closed || refusal !== null)
845
- return { kind: 'transport-uncertainty' };
1124
+ return UNCERTAIN;
846
1125
  if (declaredAction(name) === undefined)
847
- return { kind: 'undeclared' };
1126
+ return undeclared(name);
848
1127
  try {
849
1128
  await ensureAuthorization();
850
1129
  }
851
1130
  catch {
852
- return { kind: 'transport-uncertainty' };
853
- }
854
- if (closed || authorization === undefined || refusal !== null) {
855
- return { kind: 'transport-uncertainty' };
1131
+ return UNCERTAIN;
856
1132
  }
1133
+ if (closed || authorization === undefined || refusal !== null)
1134
+ return UNCERTAIN;
857
1135
  journal.record(`transfer action ${name}`);
858
1136
  try {
859
1137
  const result = await options.adapter.action(authorization, { name, input });
860
- if (result.kind === 'action' && result.at !== undefined)
861
- want(result.at);
862
1138
  if (result.kind === 'scoped-refusal') {
863
1139
  const room = replicas.get(result.locator)?.confirmed.record;
864
1140
  if (room !== undefined)
@@ -869,11 +1145,11 @@ export function composeRuntime(options) {
869
1145
  journal.record(`outcome action ${result.kind}`);
870
1146
  if (result.kind === 'rejected')
871
1147
  journal.record(`refusal rejected ${result.code}`);
872
- return result;
1148
+ return calledResult(result);
873
1149
  }
874
1150
  catch {
875
1151
  journal.record('outcome action transport-uncertainty');
876
- return { kind: 'transport-uncertainty' };
1152
+ return UNCERTAIN;
877
1153
  }
878
1154
  }
879
1155
  function continuing() {
@@ -890,32 +1166,11 @@ export function composeRuntime(options) {
890
1166
  options.adapter.reached(locator, written, roomVersion) &&
891
1167
  !options.adapter.reached(locator, roomVersion, written));
892
1168
  }
893
- function want({ locator, roomVersion }) {
894
- if (reachedBy(locator, roomVersion))
895
- return;
896
- const held = wanted.get(locator);
897
- if (held === undefined || options.adapter.reached(locator, roomVersion, held)) {
898
- wanted.set(locator, roomVersion);
899
- }
900
- }
901
- function reachOf(locator) {
902
- let target = wanted.get(locator);
903
- const versions = (replicas.get(locator)?.queue ?? []).flatMap((batch) => {
904
- const accepted = pendings.get(batch.id)?.accepted;
905
- return accepted === undefined ? [] : [accepted];
906
- });
907
- for (const version of versions) {
908
- if (target === undefined || options.adapter.reached(locator, version, target)) {
909
- target = version;
910
- }
911
- }
912
- return target === undefined || reachedBy(locator, target) ? undefined : target;
913
- }
914
- async function pullRefresh(authorized, confirmed, minPosition) {
1169
+ async function pullRefresh(authorized, confirmed) {
915
1170
  if (!continuing())
916
1171
  return unconfirmed;
917
1172
  try {
918
- 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);
919
1174
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
920
1175
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
921
1176
  return result;
@@ -925,14 +1180,33 @@ export function composeRuntime(options) {
925
1180
  throw error;
926
1181
  }
927
1182
  }
928
- 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) {
929
1203
  if (!continuing())
930
1204
  return unconfirmed;
931
1205
  try {
932
1206
  const result = await bootstrapRoom(options.adapter, authorized, room, {
933
1207
  collection: modelOf(room.locator),
934
1208
  selection: modelOf(room.locator, 'typed-selection'),
935
- }, continuing);
1209
+ }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
936
1210
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
937
1211
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
938
1212
  return result;
@@ -945,35 +1219,46 @@ export function composeRuntime(options) {
945
1219
  async function advance(room, local, authorized) {
946
1220
  const held = replicas.get(room.locator);
947
1221
  if (held !== undefined) {
948
- const refreshed = await pullRefresh(authorized, held.confirmed, reachOf(room.locator));
1222
+ const refreshed = await pullRefresh(authorized, held.confirmed);
949
1223
  if (!continuing())
950
1224
  return unconfirmed;
951
1225
  if (refreshed.kind === 'still-current') {
1226
+ stale.delete(room.locator);
952
1227
  if (sourceRefusals.has(room.locator))
953
- return unconfirmed;
1228
+ return null;
954
1229
  if (unreadable.delete(room.locator)) {
1230
+ scopedRefusals.delete(room.locator);
955
1231
  list(room, true);
956
1232
  emitRooms();
957
1233
  }
958
1234
  return null;
959
1235
  }
960
1236
  if (refreshed.kind === 'advanced') {
961
- 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));
962
1244
  return null;
963
1245
  }
964
1246
  if (refreshed.kind !== 'beyond-horizon')
965
1247
  return refreshed;
966
1248
  }
967
- const bootstrapped = await pullBootstrap(authorized, room);
1249
+ const bootstrapped = await pullBootstrap(authorized, room, local);
968
1250
  if (!continuing())
969
1251
  return unconfirmed;
970
1252
  if (bootstrapped.kind !== 'staged')
971
1253
  return bootstrapped;
972
- await publish(local, room, bootstrapped.confirmed);
1254
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
973
1255
  return null;
974
1256
  }
975
1257
  async function withhold(withheld, room, local, progress) {
976
- progress.complete = false;
1258
+ if (withheld.kind === 'refused')
1259
+ stale.delete(room.locator);
1260
+ else
1261
+ stale.add(room.locator);
977
1262
  if (withheld.kind === 'scoped-refusal') {
978
1263
  await scoped(withheld, room, local);
979
1264
  }
@@ -998,6 +1283,7 @@ export function composeRuntime(options) {
998
1283
  function refusedSource(room, { refusal }) {
999
1284
  sourceRefusals.set(room.locator, {
1000
1285
  code: refusal.rule,
1286
+ retryable: false,
1001
1287
  fields: {
1002
1288
  file: refusal.file,
1003
1289
  ...(refusal.location === undefined ? {} : { location: refusal.location }),
@@ -1012,52 +1298,46 @@ export function composeRuntime(options) {
1012
1298
  }
1013
1299
  function leaving(batch, result) {
1014
1300
  if (result.kind === 'expired')
1015
- return { batch, returned: { kind: 'expired' } };
1016
- return { batch, returned: { kind: result.kind, code: result.code, fields: result.fields } };
1301
+ return { batch, returned: returnedBy('expired') };
1302
+ return {
1303
+ batch,
1304
+ returned: {
1305
+ kind: result.kind,
1306
+ code: result.code,
1307
+ fields: result.fields,
1308
+ retryable: result.retryable,
1309
+ },
1310
+ };
1017
1311
  }
1018
1312
  function logAccepted(locator, submitted, accepted) {
1019
1313
  journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
1020
1314
  (accepted.author === undefined ? '' : ` author ${accepted.author}`) +
1021
1315
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1022
1316
  }
1023
- async function recordAccepted(room, local, submitted, accepted) {
1024
- await exclusive(async () => {
1025
- if (!continuing())
1026
- return;
1027
- const current = replicas.get(room.locator);
1028
- if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1029
- return;
1030
- }
1031
- const state = pendings.get(submitted.id);
1032
- if (state === undefined)
1033
- return;
1034
- await local.accept(room.locator, submitted.id, accepted.roomVersion);
1035
- pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1036
- logAccepted(room.locator, submitted, accepted);
1037
- emitQueue(room.locator);
1038
- });
1039
- }
1040
- async function commitAccept(room, local, submitted, accepted, base, overlay) {
1317
+ async function adoptAccepted(room, local, submitted, accepted) {
1041
1318
  if (!continuing())
1042
1319
  return unconfirmed;
1043
1320
  const current = replicas.get(room.locator);
1044
1321
  if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1045
1322
  return null;
1046
1323
  }
1047
- const remaining = current.queue.filter((batch) => batch.id !== submitted.id);
1048
- const retainedBase = readConfirmed({
1049
- ...base.record,
1050
- model: modelOf(room.locator),
1051
- selectionModel: modelOf(room.locator, 'typed-selection'),
1052
- }, base);
1053
- const applied = overlay && retainedBase.kind === 'staged'
1054
- ? applyReturned(retainedBase.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion)
1055
- : retainedBase;
1056
- 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);
1057
1337
  return applied;
1058
- const set = await settle(local, room.locator, current, applied.confirmed, remaining, {
1059
- room: true,
1060
- });
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);
1061
1341
  unreadable.delete(room.locator);
1062
1342
  sourceRefusals.delete(room.locator);
1063
1343
  declarationRefusals.delete(room.locator);
@@ -1066,17 +1346,8 @@ export function composeRuntime(options) {
1066
1346
  moveTo({ ...status, ...attention() });
1067
1347
  return null;
1068
1348
  }
1069
- async function accepting(room, local, submitted, accepted, pulled, fresh) {
1070
- const roomVersion = pulled.record.roomVersion;
1071
- if (options.adapter.reached(room.locator, roomVersion, accepted.roomVersion)) {
1072
- return committed(room, local, submitted, accepted, pulled, roomVersion === accepted.roomVersion);
1073
- }
1074
- if (fresh && continuing())
1075
- await publish(local, room, pulled);
1076
- return false;
1077
- }
1078
- async function committed(room, local, submitted, accepted, base, overlay) {
1079
- 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));
1080
1351
  if (failed === null)
1081
1352
  return true;
1082
1353
  if (failed.kind === 'refused' && continuing())
@@ -1089,10 +1360,8 @@ export function composeRuntime(options) {
1089
1360
  const head = replica?.queue[0];
1090
1361
  if (replica === undefined || head === undefined)
1091
1362
  return;
1092
- if (pendings.get(head.id)?.accepted !== undefined) {
1093
- progress.complete = false;
1363
+ if (pendings.get(head.id)?.accepted !== undefined)
1094
1364
  return;
1095
- }
1096
1365
  const state = pendings.get(head.id);
1097
1366
  const { submission, lost: earlier } = state ?? {
1098
1367
  submission: { state: 'waiting' },
@@ -1120,6 +1389,7 @@ export function composeRuntime(options) {
1120
1389
  return;
1121
1390
  }
1122
1391
  const opensBefore = openSignals;
1392
+ const answered = awaitingAnswer(room.locator);
1123
1393
  try {
1124
1394
  result = await options.adapter.mutate(authorized, {
1125
1395
  locator: room.locator,
@@ -1128,11 +1398,14 @@ export function composeRuntime(options) {
1128
1398
  }, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
1129
1399
  }
1130
1400
  catch {
1401
+ answered();
1131
1402
  progress.complete = false;
1132
1403
  if (continuing())
1133
1404
  progress.reachable = false;
1134
1405
  return;
1135
1406
  }
1407
+ if (result.kind !== 'accepted' || !continuing())
1408
+ answered();
1136
1409
  if (!continuing())
1137
1410
  return;
1138
1411
  if (result.kind === 'transport-uncertainty') {
@@ -1185,6 +1458,7 @@ export function composeRuntime(options) {
1185
1458
  const reset = result.reset;
1186
1459
  progress.complete = false;
1187
1460
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1461
+ armTimer();
1188
1462
  return;
1189
1463
  }
1190
1464
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
@@ -1197,7 +1471,8 @@ export function composeRuntime(options) {
1197
1471
  return;
1198
1472
  }
1199
1473
  const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1200
- 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))
1201
1476
  ? { ...left, returned: null }
1202
1477
  : left,
1203
1478
  });
@@ -1211,95 +1486,33 @@ export function composeRuntime(options) {
1211
1486
  return withhold(withheld, room, local, progress);
1212
1487
  continue;
1213
1488
  }
1214
- await recordAccepted(room, local, head, result);
1215
- const from = replicas.get(room.locator)?.confirmed;
1216
- if (from === undefined)
1217
- return;
1218
- const pulled = await pullRefresh(authorized, from, reachOf(room.locator));
1219
- if (!continuing())
1220
- return;
1221
- if (pulled.kind === 'revoked-authorization') {
1222
- await refuse('revoked-authorization');
1223
- return;
1224
- }
1225
- if (pulled.kind === 'scoped-refusal') {
1226
- await scoped(pulled, room, local);
1227
- progress.complete = false;
1228
- return;
1229
- }
1230
- if (pulled.kind === 'still-current' || pulled.kind === 'advanced') {
1231
- const fresh = pulled.kind === 'advanced';
1232
- if (!(await accepting(room, local, head, result, fresh ? pulled.confirmed : from, fresh))) {
1233
- progress.complete = false;
1234
- return;
1235
- }
1236
- continue;
1237
- }
1238
- if (pulled.kind === 'beyond-horizon') {
1239
- const bootstrapped = await pullBootstrap(authorized, room);
1240
- if (!continuing())
1241
- return;
1242
- if (bootstrapped.kind === 'revoked-authorization') {
1243
- await refuse('revoked-authorization');
1244
- return;
1245
- }
1246
- if (bootstrapped.kind === 'scoped-refusal') {
1247
- await scoped(bootstrapped, room, local);
1248
- progress.complete = false;
1249
- return;
1250
- }
1251
- if (bootstrapped.kind === 'staged') {
1252
- if (!(await accepting(room, local, head, result, bootstrapped.confirmed, true))) {
1253
- progress.complete = false;
1254
- return;
1255
- }
1256
- continue;
1257
- }
1489
+ const kept = await adopted(room, local, head, result).finally(answered);
1490
+ if (!kept) {
1258
1491
  progress.complete = false;
1259
- if (bootstrapped.kind === 'transport-uncertainty') {
1260
- progress.reachable = false;
1261
- }
1262
- else if (bootstrapped.kind === 'room-not-readable') {
1263
- unreadable.add(room.locator);
1264
- list(room, false);
1265
- emitRooms();
1266
- }
1267
- else if (bootstrapped.kind === 'refused') {
1268
- refusedSource(room, bootstrapped);
1269
- }
1270
1492
  return;
1271
1493
  }
1272
- progress.complete = false;
1273
- if (pulled.kind === 'transport-uncertainty') {
1274
- progress.reachable = false;
1275
- }
1276
- else if (pulled.kind === 'room-not-readable') {
1277
- unreadable.add(room.locator);
1278
- list(room, false);
1279
- emitRooms();
1280
- }
1281
- else if (pulled.kind === 'retryable-refusal') {
1282
- journal.record(`refusal ${pulled.kind} ${pulled.code}`);
1283
- }
1284
- else if (pulled.kind === 'refused') {
1285
- refusedSource(room, pulled);
1286
- }
1287
- return;
1288
1494
  }
1289
- 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
+ };
1290
1507
  }
1291
1508
  async function retainModel(local, room, attached, next) {
1292
1509
  const key = modelKey(room.locator, attached);
1293
- if (next.status === 'unavailable')
1294
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
1295
- const epoch = modelEpoch.get(key) ?? 0;
1296
1510
  const previous = modelOf(room.locator, attached);
1297
1511
  const heldBefore = replicas.get(room.locator)?.confirmed.record;
1298
1512
  if (JSON.stringify(previous) === JSON.stringify(next) &&
1299
1513
  (heldBefore === undefined ||
1300
1514
  (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
1301
1515
  JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
1302
- JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
1303
1516
  JSON.stringify(heldBefore.typedSelection) ===
1304
1517
  JSON.stringify(room.typedSelection ?? null))))
1305
1518
  return;
@@ -1310,20 +1523,17 @@ export function composeRuntime(options) {
1310
1523
  await exclusive(async () => {
1311
1524
  if (!continuing())
1312
1525
  return;
1313
- if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
1314
- return;
1315
1526
  const held = replicas.get(room.locator);
1316
1527
  if (held !== undefined) {
1317
1528
  const declared = {
1318
1529
  ...held.confirmed.record,
1319
1530
  operations: room.operations,
1320
1531
  replica: room.replica,
1321
- attachedModel: room.attachedModel,
1322
1532
  typedSelection: room.typedSelection ?? null,
1323
1533
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1324
1534
  };
1325
1535
  const reclassified = sameMembership(declared, held.confirmed.record)
1326
- ? readConfirmed(declared, held.confirmed)
1536
+ ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
1327
1537
  : readReclassified(declared, held.confirmed);
1328
1538
  const record = reclassified.kind === 'staged'
1329
1539
  ? declared
@@ -1331,14 +1541,12 @@ export function composeRuntime(options) {
1331
1541
  ...declared,
1332
1542
  typedSelection: held.confirmed.record.typedSelection,
1333
1543
  };
1334
- const read = reclassified.kind === 'deferred' ? readConfirmed(record, held.confirmed) : reclassified;
1335
- await local.write({
1336
- next: record,
1337
- previous: held.confirmed.record,
1338
- queue: held.queue,
1339
- });
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 });
1340
1548
  if (read.kind === 'staged') {
1341
- replicas.set(room.locator, replicaOf(read.confirmed, held.queue, rewriting(), held));
1549
+ replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1342
1550
  if (declarationRefusals.delete(room.locator)) {
1343
1551
  sourceRefusals.delete(room.locator);
1344
1552
  unreadable.delete(room.locator);
@@ -1362,79 +1570,12 @@ export function composeRuntime(options) {
1362
1570
  publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
1363
1571
  });
1364
1572
  }
1365
- async function acquireModel(local, room, authorized, selection) {
1366
- const attached = selection === null ? 'collection' : 'typed-selection';
1367
- const key = modelKey(room.locator, attached);
1368
- const declared = selection === null ? room.attachedModel : selection.attachedModel;
1369
- let previous = modelOf(room.locator, attached);
1370
- if (previous.status !== 'unavailable' &&
1371
- (declared === null || !sameModel(previous.snapshot, declared))) {
1372
- previous = { status: 'unavailable', reason: 'version-changed' };
1373
- await retainModel(local, room, attached, previous);
1374
- if (discoveryPaused)
1375
- return;
1376
- }
1377
- const epoch = modelEpoch.get(key) ?? 0;
1378
- let answer;
1379
- try {
1380
- answer = await options.adapter.model(authorized, selection === null
1381
- ? { locator: room.locator, attached: 'collection', model: declared }
1382
- : {
1383
- locator: room.locator,
1384
- attached: 'typed-selection',
1385
- roots: selection.roots,
1386
- model: declared,
1387
- });
1388
- }
1389
- catch {
1390
- answer = { kind: 'transport-uncertainty' };
1391
- }
1392
- if (discoveryPaused || epoch !== (modelEpoch.get(key) ?? 0))
1393
- return;
1394
- if (answer.kind === 'revoked-authorization') {
1395
- await refuse('revoked-authorization');
1396
- return;
1397
- }
1398
- if (answer.kind === 'transport-uncertainty') {
1399
- await retainModel(local, room, attached, cached(previous));
1400
- return;
1401
- }
1402
- if (answer.kind === 'model-unavailable') {
1403
- await retainModel(local, room, attached, {
1404
- status: 'unavailable',
1405
- reason: answer.reason,
1406
- });
1407
- return;
1408
- }
1409
- if (declared === null || !sameModel(answer.snapshot, declared)) {
1410
- await retainModel(local, room, attached, {
1411
- status: 'unavailable',
1412
- reason: 'version-changed',
1413
- });
1414
- return;
1415
- }
1416
- if (previous.status !== 'unavailable' &&
1417
- sameModel(previous.snapshot, answer.snapshot) &&
1418
- previous.snapshot.schema !== answer.snapshot.schema) {
1419
- await retainModel(local, room, attached, { status: 'unavailable', reason: 'invalid' });
1420
- return;
1421
- }
1422
- const verified = answer.snapshot.schema === null ? EMPTY_SCHEMA : verify(answer.snapshot.schema);
1423
- if (verified.verdict === 'refuse') {
1424
- const where = verified.location === undefined ? '' : ` ${verified.location}`;
1425
- journal.record(`model invalid ${room.locator} ${attached} ${answer.snapshot.identity} ${verified.rule}${where}`);
1426
- }
1427
- await retainModel(local, room, attached, verified.verdict === 'accept'
1428
- ? {
1429
- status: 'current',
1430
- snapshot: structuredClone(answer.snapshot),
1431
- verifiedWith: MD_MODEL,
1432
- types: verified.model.types,
1433
- }
1434
- : { status: 'unavailable', reason: 'invalid' });
1435
- }
1436
1573
  async function scoped(result, room, local) {
1437
- 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
+ });
1438
1579
  let scopedAway = room;
1439
1580
  if (result.scope === 'model')
1440
1581
  await retainModel(local, room, 'collection', {
@@ -1460,14 +1601,35 @@ export function composeRuntime(options) {
1460
1601
  emitRooms();
1461
1602
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
1462
1603
  }
1463
- async function synchronize(discovered, local, progress) {
1464
- const prepared = await Promise.allSettled(discovered.map((room) => prepareRoom(room, local, progress)));
1465
- 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) => {
1466
1622
  const outcome = prepared[index];
1467
1623
  return outcome?.status === 'fulfilled' && outcome.value;
1468
1624
  });
1469
- const rooms = await Promise.allSettled(ready.map((room) => synchronizeRoom(room, local, progress)));
1470
- 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');
1471
1633
  if (failed !== undefined)
1472
1634
  throw failed.reason;
1473
1635
  }
@@ -1484,11 +1646,8 @@ export function composeRuntime(options) {
1484
1646
  progress.complete = false;
1485
1647
  return false;
1486
1648
  }
1487
- const selection = room.typedSelection;
1488
- await Promise.all([
1489
- acquireModel(local, room, authorization, null),
1490
- ...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
1491
- ]);
1649
+ if (prior !== undefined)
1650
+ await retainModel(local, room, 'collection', modelOf(room.locator));
1492
1651
  if (discoveryPaused || authorization === undefined)
1493
1652
  return false;
1494
1653
  if (room.replica.admission !== 'admitted') {
@@ -1496,24 +1655,53 @@ export function composeRuntime(options) {
1496
1655
  progress.complete = false;
1497
1656
  return false;
1498
1657
  }
1499
- const held = replicas.get(room.locator);
1500
- if (held !== undefined && held.confirmed.record.replica.selection !== room.replica.selection) {
1501
- progress.complete = false;
1502
- return false;
1503
- }
1504
1658
  return true;
1505
1659
  }
1506
- 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) {
1507
1678
  if (discoveryPaused || authorization === undefined)
1508
1679
  return;
1680
+ if (replicas.has(room.locator) && revoked(room.locator)) {
1681
+ await reload(room, local, progress);
1682
+ return;
1683
+ }
1509
1684
  const withheld = await advance(room, local, authorization);
1510
1685
  if (withheld !== null) {
1511
1686
  await withhold(withheld, room, local, progress);
1512
1687
  return;
1513
1688
  }
1514
- await submitRoom(room, local, authorization, progress);
1689
+ await caughtUp(room, local, progress);
1515
1690
  }
1516
- async function cycle(local, progress) {
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);
1703
+ }
1704
+ async function discoverRooms(local, progress, attempt) {
1517
1705
  if (closed || authorization === undefined) {
1518
1706
  notifyReady();
1519
1707
  return;
@@ -1539,12 +1727,17 @@ export function composeRuntime(options) {
1539
1727
  }
1540
1728
  }
1541
1729
  progress.reachable = false;
1730
+ progress.complete = false;
1542
1731
  notifyReady();
1543
1732
  return;
1544
1733
  }
1545
1734
  if (result.kind === 'contract-unavailable') {
1546
1735
  journal.record(`connection contract unavailable ${result.code}`);
1547
- 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
+ });
1548
1741
  notifyReady();
1549
1742
  return;
1550
1743
  }
@@ -1553,17 +1746,6 @@ export function composeRuntime(options) {
1553
1746
  notifyReady();
1554
1747
  return;
1555
1748
  }
1556
- if (!result.supportedVersions.includes(ROOM_CONTRACT_VERSION)) {
1557
- await refuse('no-matching-version');
1558
- notifyReady();
1559
- return;
1560
- }
1561
- if (result.mdModel.identity !== MD_MODEL.identity ||
1562
- result.mdModel.version !== MD_MODEL.version) {
1563
- await refuse('md-model-version-mismatch');
1564
- notifyReady();
1565
- return;
1566
- }
1567
1749
  for (const room of result.rooms) {
1568
1750
  if (room.operations === undefined ||
1569
1751
  !['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
@@ -1571,8 +1753,7 @@ export function composeRuntime(options) {
1571
1753
  room.replica === undefined ||
1572
1754
  typeof room.replica.selection !== 'string' ||
1573
1755
  room.replica.selection.length === 0 ||
1574
- !['admitted', 'unavailable'].includes(room.replica.admission) ||
1575
- !declaredIdentity(room.attachedModel)) {
1756
+ !['admitted', 'unavailable'].includes(room.replica.admission)) {
1576
1757
  journal.record(`invalid model or replica declaration ${room.locator}`);
1577
1758
  await refuse('missing-required-guarantee');
1578
1759
  notifyReady();
@@ -1585,14 +1766,6 @@ export function composeRuntime(options) {
1585
1766
  return;
1586
1767
  }
1587
1768
  }
1588
- const declared = new Set(result.capabilities.map((capability) => capability.name));
1589
- for (const required of options.requiredGuarantees) {
1590
- if (!knownCapabilities.has(required) || !declared.has(required)) {
1591
- await refuse('missing-required-guarantee');
1592
- notifyReady();
1593
- return;
1594
- }
1595
- }
1596
1769
  if (refusal !== null) {
1597
1770
  refusal = null;
1598
1771
  emitRefusal();
@@ -1600,26 +1773,19 @@ export function composeRuntime(options) {
1600
1773
  const previousCapabilities = capabilities;
1601
1774
  capabilities = result.capabilities;
1602
1775
  notifyReady();
1603
- if (declared.has('room-version-notifications'))
1604
- observeRoomVersions();
1605
1776
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1606
1777
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
1607
1778
  limits = result.providerLimits;
1608
1779
  await local.declare(limits, capabilities);
1609
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();
1786
+ }
1610
1787
  await applyDiscovery(result.rooms, local);
1611
- handSelected(result.rooms
1612
- .filter((room) => room.replica.admission === 'admitted')
1613
- .map((room) => room.locator)
1614
- .sort());
1615
- progress.complete = true;
1616
- await synchronize(result.rooms, local, progress);
1617
- if (closed || discoveryPaused)
1618
- return;
1619
- await conversations.reconcile();
1620
- if (status.providerUnreachable)
1621
- conversations.reconnected();
1622
- await conversations.catchUp(progress);
1788
+ await synchronize(result.rooms, local, progress, attempt);
1623
1789
  }
1624
1790
  async function ensureAuthorization() {
1625
1791
  if (authorization !== undefined)
@@ -1633,64 +1799,262 @@ export function composeRuntime(options) {
1633
1799
  throw error;
1634
1800
  }
1635
1801
  }
1636
- async function connect() {
1637
- if (closed || stopping || discoveryPaused) {
1638
- 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');
1639
1856
  return;
1640
1857
  }
1641
- startTimer();
1642
- const progress = { complete: false, reachable: true };
1643
- let completedAt = new Date().toISOString();
1644
- try {
1645
- const local = await openLocal();
1646
- if (closed)
1647
- return;
1648
- await ensureAuthorization();
1649
- 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)
1650
1872
  return;
1651
- await cycle(local, progress);
1652
- if (closed)
1873
+ if (progress === null) {
1874
+ await reloadExpired(expired);
1653
1875
  return;
1654
- if (progress.complete) {
1655
- completedAt = new Date().toISOString();
1656
- await local.complete(completedAt);
1657
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)));
1658
1905
  }
1659
1906
  catch {
1660
- progress.complete = false;
1661
- 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;
1662
1925
  return;
1663
1926
  }
1664
- finally {
1665
- notifyReady();
1666
- 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);
1667
1959
  }
1668
1960
  }
1669
- async function run() {
1961
+ async function finishOpening(progress) {
1670
1962
  try {
1671
- 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;
1672
1978
  }
1673
1979
  finally {
1674
- inflight = false;
1675
- if (queued && !closed) {
1676
- queued = false;
1677
- void enqueue();
1678
- }
1980
+ complete = progress.complete;
1981
+ await rest(progress);
1679
1982
  }
1680
1983
  }
1681
- function observeRoomVersions() {
1682
- const path = options.adapter.roomVersions;
1683
- if (path === undefined || withdrawNotifications !== undefined)
1984
+ async function rest(progress) {
1985
+ if (closed)
1684
1986
  return;
1685
- withdrawNotifications = path.observe((notification) => {
1686
- 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)
1687
2012
  return;
1688
- if (notification.modelChanged === true && handed?.includes(notification.locator) === true) {
1689
- void enqueue();
2013
+ const room = discovered.get(locator);
2014
+ const held = replicas.get(locator);
2015
+ if (room === undefined || room.replica.admission !== 'admitted')
1690
2016
  return;
2017
+ if (held !== undefined &&
2018
+ (held.confirmed.record.replica.selection !== room.replica.selection ||
2019
+ options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
2020
+ return;
2021
+ }
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');
1691
2051
  }
1692
- notices.set(notification.locator, notification.roomVersion);
1693
- chain = chain.then(runNotified, runNotified);
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);
1694
2058
  });
1695
2059
  }
1696
2060
  function writesHeld() {
@@ -1711,14 +2075,17 @@ export function composeRuntime(options) {
1711
2075
  listener(mode);
1712
2076
  }
1713
2077
  if (releasing)
1714
- void enqueue();
2078
+ submitAll();
1715
2079
  }
1716
2080
  function observeConversations() {
1717
2081
  const stream = options.adapter.conversations;
1718
2082
  if (stream === undefined || withdrawConversations !== undefined)
1719
2083
  return;
1720
2084
  withdrawConversations = stream.observe((event) => {
1721
- conversations.receive(event);
2085
+ if (conversations.receive(event)) {
2086
+ void ready.then(scheduleCurrency);
2087
+ return;
2088
+ }
1722
2089
  if (event.kind !== 'gap' || conversations.current())
1723
2090
  return;
1724
2091
  show();
@@ -1729,7 +2096,7 @@ export function composeRuntime(options) {
1729
2096
  if (currencyQueued || closed || stopping)
1730
2097
  return;
1731
2098
  currencyQueued = true;
1732
- chain = chain.then(runCurrency, runCurrency);
2099
+ void task(runCurrency);
1733
2100
  }
1734
2101
  async function runCurrency() {
1735
2102
  currencyQueued = false;
@@ -1740,110 +2107,22 @@ export function composeRuntime(options) {
1740
2107
  show();
1741
2108
  return;
1742
2109
  }
1743
- const resting = status;
1744
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1745
- moveTo(transferring(resting));
2110
+ const progress = { complete: false, reachable: !status.providerUnreachable };
2111
+ moveTo(transferring(status));
1746
2112
  try {
1747
2113
  await conversations.catchUp(progress);
1748
2114
  }
1749
2115
  catch {
1750
2116
  progress.reachable = false;
1751
2117
  }
1752
- finally {
1753
- if (!closed) {
1754
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1755
- }
1756
- }
1757
- }
1758
- function handSelected(locators) {
1759
- const path = options.adapter.roomVersions;
1760
- if (path === undefined || withdrawNotifications === undefined)
1761
- return;
1762
- if (handed !== undefined && JSON.stringify(locators) === JSON.stringify(handed))
1763
- return;
1764
- handed = locators;
1765
- path.select(locators);
1766
- }
1767
- async function runNotified() {
1768
- const announced = [...notices];
1769
- notices.clear();
1770
- if (announced.length === 0 || closed || stopping || discoveryPaused)
1771
- return;
1772
- const resting = status;
1773
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1774
- moveTo(transferring(resting));
1775
- try {
1776
- const local = await openLocal();
1777
- for (const [locator, roomVersion] of announced) {
1778
- if (closed || discoveryPaused || authorization === undefined)
1779
- return;
1780
- if (handed?.includes(locator) !== true)
1781
- continue;
1782
- const room = discovered.get(locator);
1783
- const held = replicas.get(locator);
1784
- if (room === undefined)
1785
- continue;
1786
- if (sourceRefusals.has(locator)) {
1787
- void enqueue();
1788
- continue;
1789
- }
1790
- if (held === undefined)
1791
- continue;
1792
- if (held.confirmed.record.roomVersion === roomVersion)
1793
- continue;
1794
- const withheld = await advance(room, local, authorization);
1795
- if (withheld === null)
1796
- continue;
1797
- if (withheld.kind === 'refused') {
1798
- void enqueue();
1799
- continue;
1800
- }
1801
- if (withheld.kind === 'scoped-refusal') {
1802
- await scoped(withheld, room, local);
1803
- continue;
1804
- }
1805
- if (withheld.kind === 'transport-uncertainty') {
1806
- progress.reachable = false;
1807
- continue;
1808
- }
1809
- if (withheld.kind === 'room-not-readable') {
1810
- unreadable.add(locator);
1811
- list(room, false);
1812
- emitRooms();
1813
- continue;
1814
- }
1815
- if (withheld.kind === 'revoked-authorization') {
1816
- await refuse('revoked-authorization');
1817
- return;
1818
- }
1819
- }
1820
- }
1821
- catch {
1822
- progress.reachable = false;
1823
- }
1824
- finally {
1825
- if (!closed) {
1826
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1827
- }
1828
- }
1829
- }
1830
- function enqueue() {
1831
- if (stopping)
1832
- return chain;
1833
- if (inflight) {
1834
- queued = true;
1835
- return chain;
1836
- }
1837
- inflight = true;
1838
- chain = chain.then(run, run);
1839
- return chain;
2118
+ await rest(progress);
1840
2119
  }
1841
2120
  function close() {
1842
2121
  if (closed)
1843
2122
  return;
1844
2123
  closed = true;
1845
- withdrawNotifications?.();
1846
- withdrawNotifications = undefined;
2124
+ withdrawStream?.();
2125
+ withdrawStream = undefined;
1847
2126
  withdrawConversations?.();
1848
2127
  withdrawConversations = undefined;
1849
2128
  withdrawServiceMode?.();
@@ -1858,6 +2137,7 @@ export function composeRuntime(options) {
1858
2137
  storageLossListeners.clear();
1859
2138
  statusListeners.clear();
1860
2139
  serviceModeListeners.clear();
2140
+ memberListeners.clear();
1861
2141
  readSide.close();
1862
2142
  queueSubscriptions.clear();
1863
2143
  roomDraftListeners.clear();
@@ -1867,6 +2147,7 @@ export function composeRuntime(options) {
1867
2147
  stopping = true;
1868
2148
  stopTimer();
1869
2149
  await chain.catch(() => undefined);
2150
+ await Promise.all([...submitting.values()].map((run) => run.done));
1870
2151
  await mutations;
1871
2152
  close();
1872
2153
  }
@@ -1875,11 +2156,32 @@ export function composeRuntime(options) {
1875
2156
  options.signal?.addEventListener('abort', close, { once: true });
1876
2157
  if (options.signal?.aborted)
1877
2158
  close();
1878
- void openLocal().catch(() => {
2159
+ observeStream();
2160
+ void openLocal().then(() => {
2161
+ if (capabilities.length > 0)
2162
+ notifyReady();
2163
+ }, () => {
1879
2164
  notifyReady();
1880
2165
  });
1881
- void enqueue();
1882
- 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) => {
1883
2185
  if (topic === 'room-drafts') {
1884
2186
  const listener = { notify: deliver, ready: false };
1885
2187
  roomDraftListeners.add(listener);
@@ -1900,6 +2202,10 @@ export function composeRuntime(options) {
1900
2202
  if (topic === 'conversation') {
1901
2203
  return conversations.watch(input[0], input[1], deliver);
1902
2204
  }
2205
+ if (topic === 'conversations')
2206
+ return conversations.watchList(deliver);
2207
+ if (topic === 'agents')
2208
+ return conversations.watchAgents(deliver);
1903
2209
  if (topic === 'presentation') {
1904
2210
  const listener = {
1905
2211
  app: input[0],
@@ -1954,6 +2260,13 @@ export function composeRuntime(options) {
1954
2260
  serviceModeListeners.delete(deliver);
1955
2261
  };
1956
2262
  }
2263
+ if (topic === 'member') {
2264
+ memberListeners.add(deliver);
2265
+ deliver(member);
2266
+ return () => {
2267
+ memberListeners.delete(deliver);
2268
+ };
2269
+ }
1957
2270
  if (topic === 'queue') {
1958
2271
  const queue = { locator: input[0], notify: deliver };
1959
2272
  queueSubscriptions.add(queue);
@@ -1962,9 +2275,8 @@ export function composeRuntime(options) {
1962
2275
  queueSubscriptions.delete(queue);
1963
2276
  };
1964
2277
  }
1965
- const plan = planOf(input[0], roomFacts());
1966
- return readSide.watch(plan, identityOf(plan), deliver);
1967
- }
2278
+ return watchPlan(planned(input[0], null), deliver);
2279
+ };
1968
2280
  const md = {
1969
2281
  loaded() {
1970
2282
  return openLocal().then(() => undefined);
@@ -1972,15 +2284,8 @@ export function composeRuntime(options) {
1972
2284
  rooms() {
1973
2285
  return listed();
1974
2286
  },
1975
- query: ((input) => readSide.query(planOf(input, roomFacts()))),
1976
- async search(input) {
1977
- const plan = searchPlanOf(input);
1978
- const selected = plan.documents === undefined
1979
- ? null
1980
- : (await readSide.query(filterOf(plan.documents, roomFacts())))
1981
- .documents;
1982
- return searchIndex.search(plan, selected);
1983
- },
2287
+ query: ((input) => rowsQuery(planned(input, null))),
2288
+ search: (input) => searched(input, null),
1984
2289
  types(locator) {
1985
2290
  return {
1986
2291
  collection: modelTypes(modelOf(locator)),
@@ -2008,20 +2313,20 @@ export function composeRuntime(options) {
2008
2313
  return exclusive(async () => {
2009
2314
  const parts = readParts(input);
2010
2315
  if (path === '' || normalize(path) !== path)
2011
- throw new Error('room-draft-malformed');
2316
+ throw runtimeError('room-draft-malformed');
2012
2317
  const local = await openLocal();
2013
2318
  const current = roomDrafts.get(room)?.get(path);
2014
2319
  if ((current?.revision ?? null) !== revision) {
2015
- return {
2016
- outcome: 'stale',
2017
- current: current === undefined ? null : roomDraftOf(current),
2018
- };
2320
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2019
2321
  }
2322
+ const file = parts.file === undefined
2323
+ ? undefined
2324
+ : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
2020
2325
  const record = {
2021
2326
  locator: room,
2022
2327
  path,
2023
2328
  revision: roomDraftRevision(),
2024
- parts: held(current?.parts, parts),
2329
+ parts: held(current?.parts, parts, file),
2025
2330
  returned: current?.returned ?? null,
2026
2331
  };
2027
2332
  await local.keepRoomDraft(record);
@@ -2035,10 +2340,7 @@ export function composeRuntime(options) {
2035
2340
  const local = await openLocal();
2036
2341
  const current = roomDrafts.get(room)?.get(path);
2037
2342
  if (current === undefined || current.revision !== revision) {
2038
- return {
2039
- outcome: 'stale',
2040
- current: current === undefined ? null : roomDraftOf(current),
2041
- };
2343
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2042
2344
  }
2043
2345
  await local.discardRoomDraft(room, path);
2044
2346
  forgetRoomDraft(room, path);
@@ -2073,6 +2375,10 @@ export function composeRuntime(options) {
2073
2375
  history(locator, path, query) {
2074
2376
  return readHistory(locator, path, query);
2075
2377
  },
2378
+ binaryFile: {
2379
+ read: readBinaryPath,
2380
+ readReference: readBinaryReference,
2381
+ },
2076
2382
  call(name, input) {
2077
2383
  return invokeAction(name, input);
2078
2384
  },
@@ -2087,9 +2393,7 @@ export function composeRuntime(options) {
2087
2393
  persistence() {
2088
2394
  return options.shell.persistentStorage;
2089
2395
  },
2090
- observe: ((topic, ...input) => opened((deliver) => watch(topic, input, (message) => {
2091
- deliver(structuredClone(message));
2092
- }), topic === 'query')),
2396
+ observe: observing(watch),
2093
2397
  async reauthorize() {
2094
2398
  if (refusal === null || !refusal.reauthorizationOffered)
2095
2399
  return;
@@ -2098,8 +2402,20 @@ export function composeRuntime(options) {
2098
2402
  await mutations;
2099
2403
  authorization = undefined;
2100
2404
  discoveryPaused = false;
2101
- await enqueue();
2405
+ if (options.adapter.roomStream === undefined)
2406
+ await sync('unstreamed');
2407
+ else
2408
+ await task(ensureAuthorization);
2102
2409
  },
2103
2410
  };
2104
- 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) };
2105
2421
  }