@agentshouse/mdruntime 0.1.0-ca9afacd5aebd → 0.1.0-caee7332d2235

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