@agentshouse/mdruntime 0.1.0-c3903a68dcbbf → 0.1.0-c3fa75348e547

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