@agentshouse/mdruntime 0.1.0-c6e4c19c13638 → 0.1.0-c6fd2cbbff187

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 (207) hide show
  1. package/AGENTS.md +277 -219
  2. package/README.md +20 -10
  3. package/dist/acquisition.d.ts +14 -0
  4. package/dist/acquisition.d.ts.map +1 -0
  5. package/dist/acquisition.js +24 -0
  6. package/dist/adapter.d.ts +94 -20
  7. package/dist/adapter.d.ts.map +1 -1
  8. package/dist/attached-model.d.ts.map +1 -1
  9. package/dist/attached-model.js +15 -2
  10. package/dist/attempt.d.ts +16 -0
  11. package/dist/attempt.d.ts.map +1 -0
  12. package/dist/attempt.js +70 -0
  13. package/dist/binary.d.ts +23 -0
  14. package/dist/binary.d.ts.map +1 -0
  15. package/dist/binary.js +8 -0
  16. package/dist/bootstrap.d.ts +18 -5
  17. package/dist/bootstrap.d.ts.map +1 -1
  18. package/dist/bootstrap.js +113 -37
  19. package/dist/change-set.d.ts +2 -0
  20. package/dist/change-set.d.ts.map +1 -1
  21. package/dist/change-set.js +49 -16
  22. package/dist/change.d.ts +15 -0
  23. package/dist/change.d.ts.map +1 -1
  24. package/dist/change.js +26 -1
  25. package/dist/chat/acts.d.ts +33 -0
  26. package/dist/chat/acts.d.ts.map +1 -0
  27. package/dist/chat/acts.js +36 -0
  28. package/dist/chat/context.d.ts +3 -0
  29. package/dist/chat/context.d.ts.map +1 -0
  30. package/dist/chat/context.js +10 -0
  31. package/dist/chat/earlier.d.ts +10 -0
  32. package/dist/chat/earlier.d.ts.map +1 -0
  33. package/dist/chat/earlier.js +54 -0
  34. package/dist/chat/input.d.ts +9 -0
  35. package/dist/chat/input.d.ts.map +1 -0
  36. package/dist/chat/input.js +99 -0
  37. package/dist/chat/kit.d.ts +61 -0
  38. package/dist/chat/kit.d.ts.map +1 -0
  39. package/dist/chat/kit.js +123 -0
  40. package/dist/chat/lasting.d.ts +6 -0
  41. package/dist/chat/lasting.d.ts.map +1 -0
  42. package/dist/chat/lasting.js +19 -0
  43. package/dist/chat/list.d.ts +27 -0
  44. package/dist/chat/list.d.ts.map +1 -0
  45. package/dist/chat/list.js +99 -0
  46. package/dist/chat/markdown.d.ts +20 -0
  47. package/dist/chat/markdown.d.ts.map +1 -0
  48. package/dist/chat/markdown.js +188 -0
  49. package/dist/chat/message.d.ts +6 -0
  50. package/dist/chat/message.d.ts.map +1 -0
  51. package/dist/chat/message.js +192 -0
  52. package/dist/chat/messages.d.ts +22 -0
  53. package/dist/chat/messages.d.ts.map +1 -0
  54. package/dist/chat/messages.js +155 -0
  55. package/dist/chat/name.d.ts +6 -0
  56. package/dist/chat/name.d.ts.map +1 -0
  57. package/dist/chat/name.js +37 -0
  58. package/dist/chat/opened.d.ts +11 -0
  59. package/dist/chat/opened.d.ts.map +1 -0
  60. package/dist/chat/opened.js +17 -0
  61. package/dist/chat/overlay.d.ts +31 -0
  62. package/dist/chat/overlay.d.ts.map +1 -0
  63. package/dist/chat/overlay.js +97 -0
  64. package/dist/chat/parts.d.ts +57 -0
  65. package/dist/chat/parts.d.ts.map +1 -0
  66. package/dist/chat/parts.js +119 -0
  67. package/dist/chat/process.d.ts +20 -0
  68. package/dist/chat/process.d.ts.map +1 -0
  69. package/dist/chat/process.js +32 -0
  70. package/dist/chat/provider.d.ts +63 -0
  71. package/dist/chat/provider.d.ts.map +1 -0
  72. package/dist/chat/provider.js +635 -0
  73. package/dist/chat/question.d.ts +9 -0
  74. package/dist/chat/question.d.ts.map +1 -0
  75. package/dist/chat/question.js +69 -0
  76. package/dist/chat/questions.d.ts +31 -0
  77. package/dist/chat/questions.d.ts.map +1 -0
  78. package/dist/chat/questions.js +111 -0
  79. package/dist/chat/read.d.ts +5 -0
  80. package/dist/chat/read.d.ts.map +1 -0
  81. package/dist/chat/read.js +13 -0
  82. package/dist/chat/ready.d.ts +34 -0
  83. package/dist/chat/ready.d.ts.map +1 -0
  84. package/dist/chat/ready.js +91 -0
  85. package/dist/chat/style.d.ts +4 -0
  86. package/dist/chat/style.d.ts.map +1 -0
  87. package/dist/chat/style.js +302 -0
  88. package/dist/chat.d.ts +14 -0
  89. package/dist/chat.d.ts.map +1 -0
  90. package/dist/chat.js +8 -0
  91. package/dist/client.d.ts +27 -12
  92. package/dist/client.d.ts.map +1 -1
  93. package/dist/client.js +77 -16
  94. package/dist/collection.d.ts +21 -12
  95. package/dist/collection.d.ts.map +1 -1
  96. package/dist/collection.js +271 -23
  97. package/dist/connection.d.ts +46 -15
  98. package/dist/connection.d.ts.map +1 -1
  99. package/dist/connection.js +1946 -414
  100. package/dist/conversation.d.ts +30 -10
  101. package/dist/conversation.d.ts.map +1 -1
  102. package/dist/conversation.js +485 -159
  103. package/dist/durability.d.ts +7 -1
  104. package/dist/durability.d.ts.map +1 -1
  105. package/dist/entry.d.ts +17 -0
  106. package/dist/entry.d.ts.map +1 -0
  107. package/dist/entry.js +156 -0
  108. package/dist/expectation.d.ts +21 -0
  109. package/dist/expectation.d.ts.map +1 -0
  110. package/dist/expectation.js +70 -0
  111. package/dist/failure.d.ts +13 -0
  112. package/dist/failure.d.ts.map +1 -0
  113. package/dist/failure.js +19 -0
  114. package/dist/fence.d.ts +6 -0
  115. package/dist/fence.d.ts.map +1 -0
  116. package/dist/fence.js +10 -0
  117. package/dist/host.d.ts +18 -2
  118. package/dist/host.d.ts.map +1 -1
  119. package/dist/host.js +101 -19
  120. package/dist/identity.d.ts +5 -0
  121. package/dist/identity.d.ts.map +1 -1
  122. package/dist/identity.js +19 -1
  123. package/dist/index.d.ts +16 -8
  124. package/dist/index.d.ts.map +1 -1
  125. package/dist/index.js +5 -0
  126. package/dist/memory.d.ts +12 -4
  127. package/dist/memory.d.ts.map +1 -1
  128. package/dist/memory.js +94 -25
  129. package/dist/model.d.ts +14 -4
  130. package/dist/model.d.ts.map +1 -1
  131. package/dist/model.js +12 -1
  132. package/dist/observation.d.ts +7 -2
  133. package/dist/observation.d.ts.map +1 -1
  134. package/dist/observation.js +29 -12
  135. package/dist/open.d.ts +2 -0
  136. package/dist/open.d.ts.map +1 -0
  137. package/dist/open.js +1 -0
  138. package/dist/outcome.d.ts +38 -3
  139. package/dist/outcome.d.ts.map +1 -1
  140. package/dist/outcome.js +45 -1
  141. package/dist/overlay.d.ts +16 -3
  142. package/dist/overlay.d.ts.map +1 -1
  143. package/dist/overlay.js +118 -77
  144. package/dist/query.d.ts +60 -4
  145. package/dist/query.d.ts.map +1 -1
  146. package/dist/query.js +41 -38
  147. package/dist/reach.d.ts +8 -3
  148. package/dist/reach.d.ts.map +1 -1
  149. package/dist/reach.js +63 -24
  150. package/dist/react.d.ts +31 -18
  151. package/dist/react.d.ts.map +1 -1
  152. package/dist/react.js +15 -11
  153. package/dist/read-side.d.ts +13 -6
  154. package/dist/read-side.d.ts.map +1 -1
  155. package/dist/read-side.js +428 -94
  156. package/dist/readiness.d.ts +53 -0
  157. package/dist/readiness.d.ts.map +1 -0
  158. package/dist/readiness.js +33 -0
  159. package/dist/reads.d.ts +11 -0
  160. package/dist/reads.d.ts.map +1 -0
  161. package/dist/reads.js +98 -0
  162. package/dist/recovery.d.ts +25 -0
  163. package/dist/recovery.d.ts.map +1 -0
  164. package/dist/recovery.js +197 -0
  165. package/dist/refresh.d.ts +13 -3
  166. package/dist/refresh.d.ts.map +1 -1
  167. package/dist/refresh.js +49 -38
  168. package/dist/replica.d.ts +13 -6
  169. package/dist/replica.d.ts.map +1 -1
  170. package/dist/replica.js +143 -70
  171. package/dist/room-draft.d.ts +38 -9
  172. package/dist/room-draft.d.ts.map +1 -1
  173. package/dist/room-draft.js +134 -25
  174. package/dist/rows.d.ts +10 -13
  175. package/dist/rows.d.ts.map +1 -1
  176. package/dist/rows.js +19 -14
  177. package/dist/save.d.ts +32 -1
  178. package/dist/save.d.ts.map +1 -1
  179. package/dist/save.js +392 -45
  180. package/dist/search.d.ts +52 -8
  181. package/dist/search.d.ts.map +1 -1
  182. package/dist/search.js +192 -47
  183. package/dist/seed.d.ts +16 -0
  184. package/dist/seed.d.ts.map +1 -0
  185. package/dist/seed.js +75 -0
  186. package/dist/shared.d.ts +11 -0
  187. package/dist/shared.d.ts.map +1 -0
  188. package/dist/shared.js +368 -0
  189. package/dist/status.d.ts +6 -2
  190. package/dist/status.d.ts.map +1 -1
  191. package/dist/status.js +0 -8
  192. package/dist/store.d.ts +89 -18
  193. package/dist/store.d.ts.map +1 -1
  194. package/dist/store.js +573 -200
  195. package/dist/turn.d.ts +7 -0
  196. package/dist/turn.d.ts.map +1 -0
  197. package/dist/turn.js +45 -0
  198. package/dist/windows.d.ts +19 -0
  199. package/dist/windows.d.ts.map +1 -0
  200. package/dist/windows.js +83 -0
  201. package/dist/wire.d.ts +149 -33
  202. package/dist/wire.d.ts.map +1 -1
  203. package/dist/wire.js +63 -31
  204. package/package.json +26 -14
  205. package/dist/attachment.d.ts +0 -3
  206. package/dist/attachment.d.ts.map +0 -1
  207. package/dist/attachment.js +0 -8
@@ -1,25 +1,48 @@
1
1
  import { attachedModel } from './attached-model.js';
2
2
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
3
- import { observing } from './observation.js';
4
- import { filterOf, identityOf, planOf, } from './query.js';
3
+ import { observing, whole } from './observation.js';
4
+ import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
5
5
  import { createReadSide } from './read-side.js';
6
+ import { seededResult } from './seed.js';
7
+ import { markdownPath, roomOfKey } from './rows.js';
6
8
  import { createSearchIndex, searchPlanOf, } from './search.js';
9
+ import { capped, MAXIMUM_WINDOW } from './windows.js';
7
10
  import { bootstrapRoom } from './bootstrap.js';
8
- import { changeSet, mergeChangeSets } from './change-set.js';
11
+ import { changeSet, entering, mergeChangeSets } from './change-set.js';
9
12
  import { createConversationStore, } from './conversation.js';
10
- import { parseCollection, validRoot } from './collection.js';
13
+ import { queuedFile } from './change.js';
14
+ import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
15
+ import { runtimeError } from './failure.js';
11
16
  import { createLog } from './log.js';
17
+ import { binaryRecord } from './binary.js';
18
+ import { absentBinary, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
19
+ import { createRecovery } from './recovery.js';
20
+ import { doorReads } from './reads.js';
12
21
  import { refreshRoom } from './refresh.js';
13
- import { buildBatch } from './save.js';
14
- import { applyReturned, compositionOf, landed, readConfirmed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
15
- import { changedParts, changesOf, held, readParts, returnedInto, underneath, } from './room-draft.js';
16
- import { settled, same, saving, STARTING, transferring, } from './status.js';
22
+ import { buildBatch, joined, limitedIn, queuedDocuments, requeued, } from './save.js';
23
+ import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, standing, } from './replica.js';
24
+ import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
25
+ import { same, saving, STARTING, transferring, } from './status.js';
17
26
  import { detectStorageLoss, nextGeneration } from './durability.js';
18
- import { roomDraftRevision } from './identity.js';
27
+ import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
28
+ import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
29
+ import { FencedMap } from './fence.js';
30
+ import { tally } from './shared.js';
31
+ import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
32
+ import { identityTime, roomDraftRevision } from './identity.js';
19
33
  import { beneath, normalize } from './paths.js';
20
34
  import { openRuntimeStore, } from './store.js';
21
35
  import { unconfirmed } from './transfer.js';
22
36
  import { governs, holds, narrowed, planReach, } from './reach.js';
37
+ function roomsAlone(plan) {
38
+ return (plan.reach === null &&
39
+ plan.root.types === null &&
40
+ plan.root.where === null &&
41
+ plan.root.pathPrefix === null &&
42
+ plan.paths === null &&
43
+ plan.hop === null &&
44
+ plan.backlinks === null);
45
+ }
23
46
  function within(root, path) {
24
47
  return root === '' || beneath(root, path);
25
48
  }
@@ -33,22 +56,68 @@ function declaredSelection(room) {
33
56
  root !== '' &&
34
57
  selection.roots.every((other, at) => at === index || !within(other, root)));
35
58
  }
59
+ const OPENING_LANE = 'opening';
60
+ const CONVERSATION_LANE = 'conversations';
61
+ const ROOM_READS = 256;
62
+ const ROOM_ATTEMPTS = 16;
63
+ function operationOf(input) {
64
+ const operation = input?.operation_id;
65
+ return typeof operation === 'string' ? operation : null;
66
+ }
67
+ const outside = new Map();
68
+ function outsideOf(roots) {
69
+ const key = JSON.stringify(roots);
70
+ const held = outside.get(key);
71
+ if (held !== undefined)
72
+ return held;
73
+ const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
74
+ const made = {
75
+ documents: (path) => (markdownPath(path) ? counted(path) : 0),
76
+ binaries: counted,
77
+ };
78
+ outside.set(key, made);
79
+ return made;
80
+ }
81
+ function abandonedOn(signal) {
82
+ return new Promise((resolve) => {
83
+ if (signal.aborted)
84
+ resolve(null);
85
+ else
86
+ signal.addEventListener('abort', () => resolve(null), { once: true });
87
+ });
88
+ }
89
+ function roomLane(locator) {
90
+ return `room ${locator}`;
91
+ }
36
92
  export function composeRuntime(options) {
93
+ const localSaves = options.adapter.localSaves === true;
37
94
  let authorization = options.authorization;
38
95
  let opening;
39
96
  let store;
40
- const replicas = new Map();
97
+ const fences = new Map();
98
+ const isFenced = (locator) => fences.has(locator);
99
+ let fencings = 0;
100
+ const lastFenced = new Map();
101
+ const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
102
+ let discoveries = 0;
103
+ const replicas = new FencedMap(isFenced);
104
+ const queuesWithoutReplica = new FencedMap(isFenced);
41
105
  const pendings = new Map();
42
- const roomDrafts = new Map();
106
+ const seeds = new Map();
107
+ const seedWatchers = new Map();
108
+ const roomDrafts = new FencedMap(isFenced);
43
109
  const presentations = new Map();
44
110
  let limits = [];
45
111
  let capabilities = [];
46
- const rooms = new Map();
112
+ const rooms = new FencedMap(isFenced);
113
+ const counted = new Map();
47
114
  const unreadable = new Set();
48
- const models = new Map();
49
- const liveModels = new Map();
115
+ const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
116
+ const models = new FencedMap(isModelFenced);
117
+ const liveModels = new FencedMap(isModelFenced);
50
118
  const invalidModels = new Set();
51
119
  const discovered = new Map();
120
+ const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
52
121
  let roomsListed = false;
53
122
  const scopedRefusals = new Map();
54
123
  const sourceRefusals = new Map();
@@ -76,18 +145,29 @@ export function composeRuntime(options) {
76
145
  let status = STARTING;
77
146
  let shown = STARTING;
78
147
  let discoveryPaused = false;
79
- let resetTimer;
80
148
  let closed = false;
81
149
  let stopping = false;
82
150
  let withdrawStream;
83
151
  const announced = new Map();
84
152
  let underway = null;
85
- const submitting = new Map();
153
+ let streamed = null;
154
+ let handed = false;
155
+ const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
156
+ const repairing = new Set();
157
+ const calling = new Set();
158
+ const repairable = new Map();
159
+ let outcomeRetention = null;
86
160
  const answering = new Map();
87
161
  let complete = false;
88
162
  let recorded = false;
89
163
  const stale = new Set();
90
- let chain = Promise.resolve();
164
+ let generation = 0;
165
+ let discovering = null;
166
+ let opened = null;
167
+ const owed = new Set();
168
+ const lanes = new Map();
169
+ let roomReads = { generation, free: ROOM_READS, waiting: [] };
170
+ const working = new Set();
91
171
  let mutations = Promise.resolve();
92
172
  const roomListeners = new Set();
93
173
  const refusalListeners = new Set();
@@ -98,27 +178,49 @@ export function composeRuntime(options) {
98
178
  const presentationListeners = new Set();
99
179
  const readSide = createReadSide();
100
180
  let withdrawConversations;
101
- let currencyQueued = false;
181
+ let currencyQueued = null;
102
182
  let socketOpen = false;
103
183
  let serviceMode = 'open';
104
184
  let haltedRefusal = false;
105
185
  let openSignals = 0;
106
186
  const serviceModeListeners = new Set();
187
+ let member = null;
188
+ const memberListeners = new Set();
189
+ const prepared = new Set();
190
+ const projecting = new Set();
191
+ const unpreparedDemand = new Set();
192
+ const unindexable = new Set();
193
+ let preparing = false;
194
+ let demand = WHOLE_DEMAND;
195
+ let narrowing = null;
196
+ let targets = {};
197
+ let persistence = options.shell.persistence;
198
+ let capacity = null;
199
+ const paused = new Set();
200
+ let rechecking = null;
201
+ const readinessWatches = new Set();
202
+ let readinessQueued = false;
203
+ const catching = new Set();
204
+ const transfers = new FencedMap(isFenced);
107
205
  const conversations = createConversationStore({
108
206
  agents: declaredAgents,
109
- documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
207
+ documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
110
208
  call: (name, input) => invokeAction(name, input),
111
209
  durably,
112
210
  stream: () => options.adapter.conversations,
113
211
  publish: publishChangeSet,
212
+ online: () => !status.providerUnreachable && refusal === null,
114
213
  closed: () => closed,
214
+ unanswered: resendLater,
215
+ expired: (operation) => pastHorizon(operation),
115
216
  });
116
217
  const searchIndex = createSearchIndex({
117
- documents: () => readSide.documents(),
218
+ room: (room) => readSide.room(room),
118
219
  document: (room, path) => readSide.document(room, path),
119
- messages: () => conversations.messages(),
120
- message: (conversation, identity) => conversations.message(conversation, identity),
121
220
  });
221
+ const roomsToIndex = new Set();
222
+ const conversationsToIndex = new Set();
223
+ let indexing = false;
122
224
  const journal = createLog();
123
225
  let notifyReady = () => { };
124
226
  const ready = new Promise((resolve) => {
@@ -173,6 +275,15 @@ export function composeRuntime(options) {
173
275
  const value = presentations.get(app);
174
276
  return value === undefined ? null : structuredClone(value);
175
277
  }
278
+ function keepPresentation(app, next) {
279
+ return exclusive(async () => {
280
+ const local = await openLocal();
281
+ const stored = next(presentations.get(app));
282
+ await local.present({ app, value: stored });
283
+ presentations.set(app, stored);
284
+ publishChangeSet(changeSet('', null, null, { presentation: app }));
285
+ });
286
+ }
176
287
  function emitPresentation(app) {
177
288
  if (closed)
178
289
  return;
@@ -182,7 +293,9 @@ export function composeRuntime(options) {
182
293
  }
183
294
  }
184
295
  function pending() {
185
- return [...replicas.values()].some((replica) => replica.queue.length > 0);
296
+ return (!localSaves &&
297
+ (queuesWithoutReplica.size > 0 ||
298
+ [...replicas.values()].some((replica) => replica.queue.length > 0)));
186
299
  }
187
300
  function attention() {
188
301
  const attentionRequired = [];
@@ -195,9 +308,16 @@ export function composeRuntime(options) {
195
308
  returned: structuredClone(record.returned),
196
309
  });
197
310
  }
311
+ for (const [room, refusal] of sourceRefusals)
312
+ attentionRequired.push({ room, refusal: structuredClone(refusal) });
198
313
  attentionRequired.sort((left, right) => {
199
314
  if (left.room !== right.room)
200
315
  return left.room < right.room ? -1 : 1;
316
+ if ('refusal' in left || 'refusal' in right) {
317
+ if ('refusal' in left && 'refusal' in right)
318
+ return 0;
319
+ return 'refusal' in left ? -1 : 1;
320
+ }
201
321
  if (left.path !== right.path)
202
322
  return left.path < right.path ? -1 : 1;
203
323
  return 0;
@@ -211,8 +331,21 @@ export function composeRuntime(options) {
211
331
  }
212
332
  return { attentionRequired, reset };
213
333
  }
334
+ function documentsIn(locator) {
335
+ const replica = replicas.get(locator);
336
+ if (replica === undefined)
337
+ return 0;
338
+ const roots = replica.confirmed.record.typedSelection?.roots ?? [];
339
+ const weighed = outsideOf(roots);
340
+ return (tally(replica.visible.documents, weighed.documents) +
341
+ tally(replica.visible.binaries, weighed.binaries));
342
+ }
214
343
  function listed() {
215
- return structuredClone([...rooms.values()]);
344
+ return structuredClone([...rooms.values()].map((room) => {
345
+ const documents = documentsIn(room.locator);
346
+ counted.set(room.locator, documents);
347
+ return { ...room, documents };
348
+ }));
216
349
  }
217
350
  function roomFacts(reach = null) {
218
351
  const held = [...rooms.values()]
@@ -232,18 +365,26 @@ export function composeRuntime(options) {
232
365
  return { listed: roomsListed, rooms: held };
233
366
  }
234
367
  function emitRooms() {
368
+ readSide.recover();
369
+ queueReadiness();
235
370
  const value = listed();
236
371
  for (const listener of roomListeners)
237
372
  listener(value);
238
373
  }
239
374
  function emitRefusal() {
375
+ queueReadiness();
240
376
  for (const listener of refusalListeners)
241
377
  listener(refusal);
242
378
  }
243
379
  function emitStorageLoss() {
380
+ queueReadiness();
244
381
  for (const listener of storageLossListeners)
245
382
  listener(storageLoss);
246
383
  }
384
+ function emitMember() {
385
+ for (const listener of memberListeners)
386
+ listener(member);
387
+ }
247
388
  function moveTo(next) {
248
389
  if (closed)
249
390
  return;
@@ -253,7 +394,8 @@ export function composeRuntime(options) {
253
394
  function show() {
254
395
  if (closed)
255
396
  return;
256
- const next = status.state === 'synced' && !conversations.current()
397
+ queueReadiness();
398
+ const next = status.state === 'synced' && demand.conversations && !conversations.current()
257
399
  ? { ...status, state: 'syncing' }
258
400
  : status;
259
401
  if (same(shown, next))
@@ -263,6 +405,8 @@ export function composeRuntime(options) {
263
405
  listener(shown);
264
406
  }
265
407
  function entryOf(batch) {
408
+ if (localSaves)
409
+ return Object.freeze({ batch, state: 'local' });
266
410
  const state = pendings.get(batch.id);
267
411
  if (state?.accepted !== undefined) {
268
412
  return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
@@ -270,10 +414,7 @@ export function composeRuntime(options) {
270
414
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
271
415
  }
272
416
  function entriesOf(locator) {
273
- const replica = replicas.get(locator);
274
- if (replica === undefined)
275
- return [];
276
- return replica.queue.map(entryOf);
417
+ return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
277
418
  }
278
419
  function emitQueue(locator) {
279
420
  const entries = entriesOf(locator);
@@ -283,12 +424,522 @@ export function composeRuntime(options) {
283
424
  }
284
425
  }
285
426
  function applyChangeSet(set) {
286
- readSide.apply(set);
287
- searchIndex.changed(set);
427
+ const held = {
428
+ ...set,
429
+ documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
430
+ };
431
+ readSide.apply(held);
432
+ searchIndex.changed(held);
433
+ }
434
+ async function referenced(locator) {
435
+ for (;;) {
436
+ const record = replicas.get(locator)?.confirmed.record;
437
+ if (record === undefined || closed)
438
+ return;
439
+ const held = { text: record.documents, binaries: record.binaries };
440
+ const composition = compositionOf(record);
441
+ if (referencesIndexed(held, composition))
442
+ return;
443
+ await indexReferrers(held, composition);
444
+ }
445
+ }
446
+ async function prepareReplica(locator) {
447
+ await referenced(locator);
448
+ const replica = replicas.get(locator);
449
+ if (replica === undefined || closed || prepared.has(locator))
450
+ return;
451
+ const visible = replica.visible;
452
+ const paths = [
453
+ ...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
454
+ ];
455
+ const entered = [];
456
+ projecting.add(locator);
457
+ try {
458
+ await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
459
+ const current = replicas.get(locator);
460
+ if (current === undefined || closed)
461
+ return;
462
+ const set = entering(locator, current, chunk);
463
+ readSide.apply(set);
464
+ entered.push(set);
465
+ });
466
+ if (!replicas.has(locator) || closed)
467
+ return;
468
+ prepared.add(locator);
469
+ }
470
+ finally {
471
+ projecting.delete(locator);
472
+ }
473
+ conversations.changed(mergeChangeSets(entered));
474
+ readSide.recover();
475
+ queueReadiness();
476
+ }
477
+ function unprepared() {
478
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
479
+ }
480
+ function unbuilt(locator, error) {
481
+ unindexable.add(locator);
482
+ journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
483
+ queueReadiness();
484
+ }
485
+ function prepare(locators = []) {
486
+ for (const locator of locators)
487
+ unpreparedDemand.add(locator);
488
+ if (preparing)
489
+ return;
490
+ preparing = true;
491
+ void (async () => {
492
+ try {
493
+ for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
494
+ await yielded();
495
+ if (closed)
496
+ return;
497
+ unpreparedDemand.delete(waiting);
498
+ await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
499
+ }
500
+ }
501
+ finally {
502
+ preparing = false;
503
+ }
504
+ })();
505
+ }
506
+ function reachedRooms(plan) {
507
+ const reach = plan.reach;
508
+ const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
509
+ const named = plan.rooms ??
510
+ (reached === null
511
+ ? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
512
+ : [...reached]);
513
+ return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
514
+ }
515
+ function awaited(locator) {
516
+ return (!roomsListed ||
517
+ (rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
518
+ }
519
+ function covered(plan) {
520
+ const locators = reachedRooms(plan);
521
+ if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
522
+ return { pending: [] };
523
+ }
524
+ const pending = [];
525
+ const coverage = [];
526
+ const overlay = [];
527
+ for (const locator of locators) {
528
+ const replica = replicas.get(locator);
529
+ if (replica === undefined) {
530
+ if (awaited(locator))
531
+ pending.push({ room: locator, awaits: 'acquisition' });
532
+ continue;
533
+ }
534
+ if (!prepared.has(locator)) {
535
+ pending.push({ room: locator, awaits: 'preparation' });
536
+ continue;
537
+ }
538
+ coverage.push({
539
+ room: locator,
540
+ generation: replica.confirmed.record.transfer,
541
+ current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
542
+ });
543
+ for (const batch of replica.queue)
544
+ overlay.push(batch.id);
545
+ }
546
+ if (pending.length > 0)
547
+ return { pending };
548
+ return { coverage: { rooms: coverage, indexes: [], overlay } };
549
+ }
550
+ function awaitPreparation(cover) {
551
+ if (!('pending' in cover))
552
+ return;
553
+ const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
554
+ if (waiting.length > 0)
555
+ prepare(waiting.map(({ room }) => room));
556
+ }
557
+ function acquires(room) {
558
+ return (demanded(demand, room) ||
559
+ (replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
560
+ queuesWithoutReplica.has(room.locator));
561
+ }
562
+ function acquiredLocators() {
563
+ return [...discovered.values()]
564
+ .filter(acquires)
565
+ .map((room) => room.locator)
566
+ .sort();
567
+ }
568
+ function heldLocators(acquired) {
569
+ return demand.whole ? acquired : [...new Set([...acquired, ...demand.rooms])].sort();
570
+ }
571
+ function heldDemand(acquired) {
572
+ if (demand.whole)
573
+ return null;
574
+ return {
575
+ rooms: heldLocators(acquired.map((room) => room.locator)),
576
+ conversations: demand.conversations,
577
+ };
578
+ }
579
+ function applyDemand(next) {
580
+ const before = heldLocators(acquiredLocators());
581
+ const { whole, conversations: conversed } = demand;
582
+ demand = next;
583
+ const after = heldLocators(acquiredLocators());
584
+ queueReadiness();
585
+ if (!conversed && next.conversations)
586
+ scheduleCurrency();
587
+ if (JSON.stringify(before) === JSON.stringify(after) &&
588
+ whole === next.whole &&
589
+ conversed === next.conversations)
590
+ return;
591
+ journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
592
+ if (options.adapter.roomStream === undefined)
593
+ void sync('unstreamed');
594
+ else if (socketOpen)
595
+ options.adapter.roomStream.reopen();
596
+ }
597
+ function acquire(next) {
598
+ if (closed)
599
+ return;
600
+ if (narrowing !== null)
601
+ clearTimeout(narrowing);
602
+ narrowing = null;
603
+ const widening = wider(next, demand);
604
+ if (!widening && !narrower(next, demand))
605
+ return;
606
+ if (widening || !roomsListed) {
607
+ applyDemand(next);
608
+ return;
609
+ }
610
+ // A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
611
+ narrowing = setTimeout(() => {
612
+ narrowing = null;
613
+ applyDemand(next);
614
+ }, 5_000);
615
+ }
616
+ function requiredRooms(reach) {
617
+ if (reach === null)
618
+ return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
619
+ const named = [...new Set(reach.rooms)].sort();
620
+ if (roomsListed)
621
+ return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
622
+ return { listed: named.every((room) => replicas.has(room)), rooms: named };
623
+ }
624
+ function roomState(locator, search) {
625
+ if (sourceRefusals.has(locator))
626
+ return 'refused';
627
+ const listedRoom = discovered.get(locator);
628
+ if (roomsListed &&
629
+ (listedRoom === undefined ||
630
+ listedRoom.replica.admission !== 'admitted' ||
631
+ unreadable.has(locator)))
632
+ return 'unadmitted';
633
+ if (!replicas.has(locator))
634
+ return 'acquiring';
635
+ if (!prepared.has(locator))
636
+ return 'preparing';
637
+ if (search && !searchIndex.rooms().has(locator))
638
+ return 'indexing';
639
+ return 'complete';
640
+ }
641
+ function conversationCoverage(reach, search) {
642
+ const declared = reach === null || reach.conversations;
643
+ if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
644
+ return 'undeclared';
645
+ if (!conversations.known() || !conversations.current())
646
+ return 'acquiring';
647
+ if (!search)
648
+ return 'complete';
649
+ const unindexed = conversations
650
+ .searchable()
651
+ .filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
652
+ if (unindexed.length === 0)
653
+ return 'complete';
654
+ index([], unindexed);
655
+ return 'indexing';
656
+ }
657
+ function connectionOf() {
658
+ if (socketOpen)
659
+ return 'connected';
660
+ if (status.providerUnreachable)
661
+ return 'unreachable';
662
+ return options.adapter.roomStream === undefined && status.lastCompleted !== null
663
+ ? 'connected'
664
+ : 'connecting';
665
+ }
666
+ function refusedIn(locators) {
667
+ return locators.flatMap((room) => {
668
+ const held = sourceRefusals.get(room);
669
+ if (held === undefined)
670
+ return [];
671
+ const { file, location } = held.fields;
672
+ return [
673
+ {
674
+ room,
675
+ file: typeof file === 'string' ? file : '',
676
+ rule: held.code,
677
+ location: typeof location === 'string' ? location : null,
678
+ },
679
+ ];
680
+ });
681
+ }
682
+ function readinessFor(port, reach) {
683
+ const required = requiredRooms(reach);
684
+ const search = port.search === 'required';
685
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
686
+ const building = (state) => coverage
687
+ .filter((room) => room.state === state && !unindexable.has(room.room))
688
+ .map(({ room }) => room);
689
+ const preparing = building('preparing');
690
+ if (preparing.length > 0)
691
+ prepare(preparing);
692
+ const indexing = building('indexing');
693
+ if (indexing.length > 0)
694
+ index(indexing, []);
695
+ return readinessOf({
696
+ code: port.code,
697
+ coverage: {
698
+ listed: required.listed,
699
+ rooms: coverage,
700
+ conversations: conversationCoverage(reach, search),
701
+ search: search ? 'required' : 'unused',
702
+ },
703
+ persistence,
704
+ connection: connectionOf(),
705
+ house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
706
+ capacity: capacity !== null &&
707
+ coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
708
+ ? capacity
709
+ : port.capacity,
710
+ refused: refusedIn(required.rooms),
711
+ lost: storageLoss !== null,
712
+ established: targets[port.target]?.established === true,
713
+ });
714
+ }
715
+ function establish(name) {
716
+ targets = { ...targets, [name]: { established: true } };
717
+ queueReadiness();
718
+ void durably((local) => local.keepTargets(targets)).catch(() => {
719
+ journal.record(`target record failed ${name}`);
720
+ });
721
+ }
722
+ function deliverReadiness(watch) {
723
+ if (!readinessWatches.has(watch))
724
+ return;
725
+ const readiness = readinessFor(watch.port, watch.reach);
726
+ if (readiness.ready && targets[watch.port.target]?.established !== true) {
727
+ establish(watch.port.target);
728
+ }
729
+ const shown = JSON.stringify(readiness);
730
+ if (shown === watch.last)
731
+ return;
732
+ watch.last = shown;
733
+ watch.deliver(structuredClone(readiness));
734
+ }
735
+ function queueReadiness() {
736
+ if (readinessQueued || closed || readinessWatches.size === 0)
737
+ return;
738
+ readinessQueued = true;
739
+ queueMicrotask(() => {
740
+ readinessQueued = false;
741
+ if (closed)
742
+ return;
743
+ for (const watch of [...readinessWatches])
744
+ deliverReadiness(watch);
745
+ });
746
+ }
747
+ function watchReadiness(port, reach, deliver) {
748
+ const watch = { port, reach, deliver, last: null };
749
+ readinessWatches.add(watch);
750
+ void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
751
+ return () => {
752
+ readinessWatches.delete(watch);
753
+ };
754
+ }
755
+ function layerBytes(layer) {
756
+ let bytes = 0;
757
+ for (const [path, text] of Object.entries(layer.documents))
758
+ bytes += (path.length + text.length) * 2;
759
+ return bytes;
760
+ }
761
+ function grownBy(next, previous) {
762
+ if (previous === undefined)
763
+ return layerBytes(next);
764
+ const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
765
+ return layerBytes({ ...next, documents });
766
+ }
767
+ function exceeded(error) {
768
+ return (typeof error === 'object' &&
769
+ error !== null &&
770
+ error.name === 'QuotaExceededError');
771
+ }
772
+ async function estimated() {
773
+ const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
774
+ return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
775
+ }
776
+ function pause(locator, cause) {
777
+ paused.add(locator);
778
+ capacity = cause;
779
+ journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
780
+ queueReadiness();
781
+ if (rechecking !== null)
782
+ return;
783
+ // A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
784
+ rechecking = setInterval(() => {
785
+ void recheck();
786
+ }, 60_000);
787
+ }
788
+ async function recheck() {
789
+ const held = capacity;
790
+ if (held === null || closed)
791
+ return;
792
+ const available = await estimated().catch(() => null);
793
+ if (available === null || available < (held.needed ?? 0))
794
+ return;
795
+ if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
796
+ return;
797
+ resume();
798
+ }
799
+ function resume() {
800
+ if (rechecking !== null)
801
+ clearInterval(rechecking);
802
+ rechecking = null;
803
+ if (paused.size === 0 && capacity === null)
804
+ return;
805
+ paused.clear();
806
+ capacity = null;
807
+ journal.record('capacity resumed');
808
+ queueReadiness();
809
+ if (!closed && authorization !== undefined)
810
+ void sync('unstreamed');
811
+ }
812
+ async function reclaimed(local) {
813
+ const unreferenced = [...transfers.keys()].filter((locator) => {
814
+ const room = discovered.get(locator);
815
+ return room === undefined || !acquires(room);
816
+ });
817
+ for (const locator of unreferenced) {
818
+ await local.abandon(locator);
819
+ transfers.delete(locator);
820
+ }
821
+ if (unreferenced.length > 0)
822
+ journal.record(`capacity reclaimed ${unreferenced.length}`);
823
+ return unreferenced.length > 0;
824
+ }
825
+ async function admitted(local, locator, needed) {
826
+ if (paused.has(locator))
827
+ return false;
828
+ let available = await estimated().catch(() => null);
829
+ if (available === null || available >= needed)
830
+ return true;
831
+ if (await reclaimed(local))
832
+ available = await estimated().catch(() => null);
833
+ if (available === null || available >= needed)
834
+ return true;
835
+ pause(locator, { cause: 'insufficient-space', needed, available });
836
+ return false;
837
+ }
838
+ async function exceededOn(locator, bytes) {
839
+ const available = await estimated().catch(() => null);
840
+ pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
841
+ }
842
+ function seedRoomHeld(room, version) {
843
+ return prepared.has(room) && reachedBy(room, version);
844
+ }
845
+ function seedHeld(at) {
846
+ return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
847
+ }
848
+ function seedApplies(plan) {
849
+ const seed = seeds.get(plan.key);
850
+ if (seed === undefined)
851
+ return null;
852
+ if (!seedHeld(seed.at))
853
+ return seed;
854
+ seeds.delete(plan.key);
855
+ return null;
856
+ }
857
+ function seedBehind(plan, cover) {
858
+ const seed = seedApplies(plan);
859
+ if (seed === null || 'pending' in cover)
860
+ return cover;
861
+ return {
862
+ pending: Object.entries(seed.at)
863
+ .filter(([room, version]) => !seedRoomHeld(room, version))
864
+ .map(([room]) => ({ room, awaits: 'acquisition' })),
865
+ };
866
+ }
867
+ function seed(entry) {
868
+ if (seedHeld(entry.at))
869
+ return;
870
+ for (const { query, rows } of entry.queries) {
871
+ const key = queryKey(query);
872
+ seeds.set(key, { rows, at: entry.at });
873
+ for (const shown of seedWatchers.get(key) ?? [])
874
+ shown();
875
+ }
876
+ }
877
+ async function rowsQuery(plan) {
878
+ const before = covered(plan);
879
+ awaitPreparation(before);
880
+ const seeded = seedApplies(plan);
881
+ if (seeded !== null)
882
+ return seededResult(seeded).result;
883
+ if ('pending' in before)
884
+ return before;
885
+ const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
886
+ awaitPreparation(cover);
887
+ if ('pending' in cover)
888
+ return cover;
889
+ return { ...rows, coverage: cover.coverage };
890
+ }
891
+ async function indexConversation(conversation) {
892
+ searchIndex.indexMessages(conversation, [], false);
893
+ let after = null;
894
+ for (;;) {
895
+ const batch = await durably((local) => local.messagesAfter(conversation, after));
896
+ if (closed)
897
+ return;
898
+ searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
899
+ if (batch.length === 0)
900
+ return;
901
+ after = batch[batch.length - 1].identity;
902
+ }
903
+ }
904
+ function indexNext() {
905
+ const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
906
+ if (room !== undefined) {
907
+ roomsToIndex.delete(room);
908
+ return () => prepared.has(room)
909
+ ? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
910
+ : undefined;
911
+ }
912
+ const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
913
+ if (conversation === undefined)
914
+ return null;
915
+ conversationsToIndex.delete(conversation);
916
+ return () => indexConversation(conversation);
917
+ }
918
+ function index(rooms, named) {
919
+ for (const room of rooms)
920
+ roomsToIndex.add(room);
921
+ for (const conversation of named)
922
+ conversationsToIndex.add(conversation);
923
+ if (indexing)
924
+ return;
925
+ indexing = true;
926
+ void (async () => {
927
+ try {
928
+ for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
929
+ await yielded();
930
+ await unit();
931
+ queueReadiness();
932
+ }
933
+ }
934
+ finally {
935
+ indexing = false;
936
+ }
937
+ })().catch(() => { });
288
938
  }
289
939
  function publishChangeSet(set) {
290
940
  applyChangeSet(set);
291
- if (set.rooms.length > 0)
941
+ if (set.rooms.length > 0 ||
942
+ [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
292
943
  emitRooms();
293
944
  for (const locator of set.queues)
294
945
  emitQueue(locator);
@@ -300,34 +951,7 @@ export function composeRuntime(options) {
300
951
  emitPresentation(app);
301
952
  conversations.changed(set);
302
953
  }
303
- function stopTimer() {
304
- if (resetTimer === undefined)
305
- return;
306
- clearTimeout(resetTimer);
307
- resetTimer = undefined;
308
- }
309
- function armTimer() {
310
- stopTimer();
311
- if (closed || stopping)
312
- return;
313
- let earliest = null;
314
- for (const { submission } of pendings.values()) {
315
- if (submission.state !== 'reset')
316
- continue;
317
- const at = Date.parse(submission.reset);
318
- if (earliest === null || at < earliest)
319
- earliest = at;
320
- }
321
- if (earliest === null)
322
- return;
323
- // A Save House held back goes again only once the reset its answer named has passed.
324
- resetTimer = setTimeout(() => {
325
- resetTimer = undefined;
326
- submitAll();
327
- }, Math.max(0, earliest - Date.now()));
328
- }
329
954
  async function refuse(reason, provider = null) {
330
- stopTimer();
331
955
  discoveryPaused = true;
332
956
  try {
333
957
  if (reason === 'revoked-authorization') {
@@ -335,10 +959,7 @@ export function composeRuntime(options) {
335
959
  const revoked = { status: 'unavailable', reason: 'revoked' };
336
960
  try {
337
961
  const local = await openLocal();
338
- await local.revokeModels([...replicas.values()].map((replica) => ({
339
- record: replica.confirmed.record,
340
- queue: replica.queue,
341
- })));
962
+ await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
342
963
  }
343
964
  catch {
344
965
  journal.record('model revocation persistence failed');
@@ -362,10 +983,7 @@ export function composeRuntime(options) {
362
983
  model: revoked,
363
984
  selectionModel: revoked,
364
985
  };
365
- const read = readConfirmed(record, before.confirmed);
366
- replicas.set(locator, read.kind === 'staged'
367
- ? replicaOf(read.confirmed, before.queue, before)
368
- : { ...before, confirmed: { ...before.confirmed, record } });
986
+ replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
369
987
  }
370
988
  rooms.set(locator, {
371
989
  ...room,
@@ -382,7 +1000,8 @@ export function composeRuntime(options) {
382
1000
  }
383
1001
  finally {
384
1002
  refusal = {
385
- reason,
1003
+ code: reason,
1004
+ retryable: false,
386
1005
  reauthorizationOffered: reason === 'revoked-authorization',
387
1006
  refusal: provider,
388
1007
  };
@@ -407,19 +1026,31 @@ export function composeRuntime(options) {
407
1026
  },
408
1027
  providerOwnedPaths: room.providerOwnedPaths,
409
1028
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
410
- attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
411
1029
  synchronizationAvailable,
412
1030
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
413
1031
  });
414
1032
  }
415
- async function applyDiscovery(listed, local) {
1033
+ async function applyDiscovery(found, started) {
1034
+ for (const room of found) {
1035
+ const fence = fences.get(room.locator);
1036
+ if (fence !== undefined && fence.erased && fence.at < started)
1037
+ fences.delete(room.locator);
1038
+ }
1039
+ await loseRooms(found
1040
+ .filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
1041
+ .map((room) => room.locator));
1042
+ const listed = found.filter((room) => !fences.has(room.locator));
416
1043
  const listedLocators = new Set(listed.map((room) => room.locator));
417
1044
  for (const locator of listedLocators)
418
1045
  scopedRefusals.delete(locator);
419
1046
  discovered.clear();
420
1047
  for (const room of listed)
421
1048
  discovered.set(room.locator, room);
422
- await dropUnlisted(listedLocators, local);
1049
+ const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
1050
+ if (options.adapter.exhaustiveDiscovery === true)
1051
+ await loseRooms(unlisted);
1052
+ else
1053
+ unlistedKept(unlisted);
423
1054
  for (const room of listed) {
424
1055
  const held = replicas.get(room.locator)?.confirmed.record;
425
1056
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -430,43 +1061,91 @@ export function composeRuntime(options) {
430
1061
  }
431
1062
  roomsListed = true;
432
1063
  emitRooms();
1064
+ return listed;
433
1065
  }
434
- function unlistedHeld(listed) {
435
- return [...new Set([...rooms.keys(), ...roomDrafts.keys()])].filter((locator) => !listed.has(locator));
1066
+ function heldLocally(locator) {
1067
+ return (replicas.has(locator) ||
1068
+ roomDrafts.has(locator) ||
1069
+ transfers.has(locator) ||
1070
+ queuesWithoutReplica.has(locator));
436
1071
  }
437
- async function dropUnlisted(listed, local) {
438
- if (unlistedHeld(listed).length === 0)
439
- return;
440
- await exclusive(async () => {
441
- const unlisted = unlistedHeld(listed);
442
- await local.dropRooms(unlisted);
443
- const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
444
- room: true,
445
- queue: true,
446
- drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
447
- }));
448
- for (const locator of unlisted) {
449
- for (const batch of replicas.get(locator)?.queue ?? [])
450
- pendings.delete(batch.id);
451
- replicas.delete(locator);
1072
+ function unlistedHeld(listed) {
1073
+ return [
1074
+ ...new Set([
1075
+ ...rooms.keys(),
1076
+ ...roomDrafts.keys(),
1077
+ ...transfers.keys(),
1078
+ ...queuesWithoutReplica.keys(),
1079
+ ]),
1080
+ ].filter((locator) => !listed.has(locator));
1081
+ }
1082
+ function unlistedKept(unlisted) {
1083
+ for (const locator of unlisted) {
1084
+ const held = replicas.get(locator)?.confirmed.record;
1085
+ if (held !== undefined)
1086
+ list(held, false);
1087
+ else
452
1088
  rooms.delete(locator);
453
- roomDrafts.delete(locator);
454
- unreadable.delete(locator);
455
- stale.delete(locator);
456
- scopedRefusals.delete(locator);
457
- sourceRefusals.delete(locator);
458
- declarationRefusals.delete(locator);
459
- for (const attached of ['collection', 'typed-selection']) {
460
- const key = modelKey(locator, attached);
461
- models.delete(key);
462
- liveModels.delete(key);
463
- invalidModels.delete(key);
464
- }
465
- journal.record(`room unlisted ${locator}`);
1089
+ }
1090
+ }
1091
+ function fence(locators) {
1092
+ const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1093
+ const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
1094
+ room: true,
1095
+ queue: true,
1096
+ drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
1097
+ }));
1098
+ for (const locator of fresh) {
1099
+ fences.set(locator, { at: discoveries, erased: false });
1100
+ lastFenced.set(locator, ++fencings);
1101
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1102
+ pendings.delete(batch.id);
1103
+ replicas.delete(locator);
1104
+ queuesWithoutReplica.delete(locator);
1105
+ rooms.delete(locator);
1106
+ roomDrafts.delete(locator);
1107
+ transfers.delete(locator);
1108
+ catching.delete(locator);
1109
+ discovered.delete(locator);
1110
+ announced.delete(locator);
1111
+ unreadable.delete(locator);
1112
+ stale.delete(locator);
1113
+ repairable.delete(locator);
1114
+ unindexable.delete(locator);
1115
+ paused.delete(locator);
1116
+ scopedRefusals.delete(locator);
1117
+ sourceRefusals.delete(locator);
1118
+ declarationRefusals.delete(locator);
1119
+ for (const attached of ['collection', 'typed-selection']) {
1120
+ const key = modelKey(locator, attached);
1121
+ models.delete(key);
1122
+ liveModels.delete(key);
1123
+ invalidModels.delete(key);
466
1124
  }
467
- publishChangeSet(mergeChangeSets(removed));
468
- moveTo({ ...status, ...attention() });
469
- });
1125
+ journal.record(`room lost ${locator}`);
1126
+ }
1127
+ if (fresh.length === 0)
1128
+ return fresh;
1129
+ publishChangeSet(mergeChangeSets(removed));
1130
+ for (const locator of fresh)
1131
+ prepared.delete(locator);
1132
+ searchIndex.forget(fresh);
1133
+ readSide.recover();
1134
+ moveTo({ ...status, ...attention() });
1135
+ return fresh;
1136
+ }
1137
+ async function erased(local, locators) {
1138
+ await local.erase(locators);
1139
+ for (const locator of locators) {
1140
+ const held = fences.get(locator);
1141
+ if (held !== undefined)
1142
+ held.erased = true;
1143
+ }
1144
+ }
1145
+ async function loseRooms(locators) {
1146
+ const fresh = fence(locators);
1147
+ if (fresh.length > 0)
1148
+ await durably((local) => erased(local, fresh));
470
1149
  }
471
1150
  function restored(key, persisted) {
472
1151
  const prior = models.get(key);
@@ -487,7 +1166,7 @@ export function composeRuntime(options) {
487
1166
  return model;
488
1167
  }
489
1168
  async function hydrate(opened) {
490
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
1169
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
491
1170
  opened.all(),
492
1171
  opened.queues(),
493
1172
  opened.roomDrafts(),
@@ -497,9 +1176,17 @@ export function composeRuntime(options) {
497
1176
  opened.lastCompleted(),
498
1177
  opened.providerLimits(),
499
1178
  opened.capabilities(),
1179
+ opened.member(),
1180
+ opened.transfers(),
1181
+ opened.targets(),
500
1182
  ]);
1183
+ targets = heldTargets;
1184
+ transfers.clear();
1185
+ for (const record of staged)
1186
+ transfers.set(record.locator, record);
501
1187
  limits = declared;
502
1188
  capabilities = named;
1189
+ member = known;
503
1190
  roomDrafts.clear();
504
1191
  for (const record of kept)
505
1192
  rememberRoomDraft(record);
@@ -515,18 +1202,17 @@ export function composeRuntime(options) {
515
1202
  }
516
1203
  const previousReplicas = new Map(replicas);
517
1204
  replicas.clear();
1205
+ queuesWithoutReplica.clear();
1206
+ const replicated = new Set(records.map((record) => record.locator));
1207
+ for (const [locator, queue] of queuedBatches) {
1208
+ if (!replicated.has(locator))
1209
+ queuesWithoutReplica.set(locator, queue);
1210
+ }
518
1211
  for (const record of records) {
519
1212
  const model = restored(modelKey(record.locator, 'collection'), record.model);
520
1213
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
521
1214
  const prior = previousReplicas.get(record.locator);
522
- const retyped = { ...record, model, selectionModel };
523
- const read = readConfirmed(retyped, prior?.confirmed);
524
- const confirmed = read.kind === 'staged'
525
- ? read.confirmed
526
- : {
527
- record: retyped,
528
- collection: parseCollection(retyped.documents, compositionOf(retyped)),
529
- };
1215
+ const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
530
1216
  replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
531
1217
  const existing = rooms.get(record.locator);
532
1218
  if (existing === undefined)
@@ -535,7 +1221,6 @@ export function composeRuntime(options) {
535
1221
  rooms.set(record.locator, {
536
1222
  ...existing,
537
1223
  roomVersion: record.roomVersion,
538
- attachments: record.attachments,
539
1224
  operations: record.operations,
540
1225
  replica: record.replica,
541
1226
  model: modelTypes(model),
@@ -547,10 +1232,6 @@ export function composeRuntime(options) {
547
1232
  },
548
1233
  });
549
1234
  }
550
- if (read.kind === 'refused') {
551
- refusedSource(record, read);
552
- declarationRefusals.add(record.locator);
553
- }
554
1235
  }
555
1236
  return {
556
1237
  lastCompleted,
@@ -561,16 +1242,26 @@ export function composeRuntime(options) {
561
1242
  async function readState(opened) {
562
1243
  const { lastCompleted, locators, read } = await hydrate(opened);
563
1244
  applyChangeSet(read);
1245
+ prepare();
564
1246
  if (replicas.size > 0)
565
1247
  emitRooms();
1248
+ if (member !== null)
1249
+ emitMember();
566
1250
  for (const locator of locators)
567
1251
  emitQueue(locator);
568
1252
  publishRoomDrafts(false);
569
1253
  return lastCompleted;
570
1254
  }
571
1255
  async function open() {
572
- const opened = await openRuntimeStore();
1256
+ if (closed)
1257
+ throw new Error('runtime closed');
1258
+ const opened = await openRuntimeStore(isFenced);
573
1259
  store = opened;
1260
+ const interrupted = await opened.fences();
1261
+ for (const locator of interrupted) {
1262
+ fences.set(locator, { at: discoveries, erased: false });
1263
+ lastFenced.set(locator, ++fencings);
1264
+ }
574
1265
  const kept = options.shell.generation;
575
1266
  const storedGeneration = await opened.generation();
576
1267
  const loss = detectStorageLoss(storedGeneration, kept);
@@ -584,6 +1275,8 @@ export function composeRuntime(options) {
584
1275
  emitStorageLoss();
585
1276
  }
586
1277
  const lastCompleted = await readState(opened);
1278
+ if (interrupted.length > 0)
1279
+ await erased(opened, interrupted);
587
1280
  moveTo({ ...status, lastCompleted, ...attention() });
588
1281
  return opened;
589
1282
  }
@@ -610,10 +1303,16 @@ export function composeRuntime(options) {
610
1303
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
611
1304
  }
612
1305
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
613
- const withdrawn = withdrawals(confirmed, queue, withdrawable);
614
- const kept = queue.filter((batch) => !withdrawn.includes(batch));
1306
+ const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
1307
+ const { withdrawn, kept } = standing(confirmed, queue, withdrawable, localSaves);
1308
+ const founded = kept.filter((batch) => !queue.includes(batch));
1309
+ const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
1310
+ const unqueued = withdrawn.map((batch) => ({
1311
+ batch,
1312
+ returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
1313
+ }));
615
1314
  const returning = [
616
- ...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
1315
+ ...[...unqueued].reverse(),
617
1316
  ...(settling.leaving === undefined ? [] : [settling.leaving]),
618
1317
  ];
619
1318
  const written = new Map();
@@ -631,27 +1330,49 @@ export function composeRuntime(options) {
631
1330
  });
632
1331
  }
633
1332
  }
634
- await local.write({
635
- next: confirmed.record,
636
- previous: before?.confirmed.record ?? null,
637
- queue: kept,
638
- previousQueue: before?.queue ?? [],
639
- roomDrafts: [...written.values()],
640
- });
641
- for (const batch of before?.queue ?? []) {
642
- if (!kept.includes(batch))
1333
+ await local.keepAuthored({ locator, queue: kept, founded, roomDrafts: [...written.values()] });
1334
+ const staying = new Set(kept.map((batch) => batch.id));
1335
+ for (const batch of previousQueue) {
1336
+ if (!staying.has(batch.id))
643
1337
  pendings.delete(batch.id);
644
1338
  }
645
1339
  for (const record of written.values())
646
1340
  rememberRoomDraft(record);
647
- for (const batch of withdrawn)
648
- journal.record(`outcome ${locator} ${batch.id} withdrawn`);
649
- replicas.set(locator, replicaOf(confirmed, kept, before));
650
- return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
651
- room: settling.room,
652
- queue: kept.length !== (before?.queue.length ?? 0),
653
- drafts: [...written.keys()],
654
- });
1341
+ try {
1342
+ await local.write({
1343
+ next: confirmed.record,
1344
+ previous: before?.confirmed.record ?? null,
1345
+ ...(settling.stored === undefined ? {} : { stored: settling.stored }),
1346
+ });
1347
+ }
1348
+ catch (error) {
1349
+ const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
1350
+ if (held !== null)
1351
+ replicas.set(locator, held);
1352
+ else if (kept.length > 0)
1353
+ queuesWithoutReplica.set(locator, kept);
1354
+ else
1355
+ queuesWithoutReplica.delete(locator);
1356
+ publishChangeSet(changeSet(locator, before ?? null, held, {
1357
+ queue: queueChanged,
1358
+ drafts: [...written.keys()],
1359
+ }));
1360
+ moveTo({ ...status, ...attention() });
1361
+ throw error;
1362
+ }
1363
+ return () => {
1364
+ queuesWithoutReplica.delete(locator);
1365
+ for (const { batch, returned } of unqueued) {
1366
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
1367
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1368
+ }
1369
+ replicas.set(locator, replicaOf(confirmed, kept, before));
1370
+ return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
1371
+ room: settling.room,
1372
+ queue: queueChanged,
1373
+ drafts: [...written.keys()],
1374
+ });
1375
+ };
655
1376
  }
656
1377
  function adoptModels(locator, record) {
657
1378
  for (const [attached, model] of [
@@ -684,10 +1405,16 @@ export function composeRuntime(options) {
684
1405
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
685
1406
  };
686
1407
  }
687
- function publish(local, room, confirmed, base) {
1408
+ function publish(local, room, confirmed, landing, live) {
1409
+ const { base, stored } = landing;
688
1410
  return exclusive(async () => {
689
- if (!continuing())
1411
+ if (stored !== undefined) {
1412
+ await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
1413
+ }
1414
+ if (!live()) {
1415
+ caughtUpTo(room.locator);
690
1416
  return true;
1417
+ }
691
1418
  const prior = replicas.get(room.locator);
692
1419
  if (base !== undefined &&
693
1420
  (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
@@ -695,47 +1422,85 @@ export function composeRuntime(options) {
695
1422
  return false;
696
1423
  }
697
1424
  adoptModels(room.locator, confirmed.record);
698
- const queue = prior?.queue ?? [];
1425
+ const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
699
1426
  const roomVersion = confirmed.record.roomVersion;
700
1427
  const accepted = queue.filter((batch) => {
701
1428
  const accepted = pendings.get(batch.id)?.accepted;
702
1429
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
703
1430
  });
704
- const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
1431
+ let settled;
1432
+ try {
1433
+ settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
1434
+ }
1435
+ catch (error) {
1436
+ if (!exceeded(error))
1437
+ throw error;
1438
+ await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
1439
+ caughtUpTo(room.locator);
1440
+ return true;
1441
+ }
1442
+ const set = settled();
1443
+ if (stored !== undefined) {
1444
+ transfers.delete(room.locator);
1445
+ catching.delete(room.locator);
1446
+ }
705
1447
  for (const batch of accepted)
706
1448
  pendings.delete(batch.id);
707
1449
  stale.delete(room.locator);
1450
+ unindexable.delete(room.locator);
708
1451
  if (unreadable.delete(room.locator))
709
1452
  scopedRefusals.delete(room.locator);
710
1453
  sourceRefusals.delete(room.locator);
711
1454
  declarationRefusals.delete(room.locator);
712
1455
  list(room, true);
1456
+ if (prior === undefined)
1457
+ prepared.add(room.locator);
713
1458
  publishChangeSet(set);
714
1459
  moveTo({ ...status, ...attention() });
1460
+ options.adapter.roomStream?.published(room.locator, roomVersion);
715
1461
  return true;
716
1462
  });
717
1463
  }
718
1464
  async function saveRoomDrafts(locator, named, writtenBy) {
719
1465
  const local = await openLocal();
1466
+ await referenced(locator);
720
1467
  const replica = replicas.get(locator);
721
1468
  const room = rooms.get(locator);
722
1469
  if (replica === undefined || room === undefined) {
723
- throw new Error(`No Local Replica for ${locator}`);
1470
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
724
1471
  }
725
1472
  const saved = [];
726
1473
  for (const { path, revision } of named) {
727
1474
  const record = roomDrafts.get(locator)?.get(path);
728
1475
  if (record === undefined || record.revision !== revision) {
729
- return { outcome: 'refused', rule: 'stale', file: path };
1476
+ return { outcome: 'refused', code: 'stale', retryable: false, file: path };
730
1477
  }
731
1478
  if (!saved.some((draft) => draft.path === path))
732
1479
  saved.push({ path, parts: record.parts });
733
1480
  }
734
1481
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
735
1482
  if (changed.length > 0)
736
- return { outcome: 'refused', rule: 'changed-underneath', parts: changed };
1483
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
737
1484
  const parts = saved.map(({ path, parts }) => changesOf(path, parts));
738
- const built = buildBatch({
1485
+ const changes = parts.flatMap(({ changes }) => changes);
1486
+ const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
1487
+ if (requeue.kind === 'refused')
1488
+ return { outcome: 'refused', ...requeue.refusal };
1489
+ const foreign = [...requeue.withdrawn.keys()].some((origin) => {
1490
+ const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
1491
+ return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
1492
+ });
1493
+ if (foreign) {
1494
+ return {
1495
+ outcome: 'refused',
1496
+ code: 'unknown-revision',
1497
+ retryable: false,
1498
+ file: [...requeue.queued].at(-1),
1499
+ };
1500
+ }
1501
+ const withdrawing = requeue.withdrawn.size > 0;
1502
+ const input = {
1503
+ local: localSaves,
739
1504
  baseRoomVersion: replica.confirmed.record.roomVersion,
740
1505
  composition: compositionFor(replica.confirmed.record),
741
1506
  operations: room.operations,
@@ -743,29 +1508,97 @@ export function composeRuntime(options) {
743
1508
  providerLimits: limits,
744
1509
  handle: room.handle,
745
1510
  visible: replica.visible,
746
- changes: parts.flatMap(({ changes }) => changes),
1511
+ changes,
747
1512
  bases: parts.flatMap(({ bases }) => bases),
1513
+ queued: queuedDocuments(replica.queue),
748
1514
  writtenBy,
1515
+ clean: {
1516
+ text: replica.confirmed.record.documents,
1517
+ binaries: replica.confirmed.record.binaries,
1518
+ composition: compositionOf(replica.confirmed.record),
1519
+ },
1520
+ };
1521
+ const checked = withdrawing
1522
+ ? buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
1523
+ : null;
1524
+ if (checked?.kind === 'refused')
1525
+ return { outcome: 'refused', ...checked.refusal };
1526
+ const owners = [...requeue.owners].flatMap((origin) => {
1527
+ const batch = requeue.withdrawn.get(origin);
1528
+ return batch === undefined || batch === null ? [] : [{ origin, batch }];
749
1529
  });
750
- if (built.kind === 'refused')
1530
+ if (owners.length > 1) {
1531
+ return {
1532
+ outcome: 'refused',
1533
+ code: 'unknown-revision',
1534
+ retryable: false,
1535
+ file: [...requeue.queued].at(-1),
1536
+ };
1537
+ }
1538
+ const built = !withdrawing
1539
+ ? buildBatch(input)
1540
+ : requeue.changes.length === 0
1541
+ ? null
1542
+ : buildBatch({
1543
+ ...input,
1544
+ visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
1545
+ changes: requeue.changes,
1546
+ queued: queuedDocuments(requeue.queue),
1547
+ });
1548
+ if (built?.kind === 'refused')
751
1549
  return { outcome: 'refused', ...built.refusal };
752
- const batches = [...replica.queue, built.batch];
1550
+ const [owner] = owners;
1551
+ const join = built === null || owner === undefined
1552
+ ? null
1553
+ : joined(requeue.queue, owner.batch, built.batch);
1554
+ if (join?.kind === 'refused')
1555
+ return { outcome: 'refused', ...join.refusal };
1556
+ const withdrawn = new Map(requeue.withdrawn);
1557
+ if (join !== null && owner !== undefined)
1558
+ withdrawn.set(owner.origin, join.batch);
1559
+ const drafted = new Map();
1560
+ for (const [origin, replaced] of withdrawn) {
1561
+ if (replaced === null)
1562
+ continue;
1563
+ const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
1564
+ drafted.set(replaced.id, replaced === join?.batch
1565
+ ? [
1566
+ ...carried.map((held) => joinedDraft(held, saved)),
1567
+ ...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
1568
+ ]
1569
+ : carried);
1570
+ }
1571
+ if (built !== null && join === null)
1572
+ drafted.set(built.batch.id, saved);
1573
+ const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
1574
+ for (const replaced of withdrawn.values()) {
1575
+ if (replaced === null || !batches.includes(replaced))
1576
+ continue;
1577
+ const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
1578
+ if (limited !== null)
1579
+ return { outcome: 'refused', ...limited };
1580
+ }
753
1581
  const consumed = saved.map(({ path }) => path);
754
1582
  try {
755
- await local.write({
756
- next: replica.confirmed.record,
757
- previous: replica.confirmed.record,
1583
+ await local.keepAuthored({
1584
+ locator,
758
1585
  queue: batches,
759
- previousQueue: replica.queue,
760
- saved,
1586
+ saved: drafted,
761
1587
  consumedRoomDrafts: consumed,
762
1588
  });
763
1589
  }
764
1590
  catch {
765
- return { outcome: 'refused', rule: 'durable-write' };
1591
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
1592
+ }
1593
+ if (fences.has(locator))
1594
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1595
+ for (const origin of withdrawn.keys())
1596
+ pendings.delete(origin);
1597
+ for (const [id, drafts] of drafted) {
1598
+ pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
766
1599
  }
767
- pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
768
- recorded = false;
1600
+ if (!localSaves)
1601
+ recorded = false;
769
1602
  for (const path of consumed)
770
1603
  forgetRoomDraft(locator, path);
771
1604
  replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
@@ -773,9 +1606,12 @@ export function composeRuntime(options) {
773
1606
  queue: true,
774
1607
  drafts: consumed,
775
1608
  }));
776
- moveTo({ ...saving(status), ...attention() });
1609
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
777
1610
  submitLater(locator);
778
- return { outcome: 'saved', identifier: built.batch.id };
1611
+ const identifier = join?.batch.id ??
1612
+ built?.batch.id ??
1613
+ [...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
1614
+ return { outcome: 'saved', identifier };
779
1615
  }
780
1616
  async function mark(local, locator, identifier, submission, lost) {
781
1617
  const state = pendings.get(identifier);
@@ -785,6 +1621,8 @@ export function composeRuntime(options) {
785
1621
  return;
786
1622
  }
787
1623
  await local.submit(locator, identifier, submission, lost);
1624
+ if (fences.has(locator))
1625
+ return;
788
1626
  pendings.set(identifier, { ...state, submission, lost });
789
1627
  const replica = replicas.get(locator);
790
1628
  if (replica !== undefined)
@@ -795,22 +1633,24 @@ export function composeRuntime(options) {
795
1633
  if (!continuing())
796
1634
  throw new Error('connection refused');
797
1635
  await local.keepUpload(locator, identifier, upload);
1636
+ if (fences.has(locator))
1637
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
798
1638
  pendings.set(identifier, { ...pendings.get(identifier), upload });
799
1639
  if (!continuing())
800
1640
  throw new Error('connection refused');
801
1641
  }
802
1642
  async function readHistory(locator, path, query = {}) {
803
1643
  if (closed || refusal !== null)
804
- return { kind: 'transport-uncertainty' };
1644
+ return UNCERTAIN;
805
1645
  try {
806
1646
  await ensureAuthorization();
807
1647
  }
808
1648
  catch {
809
- return { kind: 'transport-uncertainty' };
810
- }
811
- if (closed || authorization === undefined || refusal !== null) {
812
- return { kind: 'transport-uncertainty' };
1649
+ return UNCERTAIN;
813
1650
  }
1651
+ if (closed || authorization === undefined || refusal !== null)
1652
+ return UNCERTAIN;
1653
+ const since = fencings;
814
1654
  const transfer = ['transfer', 'history', locator, path];
815
1655
  if (query.revision !== undefined)
816
1656
  transfer.push(query.revision);
@@ -825,6 +1665,8 @@ export function composeRuntime(options) {
825
1665
  ...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
826
1666
  });
827
1667
  journal.record(`outcome history ${result.kind}`);
1668
+ if (fencedSince(locator, since))
1669
+ return failure({ kind: 'room-not-readable', locator });
828
1670
  if (result.kind === 'scoped-refusal') {
829
1671
  const room = replicas.get(result.locator)?.confirmed.record;
830
1672
  if (room !== undefined)
@@ -832,12 +1674,125 @@ export function composeRuntime(options) {
832
1674
  }
833
1675
  if (result.kind === 'revoked-authorization')
834
1676
  await refuse('revoked-authorization');
835
- return result;
1677
+ return historyAnswer(result);
836
1678
  }
837
1679
  catch {
838
1680
  journal.record('outcome history transport-uncertainty');
839
- return { kind: 'transport-uncertainty' };
1681
+ return UNCERTAIN;
1682
+ }
1683
+ }
1684
+ function queuedBlob(locator, path, localId) {
1685
+ for (const batch of replicas.get(locator)?.queue ?? []) {
1686
+ const change = batch.changes[queuedFile(batch, path, localId)];
1687
+ if (change?.kind === 'binary')
1688
+ return change.blob;
840
1689
  }
1690
+ return null;
1691
+ }
1692
+ function binaryRoom(request) {
1693
+ if (request.kind === 'path')
1694
+ return request.locator;
1695
+ const scope = options.adapter.referrerScope(request.referrer);
1696
+ return scope?.kind === 'room' ? scope.locator : null;
1697
+ }
1698
+ async function transferBinary(request, transfer, answer) {
1699
+ if (closed || refusal !== null)
1700
+ return UNCERTAIN;
1701
+ try {
1702
+ await ensureAuthorization();
1703
+ }
1704
+ catch {
1705
+ return UNCERTAIN;
1706
+ }
1707
+ if (closed || authorization === undefined || refusal !== null)
1708
+ return UNCERTAIN;
1709
+ const since = fencings;
1710
+ journal.record(request.kind === 'path'
1711
+ ? `transfer binary ${request.locator} ${request.path}`
1712
+ : `transfer binary ${request.fileRef}`);
1713
+ try {
1714
+ const result = await transfer(authorization);
1715
+ const settled = result;
1716
+ journal.record(`outcome binary ${settled.kind}`);
1717
+ const locator = binaryRoom(request);
1718
+ if (locator !== null && fencedSince(locator, since))
1719
+ return failure({ kind: 'room-not-readable', locator });
1720
+ if (settled.kind === 'scoped-refusal') {
1721
+ const room = replicas.get(settled.locator)?.confirmed.record;
1722
+ if (room !== undefined)
1723
+ await scoped(settled, room, await openLocal());
1724
+ }
1725
+ if (settled.kind === 'revoked-authorization')
1726
+ await refuse('revoked-authorization');
1727
+ return answer(result);
1728
+ }
1729
+ catch {
1730
+ journal.record('outcome binary transport-uncertainty');
1731
+ return UNCERTAIN;
1732
+ }
1733
+ }
1734
+ function readBinary(request) {
1735
+ return transferBinary(request, (held) => options.adapter.binary(held, request), binaryAnswer);
1736
+ }
1737
+ function binaryPath(locator, path, revision) {
1738
+ if (revision !== undefined)
1739
+ return { kind: 'path', locator, path, revision };
1740
+ const visible = replicas.get(locator)?.visible;
1741
+ const held = visible?.binaries[path];
1742
+ if (visible === undefined || held === undefined)
1743
+ return null;
1744
+ if (held.state === 'pending')
1745
+ return { kind: 'pending', localId: held.localId };
1746
+ const record = binaryRecord(path, held, visible.revisions[path]);
1747
+ if (record.state !== 'accepted')
1748
+ return null;
1749
+ const replica = replicas.get(locator);
1750
+ const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
1751
+ const holds = (at) => {
1752
+ const kept = replica?.confirmed.record.binaries[at];
1753
+ return (kept?.state === 'accepted' &&
1754
+ kept.fileRef === record.fileRef &&
1755
+ replica?.confirmed.record.revisions[at] === record.revision);
1756
+ };
1757
+ let confirmed = path;
1758
+ for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
1759
+ const change = queued[index];
1760
+ if (change.kind === 'move' && change.to === confirmed)
1761
+ confirmed = change.path;
1762
+ }
1763
+ return {
1764
+ kind: 'path',
1765
+ locator,
1766
+ path: confirmed,
1767
+ revision: record.revision,
1768
+ fileRef: record.fileRef,
1769
+ };
1770
+ }
1771
+ async function readBinaryPath(locator, path, revision) {
1772
+ await ready;
1773
+ const asked = binaryPath(locator, path, revision);
1774
+ if (asked === null)
1775
+ return absentBinary(path);
1776
+ if (asked.kind === 'pending') {
1777
+ const blob = queuedBlob(locator, path, asked.localId);
1778
+ return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1779
+ }
1780
+ return readBinary(asked);
1781
+ }
1782
+ async function linkBinaryPath(locator, path, revision) {
1783
+ await ready;
1784
+ const asked = binaryPath(locator, path, revision);
1785
+ if (asked === null || asked.kind === 'pending')
1786
+ return absentBinary(path);
1787
+ return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
1788
+ }
1789
+ async function readBinaryReference(reference) {
1790
+ await ready;
1791
+ return readBinary({
1792
+ kind: 'reference',
1793
+ fileRef: reference.fileRef,
1794
+ referrer: reference.referrer,
1795
+ });
841
1796
  }
842
1797
  function declaredAgents() {
843
1798
  for (const capability of capabilities) {
@@ -855,21 +1810,21 @@ export function composeRuntime(options) {
855
1810
  }
856
1811
  return undefined;
857
1812
  }
858
- async function invokeAction(name, input) {
1813
+ async function invokeAction(name, input, dispatching) {
859
1814
  await ready;
860
1815
  if (closed || refusal !== null)
861
- return { kind: 'transport-uncertainty' };
1816
+ return UNCERTAIN;
862
1817
  if (declaredAction(name) === undefined)
863
- return { kind: 'undeclared' };
1818
+ return undeclared(name);
864
1819
  try {
865
1820
  await ensureAuthorization();
866
1821
  }
867
1822
  catch {
868
- return { kind: 'transport-uncertainty' };
869
- }
870
- if (closed || authorization === undefined || refusal !== null) {
871
- return { kind: 'transport-uncertainty' };
1823
+ return UNCERTAIN;
872
1824
  }
1825
+ if (closed || authorization === undefined || refusal !== null)
1826
+ return UNCERTAIN;
1827
+ await dispatching?.();
873
1828
  journal.record(`transfer action ${name}`);
874
1829
  try {
875
1830
  const result = await options.adapter.action(authorization, { name, input });
@@ -883,12 +1838,66 @@ export function composeRuntime(options) {
883
1838
  journal.record(`outcome action ${result.kind}`);
884
1839
  if (result.kind === 'rejected')
885
1840
  journal.record(`refusal rejected ${result.code}`);
1841
+ return calledResult(result);
1842
+ }
1843
+ catch {
1844
+ journal.record('outcome action transport-uncertainty');
1845
+ return UNCERTAIN;
1846
+ }
1847
+ }
1848
+ function unsettled(result) {
1849
+ return result.kind !== 'action' && result.retryable;
1850
+ }
1851
+ async function recoveredAction(name, input) {
1852
+ const operation = operationOf(input);
1853
+ if (operation === null)
1854
+ return invokeAction(name, input);
1855
+ const kept = async () => (await openLocal()).keepCall({ operation, name, input });
1856
+ calling.add(operation);
1857
+ try {
1858
+ let result = await invokeAction(name, input, kept);
1859
+ if (unsettled(result)) {
1860
+ await recovery.attempt(`action ${operation}`, async () => {
1861
+ if (closed || refusal !== null)
1862
+ return 'done';
1863
+ if (!socketOpen)
1864
+ return 'again';
1865
+ result = await invokeAction(name, input, kept);
1866
+ return unsettled(result) ? 'again' : 'done';
1867
+ }, { again: true });
1868
+ }
1869
+ if (!unsettled(result))
1870
+ await (await openLocal()).dropCall(operation);
886
1871
  return result;
887
1872
  }
888
- catch {
889
- journal.record('outcome action transport-uncertainty');
890
- return { kind: 'transport-uncertainty' };
1873
+ finally {
1874
+ calling.delete(operation);
1875
+ }
1876
+ }
1877
+ async function resendCalls(signal) {
1878
+ if (!socketOpen || closed || stopping)
1879
+ return 'done';
1880
+ const local = await openLocal();
1881
+ for (const held of await local.calls()) {
1882
+ if (calling.has(held.operation))
1883
+ continue;
1884
+ if (closed || signal.aborted)
1885
+ return 'done';
1886
+ if (pastHorizon(held.operation)) {
1887
+ await local.dropCall(held.operation);
1888
+ continue;
1889
+ }
1890
+ calling.add(held.operation);
1891
+ try {
1892
+ if (unsettled(await invokeAction(held.name, held.input)))
1893
+ return 'again';
1894
+ await local.dropCall(held.operation);
1895
+ }
1896
+ finally {
1897
+ calling.delete(held.operation);
1898
+ }
891
1899
  }
1900
+ return 'done';
892
1901
  }
893
1902
  function continuing() {
894
1903
  return !closed && !discoveryPaused;
@@ -904,11 +1913,12 @@ export function composeRuntime(options) {
904
1913
  options.adapter.reached(locator, written, roomVersion) &&
905
1914
  !options.adapter.reached(locator, roomVersion, written));
906
1915
  }
907
- async function pullRefresh(authorized, confirmed) {
908
- if (!continuing())
1916
+ async function pullRefresh(authorized, confirmed, live) {
1917
+ if (!live())
909
1918
  return unconfirmed;
1919
+ const turn = generation;
910
1920
  try {
911
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
1921
+ const result = await refreshRoom(reads, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
912
1922
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
913
1923
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
914
1924
  return result;
@@ -917,29 +1927,101 @@ export function composeRuntime(options) {
917
1927
  journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
918
1928
  throw error;
919
1929
  }
1930
+ finally {
1931
+ reads.finished(confirmed.record.locator, turn);
1932
+ }
920
1933
  }
921
- async function pullBootstrap(authorized, room) {
922
- if (!continuing())
1934
+ function staging(local, live) {
1935
+ return {
1936
+ held: (locator) => transfers.get(locator) ?? null,
1937
+ staged: (record) => local.staged(record),
1938
+ stage: (record, page) => exclusive(async () => {
1939
+ if (!live())
1940
+ return false;
1941
+ const bytes = layerBytes(page);
1942
+ if (!(await admitted(local, record.locator, bytes)))
1943
+ return false;
1944
+ try {
1945
+ await local.stage(record, page);
1946
+ }
1947
+ catch (error) {
1948
+ if (!exceeded(error))
1949
+ throw error;
1950
+ await exceededOn(record.locator, bytes);
1951
+ return false;
1952
+ }
1953
+ transfers.set(record.locator, record);
1954
+ readSide.recover();
1955
+ return true;
1956
+ }),
1957
+ abandon: (locator) => exclusive(async () => {
1958
+ if (!live())
1959
+ return;
1960
+ await local.abandon(locator);
1961
+ transfers.delete(locator);
1962
+ readSide.recover();
1963
+ }),
1964
+ };
1965
+ }
1966
+ function reusable(room) {
1967
+ const record = replicas.get(room.locator)?.confirmed.record;
1968
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1969
+ return null;
1970
+ return { documents: record.documents, revisions: record.revisions };
1971
+ }
1972
+ async function pullBootstrap(authorized, room, local, live) {
1973
+ if (!live())
923
1974
  return unconfirmed;
1975
+ const turn = generation;
1976
+ if (replicas.has(room.locator)) {
1977
+ catching.add(room.locator);
1978
+ readSide.recover();
1979
+ }
924
1980
  try {
925
- const result = await bootstrapRoom(options.adapter, authorized, room, {
1981
+ const result = await bootstrapRoom(reads, authorized, room, {
926
1982
  collection: modelOf(room.locator),
927
1983
  selection: modelOf(room.locator, 'typed-selection'),
928
- }, (models, held) => remodel(room.locator, models, held), continuing);
1984
+ }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
929
1985
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
930
1986
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1987
+ if (result.kind !== 'staged')
1988
+ caughtUpTo(room.locator);
931
1989
  return result;
932
1990
  }
933
1991
  catch (error) {
934
1992
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
1993
+ caughtUpTo(room.locator);
935
1994
  throw error;
936
1995
  }
1996
+ finally {
1997
+ reads.finished(room.locator, turn);
1998
+ }
937
1999
  }
938
- async function advance(room, local, authorized) {
2000
+ function caughtUpTo(locator) {
2001
+ if (catching.delete(locator))
2002
+ readSide.recover();
2003
+ }
2004
+ function behind(room) {
2005
+ const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
2006
+ return (held !== undefined &&
2007
+ [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
2008
+ }
2009
+ async function advance(room, local, authorized, live) {
2010
+ const carried = reads.carried(room.locator);
2011
+ const withheld = await advanceOnce(room, local, authorized, live);
2012
+ if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
2013
+ return withheld;
2014
+ }
2015
+ return advanceOnce(room, local, authorized, live);
2016
+ }
2017
+ async function advanceOnce(room, local, authorized, live) {
2018
+ await referenced(room.locator);
2019
+ if (!live() || paused.has(room.locator))
2020
+ return unconfirmed;
939
2021
  const held = replicas.get(room.locator);
940
2022
  if (held !== undefined) {
941
- const refreshed = await pullRefresh(authorized, held.confirmed);
942
- if (!continuing())
2023
+ const refreshed = await pullRefresh(authorized, held.confirmed, live);
2024
+ if (!live())
943
2025
  return unconfirmed;
944
2026
  if (refreshed.kind === 'still-current') {
945
2027
  stale.delete(room.locator);
@@ -953,31 +2035,45 @@ export function composeRuntime(options) {
953
2035
  return null;
954
2036
  }
955
2037
  if (refreshed.kind === 'advanced') {
956
- if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
2038
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
957
2039
  const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
958
2040
  const reached = now !== undefined &&
959
2041
  options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
960
- return reached ? null : advance(room, local, authorized);
2042
+ return reached ? null : advance(room, local, authorized, live);
961
2043
  }
962
2044
  await retainModel(local, room, 'collection', modelOf(room.locator));
963
2045
  return null;
964
2046
  }
965
2047
  if (refreshed.kind !== 'beyond-horizon')
966
2048
  return refreshed;
2049
+ if (opened !== null) {
2050
+ owed.add(room.locator);
2051
+ readSide.recover();
2052
+ return unconfirmed;
2053
+ }
967
2054
  }
968
- const bootstrapped = await pullBootstrap(authorized, room);
969
- if (!continuing())
2055
+ const bootstrapped = await pullBootstrap(authorized, room, local, live);
2056
+ if (!live())
970
2057
  return unconfirmed;
971
2058
  if (bootstrapped.kind !== 'staged')
972
2059
  return bootstrapped;
973
- await publish(local, room, bootstrapped.confirmed);
2060
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
974
2061
  return null;
975
2062
  }
976
2063
  async function withhold(withheld, room, local, progress) {
2064
+ if (withheld.kind === 'unconfirmed')
2065
+ return;
977
2066
  if (withheld.kind === 'refused')
978
2067
  stale.delete(room.locator);
979
2068
  else
980
2069
  stale.add(room.locator);
2070
+ if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
2071
+ const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
2072
+ repairable.set(room.locator, reset);
2073
+ repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
2074
+ }
2075
+ else
2076
+ repairable.delete(room.locator);
981
2077
  if (withheld.kind === 'scoped-refusal') {
982
2078
  await scoped(withheld, room, local);
983
2079
  }
@@ -1002,6 +2098,7 @@ export function composeRuntime(options) {
1002
2098
  function refusedSource(room, { refusal }) {
1003
2099
  sourceRefusals.set(room.locator, {
1004
2100
  code: refusal.rule,
2101
+ retryable: false,
1005
2102
  fields: {
1006
2103
  file: refusal.file,
1007
2104
  ...(refusal.location === undefined ? {} : { location: refusal.location }),
@@ -1012,12 +2109,21 @@ export function composeRuntime(options) {
1012
2109
  unreadable.add(room.locator);
1013
2110
  list(room, false);
1014
2111
  emitRooms();
2112
+ moveTo({ ...status, ...attention() });
1015
2113
  journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
1016
2114
  }
1017
2115
  function leaving(batch, result) {
1018
2116
  if (result.kind === 'expired')
1019
- return { batch, returned: { kind: 'expired' } };
1020
- return { batch, returned: { kind: result.kind, code: result.code, fields: result.fields } };
2117
+ return { batch, returned: returnedBy('expired') };
2118
+ return {
2119
+ batch,
2120
+ returned: {
2121
+ kind: result.kind,
2122
+ code: result.code,
2123
+ fields: result.fields,
2124
+ retryable: result.retryable,
2125
+ },
2126
+ };
1021
2127
  }
1022
2128
  function logAccepted(locator, submitted, accepted) {
1023
2129
  journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
@@ -1025,6 +2131,7 @@ export function composeRuntime(options) {
1025
2131
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1026
2132
  }
1027
2133
  async function adoptAccepted(room, local, submitted, accepted) {
2134
+ await referenced(room.locator);
1028
2135
  if (!continuing())
1029
2136
  return unconfirmed;
1030
2137
  const current = replicas.get(room.locator);
@@ -1035,22 +2142,23 @@ export function composeRuntime(options) {
1035
2142
  logAccepted(room.locator, submitted, accepted);
1036
2143
  const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1037
2144
  ? { kind: 'staged', confirmed: current.confirmed }
1038
- : applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
2145
+ : applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
1039
2146
  ? accepted.roomVersion
1040
2147
  : held, accepted.roomVersion);
1041
2148
  if (applied.kind !== 'staged') {
1042
2149
  const state = pendings.get(submitted.id);
1043
2150
  await local.accept(room.locator, submitted.id, accepted.roomVersion);
1044
- if (state !== undefined)
2151
+ if (state !== undefined && !fences.has(room.locator))
1045
2152
  pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1046
2153
  emitQueue(room.locator);
1047
2154
  return applied;
1048
2155
  }
1049
- const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
2156
+ const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1050
2157
  pendings.delete(submitted.id);
1051
2158
  unreadable.delete(room.locator);
1052
2159
  sourceRefusals.delete(room.locator);
1053
2160
  declarationRefusals.delete(room.locator);
2161
+ const set = settled();
1054
2162
  list(room, true);
1055
2163
  publishChangeSet(set);
1056
2164
  moveTo({ ...status, ...attention() });
@@ -1064,14 +2172,14 @@ export function composeRuntime(options) {
1064
2172
  refusedSource(room, failed);
1065
2173
  return false;
1066
2174
  }
1067
- async function submitRoom(room, local, authorized, progress) {
1068
- while (continuing() && authorization !== undefined) {
2175
+ async function submitRoom(room, local, authorized, progress, signal) {
2176
+ while (continuing() && authorization !== undefined && !signal.aborted) {
1069
2177
  const replica = replicas.get(room.locator);
1070
2178
  const head = replica?.queue[0];
1071
2179
  if (replica === undefined || head === undefined)
1072
- return;
2180
+ return 'done';
1073
2181
  if (pendings.get(head.id)?.accepted !== undefined)
1074
- return;
2182
+ return 'done';
1075
2183
  const state = pendings.get(head.id);
1076
2184
  const { submission, lost: earlier } = state ?? {
1077
2185
  submission: { state: 'waiting' },
@@ -1079,24 +2187,30 @@ export function composeRuntime(options) {
1079
2187
  };
1080
2188
  if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
1081
2189
  progress.complete = false;
1082
- return;
2190
+ return { at: Date.parse(submission.reset) };
1083
2191
  }
1084
2192
  if (writesHeld()) {
1085
2193
  progress.complete = false;
1086
2194
  journal.record(`submission ${room.locator} ${head.id} held`);
1087
- return;
2195
+ return 'done';
1088
2196
  }
1089
2197
  const lost = earlier || submission.state === 'submitted';
2198
+ if (lost && pastHorizon(head.id)) {
2199
+ await discardPending(room, local, signal);
2200
+ return 'done';
2201
+ }
1090
2202
  let result;
1091
2203
  journal.record(`submission ${room.locator} ${head.id}`);
1092
2204
  await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1093
- if (discoveryPaused)
1094
- return;
1095
- if (writesHeld()) {
2205
+ if (discoveryPaused || fences.has(room.locator))
2206
+ return 'done';
2207
+ if (signal.aborted || writesHeld()) {
1096
2208
  await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
2209
+ if (signal.aborted)
2210
+ return 'done';
1097
2211
  progress.complete = false;
1098
2212
  journal.record(`submission ${room.locator} ${head.id} held`);
1099
- return;
2213
+ return 'done';
1100
2214
  }
1101
2215
  const opensBefore = openSignals;
1102
2216
  const answered = awaitingAnswer(room.locator);
@@ -1112,32 +2226,36 @@ export function composeRuntime(options) {
1112
2226
  progress.complete = false;
1113
2227
  if (continuing())
1114
2228
  progress.reachable = false;
1115
- return;
2229
+ return 'again';
2230
+ }
2231
+ if (signal.aborted) {
2232
+ answered();
2233
+ return 'done';
1116
2234
  }
1117
2235
  if (result.kind !== 'accepted' || !continuing())
1118
2236
  answered();
1119
2237
  if (!continuing())
1120
- return;
2238
+ return 'done';
1121
2239
  if (result.kind === 'transport-uncertainty') {
1122
2240
  progress.complete = false;
1123
2241
  progress.reachable = false;
1124
- return;
2242
+ return 'again';
1125
2243
  }
1126
2244
  if (result.kind === 'room-not-readable') {
1127
2245
  progress.complete = false;
1128
2246
  unreadable.add(room.locator);
1129
2247
  list(room, false);
1130
2248
  emitRooms();
1131
- return;
2249
+ return 'done';
1132
2250
  }
1133
2251
  if (result.kind === 'revoked-authorization') {
1134
2252
  await refuse('revoked-authorization');
1135
- return;
2253
+ return 'done';
1136
2254
  }
1137
2255
  if (result.kind === 'scoped-refusal') {
1138
2256
  await scoped(result, room, local);
1139
2257
  progress.complete = false;
1140
- return;
2258
+ return 'done';
1141
2259
  }
1142
2260
  if (result.kind === 'write-halted') {
1143
2261
  journal.record(`outcome ${room.locator} ${head.id} write-halted`);
@@ -1146,16 +2264,24 @@ export function composeRuntime(options) {
1146
2264
  continue;
1147
2265
  haltedRefusal = true;
1148
2266
  progress.complete = false;
1149
- return;
2267
+ return 'done';
1150
2268
  }
1151
2269
  if (result.kind === 'conflicted' ||
1152
2270
  result.kind === 'rejected' ||
1153
2271
  result.kind === 'expired' ||
1154
2272
  result.kind === 'retryable-refusal') {
2273
+ if (result.kind === 'expired' && lost && result.restored !== true) {
2274
+ journal.record(`outcome ${room.locator} ${head.id} expired`);
2275
+ await discardPending(room, local, signal);
2276
+ return 'done';
2277
+ }
1155
2278
  if (result.kind === 'expired') {
1156
- const withheld = await advance(room, local, authorized);
1157
- if (withheld !== null)
1158
- return withhold(withheld, room, local, progress);
2279
+ await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
2280
+ const withheld = await advance(room, local, authorized, continuing);
2281
+ if (withheld !== null) {
2282
+ await withhold(withheld, room, local, progress);
2283
+ return 'done';
2284
+ }
1159
2285
  }
1160
2286
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
1161
2287
  (result.kind === 'conflicted' && result.roomVersion !== undefined
@@ -1168,8 +2294,7 @@ export function composeRuntime(options) {
1168
2294
  const reset = result.reset;
1169
2295
  progress.complete = false;
1170
2296
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1171
- armTimer();
1172
- return;
2297
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
1173
2298
  }
1174
2299
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
1175
2300
  const left = leaving(head, result);
@@ -1180,36 +2305,82 @@ export function composeRuntime(options) {
1180
2305
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
1181
2306
  return;
1182
2307
  }
1183
- const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1184
- leaving: result.kind === 'expired' && landed(head, current.confirmed)
2308
+ const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
2309
+ leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
2310
+ (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1185
2311
  ? { ...left, returned: null }
1186
2312
  : left,
1187
2313
  });
1188
- publishChangeSet(set);
2314
+ publishChangeSet(settled());
1189
2315
  moveTo({ ...status, ...attention() });
1190
2316
  });
1191
2317
  if (result.kind === 'expired')
1192
2318
  continue;
1193
- const withheld = await advance(room, local, authorized);
1194
- if (withheld !== null)
1195
- return withhold(withheld, room, local, progress);
2319
+ const withheld = await advance(room, local, authorized, continuing);
2320
+ if (withheld !== null) {
2321
+ await withhold(withheld, room, local, progress);
2322
+ return 'done';
2323
+ }
1196
2324
  continue;
1197
2325
  }
1198
2326
  const kept = await adopted(room, local, head, result).finally(answered);
1199
2327
  if (!kept) {
1200
2328
  progress.complete = false;
1201
- return;
2329
+ return 'done';
1202
2330
  }
1203
2331
  }
2332
+ return 'done';
2333
+ }
2334
+ function pastHorizon(identity) {
2335
+ return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
2336
+ }
2337
+ async function discardPending(room, local, signal) {
2338
+ const { locator } = room;
2339
+ const discarded = await exclusive(async () => {
2340
+ if (fences.has(locator))
2341
+ return false;
2342
+ const before = replicas.get(locator);
2343
+ const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
2344
+ fences.set(locator, { at: discoveries, erased: false });
2345
+ lastFenced.set(locator, ++fencings);
2346
+ try {
2347
+ await local.discard(locator);
2348
+ }
2349
+ finally {
2350
+ fences.delete(locator);
2351
+ }
2352
+ for (const batch of queue)
2353
+ pendings.delete(batch.id);
2354
+ queuesWithoutReplica.delete(locator);
2355
+ const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
2356
+ if (after !== null)
2357
+ replicas.set(locator, after);
2358
+ journal.record(`room discarded ${locator}`);
2359
+ publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
2360
+ moveTo({ ...status, ...attention() });
2361
+ return true;
2362
+ });
2363
+ if (!discarded)
2364
+ return;
2365
+ const progress = { complete: true, reachable: true };
2366
+ await roomTask(locator, async (live) => {
2367
+ await Promise.race([
2368
+ reload(room, local, progress, () => live() && !signal.aborted),
2369
+ abandonedOn(signal),
2370
+ ]);
2371
+ });
2372
+ if (signal.aborted)
2373
+ return;
2374
+ await rest(progress);
1204
2375
  }
1205
2376
  function awaitingAnswer(locator) {
1206
2377
  let release = () => { };
1207
2378
  const awaited = new Promise((resolve) => {
1208
2379
  release = resolve;
1209
2380
  });
1210
- answering.set(locator, awaited);
2381
+ answering.set(locator, { generation, answered: awaited });
1211
2382
  return () => {
1212
- if (answering.get(locator) === awaited)
2383
+ if (answering.get(locator)?.answered === awaited)
1213
2384
  answering.delete(locator);
1214
2385
  release();
1215
2386
  };
@@ -1242,7 +2413,7 @@ export function composeRuntime(options) {
1242
2413
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1243
2414
  };
1244
2415
  const reclassified = sameMembership(declared, held.confirmed.record)
1245
- ? readConfirmed(declared, held.confirmed)
2416
+ ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
1246
2417
  : readReclassified(declared, held.confirmed);
1247
2418
  const record = reclassified.kind === 'staged'
1248
2419
  ? declared
@@ -1250,12 +2421,10 @@ export function composeRuntime(options) {
1250
2421
  ...declared,
1251
2422
  typedSelection: held.confirmed.record.typedSelection,
1252
2423
  };
1253
- const read = reclassified.kind === 'deferred' ? readConfirmed(record, held.confirmed) : reclassified;
1254
- await local.write({
1255
- next: record,
1256
- previous: held.confirmed.record,
1257
- queue: held.queue,
1258
- });
2424
+ const read = reclassified.kind === 'deferred'
2425
+ ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
2426
+ : reclassified;
2427
+ await local.write({ next: record, previous: held.confirmed.record });
1259
2428
  if (read.kind === 'staged') {
1260
2429
  replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1261
2430
  if (declarationRefusals.delete(room.locator)) {
@@ -1282,7 +2451,16 @@ export function composeRuntime(options) {
1282
2451
  });
1283
2452
  }
1284
2453
  async function scoped(result, room, local) {
1285
- scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
2454
+ if ((result.scope === 'content' || result.scope === 'sync') && !result.retryable) {
2455
+ journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
2456
+ await loseRooms([room.locator]);
2457
+ return;
2458
+ }
2459
+ scopedRefusals.set(room.locator, {
2460
+ code: result.code,
2461
+ fields: result.fields,
2462
+ retryable: result.retryable,
2463
+ });
1286
2464
  let scopedAway = room;
1287
2465
  if (result.scope === 'model')
1288
2466
  await retainModel(local, room, 'collection', {
@@ -1319,22 +2497,27 @@ export function composeRuntime(options) {
1319
2497
  roomVersion: record.roomVersion,
1320
2498
  documents: record.documents,
1321
2499
  revisions: record.revisions,
1322
- attachments: record.attachments,
1323
2500
  },
1324
2501
  ];
1325
2502
  });
1326
2503
  }
1327
- async function synchronize(listed, local, progress, attempt) {
1328
- const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1329
- const ready = listed.filter((_, index) => {
2504
+ async function synchronize(listed, local, progress, attempt, live) {
2505
+ const acquired = listed.filter(acquires);
2506
+ const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
2507
+ if (!live())
2508
+ return;
2509
+ const ready = acquired.filter((_, index) => {
1330
2510
  const outcome = prepared[index];
1331
2511
  return outcome?.status === 'fulfilled' && outcome.value;
1332
2512
  });
1333
- if (attempt === 'opened')
1334
- options.adapter.roomStream?.hold(heldRooms(ready));
2513
+ if (attempt === 'opened') {
2514
+ options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
2515
+ handed = true;
2516
+ }
2517
+ discovering?.release();
1335
2518
  const loaded = await Promise.allSettled(ready
1336
2519
  .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1337
- .map((room) => loadRoom(room, local, progress)));
2520
+ .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
1338
2521
  const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1339
2522
  if (failed !== undefined)
1340
2523
  throw failed.reason;
@@ -1356,21 +2539,12 @@ export function composeRuntime(options) {
1356
2539
  await retainModel(local, room, 'collection', modelOf(room.locator));
1357
2540
  if (discoveryPaused || authorization === undefined)
1358
2541
  return false;
1359
- if (room.replica.admission !== 'admitted') {
1360
- if (replicas.has(room.locator))
1361
- progress.complete = false;
1362
- return false;
1363
- }
1364
- return true;
2542
+ return room.replica.admission === 'admitted';
1365
2543
  }
1366
- async function caughtUp(room, local, progress) {
1367
- const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1368
- if (held === undefined || authorization === undefined)
1369
- return;
1370
- const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1371
- if (!behind)
2544
+ async function caughtUp(room, local, progress, live) {
2545
+ if (authorization === undefined || !live() || !behind(room))
1372
2546
  return;
1373
- const withheld = await advance(room, local, authorization);
2547
+ const withheld = await advance(room, local, authorization, live);
1374
2548
  if (withheld !== null)
1375
2549
  await withhold(withheld, room, local, progress);
1376
2550
  }
@@ -1380,39 +2554,46 @@ export function composeRuntime(options) {
1380
2554
  return model.status === 'unavailable' && model.reason === 'revoked';
1381
2555
  });
1382
2556
  }
1383
- async function loadRoom(room, local, progress) {
1384
- if (discoveryPaused || authorization === undefined)
2557
+ async function loadRoom(room, local, progress, live) {
2558
+ if (!live() || authorization === undefined)
1385
2559
  return;
1386
2560
  if (replicas.has(room.locator) && revoked(room.locator)) {
1387
- await reload(room, local, progress);
2561
+ await reload(room, local, progress, live);
1388
2562
  return;
1389
2563
  }
1390
- const withheld = await advance(room, local, authorization);
2564
+ const withheld = await advance(room, local, authorization, live);
1391
2565
  if (withheld !== null) {
1392
2566
  await withhold(withheld, room, local, progress);
1393
2567
  return;
1394
2568
  }
1395
- await caughtUp(room, local, progress);
2569
+ await caughtUp(room, local, progress, live);
1396
2570
  }
1397
- async function reload(room, local, progress) {
1398
- if (discoveryPaused || authorization === undefined)
1399
- return;
1400
- const bootstrapped = await pullBootstrap(authorization, room);
1401
- if (!continuing())
1402
- return;
1403
- if (bootstrapped.kind !== 'staged') {
1404
- await withhold(bootstrapped, room, local, progress);
1405
- return;
2571
+ async function reload(room, local, progress, live) {
2572
+ try {
2573
+ if (!live() || authorization === undefined)
2574
+ return;
2575
+ const bootstrapped = await pullBootstrap(authorization, room, local, live);
2576
+ if (!live())
2577
+ return;
2578
+ if (bootstrapped.kind !== 'staged') {
2579
+ await withhold(bootstrapped, room, local, progress);
2580
+ return;
2581
+ }
2582
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
2583
+ await caughtUp(room, local, progress, live);
2584
+ }
2585
+ finally {
2586
+ caughtUpTo(room.locator);
1406
2587
  }
1407
- await publish(local, room, bootstrapped.confirmed);
1408
- await caughtUp(room, local, progress);
1409
2588
  }
1410
- async function discoverRooms(local, progress, attempt) {
2589
+ async function discoverRooms(local, progress, attempt, live) {
1411
2590
  if (closed || authorization === undefined) {
1412
2591
  notifyReady();
1413
2592
  return;
1414
2593
  }
1415
2594
  moveTo(transferring(status));
2595
+ discoveries += 1;
2596
+ const started = discoveries;
1416
2597
  let result;
1417
2598
  try {
1418
2599
  result = await options.adapter.discover(authorization);
@@ -1420,7 +2601,8 @@ export function composeRuntime(options) {
1420
2601
  catch {
1421
2602
  result = { kind: 'transport-uncertainty' };
1422
2603
  }
1423
- if (closed) {
2604
+ if (!live()) {
2605
+ progress.complete = false;
1424
2606
  notifyReady();
1425
2607
  return;
1426
2608
  }
@@ -1434,15 +2616,16 @@ export function composeRuntime(options) {
1434
2616
  }
1435
2617
  progress.reachable = false;
1436
2618
  progress.complete = false;
1437
- if (attempt === 'opened') {
1438
- options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1439
- }
1440
2619
  notifyReady();
1441
2620
  return;
1442
2621
  }
1443
2622
  if (result.kind === 'contract-unavailable') {
1444
2623
  journal.record(`connection contract unavailable ${result.code}`);
1445
- await refuse('missing-required-guarantee', { code: result.code, fields: result.fields });
2624
+ await refuse('missing-required-guarantee', {
2625
+ code: result.code,
2626
+ fields: result.fields,
2627
+ retryable: result.retryable,
2628
+ });
1446
2629
  notifyReady();
1447
2630
  return;
1448
2631
  }
@@ -1477,14 +2660,20 @@ export function composeRuntime(options) {
1477
2660
  }
1478
2661
  const previousCapabilities = capabilities;
1479
2662
  capabilities = result.capabilities;
2663
+ outcomeRetention = result.retention.outcome;
1480
2664
  notifyReady();
1481
2665
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1482
2666
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
1483
2667
  limits = result.providerLimits;
1484
2668
  await local.declare(limits, capabilities);
1485
2669
  }
1486
- await applyDiscovery(result.rooms, local);
1487
- await synchronize(result.rooms, local, progress, attempt);
2670
+ const signedIn = result.member ?? null;
2671
+ if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
2672
+ await local.keepMember(signedIn);
2673
+ member = signedIn;
2674
+ emitMember();
2675
+ }
2676
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
1488
2677
  }
1489
2678
  async function ensureAuthorization() {
1490
2679
  if (authorization !== undefined)
@@ -1498,76 +2687,175 @@ export function composeRuntime(options) {
1498
2687
  throw error;
1499
2688
  }
1500
2689
  }
1501
- function task(work) {
2690
+ async function admittedRead(work) {
2691
+ if (roomReads.generation !== generation) {
2692
+ roomReads = { generation, free: ROOM_READS, waiting: [] };
2693
+ }
2694
+ const reads = roomReads;
2695
+ while (reads.free === 0)
2696
+ await new Promise((admit) => reads.waiting.push(admit));
2697
+ reads.free -= 1;
2698
+ try {
2699
+ await work();
2700
+ }
2701
+ finally {
2702
+ reads.free += 1;
2703
+ reads.waiting.shift()?.();
2704
+ }
2705
+ }
2706
+ function roomTask(locator, work) {
2707
+ return task(roomLane(locator), (live) => admittedRead(() => work(live)));
2708
+ }
2709
+ function task(lane, work) {
1502
2710
  if (stopping)
1503
- return chain;
1504
- chain = chain.then(work, work).catch(() => undefined);
1505
- return chain;
2711
+ return Promise.resolve();
2712
+ const held = lanes.get(lane);
2713
+ const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
2714
+ const turn = current.generation;
2715
+ const live = () => turn === generation && continuing();
2716
+ const run = current.chain.then(() => work(live)).catch(() => undefined);
2717
+ current.chain = run;
2718
+ lanes.set(lane, current);
2719
+ working.add(run);
2720
+ void run.finally(() => working.delete(run));
2721
+ return run;
1506
2722
  }
1507
2723
  function sync(attempt) {
1508
- return task(async () => {
1509
- if (closed || stopping || discoveryPaused) {
2724
+ return task(OPENING_LANE, async (live) => {
2725
+ if (!live()) {
1510
2726
  notifyReady();
1511
2727
  return;
1512
2728
  }
1513
2729
  const progress = { complete: true, reachable: true };
1514
- underway = attempt === 'opened' ? progress : null;
2730
+ const gate = discover();
2731
+ if (attempt === 'opened')
2732
+ opened = generation;
2733
+ underway = attempt === 'opened' ? { generation, progress } : null;
2734
+ if (attempt === 'opened')
2735
+ streamed = progress;
1515
2736
  complete = false;
1516
2737
  recorded = false;
1517
2738
  moveTo(transferring(status));
2739
+ handed = false;
1518
2740
  try {
1519
2741
  const local = await openLocal();
1520
- if (closed)
2742
+ if (!live())
1521
2743
  return;
1522
2744
  await ensureAuthorization();
1523
- if (closed)
2745
+ if (!live())
1524
2746
  return;
1525
- await discoverRooms(local, progress, attempt);
2747
+ await discoverRooms(local, progress, attempt, live);
1526
2748
  }
1527
2749
  catch {
1528
2750
  progress.complete = false;
1529
- if (attempt === 'opened') {
1530
- options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1531
- }
2751
+ if (attempt === 'opened' && !handed)
2752
+ progress.reachable = false;
1532
2753
  }
1533
2754
  finally {
2755
+ gate.release();
1534
2756
  notifyReady();
1535
2757
  }
1536
- if (attempt === 'opened')
2758
+ if (!live())
2759
+ return;
2760
+ if (attempt !== 'opened') {
2761
+ await finishOpening(progress, live);
2762
+ return;
2763
+ }
2764
+ if (handed || closed || progress.reachable)
2765
+ return;
2766
+ underway = null;
2767
+ opened = null;
2768
+ journal.record('connection reopen');
2769
+ await rest(progress, live);
2770
+ options.adapter.roomStream?.reopen();
2771
+ });
2772
+ }
2773
+ function rediscover() {
2774
+ return task(OPENING_LANE, async (live) => {
2775
+ if (!live() || !socketOpen)
2776
+ return;
2777
+ const progress = { complete: true, reachable: true };
2778
+ const gate = discover();
2779
+ moveTo(transferring(status));
2780
+ try {
2781
+ const local = await openLocal();
2782
+ if (!live())
2783
+ return;
2784
+ await ensureAuthorization();
2785
+ if (!live())
2786
+ return;
2787
+ await discoverRooms(local, progress, 'authorized', live);
2788
+ }
2789
+ catch {
2790
+ progress.complete = false;
2791
+ }
2792
+ finally {
2793
+ gate.release();
2794
+ notifyReady();
2795
+ }
2796
+ if (!live())
1537
2797
  return;
1538
- await finishOpening(progress);
2798
+ if (!progress.complete)
2799
+ complete = false;
2800
+ await rest(progress, live);
1539
2801
  });
1540
2802
  }
1541
- function closedSocket(revoked) {
2803
+ function closedSocket(revoked, lost) {
1542
2804
  const opened = socketOpen;
1543
2805
  socketOpen = false;
2806
+ if (lost) {
2807
+ reads.lost();
2808
+ conversations.lost();
2809
+ }
1544
2810
  if (revoked) {
1545
2811
  void refuse('revoked-authorization');
1546
2812
  return;
1547
2813
  }
1548
- if (opened || status.providerUnreachable)
1549
- return;
1550
- moveTo({ ...status, providerUnreachable: true });
2814
+ complete = false;
2815
+ if (streamed !== null)
2816
+ streamed.complete = false;
2817
+ moveTo({
2818
+ ...status,
2819
+ state: status.state === 'synced' ? 'syncing' : status.state,
2820
+ providerUnreachable: status.providerUnreachable || !opened,
2821
+ });
1551
2822
  }
1552
2823
  function applyOpening(expired) {
1553
- return task(async () => {
1554
- const progress = underway;
2824
+ const progress = underway?.generation === generation ? underway.progress : null;
2825
+ const cycle = progress ?? { complete: true, reachable: true };
2826
+ const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
2827
+ if (opened === generation)
2828
+ opened = null;
2829
+ return task(OPENING_LANE, async (live) => {
1555
2830
  underway = null;
1556
- if (closed || stopping)
2831
+ if (!live())
1557
2832
  return;
1558
2833
  if (progress === null) {
1559
- await reloadExpired(expired);
2834
+ await replacing;
2835
+ await rest(cycle, live);
1560
2836
  return;
1561
2837
  }
2838
+ await roomsCarried(expired);
2839
+ if (!live())
2840
+ return;
2841
+ const replaced = new Set(owed);
2842
+ owed.clear();
1562
2843
  try {
1563
2844
  const local = await openLocal();
1564
2845
  const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1565
- const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1566
- const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
2846
+ const reloading = held.filter((room) => !expired.includes(room.locator) &&
2847
+ (replaced.has(room.locator) || revoked(room.locator)));
2848
+ for (const room of reloading)
2849
+ catching.add(room.locator);
2850
+ readSide.recover();
2851
+ const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
2852
+ const reloaded = [...(await reloads), ...(await replacing)];
1567
2853
  if (progress.reachable) {
1568
- for (const room of held)
1569
- if (!reloading.includes(room))
2854
+ for (const room of held) {
2855
+ if (!expired.includes(room.locator) && !reloading.includes(room)) {
1570
2856
  await confirmModels(room, local);
2857
+ }
2858
+ }
1571
2859
  }
1572
2860
  const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1573
2861
  if (failed !== undefined)
@@ -1577,21 +2865,21 @@ export function composeRuntime(options) {
1577
2865
  progress.complete = false;
1578
2866
  progress.reachable = false;
1579
2867
  }
1580
- await finishOpening(progress);
2868
+ if (live())
2869
+ await finishOpening(progress, live);
1581
2870
  });
1582
2871
  }
1583
- async function reloadExpired(expired) {
1584
- const progress = { complete: true, reachable: true };
2872
+ async function reloadNow(replaced, progress) {
1585
2873
  try {
1586
2874
  const local = await openLocal();
1587
- await Promise.allSettled([...discovered.values()]
1588
- .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1589
- .map((room) => reload(room, local, progress)));
2875
+ return await Promise.allSettled([...discovered.values()]
2876
+ .filter((room) => replicas.has(room.locator) && replaced(room))
2877
+ .map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
1590
2878
  }
1591
2879
  catch {
1592
2880
  progress.reachable = false;
2881
+ return [];
1593
2882
  }
1594
- await rest(progress);
1595
2883
  }
1596
2884
  async function confirmModels(room, local) {
1597
2885
  for (const attached of ['collection', 'typed-selection']) {
@@ -1602,38 +2890,96 @@ export function composeRuntime(options) {
1602
2890
  }
1603
2891
  }
1604
2892
  function submitLater(locator) {
1605
- const running = submitting.get(locator);
1606
- if (running !== undefined) {
1607
- running.again = true;
2893
+ if (localSaves)
1608
2894
  return;
2895
+ void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
2896
+ }
2897
+ async function submitPass(locator, signal) {
2898
+ const room = discovered.get(locator);
2899
+ if (closed || stopping || discoveryPaused || room === undefined)
2900
+ return 'done';
2901
+ const progress = { complete: true, reachable: true };
2902
+ let next;
2903
+ try {
2904
+ await ensureAuthorization();
2905
+ if (authorization === undefined)
2906
+ return 'done';
2907
+ next = await submitRoom(room, await openLocal(), authorization, progress, signal);
1609
2908
  }
1610
- const run = { again: false, done: Promise.resolve() };
1611
- submitting.set(locator, run);
1612
- run.done = (async () => {
1613
- try {
1614
- do {
1615
- run.again = false;
1616
- const room = discovered.get(locator);
1617
- if (closed || stopping || discoveryPaused || room === undefined)
1618
- return;
1619
- const progress = { complete: true, reachable: true };
1620
- try {
1621
- await ensureAuthorization();
1622
- if (authorization === undefined)
1623
- return;
1624
- await submitRoom(room, await openLocal(), authorization, progress);
1625
- }
1626
- catch {
1627
- progress.reachable = false;
1628
- }
1629
- armTimer();
1630
- await rest(progress);
1631
- } while (run.again);
1632
- }
1633
- finally {
1634
- submitting.delete(locator);
1635
- }
1636
- })();
2909
+ catch {
2910
+ progress.reachable = false;
2911
+ next = 'again';
2912
+ }
2913
+ if (signal.aborted)
2914
+ return 'done';
2915
+ await rest(progress);
2916
+ return next === 'again' && !socketOpen ? 'done' : next;
2917
+ }
2918
+ function repairLater(locator, when) {
2919
+ if (!socketOpen || repairing.has(locator))
2920
+ return;
2921
+ void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
2922
+ }
2923
+ async function repairPass(locator, signal) {
2924
+ const room = discovered.get(locator);
2925
+ if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
2926
+ return 'done';
2927
+ if (room === undefined || room.replica.admission !== 'admitted')
2928
+ return 'done';
2929
+ const progress = { complete: true, reachable: true };
2930
+ repairing.add(locator);
2931
+ try {
2932
+ const local = await openLocal();
2933
+ await roomTask(locator, async (live) => {
2934
+ const current = () => live() && !signal.aborted;
2935
+ if (!current() || authorization === undefined)
2936
+ return;
2937
+ const withheld = await Promise.race([
2938
+ advance(room, local, authorization, current),
2939
+ abandonedOn(signal),
2940
+ ]);
2941
+ if (withheld !== null && current())
2942
+ await withhold(withheld, room, local, progress);
2943
+ });
2944
+ }
2945
+ catch {
2946
+ stale.add(locator);
2947
+ progress.reachable = false;
2948
+ }
2949
+ finally {
2950
+ repairing.delete(locator);
2951
+ }
2952
+ if (signal.aborted)
2953
+ return 'done';
2954
+ await rest(progress);
2955
+ if (!stale.has(locator)) {
2956
+ if ((replicas.get(locator)?.queue.length ?? 0) > 0)
2957
+ submitLater(locator);
2958
+ return 'done';
2959
+ }
2960
+ if (!repairable.has(locator))
2961
+ return 'done';
2962
+ const reset = repairable.get(locator);
2963
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
2964
+ }
2965
+ function resendLater() {
2966
+ if (!socketOpen)
2967
+ return;
2968
+ void recovery.attempt('resend', resendPass, { again: true });
2969
+ }
2970
+ async function resendPass(signal) {
2971
+ if (!socketOpen || closed || stopping || discoveryPaused)
2972
+ return 'done';
2973
+ const reading = { complete: true, reachable: true };
2974
+ try {
2975
+ await conversations.resend(reading);
2976
+ }
2977
+ catch {
2978
+ reading.reachable = false;
2979
+ }
2980
+ if (signal.aborted)
2981
+ return 'done';
2982
+ return reading.reachable && reading.complete ? 'done' : 'again';
1637
2983
  }
1638
2984
  function submitAll() {
1639
2985
  for (const [locator, replica] of replicas) {
@@ -1641,31 +2987,44 @@ export function composeRuntime(options) {
1641
2987
  submitLater(locator);
1642
2988
  }
1643
2989
  }
1644
- async function finishOpening(progress) {
2990
+ async function finishOpening(progress, live) {
1645
2991
  try {
1646
- if (closed || discoveryPaused) {
2992
+ if (!live()) {
1647
2993
  progress.complete = false;
1648
2994
  return;
1649
2995
  }
2996
+ reads.applied();
1650
2997
  submitAll();
2998
+ for (const locator of stale)
2999
+ if (repairable.has(locator))
3000
+ repairLater(locator, {});
1651
3001
  await conversations.reconcile();
1652
3002
  if (status.providerUnreachable)
1653
3003
  conversations.reconnected();
1654
3004
  const reading = { complete: true, reachable: true };
1655
- await conversations.catchUp(reading);
1656
- if (!reading.reachable)
3005
+ if (demand.conversations)
3006
+ await conversations.catchUp(reading);
3007
+ if (progress.reachable && reading.reachable)
3008
+ await conversations.resend(reading);
3009
+ if (progress.reachable)
3010
+ void recovery.attempt('calls', resendCalls);
3011
+ if (!reading.reachable) {
1657
3012
  progress.reachable = false;
3013
+ resendLater();
3014
+ }
1658
3015
  }
1659
3016
  catch {
1660
3017
  progress.complete = false;
1661
3018
  }
1662
3019
  finally {
1663
- complete = progress.complete;
1664
- await rest(progress);
3020
+ if (live()) {
3021
+ complete = progress.complete;
3022
+ await rest(progress, live);
3023
+ }
1665
3024
  }
1666
3025
  }
1667
- async function rest(progress) {
1668
- if (closed)
3026
+ async function rest(progress, live = () => !closed) {
3027
+ if (!live())
1669
3028
  return;
1670
3029
  let lastCompleted = status.lastCompleted;
1671
3030
  let synced = complete && stale.size === 0 && !pending();
@@ -1673,6 +3032,8 @@ export function composeRuntime(options) {
1673
3032
  const completedAt = new Date().toISOString();
1674
3033
  try {
1675
3034
  await (await openLocal()).complete(completedAt);
3035
+ if (!live())
3036
+ return;
1676
3037
  recorded = true;
1677
3038
  lastCompleted = completedAt;
1678
3039
  }
@@ -1681,46 +3042,74 @@ export function composeRuntime(options) {
1681
3042
  }
1682
3043
  }
1683
3044
  moveTo({
1684
- state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
3045
+ state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
1685
3046
  lastCompleted,
1686
3047
  providerUnreachable: !progress.reachable,
1687
3048
  ...attention(),
1688
3049
  });
1689
3050
  }
3051
+ function discover() {
3052
+ let release = () => { };
3053
+ const done = new Promise((resolve) => {
3054
+ release = resolve;
3055
+ });
3056
+ discovering = { generation, done, release };
3057
+ return discovering;
3058
+ }
1690
3059
  function applyRoom(locator, roomVersion) {
1691
3060
  announced.set(locator, roomVersion);
1692
- return task(async () => {
1693
- await answering.get(locator);
1694
- if (closed || stopping || discoveryPaused || authorization === undefined)
3061
+ return task(roomLane(locator), async (live) => {
3062
+ if (discovering?.generation === generation)
3063
+ await discovering.done;
3064
+ const saving = answering.get(locator);
3065
+ if (saving?.generation === generation)
3066
+ await saving.answered;
3067
+ if (!live() || authorization === undefined)
1695
3068
  return;
1696
3069
  const room = discovered.get(locator);
1697
3070
  const held = replicas.get(locator);
1698
- if (room === undefined || room.replica.admission !== 'admitted')
3071
+ if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
1699
3072
  return;
1700
3073
  if (held !== undefined &&
1701
3074
  (held.confirmed.record.replica.selection !== room.replica.selection ||
1702
3075
  options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
1703
3076
  return;
1704
3077
  }
1705
- const during = underway;
3078
+ const during = underway?.generation === generation ? underway.progress : null;
1706
3079
  const progress = during ?? { complete: true, reachable: true };
1707
3080
  if (during === null)
1708
3081
  moveTo(transferring(status));
1709
3082
  try {
1710
3083
  const local = await openLocal();
1711
- const withheld = await advance(room, local, authorization);
1712
- if (withheld !== null)
1713
- await withhold(withheld, room, local, progress);
3084
+ await admittedRead(async () => {
3085
+ if (!live() || authorization === undefined)
3086
+ return;
3087
+ const withheld = await advance(room, local, authorization, live);
3088
+ if (withheld !== null)
3089
+ await withhold(withheld, room, local, progress);
3090
+ });
1714
3091
  }
1715
3092
  catch {
1716
3093
  stale.add(locator);
1717
3094
  }
1718
3095
  finally {
1719
3096
  if (during === null)
1720
- await rest(progress);
3097
+ await rest(progress, live);
1721
3098
  }
1722
3099
  });
1723
3100
  }
3101
+ function roomsCarried(replacing) {
3102
+ const replaced = new Set(replacing.map(roomLane));
3103
+ return Promise.all([...lanes]
3104
+ .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
3105
+ .map(([, held]) => held.chain));
3106
+ }
3107
+ const receipts = {
3108
+ heads: async () => (await openLocal()).receivedHeads(),
3109
+ parts: async (key, count) => (await openLocal()).receivedParts(key, count),
3110
+ keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
3111
+ drop: async (keys) => (await openLocal()).dropReceived(keys),
3112
+ };
1724
3113
  function observeStream() {
1725
3114
  const stream = options.adapter.roomStream;
1726
3115
  if (stream === undefined || withdrawStream !== undefined)
@@ -1730,15 +3119,21 @@ export function composeRuntime(options) {
1730
3119
  return;
1731
3120
  if (event.kind === 'opened') {
1732
3121
  socketOpen = true;
3122
+ generation += 1;
3123
+ reads.opened();
3124
+ conversations.reopened();
3125
+ recovery.resume();
1733
3126
  void sync('opened');
1734
3127
  }
1735
3128
  else if (event.kind === 'closed')
1736
- closedSocket(event.revoked);
3129
+ closedSocket(event.revoked, event.lost);
1737
3130
  else if (event.kind === 'opening')
1738
3131
  void applyOpening(event.expired);
3132
+ else if (event.kind === 'authority-changed')
3133
+ void rediscover();
1739
3134
  else
1740
3135
  void applyRoom(event.locator, event.roomVersion);
1741
- });
3136
+ }, receipts);
1742
3137
  }
1743
3138
  function writesHeld() {
1744
3139
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -1753,6 +3148,7 @@ export function composeRuntime(options) {
1753
3148
  haltedRefusal = false;
1754
3149
  if (mode !== serviceMode) {
1755
3150
  serviceMode = mode;
3151
+ queueReadiness();
1756
3152
  journal.record(`service mode ${mode}`);
1757
3153
  for (const listener of serviceModeListeners)
1758
3154
  listener(mode);
@@ -1765,6 +3161,8 @@ export function composeRuntime(options) {
1765
3161
  if (stream === undefined || withdrawConversations !== undefined)
1766
3162
  return;
1767
3163
  withdrawConversations = stream.observe((event) => {
3164
+ if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
3165
+ queueReadiness();
1768
3166
  if (conversations.receive(event)) {
1769
3167
  void ready.then(scheduleCurrency);
1770
3168
  return;
@@ -1776,46 +3174,47 @@ export function composeRuntime(options) {
1776
3174
  });
1777
3175
  }
1778
3176
  function scheduleCurrency() {
1779
- if (currencyQueued || closed || stopping)
3177
+ if (currencyQueued === generation || closed || stopping)
1780
3178
  return;
1781
- currencyQueued = true;
1782
- void task(runCurrency);
3179
+ currencyQueued = generation;
3180
+ void task(CONVERSATION_LANE, runCurrency);
1783
3181
  }
1784
- async function runCurrency() {
1785
- currencyQueued = false;
1786
- if (closed || stopping || discoveryPaused)
3182
+ async function runCurrency(live) {
3183
+ if (currencyQueued === generation)
3184
+ currencyQueued = null;
3185
+ if (!live())
1787
3186
  return;
1788
3187
  await conversations.reconcile();
1789
- if (conversations.current()) {
3188
+ if (conversations.current() || !demand.conversations) {
1790
3189
  show();
1791
3190
  return;
1792
3191
  }
1793
- const resting = status;
1794
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1795
- moveTo(transferring(resting));
3192
+ const progress = { complete: false, reachable: !status.providerUnreachable };
3193
+ moveTo(transferring(status));
1796
3194
  try {
1797
3195
  await conversations.catchUp(progress);
1798
3196
  }
1799
3197
  catch {
1800
3198
  progress.reachable = false;
1801
3199
  }
1802
- finally {
1803
- if (!closed) {
1804
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1805
- }
1806
- }
3200
+ await rest(progress, live);
1807
3201
  }
1808
3202
  function close() {
1809
3203
  if (closed)
1810
3204
  return;
1811
3205
  closed = true;
3206
+ if (narrowing !== null)
3207
+ clearTimeout(narrowing);
3208
+ if (rechecking !== null)
3209
+ clearInterval(rechecking);
3210
+ readinessWatches.clear();
1812
3211
  withdrawStream?.();
1813
3212
  withdrawStream = undefined;
1814
3213
  withdrawConversations?.();
1815
3214
  withdrawConversations = undefined;
1816
3215
  withdrawServiceMode?.();
1817
3216
  conversations.close();
1818
- stopTimer();
3217
+ void recovery.retire();
1819
3218
  store?.close();
1820
3219
  void opening?.then((opened) => {
1821
3220
  opened.close();
@@ -1825,6 +3224,7 @@ export function composeRuntime(options) {
1825
3224
  storageLossListeners.clear();
1826
3225
  statusListeners.clear();
1827
3226
  serviceModeListeners.clear();
3227
+ memberListeners.clear();
1828
3228
  readSide.close();
1829
3229
  queueSubscriptions.clear();
1830
3230
  roomDraftListeners.clear();
@@ -1832,9 +3232,8 @@ export function composeRuntime(options) {
1832
3232
  }
1833
3233
  async function stop() {
1834
3234
  stopping = true;
1835
- stopTimer();
1836
- await chain.catch(() => undefined);
1837
- await Promise.all([...submitting.values()].map((run) => run.done));
3235
+ await recovery.retire();
3236
+ await Promise.all(working);
1838
3237
  await mutations;
1839
3238
  close();
1840
3239
  }
@@ -1853,17 +3252,111 @@ export function composeRuntime(options) {
1853
3252
  if (options.adapter.roomStream === undefined)
1854
3253
  void sync('unstreamed');
1855
3254
  function planned(input, reach) {
1856
- return planOf(input, roomFacts(reach), planReach(reach));
3255
+ const plan = planOf(input, roomFacts(reach), planReach(reach));
3256
+ const named = plan.paths !== null &&
3257
+ plan.rooms !== null &&
3258
+ plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
3259
+ return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
1857
3260
  }
1858
3261
  function watchPlan(plan, deliver) {
1859
- return readSide.watch(plan, identityOf(plan), deliver);
3262
+ let seeded = false;
3263
+ let pending = true;
3264
+ const shown = () => {
3265
+ const seed = seeds.get(plan.key);
3266
+ if (seed === undefined || seeded || !pending)
3267
+ return;
3268
+ seeded = true;
3269
+ deliver(seededResult(seed));
3270
+ };
3271
+ const watchers = seedWatchers.get(plan.key) ?? new Set();
3272
+ seedWatchers.set(plan.key, watchers);
3273
+ watchers.add(shown);
3274
+ const stop = readSide.watch(plan, identityOf(plan), () => {
3275
+ const cover = covered(plan);
3276
+ awaitPreparation(cover);
3277
+ return seedBehind(plan, cover);
3278
+ }, (message) => {
3279
+ const answered = message;
3280
+ pending = whole(answered) && 'pending' in answered.result;
3281
+ if (pending && seeds.has(plan.key)) {
3282
+ shown();
3283
+ return;
3284
+ }
3285
+ seeded = false;
3286
+ deliver(message);
3287
+ });
3288
+ return () => {
3289
+ watchers.delete(shown);
3290
+ if (watchers.size === 0)
3291
+ seedWatchers.delete(plan.key);
3292
+ stop();
3293
+ };
1860
3294
  }
1861
3295
  async function searched(input, reach) {
1862
- const plan = searchPlanOf(input);
1863
- const selected = plan.documents === undefined && reach === null
1864
- ? null
1865
- : (await readSide.query(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
1866
- return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
3296
+ const asked = searchPlanOf(input);
3297
+ const plan = { ...asked, limit: capped(asked.limit) };
3298
+ const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
3299
+ for (;;) {
3300
+ const cover = covered(filter);
3301
+ awaitPreparation(cover);
3302
+ const pending = 'pending' in cover ? [...cover.pending] : [];
3303
+ const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3304
+ const held = covered({
3305
+ ...filter,
3306
+ rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
3307
+ });
3308
+ const covering = 'pending' in held ? [] : held.coverage.rooms;
3309
+ const unindexed = covering
3310
+ .filter(({ room }) => !searchIndex.rooms().has(room))
3311
+ .map(({ room }) => room);
3312
+ for (const room of unindexed)
3313
+ pending.push({ room, awaits: 'index' });
3314
+ const listed = conversations.searchable();
3315
+ const named = reach !== null && !reach.conversations
3316
+ ? []
3317
+ : (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
3318
+ const waiting = named.filter((one) => !searchIndex.conversations().has(one));
3319
+ for (const conversation of waiting)
3320
+ pending.push({ conversation, awaits: 'index' });
3321
+ if (unindexed.length > 0 || waiting.length > 0)
3322
+ index(unindexed, waiting);
3323
+ const complete = pending.length === 0 && !('pending' in cover);
3324
+ const answering = complete
3325
+ ? filter
3326
+ : {
3327
+ ...filter,
3328
+ rooms: covering
3329
+ .filter(({ room }) => searchIndex.rooms().has(room))
3330
+ .map(({ room }) => room),
3331
+ };
3332
+ const answered = covered(answering);
3333
+ if ('pending' in answered)
3334
+ continue;
3335
+ const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
3336
+ const seen = searchIndex.changes();
3337
+ await searchIndex.caughtUp();
3338
+ const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
3339
+ if (selected !== null && 'pending' in selected)
3340
+ continue;
3341
+ if (searchIndex.changes() !== seen ||
3342
+ JSON.stringify(covered(answering)) !== JSON.stringify(answered))
3343
+ continue;
3344
+ const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms === null ? null : new Set(answering.rooms), plan.collection
3345
+ ? new Map(answered.coverage.rooms.map(({ room }) => [
3346
+ room,
3347
+ replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
3348
+ ]))
3349
+ : null);
3350
+ const coverage = {
3351
+ rooms: answered.coverage.rooms,
3352
+ conversations: searchedConversations,
3353
+ index: searchIndex.generation(),
3354
+ overlay: answered.coverage.overlay,
3355
+ };
3356
+ return complete
3357
+ ? { ...hits, coverage }
3358
+ : { pending, documents: hits.documents, messages: hits.messages, coverage };
3359
+ }
1867
3360
  }
1868
3361
  const watch = (topic, input, deliver) => {
1869
3362
  if (topic === 'room-drafts') {
@@ -1888,6 +3381,8 @@ export function composeRuntime(options) {
1888
3381
  }
1889
3382
  if (topic === 'conversations')
1890
3383
  return conversations.watchList(deliver);
3384
+ if (topic === 'agents')
3385
+ return conversations.watchAgents(deliver);
1891
3386
  if (topic === 'presentation') {
1892
3387
  const listener = {
1893
3388
  app: input[0],
@@ -1907,6 +3402,9 @@ export function composeRuntime(options) {
1907
3402
  presentationListeners.delete(listener);
1908
3403
  };
1909
3404
  }
3405
+ if (topic === 'readiness') {
3406
+ return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
3407
+ }
1910
3408
  if (topic === 'rooms') {
1911
3409
  roomListeners.add(deliver);
1912
3410
  deliver(listed());
@@ -1942,6 +3440,13 @@ export function composeRuntime(options) {
1942
3440
  serviceModeListeners.delete(deliver);
1943
3441
  };
1944
3442
  }
3443
+ if (topic === 'member') {
3444
+ memberListeners.add(deliver);
3445
+ deliver(member);
3446
+ return () => {
3447
+ memberListeners.delete(deliver);
3448
+ };
3449
+ }
1945
3450
  if (topic === 'queue') {
1946
3451
  const queue = { locator: input[0], notify: deliver };
1947
3452
  queueSubscriptions.add(queue);
@@ -1959,7 +3464,7 @@ export function composeRuntime(options) {
1959
3464
  rooms() {
1960
3465
  return listed();
1961
3466
  },
1962
- query: ((input) => readSide.query(planned(input, null))),
3467
+ query: ((input) => rowsQuery(planned(input, null))),
1963
3468
  search: (input) => searched(input, null),
1964
3469
  types(locator) {
1965
3470
  return {
@@ -1988,20 +3493,20 @@ export function composeRuntime(options) {
1988
3493
  return exclusive(async () => {
1989
3494
  const parts = readParts(input);
1990
3495
  if (path === '' || normalize(path) !== path)
1991
- throw new Error('room-draft-malformed');
3496
+ throw runtimeError('room-draft-malformed');
1992
3497
  const local = await openLocal();
1993
3498
  const current = roomDrafts.get(room)?.get(path);
1994
3499
  if ((current?.revision ?? null) !== revision) {
1995
- return {
1996
- outcome: 'stale',
1997
- current: current === undefined ? null : roomDraftOf(current),
1998
- };
3500
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
1999
3501
  }
3502
+ const file = parts.file === undefined
3503
+ ? undefined
3504
+ : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
2000
3505
  const record = {
2001
3506
  locator: room,
2002
3507
  path,
2003
3508
  revision: roomDraftRevision(),
2004
- parts: held(current?.parts, parts),
3509
+ parts: held(current?.parts, parts, file),
2005
3510
  returned: current?.returned ?? null,
2006
3511
  };
2007
3512
  await local.keepRoomDraft(record);
@@ -2015,10 +3520,7 @@ export function composeRuntime(options) {
2015
3520
  const local = await openLocal();
2016
3521
  const current = roomDrafts.get(room)?.get(path);
2017
3522
  if (current === undefined || current.revision !== revision) {
2018
- return {
2019
- outcome: 'stale',
2020
- current: current === undefined ? null : roomDraftOf(current),
2021
- };
3523
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2022
3524
  }
2023
3525
  await local.discardRoomDraft(room, path);
2024
3526
  forgetRoomDraft(room, path);
@@ -2038,13 +3540,13 @@ export function composeRuntime(options) {
2038
3540
  });
2039
3541
  },
2040
3542
  write(app, value) {
2041
- return exclusive(async () => {
2042
- const local = await openLocal();
2043
- const stored = structuredClone(value);
2044
- await local.present({ app, value: stored });
2045
- presentations.set(app, stored);
2046
- publishChangeSet(changeSet('', null, null, { presentation: app }));
2047
- });
3543
+ return keepPresentation(app, () => structuredClone(value));
3544
+ },
3545
+ edit(app, members) {
3546
+ return keepPresentation(app, (held) => Object.fromEntries([
3547
+ ...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(members, member)),
3548
+ ...Object.entries(structuredClone(members)).filter(([, value]) => value !== null),
3549
+ ]));
2048
3550
  },
2049
3551
  },
2050
3552
  save(room, named, writtenBy) {
@@ -2053,8 +3555,13 @@ export function composeRuntime(options) {
2053
3555
  history(locator, path, query) {
2054
3556
  return readHistory(locator, path, query);
2055
3557
  },
3558
+ binaryFile: {
3559
+ read: readBinaryPath,
3560
+ link: linkBinaryPath,
3561
+ readReference: readBinaryReference,
3562
+ },
2056
3563
  call(name, input) {
2057
- return invokeAction(name, input);
3564
+ return recoveredAction(name, input);
2058
3565
  },
2059
3566
  reached(locator, roomVersion, path) {
2060
3567
  return path === undefined
@@ -2065,30 +3572,55 @@ export function composeRuntime(options) {
2065
3572
  return journal.export();
2066
3573
  },
2067
3574
  persistence() {
2068
- return options.shell.persistentStorage;
3575
+ return persistence === 'granted';
3576
+ },
3577
+ async retry() {
3578
+ journal.record('retry');
3579
+ unindexable.clear();
3580
+ queueReadiness();
3581
+ resume();
2069
3582
  },
2070
3583
  observe: observing(watch),
2071
3584
  async reauthorize() {
2072
3585
  if (refusal === null || !refusal.reauthorizationOffered)
2073
3586
  return;
2074
3587
  journal.record('connection reauthorize');
2075
- await chain.catch(() => undefined);
3588
+ await Promise.all([...lanes.values()]
3589
+ .filter((held) => held.generation === generation)
3590
+ .map((held) => held.chain));
2076
3591
  await mutations;
2077
3592
  authorization = undefined;
2078
3593
  discoveryPaused = false;
2079
3594
  if (options.adapter.roomStream === undefined)
2080
3595
  await sync('unstreamed');
2081
3596
  else
2082
- await task(ensureAuthorization);
3597
+ await task(OPENING_LANE, ensureAuthorization);
2083
3598
  },
2084
3599
  };
2085
3600
  const reachable = {
2086
3601
  md,
2087
3602
  watch,
3603
+ referrerScope: (referrer) => options.adapter.referrerScope(referrer),
2088
3604
  plan: planned,
2089
- query: (plan) => readSide.query(plan),
3605
+ query: (plan) => rowsQuery(plan),
2090
3606
  watchPlan,
2091
3607
  search: searched,
3608
+ watchReadiness,
3609
+ };
3610
+ return {
3611
+ md,
3612
+ seed,
3613
+ stop,
3614
+ lose: loseRooms,
3615
+ watch,
3616
+ acquire,
3617
+ persisted(answer) {
3618
+ if (answer === persistence)
3619
+ return;
3620
+ persistence = answer;
3621
+ journal.record(`persistence ${answer}`);
3622
+ queueReadiness();
3623
+ },
3624
+ within: (reach, port) => narrowed(reachable, reach, port),
2092
3625
  };
2093
- return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2094
3626
  }