@agentshouse/mdruntime 0.1.0-c69ca0e865e0b → 0.1.0-c6fd2cbbff187

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