@agentshouse/mdruntime 0.1.0-cde2b99d2fcbc → 0.1.0-ce131cb435f8b

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