@agentshouse/mdruntime 0.1.0-cbb8d8acc9d19 → 0.1.0-cc56db96f86ae

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