@agentshouse/mdruntime 0.1.0-c8c4b020a3ec7 → 0.1.0-c8e110144a30a

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 +57 -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 +20 -0
  39. package/dist/chat/list.d.ts.map +1 -0
  40. package/dist/chat/list.js +59 -0
  41. package/dist/chat/markdown.d.ts +19 -0
  42. package/dist/chat/markdown.d.ts.map +1 -0
  43. package/dist/chat/markdown.js +187 -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 +943 -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,33 @@ 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
+ let count = 0;
237
+ for (const path of Object.keys(replica.visible.documents)) {
238
+ if (markdownPath(path) && !roots.some((root) => beneath(root, path)))
239
+ count += 1;
240
+ }
241
+ return count;
242
+ }
234
243
  function listed() {
235
- return structuredClone([...rooms.values()]);
244
+ return structuredClone([...rooms.values()].map((room) => {
245
+ const documents = documentsIn(room.locator);
246
+ counted.set(room.locator, documents);
247
+ return { ...room, documents };
248
+ }));
236
249
  }
237
- function roomFacts() {
238
- const held = [...rooms.values()].map((room) => {
250
+ function roomFacts(reach = null) {
251
+ const held = [...rooms.values()]
252
+ .filter((room) => holds(reach, room.locator))
253
+ .map((room) => {
254
+ const governed = governs(reach, room);
239
255
  const attached = [
240
- modelOf(room.locator),
241
- ...(room.typedSelection === null ? [] : [modelOf(room.locator, 'typed-selection')]),
256
+ ...(governed.collection ? [modelOf(room.locator)] : []),
257
+ ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
242
258
  ];
243
259
  return {
244
260
  locator: room.locator,
@@ -261,6 +277,10 @@ export function composeRuntime(options) {
261
277
  for (const listener of storageLossListeners)
262
278
  listener(storageLoss);
263
279
  }
280
+ function emitMember() {
281
+ for (const listener of memberListeners)
282
+ listener(member);
283
+ }
264
284
  function moveTo(next) {
265
285
  if (closed)
266
286
  return;
@@ -280,6 +300,8 @@ export function composeRuntime(options) {
280
300
  listener(shown);
281
301
  }
282
302
  function entryOf(batch) {
303
+ if (localSaves)
304
+ return Object.freeze({ batch, state: 'local' });
283
305
  const state = pendings.get(batch.id);
284
306
  if (state?.accepted !== undefined) {
285
307
  return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
@@ -287,10 +309,7 @@ export function composeRuntime(options) {
287
309
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
288
310
  }
289
311
  function entriesOf(locator) {
290
- const replica = replicas.get(locator);
291
- if (replica === undefined)
292
- return [];
293
- return replica.queue.map(entryOf);
312
+ return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
294
313
  }
295
314
  function emitQueue(locator) {
296
315
  const entries = entriesOf(locator);
@@ -300,12 +319,47 @@ export function composeRuntime(options) {
300
319
  }
301
320
  }
302
321
  function applyChangeSet(set) {
322
+ const held = everyRoom
323
+ ? set
324
+ : { ...set, documents: set.documents.filter(({ locator }) => materialized.has(locator)) };
325
+ readSide.apply(held);
326
+ searchIndex.changed(held);
327
+ }
328
+ function materialize(locators) {
329
+ if (everyRoom)
330
+ return;
331
+ if (locators === null)
332
+ everyRoom = true;
333
+ const sets = [];
334
+ for (const locator of locators ?? [...replicas.keys()]) {
335
+ if (materialized.has(locator))
336
+ continue;
337
+ materialized.add(locator);
338
+ const replica = replicas.get(locator);
339
+ if (replica !== undefined)
340
+ sets.push(changeSet(locator, null, replica));
341
+ }
342
+ if (sets.length === 0)
343
+ return;
344
+ const set = mergeChangeSets(sets);
303
345
  readSide.apply(set);
304
346
  searchIndex.changed(set);
305
347
  }
348
+ function reachedRooms(plan) {
349
+ if (plan.rooms !== null)
350
+ return plan.rooms;
351
+ if (plan.reach === null)
352
+ return null;
353
+ return [...plan.reach.rooms, ...plan.reach.documents.map(roomOfKey)];
354
+ }
355
+ function rowsQuery(plan) {
356
+ materialize(reachedRooms(plan));
357
+ return readSide.query(plan);
358
+ }
306
359
  function publishChangeSet(set) {
307
360
  applyChangeSet(set);
308
- if (set.rooms.length > 0)
361
+ if (set.rooms.length > 0 ||
362
+ [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
309
363
  emitRooms();
310
364
  for (const locator of set.queues)
311
365
  emitQueue(locator);
@@ -316,23 +370,32 @@ export function composeRuntime(options) {
316
370
  for (const app of set.presentations)
317
371
  emitPresentation(app);
318
372
  conversations.changed(set);
319
- const cards = declaredCards();
320
- if (cards !== undefined && set.documents.some((change) => change.locator === cards.locator)) {
321
- scheduleCurrency();
322
- }
323
373
  }
324
374
  function stopTimer() {
325
- if (timer === undefined)
375
+ if (resetTimer === undefined)
326
376
  return;
327
- clearInterval(timer);
328
- timer = undefined;
377
+ clearTimeout(resetTimer);
378
+ resetTimer = undefined;
329
379
  }
330
- function startTimer() {
331
- if (timer !== undefined)
380
+ function armTimer() {
381
+ stopTimer();
382
+ if (closed || stopping)
332
383
  return;
333
- timer = setInterval(() => {
334
- void enqueue();
335
- }, period);
384
+ let earliest = null;
385
+ for (const { submission } of pendings.values()) {
386
+ if (submission.state !== 'reset')
387
+ continue;
388
+ const at = Date.parse(submission.reset);
389
+ if (earliest === null || at < earliest)
390
+ earliest = at;
391
+ }
392
+ if (earliest === null)
393
+ return;
394
+ // A Save House held back goes again only once the reset its answer named has passed.
395
+ resetTimer = setTimeout(() => {
396
+ resetTimer = undefined;
397
+ submitAll();
398
+ }, Math.max(0, earliest - Date.now()));
336
399
  }
337
400
  async function refuse(reason, provider = null) {
338
401
  stopTimer();
@@ -343,10 +406,7 @@ export function composeRuntime(options) {
343
406
  const revoked = { status: 'unavailable', reason: 'revoked' };
344
407
  try {
345
408
  const local = await openLocal();
346
- await local.revokeModels([...replicas.values()].map((replica) => ({
347
- record: replica.confirmed.record,
348
- queue: replica.queue,
349
- })));
409
+ await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
350
410
  }
351
411
  catch {
352
412
  journal.record('model revocation persistence failed');
@@ -358,7 +418,6 @@ export function composeRuntime(options) {
358
418
  }
359
419
  for (const key of keys) {
360
420
  invalidModels.add(key);
361
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
362
421
  models.set(key, revoked);
363
422
  liveModels.delete(key);
364
423
  }
@@ -371,10 +430,7 @@ export function composeRuntime(options) {
371
430
  model: revoked,
372
431
  selectionModel: revoked,
373
432
  };
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 } });
433
+ replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
378
434
  }
379
435
  rooms.set(locator, {
380
436
  ...room,
@@ -391,7 +447,8 @@ export function composeRuntime(options) {
391
447
  }
392
448
  finally {
393
449
  refusal = {
394
- reason,
450
+ code: reason,
451
+ retryable: false,
395
452
  reauthorizationOffered: reason === 'revoked-authorization',
396
453
  refusal: provider,
397
454
  };
@@ -404,20 +461,18 @@ export function composeRuntime(options) {
404
461
  rooms.set(room.locator, {
405
462
  name: room.name,
406
463
  locator: room.locator,
464
+ handle: room.handle,
407
465
  operations: room.operations,
408
466
  replica: room.replica,
409
- attachedModel: room.attachedModel,
410
467
  model: modelTypes(modelOf(room.locator)),
411
468
  typedSelection: selection === null
412
469
  ? null
413
470
  : {
414
471
  roots: selection.roots,
415
- attachedModel: selection.attachedModel,
416
472
  model: modelTypes(modelOf(room.locator, 'typed-selection')),
417
473
  },
418
474
  providerOwnedPaths: room.providerOwnedPaths,
419
475
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
420
- attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
421
476
  synchronizationAvailable,
422
477
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
423
478
  });
@@ -429,9 +484,6 @@ export function composeRuntime(options) {
429
484
  discovered.clear();
430
485
  for (const room of listed)
431
486
  discovered.set(room.locator, room);
432
- for (const locator of wanted.keys())
433
- if (!listedLocators.has(locator))
434
- wanted.delete(locator);
435
487
  await dropUnlisted(listedLocators, local);
436
488
  for (const room of listed) {
437
489
  const held = replicas.get(room.locator)?.confirmed.record;
@@ -445,7 +497,14 @@ export function composeRuntime(options) {
445
497
  emitRooms();
446
498
  }
447
499
  function unlistedHeld(listed) {
448
- return [...new Set([...rooms.keys(), ...roomDrafts.keys()])].filter((locator) => !listed.has(locator));
500
+ return [
501
+ ...new Set([
502
+ ...rooms.keys(),
503
+ ...roomDrafts.keys(),
504
+ ...transfers.keys(),
505
+ ...queuesWithoutReplica.keys(),
506
+ ]),
507
+ ].filter((locator) => !listed.has(locator));
449
508
  }
450
509
  async function dropUnlisted(listed, local) {
451
510
  if (unlistedHeld(listed).length === 0)
@@ -459,12 +518,15 @@ export function composeRuntime(options) {
459
518
  drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
460
519
  }));
461
520
  for (const locator of unlisted) {
462
- for (const batch of replicas.get(locator)?.queue ?? [])
521
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
463
522
  pendings.delete(batch.id);
464
523
  replicas.delete(locator);
524
+ queuesWithoutReplica.delete(locator);
465
525
  rooms.delete(locator);
466
526
  roomDrafts.delete(locator);
527
+ transfers.delete(locator);
467
528
  unreadable.delete(locator);
529
+ stale.delete(locator);
468
530
  scopedRefusals.delete(locator);
469
531
  sourceRefusals.delete(locator);
470
532
  declarationRefusals.delete(locator);
@@ -473,7 +535,6 @@ export function composeRuntime(options) {
473
535
  models.delete(key);
474
536
  liveModels.delete(key);
475
537
  invalidModels.delete(key);
476
- modelEpoch.delete(key);
477
538
  }
478
539
  journal.record(`room unlisted ${locator}`);
479
540
  }
@@ -486,7 +547,6 @@ export function composeRuntime(options) {
486
547
  if (prior !== undefined &&
487
548
  prior.status !== 'unavailable' &&
488
549
  (persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
489
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
490
550
  invalidModels.add(key);
491
551
  }
492
552
  const model = persisted.status === 'unavailable' ||
@@ -501,7 +561,7 @@ export function composeRuntime(options) {
501
561
  return model;
502
562
  }
503
563
  async function hydrate(opened) {
504
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
564
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
505
565
  opened.all(),
506
566
  opened.queues(),
507
567
  opened.roomDrafts(),
@@ -511,9 +571,15 @@ export function composeRuntime(options) {
511
571
  opened.lastCompleted(),
512
572
  opened.providerLimits(),
513
573
  opened.capabilities(),
574
+ opened.member(),
575
+ opened.transfers(),
514
576
  ]);
577
+ transfers.clear();
578
+ for (const record of staged)
579
+ transfers.set(record.locator, record);
515
580
  limits = declared;
516
581
  capabilities = named;
582
+ member = known;
517
583
  roomDrafts.clear();
518
584
  for (const record of kept)
519
585
  rememberRoomDraft(record);
@@ -529,19 +595,18 @@ export function composeRuntime(options) {
529
595
  }
530
596
  const previousReplicas = new Map(replicas);
531
597
  replicas.clear();
598
+ queuesWithoutReplica.clear();
599
+ const replicated = new Set(records.map((record) => record.locator));
600
+ for (const [locator, queue] of queuedBatches) {
601
+ if (!replicated.has(locator))
602
+ queuesWithoutReplica.set(locator, queue);
603
+ }
532
604
  for (const record of records) {
533
605
  const model = restored(modelKey(record.locator, 'collection'), record.model);
534
606
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
535
607
  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));
608
+ const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
609
+ replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
545
610
  const existing = rooms.get(record.locator);
546
611
  if (existing === undefined)
547
612
  list(record, false);
@@ -549,24 +614,17 @@ export function composeRuntime(options) {
549
614
  rooms.set(record.locator, {
550
615
  ...existing,
551
616
  roomVersion: record.roomVersion,
552
- attachments: record.attachments,
553
617
  operations: record.operations,
554
618
  replica: record.replica,
555
- attachedModel: record.attachedModel,
556
619
  model: modelTypes(model),
557
620
  typedSelection: record.typedSelection === null
558
621
  ? null
559
622
  : {
560
623
  roots: record.typedSelection.roots,
561
- attachedModel: record.typedSelection.attachedModel,
562
624
  model: modelTypes(selectionModel),
563
625
  },
564
626
  });
565
627
  }
566
- if (read.kind === 'refused') {
567
- refusedSource(record, read);
568
- declarationRefusals.add(record.locator);
569
- }
570
628
  }
571
629
  return {
572
630
  lastCompleted,
@@ -579,6 +637,8 @@ export function composeRuntime(options) {
579
637
  applyChangeSet(read);
580
638
  if (replicas.size > 0)
581
639
  emitRooms();
640
+ if (member !== null)
641
+ emitMember();
582
642
  for (const locator of locators)
583
643
  emitQueue(locator);
584
644
  publishRoomDrafts(false);
@@ -626,10 +686,15 @@ export function composeRuntime(options) {
626
686
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
627
687
  }
628
688
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
629
- const withdrawn = withdrawals(confirmed, queue, withdrawable, rewriting());
689
+ const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
690
+ const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
630
691
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
692
+ const unqueued = withdrawn.map((batch) => ({
693
+ batch,
694
+ returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
695
+ }));
631
696
  const returning = [
632
- ...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
697
+ ...[...unqueued].reverse(),
633
698
  ...(settling.leaving === undefined ? [] : [settling.leaving]),
634
699
  ];
635
700
  const written = new Map();
@@ -647,57 +712,110 @@ export function composeRuntime(options) {
647
712
  });
648
713
  }
649
714
  }
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 ?? []) {
715
+ await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
716
+ for (const batch of previousQueue) {
658
717
  if (!kept.includes(batch))
659
718
  pendings.delete(batch.id);
660
719
  }
661
720
  for (const record of written.values())
662
721
  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));
722
+ try {
723
+ await local.write({
724
+ next: confirmed.record,
725
+ previous: before?.confirmed.record ?? null,
726
+ ...(settling.stored === undefined ? {} : { stored: settling.stored }),
727
+ });
728
+ }
729
+ catch (error) {
730
+ const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
731
+ if (held !== null)
732
+ replicas.set(locator, held);
733
+ else if (kept.length > 0)
734
+ queuesWithoutReplica.set(locator, kept);
735
+ else
736
+ queuesWithoutReplica.delete(locator);
737
+ publishChangeSet(changeSet(locator, before ?? null, held, {
738
+ queue: kept.length !== previousQueue.length,
739
+ drafts: [...written.keys()],
740
+ }));
741
+ moveTo({ ...status, ...attention() });
742
+ throw error;
743
+ }
744
+ queuesWithoutReplica.delete(locator);
745
+ for (const { batch, returned } of unqueued) {
746
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
747
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
748
+ }
749
+ replicas.set(locator, replicaOf(confirmed, kept, before));
666
750
  return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
667
751
  room: settling.room,
668
- queue: kept.length !== (before?.queue.length ?? 0),
752
+ queue: kept.length !== previousQueue.length,
669
753
  drafts: [...written.keys()],
670
754
  });
671
755
  }
672
- function publish(local, room, confirmed) {
756
+ function adoptModels(locator, record) {
757
+ for (const [attached, model] of [
758
+ ['collection', record.model],
759
+ ['typed-selection', record.selectionModel],
760
+ ]) {
761
+ const key = modelKey(locator, attached);
762
+ models.set(key, model);
763
+ invalidModels.delete(key);
764
+ if (model.status === 'current')
765
+ liveModels.set(key, JSON.stringify(model.snapshot));
766
+ else if (model.status === 'unavailable')
767
+ liveModels.delete(key);
768
+ }
769
+ }
770
+ function remodel(locator, answers, held) {
771
+ const taken = (answer, previous, attached) => {
772
+ if (answer === undefined)
773
+ return previous;
774
+ const { state, refused } = attachedModel(answer, previous);
775
+ if (refused !== null) {
776
+ const where = refused.location === undefined ? '' : ` ${refused.location}`;
777
+ journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
778
+ }
779
+ models.set(modelKey(locator, attached), state);
780
+ return state;
781
+ };
782
+ return {
783
+ collection: taken(answers?.collection, held.collection, 'collection'),
784
+ selection: taken(answers?.selection, held.selection, 'typed-selection'),
785
+ };
786
+ }
787
+ function publish(local, room, confirmed, landing = {}) {
788
+ const { base, stored } = landing;
673
789
  return exclusive(async () => {
674
790
  if (!continuing())
675
- return;
791
+ return true;
676
792
  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;
793
+ if (base !== undefined &&
794
+ (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
795
+ prior.confirmed.record.revisions !== base.record.revisions)) {
796
+ return false;
797
+ }
798
+ adoptModels(room.locator, confirmed.record);
799
+ const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
800
+ const roomVersion = confirmed.record.roomVersion;
686
801
  const accepted = queue.filter((batch) => {
687
802
  const accepted = pendings.get(batch.id)?.accepted;
688
803
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
689
804
  });
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);
805
+ const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
806
+ if (stored !== undefined)
807
+ transfers.delete(room.locator);
808
+ for (const batch of accepted)
809
+ pendings.delete(batch.id);
810
+ stale.delete(room.locator);
811
+ if (unreadable.delete(room.locator))
812
+ scopedRefusals.delete(room.locator);
696
813
  sourceRefusals.delete(room.locator);
697
814
  declarationRefusals.delete(room.locator);
698
815
  list(room, true);
699
816
  publishChangeSet(set);
700
817
  moveTo({ ...status, ...attention() });
818
+ return true;
701
819
  });
702
820
  }
703
821
  async function saveRoomDrafts(locator, named, writtenBy) {
@@ -705,61 +823,140 @@ export function composeRuntime(options) {
705
823
  const replica = replicas.get(locator);
706
824
  const room = rooms.get(locator);
707
825
  if (replica === undefined || room === undefined) {
708
- throw new Error(`No Local Replica for ${locator}`);
826
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
709
827
  }
710
828
  const saved = [];
711
829
  for (const { path, revision } of named) {
712
830
  const record = roomDrafts.get(locator)?.get(path);
713
831
  if (record === undefined || record.revision !== revision) {
714
- return { outcome: 'refused', rule: 'stale', file: path };
832
+ return { outcome: 'refused', code: 'stale', retryable: false, file: path };
715
833
  }
716
834
  if (!saved.some((draft) => draft.path === path))
717
835
  saved.push({ path, parts: record.parts });
718
836
  }
719
837
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
720
838
  if (changed.length > 0)
721
- return { outcome: 'refused', rule: 'changed-underneath', parts: changed };
839
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
722
840
  const parts = saved.map(({ path, parts }) => changesOf(path, parts));
723
- const built = buildBatch({
841
+ const changes = parts.flatMap(({ changes }) => changes);
842
+ const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
843
+ if (requeue.kind === 'refused')
844
+ return { outcome: 'refused', ...requeue.refusal };
845
+ const foreign = [...requeue.withdrawn.keys()].some((origin) => {
846
+ const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
847
+ return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
848
+ });
849
+ if (foreign) {
850
+ return {
851
+ outcome: 'refused',
852
+ code: 'unknown-revision',
853
+ retryable: false,
854
+ file: [...requeue.queued].at(-1),
855
+ };
856
+ }
857
+ const withdrawing = requeue.withdrawn.size > 0;
858
+ const input = {
859
+ local: localSaves,
724
860
  baseRoomVersion: replica.confirmed.record.roomVersion,
725
861
  composition: compositionFor(replica.confirmed.record),
726
862
  operations: room.operations,
727
863
  providerOwnedPaths: room.providerOwnedPaths,
728
864
  providerLimits: limits,
729
- rewriting: rewriting(),
865
+ handle: room.handle,
730
866
  visible: replica.visible,
731
- changes: parts.flatMap(({ changes }) => changes),
867
+ changes,
732
868
  bases: parts.flatMap(({ bases }) => bases),
733
869
  writtenBy,
870
+ };
871
+ const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
872
+ if (checked?.kind === 'refused')
873
+ return { outcome: 'refused', ...checked.refusal };
874
+ const owners = [...requeue.owners].flatMap((origin) => {
875
+ const batch = requeue.withdrawn.get(origin);
876
+ return batch === undefined || batch === null ? [] : [{ origin, batch }];
734
877
  });
735
- if (built.kind === 'refused')
878
+ if (owners.length > 1) {
879
+ return {
880
+ outcome: 'refused',
881
+ code: 'unknown-revision',
882
+ retryable: false,
883
+ file: [...requeue.queued].at(-1),
884
+ };
885
+ }
886
+ const built = !withdrawing
887
+ ? buildBatch(input)
888
+ : requeue.changes.length === 0
889
+ ? null
890
+ : buildBatch({
891
+ ...input,
892
+ visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
893
+ changes: requeue.changes,
894
+ });
895
+ if (built?.kind === 'refused')
736
896
  return { outcome: 'refused', ...built.refusal };
737
- const batches = [...replica.queue, built.batch];
897
+ const [owner] = owners;
898
+ const join = built === null || owner === undefined
899
+ ? null
900
+ : joined(requeue.queue, owner.batch, built.batch);
901
+ if (join?.kind === 'refused')
902
+ return { outcome: 'refused', ...join.refusal };
903
+ const withdrawn = new Map(requeue.withdrawn);
904
+ if (join !== null && owner !== undefined)
905
+ withdrawn.set(owner.origin, join.batch);
906
+ const drafted = new Map();
907
+ for (const [origin, replaced] of withdrawn) {
908
+ if (replaced === null)
909
+ continue;
910
+ const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
911
+ drafted.set(replaced.id, replaced === join?.batch
912
+ ? [
913
+ ...carried.map((held) => joinedDraft(held, saved)),
914
+ ...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
915
+ ]
916
+ : carried);
917
+ }
918
+ if (built !== null && join === null)
919
+ drafted.set(built.batch.id, saved);
920
+ const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
921
+ for (const replaced of withdrawn.values()) {
922
+ if (replaced === null || !batches.includes(replaced))
923
+ continue;
924
+ const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
925
+ if (limited !== null)
926
+ return { outcome: 'refused', ...limited };
927
+ }
738
928
  const consumed = saved.map(({ path }) => path);
739
929
  try {
740
- await local.write({
741
- next: replica.confirmed.record,
742
- previous: replica.confirmed.record,
930
+ await local.keepAuthored({
931
+ locator,
743
932
  queue: batches,
744
- previousQueue: replica.queue,
745
- saved,
933
+ saved: drafted,
746
934
  consumedRoomDrafts: consumed,
747
935
  });
748
936
  }
749
937
  catch {
750
- return { outcome: 'refused', rule: 'durable-write' };
938
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
939
+ }
940
+ for (const origin of withdrawn.keys())
941
+ pendings.delete(origin);
942
+ for (const [id, drafts] of drafted) {
943
+ pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
751
944
  }
752
- pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
945
+ if (!localSaves)
946
+ recorded = false;
753
947
  for (const path of consumed)
754
948
  forgetRoomDraft(locator, path);
755
- replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting(), replica));
949
+ replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
756
950
  publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
757
951
  queue: true,
758
952
  drafts: consumed,
759
953
  }));
760
- moveTo({ ...saving(status), ...attention() });
761
- void enqueue();
762
- return { outcome: 'saved', identifier: built.batch.id };
954
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
955
+ submitLater(locator);
956
+ const identifier = join?.batch.id ??
957
+ built?.batch.id ??
958
+ [...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
959
+ return { outcome: 'saved', identifier };
763
960
  }
764
961
  async function mark(local, locator, identifier, submission, lost) {
765
962
  const state = pendings.get(identifier);
@@ -785,16 +982,15 @@ export function composeRuntime(options) {
785
982
  }
786
983
  async function readHistory(locator, path, query = {}) {
787
984
  if (closed || refusal !== null)
788
- return { kind: 'transport-uncertainty' };
985
+ return UNCERTAIN;
789
986
  try {
790
987
  await ensureAuthorization();
791
988
  }
792
989
  catch {
793
- return { kind: 'transport-uncertainty' };
794
- }
795
- if (closed || authorization === undefined || refusal !== null) {
796
- return { kind: 'transport-uncertainty' };
990
+ return UNCERTAIN;
797
991
  }
992
+ if (closed || authorization === undefined || refusal !== null)
993
+ return UNCERTAIN;
798
994
  const transfer = ['transfer', 'history', locator, path];
799
995
  if (query.revision !== undefined)
800
996
  transfer.push(query.revision);
@@ -816,17 +1012,101 @@ export function composeRuntime(options) {
816
1012
  }
817
1013
  if (result.kind === 'revoked-authorization')
818
1014
  await refuse('revoked-authorization');
819
- return result;
1015
+ return historyAnswer(result);
820
1016
  }
821
1017
  catch {
822
1018
  journal.record('outcome history transport-uncertainty');
823
- return { kind: 'transport-uncertainty' };
1019
+ return UNCERTAIN;
1020
+ }
1021
+ }
1022
+ function queuedBlob(locator, path, localId) {
1023
+ for (const batch of replicas.get(locator)?.queue ?? []) {
1024
+ const change = batch.changes[queuedFile(batch, path, localId)];
1025
+ if (change?.kind === 'binary')
1026
+ return change.blob;
1027
+ }
1028
+ return null;
1029
+ }
1030
+ async function readBinary(request) {
1031
+ if (closed || refusal !== null)
1032
+ return UNCERTAIN;
1033
+ try {
1034
+ await ensureAuthorization();
1035
+ }
1036
+ catch {
1037
+ return UNCERTAIN;
1038
+ }
1039
+ if (closed || authorization === undefined || refusal !== null)
1040
+ return UNCERTAIN;
1041
+ journal.record(request.kind === 'path'
1042
+ ? `transfer binary ${request.locator} ${request.path}`
1043
+ : `transfer binary ${request.fileRef}`);
1044
+ try {
1045
+ const result = await options.adapter.binary(authorization, request);
1046
+ journal.record(`outcome binary ${result.kind}`);
1047
+ if (result.kind === 'scoped-refusal') {
1048
+ const room = replicas.get(result.locator)?.confirmed.record;
1049
+ if (room !== undefined)
1050
+ await scoped(result, room, await openLocal());
1051
+ }
1052
+ if (result.kind === 'revoked-authorization')
1053
+ await refuse('revoked-authorization');
1054
+ return binaryAnswer(result);
1055
+ }
1056
+ catch {
1057
+ journal.record('outcome binary transport-uncertainty');
1058
+ return UNCERTAIN;
824
1059
  }
825
1060
  }
826
- function declaredCards() {
1061
+ async function readBinaryPath(locator, path, revision) {
1062
+ await ready;
1063
+ if (revision !== undefined)
1064
+ return readBinary({ kind: 'path', locator, path, revision });
1065
+ const visible = replicas.get(locator)?.visible;
1066
+ const held = visible?.binaries[path];
1067
+ if (visible === undefined || held === undefined)
1068
+ return absentBinary(path);
1069
+ if (held.state === 'pending') {
1070
+ const blob = queuedBlob(locator, path, held.localId);
1071
+ return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1072
+ }
1073
+ const record = binaryRecord(path, held, visible.revisions[path]);
1074
+ if (record.state !== 'accepted')
1075
+ return absentBinary(path);
1076
+ const replica = replicas.get(locator);
1077
+ const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
1078
+ const holds = (at) => {
1079
+ const kept = replica?.confirmed.record.binaries[at];
1080
+ return (kept?.state === 'accepted' &&
1081
+ kept.fileRef === record.fileRef &&
1082
+ replica?.confirmed.record.revisions[at] === record.revision);
1083
+ };
1084
+ let confirmed = path;
1085
+ for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
1086
+ const change = queued[index];
1087
+ if (change.kind === 'move' && change.to === confirmed)
1088
+ confirmed = change.path;
1089
+ }
1090
+ return readBinary({
1091
+ kind: 'path',
1092
+ locator,
1093
+ path: confirmed,
1094
+ revision: record.revision,
1095
+ fileRef: record.fileRef,
1096
+ });
1097
+ }
1098
+ async function readBinaryReference(reference) {
1099
+ await ready;
1100
+ return readBinary({
1101
+ kind: 'reference',
1102
+ fileRef: reference.fileRef,
1103
+ referrer: reference.referrer,
1104
+ });
1105
+ }
1106
+ function declaredAgents() {
827
1107
  for (const capability of capabilities) {
828
- if (capability.conversationCards !== undefined)
829
- return capability.conversationCards;
1108
+ if (capability.conversationAgents !== undefined)
1109
+ return capability.conversationAgents;
830
1110
  }
831
1111
  return undefined;
832
1112
  }
@@ -842,23 +1122,20 @@ export function composeRuntime(options) {
842
1122
  async function invokeAction(name, input) {
843
1123
  await ready;
844
1124
  if (closed || refusal !== null)
845
- return { kind: 'transport-uncertainty' };
1125
+ return UNCERTAIN;
846
1126
  if (declaredAction(name) === undefined)
847
- return { kind: 'undeclared' };
1127
+ return undeclared(name);
848
1128
  try {
849
1129
  await ensureAuthorization();
850
1130
  }
851
1131
  catch {
852
- return { kind: 'transport-uncertainty' };
853
- }
854
- if (closed || authorization === undefined || refusal !== null) {
855
- return { kind: 'transport-uncertainty' };
1132
+ return UNCERTAIN;
856
1133
  }
1134
+ if (closed || authorization === undefined || refusal !== null)
1135
+ return UNCERTAIN;
857
1136
  journal.record(`transfer action ${name}`);
858
1137
  try {
859
1138
  const result = await options.adapter.action(authorization, { name, input });
860
- if (result.kind === 'action' && result.at !== undefined)
861
- want(result.at);
862
1139
  if (result.kind === 'scoped-refusal') {
863
1140
  const room = replicas.get(result.locator)?.confirmed.record;
864
1141
  if (room !== undefined)
@@ -869,11 +1146,11 @@ export function composeRuntime(options) {
869
1146
  journal.record(`outcome action ${result.kind}`);
870
1147
  if (result.kind === 'rejected')
871
1148
  journal.record(`refusal rejected ${result.code}`);
872
- return result;
1149
+ return calledResult(result);
873
1150
  }
874
1151
  catch {
875
1152
  journal.record('outcome action transport-uncertainty');
876
- return { kind: 'transport-uncertainty' };
1153
+ return UNCERTAIN;
877
1154
  }
878
1155
  }
879
1156
  function continuing() {
@@ -890,32 +1167,11 @@ export function composeRuntime(options) {
890
1167
  options.adapter.reached(locator, written, roomVersion) &&
891
1168
  !options.adapter.reached(locator, roomVersion, written));
892
1169
  }
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) {
1170
+ async function pullRefresh(authorized, confirmed) {
915
1171
  if (!continuing())
916
1172
  return unconfirmed;
917
1173
  try {
918
- const result = await refreshRoom(options.adapter, authorized, confirmed, continuing, minPosition);
1174
+ const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
919
1175
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
920
1176
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
921
1177
  return result;
@@ -925,14 +1181,33 @@ export function composeRuntime(options) {
925
1181
  throw error;
926
1182
  }
927
1183
  }
928
- async function pullBootstrap(authorized, room) {
1184
+ function staging(local) {
1185
+ return {
1186
+ held: (locator) => transfers.get(locator) ?? null,
1187
+ staged: (record) => local.staged(record),
1188
+ stage: (record, page) => exclusive(async () => {
1189
+ if (!continuing())
1190
+ return false;
1191
+ await local.stage(record, page);
1192
+ transfers.set(record.locator, record);
1193
+ return true;
1194
+ }),
1195
+ abandon: (locator) => exclusive(async () => {
1196
+ if (!continuing())
1197
+ return;
1198
+ await local.abandon(locator);
1199
+ transfers.delete(locator);
1200
+ }),
1201
+ };
1202
+ }
1203
+ async function pullBootstrap(authorized, room, local) {
929
1204
  if (!continuing())
930
1205
  return unconfirmed;
931
1206
  try {
932
1207
  const result = await bootstrapRoom(options.adapter, authorized, room, {
933
1208
  collection: modelOf(room.locator),
934
1209
  selection: modelOf(room.locator, 'typed-selection'),
935
- }, continuing);
1210
+ }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
936
1211
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
937
1212
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
938
1213
  return result;
@@ -945,35 +1220,46 @@ export function composeRuntime(options) {
945
1220
  async function advance(room, local, authorized) {
946
1221
  const held = replicas.get(room.locator);
947
1222
  if (held !== undefined) {
948
- const refreshed = await pullRefresh(authorized, held.confirmed, reachOf(room.locator));
1223
+ const refreshed = await pullRefresh(authorized, held.confirmed);
949
1224
  if (!continuing())
950
1225
  return unconfirmed;
951
1226
  if (refreshed.kind === 'still-current') {
1227
+ stale.delete(room.locator);
952
1228
  if (sourceRefusals.has(room.locator))
953
- return unconfirmed;
1229
+ return null;
954
1230
  if (unreadable.delete(room.locator)) {
1231
+ scopedRefusals.delete(room.locator);
955
1232
  list(room, true);
956
1233
  emitRooms();
957
1234
  }
958
1235
  return null;
959
1236
  }
960
1237
  if (refreshed.kind === 'advanced') {
961
- await publish(local, room, refreshed.confirmed);
1238
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
1239
+ const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
1240
+ const reached = now !== undefined &&
1241
+ options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
1242
+ return reached ? null : advance(room, local, authorized);
1243
+ }
1244
+ await retainModel(local, room, 'collection', modelOf(room.locator));
962
1245
  return null;
963
1246
  }
964
1247
  if (refreshed.kind !== 'beyond-horizon')
965
1248
  return refreshed;
966
1249
  }
967
- const bootstrapped = await pullBootstrap(authorized, room);
1250
+ const bootstrapped = await pullBootstrap(authorized, room, local);
968
1251
  if (!continuing())
969
1252
  return unconfirmed;
970
1253
  if (bootstrapped.kind !== 'staged')
971
1254
  return bootstrapped;
972
- await publish(local, room, bootstrapped.confirmed);
1255
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
973
1256
  return null;
974
1257
  }
975
1258
  async function withhold(withheld, room, local, progress) {
976
- progress.complete = false;
1259
+ if (withheld.kind === 'refused')
1260
+ stale.delete(room.locator);
1261
+ else
1262
+ stale.add(room.locator);
977
1263
  if (withheld.kind === 'scoped-refusal') {
978
1264
  await scoped(withheld, room, local);
979
1265
  }
@@ -998,6 +1284,7 @@ export function composeRuntime(options) {
998
1284
  function refusedSource(room, { refusal }) {
999
1285
  sourceRefusals.set(room.locator, {
1000
1286
  code: refusal.rule,
1287
+ retryable: false,
1001
1288
  fields: {
1002
1289
  file: refusal.file,
1003
1290
  ...(refusal.location === undefined ? {} : { location: refusal.location }),
@@ -1012,52 +1299,46 @@ export function composeRuntime(options) {
1012
1299
  }
1013
1300
  function leaving(batch, result) {
1014
1301
  if (result.kind === 'expired')
1015
- return { batch, returned: { kind: 'expired' } };
1016
- return { batch, returned: { kind: result.kind, code: result.code, fields: result.fields } };
1302
+ return { batch, returned: returnedBy('expired') };
1303
+ return {
1304
+ batch,
1305
+ returned: {
1306
+ kind: result.kind,
1307
+ code: result.code,
1308
+ fields: result.fields,
1309
+ retryable: result.retryable,
1310
+ },
1311
+ };
1017
1312
  }
1018
1313
  function logAccepted(locator, submitted, accepted) {
1019
1314
  journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
1020
1315
  (accepted.author === undefined ? '' : ` author ${accepted.author}`) +
1021
1316
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1022
1317
  }
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) {
1318
+ async function adoptAccepted(room, local, submitted, accepted) {
1041
1319
  if (!continuing())
1042
1320
  return unconfirmed;
1043
1321
  const current = replicas.get(room.locator);
1044
1322
  if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1045
1323
  return null;
1046
1324
  }
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')
1325
+ const held = current.confirmed.record.roomVersion;
1326
+ logAccepted(room.locator, submitted, accepted);
1327
+ const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1328
+ ? { kind: 'staged', confirmed: current.confirmed }
1329
+ : applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
1330
+ ? accepted.roomVersion
1331
+ : held, accepted.roomVersion);
1332
+ if (applied.kind !== 'staged') {
1333
+ const state = pendings.get(submitted.id);
1334
+ await local.accept(room.locator, submitted.id, accepted.roomVersion);
1335
+ if (state !== undefined)
1336
+ pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1337
+ emitQueue(room.locator);
1057
1338
  return applied;
1058
- const set = await settle(local, room.locator, current, applied.confirmed, remaining, {
1059
- room: true,
1060
- });
1339
+ }
1340
+ const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1341
+ pendings.delete(submitted.id);
1061
1342
  unreadable.delete(room.locator);
1062
1343
  sourceRefusals.delete(room.locator);
1063
1344
  declarationRefusals.delete(room.locator);
@@ -1066,17 +1347,8 @@ export function composeRuntime(options) {
1066
1347
  moveTo({ ...status, ...attention() });
1067
1348
  return null;
1068
1349
  }
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));
1350
+ async function adopted(room, local, submitted, accepted) {
1351
+ const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
1080
1352
  if (failed === null)
1081
1353
  return true;
1082
1354
  if (failed.kind === 'refused' && continuing())
@@ -1089,10 +1361,8 @@ export function composeRuntime(options) {
1089
1361
  const head = replica?.queue[0];
1090
1362
  if (replica === undefined || head === undefined)
1091
1363
  return;
1092
- if (pendings.get(head.id)?.accepted !== undefined) {
1093
- progress.complete = false;
1364
+ if (pendings.get(head.id)?.accepted !== undefined)
1094
1365
  return;
1095
- }
1096
1366
  const state = pendings.get(head.id);
1097
1367
  const { submission, lost: earlier } = state ?? {
1098
1368
  submission: { state: 'waiting' },
@@ -1120,6 +1390,7 @@ export function composeRuntime(options) {
1120
1390
  return;
1121
1391
  }
1122
1392
  const opensBefore = openSignals;
1393
+ const answered = awaitingAnswer(room.locator);
1123
1394
  try {
1124
1395
  result = await options.adapter.mutate(authorized, {
1125
1396
  locator: room.locator,
@@ -1128,11 +1399,14 @@ export function composeRuntime(options) {
1128
1399
  }, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
1129
1400
  }
1130
1401
  catch {
1402
+ answered();
1131
1403
  progress.complete = false;
1132
1404
  if (continuing())
1133
1405
  progress.reachable = false;
1134
1406
  return;
1135
1407
  }
1408
+ if (result.kind !== 'accepted' || !continuing())
1409
+ answered();
1136
1410
  if (!continuing())
1137
1411
  return;
1138
1412
  if (result.kind === 'transport-uncertainty') {
@@ -1185,6 +1459,7 @@ export function composeRuntime(options) {
1185
1459
  const reset = result.reset;
1186
1460
  progress.complete = false;
1187
1461
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1462
+ armTimer();
1188
1463
  return;
1189
1464
  }
1190
1465
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
@@ -1197,7 +1472,8 @@ export function composeRuntime(options) {
1197
1472
  return;
1198
1473
  }
1199
1474
  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)
1475
+ leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1476
+ (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1201
1477
  ? { ...left, returned: null }
1202
1478
  : left,
1203
1479
  });
@@ -1211,95 +1487,33 @@ export function composeRuntime(options) {
1211
1487
  return withhold(withheld, room, local, progress);
1212
1488
  continue;
1213
1489
  }
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
- }
1490
+ const kept = await adopted(room, local, head, result).finally(answered);
1491
+ if (!kept) {
1258
1492
  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
1493
  return;
1271
1494
  }
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
1495
  }
1289
- return;
1496
+ }
1497
+ function awaitingAnswer(locator) {
1498
+ let release = () => { };
1499
+ const awaited = new Promise((resolve) => {
1500
+ release = resolve;
1501
+ });
1502
+ answering.set(locator, awaited);
1503
+ return () => {
1504
+ if (answering.get(locator) === awaited)
1505
+ answering.delete(locator);
1506
+ release();
1507
+ };
1290
1508
  }
1291
1509
  async function retainModel(local, room, attached, next) {
1292
1510
  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
1511
  const previous = modelOf(room.locator, attached);
1297
1512
  const heldBefore = replicas.get(room.locator)?.confirmed.record;
1298
1513
  if (JSON.stringify(previous) === JSON.stringify(next) &&
1299
1514
  (heldBefore === undefined ||
1300
1515
  (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
1301
1516
  JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
1302
- JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
1303
1517
  JSON.stringify(heldBefore.typedSelection) ===
1304
1518
  JSON.stringify(room.typedSelection ?? null))))
1305
1519
  return;
@@ -1310,20 +1524,17 @@ export function composeRuntime(options) {
1310
1524
  await exclusive(async () => {
1311
1525
  if (!continuing())
1312
1526
  return;
1313
- if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
1314
- return;
1315
1527
  const held = replicas.get(room.locator);
1316
1528
  if (held !== undefined) {
1317
1529
  const declared = {
1318
1530
  ...held.confirmed.record,
1319
1531
  operations: room.operations,
1320
1532
  replica: room.replica,
1321
- attachedModel: room.attachedModel,
1322
1533
  typedSelection: room.typedSelection ?? null,
1323
1534
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1324
1535
  };
1325
1536
  const reclassified = sameMembership(declared, held.confirmed.record)
1326
- ? readConfirmed(declared, held.confirmed)
1537
+ ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
1327
1538
  : readReclassified(declared, held.confirmed);
1328
1539
  const record = reclassified.kind === 'staged'
1329
1540
  ? declared
@@ -1331,14 +1542,12 @@ export function composeRuntime(options) {
1331
1542
  ...declared,
1332
1543
  typedSelection: held.confirmed.record.typedSelection,
1333
1544
  };
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
- });
1545
+ const read = reclassified.kind === 'deferred'
1546
+ ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
1547
+ : reclassified;
1548
+ await local.write({ next: record, previous: held.confirmed.record });
1340
1549
  if (read.kind === 'staged') {
1341
- replicas.set(room.locator, replicaOf(read.confirmed, held.queue, rewriting(), held));
1550
+ replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1342
1551
  if (declarationRefusals.delete(room.locator)) {
1343
1552
  sourceRefusals.delete(room.locator);
1344
1553
  unreadable.delete(room.locator);
@@ -1362,79 +1571,12 @@ export function composeRuntime(options) {
1362
1571
  publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
1363
1572
  });
1364
1573
  }
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
1574
  async function scoped(result, room, local) {
1437
- scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
1575
+ scopedRefusals.set(room.locator, {
1576
+ code: result.code,
1577
+ fields: result.fields,
1578
+ retryable: result.retryable,
1579
+ });
1438
1580
  let scopedAway = room;
1439
1581
  if (result.scope === 'model')
1440
1582
  await retainModel(local, room, 'collection', {
@@ -1460,14 +1602,35 @@ export function composeRuntime(options) {
1460
1602
  emitRooms();
1461
1603
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
1462
1604
  }
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) => {
1605
+ function heldRooms(listed) {
1606
+ return listed.flatMap((room) => {
1607
+ const record = replicas.get(room.locator)?.confirmed.record;
1608
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1609
+ return [];
1610
+ return [
1611
+ {
1612
+ locator: room.locator,
1613
+ roomVersion: record.roomVersion,
1614
+ documents: record.documents,
1615
+ revisions: record.revisions,
1616
+ },
1617
+ ];
1618
+ });
1619
+ }
1620
+ async function synchronize(listed, local, progress, attempt) {
1621
+ const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1622
+ const ready = listed.filter((_, index) => {
1466
1623
  const outcome = prepared[index];
1467
1624
  return outcome?.status === 'fulfilled' && outcome.value;
1468
1625
  });
1469
- const rooms = await Promise.allSettled(ready.map((room) => synchronizeRoom(room, local, progress)));
1470
- const failed = [...prepared, ...rooms].find((room) => room.status === 'rejected');
1626
+ if (attempt === 'opened') {
1627
+ options.adapter.roomStream?.hold(heldRooms(ready));
1628
+ handed = true;
1629
+ }
1630
+ const loaded = await Promise.allSettled(ready
1631
+ .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1632
+ .map((room) => loadRoom(room, local, progress)));
1633
+ const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1471
1634
  if (failed !== undefined)
1472
1635
  throw failed.reason;
1473
1636
  }
@@ -1484,11 +1647,8 @@ export function composeRuntime(options) {
1484
1647
  progress.complete = false;
1485
1648
  return false;
1486
1649
  }
1487
- const selection = room.typedSelection;
1488
- await Promise.all([
1489
- acquireModel(local, room, authorization, null),
1490
- ...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
1491
- ]);
1650
+ if (prior !== undefined)
1651
+ await retainModel(local, room, 'collection', modelOf(room.locator));
1492
1652
  if (discoveryPaused || authorization === undefined)
1493
1653
  return false;
1494
1654
  if (room.replica.admission !== 'admitted') {
@@ -1496,24 +1656,53 @@ export function composeRuntime(options) {
1496
1656
  progress.complete = false;
1497
1657
  return false;
1498
1658
  }
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
1659
  return true;
1505
1660
  }
1506
- async function synchronizeRoom(room, local, progress) {
1661
+ async function caughtUp(room, local, progress) {
1662
+ const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1663
+ if (held === undefined || authorization === undefined)
1664
+ return;
1665
+ const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1666
+ if (!behind)
1667
+ return;
1668
+ const withheld = await advance(room, local, authorization);
1669
+ if (withheld !== null)
1670
+ await withhold(withheld, room, local, progress);
1671
+ }
1672
+ function revoked(locator) {
1673
+ return ['collection', 'typed-selection'].some((attached) => {
1674
+ const model = modelOf(locator, attached);
1675
+ return model.status === 'unavailable' && model.reason === 'revoked';
1676
+ });
1677
+ }
1678
+ async function loadRoom(room, local, progress) {
1507
1679
  if (discoveryPaused || authorization === undefined)
1508
1680
  return;
1681
+ if (replicas.has(room.locator) && revoked(room.locator)) {
1682
+ await reload(room, local, progress);
1683
+ return;
1684
+ }
1509
1685
  const withheld = await advance(room, local, authorization);
1510
1686
  if (withheld !== null) {
1511
1687
  await withhold(withheld, room, local, progress);
1512
1688
  return;
1513
1689
  }
1514
- await submitRoom(room, local, authorization, progress);
1690
+ await caughtUp(room, local, progress);
1515
1691
  }
1516
- async function cycle(local, progress) {
1692
+ async function reload(room, local, progress) {
1693
+ if (discoveryPaused || authorization === undefined)
1694
+ return;
1695
+ const bootstrapped = await pullBootstrap(authorization, room, local);
1696
+ if (!continuing())
1697
+ return;
1698
+ if (bootstrapped.kind !== 'staged') {
1699
+ await withhold(bootstrapped, room, local, progress);
1700
+ return;
1701
+ }
1702
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1703
+ await caughtUp(room, local, progress);
1704
+ }
1705
+ async function discoverRooms(local, progress, attempt) {
1517
1706
  if (closed || authorization === undefined) {
1518
1707
  notifyReady();
1519
1708
  return;
@@ -1539,12 +1728,17 @@ export function composeRuntime(options) {
1539
1728
  }
1540
1729
  }
1541
1730
  progress.reachable = false;
1731
+ progress.complete = false;
1542
1732
  notifyReady();
1543
1733
  return;
1544
1734
  }
1545
1735
  if (result.kind === 'contract-unavailable') {
1546
1736
  journal.record(`connection contract unavailable ${result.code}`);
1547
- await refuse('missing-required-guarantee', { code: result.code, fields: result.fields });
1737
+ await refuse('missing-required-guarantee', {
1738
+ code: result.code,
1739
+ fields: result.fields,
1740
+ retryable: result.retryable,
1741
+ });
1548
1742
  notifyReady();
1549
1743
  return;
1550
1744
  }
@@ -1553,17 +1747,6 @@ export function composeRuntime(options) {
1553
1747
  notifyReady();
1554
1748
  return;
1555
1749
  }
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
1750
  for (const room of result.rooms) {
1568
1751
  if (room.operations === undefined ||
1569
1752
  !['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
@@ -1571,8 +1754,7 @@ export function composeRuntime(options) {
1571
1754
  room.replica === undefined ||
1572
1755
  typeof room.replica.selection !== 'string' ||
1573
1756
  room.replica.selection.length === 0 ||
1574
- !['admitted', 'unavailable'].includes(room.replica.admission) ||
1575
- !declaredIdentity(room.attachedModel)) {
1757
+ !['admitted', 'unavailable'].includes(room.replica.admission)) {
1576
1758
  journal.record(`invalid model or replica declaration ${room.locator}`);
1577
1759
  await refuse('missing-required-guarantee');
1578
1760
  notifyReady();
@@ -1585,14 +1767,6 @@ export function composeRuntime(options) {
1585
1767
  return;
1586
1768
  }
1587
1769
  }
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
1770
  if (refusal !== null) {
1597
1771
  refusal = null;
1598
1772
  emitRefusal();
@@ -1600,26 +1774,19 @@ export function composeRuntime(options) {
1600
1774
  const previousCapabilities = capabilities;
1601
1775
  capabilities = result.capabilities;
1602
1776
  notifyReady();
1603
- if (declared.has('room-version-notifications'))
1604
- observeRoomVersions();
1605
1777
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1606
1778
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
1607
1779
  limits = result.providerLimits;
1608
1780
  await local.declare(limits, capabilities);
1609
1781
  }
1782
+ const signedIn = result.member ?? null;
1783
+ if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
1784
+ await local.keepMember(signedIn);
1785
+ member = signedIn;
1786
+ emitMember();
1787
+ }
1610
1788
  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);
1789
+ await synchronize(result.rooms, local, progress, attempt);
1623
1790
  }
1624
1791
  async function ensureAuthorization() {
1625
1792
  if (authorization !== undefined)
@@ -1633,64 +1800,262 @@ export function composeRuntime(options) {
1633
1800
  throw error;
1634
1801
  }
1635
1802
  }
1636
- async function connect() {
1637
- if (closed || stopping || discoveryPaused) {
1638
- notifyReady();
1803
+ function task(work) {
1804
+ if (stopping)
1805
+ return chain;
1806
+ chain = chain.then(work, work).catch(() => undefined);
1807
+ return chain;
1808
+ }
1809
+ function sync(attempt) {
1810
+ return task(async () => {
1811
+ if (closed || stopping || discoveryPaused) {
1812
+ notifyReady();
1813
+ return;
1814
+ }
1815
+ const progress = { complete: true, reachable: true };
1816
+ underway = attempt === 'opened' ? progress : null;
1817
+ if (attempt === 'opened')
1818
+ streamed = progress;
1819
+ complete = false;
1820
+ recorded = false;
1821
+ moveTo(transferring(status));
1822
+ handed = false;
1823
+ try {
1824
+ const local = await openLocal();
1825
+ if (closed)
1826
+ return;
1827
+ await ensureAuthorization();
1828
+ if (closed)
1829
+ return;
1830
+ await discoverRooms(local, progress, attempt);
1831
+ }
1832
+ catch {
1833
+ progress.complete = false;
1834
+ if (attempt === 'opened' && !handed)
1835
+ progress.reachable = false;
1836
+ }
1837
+ finally {
1838
+ notifyReady();
1839
+ }
1840
+ if (attempt !== 'opened') {
1841
+ await finishOpening(progress);
1842
+ return;
1843
+ }
1844
+ if (handed || closed || progress.reachable)
1845
+ return;
1846
+ underway = null;
1847
+ journal.record('connection reopen');
1848
+ await rest(progress);
1849
+ options.adapter.roomStream?.reopen();
1850
+ });
1851
+ }
1852
+ function closedSocket(revoked) {
1853
+ const opened = socketOpen;
1854
+ socketOpen = false;
1855
+ if (revoked) {
1856
+ void refuse('revoked-authorization');
1639
1857
  return;
1640
1858
  }
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)
1859
+ complete = false;
1860
+ if (streamed !== null)
1861
+ streamed.complete = false;
1862
+ moveTo({
1863
+ ...status,
1864
+ state: status.state === 'synced' ? 'syncing' : status.state,
1865
+ providerUnreachable: status.providerUnreachable || !opened,
1866
+ });
1867
+ }
1868
+ function applyOpening(expired) {
1869
+ return task(async () => {
1870
+ const progress = underway;
1871
+ underway = null;
1872
+ if (closed || stopping)
1650
1873
  return;
1651
- await cycle(local, progress);
1652
- if (closed)
1874
+ if (progress === null) {
1875
+ await reloadExpired(expired);
1653
1876
  return;
1654
- if (progress.complete) {
1655
- completedAt = new Date().toISOString();
1656
- await local.complete(completedAt);
1657
1877
  }
1878
+ try {
1879
+ const local = await openLocal();
1880
+ const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1881
+ const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1882
+ const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
1883
+ if (progress.reachable) {
1884
+ for (const room of held)
1885
+ if (!reloading.includes(room))
1886
+ await confirmModels(room, local);
1887
+ }
1888
+ const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1889
+ if (failed !== undefined)
1890
+ throw failed.reason;
1891
+ }
1892
+ catch {
1893
+ progress.complete = false;
1894
+ progress.reachable = false;
1895
+ }
1896
+ await finishOpening(progress);
1897
+ });
1898
+ }
1899
+ async function reloadExpired(expired) {
1900
+ const progress = { complete: true, reachable: true };
1901
+ try {
1902
+ const local = await openLocal();
1903
+ await Promise.allSettled([...discovered.values()]
1904
+ .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1905
+ .map((room) => reload(room, local, progress)));
1658
1906
  }
1659
1907
  catch {
1660
- progress.complete = false;
1661
- notifyReady();
1908
+ progress.reachable = false;
1909
+ }
1910
+ await rest(progress);
1911
+ }
1912
+ async function confirmModels(room, local) {
1913
+ for (const attached of ['collection', 'typed-selection']) {
1914
+ const model = modelOf(room.locator, attached);
1915
+ if (model.status === 'cached') {
1916
+ await retainModel(local, room, attached, { ...model, status: 'current' });
1917
+ }
1918
+ }
1919
+ }
1920
+ function submitLater(locator) {
1921
+ if (localSaves)
1922
+ return;
1923
+ const running = submitting.get(locator);
1924
+ if (running !== undefined) {
1925
+ running.again = true;
1662
1926
  return;
1663
1927
  }
1664
- finally {
1665
- notifyReady();
1666
- moveTo(settled(status, progress, completedAt, pending(), attention()));
1928
+ const run = { again: false, done: Promise.resolve() };
1929
+ submitting.set(locator, run);
1930
+ run.done = (async () => {
1931
+ try {
1932
+ do {
1933
+ run.again = false;
1934
+ const room = discovered.get(locator);
1935
+ if (closed || stopping || discoveryPaused || room === undefined)
1936
+ return;
1937
+ const progress = { complete: true, reachable: true };
1938
+ try {
1939
+ await ensureAuthorization();
1940
+ if (authorization === undefined)
1941
+ return;
1942
+ await submitRoom(room, await openLocal(), authorization, progress);
1943
+ }
1944
+ catch {
1945
+ progress.reachable = false;
1946
+ }
1947
+ armTimer();
1948
+ await rest(progress);
1949
+ } while (run.again);
1950
+ }
1951
+ finally {
1952
+ submitting.delete(locator);
1953
+ }
1954
+ })();
1955
+ }
1956
+ function submitAll() {
1957
+ for (const [locator, replica] of replicas) {
1958
+ if (replica.queue.length > 0)
1959
+ submitLater(locator);
1667
1960
  }
1668
1961
  }
1669
- async function run() {
1962
+ async function finishOpening(progress) {
1670
1963
  try {
1671
- await connect();
1964
+ if (closed || discoveryPaused) {
1965
+ progress.complete = false;
1966
+ return;
1967
+ }
1968
+ submitAll();
1969
+ await conversations.reconcile();
1970
+ if (status.providerUnreachable)
1971
+ conversations.reconnected();
1972
+ const reading = { complete: true, reachable: true };
1973
+ await conversations.catchUp(reading);
1974
+ if (!reading.reachable)
1975
+ progress.reachable = false;
1976
+ }
1977
+ catch {
1978
+ progress.complete = false;
1672
1979
  }
1673
1980
  finally {
1674
- inflight = false;
1675
- if (queued && !closed) {
1676
- queued = false;
1677
- void enqueue();
1678
- }
1981
+ complete = progress.complete;
1982
+ await rest(progress);
1679
1983
  }
1680
1984
  }
1681
- function observeRoomVersions() {
1682
- const path = options.adapter.roomVersions;
1683
- if (path === undefined || withdrawNotifications !== undefined)
1985
+ async function rest(progress) {
1986
+ if (closed)
1684
1987
  return;
1685
- withdrawNotifications = path.observe((notification) => {
1686
- if (closed || stopping || discoveryPaused)
1988
+ let lastCompleted = status.lastCompleted;
1989
+ let synced = complete && stale.size === 0 && !pending();
1990
+ if (synced && !recorded) {
1991
+ const completedAt = new Date().toISOString();
1992
+ try {
1993
+ await (await openLocal()).complete(completedAt);
1994
+ recorded = true;
1995
+ lastCompleted = completedAt;
1996
+ }
1997
+ catch {
1998
+ synced = false;
1999
+ }
2000
+ }
2001
+ moveTo({
2002
+ state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
2003
+ lastCompleted,
2004
+ providerUnreachable: !progress.reachable,
2005
+ ...attention(),
2006
+ });
2007
+ }
2008
+ function applyRoom(locator, roomVersion) {
2009
+ announced.set(locator, roomVersion);
2010
+ return task(async () => {
2011
+ await answering.get(locator);
2012
+ if (closed || stopping || discoveryPaused || authorization === undefined)
1687
2013
  return;
1688
- if (notification.modelChanged === true && handed?.includes(notification.locator) === true) {
1689
- void enqueue();
2014
+ const room = discovered.get(locator);
2015
+ const held = replicas.get(locator);
2016
+ if (room === undefined || room.replica.admission !== 'admitted')
1690
2017
  return;
2018
+ if (held !== undefined &&
2019
+ (held.confirmed.record.replica.selection !== room.replica.selection ||
2020
+ options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
2021
+ return;
2022
+ }
2023
+ const during = underway;
2024
+ const progress = during ?? { complete: true, reachable: true };
2025
+ if (during === null)
2026
+ moveTo(transferring(status));
2027
+ try {
2028
+ const local = await openLocal();
2029
+ const withheld = await advance(room, local, authorization);
2030
+ if (withheld !== null)
2031
+ await withhold(withheld, room, local, progress);
2032
+ }
2033
+ catch {
2034
+ stale.add(locator);
2035
+ }
2036
+ finally {
2037
+ if (during === null)
2038
+ await rest(progress);
2039
+ }
2040
+ });
2041
+ }
2042
+ function observeStream() {
2043
+ const stream = options.adapter.roomStream;
2044
+ if (stream === undefined || withdrawStream !== undefined)
2045
+ return;
2046
+ withdrawStream = stream.observe((event) => {
2047
+ if (closed || stopping)
2048
+ return;
2049
+ if (event.kind === 'opened') {
2050
+ socketOpen = true;
2051
+ void sync('opened');
1691
2052
  }
1692
- notices.set(notification.locator, notification.roomVersion);
1693
- chain = chain.then(runNotified, runNotified);
2053
+ else if (event.kind === 'closed')
2054
+ closedSocket(event.revoked);
2055
+ else if (event.kind === 'opening')
2056
+ void applyOpening(event.expired);
2057
+ else
2058
+ void applyRoom(event.locator, event.roomVersion);
1694
2059
  });
1695
2060
  }
1696
2061
  function writesHeld() {
@@ -1711,14 +2076,17 @@ export function composeRuntime(options) {
1711
2076
  listener(mode);
1712
2077
  }
1713
2078
  if (releasing)
1714
- void enqueue();
2079
+ submitAll();
1715
2080
  }
1716
2081
  function observeConversations() {
1717
2082
  const stream = options.adapter.conversations;
1718
2083
  if (stream === undefined || withdrawConversations !== undefined)
1719
2084
  return;
1720
2085
  withdrawConversations = stream.observe((event) => {
1721
- conversations.receive(event);
2086
+ if (conversations.receive(event)) {
2087
+ void ready.then(scheduleCurrency);
2088
+ return;
2089
+ }
1722
2090
  if (event.kind !== 'gap' || conversations.current())
1723
2091
  return;
1724
2092
  show();
@@ -1729,7 +2097,7 @@ export function composeRuntime(options) {
1729
2097
  if (currencyQueued || closed || stopping)
1730
2098
  return;
1731
2099
  currencyQueued = true;
1732
- chain = chain.then(runCurrency, runCurrency);
2100
+ void task(runCurrency);
1733
2101
  }
1734
2102
  async function runCurrency() {
1735
2103
  currencyQueued = false;
@@ -1740,110 +2108,22 @@ export function composeRuntime(options) {
1740
2108
  show();
1741
2109
  return;
1742
2110
  }
1743
- const resting = status;
1744
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1745
- moveTo(transferring(resting));
2111
+ const progress = { complete: false, reachable: !status.providerUnreachable };
2112
+ moveTo(transferring(status));
1746
2113
  try {
1747
2114
  await conversations.catchUp(progress);
1748
2115
  }
1749
2116
  catch {
1750
2117
  progress.reachable = false;
1751
2118
  }
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;
2119
+ await rest(progress);
1840
2120
  }
1841
2121
  function close() {
1842
2122
  if (closed)
1843
2123
  return;
1844
2124
  closed = true;
1845
- withdrawNotifications?.();
1846
- withdrawNotifications = undefined;
2125
+ withdrawStream?.();
2126
+ withdrawStream = undefined;
1847
2127
  withdrawConversations?.();
1848
2128
  withdrawConversations = undefined;
1849
2129
  withdrawServiceMode?.();
@@ -1858,6 +2138,7 @@ export function composeRuntime(options) {
1858
2138
  storageLossListeners.clear();
1859
2139
  statusListeners.clear();
1860
2140
  serviceModeListeners.clear();
2141
+ memberListeners.clear();
1861
2142
  readSide.close();
1862
2143
  queueSubscriptions.clear();
1863
2144
  roomDraftListeners.clear();
@@ -1867,6 +2148,7 @@ export function composeRuntime(options) {
1867
2148
  stopping = true;
1868
2149
  stopTimer();
1869
2150
  await chain.catch(() => undefined);
2151
+ await Promise.all([...submitting.values()].map((run) => run.done));
1870
2152
  await mutations;
1871
2153
  close();
1872
2154
  }
@@ -1875,11 +2157,32 @@ export function composeRuntime(options) {
1875
2157
  options.signal?.addEventListener('abort', close, { once: true });
1876
2158
  if (options.signal?.aborted)
1877
2159
  close();
1878
- void openLocal().catch(() => {
2160
+ observeStream();
2161
+ void openLocal().then(() => {
2162
+ if (capabilities.length > 0)
2163
+ notifyReady();
2164
+ }, () => {
1879
2165
  notifyReady();
1880
2166
  });
1881
- void enqueue();
1882
- function watch(topic, input, deliver) {
2167
+ if (options.adapter.roomStream === undefined)
2168
+ void sync('unstreamed');
2169
+ function planned(input, reach) {
2170
+ return planOf(input, roomFacts(reach), planReach(reach));
2171
+ }
2172
+ function watchPlan(plan, deliver) {
2173
+ materialize(reachedRooms(plan));
2174
+ return readSide.watch(plan, identityOf(plan), deliver);
2175
+ }
2176
+ async function searched(input, reach) {
2177
+ const plan = searchPlanOf(input);
2178
+ const selected = plan.documents === undefined && reach === null
2179
+ ? null
2180
+ : (await rowsQuery(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
2181
+ if (selected === null)
2182
+ materialize(null);
2183
+ return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
2184
+ }
2185
+ const watch = (topic, input, deliver) => {
1883
2186
  if (topic === 'room-drafts') {
1884
2187
  const listener = { notify: deliver, ready: false };
1885
2188
  roomDraftListeners.add(listener);
@@ -1900,6 +2203,10 @@ export function composeRuntime(options) {
1900
2203
  if (topic === 'conversation') {
1901
2204
  return conversations.watch(input[0], input[1], deliver);
1902
2205
  }
2206
+ if (topic === 'conversations')
2207
+ return conversations.watchList(deliver);
2208
+ if (topic === 'agents')
2209
+ return conversations.watchAgents(deliver);
1903
2210
  if (topic === 'presentation') {
1904
2211
  const listener = {
1905
2212
  app: input[0],
@@ -1954,6 +2261,13 @@ export function composeRuntime(options) {
1954
2261
  serviceModeListeners.delete(deliver);
1955
2262
  };
1956
2263
  }
2264
+ if (topic === 'member') {
2265
+ memberListeners.add(deliver);
2266
+ deliver(member);
2267
+ return () => {
2268
+ memberListeners.delete(deliver);
2269
+ };
2270
+ }
1957
2271
  if (topic === 'queue') {
1958
2272
  const queue = { locator: input[0], notify: deliver };
1959
2273
  queueSubscriptions.add(queue);
@@ -1962,9 +2276,8 @@ export function composeRuntime(options) {
1962
2276
  queueSubscriptions.delete(queue);
1963
2277
  };
1964
2278
  }
1965
- const plan = planOf(input[0], roomFacts());
1966
- return readSide.watch(plan, identityOf(plan), deliver);
1967
- }
2279
+ return watchPlan(planned(input[0], null), deliver);
2280
+ };
1968
2281
  const md = {
1969
2282
  loaded() {
1970
2283
  return openLocal().then(() => undefined);
@@ -1972,15 +2285,8 @@ export function composeRuntime(options) {
1972
2285
  rooms() {
1973
2286
  return listed();
1974
2287
  },
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
- },
2288
+ query: ((input) => rowsQuery(planned(input, null))),
2289
+ search: (input) => searched(input, null),
1984
2290
  types(locator) {
1985
2291
  return {
1986
2292
  collection: modelTypes(modelOf(locator)),
@@ -2008,20 +2314,20 @@ export function composeRuntime(options) {
2008
2314
  return exclusive(async () => {
2009
2315
  const parts = readParts(input);
2010
2316
  if (path === '' || normalize(path) !== path)
2011
- throw new Error('room-draft-malformed');
2317
+ throw runtimeError('room-draft-malformed');
2012
2318
  const local = await openLocal();
2013
2319
  const current = roomDrafts.get(room)?.get(path);
2014
2320
  if ((current?.revision ?? null) !== revision) {
2015
- return {
2016
- outcome: 'stale',
2017
- current: current === undefined ? null : roomDraftOf(current),
2018
- };
2321
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2019
2322
  }
2323
+ const file = parts.file === undefined
2324
+ ? undefined
2325
+ : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
2020
2326
  const record = {
2021
2327
  locator: room,
2022
2328
  path,
2023
2329
  revision: roomDraftRevision(),
2024
- parts: held(current?.parts, parts),
2330
+ parts: held(current?.parts, parts, file),
2025
2331
  returned: current?.returned ?? null,
2026
2332
  };
2027
2333
  await local.keepRoomDraft(record);
@@ -2035,10 +2341,7 @@ export function composeRuntime(options) {
2035
2341
  const local = await openLocal();
2036
2342
  const current = roomDrafts.get(room)?.get(path);
2037
2343
  if (current === undefined || current.revision !== revision) {
2038
- return {
2039
- outcome: 'stale',
2040
- current: current === undefined ? null : roomDraftOf(current),
2041
- };
2344
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2042
2345
  }
2043
2346
  await local.discardRoomDraft(room, path);
2044
2347
  forgetRoomDraft(room, path);
@@ -2073,6 +2376,10 @@ export function composeRuntime(options) {
2073
2376
  history(locator, path, query) {
2074
2377
  return readHistory(locator, path, query);
2075
2378
  },
2379
+ binaryFile: {
2380
+ read: readBinaryPath,
2381
+ readReference: readBinaryReference,
2382
+ },
2076
2383
  call(name, input) {
2077
2384
  return invokeAction(name, input);
2078
2385
  },
@@ -2087,9 +2394,7 @@ export function composeRuntime(options) {
2087
2394
  persistence() {
2088
2395
  return options.shell.persistentStorage;
2089
2396
  },
2090
- observe: ((topic, ...input) => opened((deliver) => watch(topic, input, (message) => {
2091
- deliver(structuredClone(message));
2092
- }), topic === 'query')),
2397
+ observe: observing(watch),
2093
2398
  async reauthorize() {
2094
2399
  if (refusal === null || !refusal.reauthorizationOffered)
2095
2400
  return;
@@ -2098,8 +2403,20 @@ export function composeRuntime(options) {
2098
2403
  await mutations;
2099
2404
  authorization = undefined;
2100
2405
  discoveryPaused = false;
2101
- await enqueue();
2406
+ if (options.adapter.roomStream === undefined)
2407
+ await sync('unstreamed');
2408
+ else
2409
+ await task(ensureAuthorization);
2102
2410
  },
2103
2411
  };
2104
- return { md, stop, watch };
2412
+ const reachable = {
2413
+ md,
2414
+ watch,
2415
+ referrerScope: (referrer) => options.adapter.referrerScope(referrer),
2416
+ plan: planned,
2417
+ query: (plan) => rowsQuery(plan),
2418
+ watchPlan,
2419
+ search: searched,
2420
+ };
2421
+ return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2105
2422
  }