@agentshouse/mdruntime 0.1.0-c3903a68dcbbf → 0.1.0-c4478f1a5ac2a

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 (192) hide show
  1. package/AGENTS.md +196 -97
  2. package/README.md +22 -12
  3. package/dist/adapter.d.ts +149 -64
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/adapter.js +1 -9
  6. package/dist/attached-model.d.ts +11 -0
  7. package/dist/attached-model.d.ts.map +1 -0
  8. package/dist/attached-model.js +47 -0
  9. package/dist/attempt.d.ts +16 -0
  10. package/dist/attempt.d.ts.map +1 -0
  11. package/dist/attempt.js +70 -0
  12. package/dist/binary.d.ts +23 -0
  13. package/dist/binary.d.ts.map +1 -0
  14. package/dist/binary.js +8 -0
  15. package/dist/bootstrap.d.ts +20 -6
  16. package/dist/bootstrap.d.ts.map +1 -1
  17. package/dist/bootstrap.js +110 -38
  18. package/dist/change-set.d.ts +2 -0
  19. package/dist/change-set.d.ts.map +1 -1
  20. package/dist/change-set.js +49 -21
  21. package/dist/change.d.ts +15 -0
  22. package/dist/change.d.ts.map +1 -1
  23. package/dist/change.js +26 -1
  24. package/dist/chat/acts.d.ts +33 -0
  25. package/dist/chat/acts.d.ts.map +1 -0
  26. package/dist/chat/acts.js +36 -0
  27. package/dist/chat/context.d.ts +3 -0
  28. package/dist/chat/context.d.ts.map +1 -0
  29. package/dist/chat/context.js +10 -0
  30. package/dist/chat/input.d.ts +9 -0
  31. package/dist/chat/input.d.ts.map +1 -0
  32. package/dist/chat/input.js +99 -0
  33. package/dist/chat/kit.d.ts +60 -0
  34. package/dist/chat/kit.d.ts.map +1 -0
  35. package/dist/chat/kit.js +123 -0
  36. package/dist/chat/lasting.d.ts +6 -0
  37. package/dist/chat/lasting.d.ts.map +1 -0
  38. package/dist/chat/lasting.js +19 -0
  39. package/dist/chat/list.d.ts +26 -0
  40. package/dist/chat/list.d.ts.map +1 -0
  41. package/dist/chat/list.js +62 -0
  42. package/dist/chat/markdown.d.ts +20 -0
  43. package/dist/chat/markdown.d.ts.map +1 -0
  44. package/dist/chat/markdown.js +188 -0
  45. package/dist/chat/message.d.ts +6 -0
  46. package/dist/chat/message.d.ts.map +1 -0
  47. package/dist/chat/message.js +192 -0
  48. package/dist/chat/messages.d.ts +21 -0
  49. package/dist/chat/messages.d.ts.map +1 -0
  50. package/dist/chat/messages.js +151 -0
  51. package/dist/chat/name.d.ts +6 -0
  52. package/dist/chat/name.d.ts.map +1 -0
  53. package/dist/chat/name.js +37 -0
  54. package/dist/chat/opened.d.ts +11 -0
  55. package/dist/chat/opened.d.ts.map +1 -0
  56. package/dist/chat/opened.js +17 -0
  57. package/dist/chat/overlay.d.ts +31 -0
  58. package/dist/chat/overlay.d.ts.map +1 -0
  59. package/dist/chat/overlay.js +97 -0
  60. package/dist/chat/parts.d.ts +57 -0
  61. package/dist/chat/parts.d.ts.map +1 -0
  62. package/dist/chat/parts.js +119 -0
  63. package/dist/chat/process.d.ts +20 -0
  64. package/dist/chat/process.d.ts.map +1 -0
  65. package/dist/chat/process.js +32 -0
  66. package/dist/chat/provider.d.ts +63 -0
  67. package/dist/chat/provider.d.ts.map +1 -0
  68. package/dist/chat/provider.js +620 -0
  69. package/dist/chat/question.d.ts +9 -0
  70. package/dist/chat/question.d.ts.map +1 -0
  71. package/dist/chat/question.js +80 -0
  72. package/dist/chat/questions.d.ts +31 -0
  73. package/dist/chat/questions.d.ts.map +1 -0
  74. package/dist/chat/questions.js +111 -0
  75. package/dist/chat/read.d.ts +5 -0
  76. package/dist/chat/read.d.ts.map +1 -0
  77. package/dist/chat/read.js +13 -0
  78. package/dist/chat/ready.d.ts +33 -0
  79. package/dist/chat/ready.d.ts.map +1 -0
  80. package/dist/chat/ready.js +91 -0
  81. package/dist/chat/style.d.ts +4 -0
  82. package/dist/chat/style.d.ts.map +1 -0
  83. package/dist/chat/style.js +302 -0
  84. package/dist/chat.d.ts +14 -0
  85. package/dist/chat.d.ts.map +1 -0
  86. package/dist/chat.js +8 -0
  87. package/dist/client.d.ts +23 -19
  88. package/dist/client.d.ts.map +1 -1
  89. package/dist/client.js +69 -32
  90. package/dist/collection.d.ts +21 -14
  91. package/dist/collection.d.ts.map +1 -1
  92. package/dist/collection.js +270 -32
  93. package/dist/connection.d.ts +36 -38
  94. package/dist/connection.d.ts.map +1 -1
  95. package/dist/connection.js +1605 -841
  96. package/dist/conversation.d.ts +43 -19
  97. package/dist/conversation.d.ts.map +1 -1
  98. package/dist/conversation.js +523 -210
  99. package/dist/entry.d.ts +17 -0
  100. package/dist/entry.d.ts.map +1 -0
  101. package/dist/entry.js +156 -0
  102. package/dist/expectation.d.ts +21 -0
  103. package/dist/expectation.d.ts.map +1 -0
  104. package/dist/expectation.js +70 -0
  105. package/dist/failure.d.ts +13 -0
  106. package/dist/failure.d.ts.map +1 -0
  107. package/dist/failure.js +19 -0
  108. package/dist/fence.d.ts +6 -0
  109. package/dist/fence.d.ts.map +1 -0
  110. package/dist/fence.js +10 -0
  111. package/dist/host.d.ts +11 -3
  112. package/dist/host.d.ts.map +1 -1
  113. package/dist/host.js +54 -69
  114. package/dist/identity.d.ts +5 -0
  115. package/dist/identity.d.ts.map +1 -1
  116. package/dist/identity.js +19 -1
  117. package/dist/index.d.ts +12 -7
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +4 -1
  120. package/dist/memory.d.ts +15 -7
  121. package/dist/memory.d.ts.map +1 -1
  122. package/dist/memory.js +147 -47
  123. package/dist/model.d.ts +14 -12
  124. package/dist/model.d.ts.map +1 -1
  125. package/dist/model.js +15 -3
  126. package/dist/observation.d.ts +6 -1
  127. package/dist/observation.d.ts.map +1 -1
  128. package/dist/observation.js +38 -17
  129. package/dist/open.d.ts +2 -0
  130. package/dist/open.d.ts.map +1 -0
  131. package/dist/open.js +1 -0
  132. package/dist/outcome.d.ts +35 -22
  133. package/dist/outcome.d.ts.map +1 -1
  134. package/dist/outcome.js +42 -1
  135. package/dist/overlay.d.ts +15 -3
  136. package/dist/overlay.d.ts.map +1 -1
  137. package/dist/overlay.js +94 -77
  138. package/dist/paths.d.ts +0 -1
  139. package/dist/paths.d.ts.map +1 -1
  140. package/dist/paths.js +0 -16
  141. package/dist/query.d.ts +40 -6
  142. package/dist/query.d.ts.map +1 -1
  143. package/dist/query.js +78 -36
  144. package/dist/reach.d.ts +42 -0
  145. package/dist/reach.d.ts.map +1 -0
  146. package/dist/reach.js +294 -0
  147. package/dist/react.d.ts +41 -0
  148. package/dist/react.d.ts.map +1 -0
  149. package/dist/react.js +64 -0
  150. package/dist/read-side.d.ts +16 -5
  151. package/dist/read-side.d.ts.map +1 -1
  152. package/dist/read-side.js +405 -137
  153. package/dist/recovery.d.ts +25 -0
  154. package/dist/recovery.d.ts.map +1 -0
  155. package/dist/recovery.js +197 -0
  156. package/dist/refresh.d.ts +12 -1
  157. package/dist/refresh.d.ts.map +1 -1
  158. package/dist/refresh.js +45 -36
  159. package/dist/replica.d.ts +10 -7
  160. package/dist/replica.d.ts.map +1 -1
  161. package/dist/replica.js +122 -79
  162. package/dist/room-draft.d.ts +29 -9
  163. package/dist/room-draft.d.ts.map +1 -1
  164. package/dist/room-draft.js +137 -26
  165. package/dist/rows.d.ts +10 -14
  166. package/dist/rows.d.ts.map +1 -1
  167. package/dist/rows.js +85 -50
  168. package/dist/save.d.ts +32 -3
  169. package/dist/save.d.ts.map +1 -1
  170. package/dist/save.js +465 -93
  171. package/dist/search.d.ts +31 -8
  172. package/dist/search.d.ts.map +1 -1
  173. package/dist/search.js +109 -44
  174. package/dist/shared.d.ts +11 -0
  175. package/dist/shared.d.ts.map +1 -0
  176. package/dist/shared.js +368 -0
  177. package/dist/status.d.ts +2 -2
  178. package/dist/status.d.ts.map +1 -1
  179. package/dist/status.js +0 -8
  180. package/dist/store.d.ts +71 -32
  181. package/dist/store.d.ts.map +1 -1
  182. package/dist/store.js +509 -221
  183. package/dist/turn.d.ts +7 -0
  184. package/dist/turn.d.ts.map +1 -0
  185. package/dist/turn.js +45 -0
  186. package/dist/wire.d.ts +58 -19
  187. package/dist/wire.d.ts.map +1 -1
  188. package/dist/wire.js +65 -67
  189. package/package.json +31 -7
  190. package/dist/attachment.d.ts +0 -3
  191. package/dist/attachment.d.ts.map +0 -1
  192. package/dist/attachment.js +0 -8
@@ -1,95 +1,101 @@
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
- import { changeSet, mergeChangeSets } from './change-set.js';
9
+ import { changeSet, entering, 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 { indexReferrers, referencesIndexed, 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, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
17
+ import { createRecovery } from './recovery.js';
13
18
  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';
19
+ import { buildBatch, joined, limitedIn, requeued } from './save.js';
20
+ import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
21
+ import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
22
+ import { same, saving, STARTING, transferring, } from './status.js';
18
23
  import { detectStorageLoss, nextGeneration } from './durability.js';
19
- import { roomDraftRevision } from './identity.js';
24
+ import { FencedMap } from './fence.js';
25
+ import { tally } from './shared.js';
26
+ import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
27
+ import { identityTime, roomDraftRevision } from './identity.js';
20
28
  import { beneath, normalize } from './paths.js';
21
29
  import { openRuntimeStore, } from './store.js';
22
30
  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);
31
+ import { governs, holds, narrowed, planReach, } from './reach.js';
27
32
  function within(root, path) {
28
33
  return root === '' || beneath(root, path);
29
34
  }
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
35
  function declaredSelection(room) {
41
36
  const selection = room.typedSelection;
42
37
  if (selection === undefined)
43
38
  return true;
44
39
  if (!Array.isArray(selection.roots) || selection.roots.length === 0)
45
40
  return false;
46
- if (!declaredIdentity(selection.attachedModel))
47
- return false;
48
41
  return selection.roots.every((root, index) => validRoot(root) &&
49
42
  root !== '' &&
50
43
  selection.roots.every((other, at) => at === index || !within(other, root)));
51
44
  }
52
- function declaredUntypedFolders(room) {
53
- const roots = room.untypedFolders;
54
- if (roots === undefined)
55
- return true;
56
- if (!Array.isArray(roots) || roots.length === 0)
57
- return false;
58
- const selected = room.typedSelection?.roots ?? [];
59
- return roots.every((root, index) => validRoot(root) &&
60
- root !== '' &&
61
- roots.every((other, at) => at === index || !within(other, root)) &&
62
- selected.every((other) => !within(other, root) && !within(root, other)));
45
+ const OPENING_LANE = 'opening';
46
+ const CONVERSATION_LANE = 'conversations';
47
+ const ROOM_READS = 256;
48
+ const ROOM_ATTEMPTS = 16;
49
+ const outside = new Map();
50
+ function outsideOf(roots) {
51
+ const key = JSON.stringify(roots);
52
+ const held = outside.get(key);
53
+ if (held !== undefined)
54
+ return held;
55
+ const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
56
+ const made = {
57
+ documents: (path) => (markdownPath(path) ? counted(path) : 0),
58
+ binaries: counted,
59
+ };
60
+ outside.set(key, made);
61
+ return made;
63
62
  }
64
- export function createMdRuntime(options) {
65
- return composeRuntime(options).md;
63
+ function abandonedOn(signal) {
64
+ return new Promise((resolve) => {
65
+ if (signal.aborted)
66
+ resolve(null);
67
+ else
68
+ signal.addEventListener('abort', () => resolve(null), { once: true });
69
+ });
70
+ }
71
+ function roomLane(locator) {
72
+ return `room ${locator}`;
66
73
  }
67
74
  export function composeRuntime(options) {
68
- const period = options.discoveryIntervalMs ?? DEFAULT_DISCOVERY_INTERVAL_MS;
75
+ const localSaves = options.adapter.localSaves === true;
69
76
  let authorization = options.authorization;
70
77
  let opening;
71
78
  let store;
72
- const replicas = new Map();
79
+ const fences = new Map();
80
+ const isFenced = (locator) => fences.has(locator);
81
+ let fencings = 0;
82
+ const lastFenced = new Map();
83
+ const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
84
+ let discoveries = 0;
85
+ const replicas = new FencedMap(isFenced);
86
+ const queuesWithoutReplica = new FencedMap(isFenced);
73
87
  const pendings = new Map();
74
- const roomDrafts = new Map();
88
+ const roomDrafts = new FencedMap(isFenced);
75
89
  const presentations = new Map();
76
- const outcomes = new Map();
77
- const outcomeRooms = new Map();
78
- const wanted = new Map();
79
90
  let limits = [];
80
- let slots = [];
81
91
  let capabilities = [];
82
- function rewriting() {
83
- return capabilities.some((capability) => capability.name === 'move-reference-rewrites')
84
- ? 'provider'
85
- : 'runtime';
86
- }
87
- const rooms = new Map();
92
+ const rooms = new FencedMap(isFenced);
93
+ const counted = new Map();
88
94
  const unreadable = new Set();
89
- const models = new Map();
90
- const liveModels = new Map();
95
+ const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
96
+ const models = new FencedMap(isModelFenced);
97
+ const liveModels = new FencedMap(isModelFenced);
91
98
  const invalidModels = new Set();
92
- const modelEpoch = new Map();
93
99
  const discovered = new Map();
94
100
  let roomsListed = false;
95
101
  const scopedRefusals = new Map();
@@ -118,16 +124,29 @@ export function composeRuntime(options) {
118
124
  let status = STARTING;
119
125
  let shown = STARTING;
120
126
  let discoveryPaused = false;
121
- let timer;
122
127
  let closed = false;
123
128
  let stopping = false;
124
- let withdrawNotifications;
125
- let handed;
126
- const notices = new Map();
127
- let chain = Promise.resolve();
129
+ let withdrawStream;
130
+ const announced = new Map();
131
+ let underway = null;
132
+ let streamed = null;
133
+ let handed = false;
134
+ const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
135
+ const repairing = new Set();
136
+ const repairable = new Map();
137
+ let outcomeRetention = null;
138
+ const answering = new Map();
139
+ let complete = false;
140
+ let recorded = false;
141
+ const stale = new Set();
142
+ let generation = 0;
143
+ let discovering = null;
144
+ let opened = null;
145
+ const owed = new Set();
146
+ const lanes = new Map();
147
+ let roomReads = { generation, free: ROOM_READS, waiting: [] };
148
+ const working = new Set();
128
149
  let mutations = Promise.resolve();
129
- let inflight = false;
130
- let queued = false;
131
150
  const roomListeners = new Set();
132
151
  const refusalListeners = new Set();
133
152
  const storageLossListeners = new Set();
@@ -137,26 +156,39 @@ export function composeRuntime(options) {
137
156
  const presentationListeners = new Set();
138
157
  const readSide = createReadSide();
139
158
  let withdrawConversations;
140
- let currencyQueued = false;
159
+ let currencyQueued = null;
160
+ let socketOpen = false;
141
161
  let serviceMode = 'open';
142
162
  let haltedRefusal = false;
143
163
  let openSignals = 0;
144
164
  const serviceModeListeners = new Set();
165
+ let member = null;
166
+ const memberListeners = new Set();
167
+ const prepared = new Set();
168
+ const projecting = new Set();
169
+ const demanded = new Set();
170
+ let preparing = false;
171
+ const catching = new Set();
172
+ const transfers = new FencedMap(isFenced);
145
173
  const conversations = createConversationStore({
146
- cards: declaredCards,
147
- documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
174
+ agents: declaredAgents,
175
+ documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
148
176
  call: (name, input) => invokeAction(name, input),
149
177
  durably,
150
178
  stream: () => options.adapter.conversations,
151
179
  publish: publishChangeSet,
180
+ online: () => !status.providerUnreachable && refusal === null,
152
181
  closed: () => closed,
182
+ unanswered: resendLater,
183
+ expired: (operation) => pastHorizon(operation),
153
184
  });
154
185
  const searchIndex = createSearchIndex({
155
- documents: () => readSide.documents(),
186
+ room: (room) => readSide.room(room),
156
187
  document: (room, path) => readSide.document(room, path),
157
- messages: () => conversations.messages(),
158
- message: (conversation, identity) => conversations.message(conversation, identity),
159
188
  });
189
+ const roomsToIndex = new Set();
190
+ const conversationsToIndex = new Set();
191
+ let indexing = false;
160
192
  const journal = createLog();
161
193
  let notifyReady = () => { };
162
194
  const ready = new Promise((resolve) => {
@@ -220,7 +252,9 @@ export function composeRuntime(options) {
220
252
  }
221
253
  }
222
254
  function pending() {
223
- return [...replicas.values()].some((replica) => replica.queue.length > 0);
255
+ return (!localSaves &&
256
+ (queuesWithoutReplica.size > 0 ||
257
+ [...replicas.values()].some((replica) => replica.queue.length > 0)));
224
258
  }
225
259
  function attention() {
226
260
  const attentionRequired = [];
@@ -249,23 +283,30 @@ export function composeRuntime(options) {
249
283
  }
250
284
  return { attentionRequired, reset };
251
285
  }
286
+ function documentsIn(locator) {
287
+ const replica = replicas.get(locator);
288
+ if (replica === undefined)
289
+ return 0;
290
+ const roots = replica.confirmed.record.typedSelection?.roots ?? [];
291
+ const weighed = outsideOf(roots);
292
+ return (tally(replica.visible.documents, weighed.documents) +
293
+ tally(replica.visible.binaries, weighed.binaries));
294
+ }
252
295
  function listed() {
253
- return structuredClone([...rooms.values()]);
254
- }
255
- function slotted() {
256
- return structuredClone(slots.map(({ slot, entries }) => ({
257
- slot,
258
- entries: entries.flatMap(({ locator, segment }) => {
259
- const room = rooms.get(locator);
260
- return room === undefined ? [] : [{ room, segment }];
261
- }),
262
- })));
296
+ return structuredClone([...rooms.values()].map((room) => {
297
+ const documents = documentsIn(room.locator);
298
+ counted.set(room.locator, documents);
299
+ return { ...room, documents };
300
+ }));
263
301
  }
264
- function roomFacts() {
265
- const held = [...rooms.values()].map((room) => {
302
+ function roomFacts(reach = null) {
303
+ const held = [...rooms.values()]
304
+ .filter((room) => holds(reach, room.locator))
305
+ .map((room) => {
306
+ const governed = governs(reach, room);
266
307
  const attached = [
267
- modelOf(room.locator),
268
- ...(room.typedSelection === null ? [] : [modelOf(room.locator, 'typed-selection')]),
308
+ ...(governed.collection ? [modelOf(room.locator)] : []),
309
+ ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
269
310
  ];
270
311
  return {
271
312
  locator: room.locator,
@@ -276,6 +317,7 @@ export function composeRuntime(options) {
276
317
  return { listed: roomsListed, rooms: held };
277
318
  }
278
319
  function emitRooms() {
320
+ readSide.recover();
279
321
  const value = listed();
280
322
  for (const listener of roomListeners)
281
323
  listener(value);
@@ -288,6 +330,10 @@ export function composeRuntime(options) {
288
330
  for (const listener of storageLossListeners)
289
331
  listener(storageLoss);
290
332
  }
333
+ function emitMember() {
334
+ for (const listener of memberListeners)
335
+ listener(member);
336
+ }
291
337
  function moveTo(next) {
292
338
  if (closed)
293
339
  return;
@@ -306,14 +352,17 @@ export function composeRuntime(options) {
306
352
  for (const listener of statusListeners)
307
353
  listener(shown);
308
354
  }
309
- function submissionOf(batch) {
310
- return pendings.get(batch.id)?.submission ?? { state: 'waiting' };
355
+ function entryOf(batch) {
356
+ if (localSaves)
357
+ return Object.freeze({ batch, state: 'local' });
358
+ const state = pendings.get(batch.id);
359
+ if (state?.accepted !== undefined) {
360
+ return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
361
+ }
362
+ return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
311
363
  }
312
364
  function entriesOf(locator) {
313
- const replica = replicas.get(locator);
314
- if (replica === undefined)
315
- return [];
316
- return replica.queue.map((batch) => Object.freeze({ batch, ...submissionOf(batch) }));
365
+ return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
317
366
  }
318
367
  function emitQueue(locator) {
319
368
  const entries = entriesOf(locator);
@@ -323,12 +372,191 @@ export function composeRuntime(options) {
323
372
  }
324
373
  }
325
374
  function applyChangeSet(set) {
326
- readSide.apply(set);
327
- searchIndex.changed(set);
375
+ const held = {
376
+ ...set,
377
+ documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
378
+ };
379
+ readSide.apply(held);
380
+ searchIndex.changed(held);
381
+ }
382
+ async function referenced(locator) {
383
+ for (;;) {
384
+ const record = replicas.get(locator)?.confirmed.record;
385
+ if (record === undefined || closed)
386
+ return;
387
+ const held = { text: record.documents, binaries: record.binaries };
388
+ const composition = compositionOf(record);
389
+ if (referencesIndexed(held, composition))
390
+ return;
391
+ await indexReferrers(held, composition);
392
+ }
393
+ }
394
+ async function prepareReplica(locator) {
395
+ await referenced(locator);
396
+ const replica = replicas.get(locator);
397
+ if (replica === undefined || closed || prepared.has(locator))
398
+ return;
399
+ const visible = replica.visible;
400
+ const paths = [
401
+ ...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
402
+ ];
403
+ const entered = [];
404
+ projecting.add(locator);
405
+ try {
406
+ await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
407
+ const current = replicas.get(locator);
408
+ if (current === undefined || closed)
409
+ return;
410
+ const set = entering(locator, current, chunk);
411
+ readSide.apply(set);
412
+ entered.push(set);
413
+ });
414
+ if (!replicas.has(locator) || closed)
415
+ return;
416
+ prepared.add(locator);
417
+ }
418
+ finally {
419
+ projecting.delete(locator);
420
+ }
421
+ conversations.changed(mergeChangeSets(entered));
422
+ readSide.recover();
423
+ }
424
+ function unprepared() {
425
+ return [...demanded, ...replicas.keys()].find((locator) => !prepared.has(locator));
426
+ }
427
+ function prepare(locators = []) {
428
+ for (const locator of locators)
429
+ demanded.add(locator);
430
+ if (preparing)
431
+ return;
432
+ preparing = true;
433
+ void (async () => {
434
+ try {
435
+ for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
436
+ await yielded();
437
+ if (closed)
438
+ return;
439
+ demanded.delete(waiting);
440
+ await prepareReplica(waiting);
441
+ }
442
+ }
443
+ finally {
444
+ preparing = false;
445
+ }
446
+ })();
447
+ }
448
+ function reachedRooms(plan) {
449
+ const reach = plan.reach;
450
+ const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
451
+ const named = plan.rooms ??
452
+ (reached === null
453
+ ? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
454
+ : [...reached]);
455
+ return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
456
+ }
457
+ function awaited(locator) {
458
+ return rooms.has(locator) || !roomsListed;
459
+ }
460
+ function covered(plan) {
461
+ const locators = reachedRooms(plan);
462
+ if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
463
+ return { pending: [] };
464
+ }
465
+ const pending = [];
466
+ const coverage = [];
467
+ const overlay = [];
468
+ for (const locator of locators) {
469
+ const replica = replicas.get(locator);
470
+ if (replica === undefined) {
471
+ if (awaited(locator))
472
+ pending.push({ room: locator, awaits: 'acquisition' });
473
+ continue;
474
+ }
475
+ if (!prepared.has(locator)) {
476
+ pending.push({ room: locator, awaits: 'preparation' });
477
+ continue;
478
+ }
479
+ coverage.push({
480
+ room: locator,
481
+ generation: replica.confirmed.record.transfer,
482
+ current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
483
+ });
484
+ for (const batch of replica.queue)
485
+ overlay.push(batch.id);
486
+ }
487
+ if (pending.length > 0)
488
+ return { pending };
489
+ return { coverage: { rooms: coverage, indexes: [], overlay } };
490
+ }
491
+ function awaitPreparation(cover) {
492
+ if (!('pending' in cover))
493
+ return;
494
+ const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
495
+ if (waiting.length > 0)
496
+ prepare(waiting.map(({ room }) => room));
497
+ }
498
+ async function rowsQuery(plan) {
499
+ const before = covered(plan);
500
+ awaitPreparation(before);
501
+ if ('pending' in before)
502
+ return before;
503
+ const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
504
+ awaitPreparation(cover);
505
+ if ('pending' in cover)
506
+ return cover;
507
+ return 'groups' in rows
508
+ ? { groups: rows.groups, coverage: cover.coverage }
509
+ : { documents: rows.documents, coverage: cover.coverage };
510
+ }
511
+ async function indexConversation(conversation) {
512
+ searchIndex.indexMessages(conversation, [], false);
513
+ let after = null;
514
+ for (;;) {
515
+ const batch = await durably((local) => local.messagesAfter(conversation, after));
516
+ if (closed)
517
+ return;
518
+ searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
519
+ if (batch.length === 0)
520
+ return;
521
+ after = batch[batch.length - 1].identity;
522
+ }
523
+ }
524
+ function indexNext() {
525
+ const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator));
526
+ if (room !== undefined) {
527
+ roomsToIndex.delete(room);
528
+ return () => (prepared.has(room) ? searchIndex.indexRoom(room) : undefined);
529
+ }
530
+ const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
531
+ if (conversation === undefined)
532
+ return null;
533
+ conversationsToIndex.delete(conversation);
534
+ return () => indexConversation(conversation);
535
+ }
536
+ function index(rooms, named) {
537
+ for (const room of rooms)
538
+ roomsToIndex.add(room);
539
+ for (const conversation of named)
540
+ conversationsToIndex.add(conversation);
541
+ if (indexing)
542
+ return;
543
+ indexing = true;
544
+ void (async () => {
545
+ try {
546
+ for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
547
+ await yielded();
548
+ await unit();
549
+ }
550
+ }
551
+ finally {
552
+ indexing = false;
553
+ }
554
+ })().catch(() => { });
328
555
  }
329
556
  function publishChangeSet(set) {
330
557
  applyChangeSet(set);
331
- if (set.rooms.length > 0)
558
+ if (set.rooms.length > 0 ||
559
+ [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
332
560
  emitRooms();
333
561
  for (const locator of set.queues)
334
562
  emitQueue(locator);
@@ -339,26 +567,8 @@ export function composeRuntime(options) {
339
567
  for (const app of set.presentations)
340
568
  emitPresentation(app);
341
569
  conversations.changed(set);
342
- const cards = declaredCards();
343
- if (cards !== undefined && set.documents.some((change) => change.locator === cards.locator)) {
344
- scheduleCurrency();
345
- }
346
- }
347
- function stopTimer() {
348
- if (timer === undefined)
349
- return;
350
- clearInterval(timer);
351
- timer = undefined;
352
- }
353
- function startTimer() {
354
- if (timer !== undefined)
355
- return;
356
- timer = setInterval(() => {
357
- void enqueue();
358
- }, period);
359
570
  }
360
571
  async function refuse(reason, provider = null) {
361
- stopTimer();
362
572
  discoveryPaused = true;
363
573
  try {
364
574
  if (reason === 'revoked-authorization') {
@@ -366,10 +576,7 @@ export function composeRuntime(options) {
366
576
  const revoked = { status: 'unavailable', reason: 'revoked' };
367
577
  try {
368
578
  const local = await openLocal();
369
- await local.revokeModels([...replicas.values()].map((replica) => ({
370
- record: replica.confirmed.record,
371
- queue: replica.queue,
372
- })));
579
+ await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
373
580
  }
374
581
  catch {
375
582
  journal.record('model revocation persistence failed');
@@ -381,7 +588,6 @@ export function composeRuntime(options) {
381
588
  }
382
589
  for (const key of keys) {
383
590
  invalidModels.add(key);
384
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
385
591
  models.set(key, revoked);
386
592
  liveModels.delete(key);
387
593
  }
@@ -394,10 +600,7 @@ export function composeRuntime(options) {
394
600
  model: revoked,
395
601
  selectionModel: revoked,
396
602
  };
397
- const read = readConfirmed(record, before.confirmed);
398
- replicas.set(locator, read.kind === 'staged'
399
- ? replicaOf(read.confirmed, before.queue, rewriting(), before)
400
- : { ...before, confirmed: { ...before.confirmed, record } });
603
+ replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
401
604
  }
402
605
  rooms.set(locator, {
403
606
  ...room,
@@ -414,7 +617,8 @@ export function composeRuntime(options) {
414
617
  }
415
618
  finally {
416
619
  refusal = {
417
- reason,
620
+ code: reason,
621
+ retryable: false,
418
622
  reauthorizationOffered: reason === 'revoked-authorization',
419
623
  refusal: provider,
420
624
  };
@@ -427,36 +631,40 @@ export function composeRuntime(options) {
427
631
  rooms.set(room.locator, {
428
632
  name: room.name,
429
633
  locator: room.locator,
634
+ handle: room.handle,
430
635
  operations: room.operations,
431
636
  replica: room.replica,
432
- attachedModel: room.attachedModel,
433
637
  model: modelTypes(modelOf(room.locator)),
434
638
  typedSelection: selection === null
435
639
  ? null
436
640
  : {
437
641
  roots: selection.roots,
438
- attachedModel: selection.attachedModel,
439
642
  model: modelTypes(modelOf(room.locator, 'typed-selection')),
440
643
  },
441
- untypedFolders: room.untypedFolders ?? [],
442
644
  providerOwnedPaths: room.providerOwnedPaths,
443
645
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
444
- attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
445
646
  synchronizationAvailable,
446
647
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
447
648
  });
448
649
  }
449
- async function applyDiscovery(listed, local) {
650
+ async function applyDiscovery(found, started) {
651
+ for (const room of found) {
652
+ const fence = fences.get(room.locator);
653
+ if (fence !== undefined && fence.erased && fence.at < started)
654
+ fences.delete(room.locator);
655
+ }
656
+ const listed = found.filter((room) => !fences.has(room.locator));
450
657
  const listedLocators = new Set(listed.map((room) => room.locator));
451
658
  for (const locator of listedLocators)
452
659
  scopedRefusals.delete(locator);
453
660
  discovered.clear();
454
661
  for (const room of listed)
455
662
  discovered.set(room.locator, room);
456
- for (const locator of wanted.keys())
457
- if (!listedLocators.has(locator))
458
- wanted.delete(locator);
459
- await dropUnlisted(listedLocators, local);
663
+ const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
664
+ if (options.adapter.exhaustiveDiscovery === true)
665
+ await loseRooms(unlisted);
666
+ else
667
+ unlistedKept(unlisted);
460
668
  for (const room of listed) {
461
669
  const held = replicas.get(room.locator)?.confirmed.record;
462
670
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -467,56 +675,89 @@ export function composeRuntime(options) {
467
675
  }
468
676
  roomsListed = true;
469
677
  emitRooms();
678
+ return listed;
470
679
  }
471
680
  function unlistedHeld(listed) {
472
- return [...new Set([...rooms.keys(), ...roomDrafts.keys()])].filter((locator) => !listed.has(locator));
473
- }
474
- async function dropUnlisted(listed, local) {
475
- if (unlistedHeld(listed).length === 0)
476
- return;
477
- await exclusive(async () => {
478
- const unlisted = unlistedHeld(listed);
479
- await local.dropRooms(unlisted);
480
- for (const [batch, locator] of outcomeRooms) {
481
- if (listed.has(locator))
482
- continue;
483
- outcomeRooms.delete(batch);
484
- outcomes.delete(batch);
485
- }
486
- const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
487
- room: true,
488
- queue: true,
489
- drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
490
- }));
491
- for (const locator of unlisted) {
492
- for (const batch of replicas.get(locator)?.queue ?? [])
493
- pendings.delete(batch.id);
494
- replicas.delete(locator);
681
+ return [
682
+ ...new Set([
683
+ ...rooms.keys(),
684
+ ...roomDrafts.keys(),
685
+ ...transfers.keys(),
686
+ ...queuesWithoutReplica.keys(),
687
+ ]),
688
+ ].filter((locator) => !listed.has(locator));
689
+ }
690
+ function unlistedKept(unlisted) {
691
+ for (const locator of unlisted) {
692
+ const held = replicas.get(locator)?.confirmed.record;
693
+ if (held !== undefined)
694
+ list(held, false);
695
+ else
495
696
  rooms.delete(locator);
496
- roomDrafts.delete(locator);
497
- unreadable.delete(locator);
498
- scopedRefusals.delete(locator);
499
- sourceRefusals.delete(locator);
500
- declarationRefusals.delete(locator);
501
- for (const attached of ['collection', 'typed-selection']) {
502
- const key = modelKey(locator, attached);
503
- models.delete(key);
504
- liveModels.delete(key);
505
- invalidModels.delete(key);
506
- modelEpoch.delete(key);
507
- }
508
- journal.record(`room unlisted ${locator}`);
697
+ }
698
+ }
699
+ function fence(locators) {
700
+ const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
701
+ const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
702
+ room: true,
703
+ queue: true,
704
+ drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
705
+ }));
706
+ for (const locator of fresh) {
707
+ fences.set(locator, { at: discoveries, erased: false });
708
+ lastFenced.set(locator, ++fencings);
709
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
710
+ pendings.delete(batch.id);
711
+ replicas.delete(locator);
712
+ queuesWithoutReplica.delete(locator);
713
+ rooms.delete(locator);
714
+ roomDrafts.delete(locator);
715
+ transfers.delete(locator);
716
+ catching.delete(locator);
717
+ discovered.delete(locator);
718
+ announced.delete(locator);
719
+ unreadable.delete(locator);
720
+ stale.delete(locator);
721
+ repairable.delete(locator);
722
+ scopedRefusals.delete(locator);
723
+ sourceRefusals.delete(locator);
724
+ declarationRefusals.delete(locator);
725
+ for (const attached of ['collection', 'typed-selection']) {
726
+ const key = modelKey(locator, attached);
727
+ models.delete(key);
728
+ liveModels.delete(key);
729
+ invalidModels.delete(key);
509
730
  }
510
- publishChangeSet(mergeChangeSets(removed));
511
- moveTo({ ...status, ...attention() });
512
- });
731
+ journal.record(`room lost ${locator}`);
732
+ }
733
+ if (fresh.length === 0)
734
+ return fresh;
735
+ publishChangeSet(mergeChangeSets(removed));
736
+ for (const locator of fresh)
737
+ prepared.delete(locator);
738
+ searchIndex.forget(fresh);
739
+ readSide.recover();
740
+ moveTo({ ...status, ...attention() });
741
+ return fresh;
742
+ }
743
+ async function erased(local, locators) {
744
+ await local.erase(locators);
745
+ for (const locator of locators) {
746
+ const held = fences.get(locator);
747
+ if (held !== undefined)
748
+ held.erased = true;
749
+ }
750
+ }
751
+ async function loseRooms(locators) {
752
+ const fresh = fence(locators);
753
+ if (fresh.length > 0)
754
+ await durably((local) => erased(local, fresh));
513
755
  }
514
756
  function restored(key, persisted) {
515
757
  const prior = models.get(key);
516
758
  if (prior !== undefined &&
517
759
  prior.status !== 'unavailable' &&
518
760
  (persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
519
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
520
761
  invalidModels.add(key);
521
762
  }
522
763
  const model = persisted.status === 'unavailable' ||
@@ -531,10 +772,9 @@ export function composeRuntime(options) {
531
772
  return model;
532
773
  }
533
774
  async function hydrate(opened) {
534
- const [records, queues, recordedOutcomes, kept, presented, held, chatDrafts, lastCompleted, declared, named, listedSlots,] = await Promise.all([
775
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
535
776
  opened.all(),
536
777
  opened.queues(),
537
- opened.outcomes(),
538
778
  opened.roomDrafts(),
539
779
  opened.presentation(),
540
780
  opened.conversations(),
@@ -542,11 +782,15 @@ export function composeRuntime(options) {
542
782
  opened.lastCompleted(),
543
783
  opened.providerLimits(),
544
784
  opened.capabilities(),
545
- opened.slots(),
785
+ opened.member(),
786
+ opened.transfers(),
546
787
  ]);
788
+ transfers.clear();
789
+ for (const record of staged)
790
+ transfers.set(record.locator, record);
547
791
  limits = declared;
548
792
  capabilities = named;
549
- slots = listedSlots;
793
+ member = known;
550
794
  roomDrafts.clear();
551
795
  for (const record of kept)
552
796
  rememberRoomDraft(record);
@@ -560,27 +804,20 @@ export function composeRuntime(options) {
560
804
  for (const { batch, state } of queue.pending)
561
805
  pendings.set(batch.id, state);
562
806
  }
563
- outcomes.clear();
564
- outcomeRooms.clear();
565
- for (const record of recordedOutcomes) {
566
- outcomes.set(record.id, record.outcome);
567
- outcomeRooms.set(record.id, record.locator);
568
- }
569
807
  const previousReplicas = new Map(replicas);
570
808
  replicas.clear();
809
+ queuesWithoutReplica.clear();
810
+ const replicated = new Set(records.map((record) => record.locator));
811
+ for (const [locator, queue] of queuedBatches) {
812
+ if (!replicated.has(locator))
813
+ queuesWithoutReplica.set(locator, queue);
814
+ }
571
815
  for (const record of records) {
572
816
  const model = restored(modelKey(record.locator, 'collection'), record.model);
573
817
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
574
818
  const prior = previousReplicas.get(record.locator);
575
- const retyped = { ...record, model, selectionModel };
576
- const read = readConfirmed(retyped, prior?.confirmed);
577
- const confirmed = read.kind === 'staged'
578
- ? read.confirmed
579
- : {
580
- record: retyped,
581
- collection: parseCollection(retyped.documents, compositionOf(retyped)),
582
- };
583
- replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], rewriting(), prior));
819
+ const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
820
+ replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
584
821
  const existing = rooms.get(record.locator);
585
822
  if (existing === undefined)
586
823
  list(record, false);
@@ -588,25 +825,17 @@ export function composeRuntime(options) {
588
825
  rooms.set(record.locator, {
589
826
  ...existing,
590
827
  roomVersion: record.roomVersion,
591
- attachments: record.attachments,
592
828
  operations: record.operations,
593
829
  replica: record.replica,
594
- attachedModel: record.attachedModel,
595
830
  model: modelTypes(model),
596
831
  typedSelection: record.typedSelection === null
597
832
  ? null
598
833
  : {
599
834
  roots: record.typedSelection.roots,
600
- attachedModel: record.typedSelection.attachedModel,
601
835
  model: modelTypes(selectionModel),
602
836
  },
603
- untypedFolders: record.untypedFolders,
604
837
  });
605
838
  }
606
- if (read.kind === 'refused') {
607
- refusedSource(record, read);
608
- declarationRefusals.add(record.locator);
609
- }
610
839
  }
611
840
  return {
612
841
  lastCompleted,
@@ -617,16 +846,26 @@ export function composeRuntime(options) {
617
846
  async function readState(opened) {
618
847
  const { lastCompleted, locators, read } = await hydrate(opened);
619
848
  applyChangeSet(read);
849
+ prepare();
620
850
  if (replicas.size > 0)
621
851
  emitRooms();
852
+ if (member !== null)
853
+ emitMember();
622
854
  for (const locator of locators)
623
855
  emitQueue(locator);
624
856
  publishRoomDrafts(false);
625
857
  return lastCompleted;
626
858
  }
627
859
  async function open() {
628
- const opened = await openRuntimeStore();
860
+ if (closed)
861
+ throw new Error('runtime closed');
862
+ const opened = await openRuntimeStore(isFenced);
629
863
  store = opened;
864
+ const interrupted = await opened.fences();
865
+ for (const locator of interrupted) {
866
+ fences.set(locator, { at: discoveries, erased: false });
867
+ lastFenced.set(locator, ++fencings);
868
+ }
630
869
  const kept = options.shell.generation;
631
870
  const storedGeneration = await opened.generation();
632
871
  const loss = detectStorageLoss(storedGeneration, kept);
@@ -640,6 +879,8 @@ export function composeRuntime(options) {
640
879
  emitStorageLoss();
641
880
  }
642
881
  const lastCompleted = await readState(opened);
882
+ if (interrupted.length > 0)
883
+ await erased(opened, interrupted);
643
884
  moveTo({ ...status, lastCompleted, ...attention() });
644
885
  return opened;
645
886
  }
@@ -666,10 +907,15 @@ export function composeRuntime(options) {
666
907
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
667
908
  }
668
909
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
669
- const withdrawn = withdrawals(confirmed, queue, withdrawable, rewriting());
910
+ const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
911
+ const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
670
912
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
913
+ const unqueued = withdrawn.map((batch) => ({
914
+ batch,
915
+ returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
916
+ }));
671
917
  const returning = [
672
- ...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
918
+ ...[...unqueued].reverse(),
673
919
  ...(settling.leaving === undefined ? [] : [settling.leaving]),
674
920
  ];
675
921
  const written = new Map();
@@ -687,131 +933,271 @@ export function composeRuntime(options) {
687
933
  });
688
934
  }
689
935
  }
690
- const settledOutcomes = [
691
- ...(settling.accepted === undefined ? [] : [settling.accepted]),
692
- ...(settling.leaving === undefined
693
- ? []
694
- : [{ id: settling.leaving.batch.id, outcome: settling.leaving.outcome }]),
695
- ...withdrawn.map((batch) => ({ id: batch.id, outcome: { kind: 'withdrawn' } })),
696
- ];
697
- await local.write({
698
- next: confirmed.record,
699
- previous: before?.confirmed.record ?? null,
700
- queue: kept,
701
- previousQueue: before?.queue ?? [],
702
- outcomes: settledOutcomes,
703
- roomDrafts: [...written.values()],
704
- });
705
- for (const { id, outcome } of settledOutcomes) {
706
- outcomes.set(id, outcome);
707
- outcomeRooms.set(id, locator);
708
- pendings.delete(id);
936
+ await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
937
+ for (const batch of previousQueue) {
938
+ if (!kept.includes(batch))
939
+ pendings.delete(batch.id);
709
940
  }
710
941
  for (const record of written.values())
711
942
  rememberRoomDraft(record);
712
- for (const batch of withdrawn)
713
- journal.record(`outcome ${locator} ${batch.id} withdrawn`);
714
- replicas.set(locator, replicaOf(confirmed, kept, rewriting(), before));
715
- return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
716
- room: settling.room,
717
- queue: kept.length !== (before?.queue.length ?? 0),
718
- drafts: [...written.keys()],
719
- });
943
+ try {
944
+ await local.write({
945
+ next: confirmed.record,
946
+ previous: before?.confirmed.record ?? null,
947
+ ...(settling.stored === undefined ? {} : { stored: settling.stored }),
948
+ });
949
+ }
950
+ catch (error) {
951
+ const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
952
+ if (held !== null)
953
+ replicas.set(locator, held);
954
+ else if (kept.length > 0)
955
+ queuesWithoutReplica.set(locator, kept);
956
+ else
957
+ queuesWithoutReplica.delete(locator);
958
+ publishChangeSet(changeSet(locator, before ?? null, held, {
959
+ queue: kept.length !== previousQueue.length,
960
+ drafts: [...written.keys()],
961
+ }));
962
+ moveTo({ ...status, ...attention() });
963
+ throw error;
964
+ }
965
+ return () => {
966
+ queuesWithoutReplica.delete(locator);
967
+ for (const { batch, returned } of unqueued) {
968
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
969
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
970
+ }
971
+ replicas.set(locator, replicaOf(confirmed, kept, before));
972
+ return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
973
+ room: settling.room,
974
+ queue: kept.length !== previousQueue.length,
975
+ drafts: [...written.keys()],
976
+ });
977
+ };
978
+ }
979
+ function adoptModels(locator, record) {
980
+ for (const [attached, model] of [
981
+ ['collection', record.model],
982
+ ['typed-selection', record.selectionModel],
983
+ ]) {
984
+ const key = modelKey(locator, attached);
985
+ models.set(key, model);
986
+ invalidModels.delete(key);
987
+ if (model.status === 'current')
988
+ liveModels.set(key, JSON.stringify(model.snapshot));
989
+ else if (model.status === 'unavailable')
990
+ liveModels.delete(key);
991
+ }
992
+ }
993
+ function remodel(locator, answers, held) {
994
+ const taken = (answer, previous, attached) => {
995
+ if (answer === undefined)
996
+ return previous;
997
+ const { state, refused } = attachedModel(answer, previous);
998
+ if (refused !== null) {
999
+ const where = refused.location === undefined ? '' : ` ${refused.location}`;
1000
+ journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
1001
+ }
1002
+ models.set(modelKey(locator, attached), state);
1003
+ return state;
1004
+ };
1005
+ return {
1006
+ collection: taken(answers?.collection, held.collection, 'collection'),
1007
+ selection: taken(answers?.selection, held.selection, 'typed-selection'),
1008
+ };
720
1009
  }
721
- function publish(local, room, confirmed) {
1010
+ function publish(local, room, confirmed, landing, live) {
1011
+ const { base, stored } = landing;
722
1012
  return exclusive(async () => {
723
- if (!continuing())
724
- return;
1013
+ if (stored !== undefined) {
1014
+ await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
1015
+ }
1016
+ if (!live()) {
1017
+ caughtUpTo(room.locator);
1018
+ return true;
1019
+ }
725
1020
  const prior = replicas.get(room.locator);
726
- const retained = readConfirmed({
727
- ...confirmed.record,
728
- model: modelOf(room.locator),
729
- selectionModel: modelOf(room.locator, 'typed-selection'),
730
- }, prior?.confirmed);
731
- if (retained.kind !== 'staged')
732
- throw new Error(`Cannot retype staged Room ${room.locator}`);
733
- const queue = prior?.queue ?? [];
734
- const roomVersion = retained.confirmed.record.roomVersion;
1021
+ if (base !== undefined &&
1022
+ (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
1023
+ prior.confirmed.record.revisions !== base.record.revisions)) {
1024
+ return false;
1025
+ }
1026
+ adoptModels(room.locator, confirmed.record);
1027
+ const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
1028
+ const roomVersion = confirmed.record.roomVersion;
735
1029
  const accepted = queue.filter((batch) => {
736
- const outcome = outcomes.get(batch.id);
737
- return (outcome?.kind === 'accepted' &&
738
- options.adapter.reached(room.locator, roomVersion, outcome.roomVersion));
1030
+ const accepted = pendings.get(batch.id)?.accepted;
1031
+ return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
739
1032
  });
740
- const set = await settle(local, room.locator, prior, retained.confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
1033
+ const set = (await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) }))();
1034
+ if (stored !== undefined) {
1035
+ transfers.delete(room.locator);
1036
+ catching.delete(room.locator);
1037
+ }
741
1038
  for (const batch of accepted)
742
1039
  pendings.delete(batch.id);
743
- const target = wanted.get(room.locator);
744
- if (target !== undefined && options.adapter.reached(room.locator, roomVersion, target)) {
745
- wanted.delete(room.locator);
746
- }
747
- unreadable.delete(room.locator);
1040
+ stale.delete(room.locator);
1041
+ if (unreadable.delete(room.locator))
1042
+ scopedRefusals.delete(room.locator);
748
1043
  sourceRefusals.delete(room.locator);
749
1044
  declarationRefusals.delete(room.locator);
750
1045
  list(room, true);
1046
+ if (prior === undefined)
1047
+ prepared.add(room.locator);
751
1048
  publishChangeSet(set);
752
1049
  moveTo({ ...status, ...attention() });
1050
+ options.adapter.roomStream?.published(room.locator, roomVersion);
1051
+ return true;
753
1052
  });
754
1053
  }
755
1054
  async function saveRoomDrafts(locator, named, writtenBy) {
756
1055
  const local = await openLocal();
1056
+ await referenced(locator);
757
1057
  const replica = replicas.get(locator);
758
1058
  const room = rooms.get(locator);
759
1059
  if (replica === undefined || room === undefined) {
760
- throw new Error(`No Local Replica for ${locator}`);
1060
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
761
1061
  }
762
1062
  const saved = [];
763
1063
  for (const { path, revision } of named) {
764
1064
  const record = roomDrafts.get(locator)?.get(path);
765
1065
  if (record === undefined || record.revision !== revision) {
766
- return { outcome: 'refused', rule: 'stale', file: path };
1066
+ return { outcome: 'refused', code: 'stale', retryable: false, file: path };
767
1067
  }
768
1068
  if (!saved.some((draft) => draft.path === path))
769
1069
  saved.push({ path, parts: record.parts });
770
1070
  }
771
1071
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
772
1072
  if (changed.length > 0)
773
- return { outcome: 'refused', rule: 'changed-underneath', parts: changed };
1073
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
774
1074
  const parts = saved.map(({ path, parts }) => changesOf(path, parts));
775
- const built = buildBatch({
1075
+ const changes = parts.flatMap(({ changes }) => changes);
1076
+ const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
1077
+ if (requeue.kind === 'refused')
1078
+ return { outcome: 'refused', ...requeue.refusal };
1079
+ const foreign = [...requeue.withdrawn.keys()].some((origin) => {
1080
+ const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
1081
+ return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
1082
+ });
1083
+ if (foreign) {
1084
+ return {
1085
+ outcome: 'refused',
1086
+ code: 'unknown-revision',
1087
+ retryable: false,
1088
+ file: [...requeue.queued].at(-1),
1089
+ };
1090
+ }
1091
+ const withdrawing = requeue.withdrawn.size > 0;
1092
+ const input = {
1093
+ local: localSaves,
776
1094
  baseRoomVersion: replica.confirmed.record.roomVersion,
777
1095
  composition: compositionFor(replica.confirmed.record),
778
1096
  operations: room.operations,
779
1097
  providerOwnedPaths: room.providerOwnedPaths,
780
1098
  providerLimits: limits,
781
- rewriting: rewriting(),
1099
+ handle: room.handle,
782
1100
  visible: replica.visible,
783
- changes: parts.flatMap(({ changes }) => changes),
1101
+ changes,
784
1102
  bases: parts.flatMap(({ bases }) => bases),
785
1103
  writtenBy,
1104
+ clean: {
1105
+ text: replica.confirmed.record.documents,
1106
+ binaries: replica.confirmed.record.binaries,
1107
+ composition: compositionOf(replica.confirmed.record),
1108
+ },
1109
+ };
1110
+ const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
1111
+ if (checked?.kind === 'refused')
1112
+ return { outcome: 'refused', ...checked.refusal };
1113
+ const owners = [...requeue.owners].flatMap((origin) => {
1114
+ const batch = requeue.withdrawn.get(origin);
1115
+ return batch === undefined || batch === null ? [] : [{ origin, batch }];
786
1116
  });
787
- if (built.kind === 'refused')
1117
+ if (owners.length > 1) {
1118
+ return {
1119
+ outcome: 'refused',
1120
+ code: 'unknown-revision',
1121
+ retryable: false,
1122
+ file: [...requeue.queued].at(-1),
1123
+ };
1124
+ }
1125
+ const built = !withdrawing
1126
+ ? buildBatch(input)
1127
+ : requeue.changes.length === 0
1128
+ ? null
1129
+ : buildBatch({
1130
+ ...input,
1131
+ visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
1132
+ changes: requeue.changes,
1133
+ });
1134
+ if (built?.kind === 'refused')
788
1135
  return { outcome: 'refused', ...built.refusal };
789
- const batches = [...replica.queue, built.batch];
1136
+ const [owner] = owners;
1137
+ const join = built === null || owner === undefined
1138
+ ? null
1139
+ : joined(requeue.queue, owner.batch, built.batch);
1140
+ if (join?.kind === 'refused')
1141
+ return { outcome: 'refused', ...join.refusal };
1142
+ const withdrawn = new Map(requeue.withdrawn);
1143
+ if (join !== null && owner !== undefined)
1144
+ withdrawn.set(owner.origin, join.batch);
1145
+ const drafted = new Map();
1146
+ for (const [origin, replaced] of withdrawn) {
1147
+ if (replaced === null)
1148
+ continue;
1149
+ const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
1150
+ drafted.set(replaced.id, replaced === join?.batch
1151
+ ? [
1152
+ ...carried.map((held) => joinedDraft(held, saved)),
1153
+ ...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
1154
+ ]
1155
+ : carried);
1156
+ }
1157
+ if (built !== null && join === null)
1158
+ drafted.set(built.batch.id, saved);
1159
+ const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
1160
+ for (const replaced of withdrawn.values()) {
1161
+ if (replaced === null || !batches.includes(replaced))
1162
+ continue;
1163
+ const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
1164
+ if (limited !== null)
1165
+ return { outcome: 'refused', ...limited };
1166
+ }
790
1167
  const consumed = saved.map(({ path }) => path);
791
1168
  try {
792
- await local.write({
793
- next: replica.confirmed.record,
794
- previous: replica.confirmed.record,
1169
+ await local.keepAuthored({
1170
+ locator,
795
1171
  queue: batches,
796
- previousQueue: replica.queue,
797
- saved,
1172
+ saved: drafted,
798
1173
  consumedRoomDrafts: consumed,
799
1174
  });
800
1175
  }
801
1176
  catch {
802
- return { outcome: 'refused', rule: 'durable-write' };
1177
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
803
1178
  }
804
- pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
1179
+ if (fences.has(locator))
1180
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1181
+ for (const origin of withdrawn.keys())
1182
+ pendings.delete(origin);
1183
+ for (const [id, drafts] of drafted) {
1184
+ pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
1185
+ }
1186
+ if (!localSaves)
1187
+ recorded = false;
805
1188
  for (const path of consumed)
806
1189
  forgetRoomDraft(locator, path);
807
- replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting(), replica));
1190
+ replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
808
1191
  publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
809
1192
  queue: true,
810
1193
  drafts: consumed,
811
1194
  }));
812
- moveTo({ ...saving(status), ...attention() });
813
- void enqueue();
814
- return { outcome: 'saved', identifier: built.batch.id };
1195
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
1196
+ submitLater(locator);
1197
+ const identifier = join?.batch.id ??
1198
+ built?.batch.id ??
1199
+ [...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
1200
+ return { outcome: 'saved', identifier };
815
1201
  }
816
1202
  async function mark(local, locator, identifier, submission, lost) {
817
1203
  const state = pendings.get(identifier);
@@ -821,6 +1207,8 @@ export function composeRuntime(options) {
821
1207
  return;
822
1208
  }
823
1209
  await local.submit(locator, identifier, submission, lost);
1210
+ if (fences.has(locator))
1211
+ return;
824
1212
  pendings.set(identifier, { ...state, submission, lost });
825
1213
  const replica = replicas.get(locator);
826
1214
  if (replica !== undefined)
@@ -831,22 +1219,24 @@ export function composeRuntime(options) {
831
1219
  if (!continuing())
832
1220
  throw new Error('connection refused');
833
1221
  await local.keepUpload(locator, identifier, upload);
1222
+ if (fences.has(locator))
1223
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
834
1224
  pendings.set(identifier, { ...pendings.get(identifier), upload });
835
1225
  if (!continuing())
836
1226
  throw new Error('connection refused');
837
1227
  }
838
1228
  async function readHistory(locator, path, query = {}) {
839
1229
  if (closed || refusal !== null)
840
- return { kind: 'transport-uncertainty' };
1230
+ return UNCERTAIN;
841
1231
  try {
842
1232
  await ensureAuthorization();
843
1233
  }
844
1234
  catch {
845
- return { kind: 'transport-uncertainty' };
846
- }
847
- if (closed || authorization === undefined || refusal !== null) {
848
- return { kind: 'transport-uncertainty' };
1235
+ return UNCERTAIN;
849
1236
  }
1237
+ if (closed || authorization === undefined || refusal !== null)
1238
+ return UNCERTAIN;
1239
+ const since = fencings;
850
1240
  const transfer = ['transfer', 'history', locator, path];
851
1241
  if (query.revision !== undefined)
852
1242
  transfer.push(query.revision);
@@ -861,6 +1251,8 @@ export function composeRuntime(options) {
861
1251
  ...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
862
1252
  });
863
1253
  journal.record(`outcome history ${result.kind}`);
1254
+ if (fencedSince(locator, since))
1255
+ return failure({ kind: 'room-not-readable', locator });
864
1256
  if (result.kind === 'scoped-refusal') {
865
1257
  const room = replicas.get(result.locator)?.confirmed.record;
866
1258
  if (room !== undefined)
@@ -868,17 +1260,111 @@ export function composeRuntime(options) {
868
1260
  }
869
1261
  if (result.kind === 'revoked-authorization')
870
1262
  await refuse('revoked-authorization');
871
- return result;
1263
+ return historyAnswer(result);
872
1264
  }
873
1265
  catch {
874
1266
  journal.record('outcome history transport-uncertainty');
875
- return { kind: 'transport-uncertainty' };
1267
+ return UNCERTAIN;
876
1268
  }
877
1269
  }
878
- function declaredCards() {
1270
+ function queuedBlob(locator, path, localId) {
1271
+ for (const batch of replicas.get(locator)?.queue ?? []) {
1272
+ const change = batch.changes[queuedFile(batch, path, localId)];
1273
+ if (change?.kind === 'binary')
1274
+ return change.blob;
1275
+ }
1276
+ return null;
1277
+ }
1278
+ function binaryRoom(request) {
1279
+ if (request.kind === 'path')
1280
+ return request.locator;
1281
+ const scope = options.adapter.referrerScope(request.referrer);
1282
+ return scope?.kind === 'room' ? scope.locator : null;
1283
+ }
1284
+ async function readBinary(request) {
1285
+ if (closed || refusal !== null)
1286
+ return UNCERTAIN;
1287
+ try {
1288
+ await ensureAuthorization();
1289
+ }
1290
+ catch {
1291
+ return UNCERTAIN;
1292
+ }
1293
+ if (closed || authorization === undefined || refusal !== null)
1294
+ return UNCERTAIN;
1295
+ const since = fencings;
1296
+ journal.record(request.kind === 'path'
1297
+ ? `transfer binary ${request.locator} ${request.path}`
1298
+ : `transfer binary ${request.fileRef}`);
1299
+ try {
1300
+ const result = await options.adapter.binary(authorization, request);
1301
+ journal.record(`outcome binary ${result.kind}`);
1302
+ const locator = binaryRoom(request);
1303
+ if (locator !== null && fencedSince(locator, since))
1304
+ return failure({ kind: 'room-not-readable', locator });
1305
+ if (result.kind === 'scoped-refusal') {
1306
+ const room = replicas.get(result.locator)?.confirmed.record;
1307
+ if (room !== undefined)
1308
+ await scoped(result, room, await openLocal());
1309
+ }
1310
+ if (result.kind === 'revoked-authorization')
1311
+ await refuse('revoked-authorization');
1312
+ return binaryAnswer(result);
1313
+ }
1314
+ catch {
1315
+ journal.record('outcome binary transport-uncertainty');
1316
+ return UNCERTAIN;
1317
+ }
1318
+ }
1319
+ async function readBinaryPath(locator, path, revision) {
1320
+ await ready;
1321
+ if (revision !== undefined)
1322
+ return readBinary({ kind: 'path', locator, path, revision });
1323
+ const visible = replicas.get(locator)?.visible;
1324
+ const held = visible?.binaries[path];
1325
+ if (visible === undefined || held === undefined)
1326
+ return absentBinary(path);
1327
+ if (held.state === 'pending') {
1328
+ const blob = queuedBlob(locator, path, held.localId);
1329
+ return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1330
+ }
1331
+ const record = binaryRecord(path, held, visible.revisions[path]);
1332
+ if (record.state !== 'accepted')
1333
+ return absentBinary(path);
1334
+ const replica = replicas.get(locator);
1335
+ const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
1336
+ const holds = (at) => {
1337
+ const kept = replica?.confirmed.record.binaries[at];
1338
+ return (kept?.state === 'accepted' &&
1339
+ kept.fileRef === record.fileRef &&
1340
+ replica?.confirmed.record.revisions[at] === record.revision);
1341
+ };
1342
+ let confirmed = path;
1343
+ for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
1344
+ const change = queued[index];
1345
+ if (change.kind === 'move' && change.to === confirmed)
1346
+ confirmed = change.path;
1347
+ }
1348
+ return readBinary({
1349
+ kind: 'path',
1350
+ locator,
1351
+ path: confirmed,
1352
+ revision: record.revision,
1353
+ fileRef: record.fileRef,
1354
+ });
1355
+ }
1356
+ async function readBinaryReference(reference) {
1357
+ await ready;
1358
+ return readBinary({
1359
+ kind: 'reference',
1360
+ fileRef: reference.fileRef,
1361
+ referrer: reference.referrer,
1362
+ });
1363
+ }
1364
+ function declaredAgents() {
879
1365
  for (const capability of capabilities) {
880
- if (capability.conversationCards !== undefined)
881
- return capability.conversationCards;
1366
+ if (capability.conversationAgents !== undefined)
1367
+ return capability.conversationAgents;
882
1368
  }
883
1369
  return undefined;
884
1370
  }
@@ -894,23 +1380,20 @@ export function composeRuntime(options) {
894
1380
  async function invokeAction(name, input) {
895
1381
  await ready;
896
1382
  if (closed || refusal !== null)
897
- return { kind: 'transport-uncertainty' };
1383
+ return UNCERTAIN;
898
1384
  if (declaredAction(name) === undefined)
899
- return { kind: 'undeclared' };
1385
+ return undeclared(name);
900
1386
  try {
901
1387
  await ensureAuthorization();
902
1388
  }
903
1389
  catch {
904
- return { kind: 'transport-uncertainty' };
905
- }
906
- if (closed || authorization === undefined || refusal !== null) {
907
- return { kind: 'transport-uncertainty' };
1390
+ return UNCERTAIN;
908
1391
  }
1392
+ if (closed || authorization === undefined || refusal !== null)
1393
+ return UNCERTAIN;
909
1394
  journal.record(`transfer action ${name}`);
910
1395
  try {
911
1396
  const result = await options.adapter.action(authorization, { name, input });
912
- if (result.kind === 'action' && result.at !== undefined)
913
- want(result.at);
914
1397
  if (result.kind === 'scoped-refusal') {
915
1398
  const room = replicas.get(result.locator)?.confirmed.record;
916
1399
  if (room !== undefined)
@@ -921,11 +1404,11 @@ export function composeRuntime(options) {
921
1404
  journal.record(`outcome action ${result.kind}`);
922
1405
  if (result.kind === 'rejected')
923
1406
  journal.record(`refusal rejected ${result.code}`);
924
- return result;
1407
+ return calledResult(result);
925
1408
  }
926
1409
  catch {
927
1410
  journal.record('outcome action transport-uncertainty');
928
- return { kind: 'transport-uncertainty' };
1411
+ return UNCERTAIN;
929
1412
  }
930
1413
  }
931
1414
  function continuing() {
@@ -942,32 +1425,11 @@ export function composeRuntime(options) {
942
1425
  options.adapter.reached(locator, written, roomVersion) &&
943
1426
  !options.adapter.reached(locator, roomVersion, written));
944
1427
  }
945
- function want({ locator, roomVersion }) {
946
- if (reachedBy(locator, roomVersion))
947
- return;
948
- const held = wanted.get(locator);
949
- if (held === undefined || options.adapter.reached(locator, roomVersion, held)) {
950
- wanted.set(locator, roomVersion);
951
- }
952
- }
953
- function reachOf(locator) {
954
- let target = wanted.get(locator);
955
- const versions = (replicas.get(locator)?.queue ?? []).flatMap((batch) => {
956
- const outcome = outcomes.get(batch.id);
957
- return outcome?.kind === 'accepted' ? [outcome.roomVersion] : [];
958
- });
959
- for (const version of versions) {
960
- if (target === undefined || options.adapter.reached(locator, version, target)) {
961
- target = version;
962
- }
963
- }
964
- return target === undefined || reachedBy(locator, target) ? undefined : target;
965
- }
966
- async function pullRefresh(authorized, confirmed, minPosition) {
967
- if (!continuing())
1428
+ async function pullRefresh(authorized, confirmed, live) {
1429
+ if (!live())
968
1430
  return unconfirmed;
969
1431
  try {
970
- const result = await refreshRoom(options.adapter, authorized, confirmed, continuing, minPosition);
1432
+ const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
971
1433
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
972
1434
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
973
1435
  return result;
@@ -977,55 +1439,121 @@ export function composeRuntime(options) {
977
1439
  throw error;
978
1440
  }
979
1441
  }
980
- async function pullBootstrap(authorized, room) {
981
- if (!continuing())
1442
+ function staging(local, live) {
1443
+ return {
1444
+ held: (locator) => transfers.get(locator) ?? null,
1445
+ staged: (record) => local.staged(record),
1446
+ stage: (record, page) => exclusive(async () => {
1447
+ if (!live())
1448
+ return false;
1449
+ await local.stage(record, page);
1450
+ transfers.set(record.locator, record);
1451
+ readSide.recover();
1452
+ return true;
1453
+ }),
1454
+ abandon: (locator) => exclusive(async () => {
1455
+ if (!live())
1456
+ return;
1457
+ await local.abandon(locator);
1458
+ transfers.delete(locator);
1459
+ readSide.recover();
1460
+ }),
1461
+ };
1462
+ }
1463
+ function reusable(room) {
1464
+ const record = replicas.get(room.locator)?.confirmed.record;
1465
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1466
+ return null;
1467
+ return { documents: record.documents, revisions: record.revisions };
1468
+ }
1469
+ async function pullBootstrap(authorized, room, local, live) {
1470
+ if (!live())
982
1471
  return unconfirmed;
1472
+ if (replicas.has(room.locator)) {
1473
+ catching.add(room.locator);
1474
+ readSide.recover();
1475
+ }
983
1476
  try {
984
1477
  const result = await bootstrapRoom(options.adapter, authorized, room, {
985
1478
  collection: modelOf(room.locator),
986
1479
  selection: modelOf(room.locator, 'typed-selection'),
987
- }, continuing);
1480
+ }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
988
1481
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
989
1482
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1483
+ if (result.kind !== 'staged')
1484
+ caughtUpTo(room.locator);
990
1485
  return result;
991
1486
  }
992
1487
  catch (error) {
993
1488
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
1489
+ caughtUpTo(room.locator);
994
1490
  throw error;
995
1491
  }
996
1492
  }
997
- async function advance(room, local, authorized) {
1493
+ function caughtUpTo(locator) {
1494
+ if (catching.delete(locator))
1495
+ readSide.recover();
1496
+ }
1497
+ async function advance(room, local, authorized, live) {
1498
+ await referenced(room.locator);
1499
+ if (!live())
1500
+ return unconfirmed;
998
1501
  const held = replicas.get(room.locator);
999
1502
  if (held !== undefined) {
1000
- const refreshed = await pullRefresh(authorized, held.confirmed, reachOf(room.locator));
1001
- if (!continuing())
1503
+ const refreshed = await pullRefresh(authorized, held.confirmed, live);
1504
+ if (!live())
1002
1505
  return unconfirmed;
1003
1506
  if (refreshed.kind === 'still-current') {
1507
+ stale.delete(room.locator);
1004
1508
  if (sourceRefusals.has(room.locator))
1005
- return unconfirmed;
1509
+ return null;
1006
1510
  if (unreadable.delete(room.locator)) {
1511
+ scopedRefusals.delete(room.locator);
1007
1512
  list(room, true);
1008
1513
  emitRooms();
1009
1514
  }
1010
1515
  return null;
1011
1516
  }
1012
1517
  if (refreshed.kind === 'advanced') {
1013
- await publish(local, room, refreshed.confirmed);
1518
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
1519
+ const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
1520
+ const reached = now !== undefined &&
1521
+ options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
1522
+ return reached ? null : advance(room, local, authorized, live);
1523
+ }
1524
+ await retainModel(local, room, 'collection', modelOf(room.locator));
1014
1525
  return null;
1015
1526
  }
1016
1527
  if (refreshed.kind !== 'beyond-horizon')
1017
1528
  return refreshed;
1529
+ if (opened !== null) {
1530
+ owed.add(room.locator);
1531
+ readSide.recover();
1532
+ return unconfirmed;
1533
+ }
1018
1534
  }
1019
- const bootstrapped = await pullBootstrap(authorized, room);
1020
- if (!continuing())
1535
+ const bootstrapped = await pullBootstrap(authorized, room, local, live);
1536
+ if (!live())
1021
1537
  return unconfirmed;
1022
1538
  if (bootstrapped.kind !== 'staged')
1023
1539
  return bootstrapped;
1024
- await publish(local, room, bootstrapped.confirmed);
1540
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
1025
1541
  return null;
1026
1542
  }
1027
1543
  async function withhold(withheld, room, local, progress) {
1028
- progress.complete = false;
1544
+ if (withheld.kind === 'unconfirmed')
1545
+ return;
1546
+ if (withheld.kind === 'refused')
1547
+ stale.delete(room.locator);
1548
+ else
1549
+ stale.add(room.locator);
1550
+ if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
1551
+ const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
1552
+ repairable.set(room.locator, reset);
1553
+ repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
1554
+ }
1555
+ else
1556
+ repairable.delete(room.locator);
1029
1557
  if (withheld.kind === 'scoped-refusal') {
1030
1558
  await scoped(withheld, room, local);
1031
1559
  }
@@ -1050,6 +1578,7 @@ export function composeRuntime(options) {
1050
1578
  function refusedSource(room, { refusal }) {
1051
1579
  sourceRefusals.set(room.locator, {
1052
1580
  code: refusal.rule,
1581
+ retryable: false,
1053
1582
  fields: {
1054
1583
  file: refusal.file,
1055
1584
  ...(refusal.location === undefined ? {} : { location: refusal.location }),
@@ -1063,34 +1592,16 @@ export function composeRuntime(options) {
1063
1592
  journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
1064
1593
  }
1065
1594
  function leaving(batch, result) {
1066
- if (result.kind === 'expired') {
1067
- return { batch, outcome: { kind: 'expired' }, returned: { kind: 'expired' } };
1068
- }
1069
- const refusal = { code: result.code, fields: result.fields };
1070
- if (result.kind === 'conflicted') {
1071
- return {
1072
- batch,
1073
- outcome: {
1074
- kind: 'conflicted',
1075
- ...(result.roomVersion === undefined ? {} : { roomVersion: result.roomVersion }),
1076
- affected: result.affected,
1077
- ...refusal,
1078
- },
1079
- returned: { kind: 'conflicted', ...refusal },
1080
- };
1081
- }
1595
+ if (result.kind === 'expired')
1596
+ return { batch, returned: returnedBy('expired') };
1082
1597
  return {
1083
1598
  batch,
1084
- outcome: { kind: 'rejected', affected: result.affected, ...refusal },
1085
- returned: { kind: 'rejected', ...refusal },
1086
- };
1087
- }
1088
- function acceptedOf(accepted) {
1089
- return {
1090
- kind: 'accepted',
1091
- roomVersion: accepted.roomVersion,
1092
- ...(accepted.author === undefined ? {} : { author: accepted.author }),
1093
- ...(accepted.time === undefined ? {} : { time: accepted.time }),
1599
+ returned: {
1600
+ kind: result.kind,
1601
+ code: result.code,
1602
+ fields: result.fields,
1603
+ retryable: result.retryable,
1604
+ },
1094
1605
  };
1095
1606
  }
1096
1607
  function logAccepted(locator, submitted, accepted) {
@@ -1098,84 +1609,56 @@ export function composeRuntime(options) {
1098
1609
  (accepted.author === undefined ? '' : ` author ${accepted.author}`) +
1099
1610
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1100
1611
  }
1101
- async function recordAccepted(room, local, submitted, accepted) {
1102
- await exclusive(async () => {
1103
- if (!continuing())
1104
- return;
1105
- const current = replicas.get(room.locator);
1106
- if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1107
- return;
1108
- }
1109
- const outcome = acceptedOf(accepted);
1110
- await local.write({
1111
- next: current.confirmed.record,
1112
- previous: current.confirmed.record,
1113
- queue: current.queue,
1114
- previousQueue: current.queue,
1115
- outcomes: [{ id: submitted.id, outcome }],
1116
- });
1117
- outcomes.set(submitted.id, outcome);
1118
- outcomeRooms.set(submitted.id, room.locator);
1119
- logAccepted(room.locator, submitted, accepted);
1120
- });
1121
- }
1122
- async function commitAccept(room, local, submitted, accepted, base, overlay) {
1612
+ async function adoptAccepted(room, local, submitted, accepted) {
1613
+ await referenced(room.locator);
1123
1614
  if (!continuing())
1124
1615
  return unconfirmed;
1125
1616
  const current = replicas.get(room.locator);
1126
1617
  if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1127
1618
  return null;
1128
1619
  }
1129
- const remaining = current.queue.filter((batch) => batch.id !== submitted.id);
1130
- const retainedBase = readConfirmed({
1131
- ...base.record,
1132
- model: modelOf(room.locator),
1133
- selectionModel: modelOf(room.locator, 'typed-selection'),
1134
- }, base);
1135
- const applied = overlay && retainedBase.kind === 'staged'
1136
- ? applyReturned(retainedBase.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion)
1137
- : retainedBase;
1138
- if (applied.kind !== 'staged')
1620
+ const held = current.confirmed.record.roomVersion;
1621
+ logAccepted(room.locator, submitted, accepted);
1622
+ const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1623
+ ? { kind: 'staged', confirmed: current.confirmed }
1624
+ : applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
1625
+ ? accepted.roomVersion
1626
+ : held, accepted.roomVersion);
1627
+ if (applied.kind !== 'staged') {
1628
+ const state = pendings.get(submitted.id);
1629
+ await local.accept(room.locator, submitted.id, accepted.roomVersion);
1630
+ if (state !== undefined && !fences.has(room.locator))
1631
+ pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1632
+ emitQueue(room.locator);
1139
1633
  return applied;
1140
- const set = await settle(local, room.locator, current, applied.confirmed, remaining, {
1141
- accepted: { id: submitted.id, outcome: acceptedOf(accepted) },
1142
- room: true,
1143
- });
1634
+ }
1635
+ const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1636
+ pendings.delete(submitted.id);
1144
1637
  unreadable.delete(room.locator);
1145
1638
  sourceRefusals.delete(room.locator);
1146
1639
  declarationRefusals.delete(room.locator);
1640
+ const set = settled();
1147
1641
  list(room, true);
1148
1642
  publishChangeSet(set);
1149
1643
  moveTo({ ...status, ...attention() });
1150
1644
  return null;
1151
1645
  }
1152
- async function accepting(room, local, submitted, accepted, pulled, fresh) {
1153
- const roomVersion = pulled.record.roomVersion;
1154
- if (options.adapter.reached(room.locator, roomVersion, accepted.roomVersion)) {
1155
- return committed(room, local, submitted, accepted, pulled, roomVersion === accepted.roomVersion);
1156
- }
1157
- if (fresh && continuing())
1158
- await publish(local, room, pulled);
1159
- return false;
1160
- }
1161
- async function committed(room, local, submitted, accepted, base, overlay) {
1162
- const failed = await exclusive(() => commitAccept(room, local, submitted, accepted, base, overlay));
1646
+ async function adopted(room, local, submitted, accepted) {
1647
+ const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
1163
1648
  if (failed === null)
1164
1649
  return true;
1165
1650
  if (failed.kind === 'refused' && continuing())
1166
1651
  refusedSource(room, failed);
1167
1652
  return false;
1168
1653
  }
1169
- async function submitRoom(room, local, authorized, progress) {
1170
- while (continuing() && authorization !== undefined) {
1654
+ async function submitRoom(room, local, authorized, progress, signal) {
1655
+ while (continuing() && authorization !== undefined && !signal.aborted) {
1171
1656
  const replica = replicas.get(room.locator);
1172
1657
  const head = replica?.queue[0];
1173
1658
  if (replica === undefined || head === undefined)
1174
- return;
1175
- if (outcomes.get(head.id)?.kind === 'accepted') {
1176
- progress.complete = false;
1177
- return;
1178
- }
1659
+ return 'done';
1660
+ if (pendings.get(head.id)?.accepted !== undefined)
1661
+ return 'done';
1179
1662
  const state = pendings.get(head.id);
1180
1663
  const { submission, lost: earlier } = state ?? {
1181
1664
  submission: { state: 'waiting' },
@@ -1183,26 +1666,33 @@ export function composeRuntime(options) {
1183
1666
  };
1184
1667
  if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
1185
1668
  progress.complete = false;
1186
- return;
1669
+ return { at: Date.parse(submission.reset) };
1187
1670
  }
1188
1671
  if (writesHeld()) {
1189
1672
  progress.complete = false;
1190
1673
  journal.record(`submission ${room.locator} ${head.id} held`);
1191
- return;
1674
+ return 'done';
1192
1675
  }
1193
1676
  const lost = earlier || submission.state === 'submitted';
1677
+ if (lost && pastHorizon(head.id)) {
1678
+ await discardPending(room, local, signal);
1679
+ return 'done';
1680
+ }
1194
1681
  let result;
1195
1682
  journal.record(`submission ${room.locator} ${head.id}`);
1196
1683
  await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1197
- if (discoveryPaused)
1198
- return;
1199
- if (writesHeld()) {
1684
+ if (discoveryPaused || fences.has(room.locator))
1685
+ return 'done';
1686
+ if (signal.aborted || writesHeld()) {
1200
1687
  await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
1688
+ if (signal.aborted)
1689
+ return 'done';
1201
1690
  progress.complete = false;
1202
1691
  journal.record(`submission ${room.locator} ${head.id} held`);
1203
- return;
1692
+ return 'done';
1204
1693
  }
1205
1694
  const opensBefore = openSignals;
1695
+ const answered = awaitingAnswer(room.locator);
1206
1696
  try {
1207
1697
  result = await options.adapter.mutate(authorized, {
1208
1698
  locator: room.locator,
@@ -1211,33 +1701,40 @@ export function composeRuntime(options) {
1211
1701
  }, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
1212
1702
  }
1213
1703
  catch {
1704
+ answered();
1214
1705
  progress.complete = false;
1215
1706
  if (continuing())
1216
1707
  progress.reachable = false;
1217
- return;
1708
+ return 'again';
1709
+ }
1710
+ if (signal.aborted) {
1711
+ answered();
1712
+ return 'done';
1218
1713
  }
1714
+ if (result.kind !== 'accepted' || !continuing())
1715
+ answered();
1219
1716
  if (!continuing())
1220
- return;
1717
+ return 'done';
1221
1718
  if (result.kind === 'transport-uncertainty') {
1222
1719
  progress.complete = false;
1223
1720
  progress.reachable = false;
1224
- return;
1721
+ return 'again';
1225
1722
  }
1226
1723
  if (result.kind === 'room-not-readable') {
1227
1724
  progress.complete = false;
1228
1725
  unreadable.add(room.locator);
1229
1726
  list(room, false);
1230
1727
  emitRooms();
1231
- return;
1728
+ return 'done';
1232
1729
  }
1233
1730
  if (result.kind === 'revoked-authorization') {
1234
1731
  await refuse('revoked-authorization');
1235
- return;
1732
+ return 'done';
1236
1733
  }
1237
1734
  if (result.kind === 'scoped-refusal') {
1238
1735
  await scoped(result, room, local);
1239
1736
  progress.complete = false;
1240
- return;
1737
+ return 'done';
1241
1738
  }
1242
1739
  if (result.kind === 'write-halted') {
1243
1740
  journal.record(`outcome ${room.locator} ${head.id} write-halted`);
@@ -1246,16 +1743,24 @@ export function composeRuntime(options) {
1246
1743
  continue;
1247
1744
  haltedRefusal = true;
1248
1745
  progress.complete = false;
1249
- return;
1746
+ return 'done';
1250
1747
  }
1251
1748
  if (result.kind === 'conflicted' ||
1252
1749
  result.kind === 'rejected' ||
1253
1750
  result.kind === 'expired' ||
1254
1751
  result.kind === 'retryable-refusal') {
1752
+ if (result.kind === 'expired' && lost && result.restored !== true) {
1753
+ journal.record(`outcome ${room.locator} ${head.id} expired`);
1754
+ await discardPending(room, local, signal);
1755
+ return 'done';
1756
+ }
1255
1757
  if (result.kind === 'expired') {
1256
- const withheld = await advance(room, local, authorized);
1257
- if (withheld !== null)
1258
- return withhold(withheld, room, local, progress);
1758
+ await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
1759
+ const withheld = await advance(room, local, authorized, continuing);
1760
+ if (withheld !== null) {
1761
+ await withhold(withheld, room, local, progress);
1762
+ return 'done';
1763
+ }
1259
1764
  }
1260
1765
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
1261
1766
  (result.kind === 'conflicted' && result.roomVersion !== undefined
@@ -1268,7 +1773,7 @@ export function composeRuntime(options) {
1268
1773
  const reset = result.reset;
1269
1774
  progress.complete = false;
1270
1775
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1271
- return;
1776
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
1272
1777
  }
1273
1778
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
1274
1779
  const left = leaving(head, result);
@@ -1279,113 +1784,96 @@ export function composeRuntime(options) {
1279
1784
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
1280
1785
  return;
1281
1786
  }
1282
- const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1283
- leaving: result.kind === 'expired' && landed(head, current.confirmed)
1787
+ const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1788
+ leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1789
+ (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1284
1790
  ? { ...left, returned: null }
1285
1791
  : left,
1286
1792
  });
1287
- publishChangeSet(set);
1793
+ publishChangeSet(settled());
1288
1794
  moveTo({ ...status, ...attention() });
1289
1795
  });
1290
1796
  if (result.kind === 'expired')
1291
1797
  continue;
1292
- const withheld = await advance(room, local, authorized);
1293
- if (withheld !== null)
1294
- return withhold(withheld, room, local, progress);
1295
- continue;
1296
- }
1297
- await recordAccepted(room, local, head, result);
1298
- const from = replicas.get(room.locator)?.confirmed;
1299
- if (from === undefined)
1300
- return;
1301
- const pulled = await pullRefresh(authorized, from, reachOf(room.locator));
1302
- if (!continuing())
1303
- return;
1304
- if (pulled.kind === 'revoked-authorization') {
1305
- await refuse('revoked-authorization');
1306
- return;
1307
- }
1308
- if (pulled.kind === 'scoped-refusal') {
1309
- await scoped(pulled, room, local);
1310
- progress.complete = false;
1311
- return;
1312
- }
1313
- if (pulled.kind === 'still-current' || pulled.kind === 'advanced') {
1314
- const fresh = pulled.kind === 'advanced';
1315
- if (!(await accepting(room, local, head, result, fresh ? pulled.confirmed : from, fresh))) {
1316
- progress.complete = false;
1317
- return;
1798
+ const withheld = await advance(room, local, authorized, continuing);
1799
+ if (withheld !== null) {
1800
+ await withhold(withheld, room, local, progress);
1801
+ return 'done';
1318
1802
  }
1319
1803
  continue;
1320
1804
  }
1321
- if (pulled.kind === 'beyond-horizon') {
1322
- const bootstrapped = await pullBootstrap(authorized, room);
1323
- if (!continuing())
1324
- return;
1325
- if (bootstrapped.kind === 'revoked-authorization') {
1326
- await refuse('revoked-authorization');
1327
- return;
1328
- }
1329
- if (bootstrapped.kind === 'scoped-refusal') {
1330
- await scoped(bootstrapped, room, local);
1331
- progress.complete = false;
1332
- return;
1333
- }
1334
- if (bootstrapped.kind === 'staged') {
1335
- if (!(await accepting(room, local, head, result, bootstrapped.confirmed, true))) {
1336
- progress.complete = false;
1337
- return;
1338
- }
1339
- continue;
1340
- }
1805
+ const kept = await adopted(room, local, head, result).finally(answered);
1806
+ if (!kept) {
1341
1807
  progress.complete = false;
1342
- if (bootstrapped.kind === 'transport-uncertainty') {
1343
- progress.reachable = false;
1344
- }
1345
- else if (bootstrapped.kind === 'room-not-readable') {
1346
- unreadable.add(room.locator);
1347
- list(room, false);
1348
- emitRooms();
1349
- }
1350
- else if (bootstrapped.kind === 'refused') {
1351
- refusedSource(room, bootstrapped);
1352
- }
1353
- return;
1354
- }
1355
- progress.complete = false;
1356
- if (pulled.kind === 'transport-uncertainty') {
1357
- progress.reachable = false;
1808
+ return 'done';
1358
1809
  }
1359
- else if (pulled.kind === 'room-not-readable') {
1360
- unreadable.add(room.locator);
1361
- list(room, false);
1362
- emitRooms();
1363
- }
1364
- else if (pulled.kind === 'retryable-refusal') {
1365
- journal.record(`refusal ${pulled.kind} ${pulled.code}`);
1810
+ }
1811
+ return 'done';
1812
+ }
1813
+ function pastHorizon(identity) {
1814
+ return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
1815
+ }
1816
+ async function discardPending(room, local, signal) {
1817
+ const { locator } = room;
1818
+ const discarded = await exclusive(async () => {
1819
+ if (fences.has(locator))
1820
+ return false;
1821
+ const before = replicas.get(locator);
1822
+ const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
1823
+ fences.set(locator, { at: discoveries, erased: false });
1824
+ lastFenced.set(locator, ++fencings);
1825
+ try {
1826
+ await local.discard(locator);
1366
1827
  }
1367
- else if (pulled.kind === 'refused') {
1368
- refusedSource(room, pulled);
1828
+ finally {
1829
+ fences.delete(locator);
1369
1830
  }
1831
+ for (const batch of queue)
1832
+ pendings.delete(batch.id);
1833
+ queuesWithoutReplica.delete(locator);
1834
+ const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
1835
+ if (after !== null)
1836
+ replicas.set(locator, after);
1837
+ journal.record(`room discarded ${locator}`);
1838
+ publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
1839
+ moveTo({ ...status, ...attention() });
1840
+ return true;
1841
+ });
1842
+ if (!discarded)
1370
1843
  return;
1371
- }
1372
- return;
1844
+ const progress = { complete: true, reachable: true };
1845
+ await roomTask(locator, async (live) => {
1846
+ await Promise.race([
1847
+ reload(room, local, progress, () => live() && !signal.aborted),
1848
+ abandonedOn(signal),
1849
+ ]);
1850
+ });
1851
+ if (signal.aborted)
1852
+ return;
1853
+ await rest(progress);
1854
+ }
1855
+ function awaitingAnswer(locator) {
1856
+ let release = () => { };
1857
+ const awaited = new Promise((resolve) => {
1858
+ release = resolve;
1859
+ });
1860
+ answering.set(locator, { generation, answered: awaited });
1861
+ return () => {
1862
+ if (answering.get(locator)?.answered === awaited)
1863
+ answering.delete(locator);
1864
+ release();
1865
+ };
1373
1866
  }
1374
1867
  async function retainModel(local, room, attached, next) {
1375
1868
  const key = modelKey(room.locator, attached);
1376
- if (next.status === 'unavailable')
1377
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
1378
- const epoch = modelEpoch.get(key) ?? 0;
1379
1869
  const previous = modelOf(room.locator, attached);
1380
1870
  const heldBefore = replicas.get(room.locator)?.confirmed.record;
1381
1871
  if (JSON.stringify(previous) === JSON.stringify(next) &&
1382
1872
  (heldBefore === undefined ||
1383
1873
  (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
1384
1874
  JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
1385
- JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
1386
1875
  JSON.stringify(heldBefore.typedSelection) ===
1387
- JSON.stringify(room.typedSelection ?? null) &&
1388
- JSON.stringify(heldBefore.untypedFolders) === JSON.stringify(room.untypedFolders ?? []))))
1876
+ JSON.stringify(room.typedSelection ?? null))))
1389
1877
  return;
1390
1878
  if (next.status === 'unavailable') {
1391
1879
  invalidModels.add(key);
@@ -1394,37 +1882,30 @@ export function composeRuntime(options) {
1394
1882
  await exclusive(async () => {
1395
1883
  if (!continuing())
1396
1884
  return;
1397
- if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
1398
- return;
1399
1885
  const held = replicas.get(room.locator);
1400
1886
  if (held !== undefined) {
1401
1887
  const declared = {
1402
1888
  ...held.confirmed.record,
1403
1889
  operations: room.operations,
1404
1890
  replica: room.replica,
1405
- attachedModel: room.attachedModel,
1406
1891
  typedSelection: room.typedSelection ?? null,
1407
- untypedFolders: room.untypedFolders ?? [],
1408
1892
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1409
1893
  };
1410
1894
  const reclassified = sameMembership(declared, held.confirmed.record)
1411
- ? readConfirmed(declared, held.confirmed)
1895
+ ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
1412
1896
  : readReclassified(declared, held.confirmed);
1413
1897
  const record = reclassified.kind === 'staged'
1414
1898
  ? declared
1415
1899
  : {
1416
1900
  ...declared,
1417
1901
  typedSelection: held.confirmed.record.typedSelection,
1418
- untypedFolders: held.confirmed.record.untypedFolders,
1419
1902
  };
1420
- const read = reclassified.kind === 'deferred' ? readConfirmed(record, held.confirmed) : reclassified;
1421
- await local.write({
1422
- next: record,
1423
- previous: held.confirmed.record,
1424
- queue: held.queue,
1425
- });
1903
+ const read = reclassified.kind === 'deferred'
1904
+ ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
1905
+ : reclassified;
1906
+ await local.write({ next: record, previous: held.confirmed.record });
1426
1907
  if (read.kind === 'staged') {
1427
- replicas.set(room.locator, replicaOf(read.confirmed, held.queue, rewriting(), held));
1908
+ replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1428
1909
  if (declarationRefusals.delete(room.locator)) {
1429
1910
  sourceRefusals.delete(room.locator);
1430
1911
  unreadable.delete(room.locator);
@@ -1448,79 +1929,12 @@ export function composeRuntime(options) {
1448
1929
  publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
1449
1930
  });
1450
1931
  }
1451
- async function acquireModel(local, room, authorized, selection) {
1452
- const attached = selection === null ? 'collection' : 'typed-selection';
1453
- const key = modelKey(room.locator, attached);
1454
- const declared = selection === null ? room.attachedModel : selection.attachedModel;
1455
- let previous = modelOf(room.locator, attached);
1456
- if (previous.status !== 'unavailable' &&
1457
- (declared === null || !sameModel(previous.snapshot, declared))) {
1458
- previous = { status: 'unavailable', reason: 'version-changed' };
1459
- await retainModel(local, room, attached, previous);
1460
- if (discoveryPaused)
1461
- return;
1462
- }
1463
- const epoch = modelEpoch.get(key) ?? 0;
1464
- let answer;
1465
- try {
1466
- answer = await options.adapter.model(authorized, selection === null
1467
- ? { locator: room.locator, attached: 'collection', model: declared }
1468
- : {
1469
- locator: room.locator,
1470
- attached: 'typed-selection',
1471
- roots: selection.roots,
1472
- model: declared,
1473
- });
1474
- }
1475
- catch {
1476
- answer = { kind: 'transport-uncertainty' };
1477
- }
1478
- if (discoveryPaused || epoch !== (modelEpoch.get(key) ?? 0))
1479
- return;
1480
- if (answer.kind === 'revoked-authorization') {
1481
- await refuse('revoked-authorization');
1482
- return;
1483
- }
1484
- if (answer.kind === 'transport-uncertainty') {
1485
- await retainModel(local, room, attached, cached(previous));
1486
- return;
1487
- }
1488
- if (answer.kind === 'model-unavailable') {
1489
- await retainModel(local, room, attached, {
1490
- status: 'unavailable',
1491
- reason: answer.reason,
1492
- });
1493
- return;
1494
- }
1495
- if (declared === null || !sameModel(answer.snapshot, declared)) {
1496
- await retainModel(local, room, attached, {
1497
- status: 'unavailable',
1498
- reason: 'version-changed',
1499
- });
1500
- return;
1501
- }
1502
- if (previous.status !== 'unavailable' &&
1503
- sameModel(previous.snapshot, answer.snapshot) &&
1504
- previous.snapshot.schema !== answer.snapshot.schema) {
1505
- await retainModel(local, room, attached, { status: 'unavailable', reason: 'invalid' });
1506
- return;
1507
- }
1508
- const verified = answer.snapshot.schema === null ? EMPTY_SCHEMA : verify(answer.snapshot.schema);
1509
- if (verified.verdict === 'refuse') {
1510
- const where = verified.location === undefined ? '' : ` ${verified.location}`;
1511
- journal.record(`model invalid ${room.locator} ${attached} ${answer.snapshot.identity} ${verified.rule}${where}`);
1512
- }
1513
- await retainModel(local, room, attached, verified.verdict === 'accept'
1514
- ? {
1515
- status: 'current',
1516
- snapshot: structuredClone(answer.snapshot),
1517
- verifiedWith: MD_MODEL,
1518
- types: verified.model.types,
1519
- }
1520
- : { status: 'unavailable', reason: 'invalid' });
1521
- }
1522
1932
  async function scoped(result, room, local) {
1523
- scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
1933
+ scopedRefusals.set(room.locator, {
1934
+ code: result.code,
1935
+ fields: result.fields,
1936
+ retryable: result.retryable,
1937
+ });
1524
1938
  let scopedAway = room;
1525
1939
  if (result.scope === 'model')
1526
1940
  await retainModel(local, room, 'collection', {
@@ -1546,14 +1960,38 @@ export function composeRuntime(options) {
1546
1960
  emitRooms();
1547
1961
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
1548
1962
  }
1549
- async function synchronize(discovered, local, progress) {
1550
- const prepared = await Promise.allSettled(discovered.map((room) => prepareRoom(room, local, progress)));
1551
- const ready = discovered.filter((_, index) => {
1963
+ function heldRooms(listed) {
1964
+ return listed.flatMap((room) => {
1965
+ const record = replicas.get(room.locator)?.confirmed.record;
1966
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1967
+ return [];
1968
+ return [
1969
+ {
1970
+ locator: room.locator,
1971
+ roomVersion: record.roomVersion,
1972
+ documents: record.documents,
1973
+ revisions: record.revisions,
1974
+ },
1975
+ ];
1976
+ });
1977
+ }
1978
+ async function synchronize(listed, local, progress, attempt, live) {
1979
+ const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1980
+ if (!live())
1981
+ return;
1982
+ const ready = listed.filter((_, index) => {
1552
1983
  const outcome = prepared[index];
1553
1984
  return outcome?.status === 'fulfilled' && outcome.value;
1554
1985
  });
1555
- const rooms = await Promise.allSettled(ready.map((room) => synchronizeRoom(room, local, progress)));
1556
- const failed = [...prepared, ...rooms].find((room) => room.status === 'rejected');
1986
+ if (attempt === 'opened') {
1987
+ options.adapter.roomStream?.hold(heldRooms(ready));
1988
+ handed = true;
1989
+ }
1990
+ discovering?.release();
1991
+ const loaded = await Promise.allSettled(ready
1992
+ .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1993
+ .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
1994
+ const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1557
1995
  if (failed !== undefined)
1558
1996
  throw failed.reason;
1559
1997
  }
@@ -1570,11 +2008,8 @@ export function composeRuntime(options) {
1570
2008
  progress.complete = false;
1571
2009
  return false;
1572
2010
  }
1573
- const selection = room.typedSelection;
1574
- await Promise.all([
1575
- acquireModel(local, room, authorization, null),
1576
- ...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
1577
- ]);
2011
+ if (prior !== undefined)
2012
+ await retainModel(local, room, 'collection', modelOf(room.locator));
1578
2013
  if (discoveryPaused || authorization === undefined)
1579
2014
  return false;
1580
2015
  if (room.replica.admission !== 'admitted') {
@@ -1582,29 +2017,65 @@ export function composeRuntime(options) {
1582
2017
  progress.complete = false;
1583
2018
  return false;
1584
2019
  }
1585
- const held = replicas.get(room.locator);
1586
- if (held !== undefined && held.confirmed.record.replica.selection !== room.replica.selection) {
1587
- progress.complete = false;
1588
- return false;
1589
- }
1590
2020
  return true;
1591
2021
  }
1592
- async function synchronizeRoom(room, local, progress) {
1593
- if (discoveryPaused || authorization === undefined)
2022
+ async function caughtUp(room, local, progress, live) {
2023
+ const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
2024
+ if (held === undefined || authorization === undefined || !live())
2025
+ return;
2026
+ const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
2027
+ if (!behind)
2028
+ return;
2029
+ const withheld = await advance(room, local, authorization, live);
2030
+ if (withheld !== null)
2031
+ await withhold(withheld, room, local, progress);
2032
+ }
2033
+ function revoked(locator) {
2034
+ return ['collection', 'typed-selection'].some((attached) => {
2035
+ const model = modelOf(locator, attached);
2036
+ return model.status === 'unavailable' && model.reason === 'revoked';
2037
+ });
2038
+ }
2039
+ async function loadRoom(room, local, progress, live) {
2040
+ if (!live() || authorization === undefined)
2041
+ return;
2042
+ if (replicas.has(room.locator) && revoked(room.locator)) {
2043
+ await reload(room, local, progress, live);
1594
2044
  return;
1595
- const withheld = await advance(room, local, authorization);
2045
+ }
2046
+ const withheld = await advance(room, local, authorization, live);
1596
2047
  if (withheld !== null) {
1597
2048
  await withhold(withheld, room, local, progress);
1598
2049
  return;
1599
2050
  }
1600
- await submitRoom(room, local, authorization, progress);
2051
+ await caughtUp(room, local, progress, live);
2052
+ }
2053
+ async function reload(room, local, progress, live) {
2054
+ try {
2055
+ if (!live() || authorization === undefined)
2056
+ return;
2057
+ const bootstrapped = await pullBootstrap(authorization, room, local, live);
2058
+ if (!live())
2059
+ return;
2060
+ if (bootstrapped.kind !== 'staged') {
2061
+ await withhold(bootstrapped, room, local, progress);
2062
+ return;
2063
+ }
2064
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
2065
+ await caughtUp(room, local, progress, live);
2066
+ }
2067
+ finally {
2068
+ caughtUpTo(room.locator);
2069
+ }
1601
2070
  }
1602
- async function cycle(local, progress) {
2071
+ async function discoverRooms(local, progress, attempt, live) {
1603
2072
  if (closed || authorization === undefined) {
1604
2073
  notifyReady();
1605
2074
  return;
1606
2075
  }
1607
2076
  moveTo(transferring(status));
2077
+ discoveries += 1;
2078
+ const started = discoveries;
1608
2079
  let result;
1609
2080
  try {
1610
2081
  result = await options.adapter.discover(authorization);
@@ -1612,7 +2083,8 @@ export function composeRuntime(options) {
1612
2083
  catch {
1613
2084
  result = { kind: 'transport-uncertainty' };
1614
2085
  }
1615
- if (closed) {
2086
+ if (!live()) {
2087
+ progress.complete = false;
1616
2088
  notifyReady();
1617
2089
  return;
1618
2090
  }
@@ -1625,12 +2097,17 @@ export function composeRuntime(options) {
1625
2097
  }
1626
2098
  }
1627
2099
  progress.reachable = false;
2100
+ progress.complete = false;
1628
2101
  notifyReady();
1629
2102
  return;
1630
2103
  }
1631
2104
  if (result.kind === 'contract-unavailable') {
1632
2105
  journal.record(`connection contract unavailable ${result.code}`);
1633
- await refuse('missing-required-guarantee', { code: result.code, fields: result.fields });
2106
+ await refuse('missing-required-guarantee', {
2107
+ code: result.code,
2108
+ fields: result.fields,
2109
+ retryable: result.retryable,
2110
+ });
1634
2111
  notifyReady();
1635
2112
  return;
1636
2113
  }
@@ -1639,17 +2116,6 @@ export function composeRuntime(options) {
1639
2116
  notifyReady();
1640
2117
  return;
1641
2118
  }
1642
- if (!result.supportedVersions.includes(ROOM_CONTRACT_VERSION)) {
1643
- await refuse('no-matching-version');
1644
- notifyReady();
1645
- return;
1646
- }
1647
- if (result.mdModel.identity !== MD_MODEL.identity ||
1648
- result.mdModel.version !== MD_MODEL.version) {
1649
- await refuse('md-model-version-mismatch');
1650
- notifyReady();
1651
- return;
1652
- }
1653
2119
  for (const room of result.rooms) {
1654
2120
  if (room.operations === undefined ||
1655
2121
  !['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
@@ -1657,8 +2123,7 @@ export function composeRuntime(options) {
1657
2123
  room.replica === undefined ||
1658
2124
  typeof room.replica.selection !== 'string' ||
1659
2125
  room.replica.selection.length === 0 ||
1660
- !['admitted', 'unavailable'].includes(room.replica.admission) ||
1661
- !declaredIdentity(room.attachedModel)) {
2126
+ !['admitted', 'unavailable'].includes(room.replica.admission)) {
1662
2127
  journal.record(`invalid model or replica declaration ${room.locator}`);
1663
2128
  await refuse('missing-required-guarantee');
1664
2129
  notifyReady();
@@ -1670,20 +2135,6 @@ export function composeRuntime(options) {
1670
2135
  notifyReady();
1671
2136
  return;
1672
2137
  }
1673
- if (!declaredUntypedFolders(room)) {
1674
- journal.record(`invalid untyped folder declaration ${room.locator}`);
1675
- await refuse('missing-required-guarantee');
1676
- notifyReady();
1677
- return;
1678
- }
1679
- }
1680
- const declared = new Set(result.capabilities.map((capability) => capability.name));
1681
- for (const required of options.requiredGuarantees) {
1682
- if (!knownCapabilities.has(required) || !declared.has(required)) {
1683
- await refuse('missing-required-guarantee');
1684
- notifyReady();
1685
- return;
1686
- }
1687
2138
  }
1688
2139
  if (refusal !== null) {
1689
2140
  refusal = null;
@@ -1691,30 +2142,20 @@ export function composeRuntime(options) {
1691
2142
  }
1692
2143
  const previousCapabilities = capabilities;
1693
2144
  capabilities = result.capabilities;
2145
+ outcomeRetention = result.retention.outcome;
1694
2146
  notifyReady();
1695
- if (declared.has('room-version-notifications'))
1696
- observeRoomVersions();
1697
- const discoveredSlots = result.slots ?? [];
1698
2147
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1699
- JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities) ||
1700
- JSON.stringify(discoveredSlots) !== JSON.stringify(slots)) {
2148
+ JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
1701
2149
  limits = result.providerLimits;
1702
- slots = discoveredSlots;
1703
- await local.declare(limits, capabilities, slots);
1704
- }
1705
- await applyDiscovery(result.rooms, local);
1706
- handSelected(result.rooms
1707
- .filter((room) => room.replica.admission === 'admitted')
1708
- .map((room) => room.locator)
1709
- .sort());
1710
- progress.complete = true;
1711
- await synchronize(result.rooms, local, progress);
1712
- if (closed || discoveryPaused)
1713
- return;
1714
- await conversations.reconcile();
1715
- if (status.providerUnreachable)
1716
- conversations.reconnected();
1717
- await conversations.catchUp(progress);
2150
+ await local.declare(limits, capabilities);
2151
+ }
2152
+ const signedIn = result.member ?? null;
2153
+ if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
2154
+ await local.keepMember(signedIn);
2155
+ member = signedIn;
2156
+ emitMember();
2157
+ }
2158
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
1718
2159
  }
1719
2160
  async function ensureAuthorization() {
1720
2161
  if (authorization !== undefined)
@@ -1728,66 +2169,413 @@ export function composeRuntime(options) {
1728
2169
  throw error;
1729
2170
  }
1730
2171
  }
1731
- async function connect() {
1732
- if (closed || stopping || discoveryPaused) {
1733
- notifyReady();
1734
- return;
2172
+ async function admittedRead(work) {
2173
+ if (roomReads.generation !== generation) {
2174
+ roomReads = { generation, free: ROOM_READS, waiting: [] };
1735
2175
  }
1736
- startTimer();
1737
- const progress = { complete: false, reachable: true };
1738
- let completedAt = new Date().toISOString();
2176
+ const reads = roomReads;
2177
+ while (reads.free === 0)
2178
+ await new Promise((admit) => reads.waiting.push(admit));
2179
+ reads.free -= 1;
1739
2180
  try {
1740
- const local = await openLocal();
1741
- if (closed)
2181
+ await work();
2182
+ }
2183
+ finally {
2184
+ reads.free += 1;
2185
+ reads.waiting.shift()?.();
2186
+ }
2187
+ }
2188
+ function roomTask(locator, work) {
2189
+ return task(roomLane(locator), (live) => admittedRead(() => work(live)));
2190
+ }
2191
+ function task(lane, work) {
2192
+ if (stopping)
2193
+ return Promise.resolve();
2194
+ const held = lanes.get(lane);
2195
+ const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
2196
+ const turn = current.generation;
2197
+ const live = () => turn === generation && continuing();
2198
+ const run = current.chain.then(() => work(live)).catch(() => undefined);
2199
+ current.chain = run;
2200
+ lanes.set(lane, current);
2201
+ working.add(run);
2202
+ void run.finally(() => working.delete(run));
2203
+ return run;
2204
+ }
2205
+ function sync(attempt) {
2206
+ return task(OPENING_LANE, async (live) => {
2207
+ if (!live()) {
2208
+ notifyReady();
1742
2209
  return;
1743
- await ensureAuthorization();
1744
- if (closed)
2210
+ }
2211
+ const progress = { complete: true, reachable: true };
2212
+ const gate = discover();
2213
+ if (attempt === 'opened')
2214
+ opened = generation;
2215
+ underway = attempt === 'opened' ? { generation, progress } : null;
2216
+ if (attempt === 'opened')
2217
+ streamed = progress;
2218
+ complete = false;
2219
+ recorded = false;
2220
+ moveTo(transferring(status));
2221
+ handed = false;
2222
+ try {
2223
+ const local = await openLocal();
2224
+ if (!live())
2225
+ return;
2226
+ await ensureAuthorization();
2227
+ if (!live())
2228
+ return;
2229
+ await discoverRooms(local, progress, attempt, live);
2230
+ }
2231
+ catch {
2232
+ progress.complete = false;
2233
+ if (attempt === 'opened' && !handed)
2234
+ progress.reachable = false;
2235
+ }
2236
+ finally {
2237
+ gate.release();
2238
+ notifyReady();
2239
+ }
2240
+ if (!live())
1745
2241
  return;
1746
- await cycle(local, progress);
1747
- if (closed)
2242
+ if (attempt !== 'opened') {
2243
+ await finishOpening(progress, live);
1748
2244
  return;
1749
- if (progress.complete) {
1750
- completedAt = new Date().toISOString();
1751
- await local.complete(completedAt);
1752
2245
  }
2246
+ if (handed || closed || progress.reachable)
2247
+ return;
2248
+ underway = null;
2249
+ opened = null;
2250
+ journal.record('connection reopen');
2251
+ await rest(progress, live);
2252
+ options.adapter.roomStream?.reopen();
2253
+ });
2254
+ }
2255
+ function closedSocket(revoked) {
2256
+ const opened = socketOpen;
2257
+ socketOpen = false;
2258
+ if (revoked) {
2259
+ void refuse('revoked-authorization');
2260
+ return;
2261
+ }
2262
+ complete = false;
2263
+ if (streamed !== null)
2264
+ streamed.complete = false;
2265
+ moveTo({
2266
+ ...status,
2267
+ state: status.state === 'synced' ? 'syncing' : status.state,
2268
+ providerUnreachable: status.providerUnreachable || !opened,
2269
+ });
2270
+ }
2271
+ function applyOpening(expired) {
2272
+ const progress = underway?.generation === generation ? underway.progress : null;
2273
+ const cycle = progress ?? { complete: true, reachable: true };
2274
+ const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
2275
+ if (opened === generation)
2276
+ opened = null;
2277
+ return task(OPENING_LANE, async (live) => {
2278
+ underway = null;
2279
+ if (!live())
2280
+ return;
2281
+ if (progress === null) {
2282
+ await replacing;
2283
+ await rest(cycle, live);
2284
+ return;
2285
+ }
2286
+ await roomsCarried(expired);
2287
+ if (!live())
2288
+ return;
2289
+ const replaced = new Set(owed);
2290
+ owed.clear();
2291
+ try {
2292
+ const local = await openLocal();
2293
+ const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
2294
+ const reloading = held.filter((room) => !expired.includes(room.locator) &&
2295
+ (replaced.has(room.locator) || revoked(room.locator)));
2296
+ for (const room of reloading)
2297
+ catching.add(room.locator);
2298
+ readSide.recover();
2299
+ const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
2300
+ const reloaded = [...(await reloads), ...(await replacing)];
2301
+ if (progress.reachable) {
2302
+ for (const room of held) {
2303
+ if (!expired.includes(room.locator) && !reloading.includes(room)) {
2304
+ await confirmModels(room, local);
2305
+ }
2306
+ }
2307
+ }
2308
+ const failed = reloaded.find((outcome) => outcome.status === 'rejected');
2309
+ if (failed !== undefined)
2310
+ throw failed.reason;
2311
+ }
2312
+ catch {
2313
+ progress.complete = false;
2314
+ progress.reachable = false;
2315
+ }
2316
+ if (live())
2317
+ await finishOpening(progress, live);
2318
+ });
2319
+ }
2320
+ async function reloadNow(replaced, progress) {
2321
+ try {
2322
+ const local = await openLocal();
2323
+ return await Promise.allSettled([...discovered.values()]
2324
+ .filter((room) => replicas.has(room.locator) && replaced(room))
2325
+ .map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
1753
2326
  }
1754
2327
  catch {
1755
- progress.complete = false;
1756
- notifyReady();
2328
+ progress.reachable = false;
2329
+ return [];
2330
+ }
2331
+ }
2332
+ async function confirmModels(room, local) {
2333
+ for (const attached of ['collection', 'typed-selection']) {
2334
+ const model = modelOf(room.locator, attached);
2335
+ if (model.status === 'cached') {
2336
+ await retainModel(local, room, attached, { ...model, status: 'current' });
2337
+ }
2338
+ }
2339
+ }
2340
+ function submitLater(locator) {
2341
+ if (localSaves)
1757
2342
  return;
2343
+ void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
2344
+ }
2345
+ async function submitPass(locator, signal) {
2346
+ const room = discovered.get(locator);
2347
+ if (closed || stopping || discoveryPaused || room === undefined)
2348
+ return 'done';
2349
+ const progress = { complete: true, reachable: true };
2350
+ let next;
2351
+ try {
2352
+ await ensureAuthorization();
2353
+ if (authorization === undefined)
2354
+ return 'done';
2355
+ next = await submitRoom(room, await openLocal(), authorization, progress, signal);
2356
+ }
2357
+ catch {
2358
+ progress.reachable = false;
2359
+ next = 'again';
2360
+ }
2361
+ if (signal.aborted)
2362
+ return 'done';
2363
+ await rest(progress);
2364
+ return next === 'again' && !socketOpen ? 'done' : next;
2365
+ }
2366
+ function repairLater(locator, when) {
2367
+ if (!socketOpen || repairing.has(locator))
2368
+ return;
2369
+ void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
2370
+ }
2371
+ async function repairPass(locator, signal) {
2372
+ const room = discovered.get(locator);
2373
+ if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
2374
+ return 'done';
2375
+ if (room === undefined || room.replica.admission !== 'admitted')
2376
+ return 'done';
2377
+ const progress = { complete: true, reachable: true };
2378
+ repairing.add(locator);
2379
+ try {
2380
+ const local = await openLocal();
2381
+ await roomTask(locator, async (live) => {
2382
+ const current = () => live() && !signal.aborted;
2383
+ if (!current() || authorization === undefined)
2384
+ return;
2385
+ const withheld = await Promise.race([
2386
+ advance(room, local, authorization, current),
2387
+ abandonedOn(signal),
2388
+ ]);
2389
+ if (withheld !== null && current())
2390
+ await withhold(withheld, room, local, progress);
2391
+ });
2392
+ }
2393
+ catch {
2394
+ stale.add(locator);
2395
+ progress.reachable = false;
1758
2396
  }
1759
2397
  finally {
1760
- notifyReady();
1761
- moveTo(settled(status, progress, completedAt, pending(), attention()));
2398
+ repairing.delete(locator);
2399
+ }
2400
+ if (signal.aborted)
2401
+ return 'done';
2402
+ await rest(progress);
2403
+ if (!stale.has(locator)) {
2404
+ if ((replicas.get(locator)?.queue.length ?? 0) > 0)
2405
+ submitLater(locator);
2406
+ return 'done';
2407
+ }
2408
+ if (!repairable.has(locator))
2409
+ return 'done';
2410
+ const reset = repairable.get(locator);
2411
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
2412
+ }
2413
+ function resendLater() {
2414
+ if (!socketOpen)
2415
+ return;
2416
+ void recovery.attempt('resend', resendPass, { again: true });
2417
+ }
2418
+ async function resendPass(signal) {
2419
+ if (!socketOpen || closed || stopping || discoveryPaused)
2420
+ return 'done';
2421
+ const reading = { complete: true, reachable: true };
2422
+ try {
2423
+ await conversations.resend(reading);
2424
+ }
2425
+ catch {
2426
+ reading.reachable = false;
2427
+ }
2428
+ if (signal.aborted)
2429
+ return 'done';
2430
+ return reading.reachable && reading.complete ? 'done' : 'again';
2431
+ }
2432
+ function submitAll() {
2433
+ for (const [locator, replica] of replicas) {
2434
+ if (replica.queue.length > 0)
2435
+ submitLater(locator);
1762
2436
  }
1763
2437
  }
1764
- async function run() {
2438
+ async function finishOpening(progress, live) {
1765
2439
  try {
1766
- await connect();
2440
+ if (!live()) {
2441
+ progress.complete = false;
2442
+ return;
2443
+ }
2444
+ submitAll();
2445
+ for (const locator of stale)
2446
+ if (repairable.has(locator))
2447
+ repairLater(locator, {});
2448
+ await conversations.reconcile();
2449
+ if (status.providerUnreachable)
2450
+ conversations.reconnected();
2451
+ const reading = { complete: true, reachable: true };
2452
+ await conversations.catchUp(reading);
2453
+ if (progress.reachable && reading.reachable)
2454
+ await conversations.resend(reading);
2455
+ if (!reading.reachable) {
2456
+ progress.reachable = false;
2457
+ resendLater();
2458
+ }
2459
+ }
2460
+ catch {
2461
+ progress.complete = false;
1767
2462
  }
1768
2463
  finally {
1769
- inflight = false;
1770
- if (queued && !closed) {
1771
- queued = false;
1772
- void enqueue();
2464
+ if (live()) {
2465
+ complete = progress.complete;
2466
+ await rest(progress, live);
1773
2467
  }
1774
2468
  }
1775
2469
  }
1776
- function observeRoomVersions() {
1777
- const path = options.adapter.roomVersions;
1778
- if (path === undefined || withdrawNotifications !== undefined)
2470
+ async function rest(progress, live = () => !closed) {
2471
+ if (!live())
1779
2472
  return;
1780
- withdrawNotifications = path.observe((notification) => {
1781
- if (closed || stopping || discoveryPaused)
2473
+ let lastCompleted = status.lastCompleted;
2474
+ let synced = complete && stale.size === 0 && !pending();
2475
+ if (synced && !recorded) {
2476
+ const completedAt = new Date().toISOString();
2477
+ try {
2478
+ await (await openLocal()).complete(completedAt);
2479
+ if (!live())
2480
+ return;
2481
+ recorded = true;
2482
+ lastCompleted = completedAt;
2483
+ }
2484
+ catch {
2485
+ synced = false;
2486
+ }
2487
+ }
2488
+ moveTo({
2489
+ state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
2490
+ lastCompleted,
2491
+ providerUnreachable: !progress.reachable,
2492
+ ...attention(),
2493
+ });
2494
+ }
2495
+ function discover() {
2496
+ let release = () => { };
2497
+ const done = new Promise((resolve) => {
2498
+ release = resolve;
2499
+ });
2500
+ discovering = { generation, done, release };
2501
+ return discovering;
2502
+ }
2503
+ function applyRoom(locator, roomVersion) {
2504
+ announced.set(locator, roomVersion);
2505
+ return task(roomLane(locator), async (live) => {
2506
+ if (discovering?.generation === generation)
2507
+ await discovering.done;
2508
+ const saving = answering.get(locator);
2509
+ if (saving?.generation === generation)
2510
+ await saving.answered;
2511
+ if (!live() || authorization === undefined)
1782
2512
  return;
1783
- if (notification.modelChanged === true && handed?.includes(notification.locator) === true) {
1784
- void enqueue();
2513
+ const room = discovered.get(locator);
2514
+ const held = replicas.get(locator);
2515
+ if (room === undefined || room.replica.admission !== 'admitted')
2516
+ return;
2517
+ if (held !== undefined &&
2518
+ (held.confirmed.record.replica.selection !== room.replica.selection ||
2519
+ options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
1785
2520
  return;
1786
2521
  }
1787
- notices.set(notification.locator, notification.roomVersion);
1788
- chain = chain.then(runNotified, runNotified);
2522
+ const during = underway?.generation === generation ? underway.progress : null;
2523
+ const progress = during ?? { complete: true, reachable: true };
2524
+ if (during === null)
2525
+ moveTo(transferring(status));
2526
+ try {
2527
+ const local = await openLocal();
2528
+ await admittedRead(async () => {
2529
+ if (!live() || authorization === undefined)
2530
+ return;
2531
+ const withheld = await advance(room, local, authorization, live);
2532
+ if (withheld !== null)
2533
+ await withhold(withheld, room, local, progress);
2534
+ });
2535
+ }
2536
+ catch {
2537
+ stale.add(locator);
2538
+ }
2539
+ finally {
2540
+ if (during === null)
2541
+ await rest(progress, live);
2542
+ }
1789
2543
  });
1790
2544
  }
2545
+ function roomsCarried(replacing) {
2546
+ const replaced = new Set(replacing.map(roomLane));
2547
+ return Promise.all([...lanes]
2548
+ .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
2549
+ .map(([, held]) => held.chain));
2550
+ }
2551
+ const receipts = {
2552
+ heads: async () => (await openLocal()).receivedHeads(),
2553
+ parts: async (key, count) => (await openLocal()).receivedParts(key, count),
2554
+ keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
2555
+ drop: async (keys) => (await openLocal()).dropReceived(keys),
2556
+ };
2557
+ function observeStream() {
2558
+ const stream = options.adapter.roomStream;
2559
+ if (stream === undefined || withdrawStream !== undefined)
2560
+ return;
2561
+ withdrawStream = stream.observe((event) => {
2562
+ if (closed || stopping)
2563
+ return;
2564
+ if (event.kind === 'opened') {
2565
+ socketOpen = true;
2566
+ generation += 1;
2567
+ conversations.reopened();
2568
+ recovery.resume();
2569
+ void sync('opened');
2570
+ }
2571
+ else if (event.kind === 'closed')
2572
+ closedSocket(event.revoked);
2573
+ else if (event.kind === 'opening')
2574
+ void applyOpening(event.expired);
2575
+ else
2576
+ void applyRoom(event.locator, event.roomVersion);
2577
+ }, receipts);
2578
+ }
1791
2579
  function writesHeld() {
1792
2580
  return serviceMode === 'write-halted' || haltedRefusal;
1793
2581
  }
@@ -1806,14 +2594,17 @@ export function composeRuntime(options) {
1806
2594
  listener(mode);
1807
2595
  }
1808
2596
  if (releasing)
1809
- void enqueue();
2597
+ submitAll();
1810
2598
  }
1811
2599
  function observeConversations() {
1812
2600
  const stream = options.adapter.conversations;
1813
2601
  if (stream === undefined || withdrawConversations !== undefined)
1814
2602
  return;
1815
2603
  withdrawConversations = stream.observe((event) => {
1816
- conversations.receive(event);
2604
+ if (conversations.receive(event)) {
2605
+ void ready.then(scheduleCurrency);
2606
+ return;
2607
+ }
1817
2608
  if (event.kind !== 'gap' || conversations.current())
1818
2609
  return;
1819
2610
  show();
@@ -1821,129 +2612,42 @@ export function composeRuntime(options) {
1821
2612
  });
1822
2613
  }
1823
2614
  function scheduleCurrency() {
1824
- if (currencyQueued || closed || stopping)
2615
+ if (currencyQueued === generation || closed || stopping)
1825
2616
  return;
1826
- currencyQueued = true;
1827
- chain = chain.then(runCurrency, runCurrency);
2617
+ currencyQueued = generation;
2618
+ void task(CONVERSATION_LANE, runCurrency);
1828
2619
  }
1829
- async function runCurrency() {
1830
- currencyQueued = false;
1831
- if (closed || stopping || discoveryPaused)
2620
+ async function runCurrency(live) {
2621
+ if (currencyQueued === generation)
2622
+ currencyQueued = null;
2623
+ if (!live())
1832
2624
  return;
1833
2625
  await conversations.reconcile();
1834
2626
  if (conversations.current()) {
1835
2627
  show();
1836
2628
  return;
1837
2629
  }
1838
- const resting = status;
1839
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1840
- moveTo(transferring(resting));
2630
+ const progress = { complete: false, reachable: !status.providerUnreachable };
2631
+ moveTo(transferring(status));
1841
2632
  try {
1842
2633
  await conversations.catchUp(progress);
1843
2634
  }
1844
2635
  catch {
1845
2636
  progress.reachable = false;
1846
2637
  }
1847
- finally {
1848
- if (!closed) {
1849
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1850
- }
1851
- }
1852
- }
1853
- function handSelected(locators) {
1854
- const path = options.adapter.roomVersions;
1855
- if (path === undefined || withdrawNotifications === undefined)
1856
- return;
1857
- if (handed !== undefined && JSON.stringify(locators) === JSON.stringify(handed))
1858
- return;
1859
- handed = locators;
1860
- path.select(locators);
1861
- }
1862
- async function runNotified() {
1863
- const announced = [...notices];
1864
- notices.clear();
1865
- if (announced.length === 0 || closed || stopping || discoveryPaused)
1866
- return;
1867
- const resting = status;
1868
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1869
- moveTo(transferring(resting));
1870
- try {
1871
- const local = await openLocal();
1872
- for (const [locator, roomVersion] of announced) {
1873
- if (closed || discoveryPaused || authorization === undefined)
1874
- return;
1875
- if (handed?.includes(locator) !== true)
1876
- continue;
1877
- const room = discovered.get(locator);
1878
- const held = replicas.get(locator);
1879
- if (room === undefined)
1880
- continue;
1881
- if (sourceRefusals.has(locator)) {
1882
- void enqueue();
1883
- continue;
1884
- }
1885
- if (held === undefined)
1886
- continue;
1887
- if (held.confirmed.record.roomVersion === roomVersion)
1888
- continue;
1889
- const withheld = await advance(room, local, authorization);
1890
- if (withheld === null)
1891
- continue;
1892
- if (withheld.kind === 'refused') {
1893
- void enqueue();
1894
- continue;
1895
- }
1896
- if (withheld.kind === 'scoped-refusal') {
1897
- await scoped(withheld, room, local);
1898
- continue;
1899
- }
1900
- if (withheld.kind === 'transport-uncertainty') {
1901
- progress.reachable = false;
1902
- continue;
1903
- }
1904
- if (withheld.kind === 'room-not-readable') {
1905
- unreadable.add(locator);
1906
- list(room, false);
1907
- emitRooms();
1908
- continue;
1909
- }
1910
- if (withheld.kind === 'revoked-authorization') {
1911
- await refuse('revoked-authorization');
1912
- return;
1913
- }
1914
- }
1915
- }
1916
- catch {
1917
- progress.reachable = false;
1918
- }
1919
- finally {
1920
- if (!closed) {
1921
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1922
- }
1923
- }
1924
- }
1925
- function enqueue() {
1926
- if (stopping)
1927
- return chain;
1928
- if (inflight) {
1929
- queued = true;
1930
- return chain;
1931
- }
1932
- inflight = true;
1933
- chain = chain.then(run, run);
1934
- return chain;
2638
+ await rest(progress, live);
1935
2639
  }
1936
2640
  function close() {
1937
2641
  if (closed)
1938
2642
  return;
1939
2643
  closed = true;
1940
- withdrawNotifications?.();
1941
- withdrawNotifications = undefined;
2644
+ withdrawStream?.();
2645
+ withdrawStream = undefined;
1942
2646
  withdrawConversations?.();
1943
2647
  withdrawConversations = undefined;
1944
2648
  withdrawServiceMode?.();
1945
2649
  conversations.close();
1946
- stopTimer();
2650
+ void recovery.retire();
1947
2651
  store?.close();
1948
2652
  void opening?.then((opened) => {
1949
2653
  opened.close();
@@ -1953,6 +2657,7 @@ export function composeRuntime(options) {
1953
2657
  storageLossListeners.clear();
1954
2658
  statusListeners.clear();
1955
2659
  serviceModeListeners.clear();
2660
+ memberListeners.clear();
1956
2661
  readSide.close();
1957
2662
  queueSubscriptions.clear();
1958
2663
  roomDraftListeners.clear();
@@ -1960,8 +2665,8 @@ export function composeRuntime(options) {
1960
2665
  }
1961
2666
  async function stop() {
1962
2667
  stopping = true;
1963
- stopTimer();
1964
- await chain.catch(() => undefined);
2668
+ await recovery.retire();
2669
+ await Promise.all(working);
1965
2670
  await mutations;
1966
2671
  close();
1967
2672
  }
@@ -1970,11 +2675,69 @@ export function composeRuntime(options) {
1970
2675
  options.signal?.addEventListener('abort', close, { once: true });
1971
2676
  if (options.signal?.aborted)
1972
2677
  close();
1973
- void openLocal().catch(() => {
2678
+ observeStream();
2679
+ void openLocal().then(() => {
2680
+ if (capabilities.length > 0)
2681
+ notifyReady();
2682
+ }, () => {
1974
2683
  notifyReady();
1975
2684
  });
1976
- void enqueue();
1977
- function watch(topic, input, deliver) {
2685
+ if (options.adapter.roomStream === undefined)
2686
+ void sync('unstreamed');
2687
+ function planned(input, reach) {
2688
+ return planOf(input, roomFacts(reach), planReach(reach));
2689
+ }
2690
+ function watchPlan(plan, deliver) {
2691
+ return readSide.watch(plan, identityOf(plan), () => {
2692
+ const cover = covered(plan);
2693
+ awaitPreparation(cover);
2694
+ return cover;
2695
+ }, deliver);
2696
+ }
2697
+ async function searched(input, reach) {
2698
+ const plan = searchPlanOf(input);
2699
+ const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
2700
+ for (;;) {
2701
+ const cover = covered(filter);
2702
+ awaitPreparation(cover);
2703
+ const pending = 'pending' in cover ? [...cover.pending] : [];
2704
+ const covering = 'pending' in cover ? [] : cover.coverage.rooms;
2705
+ const unindexed = covering
2706
+ .filter(({ room }) => !searchIndex.rooms().has(room))
2707
+ .map(({ room }) => room);
2708
+ for (const room of unindexed)
2709
+ pending.push({ room, awaits: 'index' });
2710
+ const held = conversations.searchable();
2711
+ const named = reach !== null && !reach.conversations
2712
+ ? []
2713
+ : (plan.conversations ?? held).filter((one) => held.includes(one) && conversations.acquired(one));
2714
+ const waiting = named.filter((one) => !searchIndex.conversations().has(one));
2715
+ for (const conversation of waiting)
2716
+ pending.push({ conversation, awaits: 'index' });
2717
+ if (unindexed.length > 0 || waiting.length > 0)
2718
+ index(unindexed, waiting);
2719
+ if (pending.length > 0 || 'pending' in cover)
2720
+ return { pending };
2721
+ const seen = searchIndex.changes();
2722
+ await searchIndex.caughtUp();
2723
+ const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(filter);
2724
+ if (selected !== null && 'pending' in selected)
2725
+ return selected;
2726
+ if (searchIndex.changes() !== seen ||
2727
+ JSON.stringify(covered(filter)) !== JSON.stringify(cover))
2728
+ continue;
2729
+ return {
2730
+ ...searchIndex.search(plan, selected === null ? null : selected.documents, named),
2731
+ coverage: {
2732
+ rooms: covering,
2733
+ conversations: named,
2734
+ index: searchIndex.generation(),
2735
+ overlay: cover.coverage.overlay,
2736
+ },
2737
+ };
2738
+ }
2739
+ }
2740
+ const watch = (topic, input, deliver) => {
1978
2741
  if (topic === 'room-drafts') {
1979
2742
  const listener = { notify: deliver, ready: false };
1980
2743
  roomDraftListeners.add(listener);
@@ -1995,6 +2758,10 @@ export function composeRuntime(options) {
1995
2758
  if (topic === 'conversation') {
1996
2759
  return conversations.watch(input[0], input[1], deliver);
1997
2760
  }
2761
+ if (topic === 'conversations')
2762
+ return conversations.watchList(deliver);
2763
+ if (topic === 'agents')
2764
+ return conversations.watchAgents(deliver);
1998
2765
  if (topic === 'presentation') {
1999
2766
  const listener = {
2000
2767
  app: input[0],
@@ -2049,6 +2816,13 @@ export function composeRuntime(options) {
2049
2816
  serviceModeListeners.delete(deliver);
2050
2817
  };
2051
2818
  }
2819
+ if (topic === 'member') {
2820
+ memberListeners.add(deliver);
2821
+ deliver(member);
2822
+ return () => {
2823
+ memberListeners.delete(deliver);
2824
+ };
2825
+ }
2052
2826
  if (topic === 'queue') {
2053
2827
  const queue = { locator: input[0], notify: deliver };
2054
2828
  queueSubscriptions.add(queue);
@@ -2057,9 +2831,8 @@ export function composeRuntime(options) {
2057
2831
  queueSubscriptions.delete(queue);
2058
2832
  };
2059
2833
  }
2060
- const plan = planOf(input[0], roomFacts());
2061
- return readSide.watch(plan, identityOf(plan), deliver);
2062
- }
2834
+ return watchPlan(planned(input[0], null), deliver);
2835
+ };
2063
2836
  const md = {
2064
2837
  loaded() {
2065
2838
  return openLocal().then(() => undefined);
@@ -2067,18 +2840,8 @@ export function composeRuntime(options) {
2067
2840
  rooms() {
2068
2841
  return listed();
2069
2842
  },
2070
- slots() {
2071
- return slotted();
2072
- },
2073
- query: ((input) => readSide.query(planOf(input, roomFacts()))),
2074
- async search(input) {
2075
- const plan = searchPlanOf(input);
2076
- const selected = plan.documents === undefined
2077
- ? null
2078
- : (await readSide.query(filterOf(plan.documents, roomFacts())))
2079
- .documents;
2080
- return searchIndex.search(plan, selected);
2081
- },
2843
+ query: ((input) => rowsQuery(planned(input, null))),
2844
+ search: (input) => searched(input, null),
2082
2845
  types(locator) {
2083
2846
  return {
2084
2847
  collection: modelTypes(modelOf(locator)),
@@ -2088,12 +2851,6 @@ export function composeRuntime(options) {
2088
2851
  : modelTypes(modelOf(locator, 'typed-selection')),
2089
2852
  };
2090
2853
  },
2091
- syncStatus() {
2092
- return shown;
2093
- },
2094
- serviceMode() {
2095
- return serviceMode;
2096
- },
2097
2854
  roomDraft: {
2098
2855
  list() {
2099
2856
  return exclusive(async () => {
@@ -2112,20 +2869,20 @@ export function composeRuntime(options) {
2112
2869
  return exclusive(async () => {
2113
2870
  const parts = readParts(input);
2114
2871
  if (path === '' || normalize(path) !== path)
2115
- throw new Error('room-draft-malformed');
2872
+ throw runtimeError('room-draft-malformed');
2116
2873
  const local = await openLocal();
2117
2874
  const current = roomDrafts.get(room)?.get(path);
2118
2875
  if ((current?.revision ?? null) !== revision) {
2119
- return {
2120
- outcome: 'stale',
2121
- current: current === undefined ? null : roomDraftOf(current),
2122
- };
2876
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2123
2877
  }
2878
+ const file = parts.file === undefined
2879
+ ? undefined
2880
+ : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
2124
2881
  const record = {
2125
2882
  locator: room,
2126
2883
  path,
2127
2884
  revision: roomDraftRevision(),
2128
- parts: held(current?.parts, parts),
2885
+ parts: held(current?.parts, parts, file),
2129
2886
  returned: current?.returned ?? null,
2130
2887
  };
2131
2888
  await local.keepRoomDraft(record);
@@ -2139,10 +2896,7 @@ export function composeRuntime(options) {
2139
2896
  const local = await openLocal();
2140
2897
  const current = roomDrafts.get(room)?.get(path);
2141
2898
  if (current === undefined || current.revision !== revision) {
2142
- return {
2143
- outcome: 'stale',
2144
- current: current === undefined ? null : roomDraftOf(current),
2145
- };
2899
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2146
2900
  }
2147
2901
  await local.discardRoomDraft(room, path);
2148
2902
  forgetRoomDraft(room, path);
@@ -2174,12 +2928,13 @@ export function composeRuntime(options) {
2174
2928
  save(room, named, writtenBy) {
2175
2929
  return exclusive(() => saveRoomDrafts(room, named, writtenBy));
2176
2930
  },
2177
- outcome(identifier) {
2178
- return outcomes.get(identifier) ?? null;
2179
- },
2180
2931
  history(locator, path, query) {
2181
2932
  return readHistory(locator, path, query);
2182
2933
  },
2934
+ binaryFile: {
2935
+ read: readBinaryPath,
2936
+ readReference: readBinaryReference,
2937
+ },
2183
2938
  call(name, input) {
2184
2939
  return invokeAction(name, input);
2185
2940
  },
@@ -2191,25 +2946,34 @@ export function composeRuntime(options) {
2191
2946
  log() {
2192
2947
  return journal.export();
2193
2948
  },
2194
- storageLoss() {
2195
- return storageLoss;
2196
- },
2197
2949
  persistence() {
2198
2950
  return options.shell.persistentStorage;
2199
2951
  },
2200
- observe: ((topic, ...input) => opened((deliver) => watch(topic, input, (message) => {
2201
- deliver(structuredClone(message));
2202
- }), topic === 'query')),
2952
+ observe: observing(watch),
2203
2953
  async reauthorize() {
2204
2954
  if (refusal === null || !refusal.reauthorizationOffered)
2205
2955
  return;
2206
2956
  journal.record('connection reauthorize');
2207
- await chain.catch(() => undefined);
2957
+ await Promise.all([...lanes.values()]
2958
+ .filter((held) => held.generation === generation)
2959
+ .map((held) => held.chain));
2208
2960
  await mutations;
2209
2961
  authorization = undefined;
2210
2962
  discoveryPaused = false;
2211
- await enqueue();
2963
+ if (options.adapter.roomStream === undefined)
2964
+ await sync('unstreamed');
2965
+ else
2966
+ await task(OPENING_LANE, ensureAuthorization);
2212
2967
  },
2213
2968
  };
2214
- return { md, stop, watch };
2969
+ const reachable = {
2970
+ md,
2971
+ watch,
2972
+ referrerScope: (referrer) => options.adapter.referrerScope(referrer),
2973
+ plan: planned,
2974
+ query: (plan) => rowsQuery(plan),
2975
+ watchPlan,
2976
+ search: searched,
2977
+ };
2978
+ return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
2215
2979
  }