@agentshouse/mdruntime 0.1.0-c3903a68dcbbf → 0.1.0-c435c7a7c4f4e

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