@agentshouse/mdruntime 0.1.0-cec6690d5712e → 0.1.0-cedb03eaa48b6

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