@agentshouse/mdruntime 0.1.0-cacb4c3f6734e → 0.1.0-caee7332d2235

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