@agentshouse/mdruntime 0.1.0-c2aaa9c0e3ca4 → 0.1.0-c2c52158b7381

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 (199) hide show
  1. package/AGENTS.md +285 -276
  2. package/README.md +9 -8
  3. package/dist/adapter.d.ts +80 -21
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/attached-model.d.ts +4 -1
  6. package/dist/attached-model.d.ts.map +1 -1
  7. package/dist/attached-model.js +13 -3
  8. package/dist/attempt.d.ts.map +1 -1
  9. package/dist/attempt.js +14 -1
  10. package/dist/bootstrap.d.ts +3 -2
  11. package/dist/bootstrap.d.ts.map +1 -1
  12. package/dist/bootstrap.js +83 -26
  13. package/dist/change-set.d.ts +7 -2
  14. package/dist/change-set.d.ts.map +1 -1
  15. package/dist/change-set.js +37 -10
  16. package/dist/change.d.ts +31 -1
  17. package/dist/change.d.ts.map +1 -1
  18. package/dist/change.js +10 -1
  19. package/dist/chat/acts.d.ts +3 -4
  20. package/dist/chat/acts.d.ts.map +1 -1
  21. package/dist/chat/acts.js +10 -10
  22. package/dist/chat/earlier.d.ts +10 -0
  23. package/dist/chat/earlier.d.ts.map +1 -0
  24. package/dist/chat/earlier.js +54 -0
  25. package/dist/chat/house.d.ts +25 -0
  26. package/dist/chat/house.d.ts.map +1 -0
  27. package/dist/chat/house.js +38 -0
  28. package/dist/chat/input.d.ts +3 -1
  29. package/dist/chat/input.d.ts.map +1 -1
  30. package/dist/chat/input.js +11 -18
  31. package/dist/chat/kit.d.ts +2 -1
  32. package/dist/chat/kit.d.ts.map +1 -1
  33. package/dist/chat/kit.js +3 -3
  34. package/dist/chat/list.d.ts +9 -1
  35. package/dist/chat/list.d.ts.map +1 -1
  36. package/dist/chat/list.js +53 -9
  37. package/dist/chat/markdown.d.ts +3 -1
  38. package/dist/chat/markdown.d.ts.map +1 -1
  39. package/dist/chat/markdown.js +85 -26
  40. package/dist/chat/message.d.ts +1 -0
  41. package/dist/chat/message.d.ts.map +1 -1
  42. package/dist/chat/message.js +57 -12
  43. package/dist/chat/messages.d.ts +9 -1
  44. package/dist/chat/messages.d.ts.map +1 -1
  45. package/dist/chat/messages.js +67 -28
  46. package/dist/chat/name.d.ts +1 -0
  47. package/dist/chat/name.d.ts.map +1 -1
  48. package/dist/chat/name.js +1 -1
  49. package/dist/chat/opened.d.ts +1 -0
  50. package/dist/chat/opened.d.ts.map +1 -1
  51. package/dist/chat/opened.js +2 -0
  52. package/dist/chat/overlay.d.ts +1 -0
  53. package/dist/chat/overlay.d.ts.map +1 -1
  54. package/dist/chat/overlay.js +11 -5
  55. package/dist/chat/parts.d.ts +1 -0
  56. package/dist/chat/parts.d.ts.map +1 -1
  57. package/dist/chat/parts.js +3 -2
  58. package/dist/chat/pieces.d.ts +62 -0
  59. package/dist/chat/pieces.d.ts.map +1 -0
  60. package/dist/chat/pieces.js +1110 -0
  61. package/dist/chat/provider.d.ts +7 -2
  62. package/dist/chat/provider.d.ts.map +1 -1
  63. package/dist/chat/provider.js +129 -155
  64. package/dist/chat/question.d.ts +1 -1
  65. package/dist/chat/question.d.ts.map +1 -1
  66. package/dist/chat/question.js +7 -18
  67. package/dist/chat/ready.d.ts +5 -1
  68. package/dist/chat/ready.d.ts.map +1 -1
  69. package/dist/chat/ready.js +9 -7
  70. package/dist/chat/room-file.d.ts +8 -0
  71. package/dist/chat/room-file.d.ts.map +1 -0
  72. package/dist/chat/room-file.js +72 -0
  73. package/dist/chat/steady.d.ts +3 -0
  74. package/dist/chat/steady.d.ts.map +1 -0
  75. package/dist/chat/steady.js +26 -0
  76. package/dist/chat/style.d.ts.map +1 -1
  77. package/dist/chat/style.js +8 -4
  78. package/dist/chat.d.ts +3 -2
  79. package/dist/chat.d.ts.map +1 -1
  80. package/dist/chat.js +2 -2
  81. package/dist/client.d.ts +21 -9
  82. package/dist/client.d.ts.map +1 -1
  83. package/dist/client.js +47 -8
  84. package/dist/collection.d.ts +21 -6
  85. package/dist/collection.d.ts.map +1 -1
  86. package/dist/collection.js +332 -40
  87. package/dist/connection.d.ts +39 -11
  88. package/dist/connection.d.ts.map +1 -1
  89. package/dist/connection.js +2294 -492
  90. package/dist/conversation.d.ts +28 -9
  91. package/dist/conversation.d.ts.map +1 -1
  92. package/dist/conversation.js +500 -165
  93. package/dist/digest.d.ts +7 -0
  94. package/dist/digest.d.ts.map +1 -0
  95. package/dist/digest.js +113 -0
  96. package/dist/durability.d.ts +8 -1
  97. package/dist/durability.d.ts.map +1 -1
  98. package/dist/entry.d.ts +18 -0
  99. package/dist/entry.d.ts.map +1 -0
  100. package/dist/entry.js +157 -0
  101. package/dist/fence.d.ts +6 -0
  102. package/dist/fence.d.ts.map +1 -0
  103. package/dist/fence.js +10 -0
  104. package/dist/file.d.ts +2 -10
  105. package/dist/file.d.ts.map +1 -1
  106. package/dist/file.js +3 -120
  107. package/dist/host.d.ts +15 -3
  108. package/dist/host.d.ts.map +1 -1
  109. package/dist/host.js +103 -13
  110. package/dist/identity.d.ts +2 -0
  111. package/dist/identity.d.ts.map +1 -1
  112. package/dist/identity.js +6 -0
  113. package/dist/index.d.ts +15 -9
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js +5 -0
  116. package/dist/memory.d.ts +8 -2
  117. package/dist/memory.d.ts.map +1 -1
  118. package/dist/memory.js +43 -12
  119. package/dist/model.d.ts +4 -2
  120. package/dist/model.d.ts.map +1 -1
  121. package/dist/model.js +10 -1
  122. package/dist/name.d.ts +4 -0
  123. package/dist/name.d.ts.map +1 -0
  124. package/dist/name.js +122 -0
  125. package/dist/observation.d.ts +7 -2
  126. package/dist/observation.d.ts.map +1 -1
  127. package/dist/observation.js +29 -12
  128. package/dist/opened.d.ts +22 -0
  129. package/dist/opened.d.ts.map +1 -0
  130. package/dist/opened.js +107 -0
  131. package/dist/outcome.d.ts +39 -4
  132. package/dist/outcome.d.ts.map +1 -1
  133. package/dist/outcome.js +29 -0
  134. package/dist/overlay.d.ts +6 -4
  135. package/dist/overlay.d.ts.map +1 -1
  136. package/dist/overlay.js +229 -161
  137. package/dist/query.d.ts +57 -4
  138. package/dist/query.d.ts.map +1 -1
  139. package/dist/query.js +37 -7
  140. package/dist/reach.d.ts +11 -4
  141. package/dist/reach.d.ts.map +1 -1
  142. package/dist/reach.js +192 -41
  143. package/dist/react.d.ts +8 -6
  144. package/dist/react.d.ts.map +1 -1
  145. package/dist/react.js +7 -17
  146. package/dist/read-side.d.ts +18 -9
  147. package/dist/read-side.d.ts.map +1 -1
  148. package/dist/read-side.js +894 -155
  149. package/dist/readiness.d.ts +53 -0
  150. package/dist/readiness.d.ts.map +1 -0
  151. package/dist/readiness.js +33 -0
  152. package/dist/reads.d.ts +11 -0
  153. package/dist/reads.d.ts.map +1 -0
  154. package/dist/reads.js +98 -0
  155. package/dist/recovery.d.ts +25 -0
  156. package/dist/recovery.d.ts.map +1 -0
  157. package/dist/recovery.js +198 -0
  158. package/dist/refresh.d.ts +4 -4
  159. package/dist/refresh.d.ts.map +1 -1
  160. package/dist/refresh.js +46 -20
  161. package/dist/replica.d.ts +12 -7
  162. package/dist/replica.d.ts.map +1 -1
  163. package/dist/replica.js +151 -64
  164. package/dist/room-draft.d.ts +16 -2
  165. package/dist/room-draft.d.ts.map +1 -1
  166. package/dist/room-draft.js +32 -25
  167. package/dist/rows.d.ts +6 -13
  168. package/dist/rows.d.ts.map +1 -1
  169. package/dist/rows.js +10 -13
  170. package/dist/save.d.ts +52 -10
  171. package/dist/save.d.ts.map +1 -1
  172. package/dist/save.js +519 -159
  173. package/dist/search.d.ts +49 -6
  174. package/dist/search.d.ts.map +1 -1
  175. package/dist/search.js +242 -46
  176. package/dist/seed.d.ts +16 -0
  177. package/dist/seed.d.ts.map +1 -0
  178. package/dist/seed.js +75 -0
  179. package/dist/shared.d.ts +19 -0
  180. package/dist/shared.d.ts.map +1 -0
  181. package/dist/shared.js +461 -0
  182. package/dist/status.d.ts +4 -0
  183. package/dist/status.d.ts.map +1 -1
  184. package/dist/store.d.ts +61 -17
  185. package/dist/store.d.ts.map +1 -1
  186. package/dist/store.js +581 -310
  187. package/dist/tables.d.ts +242 -0
  188. package/dist/tables.d.ts.map +1 -0
  189. package/dist/tables.js +406 -0
  190. package/dist/turn.d.ts +9 -0
  191. package/dist/turn.d.ts.map +1 -0
  192. package/dist/turn.js +67 -0
  193. package/dist/windows.d.ts +21 -0
  194. package/dist/windows.d.ts.map +1 -0
  195. package/dist/windows.js +84 -0
  196. package/dist/wire.d.ts +168 -32
  197. package/dist/wire.d.ts.map +1 -1
  198. package/dist/wire.js +56 -31
  199. package/package.json +10 -2
@@ -1,30 +1,49 @@
1
- import { attachedModel } from './attached-model.js';
1
+ import { roomTarget, target } from '@agentshouse/mdmodel';
2
+ import { attachedModel, NO_ROOM_TYPES, roomTypesOf } from './attached-model.js';
2
3
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
3
- import { observing } from './observation.js';
4
- import { filterOf, identityOf, planOf, } from './query.js';
4
+ import { observing, whole } from './observation.js';
5
+ import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
5
6
  import { createReadSide } from './read-side.js';
6
- import { markdownPath, roomOfKey } from './rows.js';
7
+ import { seededResult } from './seed.js';
8
+ import { documentKey, markdownPath, roomOfKey } from './rows.js';
7
9
  import { createSearchIndex, searchPlanOf, } from './search.js';
10
+ import { capped, MAXIMUM_WINDOW } from './windows.js';
8
11
  import { bootstrapRoom } from './bootstrap.js';
9
- import { changeSet, mergeChangeSets } from './change-set.js';
12
+ import { changeSet, changing, entering, mergeChangeSets } from './change-set.js';
10
13
  import { createConversationStore, } from './conversation.js';
11
- import { queuedFile } from './change.js';
12
- import { validRoot } from './collection.js';
14
+ import { leaving as leavingIn, partnerOf, queuedFile, } from './change.js';
15
+ import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
13
16
  import { runtimeError } from './failure.js';
14
17
  import { createLog } from './log.js';
15
18
  import { binaryRecord } from './binary.js';
16
- import { absentBinary, binaryAnswer, calledResult, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
19
+ import { openedFiles } from './opened.js';
20
+ import { ABSENT_TARGET, absentBinary, absentReference, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
21
+ import { createRecovery } from './recovery.js';
22
+ import { doorReads } from './reads.js';
17
23
  import { refreshRoom } from './refresh.js';
18
- import { buildBatch, joined, limitedIn, requeued } from './save.js';
19
- import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
20
- import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
24
+ import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
25
+ import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicating, sameMembership, standing, } from './replica.js';
26
+ import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
21
27
  import { same, saving, STARTING, transferring, } from './status.js';
22
28
  import { detectStorageLoss, nextGeneration } from './durability.js';
23
- import { roomDraftRevision } from './identity.js';
29
+ import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
30
+ import { FencedMap } from './fence.js';
31
+ import { keysIn, tallying } from './shared.js';
32
+ import { finished, inChunks, ROW_WEIGHT, stepped, yielded } from './turn.js';
33
+ import { identityTime, roomDraftRevision } from './identity.js';
24
34
  import { beneath, normalize } from './paths.js';
25
35
  import { openRuntimeStore, } from './store.js';
26
36
  import { unconfirmed } from './transfer.js';
27
37
  import { governs, holds, narrowed, planReach, } from './reach.js';
38
+ function roomsAlone(plan) {
39
+ return (plan.reach === null &&
40
+ plan.root.types === null &&
41
+ plan.root.where === null &&
42
+ plan.root.pathPrefix === null &&
43
+ plan.paths === null &&
44
+ plan.hop === null &&
45
+ plan.backlinks === null);
46
+ }
28
47
  function within(root, path) {
29
48
  return root === '' || beneath(root, path);
30
49
  }
@@ -38,25 +57,74 @@ function declaredSelection(room) {
38
57
  root !== '' &&
39
58
  selection.roots.every((other, at) => at === index || !within(other, root)));
40
59
  }
60
+ const OPENING_LANE = 'opening';
61
+ const CONVERSATION_LANE = 'conversations';
62
+ const ROOM_READS = 256;
63
+ const ROOM_ATTEMPTS = 16;
64
+ // A Room that locks offline stays readable this long after the connection drops, so a laptop waking or changing network keeps working while a disconnected device locks soon after.
65
+ const LOCK_GRACE_MS = 120_000;
66
+ function operationOf(input) {
67
+ const operation = input?.operation_id;
68
+ return typeof operation === 'string' ? operation : null;
69
+ }
70
+ const outside = new Map();
71
+ function outsideOf(roots) {
72
+ const key = JSON.stringify(roots);
73
+ const held = outside.get(key);
74
+ if (held !== undefined)
75
+ return held;
76
+ const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
77
+ const made = {
78
+ documents: (path) => (markdownPath(path) ? counted(path) : 0),
79
+ binaries: counted,
80
+ };
81
+ outside.set(key, made);
82
+ return made;
83
+ }
84
+ function abandonedOn(signal) {
85
+ return new Promise((resolve) => {
86
+ if (signal.aborted)
87
+ resolve(null);
88
+ else
89
+ signal.addEventListener('abort', () => resolve(null), { once: true });
90
+ });
91
+ }
92
+ function roomLane(locator) {
93
+ return `room ${locator}`;
94
+ }
95
+ function pendingKey(locator, id) {
96
+ return `${locator}\u0000${id}`;
97
+ }
41
98
  export function composeRuntime(options) {
42
99
  const localSaves = options.adapter.localSaves === true;
100
+ const roomTypes = roomTypesOf(options.adapter.roomTypes);
43
101
  let authorization = options.authorization;
44
102
  let opening;
45
103
  let store;
46
- const replicas = new Map();
47
- const queuesWithoutReplica = new Map();
104
+ const fences = new Map();
105
+ const isFenced = (locator) => fences.has(locator);
106
+ let fencings = 0;
107
+ const lastFenced = new Map();
108
+ const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
109
+ let discoveries = 0;
110
+ const replicas = new FencedMap(isFenced);
111
+ const queuesWithoutReplica = new FencedMap(isFenced);
48
112
  const pendings = new Map();
49
- const roomDrafts = new Map();
113
+ const seeds = new Map();
114
+ const seedWatchers = new Map();
115
+ const roomDrafts = new FencedMap(isFenced);
50
116
  const presentations = new Map();
51
117
  let limits = [];
52
118
  let capabilities = [];
53
- const rooms = new Map();
119
+ const rooms = new FencedMap(isFenced);
54
120
  const counted = new Map();
55
121
  const unreadable = new Set();
56
- const models = new Map();
57
- const liveModels = new Map();
122
+ const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
123
+ const models = new FencedMap(isModelFenced);
124
+ const liveModels = new FencedMap(isModelFenced);
58
125
  const invalidModels = new Set();
59
126
  const discovered = new Map();
127
+ const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
60
128
  let roomsListed = false;
61
129
  const scopedRefusals = new Map();
62
130
  const sourceRefusals = new Map();
@@ -84,7 +152,6 @@ export function composeRuntime(options) {
84
152
  let status = STARTING;
85
153
  let shown = STARTING;
86
154
  let discoveryPaused = false;
87
- let resetTimer;
88
155
  let closed = false;
89
156
  let stopping = false;
90
157
  let withdrawStream;
@@ -92,12 +159,23 @@ export function composeRuntime(options) {
92
159
  let underway = null;
93
160
  let streamed = null;
94
161
  let handed = false;
95
- const submitting = new Map();
162
+ const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
163
+ const repairing = new Set();
164
+ const calling = new Set();
165
+ const repairable = new Map();
166
+ let outcomeRetention = null;
96
167
  const answering = new Map();
97
168
  let complete = false;
98
169
  let recorded = false;
99
170
  const stale = new Set();
100
- let chain = Promise.resolve();
171
+ let generation = 0;
172
+ let discovering = null;
173
+ let opened = null;
174
+ const owed = new Set();
175
+ const reloadsDue = new Map();
176
+ const lanes = new Map();
177
+ let roomReads = { generation, free: ROOM_READS, waiting: [] };
178
+ const working = new Set();
101
179
  let mutations = Promise.resolve();
102
180
  const roomListeners = new Set();
103
181
  const refusalListeners = new Set();
@@ -108,33 +186,52 @@ export function composeRuntime(options) {
108
186
  const presentationListeners = new Set();
109
187
  const readSide = createReadSide();
110
188
  let withdrawConversations;
111
- let currencyQueued = false;
189
+ let currencyQueued = null;
112
190
  let socketOpen = false;
191
+ const confirmed = new Set();
192
+ let lockGrace = null;
193
+ const lockListeners = new Set();
113
194
  let serviceMode = 'open';
114
195
  let haltedRefusal = false;
115
196
  let openSignals = 0;
116
197
  const serviceModeListeners = new Set();
117
198
  let member = null;
118
199
  const memberListeners = new Set();
119
- const materialized = new Set();
120
- let everyRoom = false;
121
- const transfers = new Map();
200
+ const prepared = new Set();
201
+ const projecting = new Set();
202
+ const unpreparedDemand = new Set();
203
+ const unindexable = new Set();
204
+ let preparing = false;
205
+ let targets = {};
206
+ let persistence = options.shell.persistence;
207
+ let capacity = null;
208
+ const paused = new Set();
209
+ let rechecking = null;
210
+ const readinessWatches = new Set();
211
+ let readinessQueued = false;
212
+ const catching = new Set();
213
+ const transfers = new FencedMap(isFenced);
122
214
  const conversations = createConversationStore({
123
215
  agents: declaredAgents,
124
- documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
216
+ documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
125
217
  call: (name, input) => invokeAction(name, input),
126
218
  durably,
127
219
  stream: () => options.adapter.conversations,
128
220
  publish: publishChangeSet,
221
+ online: () => connectionOf() !== 'unreachable' && refusal === null,
129
222
  closed: () => closed,
223
+ unanswered: resendLater,
224
+ expired: (operation) => pastHorizon(operation),
130
225
  });
131
226
  const searchIndex = createSearchIndex({
132
- documents: () => readSide.documents(),
227
+ room: (room) => readSide.room(room),
133
228
  document: (room, path) => readSide.document(room, path),
134
- messages: () => conversations.messages(),
135
- message: (conversation, identity) => conversations.message(conversation, identity),
136
229
  });
230
+ const roomsToIndex = new Set();
231
+ const conversationsToIndex = new Set();
232
+ let indexing = false;
137
233
  const journal = createLog();
234
+ const keptFiles = openedFiles(options.database, isFenced);
138
235
  let notifyReady = () => { };
139
236
  const ready = new Promise((resolve) => {
140
237
  notifyReady = resolve;
@@ -188,6 +285,15 @@ export function composeRuntime(options) {
188
285
  const value = presentations.get(app);
189
286
  return value === undefined ? null : structuredClone(value);
190
287
  }
288
+ function keepPresentation(app, next) {
289
+ return exclusive(async () => {
290
+ const local = await openLocal();
291
+ const stored = next(presentations.get(app));
292
+ await local.present({ app, value: stored });
293
+ presentations.set(app, stored);
294
+ publishChangeSet(changeSet('', null, null, { presentation: app }));
295
+ });
296
+ }
191
297
  function emitPresentation(app) {
192
298
  if (closed)
193
299
  return;
@@ -212,9 +318,16 @@ export function composeRuntime(options) {
212
318
  returned: structuredClone(record.returned),
213
319
  });
214
320
  }
321
+ for (const [room, refusal] of sourceRefusals)
322
+ attentionRequired.push({ room, refusal: structuredClone(refusal) });
215
323
  attentionRequired.sort((left, right) => {
216
324
  if (left.room !== right.room)
217
325
  return left.room < right.room ? -1 : 1;
326
+ if ('refusal' in left || 'refusal' in right) {
327
+ if ('refusal' in left && 'refusal' in right)
328
+ return 0;
329
+ return 'refusal' in left ? -1 : 1;
330
+ }
218
331
  if (left.path !== right.path)
219
332
  return left.path < right.path ? -1 : 1;
220
333
  return 0;
@@ -230,23 +343,74 @@ export function composeRuntime(options) {
230
343
  }
231
344
  function documentsIn(locator) {
232
345
  const replica = replicas.get(locator);
233
- if (replica === undefined)
234
- return 0;
235
- const roots = replica.confirmed.record.typedSelection?.roots ?? [];
236
- let count = 0;
237
- for (const path of Object.keys(replica.visible.documents)) {
238
- if (markdownPath(path) && !roots.some((root) => beneath(root, path)))
239
- count += 1;
240
- }
241
- return count;
346
+ return replica === undefined ? 0 : finished(counting(replica));
347
+ }
348
+ function* counting(replica) {
349
+ const weighed = outsideOf(replica.confirmed.record.typedSelection?.roots ?? []);
350
+ return ((yield* tallying(replica.visible.documents, weighed.documents)) +
351
+ (yield* tallying(replica.visible.binaries, weighed.binaries)));
242
352
  }
243
353
  function listed() {
244
354
  return structuredClone([...rooms.values()].map((room) => {
245
355
  const documents = documentsIn(room.locator);
246
356
  counted.set(room.locator, documents);
247
- return { ...room, documents };
357
+ return locked(room.locator)
358
+ ? {
359
+ ...room,
360
+ model: modelTypes(NO_MODEL),
361
+ typedSelection: null,
362
+ documents: 0,
363
+ locked: true,
364
+ }
365
+ : { ...room, documents, locked: false };
248
366
  }));
249
367
  }
368
+ function locked(locator) {
369
+ return rooms.get(locator)?.replica.offline === 'locked' && !confirmed.has(locator);
370
+ }
371
+ function lockedRooms() {
372
+ return [...rooms.keys()].filter(locked).sort();
373
+ }
374
+ function relocked(before) {
375
+ const after = lockedRooms();
376
+ if (JSON.stringify(after) === JSON.stringify(before))
377
+ return;
378
+ journal.record(`locked ${after.length}`);
379
+ emitRooms();
380
+ for (const notify of [...lockListeners])
381
+ notify();
382
+ }
383
+ function confirmLocks(listed) {
384
+ if (!socketOpen)
385
+ return;
386
+ for (const room of listed) {
387
+ if (room.replica.admission === 'admitted')
388
+ confirmed.add(room.locator);
389
+ }
390
+ }
391
+ function connectionLost() {
392
+ if (lockGrace !== null || closed)
393
+ return;
394
+ lockGrace = setTimeout(() => {
395
+ lockGrace = null;
396
+ if (socketOpen || closed)
397
+ return;
398
+ const before = lockedRooms();
399
+ confirmed.clear();
400
+ relocked(before);
401
+ }, LOCK_GRACE_MS);
402
+ }
403
+ function connectionLive() {
404
+ if (lockGrace !== null)
405
+ clearTimeout(lockGrace);
406
+ lockGrace = null;
407
+ }
408
+ function watchLocks(notify) {
409
+ lockListeners.add(notify);
410
+ return () => {
411
+ lockListeners.delete(notify);
412
+ };
413
+ }
250
414
  function roomFacts(reach = null) {
251
415
  const held = [...rooms.values()]
252
416
  .filter((room) => holds(reach, room.locator))
@@ -265,15 +429,19 @@ export function composeRuntime(options) {
265
429
  return { listed: roomsListed, rooms: held };
266
430
  }
267
431
  function emitRooms() {
432
+ readSide.recover();
433
+ queueReadiness();
268
434
  const value = listed();
269
435
  for (const listener of roomListeners)
270
436
  listener(value);
271
437
  }
272
438
  function emitRefusal() {
439
+ queueReadiness();
273
440
  for (const listener of refusalListeners)
274
441
  listener(refusal);
275
442
  }
276
443
  function emitStorageLoss() {
444
+ queueReadiness();
277
445
  for (const listener of storageLossListeners)
278
446
  listener(storageLoss);
279
447
  }
@@ -290,6 +458,7 @@ export function composeRuntime(options) {
290
458
  function show() {
291
459
  if (closed)
292
460
  return;
461
+ queueReadiness();
293
462
  const next = status.state === 'synced' && !conversations.current()
294
463
  ? { ...status, state: 'syncing' }
295
464
  : status;
@@ -299,17 +468,17 @@ export function composeRuntime(options) {
299
468
  for (const listener of statusListeners)
300
469
  listener(shown);
301
470
  }
302
- function entryOf(batch) {
471
+ function entryOf(locator, batch) {
303
472
  if (localSaves)
304
473
  return Object.freeze({ batch, state: 'local' });
305
- const state = pendings.get(batch.id);
474
+ const state = pendings.get(pendingKey(locator, batch.id));
306
475
  if (state?.accepted !== undefined) {
307
476
  return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
308
477
  }
309
478
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
310
479
  }
311
480
  function entriesOf(locator) {
312
- return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
481
+ return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map((batch) => entryOf(locator, batch));
313
482
  }
314
483
  function emitQueue(locator) {
315
484
  const entries = entriesOf(locator);
@@ -319,42 +488,462 @@ export function composeRuntime(options) {
319
488
  }
320
489
  }
321
490
  function applyChangeSet(set) {
322
- const held = everyRoom
323
- ? set
324
- : { ...set, documents: set.documents.filter(({ locator }) => materialized.has(locator)) };
325
- readSide.apply(held);
326
- searchIndex.changed(held);
327
- }
328
- function materialize(locators) {
329
- if (everyRoom)
491
+ const held = {
492
+ ...set,
493
+ documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
494
+ };
495
+ const applied = readSide.apply(held, () => searchIndex.changed(held));
496
+ if (applied instanceof Promise)
497
+ void applied.catch(unpublished);
498
+ }
499
+ function unpublished(error) {
500
+ journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
501
+ }
502
+ async function referenced(locator) {
503
+ for (;;) {
504
+ const record = replicas.get(locator)?.confirmed.record;
505
+ if (record === undefined || closed)
506
+ return;
507
+ const held = { text: record.documents, binaries: record.binaries };
508
+ const composition = compositionOf(record);
509
+ if (referencesIndexed(held, composition))
510
+ return;
511
+ await indexReferrers(held, composition);
512
+ }
513
+ }
514
+ async function prepareReplica(locator) {
515
+ await referenced(locator);
516
+ const replica = replicas.get(locator);
517
+ if (replica === undefined || closed || prepared.has(locator))
330
518
  return;
331
- if (locators === null)
332
- everyRoom = true;
333
- const sets = [];
334
- for (const locator of locators ?? [...replicas.keys()]) {
335
- if (materialized.has(locator))
336
- continue;
337
- materialized.add(locator);
338
- const replica = replicas.get(locator);
339
- if (replica !== undefined)
340
- sets.push(changeSet(locator, null, replica));
519
+ const visible = replica.visible;
520
+ const entered = [];
521
+ projecting.add(locator);
522
+ try {
523
+ await inChunks(await stepped(keysIn(visible.documents, visible.binaries)), (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
524
+ const current = replicas.get(locator);
525
+ if (current === undefined || closed)
526
+ return;
527
+ const set = entering(locator, current, chunk);
528
+ entered.push(set);
529
+ return readSide.apply(set);
530
+ });
531
+ if (!replicas.has(locator) || closed)
532
+ return;
533
+ prepared.add(locator);
341
534
  }
342
- if (sets.length === 0)
535
+ finally {
536
+ projecting.delete(locator);
537
+ }
538
+ conversations.changed(mergeChangeSets(entered));
539
+ readSide.recover();
540
+ queueReadiness();
541
+ }
542
+ function unprepared() {
543
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
544
+ }
545
+ function unbuilt(locator, error) {
546
+ unindexable.add(locator);
547
+ journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
548
+ queueReadiness();
549
+ }
550
+ function prepare(locators = []) {
551
+ for (const locator of locators)
552
+ unpreparedDemand.add(locator);
553
+ if (preparing)
343
554
  return;
344
- const set = mergeChangeSets(sets);
345
- readSide.apply(set);
346
- searchIndex.changed(set);
555
+ preparing = true;
556
+ void (async () => {
557
+ try {
558
+ for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
559
+ await yielded();
560
+ if (closed)
561
+ return;
562
+ unpreparedDemand.delete(waiting);
563
+ await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
564
+ }
565
+ }
566
+ finally {
567
+ preparing = false;
568
+ }
569
+ })();
570
+ }
571
+ function roomsOfReach(reach) {
572
+ return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
347
573
  }
348
574
  function reachedRooms(plan) {
349
- if (plan.rooms !== null)
350
- return plan.rooms;
351
- if (plan.reach === null)
575
+ const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
576
+ const named = plan.rooms ??
577
+ (reached === null
578
+ ? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
579
+ : [...reached]);
580
+ return [...new Set(named)]
581
+ .filter((locator) => (reached === null || reached.has(locator)) && !plan.locked.includes(locator))
582
+ .sort();
583
+ }
584
+ function awaited(locator) {
585
+ return (!roomsListed ||
586
+ (rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
587
+ }
588
+ function covered(plan) {
589
+ const locators = reachedRooms(plan);
590
+ if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
591
+ return { pending: [] };
592
+ }
593
+ const pending = [];
594
+ const coverage = [];
595
+ const overlay = [];
596
+ for (const locator of locators) {
597
+ const replica = replicas.get(locator);
598
+ if (replica === undefined) {
599
+ if (awaited(locator))
600
+ pending.push({ room: locator, awaits: 'acquisition' });
601
+ continue;
602
+ }
603
+ if (!prepared.has(locator)) {
604
+ pending.push({ room: locator, awaits: 'preparation' });
605
+ continue;
606
+ }
607
+ coverage.push({
608
+ room: locator,
609
+ generation: replica.confirmed.record.transfer,
610
+ current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
611
+ });
612
+ for (const batch of replica.queue)
613
+ overlay.push(batch.id);
614
+ }
615
+ if (pending.length > 0)
616
+ return { pending };
617
+ return { coverage: { rooms: coverage, indexes: [], overlay } };
618
+ }
619
+ function awaitPreparation(cover) {
620
+ if (!('pending' in cover))
621
+ return;
622
+ const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
623
+ if (waiting.length > 0)
624
+ prepare(waiting.map(({ room }) => room));
625
+ }
626
+ function requiredRooms(given) {
627
+ const reach = planReach(given);
628
+ if (reach === null)
629
+ return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
630
+ const named = [...roomsOfReach(reach)].sort();
631
+ if (roomsListed)
632
+ return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
633
+ return { listed: named.every((room) => replicas.has(room)), rooms: named };
634
+ }
635
+ function roomState(locator, search) {
636
+ if (sourceRefusals.has(locator))
637
+ return 'refused';
638
+ const listedRoom = discovered.get(locator);
639
+ if (roomsListed &&
640
+ (listedRoom === undefined ||
641
+ listedRoom.replica.admission !== 'admitted' ||
642
+ unreadable.has(locator)))
643
+ return 'unadmitted';
644
+ if (!replicas.has(locator))
645
+ return 'acquiring';
646
+ if (!prepared.has(locator))
647
+ return 'preparing';
648
+ if (search && !searchIndex.rooms().has(locator))
649
+ return 'indexing';
650
+ return 'complete';
651
+ }
652
+ function conversationCoverage(reach, search) {
653
+ const declared = reach === null || reach.conversations;
654
+ if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
655
+ return 'undeclared';
656
+ if (!conversations.known() || !conversations.current())
657
+ return 'acquiring';
658
+ if (!search)
659
+ return 'complete';
660
+ const unindexed = conversations
661
+ .searchable()
662
+ .filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
663
+ if (unindexed.length === 0)
664
+ return 'complete';
665
+ index([], unindexed);
666
+ return 'indexing';
667
+ }
668
+ function connectionOf() {
669
+ if (socketOpen)
670
+ return 'connected';
671
+ if (status.providerUnreachable)
672
+ return 'unreachable';
673
+ return options.adapter.roomStream === undefined && status.lastCompleted !== null
674
+ ? 'connected'
675
+ : 'connecting';
676
+ }
677
+ function refusedIn(locators) {
678
+ return locators.flatMap((room) => {
679
+ const held = sourceRefusals.get(room);
680
+ if (held === undefined)
681
+ return [];
682
+ const { file, location } = held.fields;
683
+ return [
684
+ {
685
+ room,
686
+ file: typeof file === 'string' ? file : '',
687
+ rule: held.code,
688
+ location: typeof location === 'string' ? location : null,
689
+ },
690
+ ];
691
+ });
692
+ }
693
+ function readinessFor(port, reach) {
694
+ const required = requiredRooms(reach);
695
+ const search = port.search === 'required';
696
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
697
+ const building = (state) => coverage
698
+ .filter((room) => room.state === state && !unindexable.has(room.room))
699
+ .map(({ room }) => room);
700
+ const preparing = building('preparing');
701
+ if (preparing.length > 0)
702
+ prepare(preparing);
703
+ const indexing = building('indexing');
704
+ if (indexing.length > 0)
705
+ index(indexing, []);
706
+ return readinessOf({
707
+ code: port.code,
708
+ coverage: {
709
+ listed: required.listed,
710
+ rooms: coverage,
711
+ conversations: conversationCoverage(reach, search),
712
+ search: search ? 'required' : 'unused',
713
+ },
714
+ persistence,
715
+ connection: connectionOf(),
716
+ house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
717
+ capacity: capacity !== null &&
718
+ coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
719
+ ? capacity
720
+ : port.capacity,
721
+ refused: refusedIn(required.rooms),
722
+ lost: storageLoss !== null,
723
+ established: targets[port.target]?.established === true,
724
+ });
725
+ }
726
+ function establish(name) {
727
+ targets = { ...targets, [name]: { established: true } };
728
+ queueReadiness();
729
+ void durably((local) => local.keepTargets(targets)).catch(() => {
730
+ journal.record(`target record failed ${name}`);
731
+ });
732
+ }
733
+ function deliverReadiness(watch) {
734
+ if (!readinessWatches.has(watch))
735
+ return;
736
+ const readiness = readinessFor(watch.port, watch.reach);
737
+ if (readiness.ready && targets[watch.port.target]?.established !== true) {
738
+ establish(watch.port.target);
739
+ }
740
+ const shown = JSON.stringify(readiness);
741
+ if (shown === watch.last)
742
+ return;
743
+ watch.last = shown;
744
+ watch.deliver(structuredClone(readiness));
745
+ }
746
+ function queueReadiness() {
747
+ if (readinessQueued || closed || readinessWatches.size === 0)
748
+ return;
749
+ readinessQueued = true;
750
+ queueMicrotask(() => {
751
+ readinessQueued = false;
752
+ if (closed)
753
+ return;
754
+ for (const watch of [...readinessWatches])
755
+ deliverReadiness(watch);
756
+ });
757
+ }
758
+ function watchReadiness(port, reach, deliver) {
759
+ const watch = { port, reach, deliver, last: null };
760
+ readinessWatches.add(watch);
761
+ void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
762
+ return () => {
763
+ readinessWatches.delete(watch);
764
+ };
765
+ }
766
+ function layerBytes(layer) {
767
+ let bytes = 0;
768
+ for (const [path, text] of Object.entries(layer.documents))
769
+ bytes += (path.length + text.length) * 2;
770
+ return bytes;
771
+ }
772
+ function grownBy(next, previous) {
773
+ if (previous === undefined)
774
+ return layerBytes(next);
775
+ const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
776
+ return layerBytes({ ...next, documents });
777
+ }
778
+ function exceeded(error) {
779
+ return (typeof error === 'object' &&
780
+ error !== null &&
781
+ error.name === 'QuotaExceededError');
782
+ }
783
+ async function estimated() {
784
+ const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
785
+ return estimate === null ? null : Math.max(0, estimate.total - estimate.usage);
786
+ }
787
+ function pause(locator, cause) {
788
+ paused.add(locator);
789
+ capacity = cause;
790
+ journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
791
+ queueReadiness();
792
+ if (rechecking !== null)
793
+ return;
794
+ // A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
795
+ rechecking = setInterval(() => {
796
+ void recheck();
797
+ }, 60_000);
798
+ }
799
+ async function recheck() {
800
+ const held = capacity;
801
+ if (held === null || closed)
802
+ return;
803
+ const available = await estimated().catch(() => null);
804
+ if (available === null || available < (held.needed ?? 0))
805
+ return;
806
+ if (held.cause === 'storage-exceeded' &&
807
+ (held.available === null || available <= held.available))
808
+ return;
809
+ resume();
810
+ }
811
+ function resume() {
812
+ if (rechecking !== null)
813
+ clearInterval(rechecking);
814
+ rechecking = null;
815
+ if (paused.size === 0 && capacity === null)
816
+ return;
817
+ paused.clear();
818
+ capacity = null;
819
+ journal.record('capacity resumed');
820
+ queueReadiness();
821
+ if (!closed && authorization !== undefined)
822
+ void sync('unstreamed');
823
+ }
824
+ async function reclaimed(local) {
825
+ const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
826
+ for (const locator of unreferenced) {
827
+ await local.abandon(locator);
828
+ transfers.delete(locator);
829
+ }
830
+ if (unreferenced.length > 0)
831
+ journal.record(`capacity reclaimed ${unreferenced.length}`);
832
+ return unreferenced.length > 0;
833
+ }
834
+ async function admitted(local, locator, needed) {
835
+ if (paused.has(locator))
836
+ return false;
837
+ let available = await estimated().catch(() => null);
838
+ if (available === null || available >= needed)
839
+ return true;
840
+ if (await reclaimed(local))
841
+ available = await estimated().catch(() => null);
842
+ if (available === null || available >= needed)
843
+ return true;
844
+ pause(locator, { cause: 'insufficient-space', needed, available });
845
+ return false;
846
+ }
847
+ async function exceededOn(locator, bytes) {
848
+ const available = await estimated().catch(() => null);
849
+ pause(locator, { cause: 'storage-exceeded', needed: bytes, available });
850
+ }
851
+ function seedRoomHeld(room, version) {
852
+ return prepared.has(room) && reachedBy(room, version);
853
+ }
854
+ function seedHeld(at) {
855
+ return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
856
+ }
857
+ function seedApplies(plan) {
858
+ const seed = seeds.get(plan.key);
859
+ if (seed === undefined)
860
+ return null;
861
+ if (!seedHeld(seed.at))
862
+ return seed;
863
+ seeds.delete(plan.key);
864
+ return null;
865
+ }
866
+ function seedBehind(plan, cover) {
867
+ const seed = seedApplies(plan);
868
+ if (seed === null || 'pending' in cover)
869
+ return cover;
870
+ return {
871
+ pending: Object.entries(seed.at)
872
+ .filter(([room, version]) => !seedRoomHeld(room, version))
873
+ .map(([room]) => ({ room, awaits: 'acquisition' })),
874
+ };
875
+ }
876
+ function seed(entry) {
877
+ if (seedHeld(entry.at))
878
+ return;
879
+ for (const { query, rows } of entry.queries) {
880
+ const key = queryKey(query);
881
+ seeds.set(key, { rows, at: entry.at });
882
+ for (const shown of seedWatchers.get(key) ?? [])
883
+ shown();
884
+ }
885
+ }
886
+ async function rowsQuery(plan) {
887
+ const before = covered(plan);
888
+ awaitPreparation(before);
889
+ const seeded = seedApplies(plan);
890
+ if (seeded !== null)
891
+ return seededResult(seeded).result;
892
+ if ('pending' in before)
893
+ return before;
894
+ const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
895
+ awaitPreparation(cover);
896
+ if ('pending' in cover)
897
+ return cover;
898
+ return { ...rows, coverage: cover.coverage };
899
+ }
900
+ async function indexConversation(conversation) {
901
+ searchIndex.indexMessages(conversation, [], false);
902
+ let after = null;
903
+ for (;;) {
904
+ const batch = await durably((local) => local.messagesAfter(conversation, after));
905
+ if (closed)
906
+ return;
907
+ searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
908
+ if (batch.length === 0)
909
+ return;
910
+ after = batch[batch.length - 1].identity;
911
+ }
912
+ }
913
+ function indexNext() {
914
+ const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
915
+ if (room !== undefined) {
916
+ roomsToIndex.delete(room);
917
+ return () => prepared.has(room)
918
+ ? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
919
+ : undefined;
920
+ }
921
+ const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
922
+ if (conversation === undefined)
352
923
  return null;
353
- return [...plan.reach.rooms, ...plan.reach.documents.map(roomOfKey)];
924
+ conversationsToIndex.delete(conversation);
925
+ return () => indexConversation(conversation);
354
926
  }
355
- function rowsQuery(plan) {
356
- materialize(reachedRooms(plan));
357
- return readSide.query(plan);
927
+ function index(rooms, named) {
928
+ for (const room of rooms)
929
+ roomsToIndex.add(room);
930
+ for (const conversation of named)
931
+ conversationsToIndex.add(conversation);
932
+ if (indexing)
933
+ return;
934
+ indexing = true;
935
+ void (async () => {
936
+ try {
937
+ for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
938
+ await yielded();
939
+ await unit();
940
+ queueReadiness();
941
+ }
942
+ }
943
+ finally {
944
+ indexing = false;
945
+ }
946
+ })().catch(() => { });
358
947
  }
359
948
  function publishChangeSet(set) {
360
949
  applyChangeSet(set);
@@ -371,34 +960,7 @@ export function composeRuntime(options) {
371
960
  emitPresentation(app);
372
961
  conversations.changed(set);
373
962
  }
374
- function stopTimer() {
375
- if (resetTimer === undefined)
376
- return;
377
- clearTimeout(resetTimer);
378
- resetTimer = undefined;
379
- }
380
- function armTimer() {
381
- stopTimer();
382
- if (closed || stopping)
383
- return;
384
- let earliest = null;
385
- for (const { submission } of pendings.values()) {
386
- if (submission.state !== 'reset')
387
- continue;
388
- const at = Date.parse(submission.reset);
389
- if (earliest === null || at < earliest)
390
- earliest = at;
391
- }
392
- if (earliest === null)
393
- return;
394
- // A Save House held back goes again only once the reset its answer named has passed.
395
- resetTimer = setTimeout(() => {
396
- resetTimer = undefined;
397
- submitAll();
398
- }, Math.max(0, earliest - Date.now()));
399
- }
400
963
  async function refuse(reason, provider = null) {
401
- stopTimer();
402
964
  discoveryPaused = true;
403
965
  try {
404
966
  if (reason === 'revoked-authorization') {
@@ -421,17 +983,22 @@ export function composeRuntime(options) {
421
983
  models.set(key, revoked);
422
984
  liveModels.delete(key);
423
985
  }
424
- const changed = [];
425
- for (const [locator, room] of rooms) {
986
+ const revocations = [];
987
+ for (const locator of [...rooms.keys()]) {
426
988
  const before = replicas.get(locator) ?? null;
427
- if (before !== null) {
428
- const record = {
429
- ...before.confirmed.record,
430
- model: revoked,
431
- selectionModel: revoked,
432
- };
433
- replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
434
- }
989
+ const after = before === null
990
+ ? null
991
+ : await stepped(replicating(await confirmedOf({ ...before.confirmed.record, model: revoked, selectionModel: revoked }, before.confirmed), before.queue, before));
992
+ const set = await stepped(changing(locator, before, after, { room: true }));
993
+ revocations.push([locator, before, after, set]);
994
+ }
995
+ const changed = [];
996
+ for (const [locator, before, after, set] of revocations) {
997
+ const room = rooms.get(locator);
998
+ if (room === undefined || (replicas.get(locator) ?? null) !== before)
999
+ continue;
1000
+ if (after !== null)
1001
+ replicas.set(locator, after);
435
1002
  rooms.set(locator, {
436
1003
  ...room,
437
1004
  model: modelTypes(revoked),
@@ -439,7 +1006,7 @@ export function composeRuntime(options) {
439
1006
  ? null
440
1007
  : { ...room.typedSelection, model: modelTypes(revoked) },
441
1008
  });
442
- changed.push(changeSet(locator, before, replicas.get(locator) ?? null, { room: true }));
1009
+ changed.push(set);
443
1010
  }
444
1011
  publishChangeSet(mergeChangeSets(changed));
445
1012
  });
@@ -477,14 +1044,32 @@ export function composeRuntime(options) {
477
1044
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
478
1045
  });
479
1046
  }
480
- async function applyDiscovery(listed, local) {
1047
+ async function applyDiscovery(found, started, covered) {
1048
+ const lockedBefore = lockedRooms();
1049
+ for (const room of found) {
1050
+ const fence = fences.get(room.locator);
1051
+ if (fence !== undefined && fence.erased && fence.at < started)
1052
+ fences.delete(room.locator);
1053
+ }
1054
+ await loseRooms(found
1055
+ .filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
1056
+ .map((room) => room.locator));
1057
+ const listed = found.filter((room) => !fences.has(room.locator));
481
1058
  const listedLocators = new Set(listed.map((room) => room.locator));
482
1059
  for (const locator of listedLocators)
483
1060
  scopedRefusals.delete(locator);
484
- discovered.clear();
1061
+ if (covered === undefined)
1062
+ discovered.clear();
1063
+ else
1064
+ for (const locator of covered)
1065
+ discovered.delete(locator);
485
1066
  for (const room of listed)
486
1067
  discovered.set(room.locator, room);
487
- await dropUnlisted(listedLocators, local);
1068
+ const unlisted = unlistedHeld(new Set(found.map((room) => room.locator))).filter((locator) => covered === undefined || covered.includes(locator));
1069
+ if (options.adapter.exhaustiveDiscovery === true)
1070
+ await loseRooms(unlisted);
1071
+ else
1072
+ unlistedKept(unlisted);
488
1073
  for (const room of listed) {
489
1074
  const held = replicas.get(room.locator)?.confirmed.record;
490
1075
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -493,8 +1078,17 @@ export function composeRuntime(options) {
493
1078
  }
494
1079
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
495
1080
  }
1081
+ confirmLocks(listed);
496
1082
  roomsListed = true;
497
1083
  emitRooms();
1084
+ relocked(lockedBefore);
1085
+ return listed;
1086
+ }
1087
+ function heldLocally(locator) {
1088
+ return (replicas.has(locator) ||
1089
+ roomDrafts.has(locator) ||
1090
+ transfers.has(locator) ||
1091
+ queuesWithoutReplica.has(locator));
498
1092
  }
499
1093
  function unlistedHeld(listed) {
500
1094
  return [
@@ -506,40 +1100,85 @@ export function composeRuntime(options) {
506
1100
  ]),
507
1101
  ].filter((locator) => !listed.has(locator));
508
1102
  }
509
- async function dropUnlisted(listed, local) {
510
- if (unlistedHeld(listed).length === 0)
511
- return;
512
- await exclusive(async () => {
513
- const unlisted = unlistedHeld(listed);
514
- await local.dropRooms(unlisted);
515
- const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
516
- room: true,
517
- queue: true,
518
- drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
519
- }));
520
- for (const locator of unlisted) {
521
- for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
522
- pendings.delete(batch.id);
523
- replicas.delete(locator);
524
- queuesWithoutReplica.delete(locator);
1103
+ function unlistedKept(unlisted) {
1104
+ for (const locator of unlisted) {
1105
+ const held = replicas.get(locator)?.confirmed.record;
1106
+ if (held !== undefined)
1107
+ list(held, false);
1108
+ else
525
1109
  rooms.delete(locator);
526
- roomDrafts.delete(locator);
527
- transfers.delete(locator);
528
- unreadable.delete(locator);
529
- stale.delete(locator);
530
- scopedRefusals.delete(locator);
531
- sourceRefusals.delete(locator);
532
- declarationRefusals.delete(locator);
533
- for (const attached of ['collection', 'typed-selection']) {
534
- const key = modelKey(locator, attached);
535
- models.delete(key);
536
- liveModels.delete(key);
537
- invalidModels.delete(key);
538
- }
539
- journal.record(`room unlisted ${locator}`);
1110
+ }
1111
+ }
1112
+ function fence(locators) {
1113
+ const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1114
+ const removed = fresh.map((locator) => ({
1115
+ locator,
1116
+ shown: prepared.has(locator) || projecting.has(locator) ? (replicas.get(locator) ?? null) : null,
1117
+ drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
1118
+ }));
1119
+ for (const locator of fresh) {
1120
+ fences.set(locator, { at: discoveries, erased: false });
1121
+ lastFenced.set(locator, ++fencings);
1122
+ confirmed.delete(locator);
1123
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1124
+ pendings.delete(pendingKey(locator, batch.id));
1125
+ replicas.delete(locator);
1126
+ queuesWithoutReplica.delete(locator);
1127
+ rooms.delete(locator);
1128
+ roomDrafts.delete(locator);
1129
+ transfers.delete(locator);
1130
+ catching.delete(locator);
1131
+ discovered.delete(locator);
1132
+ announced.delete(locator);
1133
+ unreadable.delete(locator);
1134
+ stale.delete(locator);
1135
+ repairable.delete(locator);
1136
+ unindexable.delete(locator);
1137
+ paused.delete(locator);
1138
+ prepared.delete(locator);
1139
+ scopedRefusals.delete(locator);
1140
+ sourceRefusals.delete(locator);
1141
+ declarationRefusals.delete(locator);
1142
+ for (const attached of ['collection', 'typed-selection']) {
1143
+ const key = modelKey(locator, attached);
1144
+ models.delete(key);
1145
+ liveModels.delete(key);
1146
+ invalidModels.delete(key);
540
1147
  }
541
- publishChangeSet(mergeChangeSets(removed));
542
- moveTo({ ...status, ...attention() });
1148
+ journal.record(`room lost ${locator}`);
1149
+ }
1150
+ if (fresh.length === 0)
1151
+ return fresh;
1152
+ searchIndex.forget(fresh);
1153
+ const withdrawn = readSide.apply(stepped(withdrawing(removed)), (set) => conversations.changed(set));
1154
+ if (withdrawn instanceof Promise)
1155
+ void withdrawn.catch(unpublished);
1156
+ publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, null, null, { room: true, queue: true, drafts }))));
1157
+ readSide.recover();
1158
+ moveTo({ ...status, ...attention() });
1159
+ return fresh;
1160
+ }
1161
+ function* withdrawing(removed) {
1162
+ const sets = [];
1163
+ for (const { locator, shown } of removed)
1164
+ sets.push(yield* changing(locator, shown, null));
1165
+ return mergeChangeSets(sets);
1166
+ }
1167
+ async function erased(local, locators) {
1168
+ await local.erase(locators);
1169
+ for (const locator of locators) {
1170
+ const held = fences.get(locator);
1171
+ if (held !== undefined)
1172
+ held.erased = true;
1173
+ }
1174
+ }
1175
+ async function loseRooms(locators) {
1176
+ const fresh = fence(locators);
1177
+ if (fresh.length === 0)
1178
+ return;
1179
+ await durably(async (local) => {
1180
+ await erased(local, fresh);
1181
+ await unpair(local);
543
1182
  });
544
1183
  }
545
1184
  function restored(key, persisted) {
@@ -561,7 +1200,7 @@ export function composeRuntime(options) {
561
1200
  return model;
562
1201
  }
563
1202
  async function hydrate(opened) {
564
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
1203
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
565
1204
  opened.all(),
566
1205
  opened.queues(),
567
1206
  opened.roomDrafts(),
@@ -573,7 +1212,25 @@ export function composeRuntime(options) {
573
1212
  opened.capabilities(),
574
1213
  opened.member(),
575
1214
  opened.transfers(),
1215
+ opened.targets(),
576
1216
  ]);
1217
+ const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1218
+ const previousReplicas = new Map(replicas);
1219
+ const read = new Map();
1220
+ for (const record of records) {
1221
+ const model = restored(modelKey(record.locator, 'collection'), record.model);
1222
+ const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
1223
+ const prior = previousReplicas.get(record.locator);
1224
+ const confirmed = await confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
1225
+ const replica = await stepped(replicating(confirmed, queuedBatches.get(record.locator) ?? [], prior));
1226
+ await stepped(counting(replica));
1227
+ read.set(record.locator, replica);
1228
+ }
1229
+ const sets = new Map();
1230
+ for (const locator of new Set([...previousReplicas.keys(), ...read.keys()])) {
1231
+ sets.set(locator, await stepped(changing(locator, previousReplicas.get(locator) ?? null, read.get(locator) ?? null)));
1232
+ }
1233
+ targets = heldTargets;
577
1234
  transfers.clear();
578
1235
  for (const record of staged)
579
1236
  transfers.set(record.locator, record);
@@ -587,26 +1244,24 @@ export function composeRuntime(options) {
587
1244
  for (const record of presented)
588
1245
  presentations.set(record.app, record.value);
589
1246
  conversations.hydrate(held, chatDrafts);
590
- const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
591
1247
  pendings.clear();
592
1248
  for (const queue of queues) {
593
- for (const { batch, state } of queue.pending)
594
- pendings.set(batch.id, state);
1249
+ for (const { batch, state } of queue.pending) {
1250
+ pendings.set(pendingKey(queue.locator, batch.id), state);
1251
+ }
595
1252
  }
596
- const previousReplicas = new Map(replicas);
597
1253
  replicas.clear();
598
1254
  queuesWithoutReplica.clear();
599
- const replicated = new Set(records.map((record) => record.locator));
600
1255
  for (const [locator, queue] of queuedBatches) {
601
- if (!replicated.has(locator))
1256
+ if (!read.has(locator))
602
1257
  queuesWithoutReplica.set(locator, queue);
603
1258
  }
604
1259
  for (const record of records) {
605
- const model = restored(modelKey(record.locator, 'collection'), record.model);
606
- const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
607
- const prior = previousReplicas.get(record.locator);
608
- const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
609
- replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
1260
+ if (fences.has(record.locator))
1261
+ continue;
1262
+ const replica = read.get(record.locator);
1263
+ const { model, selectionModel } = replica.confirmed.record;
1264
+ replicas.set(record.locator, replica);
610
1265
  const existing = rooms.get(record.locator);
611
1266
  if (existing === undefined)
612
1267
  list(record, false);
@@ -629,12 +1284,13 @@ export function composeRuntime(options) {
629
1284
  return {
630
1285
  lastCompleted,
631
1286
  locators: new Set([...queuedBatches.keys(), ...replicas.keys()]),
632
- read: mergeChangeSets([...new Set([...previousReplicas.keys(), ...replicas.keys()])].map((locator) => changeSet(locator, previousReplicas.get(locator) ?? null, replicas.get(locator) ?? null))),
1287
+ read: mergeChangeSets([...sets].flatMap(([locator, set]) => (fences.has(locator) ? [] : [set]))),
633
1288
  };
634
1289
  }
635
1290
  async function readState(opened) {
636
1291
  const { lastCompleted, locators, read } = await hydrate(opened);
637
1292
  applyChangeSet(read);
1293
+ prepare();
638
1294
  if (replicas.size > 0)
639
1295
  emitRooms();
640
1296
  if (member !== null)
@@ -645,8 +1301,15 @@ export function composeRuntime(options) {
645
1301
  return lastCompleted;
646
1302
  }
647
1303
  async function open() {
648
- const opened = await openRuntimeStore();
1304
+ if (closed)
1305
+ throw new Error('runtime closed');
1306
+ const opened = await openRuntimeStore(isFenced, options.database);
649
1307
  store = opened;
1308
+ const interrupted = await opened.fences();
1309
+ for (const locator of interrupted) {
1310
+ fences.set(locator, { at: discoveries, erased: false });
1311
+ lastFenced.set(locator, ++fencings);
1312
+ }
650
1313
  const kept = options.shell.generation;
651
1314
  const storedGeneration = await opened.generation();
652
1315
  const loss = detectStorageLoss(storedGeneration, kept);
@@ -660,7 +1323,12 @@ export function composeRuntime(options) {
660
1323
  emitStorageLoss();
661
1324
  }
662
1325
  const lastCompleted = await readState(opened);
1326
+ if (interrupted.length > 0)
1327
+ await erased(opened, interrupted);
663
1328
  moveTo({ ...status, lastCompleted, ...attention() });
1329
+ void exclusive(() => unpair(opened)).catch(() => {
1330
+ journal.record('unpair failed');
1331
+ });
664
1332
  return opened;
665
1333
  }
666
1334
  function durably(work) {
@@ -681,27 +1349,92 @@ export function composeRuntime(options) {
681
1349
  mutations = next.then(() => undefined, () => undefined);
682
1350
  return next;
683
1351
  }
684
- function withdrawable(batch) {
685
- const state = pendings.get(batch.id);
686
- return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
1352
+ function withdrawable(locator) {
1353
+ return (batch) => {
1354
+ const state = pendings.get(pendingKey(locator, batch.id));
1355
+ return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
1356
+ };
1357
+ }
1358
+ function landedWhole(locator, batch, confirmed, current) {
1359
+ if (!landed(batch, confirmed, current))
1360
+ return false;
1361
+ const partner = partnerOf(batch, locator);
1362
+ if (partner === null)
1363
+ return true;
1364
+ const other = replicas.get(partner);
1365
+ const half = other?.queue.find((one) => one.id === batch.id);
1366
+ return other !== undefined && half !== undefined && landed(half, other.confirmed);
1367
+ }
1368
+ function carriedBack(locator, unlanded, written) {
1369
+ const back = new Map();
1370
+ const taken = [];
1371
+ for (const batch of unlanded) {
1372
+ const between = batch.between;
1373
+ if (between === undefined)
1374
+ continue;
1375
+ const source = between.source.room;
1376
+ const destination = between.destination.room;
1377
+ for (const { path: to } of pendings.get(pendingKey(destination, batch.id))?.drafts ?? []) {
1378
+ const record = roomDrafts.get(destination)?.get(to);
1379
+ const path = between.moves.find((move) => move.to === to)?.path;
1380
+ if (record === undefined || path === undefined)
1381
+ continue;
1382
+ const [into, key] = source === locator ? [written, path] : [back, documentKey(source, path)];
1383
+ const existing = into.get(key) ?? roomDrafts.get(source)?.get(path);
1384
+ taken.push({ locator: destination, path: to });
1385
+ into.set(key, {
1386
+ locator: source,
1387
+ path,
1388
+ revision: roomDraftRevision(),
1389
+ parts: returnedInto(existing?.parts, record.parts),
1390
+ returned: existing?.returned ?? null,
1391
+ });
1392
+ }
1393
+ }
1394
+ const records = [...back.values()];
1395
+ const elsewhere = taken.filter((one) => one.locator !== locator);
1396
+ const rooms = new Set([...records, ...elsewhere].map((one) => one.locator));
1397
+ return {
1398
+ taken: taken.filter((one) => one.locator === locator).map(({ path }) => path),
1399
+ writes: [...rooms].map((room) => ({
1400
+ locator: room,
1401
+ roomDrafts: records.filter((record) => record.locator === room),
1402
+ consumedRoomDrafts: elsewhere.filter((one) => one.locator === room).map(({ path }) => path),
1403
+ })),
1404
+ changes: [...rooms].map((room) => changeSet(room, null, null, {
1405
+ drafts: [...records, ...elsewhere]
1406
+ .filter((one) => one.locator === room)
1407
+ .map(({ path }) => path),
1408
+ })),
1409
+ remember() {
1410
+ for (const one of taken)
1411
+ forgetRoomDraft(one.locator, one.path);
1412
+ for (const record of records)
1413
+ rememberRoomDraft(record);
1414
+ },
1415
+ };
687
1416
  }
688
1417
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
689
1418
  const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
690
- const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
691
- const kept = queue.filter((batch) => !withdrawn.includes(batch));
1419
+ const { withdrawn, kept } = await stepped(standing(confirmed, queue, withdrawable(locator), localSaves));
1420
+ const founded = kept.filter((batch) => !queue.includes(batch));
1421
+ const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
692
1422
  const unqueued = withdrawn.map((batch) => ({
693
1423
  batch,
694
- returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
1424
+ landed: landedWhole(locator, batch, confirmed),
695
1425
  }));
696
1426
  const returning = [
697
- ...[...unqueued].reverse(),
1427
+ ...[...unqueued].reverse().map(({ batch, landed }) => ({
1428
+ batch,
1429
+ returned: localSaves || landed ? null : returnedBy('withdrawn'),
1430
+ })),
698
1431
  ...(settling.leaving === undefined ? [] : [settling.leaving]),
699
1432
  ];
700
1433
  const written = new Map();
701
1434
  for (const { batch, returned } of returning) {
702
- if (returned === null)
1435
+ if (returned === null || batch.between?.destination.room === locator)
703
1436
  continue;
704
- for (const saved of pendings.get(batch.id)?.drafts ?? []) {
1437
+ for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
705
1438
  const existing = written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path);
706
1439
  written.set(saved.path, {
707
1440
  locator,
@@ -712,13 +1445,28 @@ export function composeRuntime(options) {
712
1445
  });
713
1446
  }
714
1447
  }
715
- await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
1448
+ const carried = carriedBack(locator, [
1449
+ ...[...unqueued].reverse().flatMap(({ batch, landed }) => (landed ? [] : [batch])),
1450
+ ...(settling.leaving === undefined || settling.leaving.returned === null
1451
+ ? []
1452
+ : [settling.leaving.batch]),
1453
+ ], written);
1454
+ await local.keepAuthored({
1455
+ locator,
1456
+ queue: kept,
1457
+ founded,
1458
+ roomDrafts: [...written.values()],
1459
+ consumedRoomDrafts: carried.taken,
1460
+ }, ...carried.writes);
1461
+ const staying = new Set(kept.map((batch) => batch.id));
716
1462
  for (const batch of previousQueue) {
717
- if (!kept.includes(batch))
718
- pendings.delete(batch.id);
1463
+ if (!staying.has(batch.id))
1464
+ pendings.delete(pendingKey(locator, batch.id));
719
1465
  }
720
1466
  for (const record of written.values())
721
1467
  rememberRoomDraft(record);
1468
+ carried.remember();
1469
+ const drafts = [...written.keys(), ...carried.taken];
722
1470
  try {
723
1471
  await local.write({
724
1472
  next: confirmed.record,
@@ -727,31 +1475,38 @@ export function composeRuntime(options) {
727
1475
  });
728
1476
  }
729
1477
  catch (error) {
730
- const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
1478
+ const held = before === undefined ? null : await stepped(replicating(before.confirmed, kept, before));
1479
+ const set = await stepped(changing(locator, before ?? null, held, { queue: queueChanged, drafts }));
731
1480
  if (held !== null)
732
1481
  replicas.set(locator, held);
733
1482
  else if (kept.length > 0)
734
1483
  queuesWithoutReplica.set(locator, kept);
735
1484
  else
736
1485
  queuesWithoutReplica.delete(locator);
737
- publishChangeSet(changeSet(locator, before ?? null, held, {
738
- queue: kept.length !== previousQueue.length,
739
- drafts: [...written.keys()],
740
- }));
1486
+ publishChangeSet(mergeChangeSets([set, ...carried.changes]));
741
1487
  moveTo({ ...status, ...attention() });
742
1488
  throw error;
743
1489
  }
744
- queuesWithoutReplica.delete(locator);
745
- for (const { batch, returned } of unqueued) {
746
- const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
747
- journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
748
- }
749
- replicas.set(locator, replicaOf(confirmed, kept, before));
750
- return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
751
- room: settling.room,
752
- queue: kept.length !== previousQueue.length,
753
- drafts: [...written.keys()],
754
- });
1490
+ const changed = { room: settling.room, queue: queueChanged, drafts };
1491
+ const next = await stepped(replicating(confirmed, kept, before));
1492
+ await stepped(counting(next));
1493
+ const set = await stepped(changing(locator, before ?? null, next, changed));
1494
+ return () => {
1495
+ queuesWithoutReplica.delete(locator);
1496
+ for (const { batch, landed } of unqueued) {
1497
+ const outcome = localSaves ? 'dropped' : landed ? 'landed' : 'withdrawn';
1498
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1499
+ }
1500
+ replicas.set(locator, next);
1501
+ return mergeChangeSets([
1502
+ heldChange(locator, before ?? null, next, set, changed),
1503
+ ...carried.changes,
1504
+ ]);
1505
+ };
1506
+ }
1507
+ function heldChange(locator, before, next, set, changed) {
1508
+ const held = replicas.get(locator) ?? null;
1509
+ return held === next ? set : changeSet(locator, before, held, changed);
755
1510
  }
756
1511
  function adoptModels(locator, record) {
757
1512
  for (const [attached, model] of [
@@ -771,7 +1526,7 @@ export function composeRuntime(options) {
771
1526
  const taken = (answer, previous, attached) => {
772
1527
  if (answer === undefined)
773
1528
  return previous;
774
- const { state, refused } = attachedModel(answer, previous);
1529
+ const { state, refused } = attachedModel(answer, previous, attached === 'collection' ? roomTypes : NO_ROOM_TYPES);
775
1530
  if (refused !== null) {
776
1531
  const where = refused.location === undefined ? '' : ` ${refused.location}`;
777
1532
  journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
@@ -784,11 +1539,16 @@ export function composeRuntime(options) {
784
1539
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
785
1540
  };
786
1541
  }
787
- function publish(local, room, confirmed, landing = {}) {
1542
+ function publish(local, room, confirmed, landing, live) {
788
1543
  const { base, stored } = landing;
789
1544
  return exclusive(async () => {
790
- if (!continuing())
1545
+ if (stored !== undefined) {
1546
+ await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
1547
+ }
1548
+ if (!live()) {
1549
+ caughtUpTo(room.locator);
791
1550
  return true;
1551
+ }
792
1552
  const prior = replicas.get(room.locator);
793
1553
  if (base !== undefined &&
794
1554
  (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
@@ -799,32 +1559,58 @@ export function composeRuntime(options) {
799
1559
  const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
800
1560
  const roomVersion = confirmed.record.roomVersion;
801
1561
  const accepted = queue.filter((batch) => {
802
- const accepted = pendings.get(batch.id)?.accepted;
1562
+ const accepted = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
803
1563
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
804
1564
  });
805
- const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
806
- if (stored !== undefined)
1565
+ let settled;
1566
+ try {
1567
+ settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
1568
+ }
1569
+ catch (error) {
1570
+ if (!exceeded(error))
1571
+ throw error;
1572
+ await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
1573
+ caughtUpTo(room.locator);
1574
+ return true;
1575
+ }
1576
+ const set = settled();
1577
+ if (stored !== undefined) {
807
1578
  transfers.delete(room.locator);
1579
+ catching.delete(room.locator);
1580
+ }
808
1581
  for (const batch of accepted)
809
- pendings.delete(batch.id);
1582
+ pendings.delete(pendingKey(room.locator, batch.id));
810
1583
  stale.delete(room.locator);
1584
+ unindexable.delete(room.locator);
811
1585
  if (unreadable.delete(room.locator))
812
1586
  scopedRefusals.delete(room.locator);
813
1587
  sourceRefusals.delete(room.locator);
814
1588
  declarationRefusals.delete(room.locator);
815
1589
  list(room, true);
1590
+ if (prior === undefined)
1591
+ prepared.add(room.locator);
816
1592
  publishChangeSet(set);
1593
+ await unpair(local);
817
1594
  moveTo({ ...status, ...attention() });
1595
+ options.adapter.roomStream?.published(room.locator, roomVersion);
1596
+ if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1597
+ submitLater(room.locator);
1598
+ }
818
1599
  return true;
819
1600
  });
820
1601
  }
821
- async function saveRoomDrafts(locator, named, writtenBy) {
1602
+ async function savedRoom(locator) {
822
1603
  const local = await openLocal();
1604
+ await referenced(locator);
823
1605
  const replica = replicas.get(locator);
824
1606
  const room = rooms.get(locator);
825
1607
  if (replica === undefined || room === undefined) {
826
1608
  throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
827
1609
  }
1610
+ return { local, replica, room };
1611
+ }
1612
+ async function saveRoomDrafts(locator, named, writtenBy) {
1613
+ const target = await savedRoom(locator);
828
1614
  const saved = [];
829
1615
  for (const { path, revision } of named) {
830
1616
  const record = roomDrafts.get(locator)?.get(path);
@@ -834,12 +1620,180 @@ export function composeRuntime(options) {
834
1620
  if (!saved.some((draft) => draft.path === path))
835
1621
  saved.push({ path, parts: record.parts });
836
1622
  }
1623
+ return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
1624
+ }
1625
+ async function saveParts(locator, named, writtenBy) {
1626
+ const read = named.map(({ path, parts }) => ({ path, parts: readParts(parts) }));
1627
+ if (read.some(({ path }, at) => path === '' ||
1628
+ normalize(path) !== path ||
1629
+ read.findIndex((one) => one.path === path) !== at)) {
1630
+ throw runtimeError('room-draft-malformed');
1631
+ }
1632
+ const target = await savedRoom(locator);
1633
+ const saved = [];
1634
+ const files = [];
1635
+ for (const { path, parts } of read) {
1636
+ const file = parts.file === undefined
1637
+ ? undefined
1638
+ : await heldFile(parts.file.blob, binaryBase(target.replica.visible, path));
1639
+ if (parts.file !== undefined && file !== undefined) {
1640
+ files.push({ key: file.localId, blob: parts.file.blob });
1641
+ }
1642
+ saved.push({ path, parts: held(undefined, parts, file) });
1643
+ }
1644
+ return committed(locator, target, saved, new Set(), writtenBy, files);
1645
+ }
1646
+ function carriedDrafts(locator, saved, consumed) {
1647
+ const holding = roomDrafts.get(locator);
1648
+ return new Map(saved.flatMap(({ path, parts }) => {
1649
+ const record = holding?.get(path);
1650
+ if (parts.move === undefined || record === undefined || consumed.has(path))
1651
+ return [];
1652
+ const to = moveTarget(parts.move);
1653
+ if (to === path || holding?.has(to) === true)
1654
+ return [];
1655
+ return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
1656
+ }));
1657
+ }
1658
+ function sideOf(locator, replica, room) {
1659
+ const record = replica.confirmed.record;
1660
+ return {
1661
+ room: locator,
1662
+ handle: room.handle,
1663
+ baseRoomVersion: record.roomVersion,
1664
+ composition: compositionFor(record),
1665
+ operations: room.operations,
1666
+ providerOwnedPaths: room.providerOwnedPaths,
1667
+ visible: replica.visible,
1668
+ queued: queuedDocuments(replica.queue),
1669
+ clean: {
1670
+ text: record.documents,
1671
+ binaries: record.binaries,
1672
+ composition: compositionOf(record),
1673
+ },
1674
+ };
1675
+ }
1676
+ function movedInto(locator, saved) {
1677
+ const into = saved.find(({ parts }) => parts.move?.room !== undefined && parts.move.room !== locator);
1678
+ return into?.parts.move?.room ?? null;
1679
+ }
1680
+ async function committedMove(locator, destination, { local, replica, room }, saved, consumed, writtenBy, files) {
1681
+ const mixed = saved.find(({ parts }) => Object.keys(parts).length !== 1 || parts.move?.room !== destination);
1682
+ if (mixed !== undefined) {
1683
+ return { outcome: 'refused', code: 'mixed-room-move', retryable: false, file: mixed.path };
1684
+ }
1685
+ const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
1686
+ if (changed.length > 0)
1687
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
1688
+ await referenced(destination);
1689
+ const target = replicas.get(destination);
1690
+ const listedRoom = rooms.get(destination);
1691
+ if (target === undefined || listedRoom === undefined) {
1692
+ throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
1693
+ }
1694
+ const built = await buildMove({
1695
+ source: sideOf(locator, replica, room),
1696
+ destination: sideOf(destination, target, listedRoom),
1697
+ moves: saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) })),
1698
+ providerLimits: limits,
1699
+ local: localSaves,
1700
+ ...(writtenBy === undefined ? {} : { writtenBy }),
1701
+ });
1702
+ if (built.kind === 'refused')
1703
+ return { outcome: 'refused', ...built.refusal };
1704
+ const id = built.leaving.id;
1705
+ const carrying = saved.flatMap(({ path, parts }) => {
1706
+ const record = roomDrafts.get(locator)?.get(path);
1707
+ const to = moveTarget(parts.move);
1708
+ if (record === undefined || consumed.has(path) || roomDrafts.get(destination)?.has(to)) {
1709
+ return [];
1710
+ }
1711
+ const moved = {
1712
+ ...record,
1713
+ locator: destination,
1714
+ path: to,
1715
+ revision: roomDraftRevision(),
1716
+ };
1717
+ return [{ from: path, record: moved }];
1718
+ });
1719
+ const carried = carrying.map(({ record }) => record);
1720
+ const marked = carried.map(({ path }) => ({ path, parts: {} }));
1721
+ const left = [...consumed, ...carrying.map(({ from }) => from)];
1722
+ const leavingQueue = [...replica.queue, built.leaving];
1723
+ const arrivingQueue = [...target.queue, built.arriving];
1724
+ let fencedRoom;
1725
+ try {
1726
+ fencedRoom = await local.saveAuthored(files, {
1727
+ locator,
1728
+ queue: leavingQueue,
1729
+ saved: new Map([[id, saved]]),
1730
+ consumedRoomDrafts: left,
1731
+ }, {
1732
+ locator: destination,
1733
+ queue: arrivingQueue,
1734
+ saved: new Map([[id, marked]]),
1735
+ roomDrafts: carried,
1736
+ });
1737
+ }
1738
+ catch {
1739
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
1740
+ }
1741
+ if (fencedRoom !== null) {
1742
+ throw runtimeError('room-not-held', `No Local Replica for ${fencedRoom}`);
1743
+ }
1744
+ const nextSource = await stepped(replicating(replica.confirmed, leavingQueue, replica));
1745
+ const nextDestination = await stepped(replicating(target.confirmed, arrivingQueue, target));
1746
+ const sets = [
1747
+ await stepped(changing(locator, replica, nextSource, { queue: true, drafts: left })),
1748
+ await stepped(changing(destination, target, nextDestination, {
1749
+ queue: true,
1750
+ drafts: carried.map(({ path }) => path),
1751
+ })),
1752
+ ];
1753
+ if (fences.has(locator) || fences.has(destination)) {
1754
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1755
+ }
1756
+ pendings.set(pendingKey(locator, id), {
1757
+ drafts: saved,
1758
+ submission: { state: 'waiting' },
1759
+ lost: false,
1760
+ });
1761
+ pendings.set(pendingKey(destination, id), {
1762
+ drafts: marked,
1763
+ submission: { state: 'waiting' },
1764
+ lost: false,
1765
+ });
1766
+ for (const path of left)
1767
+ forgetRoomDraft(locator, path);
1768
+ for (const record of carried)
1769
+ rememberRoomDraft(record);
1770
+ replicas.set(locator, nextSource);
1771
+ replicas.set(destination, nextDestination);
1772
+ publishChangeSet(mergeChangeSets(sets));
1773
+ queuedSave(locator);
1774
+ return { outcome: 'saved', identifier: id };
1775
+ }
1776
+ function queuedSave(locator) {
1777
+ if (!localSaves)
1778
+ recorded = false;
1779
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
1780
+ submitLater(locator);
1781
+ }
1782
+ async function committed(locator, target, saved, consumed, writtenBy, files = []) {
1783
+ const into = movedInto(locator, saved);
1784
+ if (into !== null) {
1785
+ return committedMove(locator, into, target, saved, consumed, writtenBy, files);
1786
+ }
1787
+ const { local, replica, room } = target;
837
1788
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
838
1789
  if (changed.length > 0)
839
1790
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
840
1791
  const parts = saved.map(({ path, parts }) => changesOf(path, parts));
841
1792
  const changes = parts.flatMap(({ changes }) => changes);
842
- const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
1793
+ const visibleOver = function* (queue) {
1794
+ return (yield* replicating(replica.confirmed, queue, replica)).visible;
1795
+ };
1796
+ const requeue = await stepped(requeuing(replica.queue, changes, replica.visible, withdrawable(locator), visibleOver));
843
1797
  if (requeue.kind === 'refused')
844
1798
  return { outcome: 'refused', ...requeue.refusal };
845
1799
  const foreign = [...requeue.withdrawn.keys()].some((origin) => {
@@ -866,9 +1820,17 @@ export function composeRuntime(options) {
866
1820
  visible: replica.visible,
867
1821
  changes,
868
1822
  bases: parts.flatMap(({ bases }) => bases),
1823
+ queued: queuedDocuments(replica.queue),
869
1824
  writtenBy,
1825
+ clean: {
1826
+ text: replica.confirmed.record.documents,
1827
+ binaries: replica.confirmed.record.binaries,
1828
+ composition: compositionOf(replica.confirmed.record),
1829
+ },
870
1830
  };
871
- const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
1831
+ const checked = withdrawing
1832
+ ? await buildBatch({ ...input, queued: new Set([...input.queued, ...requeue.queued]) })
1833
+ : null;
872
1834
  if (checked?.kind === 'refused')
873
1835
  return { outcome: 'refused', ...checked.refusal };
874
1836
  const owners = [...requeue.owners].flatMap((origin) => {
@@ -884,13 +1846,15 @@ export function composeRuntime(options) {
884
1846
  };
885
1847
  }
886
1848
  const built = !withdrawing
887
- ? buildBatch(input)
1849
+ ? await buildBatch(input)
888
1850
  : requeue.changes.length === 0
889
1851
  ? null
890
- : buildBatch({
1852
+ : await buildBatch({
891
1853
  ...input,
892
- visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
1854
+ visible: (await stepped(replicating(replica.confirmed, requeue.queue, replica)))
1855
+ .visible,
893
1856
  changes: requeue.changes,
1857
+ queued: queuedDocuments(requeue.queue),
894
1858
  });
895
1859
  if (built?.kind === 'refused')
896
1860
  return { outcome: 'refused', ...built.refusal };
@@ -907,7 +1871,7 @@ export function composeRuntime(options) {
907
1871
  for (const [origin, replaced] of withdrawn) {
908
1872
  if (replaced === null)
909
1873
  continue;
910
- const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
1874
+ const carried = withdrawnDrafts(pendings.get(pendingKey(locator, origin))?.drafts ?? [], changes);
911
1875
  drafted.set(replaced.id, replaced === join?.batch
912
1876
  ? [
913
1877
  ...carried.map((held) => joinedDraft(held, saved)),
@@ -921,62 +1885,84 @@ export function composeRuntime(options) {
921
1885
  for (const replaced of withdrawn.values()) {
922
1886
  if (replaced === null || !batches.includes(replaced))
923
1887
  continue;
924
- const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
1888
+ const limited = await stepped(limiting(batches, replaced, limits, visibleOver));
925
1889
  if (limited !== null)
926
1890
  return { outcome: 'refused', ...limited };
927
1891
  }
928
- const consumed = saved.map(({ path }) => path);
1892
+ const carried = carriedDrafts(locator, saved, consumed);
1893
+ const left = [...consumed, ...carried.keys()];
1894
+ const arrived = [...carried.values()];
1895
+ let fencedRoom;
929
1896
  try {
930
- await local.keepAuthored({
1897
+ fencedRoom = await local.saveAuthored(files, {
931
1898
  locator,
932
1899
  queue: batches,
933
1900
  saved: drafted,
934
- consumedRoomDrafts: consumed,
1901
+ consumedRoomDrafts: left,
1902
+ roomDrafts: arrived,
935
1903
  });
936
1904
  }
937
1905
  catch {
938
1906
  return { outcome: 'refused', code: 'durable-write', retryable: true };
939
1907
  }
1908
+ if (fencedRoom !== null) {
1909
+ throw runtimeError('room-not-held', `No Local Replica for ${fencedRoom}`);
1910
+ }
1911
+ const next = await stepped(replicating(replica.confirmed, batches, replica));
1912
+ const set = await stepped(changing(locator, replica, next, {
1913
+ queue: true,
1914
+ drafts: [...left, ...arrived.map(({ path }) => path)],
1915
+ }));
1916
+ if (fences.has(locator))
1917
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
940
1918
  for (const origin of withdrawn.keys())
941
- pendings.delete(origin);
1919
+ pendings.delete(pendingKey(locator, origin));
942
1920
  for (const [id, drafts] of drafted) {
943
- pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
1921
+ pendings.set(pendingKey(locator, id), {
1922
+ drafts,
1923
+ submission: { state: 'waiting' },
1924
+ lost: false,
1925
+ });
944
1926
  }
945
- if (!localSaves)
946
- recorded = false;
947
- for (const path of consumed)
1927
+ for (const path of left)
948
1928
  forgetRoomDraft(locator, path);
949
- replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
950
- publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
951
- queue: true,
952
- drafts: consumed,
953
- }));
954
- moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
955
- submitLater(locator);
1929
+ for (const record of arrived)
1930
+ rememberRoomDraft(record);
1931
+ replicas.set(locator, next);
1932
+ publishChangeSet(set);
1933
+ queuedSave(locator);
956
1934
  const identifier = join?.batch.id ??
957
1935
  built?.batch.id ??
958
1936
  [...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
959
1937
  return { outcome: 'saved', identifier };
960
1938
  }
961
- async function mark(local, locator, identifier, submission, lost) {
962
- const state = pendings.get(identifier);
963
- if (!continuing() ||
964
- state === undefined ||
965
- (JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
966
- return;
1939
+ async function mark(local, locator, batch, submission, lost) {
1940
+ const partner = partnerOf(batch, locator);
1941
+ for (const held of partner === null ? [locator] : [locator, partner]) {
1942
+ const state = pendings.get(pendingKey(held, batch.id));
1943
+ if (!continuing() ||
1944
+ state === undefined ||
1945
+ (JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
1946
+ continue;
1947
+ }
1948
+ await local.submit(held, batch.id, submission, lost);
1949
+ if (fences.has(held))
1950
+ continue;
1951
+ pendings.set(pendingKey(held, batch.id), { ...state, submission, lost });
1952
+ const replica = replicas.get(held);
1953
+ if (replica !== undefined)
1954
+ publishChangeSet(changeSet(held, replica, replica, { queue: true }));
967
1955
  }
968
- await local.submit(locator, identifier, submission, lost);
969
- pendings.set(identifier, { ...state, submission, lost });
970
- const replica = replicas.get(locator);
971
- if (replica !== undefined)
972
- publishChangeSet(changeSet(locator, replica, replica, { queue: true }));
973
1956
  moveTo({ ...status, ...attention() });
974
1957
  }
975
1958
  async function keepUpload(local, locator, identifier, upload) {
976
1959
  if (!continuing())
977
1960
  throw new Error('connection refused');
978
1961
  await local.keepUpload(locator, identifier, upload);
979
- pendings.set(identifier, { ...pendings.get(identifier), upload });
1962
+ if (fences.has(locator))
1963
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1964
+ const key = pendingKey(locator, identifier);
1965
+ pendings.set(key, { ...pendings.get(key), upload });
980
1966
  if (!continuing())
981
1967
  throw new Error('connection refused');
982
1968
  }
@@ -991,6 +1977,7 @@ export function composeRuntime(options) {
991
1977
  }
992
1978
  if (closed || authorization === undefined || refusal !== null)
993
1979
  return UNCERTAIN;
1980
+ const since = fencings;
994
1981
  const transfer = ['transfer', 'history', locator, path];
995
1982
  if (query.revision !== undefined)
996
1983
  transfer.push(query.revision);
@@ -1005,6 +1992,8 @@ export function composeRuntime(options) {
1005
1992
  ...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
1006
1993
  });
1007
1994
  journal.record(`outcome history ${result.kind}`);
1995
+ if (fencedSince(locator, since))
1996
+ return failure({ kind: 'room-not-readable', locator });
1008
1997
  if (result.kind === 'scoped-refusal') {
1009
1998
  const room = replicas.get(result.locator)?.confirmed.record;
1010
1999
  if (room !== undefined)
@@ -1019,15 +2008,61 @@ export function composeRuntime(options) {
1019
2008
  return UNCERTAIN;
1020
2009
  }
1021
2010
  }
1022
- function queuedBlob(locator, path, localId) {
2011
+ function located(room, path, destination) {
2012
+ const crossing = roomTarget(destination);
2013
+ if (crossing === null) {
2014
+ const named = target(path, destination);
2015
+ return named === null || !rooms.has(room) ? null : { room, path: named };
2016
+ }
2017
+ const held = [...rooms.values()].find((listed) => listed.handle === crossing.handle);
2018
+ return held === undefined ? null : { room: held.locator, path: crossing.path };
2019
+ }
2020
+ async function resolveReference(room, path, destination) {
2021
+ if (roomTarget(destination) === null && target(path, destination) === null) {
2022
+ return ABSENT_TARGET;
2023
+ }
2024
+ const at = located(room, path, destination);
2025
+ if (at === null)
2026
+ return UNREADABLE;
2027
+ if (locked(at.room))
2028
+ return LOCKED_UNREADABLE;
2029
+ const visible = replicas.get(at.room)?.visible;
2030
+ if (visible?.documents[at.path] !== undefined || visible?.binaries[at.path] !== undefined) {
2031
+ return { kind: 'document', ...at };
2032
+ }
2033
+ const history = await readHistory(at.room, at.path);
2034
+ if (history.kind === 'no-history' || history.kind === 'revision')
2035
+ return ABSENT_TARGET;
2036
+ if (history.kind !== 'history')
2037
+ return history;
2038
+ const [last] = history.revisions;
2039
+ if (last === undefined)
2040
+ return ABSENT_TARGET;
2041
+ return history.deleted === true
2042
+ ? { kind: 'deleted', ...at, revision: last }
2043
+ : { kind: 'document', ...at };
2044
+ }
2045
+ function queuedFacts(locator, path, localId) {
1023
2046
  for (const batch of replicas.get(locator)?.queue ?? []) {
1024
2047
  const change = batch.changes[queuedFile(batch, path, localId)];
1025
2048
  if (change?.kind === 'binary')
1026
- return change.blob;
2049
+ return change;
1027
2050
  }
1028
- return null;
2051
+ const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2052
+ return drafted?.localId === localId ? drafted : null;
2053
+ }
2054
+ function batchFile(batch) {
2055
+ return async (localId) => {
2056
+ const change = batch.changes.find((held) => held.kind === 'binary' && held.localId === localId);
2057
+ return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
2058
+ };
1029
2059
  }
1030
- async function readBinary(request) {
2060
+ function binaryRoom(request) {
2061
+ if (request.kind === 'path')
2062
+ return request.locator;
2063
+ return options.adapter.referrerScope(request.referrer)?.locator ?? null;
2064
+ }
2065
+ async function transferBinary(request, transfer, answer) {
1031
2066
  if (closed || refusal !== null)
1032
2067
  return UNCERTAIN;
1033
2068
  try {
@@ -1038,41 +2073,51 @@ export function composeRuntime(options) {
1038
2073
  }
1039
2074
  if (closed || authorization === undefined || refusal !== null)
1040
2075
  return UNCERTAIN;
2076
+ const since = fencings;
1041
2077
  journal.record(request.kind === 'path'
1042
2078
  ? `transfer binary ${request.locator} ${request.path}`
1043
2079
  : `transfer binary ${request.fileRef}`);
1044
2080
  try {
1045
- const result = await options.adapter.binary(authorization, request);
1046
- journal.record(`outcome binary ${result.kind}`);
1047
- if (result.kind === 'scoped-refusal') {
1048
- const room = replicas.get(result.locator)?.confirmed.record;
2081
+ const result = await transfer(authorization);
2082
+ const settled = result;
2083
+ journal.record(`outcome binary ${settled.kind}`);
2084
+ const locator = binaryRoom(request);
2085
+ if (locator !== null && fencedSince(locator, since))
2086
+ return failure({ kind: 'room-not-readable', locator });
2087
+ if (settled.kind === 'scoped-refusal') {
2088
+ const room = replicas.get(settled.locator)?.confirmed.record;
1049
2089
  if (room !== undefined)
1050
- await scoped(result, room, await openLocal());
2090
+ await scoped(settled, room, await openLocal());
1051
2091
  }
1052
- if (result.kind === 'revoked-authorization')
2092
+ if (settled.kind === 'revoked-authorization')
1053
2093
  await refuse('revoked-authorization');
1054
- return binaryAnswer(result);
2094
+ return answer(result);
1055
2095
  }
1056
2096
  catch {
1057
2097
  journal.record('outcome binary transport-uncertainty');
1058
2098
  return UNCERTAIN;
1059
2099
  }
1060
2100
  }
1061
- async function readBinaryPath(locator, path, revision) {
1062
- await ready;
2101
+ async function readBinary(request, name) {
2102
+ if (closed)
2103
+ return UNCERTAIN;
2104
+ const held = await keptFiles.find(name);
2105
+ if (held !== null)
2106
+ return held;
2107
+ return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2108
+ }
2109
+ function binaryPath(locator, path, revision) {
1063
2110
  if (revision !== undefined)
1064
- return readBinary({ kind: 'path', locator, path, revision });
2111
+ return { kind: 'path', locator, path, revision };
1065
2112
  const visible = replicas.get(locator)?.visible;
1066
2113
  const held = visible?.binaries[path];
1067
2114
  if (visible === undefined || held === undefined)
1068
- return absentBinary(path);
1069
- if (held.state === 'pending') {
1070
- const blob = queuedBlob(locator, path, held.localId);
1071
- return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1072
- }
2115
+ return null;
2116
+ if (held.state === 'pending')
2117
+ return { kind: 'pending', localId: held.localId };
1073
2118
  const record = binaryRecord(path, held, visible.revisions[path]);
1074
2119
  if (record.state !== 'accepted')
1075
- return absentBinary(path);
2120
+ return null;
1076
2121
  const replica = replicas.get(locator);
1077
2122
  const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
1078
2123
  const holds = (at) => {
@@ -1087,21 +2132,55 @@ export function composeRuntime(options) {
1087
2132
  if (change.kind === 'move' && change.to === confirmed)
1088
2133
  confirmed = change.path;
1089
2134
  }
1090
- return readBinary({
2135
+ return {
1091
2136
  kind: 'path',
1092
2137
  locator,
1093
2138
  path: confirmed,
1094
2139
  revision: record.revision,
1095
2140
  fileRef: record.fileRef,
1096
- });
2141
+ };
2142
+ }
2143
+ async function readBinaryPath(locator, path, revision) {
2144
+ await ready;
2145
+ const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2146
+ const asked = drafted !== undefined && drafted.localId === revision
2147
+ ? { kind: 'pending', localId: revision }
2148
+ : binaryPath(locator, path, revision);
2149
+ if (asked === null)
2150
+ return absentBinary(path);
2151
+ if (asked.kind === 'pending') {
2152
+ const facts = queuedFacts(locator, path, asked.localId);
2153
+ return facts === null
2154
+ ? absentBinary(path)
2155
+ : {
2156
+ kind: 'binary',
2157
+ file: asked.localId,
2158
+ byteLength: facts.byteLength,
2159
+ mediaType: facts.mediaType,
2160
+ };
2161
+ }
2162
+ return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
2163
+ }
2164
+ async function linkBinaryPath(locator, path, revision) {
2165
+ await ready;
2166
+ const asked = binaryPath(locator, path, revision);
2167
+ if (asked === null || asked.kind === 'pending')
2168
+ return absentBinary(path);
2169
+ return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
1097
2170
  }
1098
2171
  async function readBinaryReference(reference) {
1099
2172
  await ready;
1100
- return readBinary({
1101
- kind: 'reference',
1102
- fileRef: reference.fileRef,
1103
- referrer: reference.referrer,
1104
- });
2173
+ const scope = options.adapter.referrerScope(reference.referrer);
2174
+ if (scope === null)
2175
+ return absentReference(reference.fileRef);
2176
+ return readBinary({ kind: 'reference', fileRef: reference.fileRef, referrer: reference.referrer }, { locator: scope.locator, path: scope.path ?? '', revision: reference.fileRef });
2177
+ }
2178
+ function readPart(file, part) {
2179
+ return options.database.ask({ kind: 'file.part', key: file, part });
2180
+ }
2181
+ async function cleanUp() {
2182
+ const freed = await options.database.ask({ kind: 'opened.clean' });
2183
+ return freed + ((await options.shell.cleanUp?.()) ?? 0);
1105
2184
  }
1106
2185
  function declaredAgents() {
1107
2186
  for (const capability of capabilities) {
@@ -1119,7 +2198,14 @@ export function composeRuntime(options) {
1119
2198
  }
1120
2199
  return undefined;
1121
2200
  }
1122
- async function invokeAction(name, input) {
2201
+ function calledRooms(name, input) {
2202
+ const member = declaredAction(name)?.room;
2203
+ if (member === undefined || input === null || typeof input !== 'object')
2204
+ return [];
2205
+ const named = input[member];
2206
+ return (Array.isArray(named) ? named : [named]).filter((room) => typeof room === 'string');
2207
+ }
2208
+ async function invokeAction(name, input, dispatching) {
1123
2209
  await ready;
1124
2210
  if (closed || refusal !== null)
1125
2211
  return UNCERTAIN;
@@ -1133,6 +2219,7 @@ export function composeRuntime(options) {
1133
2219
  }
1134
2220
  if (closed || authorization === undefined || refusal !== null)
1135
2221
  return UNCERTAIN;
2222
+ await dispatching?.();
1136
2223
  journal.record(`transfer action ${name}`);
1137
2224
  try {
1138
2225
  const result = await options.adapter.action(authorization, { name, input });
@@ -1153,6 +2240,60 @@ export function composeRuntime(options) {
1153
2240
  return UNCERTAIN;
1154
2241
  }
1155
2242
  }
2243
+ function unsettled(result) {
2244
+ return result.kind !== 'action' && result.retryable;
2245
+ }
2246
+ async function recoveredAction(name, input) {
2247
+ const operation = operationOf(input);
2248
+ if (operation === null)
2249
+ return invokeAction(name, input);
2250
+ const kept = async () => (await openLocal()).keepCall({ operation, name, input });
2251
+ calling.add(operation);
2252
+ try {
2253
+ let result = await invokeAction(name, input, kept);
2254
+ if (unsettled(result)) {
2255
+ await recovery.attempt(`action ${operation}`, async () => {
2256
+ if (closed || refusal !== null)
2257
+ return 'done';
2258
+ if (!socketOpen)
2259
+ return 'again';
2260
+ result = await invokeAction(name, input, kept);
2261
+ return unsettled(result) ? 'again' : 'done';
2262
+ }, { again: true });
2263
+ }
2264
+ if (!unsettled(result))
2265
+ await (await openLocal()).dropCall(operation);
2266
+ return result;
2267
+ }
2268
+ finally {
2269
+ calling.delete(operation);
2270
+ }
2271
+ }
2272
+ async function resendCalls(signal) {
2273
+ if (!socketOpen || closed || stopping)
2274
+ return 'done';
2275
+ const local = await openLocal();
2276
+ for (const held of await local.calls()) {
2277
+ if (calling.has(held.operation))
2278
+ continue;
2279
+ if (closed || signal.aborted)
2280
+ return 'done';
2281
+ if (pastHorizon(held.operation)) {
2282
+ await local.dropCall(held.operation);
2283
+ continue;
2284
+ }
2285
+ calling.add(held.operation);
2286
+ try {
2287
+ if (unsettled(await invokeAction(held.name, held.input)))
2288
+ return 'again';
2289
+ await local.dropCall(held.operation);
2290
+ }
2291
+ finally {
2292
+ calling.delete(held.operation);
2293
+ }
2294
+ }
2295
+ return 'done';
2296
+ }
1156
2297
  function continuing() {
1157
2298
  return !closed && !discoveryPaused;
1158
2299
  }
@@ -1167,11 +2308,12 @@ export function composeRuntime(options) {
1167
2308
  options.adapter.reached(locator, written, roomVersion) &&
1168
2309
  !options.adapter.reached(locator, roomVersion, written));
1169
2310
  }
1170
- async function pullRefresh(authorized, confirmed) {
1171
- if (!continuing())
2311
+ async function pullRefresh(authorized, confirmed, live) {
2312
+ if (!live())
1172
2313
  return unconfirmed;
2314
+ const turn = generation;
1173
2315
  try {
1174
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
2316
+ const result = await refreshRoom(reads, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1175
2317
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
1176
2318
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
1177
2319
  return result;
@@ -1180,48 +2322,101 @@ export function composeRuntime(options) {
1180
2322
  journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
1181
2323
  throw error;
1182
2324
  }
2325
+ finally {
2326
+ reads.finished(confirmed.record.locator, turn);
2327
+ }
1183
2328
  }
1184
- function staging(local) {
2329
+ function staging(local, live) {
1185
2330
  return {
1186
2331
  held: (locator) => transfers.get(locator) ?? null,
1187
2332
  staged: (record) => local.staged(record),
1188
2333
  stage: (record, page) => exclusive(async () => {
1189
- if (!continuing())
2334
+ if (!live())
2335
+ return false;
2336
+ const bytes = layerBytes(page);
2337
+ if (!(await admitted(local, record.locator, bytes)))
1190
2338
  return false;
1191
- await local.stage(record, page);
2339
+ try {
2340
+ await local.stage(record, page);
2341
+ }
2342
+ catch (error) {
2343
+ if (!exceeded(error))
2344
+ throw error;
2345
+ await exceededOn(record.locator, bytes);
2346
+ return false;
2347
+ }
1192
2348
  transfers.set(record.locator, record);
2349
+ readSide.recover();
1193
2350
  return true;
1194
2351
  }),
1195
2352
  abandon: (locator) => exclusive(async () => {
1196
- if (!continuing())
2353
+ if (!live())
1197
2354
  return;
1198
2355
  await local.abandon(locator);
1199
2356
  transfers.delete(locator);
2357
+ readSide.recover();
1200
2358
  }),
1201
2359
  };
1202
2360
  }
1203
- async function pullBootstrap(authorized, room, local) {
1204
- if (!continuing())
2361
+ function reusable(room) {
2362
+ const record = replicas.get(room.locator)?.confirmed.record;
2363
+ if (record === undefined || record.replica.selection !== room.replica.selection)
2364
+ return null;
2365
+ return { documents: record.documents, revisions: record.revisions };
2366
+ }
2367
+ async function pullBootstrap(authorized, room, local, live) {
2368
+ if (!live())
1205
2369
  return unconfirmed;
2370
+ const turn = generation;
2371
+ if (replicas.has(room.locator)) {
2372
+ catching.add(room.locator);
2373
+ readSide.recover();
2374
+ }
1206
2375
  try {
1207
- const result = await bootstrapRoom(options.adapter, authorized, room, {
2376
+ const result = await bootstrapRoom(reads, authorized, room, {
1208
2377
  collection: modelOf(room.locator),
1209
2378
  selection: modelOf(room.locator, 'typed-selection'),
1210
- }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
2379
+ }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
1211
2380
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
1212
2381
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
2382
+ if (result.kind !== 'staged')
2383
+ caughtUpTo(room.locator);
1213
2384
  return result;
1214
2385
  }
1215
2386
  catch (error) {
1216
2387
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
2388
+ caughtUpTo(room.locator);
1217
2389
  throw error;
1218
2390
  }
2391
+ finally {
2392
+ reads.finished(room.locator, turn);
2393
+ }
2394
+ }
2395
+ function caughtUpTo(locator) {
2396
+ if (catching.delete(locator))
2397
+ readSide.recover();
2398
+ }
2399
+ function behind(room) {
2400
+ const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
2401
+ return (held !== undefined &&
2402
+ [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
2403
+ }
2404
+ async function advance(room, local, authorized, live) {
2405
+ const carried = reads.carried(room.locator);
2406
+ const withheld = await advanceOnce(room, local, authorized, live);
2407
+ if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
2408
+ return withheld;
2409
+ }
2410
+ return advanceOnce(room, local, authorized, live);
1219
2411
  }
1220
- async function advance(room, local, authorized) {
2412
+ async function advanceOnce(room, local, authorized, live) {
2413
+ await referenced(room.locator);
2414
+ if (!live() || paused.has(room.locator))
2415
+ return unconfirmed;
1221
2416
  const held = replicas.get(room.locator);
1222
2417
  if (held !== undefined) {
1223
- const refreshed = await pullRefresh(authorized, held.confirmed);
1224
- if (!continuing())
2418
+ const refreshed = await pullRefresh(authorized, held.confirmed, live);
2419
+ if (!live())
1225
2420
  return unconfirmed;
1226
2421
  if (refreshed.kind === 'still-current') {
1227
2422
  stale.delete(room.locator);
@@ -1235,31 +2430,47 @@ export function composeRuntime(options) {
1235
2430
  return null;
1236
2431
  }
1237
2432
  if (refreshed.kind === 'advanced') {
1238
- if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
2433
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
1239
2434
  const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
1240
2435
  const reached = now !== undefined &&
1241
2436
  options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
1242
- return reached ? null : advance(room, local, authorized);
2437
+ return reached ? null : advance(room, local, authorized, live);
1243
2438
  }
1244
2439
  await retainModel(local, room, 'collection', modelOf(room.locator));
1245
2440
  return null;
1246
2441
  }
1247
2442
  if (refreshed.kind !== 'beyond-horizon')
1248
2443
  return refreshed;
2444
+ if (opened !== null) {
2445
+ owed.add(room.locator);
2446
+ readSide.recover();
2447
+ return unconfirmed;
2448
+ }
2449
+ if (reloadsDue.get(room.locator) === generation)
2450
+ return unconfirmed;
1249
2451
  }
1250
- const bootstrapped = await pullBootstrap(authorized, room, local);
1251
- if (!continuing())
2452
+ const bootstrapped = await pullBootstrap(authorized, room, local, live);
2453
+ if (!live())
1252
2454
  return unconfirmed;
1253
2455
  if (bootstrapped.kind !== 'staged')
1254
2456
  return bootstrapped;
1255
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
2457
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
1256
2458
  return null;
1257
2459
  }
1258
2460
  async function withhold(withheld, room, local, progress) {
2461
+ if (withheld.kind === 'unconfirmed')
2462
+ return;
1259
2463
  if (withheld.kind === 'refused')
1260
2464
  stale.delete(room.locator);
1261
2465
  else
1262
2466
  stale.add(room.locator);
2467
+ if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
2468
+ const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
2469
+ repairable.set(room.locator, reset);
2470
+ repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
2471
+ }
2472
+ else
2473
+ repairable.delete(room.locator);
1263
2474
  if (withheld.kind === 'scoped-refusal') {
1264
2475
  await scoped(withheld, room, local);
1265
2476
  }
@@ -1295,6 +2506,7 @@ export function composeRuntime(options) {
1295
2506
  unreadable.add(room.locator);
1296
2507
  list(room, false);
1297
2508
  emitRooms();
2509
+ moveTo({ ...status, ...attention() });
1298
2510
  journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
1299
2511
  }
1300
2512
  function leaving(batch, result) {
@@ -1316,6 +2528,7 @@ export function composeRuntime(options) {
1316
2528
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1317
2529
  }
1318
2530
  async function adoptAccepted(room, local, submitted, accepted) {
2531
+ await referenced(room.locator);
1319
2532
  if (!continuing())
1320
2533
  return unconfirmed;
1321
2534
  const current = replicas.get(room.locator);
@@ -1326,75 +2539,217 @@ export function composeRuntime(options) {
1326
2539
  logAccepted(room.locator, submitted, accepted);
1327
2540
  const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1328
2541
  ? { kind: 'staged', confirmed: current.confirmed }
1329
- : applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
2542
+ : await applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
1330
2543
  ? accepted.roomVersion
1331
2544
  : held, accepted.roomVersion);
1332
2545
  if (applied.kind !== 'staged') {
1333
- const state = pendings.get(submitted.id);
2546
+ const key = pendingKey(room.locator, submitted.id);
2547
+ const state = pendings.get(key);
1334
2548
  await local.accept(room.locator, submitted.id, accepted.roomVersion);
1335
- if (state !== undefined)
1336
- pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
2549
+ if (state !== undefined && !fences.has(room.locator))
2550
+ pendings.set(key, { ...state, accepted: accepted.roomVersion });
1337
2551
  emitQueue(room.locator);
1338
2552
  return applied;
1339
2553
  }
1340
- const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1341
- pendings.delete(submitted.id);
2554
+ const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
2555
+ pendings.delete(pendingKey(room.locator, submitted.id));
1342
2556
  unreadable.delete(room.locator);
1343
2557
  sourceRefusals.delete(room.locator);
1344
2558
  declarationRefusals.delete(room.locator);
2559
+ const set = settled();
1345
2560
  list(room, true);
1346
2561
  publishChangeSet(set);
1347
2562
  moveTo({ ...status, ...attention() });
1348
2563
  return null;
1349
2564
  }
1350
2565
  async function adopted(room, local, submitted, accepted) {
1351
- const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
2566
+ const failed = await exclusive(async () => {
2567
+ const adopting = await adoptAccepted(room, local, submitted, accepted);
2568
+ await unpair(local);
2569
+ return adopting;
2570
+ });
2571
+ if (failed === null)
2572
+ return true;
2573
+ if (failed.kind === 'refused' && continuing())
2574
+ refusedSource(room, failed);
2575
+ return false;
2576
+ }
2577
+ async function adoptedMove(room, partner, local, submitted, accepted) {
2578
+ const other = discovered.get(partner);
2579
+ const failed = await exclusive(async () => {
2580
+ const left = await adoptAccepted(room, local, submitted, accepted);
2581
+ const arriving = replicas.get(partner)?.queue.find((batch) => batch.id === submitted.id);
2582
+ const arrived = other === undefined || arriving === undefined || accepted.destination === undefined
2583
+ ? null
2584
+ : await adoptAccepted(other, local, arriving, {
2585
+ kind: 'accepted',
2586
+ ...accepted.destination,
2587
+ });
2588
+ if (other !== undefined && arrived?.kind === 'refused' && continuing()) {
2589
+ refusedSource(other, arrived);
2590
+ }
2591
+ await unpair(local);
2592
+ return left;
2593
+ });
2594
+ submitLater(partner);
1352
2595
  if (failed === null)
1353
2596
  return true;
1354
2597
  if (failed.kind === 'refused' && continuing())
1355
2598
  refusedSource(room, failed);
1356
2599
  return false;
1357
2600
  }
1358
- async function submitRoom(room, local, authorized, progress) {
1359
- while (continuing() && authorization !== undefined) {
2601
+ function queueIn(locator) {
2602
+ return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
2603
+ }
2604
+ function behindIn(locator, batch) {
2605
+ const held = replicas.get(locator)?.queue;
2606
+ if (held === undefined)
2607
+ return queuesWithoutReplica.get(locator) !== undefined;
2608
+ return held.findIndex((one) => one.id === batch.id) > 0;
2609
+ }
2610
+ function unpaired(skipped) {
2611
+ for (const locator of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
2612
+ for (const batch of queueIn(locator)) {
2613
+ const partner = partnerOf(batch, locator);
2614
+ if (partner === null || skipped.has(pendingKey(locator, batch.id)))
2615
+ continue;
2616
+ if (queueIn(partner).some((held) => held.id === batch.id))
2617
+ continue;
2618
+ if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
2619
+ continue;
2620
+ if (leavingIn(batch, locator) && !withdrawable(locator)(batch))
2621
+ continue;
2622
+ return { locator, batch };
2623
+ }
2624
+ }
2625
+ return null;
2626
+ }
2627
+ async function unpair(local) {
2628
+ const handled = new Set();
2629
+ for (let held = unpaired(handled); held !== null && continuing(); held = unpaired(handled)) {
2630
+ const { locator, batch } = held;
2631
+ handled.add(pendingKey(locator, batch.id));
2632
+ journal.record(`outcome ${locator} ${batch.id} ${localSaves ? 'dropped' : 'unpaired'}`);
2633
+ const leaving = {
2634
+ batch,
2635
+ returned: !localSaves && leavingIn(batch, locator) ? returnedBy('withdrawn') : null,
2636
+ };
2637
+ const current = replicas.get(locator);
2638
+ if (current !== undefined) {
2639
+ const settled = await settle(local, locator, current, current.confirmed, current.queue.filter((one) => one.id !== batch.id), { leaving });
2640
+ publishChangeSet(settled());
2641
+ continue;
2642
+ }
2643
+ const queue = queueIn(locator).filter((one) => one.id !== batch.id);
2644
+ const written = new Map((leaving.returned === null
2645
+ ? []
2646
+ : (pendings.get(pendingKey(locator, batch.id))?.drafts ?? [])).map((saved) => [
2647
+ saved.path,
2648
+ {
2649
+ locator,
2650
+ path: saved.path,
2651
+ revision: roomDraftRevision(),
2652
+ parts: returnedInto(roomDrafts.get(locator)?.get(saved.path)?.parts, saved.parts),
2653
+ returned: leaving.returned,
2654
+ },
2655
+ ]));
2656
+ const carried = carriedBack(locator, leaving.returned === null ? [] : [batch], written);
2657
+ await local.keepAuthored({
2658
+ locator,
2659
+ queue,
2660
+ roomDrafts: [...written.values()],
2661
+ consumedRoomDrafts: carried.taken,
2662
+ }, ...carried.writes);
2663
+ pendings.delete(pendingKey(locator, batch.id));
2664
+ if (queue.length > 0)
2665
+ queuesWithoutReplica.set(locator, queue);
2666
+ else
2667
+ queuesWithoutReplica.delete(locator);
2668
+ for (const record of written.values())
2669
+ rememberRoomDraft(record);
2670
+ carried.remember();
2671
+ publishChangeSet(mergeChangeSets([
2672
+ changeSet(locator, null, null, {
2673
+ queue: true,
2674
+ drafts: [...written.keys(), ...carried.taken],
2675
+ }),
2676
+ ...carried.changes,
2677
+ ]));
2678
+ }
2679
+ if (handled.size === 0)
2680
+ return;
2681
+ moveTo({ ...status, ...attention() });
2682
+ for (const key of handled)
2683
+ submitLater(key.slice(0, key.indexOf('\u0000')));
2684
+ }
2685
+ async function discardHeld(head, room, local, signal) {
2686
+ const partner = partnerOf(head, room.locator);
2687
+ const other = partner === null ? undefined : discovered.get(partner);
2688
+ await discardPending(room, local, signal);
2689
+ if (other !== undefined && !signal.aborted)
2690
+ await discardPending(other, local, signal);
2691
+ }
2692
+ async function submitRoom(room, local, authorized, progress, signal) {
2693
+ while (continuing() && authorization !== undefined && !signal.aborted) {
1360
2694
  const replica = replicas.get(room.locator);
1361
2695
  const head = replica?.queue[0];
1362
2696
  if (replica === undefined || head === undefined)
1363
- return;
1364
- if (pendings.get(head.id)?.accepted !== undefined)
1365
- return;
1366
- const state = pendings.get(head.id);
2697
+ return 'done';
2698
+ const state = pendings.get(pendingKey(room.locator, head.id));
2699
+ if (state?.accepted !== undefined)
2700
+ return 'done';
2701
+ const partner = partnerOf(head, room.locator);
2702
+ if (partner !== null && !leavingIn(head, room.locator)) {
2703
+ submitLater(partner);
2704
+ return 'done';
2705
+ }
2706
+ if (partner !== null && behindIn(partner, head)) {
2707
+ progress.complete = false;
2708
+ return 'done';
2709
+ }
1367
2710
  const { submission, lost: earlier } = state ?? {
1368
2711
  submission: { state: 'waiting' },
1369
2712
  lost: false,
1370
2713
  };
1371
2714
  if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
1372
2715
  progress.complete = false;
1373
- return;
2716
+ return { at: Date.parse(submission.reset) };
1374
2717
  }
1375
2718
  if (writesHeld()) {
1376
2719
  progress.complete = false;
1377
2720
  journal.record(`submission ${room.locator} ${head.id} held`);
1378
- return;
2721
+ return 'done';
1379
2722
  }
1380
2723
  const lost = earlier || submission.state === 'submitted';
2724
+ if (lost && pastHorizon(head.id)) {
2725
+ await discardHeld(head, room, local, signal);
2726
+ return 'done';
2727
+ }
1381
2728
  let result;
1382
2729
  journal.record(`submission ${room.locator} ${head.id}`);
1383
- await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1384
- if (discoveryPaused)
1385
- return;
1386
- if (writesHeld()) {
1387
- await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
2730
+ await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
2731
+ if (discoveryPaused || fences.has(room.locator))
2732
+ return 'done';
2733
+ if (signal.aborted || writesHeld()) {
2734
+ await exclusive(() => mark(local, room.locator, head, submission, earlier));
2735
+ if (signal.aborted)
2736
+ return 'done';
1388
2737
  progress.complete = false;
1389
2738
  journal.record(`submission ${room.locator} ${head.id} held`);
1390
- return;
2739
+ return 'done';
1391
2740
  }
1392
2741
  const opensBefore = openSignals;
1393
- const answered = awaitingAnswer(room.locator);
2742
+ const answeredHere = awaitingAnswer(room.locator);
2743
+ const answeredThere = partner === null ? () => { } : awaitingAnswer(partner);
2744
+ const answered = () => {
2745
+ answeredHere();
2746
+ answeredThere();
2747
+ };
1394
2748
  try {
1395
2749
  result = await options.adapter.mutate(authorized, {
1396
2750
  locator: room.locator,
1397
2751
  changeBatch: head,
2752
+ file: batchFile(head),
1398
2753
  ...(state?.upload === undefined ? {} : { upload: state.upload }),
1399
2754
  }, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
1400
2755
  }
@@ -1403,50 +2758,69 @@ export function composeRuntime(options) {
1403
2758
  progress.complete = false;
1404
2759
  if (continuing())
1405
2760
  progress.reachable = false;
1406
- return;
2761
+ return 'again';
2762
+ }
2763
+ if (signal.aborted) {
2764
+ answered();
2765
+ return 'done';
1407
2766
  }
1408
2767
  if (result.kind !== 'accepted' || !continuing())
1409
2768
  answered();
1410
2769
  if (!continuing())
1411
- return;
2770
+ return 'done';
1412
2771
  if (result.kind === 'transport-uncertainty') {
1413
2772
  progress.complete = false;
1414
2773
  progress.reachable = false;
1415
- return;
2774
+ return 'again';
1416
2775
  }
1417
2776
  if (result.kind === 'room-not-readable') {
2777
+ const unread = discovered.get(result.locator) ?? room;
1418
2778
  progress.complete = false;
1419
- unreadable.add(room.locator);
1420
- list(room, false);
2779
+ unreadable.add(unread.locator);
2780
+ list(unread, false);
1421
2781
  emitRooms();
1422
- return;
2782
+ return 'done';
1423
2783
  }
1424
2784
  if (result.kind === 'revoked-authorization') {
1425
2785
  await refuse('revoked-authorization');
1426
- return;
2786
+ return 'done';
1427
2787
  }
1428
2788
  if (result.kind === 'scoped-refusal') {
1429
2789
  await scoped(result, room, local);
1430
2790
  progress.complete = false;
1431
- return;
2791
+ return 'done';
1432
2792
  }
1433
2793
  if (result.kind === 'write-halted') {
1434
2794
  journal.record(`outcome ${room.locator} ${head.id} write-halted`);
1435
- await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, lost));
2795
+ await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
1436
2796
  if (serviceMode === 'open' && openSignals !== opensBefore)
1437
2797
  continue;
1438
2798
  haltedRefusal = true;
1439
2799
  progress.complete = false;
1440
- return;
2800
+ return 'done';
1441
2801
  }
2802
+ const other = partner === null ? undefined : discovered.get(partner);
1442
2803
  if (result.kind === 'conflicted' ||
1443
2804
  result.kind === 'rejected' ||
1444
2805
  result.kind === 'expired' ||
1445
2806
  result.kind === 'retryable-refusal') {
2807
+ if (result.kind === 'expired' && lost && result.restored !== true) {
2808
+ journal.record(`outcome ${room.locator} ${head.id} expired`);
2809
+ await discardHeld(head, room, local, signal);
2810
+ return 'done';
2811
+ }
1446
2812
  if (result.kind === 'expired') {
1447
- const withheld = await advance(room, local, authorized);
1448
- if (withheld !== null)
1449
- return withhold(withheld, room, local, progress);
2813
+ await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
2814
+ const there = other === undefined ? null : await advance(other, local, authorized, continuing);
2815
+ if (other !== undefined && there !== null) {
2816
+ await withhold(there, other, local, progress);
2817
+ return 'done';
2818
+ }
2819
+ const withheld = await advance(room, local, authorized, continuing);
2820
+ if (withheld !== null) {
2821
+ await withhold(withheld, room, local, progress);
2822
+ return 'done';
2823
+ }
1450
2824
  }
1451
2825
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
1452
2826
  (result.kind === 'conflicted' && result.roomVersion !== undefined
@@ -1458,12 +2832,25 @@ export function composeRuntime(options) {
1458
2832
  if (result.kind === 'retryable-refusal') {
1459
2833
  const reset = result.reset;
1460
2834
  progress.complete = false;
1461
- await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1462
- armTimer();
1463
- return;
2835
+ await exclusive(() => mark(local, room.locator, head, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
2836
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
1464
2837
  }
1465
2838
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
1466
2839
  const left = leaving(head, result);
2840
+ const refreshed = async () => ({
2841
+ there: other === undefined ? null : await advance(other, local, authorized, continuing),
2842
+ here: await advance(room, local, authorized, continuing),
2843
+ });
2844
+ const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
2845
+ if (judged !== null && (judged.there !== null || judged.here !== null)) {
2846
+ await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, lost));
2847
+ if (other !== undefined && judged.there !== null) {
2848
+ await withhold(judged.there, other, local, progress);
2849
+ }
2850
+ if (judged.here !== null)
2851
+ await withhold(judged.here, room, local, progress);
2852
+ return 'done';
2853
+ }
1467
2854
  await exclusive(async () => {
1468
2855
  if (!continuing())
1469
2856
  return;
@@ -1471,37 +2858,88 @@ export function composeRuntime(options) {
1471
2858
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
1472
2859
  return;
1473
2860
  }
1474
- const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1475
- leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1476
- (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
2861
+ const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
2862
+ leaving: (result.kind === 'expired' && landedWhole(room.locator, head, current.confirmed)) ||
2863
+ (result.kind === 'conflicted' &&
2864
+ landedWhole(room.locator, head, current.confirmed, result.affected))
1477
2865
  ? { ...left, returned: null }
1478
2866
  : left,
1479
2867
  });
1480
- publishChangeSet(set);
2868
+ publishChangeSet(settled());
2869
+ await unpair(local);
1481
2870
  moveTo({ ...status, ...attention() });
1482
2871
  });
1483
2872
  if (result.kind === 'expired')
1484
2873
  continue;
1485
- const withheld = await advance(room, local, authorized);
1486
- if (withheld !== null)
1487
- return withhold(withheld, room, local, progress);
2874
+ const { there, here } = judged ?? (await refreshed());
2875
+ if (other !== undefined && there !== null)
2876
+ await withhold(there, other, local, progress);
2877
+ if (here !== null) {
2878
+ await withhold(here, room, local, progress);
2879
+ return 'done';
2880
+ }
1488
2881
  continue;
1489
2882
  }
1490
- const kept = await adopted(room, local, head, result).finally(answered);
2883
+ const kept = await (partner === null
2884
+ ? adopted(room, local, head, result)
2885
+ : adoptedMove(room, partner, local, head, result)).finally(answered);
1491
2886
  if (!kept) {
1492
2887
  progress.complete = false;
1493
- return;
2888
+ return 'done';
1494
2889
  }
1495
2890
  }
2891
+ return 'done';
2892
+ }
2893
+ function pastHorizon(identity) {
2894
+ return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
2895
+ }
2896
+ async function discardPending(room, local, signal) {
2897
+ const { locator } = room;
2898
+ const discarded = await exclusive(async () => {
2899
+ if (fences.has(locator))
2900
+ return false;
2901
+ const before = replicas.get(locator);
2902
+ const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
2903
+ fences.set(locator, { at: discoveries, erased: false });
2904
+ lastFenced.set(locator, ++fencings);
2905
+ try {
2906
+ await local.discard(locator);
2907
+ }
2908
+ finally {
2909
+ fences.delete(locator);
2910
+ }
2911
+ for (const batch of queue)
2912
+ pendings.delete(pendingKey(locator, batch.id));
2913
+ queuesWithoutReplica.delete(locator);
2914
+ const after = before === undefined ? null : await stepped(replicating(before.confirmed, [], before));
2915
+ if (after !== null)
2916
+ replicas.set(locator, after);
2917
+ journal.record(`room discarded ${locator}`);
2918
+ publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
2919
+ moveTo({ ...status, ...attention() });
2920
+ return true;
2921
+ });
2922
+ if (!discarded)
2923
+ return;
2924
+ const progress = { complete: true, reachable: true };
2925
+ await roomTask(locator, async (live) => {
2926
+ await Promise.race([
2927
+ reload(room, local, progress, () => live() && !signal.aborted),
2928
+ abandonedOn(signal),
2929
+ ]);
2930
+ });
2931
+ if (signal.aborted)
2932
+ return;
2933
+ await rest(progress);
1496
2934
  }
1497
2935
  function awaitingAnswer(locator) {
1498
2936
  let release = () => { };
1499
2937
  const awaited = new Promise((resolve) => {
1500
2938
  release = resolve;
1501
2939
  });
1502
- answering.set(locator, awaited);
2940
+ answering.set(locator, { generation, answered: awaited });
1503
2941
  return () => {
1504
- if (answering.get(locator) === awaited)
2942
+ if (answering.get(locator)?.answered === awaited)
1505
2943
  answering.delete(locator);
1506
2944
  release();
1507
2945
  };
@@ -1525,6 +2963,7 @@ export function composeRuntime(options) {
1525
2963
  if (!continuing())
1526
2964
  return;
1527
2965
  const held = replicas.get(room.locator);
2966
+ let changed = null;
1528
2967
  if (held !== undefined) {
1529
2968
  const declared = {
1530
2969
  ...held.confirmed.record,
@@ -1534,8 +2973,8 @@ export function composeRuntime(options) {
1534
2973
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1535
2974
  };
1536
2975
  const reclassified = sameMembership(declared, held.confirmed.record)
1537
- ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
1538
- : readReclassified(declared, held.confirmed);
2976
+ ? { kind: 'staged', confirmed: await confirmedOf(declared, held.confirmed) }
2977
+ : await readReclassified(declared, held.confirmed);
1539
2978
  const record = reclassified.kind === 'staged'
1540
2979
  ? declared
1541
2980
  : {
@@ -1543,18 +2982,23 @@ export function composeRuntime(options) {
1543
2982
  typedSelection: held.confirmed.record.typedSelection,
1544
2983
  };
1545
2984
  const read = reclassified.kind === 'deferred'
1546
- ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
2985
+ ? { kind: 'staged', confirmed: await confirmedOf(record, held.confirmed) }
1547
2986
  : reclassified;
1548
2987
  await local.write({ next: record, previous: held.confirmed.record });
2988
+ const replica = read.kind === 'staged'
2989
+ ? await stepped(replicating(read.confirmed, held.queue, held))
2990
+ : { ...held, confirmed: { ...held.confirmed, record } };
2991
+ await stepped(counting(replica));
2992
+ const set = await stepped(changing(room.locator, held, replica, { room: true }));
2993
+ replicas.set(room.locator, replica);
2994
+ changed = heldChange(room.locator, held, replica, set, { room: true });
1549
2995
  if (read.kind === 'staged') {
1550
- replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1551
2996
  if (declarationRefusals.delete(room.locator)) {
1552
2997
  sourceRefusals.delete(room.locator);
1553
2998
  unreadable.delete(room.locator);
1554
2999
  }
1555
3000
  }
1556
- else if (read.kind === 'refused') {
1557
- replicas.set(room.locator, { ...held, confirmed: { ...held.confirmed, record } });
3001
+ else {
1558
3002
  refusedSource(room, read);
1559
3003
  declarationRefusals.add(room.locator);
1560
3004
  }
@@ -1565,13 +3009,18 @@ export function composeRuntime(options) {
1565
3009
  invalidModels.delete(key);
1566
3010
  }
1567
3011
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
1568
- if (held === undefined)
3012
+ if (changed === null)
1569
3013
  emitRooms();
1570
3014
  else
1571
- publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
3015
+ publishChangeSet(changed);
1572
3016
  });
1573
3017
  }
1574
3018
  async function scoped(result, room, local) {
3019
+ if (result.scope === 'content' && !result.retryable) {
3020
+ journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
3021
+ await loseRooms([room.locator]);
3022
+ return;
3023
+ }
1575
3024
  scopedRefusals.set(room.locator, {
1576
3025
  code: result.code,
1577
3026
  fields: result.fields,
@@ -1585,15 +3034,7 @@ export function composeRuntime(options) {
1585
3034
  });
1586
3035
  else if (result.scope !== 'operation') {
1587
3036
  unreadable.add(room.locator);
1588
- scopedAway = {
1589
- ...room,
1590
- replica: { ...room.replica, admission: 'unavailable' },
1591
- operations: {
1592
- ...room.operations,
1593
- ...(result.scope === 'content' ? { read: [] } : {}),
1594
- ...(result.scope === 'sync' ? { sync: [] } : {}),
1595
- },
1596
- };
3037
+ scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
1597
3038
  await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
1598
3039
  }
1599
3040
  if (!continuing())
@@ -1617,8 +3058,10 @@ export function composeRuntime(options) {
1617
3058
  ];
1618
3059
  });
1619
3060
  }
1620
- async function synchronize(listed, local, progress, attempt) {
3061
+ async function synchronize(listed, local, progress, attempt, live) {
1621
3062
  const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
3063
+ if (!live())
3064
+ return;
1622
3065
  const ready = listed.filter((_, index) => {
1623
3066
  const outcome = prepared[index];
1624
3067
  return outcome?.status === 'fulfilled' && outcome.value;
@@ -1627,9 +3070,10 @@ export function composeRuntime(options) {
1627
3070
  options.adapter.roomStream?.hold(heldRooms(ready));
1628
3071
  handed = true;
1629
3072
  }
3073
+ discovering?.release();
1630
3074
  const loaded = await Promise.allSettled(ready
1631
3075
  .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1632
- .map((room) => loadRoom(room, local, progress)));
3076
+ .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
1633
3077
  const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1634
3078
  if (failed !== undefined)
1635
3079
  throw failed.reason;
@@ -1651,21 +3095,12 @@ export function composeRuntime(options) {
1651
3095
  await retainModel(local, room, 'collection', modelOf(room.locator));
1652
3096
  if (discoveryPaused || authorization === undefined)
1653
3097
  return false;
1654
- if (room.replica.admission !== 'admitted') {
1655
- if (replicas.has(room.locator))
1656
- progress.complete = false;
1657
- return false;
1658
- }
1659
- return true;
3098
+ return room.replica.admission === 'admitted';
1660
3099
  }
1661
- async function caughtUp(room, local, progress) {
1662
- const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1663
- if (held === undefined || authorization === undefined)
1664
- return;
1665
- const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1666
- if (!behind)
3100
+ async function caughtUp(room, local, progress, live) {
3101
+ if (authorization === undefined || !live() || !behind(room))
1667
3102
  return;
1668
- const withheld = await advance(room, local, authorization);
3103
+ const withheld = await advance(room, local, authorization, live);
1669
3104
  if (withheld !== null)
1670
3105
  await withhold(withheld, room, local, progress);
1671
3106
  }
@@ -1675,47 +3110,57 @@ export function composeRuntime(options) {
1675
3110
  return model.status === 'unavailable' && model.reason === 'revoked';
1676
3111
  });
1677
3112
  }
1678
- async function loadRoom(room, local, progress) {
1679
- if (discoveryPaused || authorization === undefined)
3113
+ async function loadRoom(room, local, progress, live) {
3114
+ if (!live() || authorization === undefined)
1680
3115
  return;
1681
3116
  if (replicas.has(room.locator) && revoked(room.locator)) {
1682
- await reload(room, local, progress);
3117
+ await reload(room, local, progress, live);
1683
3118
  return;
1684
3119
  }
1685
- const withheld = await advance(room, local, authorization);
3120
+ const withheld = await advance(room, local, authorization, live);
1686
3121
  if (withheld !== null) {
1687
3122
  await withhold(withheld, room, local, progress);
1688
3123
  return;
1689
3124
  }
1690
- await caughtUp(room, local, progress);
3125
+ await caughtUp(room, local, progress, live);
1691
3126
  }
1692
- async function reload(room, local, progress) {
1693
- if (discoveryPaused || authorization === undefined)
1694
- return;
1695
- const bootstrapped = await pullBootstrap(authorization, room, local);
1696
- if (!continuing())
1697
- return;
1698
- if (bootstrapped.kind !== 'staged') {
1699
- await withhold(bootstrapped, room, local, progress);
1700
- return;
3127
+ async function reload(room, local, progress, live) {
3128
+ try {
3129
+ if (live())
3130
+ reloadsDue.delete(room.locator);
3131
+ if (!live() || authorization === undefined)
3132
+ return;
3133
+ const bootstrapped = await pullBootstrap(authorization, room, local, live);
3134
+ if (!live())
3135
+ return;
3136
+ if (bootstrapped.kind !== 'staged') {
3137
+ await withhold(bootstrapped, room, local, progress);
3138
+ return;
3139
+ }
3140
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
3141
+ await caughtUp(room, local, progress, live);
3142
+ }
3143
+ finally {
3144
+ caughtUpTo(room.locator);
1701
3145
  }
1702
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1703
- await caughtUp(room, local, progress);
1704
3146
  }
1705
- async function discoverRooms(local, progress, attempt) {
3147
+ async function discoverRooms(local, progress, attempt, live, scope) {
1706
3148
  if (closed || authorization === undefined) {
1707
3149
  notifyReady();
1708
3150
  return;
1709
3151
  }
1710
3152
  moveTo(transferring(status));
3153
+ discoveries += 1;
3154
+ const started = discoveries;
1711
3155
  let result;
1712
3156
  try {
1713
- result = await options.adapter.discover(authorization);
3157
+ result = await options.adapter.discover(authorization, scope);
1714
3158
  }
1715
3159
  catch {
1716
3160
  result = { kind: 'transport-uncertainty' };
1717
3161
  }
1718
- if (closed) {
3162
+ if (!live()) {
3163
+ progress.complete = false;
1719
3164
  notifyReady();
1720
3165
  return;
1721
3166
  }
@@ -1749,12 +3194,13 @@ export function composeRuntime(options) {
1749
3194
  }
1750
3195
  for (const room of result.rooms) {
1751
3196
  if (room.operations === undefined ||
1752
- !['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
3197
+ !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
1753
3198
  room.operations[operation].every((scope) => typeof scope === 'string')) ||
1754
3199
  room.replica === undefined ||
1755
3200
  typeof room.replica.selection !== 'string' ||
1756
3201
  room.replica.selection.length === 0 ||
1757
- !['admitted', 'unavailable'].includes(room.replica.admission)) {
3202
+ !['admitted', 'unavailable'].includes(room.replica.admission) ||
3203
+ !['locked', 'open'].includes(room.replica.offline)) {
1758
3204
  journal.record(`invalid model or replica declaration ${room.locator}`);
1759
3205
  await refuse('missing-required-guarantee');
1760
3206
  notifyReady();
@@ -1773,6 +3219,7 @@ export function composeRuntime(options) {
1773
3219
  }
1774
3220
  const previousCapabilities = capabilities;
1775
3221
  capabilities = result.capabilities;
3222
+ outcomeRetention = result.retention.outcome;
1776
3223
  notifyReady();
1777
3224
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1778
3225
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
@@ -1785,8 +3232,7 @@ export function composeRuntime(options) {
1785
3232
  member = signedIn;
1786
3233
  emitMember();
1787
3234
  }
1788
- await applyDiscovery(result.rooms, local);
1789
- await synchronize(result.rooms, local, progress, attempt);
3235
+ await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
1790
3236
  }
1791
3237
  async function ensureAuthorization() {
1792
3238
  if (authorization !== undefined)
@@ -1800,20 +3246,50 @@ export function composeRuntime(options) {
1800
3246
  throw error;
1801
3247
  }
1802
3248
  }
1803
- function task(work) {
3249
+ async function admittedRead(work) {
3250
+ if (roomReads.generation !== generation) {
3251
+ roomReads = { generation, free: ROOM_READS, waiting: [] };
3252
+ }
3253
+ const reads = roomReads;
3254
+ while (reads.free === 0)
3255
+ await new Promise((admit) => reads.waiting.push(admit));
3256
+ reads.free -= 1;
3257
+ try {
3258
+ await work();
3259
+ }
3260
+ finally {
3261
+ reads.free += 1;
3262
+ reads.waiting.shift()?.();
3263
+ }
3264
+ }
3265
+ function roomTask(locator, work) {
3266
+ return task(roomLane(locator), (live) => admittedRead(() => work(live)));
3267
+ }
3268
+ function task(lane, work) {
1804
3269
  if (stopping)
1805
- return chain;
1806
- chain = chain.then(work, work).catch(() => undefined);
1807
- return chain;
3270
+ return Promise.resolve();
3271
+ const held = lanes.get(lane);
3272
+ const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
3273
+ const turn = current.generation;
3274
+ const live = () => turn === generation && continuing();
3275
+ const run = current.chain.then(() => work(live)).catch(() => undefined);
3276
+ current.chain = run;
3277
+ lanes.set(lane, current);
3278
+ working.add(run);
3279
+ void run.finally(() => working.delete(run));
3280
+ return run;
1808
3281
  }
1809
3282
  function sync(attempt) {
1810
- return task(async () => {
1811
- if (closed || stopping || discoveryPaused) {
3283
+ return task(OPENING_LANE, async (live) => {
3284
+ if (!live()) {
1812
3285
  notifyReady();
1813
3286
  return;
1814
3287
  }
1815
3288
  const progress = { complete: true, reachable: true };
1816
- underway = attempt === 'opened' ? progress : null;
3289
+ const gate = discover();
3290
+ if (attempt === 'opened')
3291
+ opened = generation;
3292
+ underway = attempt === 'opened' ? { generation, progress } : null;
1817
3293
  if (attempt === 'opened')
1818
3294
  streamed = progress;
1819
3295
  complete = false;
@@ -1822,12 +3298,12 @@ export function composeRuntime(options) {
1822
3298
  handed = false;
1823
3299
  try {
1824
3300
  const local = await openLocal();
1825
- if (closed)
3301
+ if (!live())
1826
3302
  return;
1827
3303
  await ensureAuthorization();
1828
- if (closed)
3304
+ if (!live())
1829
3305
  return;
1830
- await discoverRooms(local, progress, attempt);
3306
+ await discoverRooms(local, progress, attempt, live);
1831
3307
  }
1832
3308
  catch {
1833
3309
  progress.complete = false;
@@ -1835,23 +3311,62 @@ export function composeRuntime(options) {
1835
3311
  progress.reachable = false;
1836
3312
  }
1837
3313
  finally {
3314
+ gate.release();
1838
3315
  notifyReady();
1839
3316
  }
3317
+ if (!live())
3318
+ return;
1840
3319
  if (attempt !== 'opened') {
1841
- await finishOpening(progress);
3320
+ await finishOpening(progress, live);
1842
3321
  return;
1843
3322
  }
1844
3323
  if (handed || closed || progress.reachable)
1845
3324
  return;
1846
3325
  underway = null;
3326
+ opened = null;
1847
3327
  journal.record('connection reopen');
1848
- await rest(progress);
3328
+ await rest(progress, live);
1849
3329
  options.adapter.roomStream?.reopen();
1850
3330
  });
1851
3331
  }
1852
- function closedSocket(revoked) {
3332
+ function rediscover(scope) {
3333
+ return task(OPENING_LANE, async (live) => {
3334
+ if (!live() || !socketOpen)
3335
+ return;
3336
+ const progress = { complete: true, reachable: true };
3337
+ const gate = discover();
3338
+ moveTo(transferring(status));
3339
+ try {
3340
+ const local = await openLocal();
3341
+ if (!live())
3342
+ return;
3343
+ await ensureAuthorization();
3344
+ if (!live())
3345
+ return;
3346
+ await discoverRooms(local, progress, 'authorized', live, scope);
3347
+ }
3348
+ catch {
3349
+ progress.complete = false;
3350
+ }
3351
+ finally {
3352
+ gate.release();
3353
+ notifyReady();
3354
+ }
3355
+ if (!live())
3356
+ return;
3357
+ if (!progress.complete)
3358
+ complete = false;
3359
+ await rest(progress, live);
3360
+ });
3361
+ }
3362
+ function closedSocket(revoked, lost) {
1853
3363
  const opened = socketOpen;
1854
3364
  socketOpen = false;
3365
+ connectionLost();
3366
+ if (lost) {
3367
+ reads.lost();
3368
+ conversations.lost();
3369
+ }
1855
3370
  if (revoked) {
1856
3371
  void refuse('revoked-authorization');
1857
3372
  return;
@@ -1866,24 +3381,35 @@ export function composeRuntime(options) {
1866
3381
  });
1867
3382
  }
1868
3383
  function applyOpening(expired) {
1869
- return task(async () => {
1870
- const progress = underway;
3384
+ const progress = underway?.generation === generation ? underway.progress : null;
3385
+ const cycle = progress ?? { complete: true, reachable: true };
3386
+ if (opened === generation)
3387
+ opened = null;
3388
+ const reloading = progress === null ? [] : owedReloads(expired);
3389
+ const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
3390
+ return task(OPENING_LANE, async (live) => {
1871
3391
  underway = null;
1872
- if (closed || stopping)
3392
+ if (!live())
1873
3393
  return;
1874
3394
  if (progress === null) {
1875
- await reloadExpired(expired);
3395
+ await replacing;
3396
+ await rest(cycle, live);
1876
3397
  return;
1877
3398
  }
3399
+ await roomsCarried([...expired, ...reloading]);
3400
+ if (!live())
3401
+ return;
1878
3402
  try {
1879
3403
  const local = await openLocal();
1880
- const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1881
- const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1882
- const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
3404
+ const reloaded = await replacing;
1883
3405
  if (progress.reachable) {
1884
- for (const room of held)
1885
- if (!reloading.includes(room))
3406
+ for (const room of discovered.values()) {
3407
+ if (replicas.has(room.locator) &&
3408
+ !expired.includes(room.locator) &&
3409
+ !reloading.includes(room.locator)) {
1886
3410
  await confirmModels(room, local);
3411
+ }
3412
+ }
1887
3413
  }
1888
3414
  const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1889
3415
  if (failed !== undefined)
@@ -1893,21 +3419,36 @@ export function composeRuntime(options) {
1893
3419
  progress.complete = false;
1894
3420
  progress.reachable = false;
1895
3421
  }
1896
- await finishOpening(progress);
3422
+ if (live())
3423
+ await finishOpening(progress, live);
1897
3424
  });
1898
3425
  }
1899
- async function reloadExpired(expired) {
1900
- const progress = { complete: true, reachable: true };
3426
+ function owedReloads(expired) {
3427
+ const reloading = [...discovered.values()]
3428
+ .filter((room) => replicas.has(room.locator) &&
3429
+ !expired.includes(room.locator) &&
3430
+ (owed.has(room.locator) || revoked(room.locator)))
3431
+ .map((room) => room.locator);
3432
+ owed.clear();
3433
+ for (const locator of reloading)
3434
+ catching.add(locator);
3435
+ readSide.recover();
3436
+ return reloading;
3437
+ }
3438
+ async function reloadNow(replaced, progress) {
3439
+ const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
3440
+ for (const room of reloaded)
3441
+ reloadsDue.set(room.locator, generation);
1901
3442
  try {
1902
3443
  const local = await openLocal();
1903
- await Promise.allSettled([...discovered.values()]
1904
- .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1905
- .map((room) => reload(room, local, progress)));
3444
+ return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
1906
3445
  }
1907
3446
  catch {
3447
+ for (const room of reloaded)
3448
+ reloadsDue.delete(room.locator);
1908
3449
  progress.reachable = false;
3450
+ return [];
1909
3451
  }
1910
- await rest(progress);
1911
3452
  }
1912
3453
  async function confirmModels(room, local) {
1913
3454
  for (const attached of ['collection', 'typed-selection']) {
@@ -1920,38 +3461,94 @@ export function composeRuntime(options) {
1920
3461
  function submitLater(locator) {
1921
3462
  if (localSaves)
1922
3463
  return;
1923
- const running = submitting.get(locator);
1924
- if (running !== undefined) {
1925
- running.again = true;
3464
+ void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
3465
+ }
3466
+ async function submitPass(locator, signal) {
3467
+ const room = discovered.get(locator);
3468
+ if (closed || stopping || discoveryPaused || room === undefined)
3469
+ return 'done';
3470
+ const progress = { complete: true, reachable: true };
3471
+ let next;
3472
+ try {
3473
+ await ensureAuthorization();
3474
+ if (authorization === undefined)
3475
+ return 'done';
3476
+ next = await submitRoom(room, await openLocal(), authorization, progress, signal);
3477
+ }
3478
+ catch {
3479
+ progress.reachable = false;
3480
+ next = 'again';
3481
+ }
3482
+ if (signal.aborted)
3483
+ return 'done';
3484
+ await rest(progress);
3485
+ return next === 'again' && !socketOpen ? 'done' : next;
3486
+ }
3487
+ function repairLater(locator, when) {
3488
+ if (!socketOpen || repairing.has(locator))
1926
3489
  return;
3490
+ void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
3491
+ }
3492
+ async function repairPass(locator, signal) {
3493
+ const room = discovered.get(locator);
3494
+ if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
3495
+ return 'done';
3496
+ if (room === undefined || room.replica.admission !== 'admitted')
3497
+ return 'done';
3498
+ const progress = { complete: true, reachable: true };
3499
+ repairing.add(locator);
3500
+ try {
3501
+ const local = await openLocal();
3502
+ await roomTask(locator, async (live) => {
3503
+ const current = () => live() && !signal.aborted;
3504
+ if (!current() || authorization === undefined)
3505
+ return;
3506
+ const withheld = await Promise.race([
3507
+ advance(room, local, authorization, current),
3508
+ abandonedOn(signal),
3509
+ ]);
3510
+ if (withheld !== null && current())
3511
+ await withhold(withheld, room, local, progress);
3512
+ });
1927
3513
  }
1928
- const run = { again: false, done: Promise.resolve() };
1929
- submitting.set(locator, run);
1930
- run.done = (async () => {
1931
- try {
1932
- do {
1933
- run.again = false;
1934
- const room = discovered.get(locator);
1935
- if (closed || stopping || discoveryPaused || room === undefined)
1936
- return;
1937
- const progress = { complete: true, reachable: true };
1938
- try {
1939
- await ensureAuthorization();
1940
- if (authorization === undefined)
1941
- return;
1942
- await submitRoom(room, await openLocal(), authorization, progress);
1943
- }
1944
- catch {
1945
- progress.reachable = false;
1946
- }
1947
- armTimer();
1948
- await rest(progress);
1949
- } while (run.again);
1950
- }
1951
- finally {
1952
- submitting.delete(locator);
1953
- }
1954
- })();
3514
+ catch {
3515
+ stale.add(locator);
3516
+ progress.reachable = false;
3517
+ }
3518
+ finally {
3519
+ repairing.delete(locator);
3520
+ }
3521
+ if (signal.aborted)
3522
+ return 'done';
3523
+ await rest(progress);
3524
+ if (!stale.has(locator)) {
3525
+ if ((replicas.get(locator)?.queue.length ?? 0) > 0)
3526
+ submitLater(locator);
3527
+ return 'done';
3528
+ }
3529
+ if (!repairable.has(locator))
3530
+ return 'done';
3531
+ const reset = repairable.get(locator);
3532
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
3533
+ }
3534
+ function resendLater() {
3535
+ if (!socketOpen)
3536
+ return;
3537
+ void recovery.attempt('resend', resendPass, { again: true });
3538
+ }
3539
+ async function resendPass(signal) {
3540
+ if (!socketOpen || closed || stopping || discoveryPaused)
3541
+ return 'done';
3542
+ const reading = { complete: true, reachable: true };
3543
+ try {
3544
+ await conversations.resend(reading);
3545
+ }
3546
+ catch {
3547
+ reading.reachable = false;
3548
+ }
3549
+ if (signal.aborted)
3550
+ return 'done';
3551
+ return reading.reachable && reading.complete ? 'done' : 'again';
1955
3552
  }
1956
3553
  function submitAll() {
1957
3554
  for (const [locator, replica] of replicas) {
@@ -1959,31 +3556,43 @@ export function composeRuntime(options) {
1959
3556
  submitLater(locator);
1960
3557
  }
1961
3558
  }
1962
- async function finishOpening(progress) {
3559
+ async function finishOpening(progress, live) {
1963
3560
  try {
1964
- if (closed || discoveryPaused) {
3561
+ if (!live()) {
1965
3562
  progress.complete = false;
1966
3563
  return;
1967
3564
  }
3565
+ reads.applied();
1968
3566
  submitAll();
3567
+ for (const locator of stale)
3568
+ if (repairable.has(locator))
3569
+ repairLater(locator, {});
1969
3570
  await conversations.reconcile();
1970
3571
  if (status.providerUnreachable)
1971
3572
  conversations.reconnected();
1972
3573
  const reading = { complete: true, reachable: true };
1973
3574
  await conversations.catchUp(reading);
1974
- if (!reading.reachable)
3575
+ if (progress.reachable && reading.reachable)
3576
+ await conversations.resend(reading);
3577
+ if (progress.reachable)
3578
+ void recovery.attempt('calls', resendCalls);
3579
+ if (!reading.reachable) {
1975
3580
  progress.reachable = false;
3581
+ resendLater();
3582
+ }
1976
3583
  }
1977
3584
  catch {
1978
3585
  progress.complete = false;
1979
3586
  }
1980
3587
  finally {
1981
- complete = progress.complete;
1982
- await rest(progress);
3588
+ if (live()) {
3589
+ complete = progress.complete;
3590
+ await rest(progress, live);
3591
+ }
1983
3592
  }
1984
3593
  }
1985
- async function rest(progress) {
1986
- if (closed)
3594
+ async function rest(progress, live = () => !closed) {
3595
+ if (!live())
1987
3596
  return;
1988
3597
  let lastCompleted = status.lastCompleted;
1989
3598
  let synced = complete && stale.size === 0 && !pending();
@@ -1991,6 +3600,8 @@ export function composeRuntime(options) {
1991
3600
  const completedAt = new Date().toISOString();
1992
3601
  try {
1993
3602
  await (await openLocal()).complete(completedAt);
3603
+ if (!live())
3604
+ return;
1994
3605
  recorded = true;
1995
3606
  lastCompleted = completedAt;
1996
3607
  }
@@ -2005,11 +3616,23 @@ export function composeRuntime(options) {
2005
3616
  ...attention(),
2006
3617
  });
2007
3618
  }
3619
+ function discover() {
3620
+ let release = () => { };
3621
+ const done = new Promise((resolve) => {
3622
+ release = resolve;
3623
+ });
3624
+ discovering = { generation, done, release };
3625
+ return discovering;
3626
+ }
2008
3627
  function applyRoom(locator, roomVersion) {
2009
3628
  announced.set(locator, roomVersion);
2010
- return task(async () => {
2011
- await answering.get(locator);
2012
- if (closed || stopping || discoveryPaused || authorization === undefined)
3629
+ return task(roomLane(locator), async (live) => {
3630
+ if (discovering?.generation === generation)
3631
+ await discovering.done;
3632
+ const saving = answering.get(locator);
3633
+ if (saving?.generation === generation)
3634
+ await saving.answered;
3635
+ if (!live() || authorization === undefined)
2013
3636
  return;
2014
3637
  const room = discovered.get(locator);
2015
3638
  const held = replicas.get(locator);
@@ -2020,25 +3643,41 @@ export function composeRuntime(options) {
2020
3643
  options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
2021
3644
  return;
2022
3645
  }
2023
- const during = underway;
3646
+ const during = underway?.generation === generation ? underway.progress : null;
2024
3647
  const progress = during ?? { complete: true, reachable: true };
2025
3648
  if (during === null)
2026
3649
  moveTo(transferring(status));
2027
3650
  try {
2028
3651
  const local = await openLocal();
2029
- const withheld = await advance(room, local, authorization);
2030
- if (withheld !== null)
2031
- await withhold(withheld, room, local, progress);
3652
+ await admittedRead(async () => {
3653
+ if (!live() || authorization === undefined)
3654
+ return;
3655
+ const withheld = await advance(room, local, authorization, live);
3656
+ if (withheld !== null)
3657
+ await withhold(withheld, room, local, progress);
3658
+ });
2032
3659
  }
2033
3660
  catch {
2034
3661
  stale.add(locator);
2035
3662
  }
2036
3663
  finally {
2037
3664
  if (during === null)
2038
- await rest(progress);
3665
+ await rest(progress, live);
2039
3666
  }
2040
3667
  });
2041
3668
  }
3669
+ function roomsCarried(replacing) {
3670
+ const replaced = new Set(replacing.map(roomLane));
3671
+ return Promise.all([...lanes]
3672
+ .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
3673
+ .map(([, held]) => held.chain));
3674
+ }
3675
+ const receipts = {
3676
+ heads: async () => (await openLocal()).receivedHeads(),
3677
+ parts: async (key, count) => (await openLocal()).receivedParts(key, count),
3678
+ keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
3679
+ drop: async (keys) => (await openLocal()).dropReceived(keys),
3680
+ };
2042
3681
  function observeStream() {
2043
3682
  const stream = options.adapter.roomStream;
2044
3683
  if (stream === undefined || withdrawStream !== undefined)
@@ -2048,15 +3687,22 @@ export function composeRuntime(options) {
2048
3687
  return;
2049
3688
  if (event.kind === 'opened') {
2050
3689
  socketOpen = true;
3690
+ connectionLive();
3691
+ generation += 1;
3692
+ reads.opened();
3693
+ conversations.reopened();
3694
+ recovery.resume();
2051
3695
  void sync('opened');
2052
3696
  }
2053
3697
  else if (event.kind === 'closed')
2054
- closedSocket(event.revoked);
3698
+ closedSocket(event.revoked, event.lost);
2055
3699
  else if (event.kind === 'opening')
2056
3700
  void applyOpening(event.expired);
3701
+ else if (event.kind === 'authority-changed')
3702
+ void rediscover(event.scope);
2057
3703
  else
2058
3704
  void applyRoom(event.locator, event.roomVersion);
2059
- });
3705
+ }, receipts);
2060
3706
  }
2061
3707
  function writesHeld() {
2062
3708
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -2071,6 +3717,7 @@ export function composeRuntime(options) {
2071
3717
  haltedRefusal = false;
2072
3718
  if (mode !== serviceMode) {
2073
3719
  serviceMode = mode;
3720
+ queueReadiness();
2074
3721
  journal.record(`service mode ${mode}`);
2075
3722
  for (const listener of serviceModeListeners)
2076
3723
  listener(mode);
@@ -2083,6 +3730,8 @@ export function composeRuntime(options) {
2083
3730
  if (stream === undefined || withdrawConversations !== undefined)
2084
3731
  return;
2085
3732
  withdrawConversations = stream.observe((event) => {
3733
+ if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
3734
+ queueReadiness();
2086
3735
  if (conversations.receive(event)) {
2087
3736
  void ready.then(scheduleCurrency);
2088
3737
  return;
@@ -2094,14 +3743,15 @@ export function composeRuntime(options) {
2094
3743
  });
2095
3744
  }
2096
3745
  function scheduleCurrency() {
2097
- if (currencyQueued || closed || stopping)
3746
+ if (currencyQueued === generation || closed || stopping)
2098
3747
  return;
2099
- currencyQueued = true;
2100
- void task(runCurrency);
3748
+ currencyQueued = generation;
3749
+ void task(CONVERSATION_LANE, runCurrency);
2101
3750
  }
2102
- async function runCurrency() {
2103
- currencyQueued = false;
2104
- if (closed || stopping || discoveryPaused)
3751
+ async function runCurrency(live) {
3752
+ if (currencyQueued === generation)
3753
+ currencyQueued = null;
3754
+ if (!live())
2105
3755
  return;
2106
3756
  await conversations.reconcile();
2107
3757
  if (conversations.current()) {
@@ -2116,19 +3766,24 @@ export function composeRuntime(options) {
2116
3766
  catch {
2117
3767
  progress.reachable = false;
2118
3768
  }
2119
- await rest(progress);
3769
+ await rest(progress, live);
2120
3770
  }
2121
3771
  function close() {
2122
3772
  if (closed)
2123
3773
  return;
2124
3774
  closed = true;
3775
+ if (rechecking !== null)
3776
+ clearInterval(rechecking);
3777
+ connectionLive();
3778
+ lockListeners.clear();
3779
+ readinessWatches.clear();
2125
3780
  withdrawStream?.();
2126
3781
  withdrawStream = undefined;
2127
3782
  withdrawConversations?.();
2128
3783
  withdrawConversations = undefined;
2129
3784
  withdrawServiceMode?.();
2130
3785
  conversations.close();
2131
- stopTimer();
3786
+ void recovery.retire();
2132
3787
  store?.close();
2133
3788
  void opening?.then((opened) => {
2134
3789
  opened.close();
@@ -2146,9 +3801,8 @@ export function composeRuntime(options) {
2146
3801
  }
2147
3802
  async function stop() {
2148
3803
  stopping = true;
2149
- stopTimer();
2150
- await chain.catch(() => undefined);
2151
- await Promise.all([...submitting.values()].map((run) => run.done));
3804
+ await recovery.retire();
3805
+ await Promise.all(working);
2152
3806
  await mutations;
2153
3807
  close();
2154
3808
  }
@@ -2167,20 +3821,125 @@ export function composeRuntime(options) {
2167
3821
  if (options.adapter.roomStream === undefined)
2168
3822
  void sync('unstreamed');
2169
3823
  function planned(input, reach) {
2170
- return planOf(input, roomFacts(reach), planReach(reach));
3824
+ const plan = planOf(input, roomFacts(reach), planReach(reach));
3825
+ const named = plan.paths !== null &&
3826
+ plan.rooms !== null &&
3827
+ plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
3828
+ return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
2171
3829
  }
2172
3830
  function watchPlan(plan, deliver) {
2173
- materialize(reachedRooms(plan));
2174
- return readSide.watch(plan, identityOf(plan), deliver);
3831
+ let seeded = false;
3832
+ let pending = true;
3833
+ const shown = () => {
3834
+ const seed = seeds.get(plan.key);
3835
+ if (seed === undefined || seeded || !pending)
3836
+ return;
3837
+ seeded = true;
3838
+ deliver(seededResult(seed));
3839
+ };
3840
+ const watchers = seedWatchers.get(plan.key) ?? new Set();
3841
+ seedWatchers.set(plan.key, watchers);
3842
+ watchers.add(shown);
3843
+ const stop = readSide.watch(plan, identityOf(plan), () => {
3844
+ const cover = covered(plan);
3845
+ awaitPreparation(cover);
3846
+ return seedBehind(plan, cover);
3847
+ }, (message) => {
3848
+ const answered = message;
3849
+ pending = whole(answered) && 'pending' in answered.result;
3850
+ if (pending && seeds.has(plan.key)) {
3851
+ shown();
3852
+ return;
3853
+ }
3854
+ seeded = false;
3855
+ deliver(message);
3856
+ });
3857
+ return () => {
3858
+ watchers.delete(shown);
3859
+ if (watchers.size === 0)
3860
+ seedWatchers.delete(plan.key);
3861
+ stop();
3862
+ };
2175
3863
  }
2176
- async function searched(input, reach) {
2177
- const plan = searchPlanOf(input);
2178
- const selected = plan.documents === undefined && reach === null
2179
- ? null
2180
- : (await rowsQuery(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
2181
- if (selected === null)
2182
- materialize(null);
2183
- return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
3864
+ async function searched(input, reach, locked) {
3865
+ const asked = searchPlanOf(input);
3866
+ const plan = { ...asked, limit: capped(asked.limit) };
3867
+ const filter = {
3868
+ ...filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)),
3869
+ locked,
3870
+ };
3871
+ const unlocking = (filter.rooms ?? [])
3872
+ .filter((room) => locked.includes(room))
3873
+ .map((room) => ({ room, awaits: 'unlock' }));
3874
+ for (;;) {
3875
+ const cover = covered(filter);
3876
+ awaitPreparation(cover);
3877
+ const pending = [
3878
+ ...unlocking,
3879
+ ...('pending' in cover ? cover.pending : []),
3880
+ ];
3881
+ const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3882
+ const held = covered({
3883
+ ...filter,
3884
+ rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
3885
+ });
3886
+ const covering = 'pending' in held ? [] : held.coverage.rooms;
3887
+ const unindexed = covering
3888
+ .filter(({ room }) => !searchIndex.rooms().has(room))
3889
+ .map(({ room }) => room);
3890
+ for (const room of unindexed)
3891
+ pending.push({ room, awaits: 'index' });
3892
+ const listed = conversations.searchable();
3893
+ const named = reach !== null && !reach.conversations
3894
+ ? []
3895
+ : (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
3896
+ const waiting = named.filter((one) => !searchIndex.conversations().has(one));
3897
+ for (const conversation of waiting)
3898
+ pending.push({ conversation, awaits: 'index' });
3899
+ if (unindexed.length > 0 || waiting.length > 0)
3900
+ index(unindexed, waiting);
3901
+ const complete = pending.length === 0 && !('pending' in cover);
3902
+ const answering = complete
3903
+ ? filter
3904
+ : {
3905
+ ...filter,
3906
+ rooms: covering
3907
+ .filter(({ room }) => searchIndex.rooms().has(room))
3908
+ .map(({ room }) => room),
3909
+ };
3910
+ const answered = covered(answering);
3911
+ if ('pending' in answered)
3912
+ continue;
3913
+ const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
3914
+ await readSide.settled();
3915
+ const seen = searchIndex.changes();
3916
+ await searchIndex.caughtUp();
3917
+ const selected = roomsAlone(answering) ? null : await rowsQuery(answering);
3918
+ if (selected !== null && 'pending' in selected)
3919
+ continue;
3920
+ if (searchIndex.changes() !== seen ||
3921
+ JSON.stringify(covered(answering)) !== JSON.stringify(answered))
3922
+ continue;
3923
+ const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms !== null
3924
+ ? new Set(answering.rooms)
3925
+ : locked.length === 0
3926
+ ? null
3927
+ : new Set(reachedRooms(answering)), plan.collection
3928
+ ? new Map(answered.coverage.rooms.map(({ room }) => [
3929
+ room,
3930
+ replicas.get(room)?.confirmed.record.typedSelection?.roots ?? [],
3931
+ ]))
3932
+ : null);
3933
+ const coverage = {
3934
+ rooms: answered.coverage.rooms,
3935
+ conversations: searchedConversations,
3936
+ index: searchIndex.generation(),
3937
+ overlay: answered.coverage.overlay,
3938
+ };
3939
+ return complete
3940
+ ? { ...hits, coverage }
3941
+ : { pending, documents: hits.documents, messages: hits.messages, coverage };
3942
+ }
2184
3943
  }
2185
3944
  const watch = (topic, input, deliver) => {
2186
3945
  if (topic === 'room-drafts') {
@@ -2226,6 +3985,9 @@ export function composeRuntime(options) {
2226
3985
  presentationListeners.delete(listener);
2227
3986
  };
2228
3987
  }
3988
+ if (topic === 'readiness') {
3989
+ return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
3990
+ }
2229
3991
  if (topic === 'rooms') {
2230
3992
  roomListeners.add(deliver);
2231
3993
  deliver(listed());
@@ -2286,7 +4048,7 @@ export function composeRuntime(options) {
2286
4048
  return listed();
2287
4049
  },
2288
4050
  query: ((input) => rowsQuery(planned(input, null))),
2289
- search: (input) => searched(input, null),
4051
+ search: (input) => searched(input, null, []),
2290
4052
  types(locator) {
2291
4053
  return {
2292
4054
  collection: modelTypes(modelOf(locator)),
@@ -2330,7 +4092,9 @@ export function composeRuntime(options) {
2330
4092
  parts: held(current?.parts, parts, file),
2331
4093
  returned: current?.returned ?? null,
2332
4094
  };
2333
- await local.keepRoomDraft(record);
4095
+ await local.keepRoomDraft(record, parts.file === undefined || file === undefined
4096
+ ? []
4097
+ : [{ key: file.localId, blob: parts.file.blob }]);
2334
4098
  rememberRoomDraft(record);
2335
4099
  publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
2336
4100
  return { outcome: 'written', roomDraft: roomDraftOf(record) };
@@ -2361,27 +4125,37 @@ export function composeRuntime(options) {
2361
4125
  });
2362
4126
  },
2363
4127
  write(app, value) {
2364
- return exclusive(async () => {
2365
- const local = await openLocal();
2366
- const stored = structuredClone(value);
2367
- await local.present({ app, value: stored });
2368
- presentations.set(app, stored);
2369
- publishChangeSet(changeSet('', null, null, { presentation: app }));
2370
- });
4128
+ return keepPresentation(app, () => structuredClone(value));
4129
+ },
4130
+ edit(app, members) {
4131
+ return keepPresentation(app, (held) => Object.fromEntries([
4132
+ ...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(members, member)),
4133
+ ...Object.entries(structuredClone(members)).filter(([, value]) => value !== null),
4134
+ ]));
2371
4135
  },
2372
4136
  },
2373
4137
  save(room, named, writtenBy) {
2374
4138
  return exclusive(() => saveRoomDrafts(room, named, writtenBy));
2375
4139
  },
4140
+ saveParts(room, parts, writtenBy) {
4141
+ return exclusive(() => saveParts(room, parts, writtenBy));
4142
+ },
2376
4143
  history(locator, path, query) {
2377
4144
  return readHistory(locator, path, query);
2378
4145
  },
4146
+ resolve(room, path, destination) {
4147
+ return resolveReference(room, path, destination);
4148
+ },
2379
4149
  binaryFile: {
4150
+ part: readPart,
2380
4151
  read: readBinaryPath,
4152
+ link: linkBinaryPath,
2381
4153
  readReference: readBinaryReference,
2382
4154
  },
2383
- call(name, input) {
2384
- return invokeAction(name, input);
4155
+ async call(name, input) {
4156
+ await ready;
4157
+ const room = calledRooms(name, input).find((one) => locked(one));
4158
+ return room === undefined ? recoveredAction(name, input) : lockedRoom(room);
2385
4159
  },
2386
4160
  reached(locator, roomVersion, path) {
2387
4161
  return path === undefined
@@ -2391,22 +4165,31 @@ export function composeRuntime(options) {
2391
4165
  log() {
2392
4166
  return journal.export();
2393
4167
  },
4168
+ cleanUp,
2394
4169
  persistence() {
2395
- return options.shell.persistentStorage;
4170
+ return persistence === 'granted';
4171
+ },
4172
+ async retry() {
4173
+ journal.record('retry');
4174
+ unindexable.clear();
4175
+ queueReadiness();
4176
+ resume();
2396
4177
  },
2397
4178
  observe: observing(watch),
2398
4179
  async reauthorize() {
2399
4180
  if (refusal === null || !refusal.reauthorizationOffered)
2400
4181
  return;
2401
4182
  journal.record('connection reauthorize');
2402
- await chain.catch(() => undefined);
4183
+ await Promise.all([...lanes.values()]
4184
+ .filter((held) => held.generation === generation)
4185
+ .map((held) => held.chain));
2403
4186
  await mutations;
2404
4187
  authorization = undefined;
2405
4188
  discoveryPaused = false;
2406
4189
  if (options.adapter.roomStream === undefined)
2407
4190
  await sync('unstreamed');
2408
4191
  else
2409
- await task(ensureAuthorization);
4192
+ await task(OPENING_LANE, ensureAuthorization);
2410
4193
  },
2411
4194
  };
2412
4195
  const reachable = {
@@ -2417,6 +4200,25 @@ export function composeRuntime(options) {
2417
4200
  query: (plan) => rowsQuery(plan),
2418
4201
  watchPlan,
2419
4202
  search: searched,
4203
+ watchReadiness,
4204
+ locked,
4205
+ lockedRooms,
4206
+ watchLocks,
4207
+ located,
4208
+ };
4209
+ return {
4210
+ md,
4211
+ seed,
4212
+ stop,
4213
+ lose: loseRooms,
4214
+ watch,
4215
+ persisted(answer) {
4216
+ if (answer === persistence)
4217
+ return;
4218
+ persistence = answer;
4219
+ journal.record(`persistence ${answer}`);
4220
+ queueReadiness();
4221
+ },
4222
+ within: (reach, port) => narrowed(reachable, reach, port),
2420
4223
  };
2421
- return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2422
4224
  }