@agentshouse/mdruntime 0.1.0-ce531b51ff088 → 0.1.0-ceb97106cc2fd

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 (203) hide show
  1. package/AGENTS.md +273 -175
  2. package/README.md +21 -11
  3. package/dist/acquisition.d.ts +14 -0
  4. package/dist/acquisition.d.ts.map +1 -0
  5. package/dist/acquisition.js +24 -0
  6. package/dist/adapter.d.ts +129 -41
  7. package/dist/adapter.d.ts.map +1 -1
  8. package/dist/adapter.js +1 -9
  9. package/dist/attached-model.d.ts +11 -0
  10. package/dist/attached-model.d.ts.map +1 -0
  11. package/dist/attached-model.js +47 -0
  12. package/dist/attempt.d.ts +16 -0
  13. package/dist/attempt.d.ts.map +1 -0
  14. package/dist/attempt.js +70 -0
  15. package/dist/binary.d.ts +23 -0
  16. package/dist/binary.d.ts.map +1 -0
  17. package/dist/binary.js +8 -0
  18. package/dist/bootstrap.d.ts +20 -6
  19. package/dist/bootstrap.d.ts.map +1 -1
  20. package/dist/bootstrap.js +114 -37
  21. package/dist/change-set.d.ts +2 -0
  22. package/dist/change-set.d.ts.map +1 -1
  23. package/dist/change-set.js +49 -16
  24. package/dist/change.d.ts +15 -0
  25. package/dist/change.d.ts.map +1 -1
  26. package/dist/change.js +26 -1
  27. package/dist/chat/acts.d.ts +33 -0
  28. package/dist/chat/acts.d.ts.map +1 -0
  29. package/dist/chat/acts.js +36 -0
  30. package/dist/chat/context.d.ts +3 -0
  31. package/dist/chat/context.d.ts.map +1 -0
  32. package/dist/chat/context.js +10 -0
  33. package/dist/chat/input.d.ts +9 -0
  34. package/dist/chat/input.d.ts.map +1 -0
  35. package/dist/chat/input.js +99 -0
  36. package/dist/chat/kit.d.ts +61 -0
  37. package/dist/chat/kit.d.ts.map +1 -0
  38. package/dist/chat/kit.js +123 -0
  39. package/dist/chat/lasting.d.ts +6 -0
  40. package/dist/chat/lasting.d.ts.map +1 -0
  41. package/dist/chat/lasting.js +19 -0
  42. package/dist/chat/list.d.ts +27 -0
  43. package/dist/chat/list.d.ts.map +1 -0
  44. package/dist/chat/list.js +63 -0
  45. package/dist/chat/markdown.d.ts +20 -0
  46. package/dist/chat/markdown.d.ts.map +1 -0
  47. package/dist/chat/markdown.js +188 -0
  48. package/dist/chat/message.d.ts +6 -0
  49. package/dist/chat/message.d.ts.map +1 -0
  50. package/dist/chat/message.js +192 -0
  51. package/dist/chat/messages.d.ts +22 -0
  52. package/dist/chat/messages.d.ts.map +1 -0
  53. package/dist/chat/messages.js +155 -0
  54. package/dist/chat/name.d.ts +6 -0
  55. package/dist/chat/name.d.ts.map +1 -0
  56. package/dist/chat/name.js +37 -0
  57. package/dist/chat/opened.d.ts +11 -0
  58. package/dist/chat/opened.d.ts.map +1 -0
  59. package/dist/chat/opened.js +17 -0
  60. package/dist/chat/overlay.d.ts +31 -0
  61. package/dist/chat/overlay.d.ts.map +1 -0
  62. package/dist/chat/overlay.js +97 -0
  63. package/dist/chat/parts.d.ts +57 -0
  64. package/dist/chat/parts.d.ts.map +1 -0
  65. package/dist/chat/parts.js +119 -0
  66. package/dist/chat/process.d.ts +20 -0
  67. package/dist/chat/process.d.ts.map +1 -0
  68. package/dist/chat/process.js +32 -0
  69. package/dist/chat/provider.d.ts +63 -0
  70. package/dist/chat/provider.d.ts.map +1 -0
  71. package/dist/chat/provider.js +629 -0
  72. package/dist/chat/question.d.ts +9 -0
  73. package/dist/chat/question.d.ts.map +1 -0
  74. package/dist/chat/question.js +80 -0
  75. package/dist/chat/questions.d.ts +31 -0
  76. package/dist/chat/questions.d.ts.map +1 -0
  77. package/dist/chat/questions.js +111 -0
  78. package/dist/chat/read.d.ts +5 -0
  79. package/dist/chat/read.d.ts.map +1 -0
  80. package/dist/chat/read.js +13 -0
  81. package/dist/chat/ready.d.ts +34 -0
  82. package/dist/chat/ready.d.ts.map +1 -0
  83. package/dist/chat/ready.js +91 -0
  84. package/dist/chat/style.d.ts +4 -0
  85. package/dist/chat/style.d.ts.map +1 -0
  86. package/dist/chat/style.js +302 -0
  87. package/dist/chat.d.ts +14 -0
  88. package/dist/chat.d.ts.map +1 -0
  89. package/dist/chat.js +8 -0
  90. package/dist/client.d.ts +28 -14
  91. package/dist/client.d.ts.map +1 -1
  92. package/dist/client.js +71 -26
  93. package/dist/collection.d.ts +21 -12
  94. package/dist/collection.d.ts.map +1 -1
  95. package/dist/collection.js +271 -23
  96. package/dist/connection.d.ts +50 -23
  97. package/dist/connection.d.ts.map +1 -1
  98. package/dist/connection.js +2074 -735
  99. package/dist/conversation.d.ts +41 -14
  100. package/dist/conversation.d.ts.map +1 -1
  101. package/dist/conversation.js +546 -214
  102. package/dist/durability.d.ts +7 -1
  103. package/dist/durability.d.ts.map +1 -1
  104. package/dist/entry.d.ts +17 -0
  105. package/dist/entry.d.ts.map +1 -0
  106. package/dist/entry.js +156 -0
  107. package/dist/expectation.d.ts +21 -0
  108. package/dist/expectation.d.ts.map +1 -0
  109. package/dist/expectation.js +70 -0
  110. package/dist/failure.d.ts +13 -0
  111. package/dist/failure.d.ts.map +1 -0
  112. package/dist/failure.js +19 -0
  113. package/dist/fence.d.ts +6 -0
  114. package/dist/fence.d.ts.map +1 -0
  115. package/dist/fence.js +10 -0
  116. package/dist/host.d.ts +24 -2
  117. package/dist/host.d.ts.map +1 -1
  118. package/dist/host.js +126 -45
  119. package/dist/identity.d.ts +5 -0
  120. package/dist/identity.d.ts.map +1 -1
  121. package/dist/identity.js +19 -1
  122. package/dist/index.d.ts +16 -8
  123. package/dist/index.d.ts.map +1 -1
  124. package/dist/index.js +5 -0
  125. package/dist/memory.d.ts +17 -7
  126. package/dist/memory.d.ts.map +1 -1
  127. package/dist/memory.js +159 -46
  128. package/dist/model.d.ts +17 -14
  129. package/dist/model.d.ts.map +1 -1
  130. package/dist/model.js +16 -4
  131. package/dist/observation.d.ts +6 -1
  132. package/dist/observation.d.ts.map +1 -1
  133. package/dist/observation.js +38 -17
  134. package/dist/open.d.ts +2 -0
  135. package/dist/open.d.ts.map +1 -0
  136. package/dist/open.js +1 -0
  137. package/dist/outcome.d.ts +32 -3
  138. package/dist/outcome.d.ts.map +1 -1
  139. package/dist/outcome.js +42 -1
  140. package/dist/overlay.d.ts +16 -3
  141. package/dist/overlay.d.ts.map +1 -1
  142. package/dist/overlay.js +118 -77
  143. package/dist/paths.d.ts +0 -1
  144. package/dist/paths.d.ts.map +1 -1
  145. package/dist/paths.js +0 -16
  146. package/dist/query.d.ts +46 -6
  147. package/dist/query.d.ts.map +1 -1
  148. package/dist/query.js +85 -37
  149. package/dist/reach.d.ts +45 -0
  150. package/dist/reach.d.ts.map +1 -0
  151. package/dist/reach.js +306 -0
  152. package/dist/react.d.ts +44 -0
  153. package/dist/react.d.ts.map +1 -0
  154. package/dist/react.js +66 -0
  155. package/dist/read-side.d.ts +16 -5
  156. package/dist/read-side.d.ts.map +1 -1
  157. package/dist/read-side.js +408 -137
  158. package/dist/readiness.d.ts +53 -0
  159. package/dist/readiness.d.ts.map +1 -0
  160. package/dist/readiness.js +33 -0
  161. package/dist/recovery.d.ts +25 -0
  162. package/dist/recovery.d.ts.map +1 -0
  163. package/dist/recovery.js +197 -0
  164. package/dist/refresh.d.ts +12 -1
  165. package/dist/refresh.d.ts.map +1 -1
  166. package/dist/refresh.js +51 -36
  167. package/dist/replica.d.ts +10 -7
  168. package/dist/replica.d.ts.map +1 -1
  169. package/dist/replica.js +129 -73
  170. package/dist/room-draft.d.ts +41 -9
  171. package/dist/room-draft.d.ts.map +1 -1
  172. package/dist/room-draft.js +139 -28
  173. package/dist/rows.d.ts +11 -14
  174. package/dist/rows.d.ts.map +1 -1
  175. package/dist/rows.js +87 -50
  176. package/dist/save.d.ts +32 -3
  177. package/dist/save.d.ts.map +1 -1
  178. package/dist/save.js +465 -92
  179. package/dist/search.d.ts +37 -8
  180. package/dist/search.d.ts.map +1 -1
  181. package/dist/search.js +116 -45
  182. package/dist/seed.d.ts +16 -0
  183. package/dist/seed.d.ts.map +1 -0
  184. package/dist/seed.js +61 -0
  185. package/dist/shared.d.ts +11 -0
  186. package/dist/shared.d.ts.map +1 -0
  187. package/dist/shared.js +368 -0
  188. package/dist/status.d.ts +6 -2
  189. package/dist/status.d.ts.map +1 -1
  190. package/dist/status.js +0 -8
  191. package/dist/store.d.ts +76 -19
  192. package/dist/store.d.ts.map +1 -1
  193. package/dist/store.js +539 -200
  194. package/dist/turn.d.ts +7 -0
  195. package/dist/turn.d.ts.map +1 -0
  196. package/dist/turn.js +45 -0
  197. package/dist/wire.d.ts +162 -19
  198. package/dist/wire.d.ts.map +1 -1
  199. package/dist/wire.js +82 -61
  200. package/package.json +31 -7
  201. package/dist/attachment.d.ts +0 -3
  202. package/dist/attachment.d.ts.map +0 -1
  203. package/dist/attachment.js +0 -8
@@ -1,77 +1,106 @@
1
- import { verify } from '@agentshouse/mdmodel';
1
+ import { attachedModel } from './attached-model.js';
2
2
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
3
- import { opened } from './observation.js';
4
- import { filterOf, identityOf, planOf, } from './query.js';
3
+ import { observing, whole } from './observation.js';
4
+ import { filterOf, identityOf, planOf, queryKey, } from './query.js';
5
5
  import { createReadSide } from './read-side.js';
6
+ import { seededResult } from './seed.js';
7
+ import { markdownPath, roomOfKey } from './rows.js';
6
8
  import { createSearchIndex, searchPlanOf, } from './search.js';
7
- import { ROOM_CONTRACT_VERSION, VERSION_1_CAPABILITIES, } from './adapter.js';
8
9
  import { bootstrapRoom } from './bootstrap.js';
9
- import { changeSet, mergeChangeSets } from './change-set.js';
10
+ import { changeSet, entering, mergeChangeSets } from './change-set.js';
10
11
  import { createConversationStore, } from './conversation.js';
11
- import { MD_MODEL, parseCollection, validRoot } from './collection.js';
12
+ import { queuedFile } from './change.js';
13
+ import { indexReferrers, referencesIndexed, validRoot, } from './collection.js';
14
+ import { runtimeError } from './failure.js';
12
15
  import { createLog } from './log.js';
16
+ import { binaryRecord } from './binary.js';
17
+ import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
18
+ import { createRecovery } from './recovery.js';
13
19
  import { refreshRoom } from './refresh.js';
14
- import { buildBatch } from './save.js';
15
- import { applyReturned, compositionOf, landed, readConfirmed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
16
- import { changedParts, changesOf, held, readParts, returnedInto, underneath, } from './room-draft.js';
17
- import { settled, same, saving, STARTING, transferring, } from './status.js';
20
+ import { buildBatch, joined, limitedIn, requeued } from './save.js';
21
+ import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
22
+ import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
23
+ import { same, saving, STARTING, transferring, } from './status.js';
18
24
  import { detectStorageLoss, nextGeneration } from './durability.js';
19
- import { roomDraftRevision } from './identity.js';
25
+ import { demanded, narrower, WHOLE_DEMAND, wider } from './acquisition.js';
26
+ import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
27
+ import { FencedMap } from './fence.js';
28
+ import { tally } from './shared.js';
29
+ import { inChunks, ROW_WEIGHT, yielded } from './turn.js';
30
+ import { identityTime, roomDraftRevision } from './identity.js';
20
31
  import { beneath, normalize } from './paths.js';
21
32
  import { openRuntimeStore, } from './store.js';
22
33
  import { unconfirmed } from './transfer.js';
23
- // Discovery repeats each minute so a Room the stream stopped serving stays unrefreshed no longer.
24
- const DEFAULT_DISCOVERY_INTERVAL_MS = 60_000;
25
- const EMPTY_SCHEMA = { verdict: 'accept', model: { types: {} } };
26
- const knownCapabilities = new Set(VERSION_1_CAPABILITIES);
34
+ import { governs, holds, narrowed, planReach, } from './reach.js';
27
35
  function within(root, path) {
28
36
  return root === '' || beneath(root, path);
29
37
  }
30
- function declaredIdentity(model) {
31
- if (model === undefined)
32
- return false;
33
- if (model === null)
34
- return true;
35
- return (typeof model.identity === 'string' &&
36
- model.identity.length > 0 &&
37
- typeof model.version === 'string' &&
38
- model.version.length > 0);
39
- }
40
38
  function declaredSelection(room) {
41
39
  const selection = room.typedSelection;
42
40
  if (selection === undefined)
43
41
  return true;
44
42
  if (!Array.isArray(selection.roots) || selection.roots.length === 0)
45
43
  return false;
46
- if (!declaredIdentity(selection.attachedModel))
47
- return false;
48
44
  return selection.roots.every((root, index) => validRoot(root) &&
49
45
  root !== '' &&
50
46
  selection.roots.every((other, at) => at === index || !within(other, root)));
51
47
  }
48
+ const OPENING_LANE = 'opening';
49
+ const CONVERSATION_LANE = 'conversations';
50
+ const ROOM_READS = 256;
51
+ const ROOM_ATTEMPTS = 16;
52
+ const outside = new Map();
53
+ function outsideOf(roots) {
54
+ const key = JSON.stringify(roots);
55
+ const held = outside.get(key);
56
+ if (held !== undefined)
57
+ return held;
58
+ const counted = (path) => (roots.some((root) => beneath(root, path)) ? 0 : 1);
59
+ const made = {
60
+ documents: (path) => (markdownPath(path) ? counted(path) : 0),
61
+ binaries: counted,
62
+ };
63
+ outside.set(key, made);
64
+ return made;
65
+ }
66
+ function abandonedOn(signal) {
67
+ return new Promise((resolve) => {
68
+ if (signal.aborted)
69
+ resolve(null);
70
+ else
71
+ signal.addEventListener('abort', () => resolve(null), { once: true });
72
+ });
73
+ }
74
+ function roomLane(locator) {
75
+ return `room ${locator}`;
76
+ }
52
77
  export function composeRuntime(options) {
53
- const period = options.discoveryIntervalMs ?? DEFAULT_DISCOVERY_INTERVAL_MS;
78
+ const localSaves = options.adapter.localSaves === true;
54
79
  let authorization = options.authorization;
55
80
  let opening;
56
81
  let store;
57
- const replicas = new Map();
82
+ const fences = new Map();
83
+ const isFenced = (locator) => fences.has(locator);
84
+ let fencings = 0;
85
+ const lastFenced = new Map();
86
+ const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
87
+ let discoveries = 0;
88
+ const replicas = new FencedMap(isFenced);
89
+ const queuesWithoutReplica = new FencedMap(isFenced);
58
90
  const pendings = new Map();
59
- const roomDrafts = new Map();
91
+ const seeds = new Map();
92
+ const seedWatchers = new Map();
93
+ const roomDrafts = new FencedMap(isFenced);
60
94
  const presentations = new Map();
61
- const wanted = new Map();
62
95
  let limits = [];
63
96
  let capabilities = [];
64
- function rewriting() {
65
- return capabilities.some((capability) => capability.name === 'move-reference-rewrites')
66
- ? 'provider'
67
- : 'runtime';
68
- }
69
- const rooms = new Map();
97
+ const rooms = new FencedMap(isFenced);
98
+ const counted = new Map();
70
99
  const unreadable = new Set();
71
- const models = new Map();
72
- const liveModels = new Map();
100
+ const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
101
+ const models = new FencedMap(isModelFenced);
102
+ const liveModels = new FencedMap(isModelFenced);
73
103
  const invalidModels = new Set();
74
- const modelEpoch = new Map();
75
104
  const discovered = new Map();
76
105
  let roomsListed = false;
77
106
  const scopedRefusals = new Map();
@@ -100,16 +129,29 @@ export function composeRuntime(options) {
100
129
  let status = STARTING;
101
130
  let shown = STARTING;
102
131
  let discoveryPaused = false;
103
- let timer;
104
132
  let closed = false;
105
133
  let stopping = false;
106
- let withdrawNotifications;
107
- let handed;
108
- const notices = new Map();
109
- let chain = Promise.resolve();
134
+ let withdrawStream;
135
+ const announced = new Map();
136
+ let underway = null;
137
+ let streamed = null;
138
+ let handed = false;
139
+ const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
140
+ const repairing = new Set();
141
+ const repairable = new Map();
142
+ let outcomeRetention = null;
143
+ const answering = new Map();
144
+ let complete = false;
145
+ let recorded = false;
146
+ const stale = new Set();
147
+ let generation = 0;
148
+ let discovering = null;
149
+ let opened = null;
150
+ const owed = new Set();
151
+ const lanes = new Map();
152
+ let roomReads = { generation, free: ROOM_READS, waiting: [] };
153
+ const working = new Set();
110
154
  let mutations = Promise.resolve();
111
- let inflight = false;
112
- let queued = false;
113
155
  const roomListeners = new Set();
114
156
  const refusalListeners = new Set();
115
157
  const storageLossListeners = new Set();
@@ -119,26 +161,49 @@ export function composeRuntime(options) {
119
161
  const presentationListeners = new Set();
120
162
  const readSide = createReadSide();
121
163
  let withdrawConversations;
122
- let currencyQueued = false;
164
+ let currencyQueued = null;
165
+ let socketOpen = false;
123
166
  let serviceMode = 'open';
124
167
  let haltedRefusal = false;
125
168
  let openSignals = 0;
126
169
  const serviceModeListeners = new Set();
170
+ let member = null;
171
+ const memberListeners = new Set();
172
+ const prepared = new Set();
173
+ const projecting = new Set();
174
+ const unpreparedDemand = new Set();
175
+ const unindexable = new Set();
176
+ let preparing = false;
177
+ let demand = WHOLE_DEMAND;
178
+ let narrowing = null;
179
+ let targets = {};
180
+ let persistence = options.shell.persistence;
181
+ let capacity = null;
182
+ const paused = new Set();
183
+ let rechecking = null;
184
+ const readinessWatches = new Set();
185
+ let readinessQueued = false;
186
+ const catching = new Set();
187
+ const transfers = new FencedMap(isFenced);
127
188
  const conversations = createConversationStore({
128
- cards: declaredCards,
129
- documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
189
+ agents: declaredAgents,
190
+ documents: ({ locator }) => rowsQuery(planOf({ rooms: [locator], select: { fields: 'all' } }, roomFacts())),
130
191
  call: (name, input) => invokeAction(name, input),
131
192
  durably,
132
193
  stream: () => options.adapter.conversations,
133
194
  publish: publishChangeSet,
195
+ online: () => !status.providerUnreachable && refusal === null,
134
196
  closed: () => closed,
197
+ unanswered: resendLater,
198
+ expired: (operation) => pastHorizon(operation),
135
199
  });
136
200
  const searchIndex = createSearchIndex({
137
- documents: () => readSide.documents(),
201
+ room: (room) => readSide.room(room),
138
202
  document: (room, path) => readSide.document(room, path),
139
- messages: () => conversations.messages(),
140
- message: (conversation, identity) => conversations.message(conversation, identity),
141
203
  });
204
+ const roomsToIndex = new Set();
205
+ const conversationsToIndex = new Set();
206
+ let indexing = false;
142
207
  const journal = createLog();
143
208
  let notifyReady = () => { };
144
209
  const ready = new Promise((resolve) => {
@@ -202,7 +267,9 @@ export function composeRuntime(options) {
202
267
  }
203
268
  }
204
269
  function pending() {
205
- return [...replicas.values()].some((replica) => replica.queue.length > 0);
270
+ return (!localSaves &&
271
+ (queuesWithoutReplica.size > 0 ||
272
+ [...replicas.values()].some((replica) => replica.queue.length > 0)));
206
273
  }
207
274
  function attention() {
208
275
  const attentionRequired = [];
@@ -215,9 +282,16 @@ export function composeRuntime(options) {
215
282
  returned: structuredClone(record.returned),
216
283
  });
217
284
  }
285
+ for (const [room, refusal] of sourceRefusals)
286
+ attentionRequired.push({ room, refusal: structuredClone(refusal) });
218
287
  attentionRequired.sort((left, right) => {
219
288
  if (left.room !== right.room)
220
289
  return left.room < right.room ? -1 : 1;
290
+ if ('refusal' in left || 'refusal' in right) {
291
+ if ('refusal' in left && 'refusal' in right)
292
+ return 0;
293
+ return 'refusal' in left ? -1 : 1;
294
+ }
221
295
  if (left.path !== right.path)
222
296
  return left.path < right.path ? -1 : 1;
223
297
  return 0;
@@ -231,14 +305,30 @@ export function composeRuntime(options) {
231
305
  }
232
306
  return { attentionRequired, reset };
233
307
  }
308
+ function documentsIn(locator) {
309
+ const replica = replicas.get(locator);
310
+ if (replica === undefined)
311
+ return 0;
312
+ const roots = replica.confirmed.record.typedSelection?.roots ?? [];
313
+ const weighed = outsideOf(roots);
314
+ return (tally(replica.visible.documents, weighed.documents) +
315
+ tally(replica.visible.binaries, weighed.binaries));
316
+ }
234
317
  function listed() {
235
- return structuredClone([...rooms.values()]);
318
+ return structuredClone([...rooms.values()].map((room) => {
319
+ const documents = documentsIn(room.locator);
320
+ counted.set(room.locator, documents);
321
+ return { ...room, documents };
322
+ }));
236
323
  }
237
- function roomFacts() {
238
- const held = [...rooms.values()].map((room) => {
324
+ function roomFacts(reach = null) {
325
+ const held = [...rooms.values()]
326
+ .filter((room) => holds(reach, room.locator))
327
+ .map((room) => {
328
+ const governed = governs(reach, room);
239
329
  const attached = [
240
- modelOf(room.locator),
241
- ...(room.typedSelection === null ? [] : [modelOf(room.locator, 'typed-selection')]),
330
+ ...(governed.collection ? [modelOf(room.locator)] : []),
331
+ ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
242
332
  ];
243
333
  return {
244
334
  locator: room.locator,
@@ -249,18 +339,26 @@ export function composeRuntime(options) {
249
339
  return { listed: roomsListed, rooms: held };
250
340
  }
251
341
  function emitRooms() {
342
+ readSide.recover();
343
+ queueReadiness();
252
344
  const value = listed();
253
345
  for (const listener of roomListeners)
254
346
  listener(value);
255
347
  }
256
348
  function emitRefusal() {
349
+ queueReadiness();
257
350
  for (const listener of refusalListeners)
258
351
  listener(refusal);
259
352
  }
260
353
  function emitStorageLoss() {
354
+ queueReadiness();
261
355
  for (const listener of storageLossListeners)
262
356
  listener(storageLoss);
263
357
  }
358
+ function emitMember() {
359
+ for (const listener of memberListeners)
360
+ listener(member);
361
+ }
264
362
  function moveTo(next) {
265
363
  if (closed)
266
364
  return;
@@ -270,7 +368,8 @@ export function composeRuntime(options) {
270
368
  function show() {
271
369
  if (closed)
272
370
  return;
273
- const next = status.state === 'synced' && !conversations.current()
371
+ queueReadiness();
372
+ const next = status.state === 'synced' && demand.conversations && !conversations.current()
274
373
  ? { ...status, state: 'syncing' }
275
374
  : status;
276
375
  if (same(shown, next))
@@ -280,6 +379,8 @@ export function composeRuntime(options) {
280
379
  listener(shown);
281
380
  }
282
381
  function entryOf(batch) {
382
+ if (localSaves)
383
+ return Object.freeze({ batch, state: 'local' });
283
384
  const state = pendings.get(batch.id);
284
385
  if (state?.accepted !== undefined) {
285
386
  return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
@@ -287,10 +388,7 @@ export function composeRuntime(options) {
287
388
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
288
389
  }
289
390
  function entriesOf(locator) {
290
- const replica = replicas.get(locator);
291
- if (replica === undefined)
292
- return [];
293
- return replica.queue.map(entryOf);
391
+ return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
294
392
  }
295
393
  function emitQueue(locator) {
296
394
  const entries = entriesOf(locator);
@@ -300,12 +398,516 @@ export function composeRuntime(options) {
300
398
  }
301
399
  }
302
400
  function applyChangeSet(set) {
303
- readSide.apply(set);
304
- searchIndex.changed(set);
401
+ const held = {
402
+ ...set,
403
+ documents: set.documents.filter(({ locator }) => prepared.has(locator) || projecting.has(locator)),
404
+ };
405
+ readSide.apply(held);
406
+ searchIndex.changed(held);
407
+ }
408
+ async function referenced(locator) {
409
+ for (;;) {
410
+ const record = replicas.get(locator)?.confirmed.record;
411
+ if (record === undefined || closed)
412
+ return;
413
+ const held = { text: record.documents, binaries: record.binaries };
414
+ const composition = compositionOf(record);
415
+ if (referencesIndexed(held, composition))
416
+ return;
417
+ await indexReferrers(held, composition);
418
+ }
419
+ }
420
+ async function prepareReplica(locator) {
421
+ await referenced(locator);
422
+ const replica = replicas.get(locator);
423
+ if (replica === undefined || closed || prepared.has(locator))
424
+ return;
425
+ const visible = replica.visible;
426
+ const paths = [
427
+ ...new Set([...Object.keys(visible.documents), ...Object.keys(visible.binaries)]),
428
+ ];
429
+ const entered = [];
430
+ projecting.add(locator);
431
+ try {
432
+ await inChunks(paths, (path) => ROW_WEIGHT + (visible.documents[path]?.length ?? 0), (chunk) => {
433
+ const current = replicas.get(locator);
434
+ if (current === undefined || closed)
435
+ return;
436
+ const set = entering(locator, current, chunk);
437
+ readSide.apply(set);
438
+ entered.push(set);
439
+ });
440
+ if (!replicas.has(locator) || closed)
441
+ return;
442
+ prepared.add(locator);
443
+ }
444
+ finally {
445
+ projecting.delete(locator);
446
+ }
447
+ conversations.changed(mergeChangeSets(entered));
448
+ readSide.recover();
449
+ queueReadiness();
450
+ }
451
+ function unprepared() {
452
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
453
+ }
454
+ function unbuilt(locator, error) {
455
+ unindexable.add(locator);
456
+ journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
457
+ queueReadiness();
458
+ }
459
+ function prepare(locators = []) {
460
+ for (const locator of locators)
461
+ unpreparedDemand.add(locator);
462
+ if (preparing)
463
+ return;
464
+ preparing = true;
465
+ void (async () => {
466
+ try {
467
+ for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
468
+ await yielded();
469
+ if (closed)
470
+ return;
471
+ unpreparedDemand.delete(waiting);
472
+ await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
473
+ }
474
+ }
475
+ finally {
476
+ preparing = false;
477
+ }
478
+ })();
479
+ }
480
+ function reachedRooms(plan) {
481
+ const reach = plan.reach;
482
+ const reached = reach === null ? null : new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
483
+ const named = plan.rooms ??
484
+ (reached === null
485
+ ? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
486
+ : [...reached]);
487
+ return [...new Set(named)].filter((locator) => reached === null || reached.has(locator)).sort();
488
+ }
489
+ function awaited(locator) {
490
+ return (!roomsListed ||
491
+ (rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
492
+ }
493
+ function covered(plan) {
494
+ const locators = reachedRooms(plan);
495
+ if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
496
+ return { pending: [] };
497
+ }
498
+ const pending = [];
499
+ const coverage = [];
500
+ const overlay = [];
501
+ for (const locator of locators) {
502
+ const replica = replicas.get(locator);
503
+ if (replica === undefined) {
504
+ if (awaited(locator))
505
+ pending.push({ room: locator, awaits: 'acquisition' });
506
+ continue;
507
+ }
508
+ if (!prepared.has(locator)) {
509
+ pending.push({ room: locator, awaits: 'preparation' });
510
+ continue;
511
+ }
512
+ coverage.push({
513
+ room: locator,
514
+ generation: replica.confirmed.record.transfer,
515
+ current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
516
+ });
517
+ for (const batch of replica.queue)
518
+ overlay.push(batch.id);
519
+ }
520
+ if (pending.length > 0)
521
+ return { pending };
522
+ return { coverage: { rooms: coverage, indexes: [], overlay } };
523
+ }
524
+ function awaitPreparation(cover) {
525
+ if (!('pending' in cover))
526
+ return;
527
+ const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
528
+ if (waiting.length > 0)
529
+ prepare(waiting.map(({ room }) => room));
530
+ }
531
+ function acquires(room) {
532
+ return (demanded(demand, room) ||
533
+ (replicas.get(room.locator)?.queue.length ?? 0) > 0 ||
534
+ queuesWithoutReplica.has(room.locator));
535
+ }
536
+ function acquiredLocators() {
537
+ return [...discovered.values()]
538
+ .filter(acquires)
539
+ .map((room) => room.locator)
540
+ .sort();
541
+ }
542
+ function heldDemand(acquired) {
543
+ if (demand.whole)
544
+ return null;
545
+ return { rooms: acquired.map((room) => room.locator), conversations: demand.conversations };
546
+ }
547
+ function applyDemand(next) {
548
+ const before = acquiredLocators();
549
+ const conversed = demand.conversations;
550
+ demand = next;
551
+ const after = acquiredLocators();
552
+ queueReadiness();
553
+ if (!conversed && next.conversations)
554
+ scheduleCurrency();
555
+ if (JSON.stringify(before) === JSON.stringify(after) && conversed === next.conversations)
556
+ return;
557
+ journal.record(`acquisition ${next.whole ? 'whole' : after.length}`);
558
+ if (options.adapter.roomStream === undefined)
559
+ void sync('unstreamed');
560
+ else if (socketOpen)
561
+ options.adapter.roomStream.reopen();
562
+ }
563
+ function acquire(next) {
564
+ if (closed)
565
+ return;
566
+ if (narrowing !== null)
567
+ clearTimeout(narrowing);
568
+ narrowing = null;
569
+ const widening = wider(next, demand);
570
+ if (!widening && !narrower(next, demand))
571
+ return;
572
+ if (widening || !roomsListed) {
573
+ applyDemand(next);
574
+ return;
575
+ }
576
+ // A narrower demand waits five seconds, so a tab that reloads returns before the stream reopens twice.
577
+ narrowing = setTimeout(() => {
578
+ narrowing = null;
579
+ applyDemand(next);
580
+ }, 5_000);
581
+ }
582
+ function requiredRooms(reach) {
583
+ if (reach === null)
584
+ return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
585
+ const named = [...new Set(reach.rooms)].sort();
586
+ if (roomsListed)
587
+ return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
588
+ return { listed: named.every((room) => replicas.has(room)), rooms: named };
589
+ }
590
+ function roomState(locator, search) {
591
+ if (sourceRefusals.has(locator))
592
+ return 'refused';
593
+ const listedRoom = discovered.get(locator);
594
+ if (roomsListed &&
595
+ (listedRoom === undefined ||
596
+ listedRoom.replica.admission !== 'admitted' ||
597
+ unreadable.has(locator)))
598
+ return 'unadmitted';
599
+ if (!replicas.has(locator))
600
+ return 'acquiring';
601
+ if (!prepared.has(locator))
602
+ return 'preparing';
603
+ if (search && !searchIndex.rooms().has(locator))
604
+ return 'indexing';
605
+ return 'complete';
606
+ }
607
+ function conversationCoverage(reach, search) {
608
+ const declared = reach === null || reach.conversations;
609
+ if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
610
+ return 'undeclared';
611
+ if (!conversations.known() || !conversations.current())
612
+ return 'acquiring';
613
+ if (!search)
614
+ return 'complete';
615
+ const unindexed = conversations
616
+ .searchable()
617
+ .filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
618
+ if (unindexed.length === 0)
619
+ return 'complete';
620
+ index([], unindexed);
621
+ return 'indexing';
622
+ }
623
+ function connectionOf() {
624
+ if (socketOpen)
625
+ return 'connected';
626
+ if (status.providerUnreachable)
627
+ return 'unreachable';
628
+ return options.adapter.roomStream === undefined && status.lastCompleted !== null
629
+ ? 'connected'
630
+ : 'connecting';
631
+ }
632
+ function refusedIn(locators) {
633
+ return locators.flatMap((room) => {
634
+ const held = sourceRefusals.get(room);
635
+ if (held === undefined)
636
+ return [];
637
+ const { file, location } = held.fields;
638
+ return [
639
+ {
640
+ room,
641
+ file: typeof file === 'string' ? file : '',
642
+ rule: held.code,
643
+ location: typeof location === 'string' ? location : null,
644
+ },
645
+ ];
646
+ });
647
+ }
648
+ function readinessFor(port, reach) {
649
+ const required = requiredRooms(reach);
650
+ const search = port.search === 'required';
651
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
652
+ const building = (state) => coverage
653
+ .filter((room) => room.state === state && !unindexable.has(room.room))
654
+ .map(({ room }) => room);
655
+ const preparing = building('preparing');
656
+ if (preparing.length > 0)
657
+ prepare(preparing);
658
+ const indexing = building('indexing');
659
+ if (indexing.length > 0)
660
+ index(indexing, []);
661
+ return readinessOf({
662
+ code: port.code,
663
+ coverage: {
664
+ listed: required.listed,
665
+ rooms: coverage,
666
+ conversations: conversationCoverage(reach, search),
667
+ search: search ? 'required' : 'unused',
668
+ },
669
+ persistence,
670
+ connection: connectionOf(),
671
+ house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
672
+ capacity: capacity !== null &&
673
+ coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
674
+ ? capacity
675
+ : port.capacity,
676
+ refused: refusedIn(required.rooms),
677
+ lost: storageLoss !== null,
678
+ established: targets[port.target]?.established === true,
679
+ });
680
+ }
681
+ function establish(name) {
682
+ targets = { ...targets, [name]: { established: true } };
683
+ queueReadiness();
684
+ void durably((local) => local.keepTargets(targets)).catch(() => {
685
+ journal.record(`target record failed ${name}`);
686
+ });
687
+ }
688
+ function deliverReadiness(watch) {
689
+ if (!readinessWatches.has(watch))
690
+ return;
691
+ const readiness = readinessFor(watch.port, watch.reach);
692
+ if (readiness.ready && targets[watch.port.target]?.established !== true) {
693
+ establish(watch.port.target);
694
+ }
695
+ const shown = JSON.stringify(readiness);
696
+ if (shown === watch.last)
697
+ return;
698
+ watch.last = shown;
699
+ watch.deliver(structuredClone(readiness));
700
+ }
701
+ function queueReadiness() {
702
+ if (readinessQueued || closed || readinessWatches.size === 0)
703
+ return;
704
+ readinessQueued = true;
705
+ queueMicrotask(() => {
706
+ readinessQueued = false;
707
+ if (closed)
708
+ return;
709
+ for (const watch of [...readinessWatches])
710
+ deliverReadiness(watch);
711
+ });
712
+ }
713
+ function watchReadiness(port, reach, deliver) {
714
+ const watch = { port, reach, deliver, last: null };
715
+ readinessWatches.add(watch);
716
+ void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
717
+ return () => {
718
+ readinessWatches.delete(watch);
719
+ };
720
+ }
721
+ function layerBytes(layer) {
722
+ let bytes = 0;
723
+ for (const [path, text] of Object.entries(layer.documents))
724
+ bytes += (path.length + text.length) * 2;
725
+ return bytes;
726
+ }
727
+ function grownBy(next, previous) {
728
+ if (previous === undefined)
729
+ return layerBytes(next);
730
+ const documents = Object.fromEntries(Object.entries(next.documents).filter(([path, text]) => previous.documents[path] !== text));
731
+ return layerBytes({ ...next, documents });
732
+ }
733
+ function exceeded(error) {
734
+ return (typeof error === 'object' &&
735
+ error !== null &&
736
+ error.name === 'QuotaExceededError');
737
+ }
738
+ async function estimated() {
739
+ const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
740
+ return estimate === null ? null : Math.max(0, estimate.quota - estimate.usage);
741
+ }
742
+ function pause(locator, cause) {
743
+ paused.add(locator);
744
+ capacity = cause;
745
+ journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
746
+ queueReadiness();
747
+ if (rechecking !== null)
748
+ return;
749
+ // A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
750
+ rechecking = setInterval(() => {
751
+ void recheck();
752
+ }, 60_000);
753
+ }
754
+ async function recheck() {
755
+ const held = capacity;
756
+ if (held === null || closed)
757
+ return;
758
+ const available = await estimated().catch(() => null);
759
+ if (available === null || available < (held.needed ?? 0))
760
+ return;
761
+ if (held.cause === 'quota-exceeded' && (held.available === null || available <= held.available))
762
+ return;
763
+ resume();
764
+ }
765
+ function resume() {
766
+ if (rechecking !== null)
767
+ clearInterval(rechecking);
768
+ rechecking = null;
769
+ if (paused.size === 0 && capacity === null)
770
+ return;
771
+ paused.clear();
772
+ capacity = null;
773
+ journal.record('capacity resumed');
774
+ queueReadiness();
775
+ if (!closed && authorization !== undefined)
776
+ void sync('unstreamed');
777
+ }
778
+ async function reclaimed(local) {
779
+ const unreferenced = [...transfers.keys()].filter((locator) => {
780
+ const room = discovered.get(locator);
781
+ return room === undefined || !acquires(room);
782
+ });
783
+ for (const locator of unreferenced) {
784
+ await local.abandon(locator);
785
+ transfers.delete(locator);
786
+ }
787
+ if (unreferenced.length > 0)
788
+ journal.record(`capacity reclaimed ${unreferenced.length}`);
789
+ return unreferenced.length > 0;
790
+ }
791
+ async function admitted(local, locator, needed) {
792
+ if (paused.has(locator))
793
+ return false;
794
+ let available = await estimated().catch(() => null);
795
+ if (available === null || available >= needed)
796
+ return true;
797
+ if (await reclaimed(local))
798
+ available = await estimated().catch(() => null);
799
+ if (available === null || available >= needed)
800
+ return true;
801
+ pause(locator, { cause: 'insufficient-space', needed, available });
802
+ return false;
803
+ }
804
+ async function exceededOn(locator, bytes) {
805
+ const available = await estimated().catch(() => null);
806
+ pause(locator, { cause: 'quota-exceeded', needed: bytes, available });
807
+ }
808
+ function seedRoomHeld(room, version) {
809
+ return prepared.has(room) && reachedBy(room, version);
810
+ }
811
+ function seedHeld(at) {
812
+ return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
813
+ }
814
+ function seedApplies(plan) {
815
+ const seed = seeds.get(plan.key);
816
+ if (seed === undefined)
817
+ return null;
818
+ if (!seedHeld(seed.at))
819
+ return seed;
820
+ seeds.delete(plan.key);
821
+ return null;
822
+ }
823
+ function seedBehind(plan, cover) {
824
+ const seed = seedApplies(plan);
825
+ if (seed === null || 'pending' in cover)
826
+ return cover;
827
+ return {
828
+ pending: Object.entries(seed.at)
829
+ .filter(([room, version]) => !seedRoomHeld(room, version))
830
+ .map(([room]) => ({ room, awaits: 'acquisition' })),
831
+ };
832
+ }
833
+ function seed(entry) {
834
+ if (seedHeld(entry.at))
835
+ return;
836
+ for (const { query, rows } of entry.queries) {
837
+ const key = queryKey(query);
838
+ seeds.set(key, { rows, at: entry.at });
839
+ for (const shown of seedWatchers.get(key) ?? [])
840
+ shown();
841
+ }
842
+ }
843
+ async function rowsQuery(plan) {
844
+ const before = covered(plan);
845
+ awaitPreparation(before);
846
+ const seeded = seedApplies(plan);
847
+ if (seeded !== null)
848
+ return seededResult(seeded).result;
849
+ if ('pending' in before)
850
+ return before;
851
+ const { rows, at: cover } = await readSide.query(plan, () => covered(plan));
852
+ awaitPreparation(cover);
853
+ if ('pending' in cover)
854
+ return cover;
855
+ return 'groups' in rows
856
+ ? { groups: rows.groups, coverage: cover.coverage }
857
+ : { documents: rows.documents, coverage: cover.coverage };
858
+ }
859
+ async function indexConversation(conversation) {
860
+ searchIndex.indexMessages(conversation, [], false);
861
+ let after = null;
862
+ for (;;) {
863
+ const batch = await durably((local) => local.messagesAfter(conversation, after));
864
+ if (closed)
865
+ return;
866
+ searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
867
+ if (batch.length === 0)
868
+ return;
869
+ after = batch[batch.length - 1].identity;
870
+ }
871
+ }
872
+ function indexNext() {
873
+ const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
874
+ if (room !== undefined) {
875
+ roomsToIndex.delete(room);
876
+ return () => prepared.has(room)
877
+ ? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
878
+ : undefined;
879
+ }
880
+ const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
881
+ if (conversation === undefined)
882
+ return null;
883
+ conversationsToIndex.delete(conversation);
884
+ return () => indexConversation(conversation);
885
+ }
886
+ function index(rooms, named) {
887
+ for (const room of rooms)
888
+ roomsToIndex.add(room);
889
+ for (const conversation of named)
890
+ conversationsToIndex.add(conversation);
891
+ if (indexing)
892
+ return;
893
+ indexing = true;
894
+ void (async () => {
895
+ try {
896
+ for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
897
+ await yielded();
898
+ await unit();
899
+ queueReadiness();
900
+ }
901
+ }
902
+ finally {
903
+ indexing = false;
904
+ }
905
+ })().catch(() => { });
305
906
  }
306
907
  function publishChangeSet(set) {
307
908
  applyChangeSet(set);
308
- if (set.rooms.length > 0)
909
+ if (set.rooms.length > 0 ||
910
+ [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
309
911
  emitRooms();
310
912
  for (const locator of set.queues)
311
913
  emitQueue(locator);
@@ -316,26 +918,8 @@ export function composeRuntime(options) {
316
918
  for (const app of set.presentations)
317
919
  emitPresentation(app);
318
920
  conversations.changed(set);
319
- const cards = declaredCards();
320
- if (cards !== undefined && set.documents.some((change) => change.locator === cards.locator)) {
321
- scheduleCurrency();
322
- }
323
- }
324
- function stopTimer() {
325
- if (timer === undefined)
326
- return;
327
- clearInterval(timer);
328
- timer = undefined;
329
- }
330
- function startTimer() {
331
- if (timer !== undefined)
332
- return;
333
- timer = setInterval(() => {
334
- void enqueue();
335
- }, period);
336
921
  }
337
922
  async function refuse(reason, provider = null) {
338
- stopTimer();
339
923
  discoveryPaused = true;
340
924
  try {
341
925
  if (reason === 'revoked-authorization') {
@@ -343,10 +927,7 @@ export function composeRuntime(options) {
343
927
  const revoked = { status: 'unavailable', reason: 'revoked' };
344
928
  try {
345
929
  const local = await openLocal();
346
- await local.revokeModels([...replicas.values()].map((replica) => ({
347
- record: replica.confirmed.record,
348
- queue: replica.queue,
349
- })));
930
+ await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
350
931
  }
351
932
  catch {
352
933
  journal.record('model revocation persistence failed');
@@ -358,7 +939,6 @@ export function composeRuntime(options) {
358
939
  }
359
940
  for (const key of keys) {
360
941
  invalidModels.add(key);
361
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
362
942
  models.set(key, revoked);
363
943
  liveModels.delete(key);
364
944
  }
@@ -371,10 +951,7 @@ export function composeRuntime(options) {
371
951
  model: revoked,
372
952
  selectionModel: revoked,
373
953
  };
374
- const read = readConfirmed(record, before.confirmed);
375
- replicas.set(locator, read.kind === 'staged'
376
- ? replicaOf(read.confirmed, before.queue, rewriting(), before)
377
- : { ...before, confirmed: { ...before.confirmed, record } });
954
+ replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
378
955
  }
379
956
  rooms.set(locator, {
380
957
  ...room,
@@ -391,7 +968,8 @@ export function composeRuntime(options) {
391
968
  }
392
969
  finally {
393
970
  refusal = {
394
- reason,
971
+ code: reason,
972
+ retryable: false,
395
973
  reauthorizationOffered: reason === 'revoked-authorization',
396
974
  refusal: provider,
397
975
  };
@@ -404,35 +982,43 @@ export function composeRuntime(options) {
404
982
  rooms.set(room.locator, {
405
983
  name: room.name,
406
984
  locator: room.locator,
985
+ handle: room.handle,
407
986
  operations: room.operations,
408
987
  replica: room.replica,
409
- attachedModel: room.attachedModel,
410
988
  model: modelTypes(modelOf(room.locator)),
411
989
  typedSelection: selection === null
412
990
  ? null
413
991
  : {
414
992
  roots: selection.roots,
415
- attachedModel: selection.attachedModel,
416
993
  model: modelTypes(modelOf(room.locator, 'typed-selection')),
417
994
  },
418
995
  providerOwnedPaths: room.providerOwnedPaths,
419
996
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
420
- attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
421
997
  synchronizationAvailable,
422
998
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
423
999
  });
424
1000
  }
425
- async function applyDiscovery(listed, local) {
1001
+ async function applyDiscovery(found, started) {
1002
+ for (const room of found) {
1003
+ const fence = fences.get(room.locator);
1004
+ if (fence !== undefined && fence.erased && fence.at < started)
1005
+ fences.delete(room.locator);
1006
+ }
1007
+ await loseRooms(found
1008
+ .filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
1009
+ .map((room) => room.locator));
1010
+ const listed = found.filter((room) => !fences.has(room.locator));
426
1011
  const listedLocators = new Set(listed.map((room) => room.locator));
427
1012
  for (const locator of listedLocators)
428
1013
  scopedRefusals.delete(locator);
429
1014
  discovered.clear();
430
1015
  for (const room of listed)
431
1016
  discovered.set(room.locator, room);
432
- for (const locator of wanted.keys())
433
- if (!listedLocators.has(locator))
434
- wanted.delete(locator);
435
- await dropUnlisted(listedLocators, local);
1017
+ const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
1018
+ if (options.adapter.exhaustiveDiscovery === true)
1019
+ await loseRooms(unlisted);
1020
+ else
1021
+ unlistedKept(unlisted);
436
1022
  for (const room of listed) {
437
1023
  const held = replicas.get(room.locator)?.confirmed.record;
438
1024
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -443,50 +1029,97 @@ export function composeRuntime(options) {
443
1029
  }
444
1030
  roomsListed = true;
445
1031
  emitRooms();
1032
+ return listed;
446
1033
  }
447
- function unlistedHeld(listed) {
448
- return [...new Set([...rooms.keys(), ...roomDrafts.keys()])].filter((locator) => !listed.has(locator));
1034
+ function heldLocally(locator) {
1035
+ return (replicas.has(locator) ||
1036
+ roomDrafts.has(locator) ||
1037
+ transfers.has(locator) ||
1038
+ queuesWithoutReplica.has(locator));
449
1039
  }
450
- async function dropUnlisted(listed, local) {
451
- if (unlistedHeld(listed).length === 0)
452
- return;
453
- await exclusive(async () => {
454
- const unlisted = unlistedHeld(listed);
455
- await local.dropRooms(unlisted);
456
- const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
457
- room: true,
458
- queue: true,
459
- drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
460
- }));
461
- for (const locator of unlisted) {
462
- for (const batch of replicas.get(locator)?.queue ?? [])
463
- pendings.delete(batch.id);
464
- replicas.delete(locator);
1040
+ function unlistedHeld(listed) {
1041
+ return [
1042
+ ...new Set([
1043
+ ...rooms.keys(),
1044
+ ...roomDrafts.keys(),
1045
+ ...transfers.keys(),
1046
+ ...queuesWithoutReplica.keys(),
1047
+ ]),
1048
+ ].filter((locator) => !listed.has(locator));
1049
+ }
1050
+ function unlistedKept(unlisted) {
1051
+ for (const locator of unlisted) {
1052
+ const held = replicas.get(locator)?.confirmed.record;
1053
+ if (held !== undefined)
1054
+ list(held, false);
1055
+ else
465
1056
  rooms.delete(locator);
466
- roomDrafts.delete(locator);
467
- unreadable.delete(locator);
468
- scopedRefusals.delete(locator);
469
- sourceRefusals.delete(locator);
470
- declarationRefusals.delete(locator);
471
- for (const attached of ['collection', 'typed-selection']) {
472
- const key = modelKey(locator, attached);
473
- models.delete(key);
474
- liveModels.delete(key);
475
- invalidModels.delete(key);
476
- modelEpoch.delete(key);
477
- }
478
- journal.record(`room unlisted ${locator}`);
1057
+ }
1058
+ }
1059
+ function fence(locators) {
1060
+ const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1061
+ const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
1062
+ room: true,
1063
+ queue: true,
1064
+ drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
1065
+ }));
1066
+ for (const locator of fresh) {
1067
+ fences.set(locator, { at: discoveries, erased: false });
1068
+ lastFenced.set(locator, ++fencings);
1069
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1070
+ pendings.delete(batch.id);
1071
+ replicas.delete(locator);
1072
+ queuesWithoutReplica.delete(locator);
1073
+ rooms.delete(locator);
1074
+ roomDrafts.delete(locator);
1075
+ transfers.delete(locator);
1076
+ catching.delete(locator);
1077
+ discovered.delete(locator);
1078
+ announced.delete(locator);
1079
+ unreadable.delete(locator);
1080
+ stale.delete(locator);
1081
+ repairable.delete(locator);
1082
+ unindexable.delete(locator);
1083
+ paused.delete(locator);
1084
+ scopedRefusals.delete(locator);
1085
+ sourceRefusals.delete(locator);
1086
+ declarationRefusals.delete(locator);
1087
+ for (const attached of ['collection', 'typed-selection']) {
1088
+ const key = modelKey(locator, attached);
1089
+ models.delete(key);
1090
+ liveModels.delete(key);
1091
+ invalidModels.delete(key);
479
1092
  }
480
- publishChangeSet(mergeChangeSets(removed));
481
- moveTo({ ...status, ...attention() });
482
- });
1093
+ journal.record(`room lost ${locator}`);
1094
+ }
1095
+ if (fresh.length === 0)
1096
+ return fresh;
1097
+ publishChangeSet(mergeChangeSets(removed));
1098
+ for (const locator of fresh)
1099
+ prepared.delete(locator);
1100
+ searchIndex.forget(fresh);
1101
+ readSide.recover();
1102
+ moveTo({ ...status, ...attention() });
1103
+ return fresh;
1104
+ }
1105
+ async function erased(local, locators) {
1106
+ await local.erase(locators);
1107
+ for (const locator of locators) {
1108
+ const held = fences.get(locator);
1109
+ if (held !== undefined)
1110
+ held.erased = true;
1111
+ }
1112
+ }
1113
+ async function loseRooms(locators) {
1114
+ const fresh = fence(locators);
1115
+ if (fresh.length > 0)
1116
+ await durably((local) => erased(local, fresh));
483
1117
  }
484
1118
  function restored(key, persisted) {
485
1119
  const prior = models.get(key);
486
1120
  if (prior !== undefined &&
487
1121
  prior.status !== 'unavailable' &&
488
1122
  (persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
489
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
490
1123
  invalidModels.add(key);
491
1124
  }
492
1125
  const model = persisted.status === 'unavailable' ||
@@ -501,7 +1134,7 @@ export function composeRuntime(options) {
501
1134
  return model;
502
1135
  }
503
1136
  async function hydrate(opened) {
504
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
1137
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged, heldTargets,] = await Promise.all([
505
1138
  opened.all(),
506
1139
  opened.queues(),
507
1140
  opened.roomDrafts(),
@@ -511,9 +1144,17 @@ export function composeRuntime(options) {
511
1144
  opened.lastCompleted(),
512
1145
  opened.providerLimits(),
513
1146
  opened.capabilities(),
1147
+ opened.member(),
1148
+ opened.transfers(),
1149
+ opened.targets(),
514
1150
  ]);
1151
+ targets = heldTargets;
1152
+ transfers.clear();
1153
+ for (const record of staged)
1154
+ transfers.set(record.locator, record);
515
1155
  limits = declared;
516
1156
  capabilities = named;
1157
+ member = known;
517
1158
  roomDrafts.clear();
518
1159
  for (const record of kept)
519
1160
  rememberRoomDraft(record);
@@ -529,19 +1170,18 @@ export function composeRuntime(options) {
529
1170
  }
530
1171
  const previousReplicas = new Map(replicas);
531
1172
  replicas.clear();
1173
+ queuesWithoutReplica.clear();
1174
+ const replicated = new Set(records.map((record) => record.locator));
1175
+ for (const [locator, queue] of queuedBatches) {
1176
+ if (!replicated.has(locator))
1177
+ queuesWithoutReplica.set(locator, queue);
1178
+ }
532
1179
  for (const record of records) {
533
1180
  const model = restored(modelKey(record.locator, 'collection'), record.model);
534
1181
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
535
1182
  const prior = previousReplicas.get(record.locator);
536
- const retyped = { ...record, model, selectionModel };
537
- const read = readConfirmed(retyped, prior?.confirmed);
538
- const confirmed = read.kind === 'staged'
539
- ? read.confirmed
540
- : {
541
- record: retyped,
542
- collection: parseCollection(retyped.documents, compositionOf(retyped)),
543
- };
544
- replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], rewriting(), prior));
1183
+ const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
1184
+ replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
545
1185
  const existing = rooms.get(record.locator);
546
1186
  if (existing === undefined)
547
1187
  list(record, false);
@@ -549,24 +1189,17 @@ export function composeRuntime(options) {
549
1189
  rooms.set(record.locator, {
550
1190
  ...existing,
551
1191
  roomVersion: record.roomVersion,
552
- attachments: record.attachments,
553
1192
  operations: record.operations,
554
1193
  replica: record.replica,
555
- attachedModel: record.attachedModel,
556
1194
  model: modelTypes(model),
557
1195
  typedSelection: record.typedSelection === null
558
1196
  ? null
559
1197
  : {
560
1198
  roots: record.typedSelection.roots,
561
- attachedModel: record.typedSelection.attachedModel,
562
1199
  model: modelTypes(selectionModel),
563
1200
  },
564
1201
  });
565
1202
  }
566
- if (read.kind === 'refused') {
567
- refusedSource(record, read);
568
- declarationRefusals.add(record.locator);
569
- }
570
1203
  }
571
1204
  return {
572
1205
  lastCompleted,
@@ -577,16 +1210,26 @@ export function composeRuntime(options) {
577
1210
  async function readState(opened) {
578
1211
  const { lastCompleted, locators, read } = await hydrate(opened);
579
1212
  applyChangeSet(read);
1213
+ prepare();
580
1214
  if (replicas.size > 0)
581
1215
  emitRooms();
1216
+ if (member !== null)
1217
+ emitMember();
582
1218
  for (const locator of locators)
583
1219
  emitQueue(locator);
584
1220
  publishRoomDrafts(false);
585
1221
  return lastCompleted;
586
1222
  }
587
1223
  async function open() {
588
- const opened = await openRuntimeStore();
1224
+ if (closed)
1225
+ throw new Error('runtime closed');
1226
+ const opened = await openRuntimeStore(isFenced);
589
1227
  store = opened;
1228
+ const interrupted = await opened.fences();
1229
+ for (const locator of interrupted) {
1230
+ fences.set(locator, { at: discoveries, erased: false });
1231
+ lastFenced.set(locator, ++fencings);
1232
+ }
590
1233
  const kept = options.shell.generation;
591
1234
  const storedGeneration = await opened.generation();
592
1235
  const loss = detectStorageLoss(storedGeneration, kept);
@@ -600,6 +1243,8 @@ export function composeRuntime(options) {
600
1243
  emitStorageLoss();
601
1244
  }
602
1245
  const lastCompleted = await readState(opened);
1246
+ if (interrupted.length > 0)
1247
+ await erased(opened, interrupted);
603
1248
  moveTo({ ...status, lastCompleted, ...attention() });
604
1249
  return opened;
605
1250
  }
@@ -626,10 +1271,15 @@ export function composeRuntime(options) {
626
1271
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
627
1272
  }
628
1273
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
629
- const withdrawn = withdrawals(confirmed, queue, withdrawable, rewriting());
1274
+ const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
1275
+ const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
630
1276
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
1277
+ const unqueued = withdrawn.map((batch) => ({
1278
+ batch,
1279
+ returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
1280
+ }));
631
1281
  const returning = [
632
- ...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
1282
+ ...[...unqueued].reverse(),
633
1283
  ...(settling.leaving === undefined ? [] : [settling.leaving]),
634
1284
  ];
635
1285
  const written = new Map();
@@ -647,119 +1297,283 @@ export function composeRuntime(options) {
647
1297
  });
648
1298
  }
649
1299
  }
650
- await local.write({
651
- next: confirmed.record,
652
- previous: before?.confirmed.record ?? null,
653
- queue: kept,
654
- previousQueue: before?.queue ?? [],
655
- roomDrafts: [...written.values()],
656
- });
657
- for (const batch of before?.queue ?? []) {
1300
+ await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
1301
+ for (const batch of previousQueue) {
658
1302
  if (!kept.includes(batch))
659
1303
  pendings.delete(batch.id);
660
1304
  }
661
1305
  for (const record of written.values())
662
1306
  rememberRoomDraft(record);
663
- for (const batch of withdrawn)
664
- journal.record(`outcome ${locator} ${batch.id} withdrawn`);
665
- replicas.set(locator, replicaOf(confirmed, kept, rewriting(), before));
666
- return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
667
- room: settling.room,
668
- queue: kept.length !== (before?.queue.length ?? 0),
669
- drafts: [...written.keys()],
670
- });
1307
+ try {
1308
+ await local.write({
1309
+ next: confirmed.record,
1310
+ previous: before?.confirmed.record ?? null,
1311
+ ...(settling.stored === undefined ? {} : { stored: settling.stored }),
1312
+ });
1313
+ }
1314
+ catch (error) {
1315
+ const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
1316
+ if (held !== null)
1317
+ replicas.set(locator, held);
1318
+ else if (kept.length > 0)
1319
+ queuesWithoutReplica.set(locator, kept);
1320
+ else
1321
+ queuesWithoutReplica.delete(locator);
1322
+ publishChangeSet(changeSet(locator, before ?? null, held, {
1323
+ queue: kept.length !== previousQueue.length,
1324
+ drafts: [...written.keys()],
1325
+ }));
1326
+ moveTo({ ...status, ...attention() });
1327
+ throw error;
1328
+ }
1329
+ return () => {
1330
+ queuesWithoutReplica.delete(locator);
1331
+ for (const { batch, returned } of unqueued) {
1332
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
1333
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1334
+ }
1335
+ replicas.set(locator, replicaOf(confirmed, kept, before));
1336
+ return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
1337
+ room: settling.room,
1338
+ queue: kept.length !== previousQueue.length,
1339
+ drafts: [...written.keys()],
1340
+ });
1341
+ };
1342
+ }
1343
+ function adoptModels(locator, record) {
1344
+ for (const [attached, model] of [
1345
+ ['collection', record.model],
1346
+ ['typed-selection', record.selectionModel],
1347
+ ]) {
1348
+ const key = modelKey(locator, attached);
1349
+ models.set(key, model);
1350
+ invalidModels.delete(key);
1351
+ if (model.status === 'current')
1352
+ liveModels.set(key, JSON.stringify(model.snapshot));
1353
+ else if (model.status === 'unavailable')
1354
+ liveModels.delete(key);
1355
+ }
1356
+ }
1357
+ function remodel(locator, answers, held) {
1358
+ const taken = (answer, previous, attached) => {
1359
+ if (answer === undefined)
1360
+ return previous;
1361
+ const { state, refused } = attachedModel(answer, previous);
1362
+ if (refused !== null) {
1363
+ const where = refused.location === undefined ? '' : ` ${refused.location}`;
1364
+ journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
1365
+ }
1366
+ models.set(modelKey(locator, attached), state);
1367
+ return state;
1368
+ };
1369
+ return {
1370
+ collection: taken(answers?.collection, held.collection, 'collection'),
1371
+ selection: taken(answers?.selection, held.selection, 'typed-selection'),
1372
+ };
671
1373
  }
672
- function publish(local, room, confirmed) {
1374
+ function publish(local, room, confirmed, landing, live) {
1375
+ const { base, stored } = landing;
673
1376
  return exclusive(async () => {
674
- if (!continuing())
675
- return;
1377
+ if (stored !== undefined) {
1378
+ await indexReferrers({ text: confirmed.record.documents, binaries: confirmed.record.binaries }, compositionOf(confirmed.record));
1379
+ }
1380
+ if (!live()) {
1381
+ caughtUpTo(room.locator);
1382
+ return true;
1383
+ }
676
1384
  const prior = replicas.get(room.locator);
677
- const retained = readConfirmed({
678
- ...confirmed.record,
679
- model: modelOf(room.locator),
680
- selectionModel: modelOf(room.locator, 'typed-selection'),
681
- }, prior?.confirmed);
682
- if (retained.kind !== 'staged')
683
- throw new Error(`Cannot retype staged Room ${room.locator}`);
684
- const queue = prior?.queue ?? [];
685
- const roomVersion = retained.confirmed.record.roomVersion;
1385
+ if (base !== undefined &&
1386
+ (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
1387
+ prior.confirmed.record.revisions !== base.record.revisions)) {
1388
+ return false;
1389
+ }
1390
+ adoptModels(room.locator, confirmed.record);
1391
+ const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
1392
+ const roomVersion = confirmed.record.roomVersion;
686
1393
  const accepted = queue.filter((batch) => {
687
1394
  const accepted = pendings.get(batch.id)?.accepted;
688
1395
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
689
1396
  });
690
- const set = await settle(local, room.locator, prior, retained.confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
691
- const target = wanted.get(room.locator);
692
- if (target !== undefined && options.adapter.reached(room.locator, roomVersion, target)) {
693
- wanted.delete(room.locator);
1397
+ let settled;
1398
+ try {
1399
+ settled = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
1400
+ }
1401
+ catch (error) {
1402
+ if (!exceeded(error))
1403
+ throw error;
1404
+ await exceededOn(room.locator, grownBy(confirmed.record, prior?.confirmed.record));
1405
+ caughtUpTo(room.locator);
1406
+ return true;
694
1407
  }
695
- unreadable.delete(room.locator);
1408
+ const set = settled();
1409
+ if (stored !== undefined) {
1410
+ transfers.delete(room.locator);
1411
+ catching.delete(room.locator);
1412
+ }
1413
+ for (const batch of accepted)
1414
+ pendings.delete(batch.id);
1415
+ stale.delete(room.locator);
1416
+ unindexable.delete(room.locator);
1417
+ if (unreadable.delete(room.locator))
1418
+ scopedRefusals.delete(room.locator);
696
1419
  sourceRefusals.delete(room.locator);
697
1420
  declarationRefusals.delete(room.locator);
698
1421
  list(room, true);
1422
+ if (prior === undefined)
1423
+ prepared.add(room.locator);
699
1424
  publishChangeSet(set);
700
1425
  moveTo({ ...status, ...attention() });
1426
+ options.adapter.roomStream?.published(room.locator, roomVersion);
1427
+ return true;
701
1428
  });
702
1429
  }
703
1430
  async function saveRoomDrafts(locator, named, writtenBy) {
704
1431
  const local = await openLocal();
1432
+ await referenced(locator);
705
1433
  const replica = replicas.get(locator);
706
1434
  const room = rooms.get(locator);
707
1435
  if (replica === undefined || room === undefined) {
708
- throw new Error(`No Local Replica for ${locator}`);
1436
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
709
1437
  }
710
1438
  const saved = [];
711
1439
  for (const { path, revision } of named) {
712
1440
  const record = roomDrafts.get(locator)?.get(path);
713
1441
  if (record === undefined || record.revision !== revision) {
714
- return { outcome: 'refused', rule: 'stale', file: path };
1442
+ return { outcome: 'refused', code: 'stale', retryable: false, file: path };
715
1443
  }
716
1444
  if (!saved.some((draft) => draft.path === path))
717
1445
  saved.push({ path, parts: record.parts });
718
1446
  }
719
1447
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
720
1448
  if (changed.length > 0)
721
- return { outcome: 'refused', rule: 'changed-underneath', parts: changed };
1449
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
722
1450
  const parts = saved.map(({ path, parts }) => changesOf(path, parts));
723
- const built = buildBatch({
1451
+ const changes = parts.flatMap(({ changes }) => changes);
1452
+ const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
1453
+ if (requeue.kind === 'refused')
1454
+ return { outcome: 'refused', ...requeue.refusal };
1455
+ const foreign = [...requeue.withdrawn.keys()].some((origin) => {
1456
+ const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
1457
+ return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
1458
+ });
1459
+ if (foreign) {
1460
+ return {
1461
+ outcome: 'refused',
1462
+ code: 'unknown-revision',
1463
+ retryable: false,
1464
+ file: [...requeue.queued].at(-1),
1465
+ };
1466
+ }
1467
+ const withdrawing = requeue.withdrawn.size > 0;
1468
+ const input = {
1469
+ local: localSaves,
724
1470
  baseRoomVersion: replica.confirmed.record.roomVersion,
725
1471
  composition: compositionFor(replica.confirmed.record),
726
1472
  operations: room.operations,
727
1473
  providerOwnedPaths: room.providerOwnedPaths,
728
1474
  providerLimits: limits,
729
- rewriting: rewriting(),
1475
+ handle: room.handle,
730
1476
  visible: replica.visible,
731
- changes: parts.flatMap(({ changes }) => changes),
1477
+ changes,
732
1478
  bases: parts.flatMap(({ bases }) => bases),
733
1479
  writtenBy,
1480
+ clean: {
1481
+ text: replica.confirmed.record.documents,
1482
+ binaries: replica.confirmed.record.binaries,
1483
+ composition: compositionOf(replica.confirmed.record),
1484
+ },
1485
+ };
1486
+ const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
1487
+ if (checked?.kind === 'refused')
1488
+ return { outcome: 'refused', ...checked.refusal };
1489
+ const owners = [...requeue.owners].flatMap((origin) => {
1490
+ const batch = requeue.withdrawn.get(origin);
1491
+ return batch === undefined || batch === null ? [] : [{ origin, batch }];
734
1492
  });
735
- if (built.kind === 'refused')
1493
+ if (owners.length > 1) {
1494
+ return {
1495
+ outcome: 'refused',
1496
+ code: 'unknown-revision',
1497
+ retryable: false,
1498
+ file: [...requeue.queued].at(-1),
1499
+ };
1500
+ }
1501
+ const built = !withdrawing
1502
+ ? buildBatch(input)
1503
+ : requeue.changes.length === 0
1504
+ ? null
1505
+ : buildBatch({
1506
+ ...input,
1507
+ visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
1508
+ changes: requeue.changes,
1509
+ });
1510
+ if (built?.kind === 'refused')
736
1511
  return { outcome: 'refused', ...built.refusal };
737
- const batches = [...replica.queue, built.batch];
1512
+ const [owner] = owners;
1513
+ const join = built === null || owner === undefined
1514
+ ? null
1515
+ : joined(requeue.queue, owner.batch, built.batch);
1516
+ if (join?.kind === 'refused')
1517
+ return { outcome: 'refused', ...join.refusal };
1518
+ const withdrawn = new Map(requeue.withdrawn);
1519
+ if (join !== null && owner !== undefined)
1520
+ withdrawn.set(owner.origin, join.batch);
1521
+ const drafted = new Map();
1522
+ for (const [origin, replaced] of withdrawn) {
1523
+ if (replaced === null)
1524
+ continue;
1525
+ const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
1526
+ drafted.set(replaced.id, replaced === join?.batch
1527
+ ? [
1528
+ ...carried.map((held) => joinedDraft(held, saved)),
1529
+ ...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
1530
+ ]
1531
+ : carried);
1532
+ }
1533
+ if (built !== null && join === null)
1534
+ drafted.set(built.batch.id, saved);
1535
+ const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
1536
+ for (const replaced of withdrawn.values()) {
1537
+ if (replaced === null || !batches.includes(replaced))
1538
+ continue;
1539
+ const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
1540
+ if (limited !== null)
1541
+ return { outcome: 'refused', ...limited };
1542
+ }
738
1543
  const consumed = saved.map(({ path }) => path);
739
1544
  try {
740
- await local.write({
741
- next: replica.confirmed.record,
742
- previous: replica.confirmed.record,
1545
+ await local.keepAuthored({
1546
+ locator,
743
1547
  queue: batches,
744
- previousQueue: replica.queue,
745
- saved,
1548
+ saved: drafted,
746
1549
  consumedRoomDrafts: consumed,
747
1550
  });
748
1551
  }
749
1552
  catch {
750
- return { outcome: 'refused', rule: 'durable-write' };
1553
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
1554
+ }
1555
+ if (fences.has(locator))
1556
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1557
+ for (const origin of withdrawn.keys())
1558
+ pendings.delete(origin);
1559
+ for (const [id, drafts] of drafted) {
1560
+ pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
751
1561
  }
752
- pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
1562
+ if (!localSaves)
1563
+ recorded = false;
753
1564
  for (const path of consumed)
754
1565
  forgetRoomDraft(locator, path);
755
- replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting(), replica));
1566
+ replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
756
1567
  publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
757
1568
  queue: true,
758
1569
  drafts: consumed,
759
1570
  }));
760
- moveTo({ ...saving(status), ...attention() });
761
- void enqueue();
762
- return { outcome: 'saved', identifier: built.batch.id };
1571
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
1572
+ submitLater(locator);
1573
+ const identifier = join?.batch.id ??
1574
+ built?.batch.id ??
1575
+ [...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
1576
+ return { outcome: 'saved', identifier };
763
1577
  }
764
1578
  async function mark(local, locator, identifier, submission, lost) {
765
1579
  const state = pendings.get(identifier);
@@ -769,6 +1583,8 @@ export function composeRuntime(options) {
769
1583
  return;
770
1584
  }
771
1585
  await local.submit(locator, identifier, submission, lost);
1586
+ if (fences.has(locator))
1587
+ return;
772
1588
  pendings.set(identifier, { ...state, submission, lost });
773
1589
  const replica = replicas.get(locator);
774
1590
  if (replica !== undefined)
@@ -779,22 +1595,24 @@ export function composeRuntime(options) {
779
1595
  if (!continuing())
780
1596
  throw new Error('connection refused');
781
1597
  await local.keepUpload(locator, identifier, upload);
1598
+ if (fences.has(locator))
1599
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
782
1600
  pendings.set(identifier, { ...pendings.get(identifier), upload });
783
1601
  if (!continuing())
784
1602
  throw new Error('connection refused');
785
1603
  }
786
1604
  async function readHistory(locator, path, query = {}) {
787
1605
  if (closed || refusal !== null)
788
- return { kind: 'transport-uncertainty' };
1606
+ return UNCERTAIN;
789
1607
  try {
790
1608
  await ensureAuthorization();
791
1609
  }
792
1610
  catch {
793
- return { kind: 'transport-uncertainty' };
794
- }
795
- if (closed || authorization === undefined || refusal !== null) {
796
- return { kind: 'transport-uncertainty' };
1611
+ return UNCERTAIN;
797
1612
  }
1613
+ if (closed || authorization === undefined || refusal !== null)
1614
+ return UNCERTAIN;
1615
+ const since = fencings;
798
1616
  const transfer = ['transfer', 'history', locator, path];
799
1617
  if (query.revision !== undefined)
800
1618
  transfer.push(query.revision);
@@ -809,6 +1627,8 @@ export function composeRuntime(options) {
809
1627
  ...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
810
1628
  });
811
1629
  journal.record(`outcome history ${result.kind}`);
1630
+ if (fencedSince(locator, since))
1631
+ return failure({ kind: 'room-not-readable', locator });
812
1632
  if (result.kind === 'scoped-refusal') {
813
1633
  const room = replicas.get(result.locator)?.confirmed.record;
814
1634
  if (room !== undefined)
@@ -816,17 +1636,111 @@ export function composeRuntime(options) {
816
1636
  }
817
1637
  if (result.kind === 'revoked-authorization')
818
1638
  await refuse('revoked-authorization');
819
- return result;
1639
+ return historyAnswer(result);
820
1640
  }
821
1641
  catch {
822
1642
  journal.record('outcome history transport-uncertainty');
823
- return { kind: 'transport-uncertainty' };
1643
+ return UNCERTAIN;
1644
+ }
1645
+ }
1646
+ function queuedBlob(locator, path, localId) {
1647
+ for (const batch of replicas.get(locator)?.queue ?? []) {
1648
+ const change = batch.changes[queuedFile(batch, path, localId)];
1649
+ if (change?.kind === 'binary')
1650
+ return change.blob;
1651
+ }
1652
+ return null;
1653
+ }
1654
+ function binaryRoom(request) {
1655
+ if (request.kind === 'path')
1656
+ return request.locator;
1657
+ const scope = options.adapter.referrerScope(request.referrer);
1658
+ return scope?.kind === 'room' ? scope.locator : null;
1659
+ }
1660
+ async function readBinary(request) {
1661
+ if (closed || refusal !== null)
1662
+ return UNCERTAIN;
1663
+ try {
1664
+ await ensureAuthorization();
1665
+ }
1666
+ catch {
1667
+ return UNCERTAIN;
1668
+ }
1669
+ if (closed || authorization === undefined || refusal !== null)
1670
+ return UNCERTAIN;
1671
+ const since = fencings;
1672
+ journal.record(request.kind === 'path'
1673
+ ? `transfer binary ${request.locator} ${request.path}`
1674
+ : `transfer binary ${request.fileRef}`);
1675
+ try {
1676
+ const result = await options.adapter.binary(authorization, request);
1677
+ journal.record(`outcome binary ${result.kind}`);
1678
+ const locator = binaryRoom(request);
1679
+ if (locator !== null && fencedSince(locator, since))
1680
+ return failure({ kind: 'room-not-readable', locator });
1681
+ if (result.kind === 'scoped-refusal') {
1682
+ const room = replicas.get(result.locator)?.confirmed.record;
1683
+ if (room !== undefined)
1684
+ await scoped(result, room, await openLocal());
1685
+ }
1686
+ if (result.kind === 'revoked-authorization')
1687
+ await refuse('revoked-authorization');
1688
+ return binaryAnswer(result);
1689
+ }
1690
+ catch {
1691
+ journal.record('outcome binary transport-uncertainty');
1692
+ return UNCERTAIN;
824
1693
  }
825
1694
  }
826
- function declaredCards() {
1695
+ async function readBinaryPath(locator, path, revision) {
1696
+ await ready;
1697
+ if (revision !== undefined)
1698
+ return readBinary({ kind: 'path', locator, path, revision });
1699
+ const visible = replicas.get(locator)?.visible;
1700
+ const held = visible?.binaries[path];
1701
+ if (visible === undefined || held === undefined)
1702
+ return absentBinary(path);
1703
+ if (held.state === 'pending') {
1704
+ const blob = queuedBlob(locator, path, held.localId);
1705
+ return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1706
+ }
1707
+ const record = binaryRecord(path, held, visible.revisions[path]);
1708
+ if (record.state !== 'accepted')
1709
+ return absentBinary(path);
1710
+ const replica = replicas.get(locator);
1711
+ const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
1712
+ const holds = (at) => {
1713
+ const kept = replica?.confirmed.record.binaries[at];
1714
+ return (kept?.state === 'accepted' &&
1715
+ kept.fileRef === record.fileRef &&
1716
+ replica?.confirmed.record.revisions[at] === record.revision);
1717
+ };
1718
+ let confirmed = path;
1719
+ for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
1720
+ const change = queued[index];
1721
+ if (change.kind === 'move' && change.to === confirmed)
1722
+ confirmed = change.path;
1723
+ }
1724
+ return readBinary({
1725
+ kind: 'path',
1726
+ locator,
1727
+ path: confirmed,
1728
+ revision: record.revision,
1729
+ fileRef: record.fileRef,
1730
+ });
1731
+ }
1732
+ async function readBinaryReference(reference) {
1733
+ await ready;
1734
+ return readBinary({
1735
+ kind: 'reference',
1736
+ fileRef: reference.fileRef,
1737
+ referrer: reference.referrer,
1738
+ });
1739
+ }
1740
+ function declaredAgents() {
827
1741
  for (const capability of capabilities) {
828
- if (capability.conversationCards !== undefined)
829
- return capability.conversationCards;
1742
+ if (capability.conversationAgents !== undefined)
1743
+ return capability.conversationAgents;
830
1744
  }
831
1745
  return undefined;
832
1746
  }
@@ -842,23 +1756,20 @@ export function composeRuntime(options) {
842
1756
  async function invokeAction(name, input) {
843
1757
  await ready;
844
1758
  if (closed || refusal !== null)
845
- return { kind: 'transport-uncertainty' };
1759
+ return UNCERTAIN;
846
1760
  if (declaredAction(name) === undefined)
847
- return { kind: 'undeclared' };
1761
+ return undeclared(name);
848
1762
  try {
849
1763
  await ensureAuthorization();
850
1764
  }
851
1765
  catch {
852
- return { kind: 'transport-uncertainty' };
853
- }
854
- if (closed || authorization === undefined || refusal !== null) {
855
- return { kind: 'transport-uncertainty' };
1766
+ return UNCERTAIN;
856
1767
  }
1768
+ if (closed || authorization === undefined || refusal !== null)
1769
+ return UNCERTAIN;
857
1770
  journal.record(`transfer action ${name}`);
858
1771
  try {
859
1772
  const result = await options.adapter.action(authorization, { name, input });
860
- if (result.kind === 'action' && result.at !== undefined)
861
- want(result.at);
862
1773
  if (result.kind === 'scoped-refusal') {
863
1774
  const room = replicas.get(result.locator)?.confirmed.record;
864
1775
  if (room !== undefined)
@@ -869,11 +1780,11 @@ export function composeRuntime(options) {
869
1780
  journal.record(`outcome action ${result.kind}`);
870
1781
  if (result.kind === 'rejected')
871
1782
  journal.record(`refusal rejected ${result.code}`);
872
- return result;
1783
+ return calledResult(result);
873
1784
  }
874
1785
  catch {
875
1786
  journal.record('outcome action transport-uncertainty');
876
- return { kind: 'transport-uncertainty' };
1787
+ return UNCERTAIN;
877
1788
  }
878
1789
  }
879
1790
  function continuing() {
@@ -890,32 +1801,11 @@ export function composeRuntime(options) {
890
1801
  options.adapter.reached(locator, written, roomVersion) &&
891
1802
  !options.adapter.reached(locator, roomVersion, written));
892
1803
  }
893
- function want({ locator, roomVersion }) {
894
- if (reachedBy(locator, roomVersion))
895
- return;
896
- const held = wanted.get(locator);
897
- if (held === undefined || options.adapter.reached(locator, roomVersion, held)) {
898
- wanted.set(locator, roomVersion);
899
- }
900
- }
901
- function reachOf(locator) {
902
- let target = wanted.get(locator);
903
- const versions = (replicas.get(locator)?.queue ?? []).flatMap((batch) => {
904
- const accepted = pendings.get(batch.id)?.accepted;
905
- return accepted === undefined ? [] : [accepted];
906
- });
907
- for (const version of versions) {
908
- if (target === undefined || options.adapter.reached(locator, version, target)) {
909
- target = version;
910
- }
911
- }
912
- return target === undefined || reachedBy(locator, target) ? undefined : target;
913
- }
914
- async function pullRefresh(authorized, confirmed, minPosition) {
915
- if (!continuing())
1804
+ async function pullRefresh(authorized, confirmed, live) {
1805
+ if (!live())
916
1806
  return unconfirmed;
917
1807
  try {
918
- const result = await refreshRoom(options.adapter, authorized, confirmed, continuing, minPosition);
1808
+ const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
919
1809
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
920
1810
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
921
1811
  return result;
@@ -925,55 +1815,132 @@ export function composeRuntime(options) {
925
1815
  throw error;
926
1816
  }
927
1817
  }
928
- async function pullBootstrap(authorized, room) {
929
- if (!continuing())
930
- return unconfirmed;
931
- try {
932
- const result = await bootstrapRoom(options.adapter, authorized, room, {
933
- collection: modelOf(room.locator),
1818
+ function staging(local, live) {
1819
+ return {
1820
+ held: (locator) => transfers.get(locator) ?? null,
1821
+ staged: (record) => local.staged(record),
1822
+ stage: (record, page) => exclusive(async () => {
1823
+ if (!live())
1824
+ return false;
1825
+ const bytes = layerBytes(page);
1826
+ if (!(await admitted(local, record.locator, bytes)))
1827
+ return false;
1828
+ try {
1829
+ await local.stage(record, page);
1830
+ }
1831
+ catch (error) {
1832
+ if (!exceeded(error))
1833
+ throw error;
1834
+ await exceededOn(record.locator, bytes);
1835
+ return false;
1836
+ }
1837
+ transfers.set(record.locator, record);
1838
+ readSide.recover();
1839
+ return true;
1840
+ }),
1841
+ abandon: (locator) => exclusive(async () => {
1842
+ if (!live())
1843
+ return;
1844
+ await local.abandon(locator);
1845
+ transfers.delete(locator);
1846
+ readSide.recover();
1847
+ }),
1848
+ };
1849
+ }
1850
+ function reusable(room) {
1851
+ const record = replicas.get(room.locator)?.confirmed.record;
1852
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1853
+ return null;
1854
+ return { documents: record.documents, revisions: record.revisions };
1855
+ }
1856
+ async function pullBootstrap(authorized, room, local, live) {
1857
+ if (!live())
1858
+ return unconfirmed;
1859
+ if (replicas.has(room.locator)) {
1860
+ catching.add(room.locator);
1861
+ readSide.recover();
1862
+ }
1863
+ try {
1864
+ const result = await bootstrapRoom(options.adapter, authorized, room, {
1865
+ collection: modelOf(room.locator),
934
1866
  selection: modelOf(room.locator, 'typed-selection'),
935
- }, continuing);
1867
+ }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
936
1868
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
937
1869
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1870
+ if (result.kind !== 'staged')
1871
+ caughtUpTo(room.locator);
938
1872
  return result;
939
1873
  }
940
1874
  catch (error) {
941
1875
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
1876
+ caughtUpTo(room.locator);
942
1877
  throw error;
943
1878
  }
944
1879
  }
945
- async function advance(room, local, authorized) {
1880
+ function caughtUpTo(locator) {
1881
+ if (catching.delete(locator))
1882
+ readSide.recover();
1883
+ }
1884
+ async function advance(room, local, authorized, live) {
1885
+ await referenced(room.locator);
1886
+ if (!live() || paused.has(room.locator))
1887
+ return unconfirmed;
946
1888
  const held = replicas.get(room.locator);
947
1889
  if (held !== undefined) {
948
- const refreshed = await pullRefresh(authorized, held.confirmed, reachOf(room.locator));
949
- if (!continuing())
1890
+ const refreshed = await pullRefresh(authorized, held.confirmed, live);
1891
+ if (!live())
950
1892
  return unconfirmed;
951
1893
  if (refreshed.kind === 'still-current') {
1894
+ stale.delete(room.locator);
952
1895
  if (sourceRefusals.has(room.locator))
953
- return unconfirmed;
1896
+ return null;
954
1897
  if (unreadable.delete(room.locator)) {
1898
+ scopedRefusals.delete(room.locator);
955
1899
  list(room, true);
956
1900
  emitRooms();
957
1901
  }
958
1902
  return null;
959
1903
  }
960
1904
  if (refreshed.kind === 'advanced') {
961
- await publish(local, room, refreshed.confirmed);
1905
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
1906
+ const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
1907
+ const reached = now !== undefined &&
1908
+ options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
1909
+ return reached ? null : advance(room, local, authorized, live);
1910
+ }
1911
+ await retainModel(local, room, 'collection', modelOf(room.locator));
962
1912
  return null;
963
1913
  }
964
1914
  if (refreshed.kind !== 'beyond-horizon')
965
1915
  return refreshed;
1916
+ if (opened !== null) {
1917
+ owed.add(room.locator);
1918
+ readSide.recover();
1919
+ return unconfirmed;
1920
+ }
966
1921
  }
967
- const bootstrapped = await pullBootstrap(authorized, room);
968
- if (!continuing())
1922
+ const bootstrapped = await pullBootstrap(authorized, room, local, live);
1923
+ if (!live())
969
1924
  return unconfirmed;
970
1925
  if (bootstrapped.kind !== 'staged')
971
1926
  return bootstrapped;
972
- await publish(local, room, bootstrapped.confirmed);
1927
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
973
1928
  return null;
974
1929
  }
975
1930
  async function withhold(withheld, room, local, progress) {
976
- progress.complete = false;
1931
+ if (withheld.kind === 'unconfirmed')
1932
+ return;
1933
+ if (withheld.kind === 'refused')
1934
+ stale.delete(room.locator);
1935
+ else
1936
+ stale.add(room.locator);
1937
+ if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
1938
+ const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
1939
+ repairable.set(room.locator, reset);
1940
+ repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
1941
+ }
1942
+ else
1943
+ repairable.delete(room.locator);
977
1944
  if (withheld.kind === 'scoped-refusal') {
978
1945
  await scoped(withheld, room, local);
979
1946
  }
@@ -998,6 +1965,7 @@ export function composeRuntime(options) {
998
1965
  function refusedSource(room, { refusal }) {
999
1966
  sourceRefusals.set(room.locator, {
1000
1967
  code: refusal.rule,
1968
+ retryable: false,
1001
1969
  fields: {
1002
1970
  file: refusal.file,
1003
1971
  ...(refusal.location === undefined ? {} : { location: refusal.location }),
@@ -1008,91 +1976,77 @@ export function composeRuntime(options) {
1008
1976
  unreadable.add(room.locator);
1009
1977
  list(room, false);
1010
1978
  emitRooms();
1979
+ moveTo({ ...status, ...attention() });
1011
1980
  journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
1012
1981
  }
1013
1982
  function leaving(batch, result) {
1014
1983
  if (result.kind === 'expired')
1015
- return { batch, returned: { kind: 'expired' } };
1016
- return { batch, returned: { kind: result.kind, code: result.code, fields: result.fields } };
1984
+ return { batch, returned: returnedBy('expired') };
1985
+ return {
1986
+ batch,
1987
+ returned: {
1988
+ kind: result.kind,
1989
+ code: result.code,
1990
+ fields: result.fields,
1991
+ retryable: result.retryable,
1992
+ },
1993
+ };
1017
1994
  }
1018
1995
  function logAccepted(locator, submitted, accepted) {
1019
1996
  journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
1020
1997
  (accepted.author === undefined ? '' : ` author ${accepted.author}`) +
1021
1998
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1022
1999
  }
1023
- async function recordAccepted(room, local, submitted, accepted) {
1024
- await exclusive(async () => {
1025
- if (!continuing())
1026
- return;
1027
- const current = replicas.get(room.locator);
1028
- if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1029
- return;
1030
- }
1031
- const state = pendings.get(submitted.id);
1032
- if (state === undefined)
1033
- return;
1034
- await local.accept(room.locator, submitted.id, accepted.roomVersion);
1035
- pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1036
- logAccepted(room.locator, submitted, accepted);
1037
- emitQueue(room.locator);
1038
- });
1039
- }
1040
- async function commitAccept(room, local, submitted, accepted, base, overlay) {
2000
+ async function adoptAccepted(room, local, submitted, accepted) {
2001
+ await referenced(room.locator);
1041
2002
  if (!continuing())
1042
2003
  return unconfirmed;
1043
2004
  const current = replicas.get(room.locator);
1044
2005
  if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1045
2006
  return null;
1046
2007
  }
1047
- const remaining = current.queue.filter((batch) => batch.id !== submitted.id);
1048
- const retainedBase = readConfirmed({
1049
- ...base.record,
1050
- model: modelOf(room.locator),
1051
- selectionModel: modelOf(room.locator, 'typed-selection'),
1052
- }, base);
1053
- const applied = overlay && retainedBase.kind === 'staged'
1054
- ? applyReturned(retainedBase.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion)
1055
- : retainedBase;
1056
- if (applied.kind !== 'staged')
2008
+ const held = current.confirmed.record.roomVersion;
2009
+ logAccepted(room.locator, submitted, accepted);
2010
+ const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
2011
+ ? { kind: 'staged', confirmed: current.confirmed }
2012
+ : applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
2013
+ ? accepted.roomVersion
2014
+ : held, accepted.roomVersion);
2015
+ if (applied.kind !== 'staged') {
2016
+ const state = pendings.get(submitted.id);
2017
+ await local.accept(room.locator, submitted.id, accepted.roomVersion);
2018
+ if (state !== undefined && !fences.has(room.locator))
2019
+ pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
2020
+ emitQueue(room.locator);
1057
2021
  return applied;
1058
- const set = await settle(local, room.locator, current, applied.confirmed, remaining, {
1059
- room: true,
1060
- });
2022
+ }
2023
+ const settled = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
2024
+ pendings.delete(submitted.id);
1061
2025
  unreadable.delete(room.locator);
1062
2026
  sourceRefusals.delete(room.locator);
1063
2027
  declarationRefusals.delete(room.locator);
2028
+ const set = settled();
1064
2029
  list(room, true);
1065
2030
  publishChangeSet(set);
1066
2031
  moveTo({ ...status, ...attention() });
1067
2032
  return null;
1068
2033
  }
1069
- async function accepting(room, local, submitted, accepted, pulled, fresh) {
1070
- const roomVersion = pulled.record.roomVersion;
1071
- if (options.adapter.reached(room.locator, roomVersion, accepted.roomVersion)) {
1072
- return committed(room, local, submitted, accepted, pulled, roomVersion === accepted.roomVersion);
1073
- }
1074
- if (fresh && continuing())
1075
- await publish(local, room, pulled);
1076
- return false;
1077
- }
1078
- async function committed(room, local, submitted, accepted, base, overlay) {
1079
- const failed = await exclusive(() => commitAccept(room, local, submitted, accepted, base, overlay));
2034
+ async function adopted(room, local, submitted, accepted) {
2035
+ const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
1080
2036
  if (failed === null)
1081
2037
  return true;
1082
2038
  if (failed.kind === 'refused' && continuing())
1083
2039
  refusedSource(room, failed);
1084
2040
  return false;
1085
2041
  }
1086
- async function submitRoom(room, local, authorized, progress) {
1087
- while (continuing() && authorization !== undefined) {
2042
+ async function submitRoom(room, local, authorized, progress, signal) {
2043
+ while (continuing() && authorization !== undefined && !signal.aborted) {
1088
2044
  const replica = replicas.get(room.locator);
1089
2045
  const head = replica?.queue[0];
1090
2046
  if (replica === undefined || head === undefined)
1091
- return;
1092
- if (pendings.get(head.id)?.accepted !== undefined) {
1093
- progress.complete = false;
1094
- return;
1095
- }
2047
+ return 'done';
2048
+ if (pendings.get(head.id)?.accepted !== undefined)
2049
+ return 'done';
1096
2050
  const state = pendings.get(head.id);
1097
2051
  const { submission, lost: earlier } = state ?? {
1098
2052
  submission: { state: 'waiting' },
@@ -1100,26 +2054,33 @@ export function composeRuntime(options) {
1100
2054
  };
1101
2055
  if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
1102
2056
  progress.complete = false;
1103
- return;
2057
+ return { at: Date.parse(submission.reset) };
1104
2058
  }
1105
2059
  if (writesHeld()) {
1106
2060
  progress.complete = false;
1107
2061
  journal.record(`submission ${room.locator} ${head.id} held`);
1108
- return;
2062
+ return 'done';
1109
2063
  }
1110
2064
  const lost = earlier || submission.state === 'submitted';
2065
+ if (lost && pastHorizon(head.id)) {
2066
+ await discardPending(room, local, signal);
2067
+ return 'done';
2068
+ }
1111
2069
  let result;
1112
2070
  journal.record(`submission ${room.locator} ${head.id}`);
1113
2071
  await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1114
- if (discoveryPaused)
1115
- return;
1116
- if (writesHeld()) {
2072
+ if (discoveryPaused || fences.has(room.locator))
2073
+ return 'done';
2074
+ if (signal.aborted || writesHeld()) {
1117
2075
  await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
2076
+ if (signal.aborted)
2077
+ return 'done';
1118
2078
  progress.complete = false;
1119
2079
  journal.record(`submission ${room.locator} ${head.id} held`);
1120
- return;
2080
+ return 'done';
1121
2081
  }
1122
2082
  const opensBefore = openSignals;
2083
+ const answered = awaitingAnswer(room.locator);
1123
2084
  try {
1124
2085
  result = await options.adapter.mutate(authorized, {
1125
2086
  locator: room.locator,
@@ -1128,33 +2089,40 @@ export function composeRuntime(options) {
1128
2089
  }, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
1129
2090
  }
1130
2091
  catch {
2092
+ answered();
1131
2093
  progress.complete = false;
1132
2094
  if (continuing())
1133
2095
  progress.reachable = false;
1134
- return;
2096
+ return 'again';
1135
2097
  }
2098
+ if (signal.aborted) {
2099
+ answered();
2100
+ return 'done';
2101
+ }
2102
+ if (result.kind !== 'accepted' || !continuing())
2103
+ answered();
1136
2104
  if (!continuing())
1137
- return;
2105
+ return 'done';
1138
2106
  if (result.kind === 'transport-uncertainty') {
1139
2107
  progress.complete = false;
1140
2108
  progress.reachable = false;
1141
- return;
2109
+ return 'again';
1142
2110
  }
1143
2111
  if (result.kind === 'room-not-readable') {
1144
2112
  progress.complete = false;
1145
2113
  unreadable.add(room.locator);
1146
2114
  list(room, false);
1147
2115
  emitRooms();
1148
- return;
2116
+ return 'done';
1149
2117
  }
1150
2118
  if (result.kind === 'revoked-authorization') {
1151
2119
  await refuse('revoked-authorization');
1152
- return;
2120
+ return 'done';
1153
2121
  }
1154
2122
  if (result.kind === 'scoped-refusal') {
1155
2123
  await scoped(result, room, local);
1156
2124
  progress.complete = false;
1157
- return;
2125
+ return 'done';
1158
2126
  }
1159
2127
  if (result.kind === 'write-halted') {
1160
2128
  journal.record(`outcome ${room.locator} ${head.id} write-halted`);
@@ -1163,16 +2131,24 @@ export function composeRuntime(options) {
1163
2131
  continue;
1164
2132
  haltedRefusal = true;
1165
2133
  progress.complete = false;
1166
- return;
2134
+ return 'done';
1167
2135
  }
1168
2136
  if (result.kind === 'conflicted' ||
1169
2137
  result.kind === 'rejected' ||
1170
2138
  result.kind === 'expired' ||
1171
2139
  result.kind === 'retryable-refusal') {
2140
+ if (result.kind === 'expired' && lost && result.restored !== true) {
2141
+ journal.record(`outcome ${room.locator} ${head.id} expired`);
2142
+ await discardPending(room, local, signal);
2143
+ return 'done';
2144
+ }
1172
2145
  if (result.kind === 'expired') {
1173
- const withheld = await advance(room, local, authorized);
1174
- if (withheld !== null)
1175
- return withhold(withheld, room, local, progress);
2146
+ await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, false));
2147
+ const withheld = await advance(room, local, authorized, continuing);
2148
+ if (withheld !== null) {
2149
+ await withhold(withheld, room, local, progress);
2150
+ return 'done';
2151
+ }
1176
2152
  }
1177
2153
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
1178
2154
  (result.kind === 'conflicted' && result.roomVersion !== undefined
@@ -1185,7 +2161,7 @@ export function composeRuntime(options) {
1185
2161
  const reset = result.reset;
1186
2162
  progress.complete = false;
1187
2163
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1188
- return;
2164
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
1189
2165
  }
1190
2166
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
1191
2167
  const left = leaving(head, result);
@@ -1196,110 +2172,94 @@ export function composeRuntime(options) {
1196
2172
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
1197
2173
  return;
1198
2174
  }
1199
- const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1200
- leaving: result.kind === 'expired' && landed(head, current.confirmed)
2175
+ const settled = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
2176
+ leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
2177
+ (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1201
2178
  ? { ...left, returned: null }
1202
2179
  : left,
1203
2180
  });
1204
- publishChangeSet(set);
2181
+ publishChangeSet(settled());
1205
2182
  moveTo({ ...status, ...attention() });
1206
2183
  });
1207
2184
  if (result.kind === 'expired')
1208
2185
  continue;
1209
- const withheld = await advance(room, local, authorized);
1210
- if (withheld !== null)
1211
- return withhold(withheld, room, local, progress);
1212
- continue;
1213
- }
1214
- await recordAccepted(room, local, head, result);
1215
- const from = replicas.get(room.locator)?.confirmed;
1216
- if (from === undefined)
1217
- return;
1218
- const pulled = await pullRefresh(authorized, from, reachOf(room.locator));
1219
- if (!continuing())
1220
- return;
1221
- if (pulled.kind === 'revoked-authorization') {
1222
- await refuse('revoked-authorization');
1223
- return;
1224
- }
1225
- if (pulled.kind === 'scoped-refusal') {
1226
- await scoped(pulled, room, local);
1227
- progress.complete = false;
1228
- return;
1229
- }
1230
- if (pulled.kind === 'still-current' || pulled.kind === 'advanced') {
1231
- const fresh = pulled.kind === 'advanced';
1232
- if (!(await accepting(room, local, head, result, fresh ? pulled.confirmed : from, fresh))) {
1233
- progress.complete = false;
1234
- return;
2186
+ const withheld = await advance(room, local, authorized, continuing);
2187
+ if (withheld !== null) {
2188
+ await withhold(withheld, room, local, progress);
2189
+ return 'done';
1235
2190
  }
1236
2191
  continue;
1237
2192
  }
1238
- if (pulled.kind === 'beyond-horizon') {
1239
- const bootstrapped = await pullBootstrap(authorized, room);
1240
- if (!continuing())
1241
- return;
1242
- if (bootstrapped.kind === 'revoked-authorization') {
1243
- await refuse('revoked-authorization');
1244
- return;
1245
- }
1246
- if (bootstrapped.kind === 'scoped-refusal') {
1247
- await scoped(bootstrapped, room, local);
1248
- progress.complete = false;
1249
- return;
1250
- }
1251
- if (bootstrapped.kind === 'staged') {
1252
- if (!(await accepting(room, local, head, result, bootstrapped.confirmed, true))) {
1253
- progress.complete = false;
1254
- return;
1255
- }
1256
- continue;
1257
- }
2193
+ const kept = await adopted(room, local, head, result).finally(answered);
2194
+ if (!kept) {
1258
2195
  progress.complete = false;
1259
- if (bootstrapped.kind === 'transport-uncertainty') {
1260
- progress.reachable = false;
1261
- }
1262
- else if (bootstrapped.kind === 'room-not-readable') {
1263
- unreadable.add(room.locator);
1264
- list(room, false);
1265
- emitRooms();
1266
- }
1267
- else if (bootstrapped.kind === 'refused') {
1268
- refusedSource(room, bootstrapped);
1269
- }
1270
- return;
1271
- }
1272
- progress.complete = false;
1273
- if (pulled.kind === 'transport-uncertainty') {
1274
- progress.reachable = false;
1275
- }
1276
- else if (pulled.kind === 'room-not-readable') {
1277
- unreadable.add(room.locator);
1278
- list(room, false);
1279
- emitRooms();
2196
+ return 'done';
1280
2197
  }
1281
- else if (pulled.kind === 'retryable-refusal') {
1282
- journal.record(`refusal ${pulled.kind} ${pulled.code}`);
2198
+ }
2199
+ return 'done';
2200
+ }
2201
+ function pastHorizon(identity) {
2202
+ return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
2203
+ }
2204
+ async function discardPending(room, local, signal) {
2205
+ const { locator } = room;
2206
+ const discarded = await exclusive(async () => {
2207
+ if (fences.has(locator))
2208
+ return false;
2209
+ const before = replicas.get(locator);
2210
+ const queue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
2211
+ fences.set(locator, { at: discoveries, erased: false });
2212
+ lastFenced.set(locator, ++fencings);
2213
+ try {
2214
+ await local.discard(locator);
1283
2215
  }
1284
- else if (pulled.kind === 'refused') {
1285
- refusedSource(room, pulled);
2216
+ finally {
2217
+ fences.delete(locator);
1286
2218
  }
2219
+ for (const batch of queue)
2220
+ pendings.delete(batch.id);
2221
+ queuesWithoutReplica.delete(locator);
2222
+ const after = before === undefined ? null : replicaOf(before.confirmed, [], before);
2223
+ if (after !== null)
2224
+ replicas.set(locator, after);
2225
+ journal.record(`room discarded ${locator}`);
2226
+ publishChangeSet(changeSet(locator, before ?? null, after, { queue: true }));
2227
+ moveTo({ ...status, ...attention() });
2228
+ return true;
2229
+ });
2230
+ if (!discarded)
1287
2231
  return;
1288
- }
1289
- return;
2232
+ const progress = { complete: true, reachable: true };
2233
+ await roomTask(locator, async (live) => {
2234
+ await Promise.race([
2235
+ reload(room, local, progress, () => live() && !signal.aborted),
2236
+ abandonedOn(signal),
2237
+ ]);
2238
+ });
2239
+ if (signal.aborted)
2240
+ return;
2241
+ await rest(progress);
2242
+ }
2243
+ function awaitingAnswer(locator) {
2244
+ let release = () => { };
2245
+ const awaited = new Promise((resolve) => {
2246
+ release = resolve;
2247
+ });
2248
+ answering.set(locator, { generation, answered: awaited });
2249
+ return () => {
2250
+ if (answering.get(locator)?.answered === awaited)
2251
+ answering.delete(locator);
2252
+ release();
2253
+ };
1290
2254
  }
1291
2255
  async function retainModel(local, room, attached, next) {
1292
2256
  const key = modelKey(room.locator, attached);
1293
- if (next.status === 'unavailable')
1294
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
1295
- const epoch = modelEpoch.get(key) ?? 0;
1296
2257
  const previous = modelOf(room.locator, attached);
1297
2258
  const heldBefore = replicas.get(room.locator)?.confirmed.record;
1298
2259
  if (JSON.stringify(previous) === JSON.stringify(next) &&
1299
2260
  (heldBefore === undefined ||
1300
2261
  (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
1301
2262
  JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
1302
- JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
1303
2263
  JSON.stringify(heldBefore.typedSelection) ===
1304
2264
  JSON.stringify(room.typedSelection ?? null))))
1305
2265
  return;
@@ -1310,20 +2270,17 @@ export function composeRuntime(options) {
1310
2270
  await exclusive(async () => {
1311
2271
  if (!continuing())
1312
2272
  return;
1313
- if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
1314
- return;
1315
2273
  const held = replicas.get(room.locator);
1316
2274
  if (held !== undefined) {
1317
2275
  const declared = {
1318
2276
  ...held.confirmed.record,
1319
2277
  operations: room.operations,
1320
2278
  replica: room.replica,
1321
- attachedModel: room.attachedModel,
1322
2279
  typedSelection: room.typedSelection ?? null,
1323
2280
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1324
2281
  };
1325
2282
  const reclassified = sameMembership(declared, held.confirmed.record)
1326
- ? readConfirmed(declared, held.confirmed)
2283
+ ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
1327
2284
  : readReclassified(declared, held.confirmed);
1328
2285
  const record = reclassified.kind === 'staged'
1329
2286
  ? declared
@@ -1331,14 +2288,12 @@ export function composeRuntime(options) {
1331
2288
  ...declared,
1332
2289
  typedSelection: held.confirmed.record.typedSelection,
1333
2290
  };
1334
- const read = reclassified.kind === 'deferred' ? readConfirmed(record, held.confirmed) : reclassified;
1335
- await local.write({
1336
- next: record,
1337
- previous: held.confirmed.record,
1338
- queue: held.queue,
1339
- });
2291
+ const read = reclassified.kind === 'deferred'
2292
+ ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
2293
+ : reclassified;
2294
+ await local.write({ next: record, previous: held.confirmed.record });
1340
2295
  if (read.kind === 'staged') {
1341
- replicas.set(room.locator, replicaOf(read.confirmed, held.queue, rewriting(), held));
2296
+ replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1342
2297
  if (declarationRefusals.delete(room.locator)) {
1343
2298
  sourceRefusals.delete(room.locator);
1344
2299
  unreadable.delete(room.locator);
@@ -1362,79 +2317,17 @@ export function composeRuntime(options) {
1362
2317
  publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
1363
2318
  });
1364
2319
  }
1365
- async function acquireModel(local, room, authorized, selection) {
1366
- const attached = selection === null ? 'collection' : 'typed-selection';
1367
- const key = modelKey(room.locator, attached);
1368
- const declared = selection === null ? room.attachedModel : selection.attachedModel;
1369
- let previous = modelOf(room.locator, attached);
1370
- if (previous.status !== 'unavailable' &&
1371
- (declared === null || !sameModel(previous.snapshot, declared))) {
1372
- previous = { status: 'unavailable', reason: 'version-changed' };
1373
- await retainModel(local, room, attached, previous);
1374
- if (discoveryPaused)
1375
- return;
1376
- }
1377
- const epoch = modelEpoch.get(key) ?? 0;
1378
- let answer;
1379
- try {
1380
- answer = await options.adapter.model(authorized, selection === null
1381
- ? { locator: room.locator, attached: 'collection', model: declared }
1382
- : {
1383
- locator: room.locator,
1384
- attached: 'typed-selection',
1385
- roots: selection.roots,
1386
- model: declared,
1387
- });
1388
- }
1389
- catch {
1390
- answer = { kind: 'transport-uncertainty' };
1391
- }
1392
- if (discoveryPaused || epoch !== (modelEpoch.get(key) ?? 0))
1393
- return;
1394
- if (answer.kind === 'revoked-authorization') {
1395
- await refuse('revoked-authorization');
1396
- return;
1397
- }
1398
- if (answer.kind === 'transport-uncertainty') {
1399
- await retainModel(local, room, attached, cached(previous));
1400
- return;
1401
- }
1402
- if (answer.kind === 'model-unavailable') {
1403
- await retainModel(local, room, attached, {
1404
- status: 'unavailable',
1405
- reason: answer.reason,
1406
- });
1407
- return;
1408
- }
1409
- if (declared === null || !sameModel(answer.snapshot, declared)) {
1410
- await retainModel(local, room, attached, {
1411
- status: 'unavailable',
1412
- reason: 'version-changed',
1413
- });
1414
- return;
1415
- }
1416
- if (previous.status !== 'unavailable' &&
1417
- sameModel(previous.snapshot, answer.snapshot) &&
1418
- previous.snapshot.schema !== answer.snapshot.schema) {
1419
- await retainModel(local, room, attached, { status: 'unavailable', reason: 'invalid' });
2320
+ async function scoped(result, room, local) {
2321
+ if ((result.scope === 'content' || result.scope === 'sync') && !result.retryable) {
2322
+ journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
2323
+ await loseRooms([room.locator]);
1420
2324
  return;
1421
2325
  }
1422
- const verified = answer.snapshot.schema === null ? EMPTY_SCHEMA : verify(answer.snapshot.schema);
1423
- if (verified.verdict === 'refuse') {
1424
- const where = verified.location === undefined ? '' : ` ${verified.location}`;
1425
- journal.record(`model invalid ${room.locator} ${attached} ${answer.snapshot.identity} ${verified.rule}${where}`);
1426
- }
1427
- await retainModel(local, room, attached, verified.verdict === 'accept'
1428
- ? {
1429
- status: 'current',
1430
- snapshot: structuredClone(answer.snapshot),
1431
- verifiedWith: MD_MODEL,
1432
- types: verified.model.types,
1433
- }
1434
- : { status: 'unavailable', reason: 'invalid' });
1435
- }
1436
- async function scoped(result, room, local) {
1437
- scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
2326
+ scopedRefusals.set(room.locator, {
2327
+ code: result.code,
2328
+ fields: result.fields,
2329
+ retryable: result.retryable,
2330
+ });
1438
2331
  let scopedAway = room;
1439
2332
  if (result.scope === 'model')
1440
2333
  await retainModel(local, room, 'collection', {
@@ -1460,14 +2353,39 @@ export function composeRuntime(options) {
1460
2353
  emitRooms();
1461
2354
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
1462
2355
  }
1463
- async function synchronize(discovered, local, progress) {
1464
- const prepared = await Promise.allSettled(discovered.map((room) => prepareRoom(room, local, progress)));
1465
- const ready = discovered.filter((_, index) => {
2356
+ function heldRooms(listed) {
2357
+ return listed.flatMap((room) => {
2358
+ const record = replicas.get(room.locator)?.confirmed.record;
2359
+ if (record === undefined || record.replica.selection !== room.replica.selection)
2360
+ return [];
2361
+ return [
2362
+ {
2363
+ locator: room.locator,
2364
+ roomVersion: record.roomVersion,
2365
+ documents: record.documents,
2366
+ revisions: record.revisions,
2367
+ },
2368
+ ];
2369
+ });
2370
+ }
2371
+ async function synchronize(listed, local, progress, attempt, live) {
2372
+ const acquired = listed.filter(acquires);
2373
+ const prepared = await Promise.allSettled(acquired.map((room) => prepareRoom(room, local, progress)));
2374
+ if (!live())
2375
+ return;
2376
+ const ready = acquired.filter((_, index) => {
1466
2377
  const outcome = prepared[index];
1467
2378
  return outcome?.status === 'fulfilled' && outcome.value;
1468
2379
  });
1469
- const rooms = await Promise.allSettled(ready.map((room) => synchronizeRoom(room, local, progress)));
1470
- const failed = [...prepared, ...rooms].find((room) => room.status === 'rejected');
2380
+ if (attempt === 'opened') {
2381
+ options.adapter.roomStream?.hold(heldRooms(ready), heldDemand(acquired));
2382
+ handed = true;
2383
+ }
2384
+ discovering?.release();
2385
+ const loaded = await Promise.allSettled(ready
2386
+ .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
2387
+ .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
2388
+ const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1471
2389
  if (failed !== undefined)
1472
2390
  throw failed.reason;
1473
2391
  }
@@ -1484,41 +2402,69 @@ export function composeRuntime(options) {
1484
2402
  progress.complete = false;
1485
2403
  return false;
1486
2404
  }
1487
- const selection = room.typedSelection;
1488
- await Promise.all([
1489
- acquireModel(local, room, authorization, null),
1490
- ...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
1491
- ]);
2405
+ if (prior !== undefined)
2406
+ await retainModel(local, room, 'collection', modelOf(room.locator));
1492
2407
  if (discoveryPaused || authorization === undefined)
1493
2408
  return false;
1494
- if (room.replica.admission !== 'admitted') {
1495
- if (replicas.has(room.locator))
1496
- progress.complete = false;
1497
- return false;
1498
- }
1499
- const held = replicas.get(room.locator);
1500
- if (held !== undefined && held.confirmed.record.replica.selection !== room.replica.selection) {
1501
- progress.complete = false;
1502
- return false;
1503
- }
1504
- return true;
2409
+ return room.replica.admission === 'admitted';
1505
2410
  }
1506
- async function synchronizeRoom(room, local, progress) {
1507
- if (discoveryPaused || authorization === undefined)
2411
+ async function caughtUp(room, local, progress, live) {
2412
+ const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
2413
+ if (held === undefined || authorization === undefined || !live())
2414
+ return;
2415
+ const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
2416
+ if (!behind)
2417
+ return;
2418
+ const withheld = await advance(room, local, authorization, live);
2419
+ if (withheld !== null)
2420
+ await withhold(withheld, room, local, progress);
2421
+ }
2422
+ function revoked(locator) {
2423
+ return ['collection', 'typed-selection'].some((attached) => {
2424
+ const model = modelOf(locator, attached);
2425
+ return model.status === 'unavailable' && model.reason === 'revoked';
2426
+ });
2427
+ }
2428
+ async function loadRoom(room, local, progress, live) {
2429
+ if (!live() || authorization === undefined)
2430
+ return;
2431
+ if (replicas.has(room.locator) && revoked(room.locator)) {
2432
+ await reload(room, local, progress, live);
1508
2433
  return;
1509
- const withheld = await advance(room, local, authorization);
2434
+ }
2435
+ const withheld = await advance(room, local, authorization, live);
1510
2436
  if (withheld !== null) {
1511
2437
  await withhold(withheld, room, local, progress);
1512
2438
  return;
1513
2439
  }
1514
- await submitRoom(room, local, authorization, progress);
2440
+ await caughtUp(room, local, progress, live);
2441
+ }
2442
+ async function reload(room, local, progress, live) {
2443
+ try {
2444
+ if (!live() || authorization === undefined)
2445
+ return;
2446
+ const bootstrapped = await pullBootstrap(authorization, room, local, live);
2447
+ if (!live())
2448
+ return;
2449
+ if (bootstrapped.kind !== 'staged') {
2450
+ await withhold(bootstrapped, room, local, progress);
2451
+ return;
2452
+ }
2453
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
2454
+ await caughtUp(room, local, progress, live);
2455
+ }
2456
+ finally {
2457
+ caughtUpTo(room.locator);
2458
+ }
1515
2459
  }
1516
- async function cycle(local, progress) {
2460
+ async function discoverRooms(local, progress, attempt, live) {
1517
2461
  if (closed || authorization === undefined) {
1518
2462
  notifyReady();
1519
2463
  return;
1520
2464
  }
1521
2465
  moveTo(transferring(status));
2466
+ discoveries += 1;
2467
+ const started = discoveries;
1522
2468
  let result;
1523
2469
  try {
1524
2470
  result = await options.adapter.discover(authorization);
@@ -1526,7 +2472,8 @@ export function composeRuntime(options) {
1526
2472
  catch {
1527
2473
  result = { kind: 'transport-uncertainty' };
1528
2474
  }
1529
- if (closed) {
2475
+ if (!live()) {
2476
+ progress.complete = false;
1530
2477
  notifyReady();
1531
2478
  return;
1532
2479
  }
@@ -1539,12 +2486,17 @@ export function composeRuntime(options) {
1539
2486
  }
1540
2487
  }
1541
2488
  progress.reachable = false;
2489
+ progress.complete = false;
1542
2490
  notifyReady();
1543
2491
  return;
1544
2492
  }
1545
2493
  if (result.kind === 'contract-unavailable') {
1546
2494
  journal.record(`connection contract unavailable ${result.code}`);
1547
- await refuse('missing-required-guarantee', { code: result.code, fields: result.fields });
2495
+ await refuse('missing-required-guarantee', {
2496
+ code: result.code,
2497
+ fields: result.fields,
2498
+ retryable: result.retryable,
2499
+ });
1548
2500
  notifyReady();
1549
2501
  return;
1550
2502
  }
@@ -1553,17 +2505,6 @@ export function composeRuntime(options) {
1553
2505
  notifyReady();
1554
2506
  return;
1555
2507
  }
1556
- if (!result.supportedVersions.includes(ROOM_CONTRACT_VERSION)) {
1557
- await refuse('no-matching-version');
1558
- notifyReady();
1559
- return;
1560
- }
1561
- if (result.mdModel.identity !== MD_MODEL.identity ||
1562
- result.mdModel.version !== MD_MODEL.version) {
1563
- await refuse('md-model-version-mismatch');
1564
- notifyReady();
1565
- return;
1566
- }
1567
2508
  for (const room of result.rooms) {
1568
2509
  if (room.operations === undefined ||
1569
2510
  !['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
@@ -1571,8 +2512,7 @@ export function composeRuntime(options) {
1571
2512
  room.replica === undefined ||
1572
2513
  typeof room.replica.selection !== 'string' ||
1573
2514
  room.replica.selection.length === 0 ||
1574
- !['admitted', 'unavailable'].includes(room.replica.admission) ||
1575
- !declaredIdentity(room.attachedModel)) {
2515
+ !['admitted', 'unavailable'].includes(room.replica.admission)) {
1576
2516
  journal.record(`invalid model or replica declaration ${room.locator}`);
1577
2517
  await refuse('missing-required-guarantee');
1578
2518
  notifyReady();
@@ -1585,41 +2525,26 @@ export function composeRuntime(options) {
1585
2525
  return;
1586
2526
  }
1587
2527
  }
1588
- const declared = new Set(result.capabilities.map((capability) => capability.name));
1589
- for (const required of options.requiredGuarantees) {
1590
- if (!knownCapabilities.has(required) || !declared.has(required)) {
1591
- await refuse('missing-required-guarantee');
1592
- notifyReady();
1593
- return;
1594
- }
1595
- }
1596
2528
  if (refusal !== null) {
1597
2529
  refusal = null;
1598
2530
  emitRefusal();
1599
2531
  }
1600
2532
  const previousCapabilities = capabilities;
1601
2533
  capabilities = result.capabilities;
2534
+ outcomeRetention = result.retention.outcome;
1602
2535
  notifyReady();
1603
- if (declared.has('room-version-notifications'))
1604
- observeRoomVersions();
1605
2536
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1606
2537
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
1607
2538
  limits = result.providerLimits;
1608
2539
  await local.declare(limits, capabilities);
1609
2540
  }
1610
- await applyDiscovery(result.rooms, local);
1611
- handSelected(result.rooms
1612
- .filter((room) => room.replica.admission === 'admitted')
1613
- .map((room) => room.locator)
1614
- .sort());
1615
- progress.complete = true;
1616
- await synchronize(result.rooms, local, progress);
1617
- if (closed || discoveryPaused)
1618
- return;
1619
- await conversations.reconcile();
1620
- if (status.providerUnreachable)
1621
- conversations.reconnected();
1622
- await conversations.catchUp(progress);
2541
+ const signedIn = result.member ?? null;
2542
+ if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
2543
+ await local.keepMember(signedIn);
2544
+ member = signedIn;
2545
+ emitMember();
2546
+ }
2547
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
1623
2548
  }
1624
2549
  async function ensureAuthorization() {
1625
2550
  if (authorization !== undefined)
@@ -1633,66 +2558,414 @@ export function composeRuntime(options) {
1633
2558
  throw error;
1634
2559
  }
1635
2560
  }
1636
- async function connect() {
1637
- if (closed || stopping || discoveryPaused) {
1638
- notifyReady();
1639
- return;
2561
+ async function admittedRead(work) {
2562
+ if (roomReads.generation !== generation) {
2563
+ roomReads = { generation, free: ROOM_READS, waiting: [] };
1640
2564
  }
1641
- startTimer();
1642
- const progress = { complete: false, reachable: true };
1643
- let completedAt = new Date().toISOString();
2565
+ const reads = roomReads;
2566
+ while (reads.free === 0)
2567
+ await new Promise((admit) => reads.waiting.push(admit));
2568
+ reads.free -= 1;
1644
2569
  try {
1645
- const local = await openLocal();
1646
- if (closed)
2570
+ await work();
2571
+ }
2572
+ finally {
2573
+ reads.free += 1;
2574
+ reads.waiting.shift()?.();
2575
+ }
2576
+ }
2577
+ function roomTask(locator, work) {
2578
+ return task(roomLane(locator), (live) => admittedRead(() => work(live)));
2579
+ }
2580
+ function task(lane, work) {
2581
+ if (stopping)
2582
+ return Promise.resolve();
2583
+ const held = lanes.get(lane);
2584
+ const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
2585
+ const turn = current.generation;
2586
+ const live = () => turn === generation && continuing();
2587
+ const run = current.chain.then(() => work(live)).catch(() => undefined);
2588
+ current.chain = run;
2589
+ lanes.set(lane, current);
2590
+ working.add(run);
2591
+ void run.finally(() => working.delete(run));
2592
+ return run;
2593
+ }
2594
+ function sync(attempt) {
2595
+ return task(OPENING_LANE, async (live) => {
2596
+ if (!live()) {
2597
+ notifyReady();
1647
2598
  return;
1648
- await ensureAuthorization();
1649
- if (closed)
2599
+ }
2600
+ const progress = { complete: true, reachable: true };
2601
+ const gate = discover();
2602
+ if (attempt === 'opened')
2603
+ opened = generation;
2604
+ underway = attempt === 'opened' ? { generation, progress } : null;
2605
+ if (attempt === 'opened')
2606
+ streamed = progress;
2607
+ complete = false;
2608
+ recorded = false;
2609
+ moveTo(transferring(status));
2610
+ handed = false;
2611
+ try {
2612
+ const local = await openLocal();
2613
+ if (!live())
2614
+ return;
2615
+ await ensureAuthorization();
2616
+ if (!live())
2617
+ return;
2618
+ await discoverRooms(local, progress, attempt, live);
2619
+ }
2620
+ catch {
2621
+ progress.complete = false;
2622
+ if (attempt === 'opened' && !handed)
2623
+ progress.reachable = false;
2624
+ }
2625
+ finally {
2626
+ gate.release();
2627
+ notifyReady();
2628
+ }
2629
+ if (!live())
1650
2630
  return;
1651
- await cycle(local, progress);
1652
- if (closed)
2631
+ if (attempt !== 'opened') {
2632
+ await finishOpening(progress, live);
2633
+ return;
2634
+ }
2635
+ if (handed || closed || progress.reachable)
2636
+ return;
2637
+ underway = null;
2638
+ opened = null;
2639
+ journal.record('connection reopen');
2640
+ await rest(progress, live);
2641
+ options.adapter.roomStream?.reopen();
2642
+ });
2643
+ }
2644
+ function closedSocket(revoked) {
2645
+ const opened = socketOpen;
2646
+ socketOpen = false;
2647
+ if (revoked) {
2648
+ void refuse('revoked-authorization');
2649
+ return;
2650
+ }
2651
+ complete = false;
2652
+ if (streamed !== null)
2653
+ streamed.complete = false;
2654
+ moveTo({
2655
+ ...status,
2656
+ state: status.state === 'synced' ? 'syncing' : status.state,
2657
+ providerUnreachable: status.providerUnreachable || !opened,
2658
+ });
2659
+ }
2660
+ function applyOpening(expired) {
2661
+ const progress = underway?.generation === generation ? underway.progress : null;
2662
+ const cycle = progress ?? { complete: true, reachable: true };
2663
+ const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
2664
+ if (opened === generation)
2665
+ opened = null;
2666
+ return task(OPENING_LANE, async (live) => {
2667
+ underway = null;
2668
+ if (!live())
2669
+ return;
2670
+ if (progress === null) {
2671
+ await replacing;
2672
+ await rest(cycle, live);
1653
2673
  return;
1654
- if (progress.complete) {
1655
- completedAt = new Date().toISOString();
1656
- await local.complete(completedAt);
1657
2674
  }
2675
+ await roomsCarried(expired);
2676
+ if (!live())
2677
+ return;
2678
+ const replaced = new Set(owed);
2679
+ owed.clear();
2680
+ try {
2681
+ const local = await openLocal();
2682
+ const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
2683
+ const reloading = held.filter((room) => !expired.includes(room.locator) &&
2684
+ (replaced.has(room.locator) || revoked(room.locator)));
2685
+ for (const room of reloading)
2686
+ catching.add(room.locator);
2687
+ readSide.recover();
2688
+ const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
2689
+ const reloaded = [...(await reloads), ...(await replacing)];
2690
+ if (progress.reachable) {
2691
+ for (const room of held) {
2692
+ if (!expired.includes(room.locator) && !reloading.includes(room)) {
2693
+ await confirmModels(room, local);
2694
+ }
2695
+ }
2696
+ }
2697
+ const failed = reloaded.find((outcome) => outcome.status === 'rejected');
2698
+ if (failed !== undefined)
2699
+ throw failed.reason;
2700
+ }
2701
+ catch {
2702
+ progress.complete = false;
2703
+ progress.reachable = false;
2704
+ }
2705
+ if (live())
2706
+ await finishOpening(progress, live);
2707
+ });
2708
+ }
2709
+ async function reloadNow(replaced, progress) {
2710
+ try {
2711
+ const local = await openLocal();
2712
+ return await Promise.allSettled([...discovered.values()]
2713
+ .filter((room) => replicas.has(room.locator) && replaced(room))
2714
+ .map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
1658
2715
  }
1659
2716
  catch {
1660
- progress.complete = false;
1661
- notifyReady();
2717
+ progress.reachable = false;
2718
+ return [];
2719
+ }
2720
+ }
2721
+ async function confirmModels(room, local) {
2722
+ for (const attached of ['collection', 'typed-selection']) {
2723
+ const model = modelOf(room.locator, attached);
2724
+ if (model.status === 'cached') {
2725
+ await retainModel(local, room, attached, { ...model, status: 'current' });
2726
+ }
2727
+ }
2728
+ }
2729
+ function submitLater(locator) {
2730
+ if (localSaves)
1662
2731
  return;
2732
+ void recovery.attempt(`submit ${locator}`, (signal) => submitPass(locator, signal));
2733
+ }
2734
+ async function submitPass(locator, signal) {
2735
+ const room = discovered.get(locator);
2736
+ if (closed || stopping || discoveryPaused || room === undefined)
2737
+ return 'done';
2738
+ const progress = { complete: true, reachable: true };
2739
+ let next;
2740
+ try {
2741
+ await ensureAuthorization();
2742
+ if (authorization === undefined)
2743
+ return 'done';
2744
+ next = await submitRoom(room, await openLocal(), authorization, progress, signal);
2745
+ }
2746
+ catch {
2747
+ progress.reachable = false;
2748
+ next = 'again';
2749
+ }
2750
+ if (signal.aborted)
2751
+ return 'done';
2752
+ await rest(progress);
2753
+ return next === 'again' && !socketOpen ? 'done' : next;
2754
+ }
2755
+ function repairLater(locator, when) {
2756
+ if (!socketOpen || repairing.has(locator))
2757
+ return;
2758
+ void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
2759
+ }
2760
+ async function repairPass(locator, signal) {
2761
+ const room = discovered.get(locator);
2762
+ if (!socketOpen || closed || stopping || discoveryPaused || !stale.has(locator))
2763
+ return 'done';
2764
+ if (room === undefined || room.replica.admission !== 'admitted')
2765
+ return 'done';
2766
+ const progress = { complete: true, reachable: true };
2767
+ repairing.add(locator);
2768
+ try {
2769
+ const local = await openLocal();
2770
+ await roomTask(locator, async (live) => {
2771
+ const current = () => live() && !signal.aborted;
2772
+ if (!current() || authorization === undefined)
2773
+ return;
2774
+ const withheld = await Promise.race([
2775
+ advance(room, local, authorization, current),
2776
+ abandonedOn(signal),
2777
+ ]);
2778
+ if (withheld !== null && current())
2779
+ await withhold(withheld, room, local, progress);
2780
+ });
2781
+ }
2782
+ catch {
2783
+ stale.add(locator);
2784
+ progress.reachable = false;
1663
2785
  }
1664
2786
  finally {
1665
- notifyReady();
1666
- moveTo(settled(status, progress, completedAt, pending(), attention()));
2787
+ repairing.delete(locator);
2788
+ }
2789
+ if (signal.aborted)
2790
+ return 'done';
2791
+ await rest(progress);
2792
+ if (!stale.has(locator)) {
2793
+ if ((replicas.get(locator)?.queue.length ?? 0) > 0)
2794
+ submitLater(locator);
2795
+ return 'done';
2796
+ }
2797
+ if (!repairable.has(locator))
2798
+ return 'done';
2799
+ const reset = repairable.get(locator);
2800
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
2801
+ }
2802
+ function resendLater() {
2803
+ if (!socketOpen)
2804
+ return;
2805
+ void recovery.attempt('resend', resendPass, { again: true });
2806
+ }
2807
+ async function resendPass(signal) {
2808
+ if (!socketOpen || closed || stopping || discoveryPaused)
2809
+ return 'done';
2810
+ const reading = { complete: true, reachable: true };
2811
+ try {
2812
+ await conversations.resend(reading);
2813
+ }
2814
+ catch {
2815
+ reading.reachable = false;
2816
+ }
2817
+ if (signal.aborted)
2818
+ return 'done';
2819
+ return reading.reachable && reading.complete ? 'done' : 'again';
2820
+ }
2821
+ function submitAll() {
2822
+ for (const [locator, replica] of replicas) {
2823
+ if (replica.queue.length > 0)
2824
+ submitLater(locator);
1667
2825
  }
1668
2826
  }
1669
- async function run() {
2827
+ async function finishOpening(progress, live) {
1670
2828
  try {
1671
- await connect();
2829
+ if (!live()) {
2830
+ progress.complete = false;
2831
+ return;
2832
+ }
2833
+ submitAll();
2834
+ for (const locator of stale)
2835
+ if (repairable.has(locator))
2836
+ repairLater(locator, {});
2837
+ await conversations.reconcile();
2838
+ if (status.providerUnreachable)
2839
+ conversations.reconnected();
2840
+ const reading = { complete: true, reachable: true };
2841
+ if (demand.conversations)
2842
+ await conversations.catchUp(reading);
2843
+ if (progress.reachable && reading.reachable)
2844
+ await conversations.resend(reading);
2845
+ if (!reading.reachable) {
2846
+ progress.reachable = false;
2847
+ resendLater();
2848
+ }
2849
+ }
2850
+ catch {
2851
+ progress.complete = false;
1672
2852
  }
1673
2853
  finally {
1674
- inflight = false;
1675
- if (queued && !closed) {
1676
- queued = false;
1677
- void enqueue();
2854
+ if (live()) {
2855
+ complete = progress.complete;
2856
+ await rest(progress, live);
1678
2857
  }
1679
2858
  }
1680
2859
  }
1681
- function observeRoomVersions() {
1682
- const path = options.adapter.roomVersions;
1683
- if (path === undefined || withdrawNotifications !== undefined)
2860
+ async function rest(progress, live = () => !closed) {
2861
+ if (!live())
1684
2862
  return;
1685
- withdrawNotifications = path.observe((notification) => {
1686
- if (closed || stopping || discoveryPaused)
2863
+ let lastCompleted = status.lastCompleted;
2864
+ let synced = complete && stale.size === 0 && !pending();
2865
+ if (synced && !recorded) {
2866
+ const completedAt = new Date().toISOString();
2867
+ try {
2868
+ await (await openLocal()).complete(completedAt);
2869
+ if (!live())
2870
+ return;
2871
+ recorded = true;
2872
+ lastCompleted = completedAt;
2873
+ }
2874
+ catch {
2875
+ synced = false;
2876
+ }
2877
+ }
2878
+ moveTo({
2879
+ state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
2880
+ lastCompleted,
2881
+ providerUnreachable: !progress.reachable,
2882
+ ...attention(),
2883
+ });
2884
+ }
2885
+ function discover() {
2886
+ let release = () => { };
2887
+ const done = new Promise((resolve) => {
2888
+ release = resolve;
2889
+ });
2890
+ discovering = { generation, done, release };
2891
+ return discovering;
2892
+ }
2893
+ function applyRoom(locator, roomVersion) {
2894
+ announced.set(locator, roomVersion);
2895
+ return task(roomLane(locator), async (live) => {
2896
+ if (discovering?.generation === generation)
2897
+ await discovering.done;
2898
+ const saving = answering.get(locator);
2899
+ if (saving?.generation === generation)
2900
+ await saving.answered;
2901
+ if (!live() || authorization === undefined)
2902
+ return;
2903
+ const room = discovered.get(locator);
2904
+ const held = replicas.get(locator);
2905
+ if (room === undefined || room.replica.admission !== 'admitted' || !acquires(room))
1687
2906
  return;
1688
- if (notification.modelChanged === true && handed?.includes(notification.locator) === true) {
1689
- void enqueue();
2907
+ if (held !== undefined &&
2908
+ (held.confirmed.record.replica.selection !== room.replica.selection ||
2909
+ options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
1690
2910
  return;
1691
2911
  }
1692
- notices.set(notification.locator, notification.roomVersion);
1693
- chain = chain.then(runNotified, runNotified);
2912
+ const during = underway?.generation === generation ? underway.progress : null;
2913
+ const progress = during ?? { complete: true, reachable: true };
2914
+ if (during === null)
2915
+ moveTo(transferring(status));
2916
+ try {
2917
+ const local = await openLocal();
2918
+ await admittedRead(async () => {
2919
+ if (!live() || authorization === undefined)
2920
+ return;
2921
+ const withheld = await advance(room, local, authorization, live);
2922
+ if (withheld !== null)
2923
+ await withhold(withheld, room, local, progress);
2924
+ });
2925
+ }
2926
+ catch {
2927
+ stale.add(locator);
2928
+ }
2929
+ finally {
2930
+ if (during === null)
2931
+ await rest(progress, live);
2932
+ }
1694
2933
  });
1695
2934
  }
2935
+ function roomsCarried(replacing) {
2936
+ const replaced = new Set(replacing.map(roomLane));
2937
+ return Promise.all([...lanes]
2938
+ .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
2939
+ .map(([, held]) => held.chain));
2940
+ }
2941
+ const receipts = {
2942
+ heads: async () => (await openLocal()).receivedHeads(),
2943
+ parts: async (key, count) => (await openLocal()).receivedParts(key, count),
2944
+ keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
2945
+ drop: async (keys) => (await openLocal()).dropReceived(keys),
2946
+ };
2947
+ function observeStream() {
2948
+ const stream = options.adapter.roomStream;
2949
+ if (stream === undefined || withdrawStream !== undefined)
2950
+ return;
2951
+ withdrawStream = stream.observe((event) => {
2952
+ if (closed || stopping)
2953
+ return;
2954
+ if (event.kind === 'opened') {
2955
+ socketOpen = true;
2956
+ generation += 1;
2957
+ conversations.reopened();
2958
+ recovery.resume();
2959
+ void sync('opened');
2960
+ }
2961
+ else if (event.kind === 'closed')
2962
+ closedSocket(event.revoked);
2963
+ else if (event.kind === 'opening')
2964
+ void applyOpening(event.expired);
2965
+ else
2966
+ void applyRoom(event.locator, event.roomVersion);
2967
+ }, receipts);
2968
+ }
1696
2969
  function writesHeld() {
1697
2970
  return serviceMode === 'write-halted' || haltedRefusal;
1698
2971
  }
@@ -1706,19 +2979,25 @@ export function composeRuntime(options) {
1706
2979
  haltedRefusal = false;
1707
2980
  if (mode !== serviceMode) {
1708
2981
  serviceMode = mode;
2982
+ queueReadiness();
1709
2983
  journal.record(`service mode ${mode}`);
1710
2984
  for (const listener of serviceModeListeners)
1711
2985
  listener(mode);
1712
2986
  }
1713
2987
  if (releasing)
1714
- void enqueue();
2988
+ submitAll();
1715
2989
  }
1716
2990
  function observeConversations() {
1717
2991
  const stream = options.adapter.conversations;
1718
2992
  if (stream === undefined || withdrawConversations !== undefined)
1719
2993
  return;
1720
2994
  withdrawConversations = stream.observe((event) => {
1721
- conversations.receive(event);
2995
+ if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
2996
+ queueReadiness();
2997
+ if (conversations.receive(event)) {
2998
+ void ready.then(scheduleCurrency);
2999
+ return;
3000
+ }
1722
3001
  if (event.kind !== 'gap' || conversations.current())
1723
3002
  return;
1724
3003
  show();
@@ -1726,129 +3005,47 @@ export function composeRuntime(options) {
1726
3005
  });
1727
3006
  }
1728
3007
  function scheduleCurrency() {
1729
- if (currencyQueued || closed || stopping)
3008
+ if (currencyQueued === generation || closed || stopping)
1730
3009
  return;
1731
- currencyQueued = true;
1732
- chain = chain.then(runCurrency, runCurrency);
3010
+ currencyQueued = generation;
3011
+ void task(CONVERSATION_LANE, runCurrency);
1733
3012
  }
1734
- async function runCurrency() {
1735
- currencyQueued = false;
1736
- if (closed || stopping || discoveryPaused)
3013
+ async function runCurrency(live) {
3014
+ if (currencyQueued === generation)
3015
+ currencyQueued = null;
3016
+ if (!live())
1737
3017
  return;
1738
3018
  await conversations.reconcile();
1739
- if (conversations.current()) {
3019
+ if (conversations.current() || !demand.conversations) {
1740
3020
  show();
1741
3021
  return;
1742
3022
  }
1743
- const resting = status;
1744
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1745
- moveTo(transferring(resting));
3023
+ const progress = { complete: false, reachable: !status.providerUnreachable };
3024
+ moveTo(transferring(status));
1746
3025
  try {
1747
3026
  await conversations.catchUp(progress);
1748
3027
  }
1749
3028
  catch {
1750
3029
  progress.reachable = false;
1751
3030
  }
1752
- finally {
1753
- if (!closed) {
1754
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1755
- }
1756
- }
1757
- }
1758
- function handSelected(locators) {
1759
- const path = options.adapter.roomVersions;
1760
- if (path === undefined || withdrawNotifications === undefined)
1761
- return;
1762
- if (handed !== undefined && JSON.stringify(locators) === JSON.stringify(handed))
1763
- return;
1764
- handed = locators;
1765
- path.select(locators);
1766
- }
1767
- async function runNotified() {
1768
- const announced = [...notices];
1769
- notices.clear();
1770
- if (announced.length === 0 || closed || stopping || discoveryPaused)
1771
- return;
1772
- const resting = status;
1773
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1774
- moveTo(transferring(resting));
1775
- try {
1776
- const local = await openLocal();
1777
- for (const [locator, roomVersion] of announced) {
1778
- if (closed || discoveryPaused || authorization === undefined)
1779
- return;
1780
- if (handed?.includes(locator) !== true)
1781
- continue;
1782
- const room = discovered.get(locator);
1783
- const held = replicas.get(locator);
1784
- if (room === undefined)
1785
- continue;
1786
- if (sourceRefusals.has(locator)) {
1787
- void enqueue();
1788
- continue;
1789
- }
1790
- if (held === undefined)
1791
- continue;
1792
- if (held.confirmed.record.roomVersion === roomVersion)
1793
- continue;
1794
- const withheld = await advance(room, local, authorization);
1795
- if (withheld === null)
1796
- continue;
1797
- if (withheld.kind === 'refused') {
1798
- void enqueue();
1799
- continue;
1800
- }
1801
- if (withheld.kind === 'scoped-refusal') {
1802
- await scoped(withheld, room, local);
1803
- continue;
1804
- }
1805
- if (withheld.kind === 'transport-uncertainty') {
1806
- progress.reachable = false;
1807
- continue;
1808
- }
1809
- if (withheld.kind === 'room-not-readable') {
1810
- unreadable.add(locator);
1811
- list(room, false);
1812
- emitRooms();
1813
- continue;
1814
- }
1815
- if (withheld.kind === 'revoked-authorization') {
1816
- await refuse('revoked-authorization');
1817
- return;
1818
- }
1819
- }
1820
- }
1821
- catch {
1822
- progress.reachable = false;
1823
- }
1824
- finally {
1825
- if (!closed) {
1826
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1827
- }
1828
- }
1829
- }
1830
- function enqueue() {
1831
- if (stopping)
1832
- return chain;
1833
- if (inflight) {
1834
- queued = true;
1835
- return chain;
1836
- }
1837
- inflight = true;
1838
- chain = chain.then(run, run);
1839
- return chain;
3031
+ await rest(progress, live);
1840
3032
  }
1841
3033
  function close() {
1842
3034
  if (closed)
1843
3035
  return;
1844
3036
  closed = true;
1845
- withdrawNotifications?.();
1846
- withdrawNotifications = undefined;
3037
+ if (narrowing !== null)
3038
+ clearTimeout(narrowing);
3039
+ if (rechecking !== null)
3040
+ clearInterval(rechecking);
3041
+ readinessWatches.clear();
3042
+ withdrawStream?.();
3043
+ withdrawStream = undefined;
1847
3044
  withdrawConversations?.();
1848
3045
  withdrawConversations = undefined;
1849
3046
  withdrawServiceMode?.();
1850
3047
  conversations.close();
1851
- stopTimer();
3048
+ void recovery.retire();
1852
3049
  store?.close();
1853
3050
  void opening?.then((opened) => {
1854
3051
  opened.close();
@@ -1858,6 +3055,7 @@ export function composeRuntime(options) {
1858
3055
  storageLossListeners.clear();
1859
3056
  statusListeners.clear();
1860
3057
  serviceModeListeners.clear();
3058
+ memberListeners.clear();
1861
3059
  readSide.close();
1862
3060
  queueSubscriptions.clear();
1863
3061
  roomDraftListeners.clear();
@@ -1865,8 +3063,8 @@ export function composeRuntime(options) {
1865
3063
  }
1866
3064
  async function stop() {
1867
3065
  stopping = true;
1868
- stopTimer();
1869
- await chain.catch(() => undefined);
3066
+ await recovery.retire();
3067
+ await Promise.all(working);
1870
3068
  await mutations;
1871
3069
  close();
1872
3070
  }
@@ -1875,11 +3073,111 @@ export function composeRuntime(options) {
1875
3073
  options.signal?.addEventListener('abort', close, { once: true });
1876
3074
  if (options.signal?.aborted)
1877
3075
  close();
1878
- void openLocal().catch(() => {
3076
+ observeStream();
3077
+ void openLocal().then(() => {
3078
+ if (capabilities.length > 0)
3079
+ notifyReady();
3080
+ }, () => {
1879
3081
  notifyReady();
1880
3082
  });
1881
- void enqueue();
1882
- function watch(topic, input, deliver) {
3083
+ if (options.adapter.roomStream === undefined)
3084
+ void sync('unstreamed');
3085
+ function planned(input, reach) {
3086
+ return planOf(input, roomFacts(reach), planReach(reach));
3087
+ }
3088
+ function watchPlan(plan, deliver) {
3089
+ let seeded = false;
3090
+ let pending = true;
3091
+ const shown = () => {
3092
+ const seed = seeds.get(plan.key);
3093
+ if (seed === undefined || seeded || !pending)
3094
+ return;
3095
+ seeded = true;
3096
+ deliver(seededResult(seed));
3097
+ };
3098
+ const watchers = seedWatchers.get(plan.key) ?? new Set();
3099
+ seedWatchers.set(plan.key, watchers);
3100
+ watchers.add(shown);
3101
+ const stop = readSide.watch(plan, identityOf(plan), () => {
3102
+ const cover = covered(plan);
3103
+ awaitPreparation(cover);
3104
+ return seedBehind(plan, cover);
3105
+ }, (message) => {
3106
+ const answered = message;
3107
+ pending = whole(answered) && 'pending' in answered.result;
3108
+ if (pending && seeds.has(plan.key)) {
3109
+ shown();
3110
+ return;
3111
+ }
3112
+ seeded = false;
3113
+ deliver(message);
3114
+ });
3115
+ return () => {
3116
+ watchers.delete(shown);
3117
+ if (watchers.size === 0)
3118
+ seedWatchers.delete(plan.key);
3119
+ stop();
3120
+ };
3121
+ }
3122
+ async function searched(input, reach) {
3123
+ const plan = searchPlanOf(input);
3124
+ const filter = filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach));
3125
+ for (;;) {
3126
+ const cover = covered(filter);
3127
+ awaitPreparation(cover);
3128
+ const pending = 'pending' in cover ? [...cover.pending] : [];
3129
+ const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
3130
+ const held = covered({
3131
+ ...filter,
3132
+ rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
3133
+ });
3134
+ const covering = 'pending' in held ? [] : held.coverage.rooms;
3135
+ const unindexed = covering
3136
+ .filter(({ room }) => !searchIndex.rooms().has(room))
3137
+ .map(({ room }) => room);
3138
+ for (const room of unindexed)
3139
+ pending.push({ room, awaits: 'index' });
3140
+ const listed = conversations.searchable();
3141
+ const named = reach !== null && !reach.conversations
3142
+ ? []
3143
+ : (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
3144
+ const waiting = named.filter((one) => !searchIndex.conversations().has(one));
3145
+ for (const conversation of waiting)
3146
+ pending.push({ conversation, awaits: 'index' });
3147
+ if (unindexed.length > 0 || waiting.length > 0)
3148
+ index(unindexed, waiting);
3149
+ const complete = pending.length === 0 && !('pending' in cover);
3150
+ const answering = complete
3151
+ ? filter
3152
+ : {
3153
+ ...filter,
3154
+ rooms: covering
3155
+ .filter(({ room }) => searchIndex.rooms().has(room))
3156
+ .map(({ room }) => room),
3157
+ };
3158
+ const answered = covered(answering);
3159
+ if ('pending' in answered)
3160
+ continue;
3161
+ const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
3162
+ const seen = searchIndex.changes();
3163
+ await searchIndex.caughtUp();
3164
+ const selected = plan.documents === undefined && reach === null ? null : await rowsQuery(answering);
3165
+ if (selected !== null && 'pending' in selected)
3166
+ continue;
3167
+ if (searchIndex.changes() !== seen ||
3168
+ JSON.stringify(covered(answering)) !== JSON.stringify(answered))
3169
+ continue;
3170
+ const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, complete ? null : new Set(answering.rooms));
3171
+ const coverage = {
3172
+ rooms: answered.coverage.rooms,
3173
+ conversations: searchedConversations,
3174
+ index: searchIndex.generation(),
3175
+ overlay: answered.coverage.overlay,
3176
+ };
3177
+ return complete ? { ...hits, coverage } : { pending, ...hits, coverage };
3178
+ }
3179
+ }
3180
+ const watch = (topic, input, deliver) => {
1883
3181
  if (topic === 'room-drafts') {
1884
3182
  const listener = { notify: deliver, ready: false };
1885
3183
  roomDraftListeners.add(listener);
@@ -1900,6 +3198,10 @@ export function composeRuntime(options) {
1900
3198
  if (topic === 'conversation') {
1901
3199
  return conversations.watch(input[0], input[1], deliver);
1902
3200
  }
3201
+ if (topic === 'conversations')
3202
+ return conversations.watchList(deliver);
3203
+ if (topic === 'agents')
3204
+ return conversations.watchAgents(deliver);
1903
3205
  if (topic === 'presentation') {
1904
3206
  const listener = {
1905
3207
  app: input[0],
@@ -1919,6 +3221,9 @@ export function composeRuntime(options) {
1919
3221
  presentationListeners.delete(listener);
1920
3222
  };
1921
3223
  }
3224
+ if (topic === 'readiness') {
3225
+ return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
3226
+ }
1922
3227
  if (topic === 'rooms') {
1923
3228
  roomListeners.add(deliver);
1924
3229
  deliver(listed());
@@ -1954,6 +3259,13 @@ export function composeRuntime(options) {
1954
3259
  serviceModeListeners.delete(deliver);
1955
3260
  };
1956
3261
  }
3262
+ if (topic === 'member') {
3263
+ memberListeners.add(deliver);
3264
+ deliver(member);
3265
+ return () => {
3266
+ memberListeners.delete(deliver);
3267
+ };
3268
+ }
1957
3269
  if (topic === 'queue') {
1958
3270
  const queue = { locator: input[0], notify: deliver };
1959
3271
  queueSubscriptions.add(queue);
@@ -1962,9 +3274,8 @@ export function composeRuntime(options) {
1962
3274
  queueSubscriptions.delete(queue);
1963
3275
  };
1964
3276
  }
1965
- const plan = planOf(input[0], roomFacts());
1966
- return readSide.watch(plan, identityOf(plan), deliver);
1967
- }
3277
+ return watchPlan(planned(input[0], null), deliver);
3278
+ };
1968
3279
  const md = {
1969
3280
  loaded() {
1970
3281
  return openLocal().then(() => undefined);
@@ -1972,15 +3283,8 @@ export function composeRuntime(options) {
1972
3283
  rooms() {
1973
3284
  return listed();
1974
3285
  },
1975
- query: ((input) => readSide.query(planOf(input, roomFacts()))),
1976
- async search(input) {
1977
- const plan = searchPlanOf(input);
1978
- const selected = plan.documents === undefined
1979
- ? null
1980
- : (await readSide.query(filterOf(plan.documents, roomFacts())))
1981
- .documents;
1982
- return searchIndex.search(plan, selected);
1983
- },
3286
+ query: ((input) => rowsQuery(planned(input, null))),
3287
+ search: (input) => searched(input, null),
1984
3288
  types(locator) {
1985
3289
  return {
1986
3290
  collection: modelTypes(modelOf(locator)),
@@ -2008,20 +3312,20 @@ export function composeRuntime(options) {
2008
3312
  return exclusive(async () => {
2009
3313
  const parts = readParts(input);
2010
3314
  if (path === '' || normalize(path) !== path)
2011
- throw new Error('room-draft-malformed');
3315
+ throw runtimeError('room-draft-malformed');
2012
3316
  const local = await openLocal();
2013
3317
  const current = roomDrafts.get(room)?.get(path);
2014
3318
  if ((current?.revision ?? null) !== revision) {
2015
- return {
2016
- outcome: 'stale',
2017
- current: current === undefined ? null : roomDraftOf(current),
2018
- };
3319
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2019
3320
  }
3321
+ const file = parts.file === undefined
3322
+ ? undefined
3323
+ : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
2020
3324
  const record = {
2021
3325
  locator: room,
2022
3326
  path,
2023
3327
  revision: roomDraftRevision(),
2024
- parts: held(current?.parts, parts),
3328
+ parts: held(current?.parts, parts, file),
2025
3329
  returned: current?.returned ?? null,
2026
3330
  };
2027
3331
  await local.keepRoomDraft(record);
@@ -2035,10 +3339,7 @@ export function composeRuntime(options) {
2035
3339
  const local = await openLocal();
2036
3340
  const current = roomDrafts.get(room)?.get(path);
2037
3341
  if (current === undefined || current.revision !== revision) {
2038
- return {
2039
- outcome: 'stale',
2040
- current: current === undefined ? null : roomDraftOf(current),
2041
- };
3342
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2042
3343
  }
2043
3344
  await local.discardRoomDraft(room, path);
2044
3345
  forgetRoomDraft(room, path);
@@ -2073,6 +3374,10 @@ export function composeRuntime(options) {
2073
3374
  history(locator, path, query) {
2074
3375
  return readHistory(locator, path, query);
2075
3376
  },
3377
+ binaryFile: {
3378
+ read: readBinaryPath,
3379
+ readReference: readBinaryReference,
3380
+ },
2076
3381
  call(name, input) {
2077
3382
  return invokeAction(name, input);
2078
3383
  },
@@ -2085,21 +3390,55 @@ export function composeRuntime(options) {
2085
3390
  return journal.export();
2086
3391
  },
2087
3392
  persistence() {
2088
- return options.shell.persistentStorage;
3393
+ return persistence === 'granted';
2089
3394
  },
2090
- observe: ((topic, ...input) => opened((deliver) => watch(topic, input, (message) => {
2091
- deliver(structuredClone(message));
2092
- }), topic === 'query')),
3395
+ async retry() {
3396
+ journal.record('retry');
3397
+ unindexable.clear();
3398
+ queueReadiness();
3399
+ resume();
3400
+ },
3401
+ observe: observing(watch),
2093
3402
  async reauthorize() {
2094
3403
  if (refusal === null || !refusal.reauthorizationOffered)
2095
3404
  return;
2096
3405
  journal.record('connection reauthorize');
2097
- await chain.catch(() => undefined);
3406
+ await Promise.all([...lanes.values()]
3407
+ .filter((held) => held.generation === generation)
3408
+ .map((held) => held.chain));
2098
3409
  await mutations;
2099
3410
  authorization = undefined;
2100
3411
  discoveryPaused = false;
2101
- await enqueue();
3412
+ if (options.adapter.roomStream === undefined)
3413
+ await sync('unstreamed');
3414
+ else
3415
+ await task(OPENING_LANE, ensureAuthorization);
3416
+ },
3417
+ };
3418
+ const reachable = {
3419
+ md,
3420
+ watch,
3421
+ referrerScope: (referrer) => options.adapter.referrerScope(referrer),
3422
+ plan: planned,
3423
+ query: (plan) => rowsQuery(plan),
3424
+ watchPlan,
3425
+ search: searched,
3426
+ watchReadiness,
3427
+ };
3428
+ return {
3429
+ md,
3430
+ seed,
3431
+ stop,
3432
+ lose: loseRooms,
3433
+ watch,
3434
+ acquire,
3435
+ persisted(answer) {
3436
+ if (answer === persistence)
3437
+ return;
3438
+ persistence = answer;
3439
+ journal.record(`persistence ${answer}`);
3440
+ queueReadiness();
2102
3441
  },
3442
+ within: (reach, port) => narrowed(reachable, reach, port),
2103
3443
  };
2104
- return { md, stop, watch };
2105
3444
  }