@agentshouse/mdruntime 0.1.0-ce15a88ae8478 → 0.1.0-ce41fec66d91b

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 (196) hide show
  1. package/README.md +10 -8
  2. package/dist/adapter.d.ts +71 -59
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/attached-model.d.ts +2 -1
  5. package/dist/attached-model.d.ts.map +1 -1
  6. package/dist/attached-model.js +28 -4
  7. package/dist/binary.d.ts +0 -5
  8. package/dist/binary.d.ts.map +1 -1
  9. package/dist/binary.js +1 -1
  10. package/dist/bootstrap.d.ts +16 -8
  11. package/dist/bootstrap.d.ts.map +1 -1
  12. package/dist/bootstrap.js +142 -94
  13. package/dist/client.d.ts +8 -10
  14. package/dist/client.d.ts.map +1 -1
  15. package/dist/client.js +54 -8
  16. package/dist/collection.d.ts +5 -3
  17. package/dist/collection.d.ts.map +1 -1
  18. package/dist/collection.js +24 -19
  19. package/dist/compile.d.ts.map +1 -1
  20. package/dist/compile.js +95 -30
  21. package/dist/connection.d.ts +8 -8
  22. package/dist/connection.d.ts.map +1 -1
  23. package/dist/connection.js +1304 -1071
  24. package/dist/conversation.d.ts +13 -11
  25. package/dist/conversation.d.ts.map +1 -1
  26. package/dist/conversation.js +244 -136
  27. package/dist/durability.d.ts +1 -5
  28. package/dist/durability.d.ts.map +1 -1
  29. package/dist/durability.js +5 -0
  30. package/dist/entry.d.ts +1 -0
  31. package/dist/entry.d.ts.map +1 -1
  32. package/dist/entry.js +1 -1
  33. package/dist/file.d.ts +2 -11
  34. package/dist/file.d.ts.map +1 -1
  35. package/dist/file.js +6 -4
  36. package/dist/fulltext.d.ts.map +1 -1
  37. package/dist/fulltext.js +8 -6
  38. package/dist/host.d.ts +3 -4
  39. package/dist/host.d.ts.map +1 -1
  40. package/dist/host.js +40 -20
  41. package/dist/index.d.ts +10 -10
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +4 -3
  44. package/dist/media.d.ts +8 -0
  45. package/dist/media.d.ts.map +1 -0
  46. package/dist/media.js +218 -0
  47. package/dist/memory.d.ts +9 -6
  48. package/dist/memory.d.ts.map +1 -1
  49. package/dist/memory.js +175 -98
  50. package/dist/model.d.ts +6 -1
  51. package/dist/model.d.ts.map +1 -1
  52. package/dist/model.js +7 -10
  53. package/dist/observation.d.ts +1 -0
  54. package/dist/observation.d.ts.map +1 -1
  55. package/dist/observation.js +1 -1
  56. package/dist/outcome.d.ts +13 -6
  57. package/dist/outcome.d.ts.map +1 -1
  58. package/dist/outcome.js +16 -5
  59. package/dist/overlay.js +1 -1
  60. package/dist/paths.d.ts +1 -0
  61. package/dist/paths.d.ts.map +1 -1
  62. package/dist/paths.js +4 -0
  63. package/dist/query.d.ts +6 -0
  64. package/dist/query.d.ts.map +1 -1
  65. package/dist/reach.d.ts +8 -3
  66. package/dist/reach.d.ts.map +1 -1
  67. package/dist/reach.js +67 -51
  68. package/dist/readiness.d.ts +13 -14
  69. package/dist/readiness.d.ts.map +1 -1
  70. package/dist/readiness.js +2 -8
  71. package/dist/recovery.d.ts +1 -0
  72. package/dist/recovery.d.ts.map +1 -1
  73. package/dist/recovery.js +26 -0
  74. package/dist/refresh.d.ts +10 -4
  75. package/dist/refresh.d.ts.map +1 -1
  76. package/dist/refresh.js +85 -30
  77. package/dist/replica.d.ts +5 -5
  78. package/dist/replica.d.ts.map +1 -1
  79. package/dist/replica.js +4 -7
  80. package/dist/room-draft.d.ts +18 -2
  81. package/dist/room-draft.d.ts.map +1 -1
  82. package/dist/room-draft.js +73 -6
  83. package/dist/rows.d.ts +15 -6
  84. package/dist/rows.d.ts.map +1 -1
  85. package/dist/rows.js +102 -74
  86. package/dist/save.d.ts +2 -0
  87. package/dist/save.d.ts.map +1 -1
  88. package/dist/save.js +13 -7
  89. package/dist/shared.d.ts +0 -1
  90. package/dist/shared.d.ts.map +1 -1
  91. package/dist/shared.js +0 -10
  92. package/dist/status.d.ts +4 -3
  93. package/dist/status.d.ts.map +1 -1
  94. package/dist/status.js +3 -1
  95. package/dist/store.d.ts +20 -26
  96. package/dist/store.d.ts.map +1 -1
  97. package/dist/store.js +27 -51
  98. package/dist/stored.d.ts +10 -18
  99. package/dist/stored.d.ts.map +1 -1
  100. package/dist/stored.js +209 -193
  101. package/dist/tables.d.ts +28 -35
  102. package/dist/tables.d.ts.map +1 -1
  103. package/dist/tables.js +58 -50
  104. package/dist/transfer.d.ts +0 -5
  105. package/dist/transfer.d.ts.map +1 -1
  106. package/dist/validation.d.ts +3 -4
  107. package/dist/validation.d.ts.map +1 -1
  108. package/dist/validation.js +35 -96
  109. package/dist/wire.d.ts +5 -9
  110. package/dist/wire.d.ts.map +1 -1
  111. package/dist/wire.js +2 -3
  112. package/dist/words.d.ts.map +1 -1
  113. package/dist/words.js +8 -2
  114. package/package.json +11 -18
  115. package/AGENTS.md +0 -319
  116. package/dist/chat/acts.d.ts +0 -33
  117. package/dist/chat/acts.d.ts.map +0 -1
  118. package/dist/chat/acts.js +0 -36
  119. package/dist/chat/context.d.ts +0 -3
  120. package/dist/chat/context.d.ts.map +0 -1
  121. package/dist/chat/context.js +0 -10
  122. package/dist/chat/earlier.d.ts +0 -10
  123. package/dist/chat/earlier.d.ts.map +0 -1
  124. package/dist/chat/earlier.js +0 -54
  125. package/dist/chat/house.d.ts +0 -25
  126. package/dist/chat/house.d.ts.map +0 -1
  127. package/dist/chat/house.js +0 -38
  128. package/dist/chat/input.d.ts +0 -11
  129. package/dist/chat/input.d.ts.map +0 -1
  130. package/dist/chat/input.js +0 -92
  131. package/dist/chat/kit.d.ts +0 -61
  132. package/dist/chat/kit.d.ts.map +0 -1
  133. package/dist/chat/kit.js +0 -123
  134. package/dist/chat/lasting.d.ts +0 -6
  135. package/dist/chat/lasting.d.ts.map +0 -1
  136. package/dist/chat/lasting.js +0 -19
  137. package/dist/chat/list.d.ts +0 -28
  138. package/dist/chat/list.d.ts.map +0 -1
  139. package/dist/chat/list.js +0 -103
  140. package/dist/chat/markdown.d.ts +0 -21
  141. package/dist/chat/markdown.d.ts.map +0 -1
  142. package/dist/chat/markdown.js +0 -246
  143. package/dist/chat/message.d.ts +0 -7
  144. package/dist/chat/message.d.ts.map +0 -1
  145. package/dist/chat/message.js +0 -254
  146. package/dist/chat/messages.d.ts +0 -35
  147. package/dist/chat/messages.d.ts.map +0 -1
  148. package/dist/chat/messages.js +0 -210
  149. package/dist/chat/name.d.ts +0 -7
  150. package/dist/chat/name.d.ts.map +0 -1
  151. package/dist/chat/name.js +0 -37
  152. package/dist/chat/opened.d.ts +0 -12
  153. package/dist/chat/opened.d.ts.map +0 -1
  154. package/dist/chat/opened.js +0 -19
  155. package/dist/chat/overlay.d.ts +0 -32
  156. package/dist/chat/overlay.d.ts.map +0 -1
  157. package/dist/chat/overlay.js +0 -103
  158. package/dist/chat/parts.d.ts +0 -58
  159. package/dist/chat/parts.d.ts.map +0 -1
  160. package/dist/chat/parts.js +0 -120
  161. package/dist/chat/pieces.d.ts +0 -62
  162. package/dist/chat/pieces.d.ts.map +0 -1
  163. package/dist/chat/pieces.js +0 -1110
  164. package/dist/chat/process.d.ts +0 -20
  165. package/dist/chat/process.d.ts.map +0 -1
  166. package/dist/chat/process.js +0 -32
  167. package/dist/chat/provider.d.ts +0 -70
  168. package/dist/chat/provider.d.ts.map +0 -1
  169. package/dist/chat/provider.js +0 -635
  170. package/dist/chat/question.d.ts +0 -9
  171. package/dist/chat/question.d.ts.map +0 -1
  172. package/dist/chat/question.js +0 -69
  173. package/dist/chat/questions.d.ts +0 -31
  174. package/dist/chat/questions.d.ts.map +0 -1
  175. package/dist/chat/questions.js +0 -111
  176. package/dist/chat/read.d.ts +0 -5
  177. package/dist/chat/read.d.ts.map +0 -1
  178. package/dist/chat/read.js +0 -13
  179. package/dist/chat/ready.d.ts +0 -37
  180. package/dist/chat/ready.d.ts.map +0 -1
  181. package/dist/chat/ready.js +0 -93
  182. package/dist/chat/room-file.d.ts +0 -8
  183. package/dist/chat/room-file.d.ts.map +0 -1
  184. package/dist/chat/room-file.js +0 -72
  185. package/dist/chat/steady.d.ts +0 -3
  186. package/dist/chat/steady.d.ts.map +0 -1
  187. package/dist/chat/steady.js +0 -26
  188. package/dist/chat/style.d.ts +0 -4
  189. package/dist/chat/style.d.ts.map +0 -1
  190. package/dist/chat/style.js +0 -307
  191. package/dist/chat.d.ts +0 -15
  192. package/dist/chat.d.ts.map +0 -1
  193. package/dist/chat.js +0 -8
  194. package/dist/reads.d.ts +0 -11
  195. package/dist/reads.d.ts.map +0 -1
  196. package/dist/reads.js +0 -98
@@ -1,5 +1,5 @@
1
- import { escapePointer, parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
2
- import { attachedModel, NO_ROOM_TYPES, roomTypesOf } from './attached-model.js';
1
+ import { parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
2
+ import { attachedModel, builtInModel, roomTypesOf } from './attached-model.js';
3
3
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
4
4
  import { observing, whole } from './observation.js';
5
5
  import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
@@ -12,29 +12,28 @@ import { bootstrapRoom } from './bootstrap.js';
12
12
  import { changeSet, mergeChangeSets } from './change-set.js';
13
13
  import { createConversationStore, } from './conversation.js';
14
14
  import { leaving as leavingIn, partnerOf, queuedFile, } from './change.js';
15
- import { available, collectionOf, unreadable as unreadableIn, validRoot, } from './collection.js';
15
+ import { available, collectionOf, composedOf, validRoot, } from './collection.js';
16
16
  import { runtimeError } from './failure.js';
17
17
  import { createLog } from './log.js';
18
18
  import { binaryRecord } from './binary.js';
19
+ import { attachedFile } from './file.js';
19
20
  import { openedFiles } from './opened.js';
20
- import { ABSENT_TARGET, absentBinary, absentReference, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
21
+ import { ABSENT_TARGET, absentBinary, attachAnswer, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, OFFLINE, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
21
22
  import { createRecovery } from './recovery.js';
22
- import { doorReads } from './reads.js';
23
23
  import { advancement } from './refresh.js';
24
24
  import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
25
- import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, sameMembership, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
26
- import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
25
+ import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
26
+ import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, readSavedParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
27
27
  import { same, saving, STARTING, transferring, } from './status.js';
28
- import { detectStorageLoss, nextGeneration } from './durability.js';
29
- import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
28
+ import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
29
+ import { readinessOf, } from './readiness.js';
30
30
  import { FencedMap } from './fence.js';
31
- import { fingerprint } from './stored.js';
32
31
  import { sameBinary } from './overlay.js';
33
32
  import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
34
33
  import { identityTime, roomDraftRevision } from './identity.js';
35
- import { beneath, normalize } from './paths.js';
34
+ import { ancestors, beneath, normalize } from './paths.js';
36
35
  import { openRuntimeStore, } from './store.js';
37
- import { changedCleanly, sameComposition, validatedCollection, } from './validation.js';
36
+ import { validatedNeighbourhood } from './validation.js';
38
37
  import { unconfirmed } from './transfer.js';
39
38
  import { governs, holds, narrowed, planReach, } from './reach.js';
40
39
  function within(root, path) {
@@ -55,34 +54,43 @@ const CONVERSATION_LANE = 'conversations';
55
54
  const ROOM_READS = 256;
56
55
  const ROOM_ATTEMPTS = 16;
57
56
  const ERASE_BATCH = 512;
58
- const REDERIVE_BATCH = 256;
59
- const WALK_BYTES = 1 << 20;
57
+ const DERIVE_BYTES = 1 << 23;
58
+ const DERIVE_FLOOR = 1 << 18;
59
+ // An index build aims at this much database work, so a Save, a query or the next part's progress waits behind at most one short build.
60
+ const DERIVE_AIM_MS = 250;
61
+ // A build past its floor of text stops after a second and a half of work, its full-text writes included, so a Room whose text costs ten times the usual holds a Save, a query or the next part's progress behind no longer build.
62
+ const DERIVE_LONGEST_MS = 1_500;
60
63
  const QUERY_BYTES = 1 << 20;
61
64
  const STAGE_BYTES = 1 << 20;
62
- const CLEAN_CHANGES = 2_000;
65
+ const STAGE_FLOOR = 1 << 16;
66
+ // A batch aims at this much database work, so a Save or a query waits behind at most one short write.
67
+ const STAGE_AIM_MS = 50;
68
+ const STREAMS_HELD = 4;
69
+ const NOTHING_HELD = { bytes: 0 };
70
+ const QUEUED_TEXT = 1 << 23;
71
+ // Bytes still arriving are reported at most this often, so readiness is not rebuilt for every chunk the network hands over while a slow link still moves the figure.
72
+ const ARRIVAL_REPORT_MS = 250;
63
73
  // A Room that locks offline stays readable this long after the connection drops, so a laptop waking or changing network keeps working while a disconnected device locks soon after.
64
74
  const LOCK_GRACE_MS = 120_000;
65
75
  function operationOf(input) {
66
76
  const operation = input?.operation_id;
67
77
  return typeof operation === 'string' ? operation : null;
68
78
  }
69
- function abandonedOn(signal) {
70
- return new Promise((resolve) => {
71
- if (signal.aborted)
72
- resolve(null);
73
- else
74
- signal.addEventListener('abort', () => resolve(null), { once: true });
79
+ function readUntil(read, signal) {
80
+ if (signal.aborted)
81
+ return Promise.resolve({ kind: 'transport-uncertainty' });
82
+ return new Promise((resolve, reject) => {
83
+ const stop = () => resolve({ kind: 'transport-uncertainty' });
84
+ signal.addEventListener('abort', stop, { once: true });
85
+ read.then(resolve, reject).finally(() => signal.removeEventListener('abort', stop));
75
86
  });
76
87
  }
77
- function roomLane(locator) {
78
- return `room ${locator}`;
79
- }
80
88
  function pendingKey(locator, id) {
81
89
  return `${locator}\u0000${id}`;
82
90
  }
83
91
  export function composeRuntime(options) {
84
92
  const localSaves = options.adapter.localSaves === true;
85
- const roomTypes = roomTypesOf(options.adapter.roomTypes);
93
+ const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
86
94
  let authorization = options.authorization;
87
95
  let opening;
88
96
  const fences = new Map();
@@ -92,44 +100,51 @@ export function composeRuntime(options) {
92
100
  const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
93
101
  let discoveries = 0;
94
102
  const replicas = new FencedMap(isFenced);
103
+ const reopened = new Set();
104
+ const usableWhenHeld = options.usableWhenHeld ?? [];
95
105
  const queuesWithoutReplica = new FencedMap(isFenced);
96
106
  const pendings = new Map();
97
107
  const seeds = new Map();
98
108
  const seedWatchers = new Map();
99
109
  const roomDrafts = new FencedMap(isFenced);
110
+ const attachedFiles = new FencedMap(isFenced);
100
111
  const presentations = new Map();
101
112
  let limits = [];
102
113
  let capabilities = [];
103
114
  const rooms = new FencedMap(isFenced);
104
115
  const counted = new Map();
105
116
  const unreadable = new Set();
106
- const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
107
- const models = new FencedMap(isModelFenced);
108
- const liveModels = new FencedMap(isModelFenced);
117
+ const builtIn = builtInModel(roomTypes);
118
+ const models = new FencedMap(isFenced);
119
+ const liveModels = new FencedMap(isFenced);
109
120
  const invalidModels = new Set();
110
121
  const discovered = new Map();
111
- const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
122
+ let inFlight = new AbortController();
123
+ const refreshed = options.adapter.refresh?.bind(options.adapter);
124
+ const reads = {
125
+ bootstrap: (authorized, request) => readUntil(options.adapter.bootstrap(authorized, request), inFlight.signal),
126
+ ...(refreshed === undefined
127
+ ? {}
128
+ : {
129
+ refresh: (authorized, request) => readUntil(refreshed(authorized, request), inFlight.signal),
130
+ }),
131
+ reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
132
+ };
112
133
  let roomsListed = false;
113
134
  const scopedRefusals = new Map();
114
- const sourceRefusals = new Map();
115
- const declarationRefusals = new Set();
116
- function modelKey(locator, attached) {
117
- return `${attached} ${locator}`;
118
- }
119
- function modelOf(locator, attached = 'collection') {
120
- return models.get(modelKey(locator, attached)) ?? NO_MODEL;
135
+ function modelOf(locator) {
136
+ return models.get(locator) ?? NO_MODEL;
121
137
  }
122
- function savedModel(locator, attached) {
123
- return invalidModels.has(modelKey(locator, attached))
138
+ function savedModel(locator) {
139
+ return invalidModels.has(locator)
124
140
  ? { status: 'unavailable', reason: 'revoked' }
125
- : modelOf(locator, attached);
141
+ : modelOf(locator);
142
+ }
143
+ function composed(record) {
144
+ return compositionOf(record, builtIn);
126
145
  }
127
146
  function compositionFor(record) {
128
- return compositionOf({
129
- ...record,
130
- model: savedModel(record.locator, 'collection'),
131
- selectionModel: savedModel(record.locator, 'typed-selection'),
132
- });
147
+ return composed({ ...record, model: savedModel(record.locator) });
133
148
  }
134
149
  let refusal = null;
135
150
  let storageLoss = null;
@@ -139,10 +154,10 @@ export function composeRuntime(options) {
139
154
  let closed = false;
140
155
  let stopping = false;
141
156
  let withdrawStream;
142
- const announced = new Map();
143
157
  let underway = null;
144
158
  let streamed = null;
145
159
  let handed = false;
160
+ let naming = 0;
146
161
  const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
147
162
  const repairing = new Set();
148
163
  const calling = new Set();
@@ -153,15 +168,18 @@ export function composeRuntime(options) {
153
168
  let recorded = false;
154
169
  const stale = new Set();
155
170
  let generation = 0;
156
- let discovering = null;
157
- let opened = null;
158
- const owed = new Set();
159
- const reloadsDue = new Map();
171
+ const reloadsDue = new Set();
172
+ const highest = new Map();
173
+ const beyondHorizon = new Set();
160
174
  const lanes = new Map();
161
- let roomReads = { generation, free: ROOM_READS, waiting: [] };
175
+ const roomLanes = new Map();
176
+ let roomWork = 0;
177
+ const roomReads = { free: ROOM_READS, waiting: [] };
162
178
  const working = new Set();
163
179
  let mutations = Promise.resolve();
180
+ let gathering = null;
164
181
  const roomListeners = new Set();
182
+ const unlistedRooms = new Set();
165
183
  const refusalListeners = new Set();
166
184
  const storageLossListeners = new Set();
167
185
  const statusListeners = new Set();
@@ -172,6 +190,7 @@ export function composeRuntime(options) {
172
190
  let withdrawConversations;
173
191
  let currencyQueued = null;
174
192
  let socketOpen = false;
193
+ let announced = false;
175
194
  const confirmed = new Set();
176
195
  let lockGrace = null;
177
196
  const lockListeners = new Set();
@@ -181,49 +200,80 @@ export function composeRuntime(options) {
181
200
  const serviceModeListeners = new Set();
182
201
  let member = null;
183
202
  const memberListeners = new Set();
184
- const prepared = new Set();
185
203
  const unpreparedDemand = new Set();
186
204
  const unpreparable = new Set();
187
205
  let preparing = false;
188
206
  let targets = {};
189
207
  let persistence = options.shell.persistence;
190
- let capacity = null;
191
- const paused = new Set();
192
- let rechecking = null;
208
+ let full = false;
209
+ let probing = null;
210
+ let testing = null;
211
+ const database = {
212
+ async ask(request) {
213
+ try {
214
+ return await options.database.ask(request);
215
+ }
216
+ catch (error) {
217
+ if (refusedForSpace(error))
218
+ filled();
219
+ throw error;
220
+ }
221
+ },
222
+ };
193
223
  const readinessWatches = new Set();
194
224
  let readinessQueued = false;
195
225
  const catching = new Set();
196
226
  const transfers = new FencedMap(isFenced);
227
+ const installs = new Set();
228
+ const arrivals = new Map();
229
+ const streaming = new Set();
230
+ const underived = new Map();
231
+ const unindexed = new Map();
232
+ const startedOver = new Map();
233
+ let arrivalReported = 0;
234
+ let streamsFree = STREAMS_HELD;
235
+ let queuedText = 0;
236
+ const installWaiters = [];
237
+ const stageBudget = {
238
+ bytes: STAGE_FLOOR * 4,
239
+ floor: STAGE_FLOOR,
240
+ ceiling: STAGE_BYTES,
241
+ aim: STAGE_AIM_MS,
242
+ };
243
+ const deriveBudget = {
244
+ bytes: DERIVE_FLOOR * 4,
245
+ floor: DERIVE_FLOOR,
246
+ ceiling: DERIVE_BYTES,
247
+ aim: DERIVE_AIM_MS,
248
+ };
249
+ const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
197
250
  const conversations = createConversationStore({
198
- agents: declaredAgents,
199
- documents: (declaration, after) => rowsQuery(planOf({
200
- rooms: [declaration.locator],
201
- types: [declaration.type],
202
- select: {
203
- fields: [
204
- declaration.identity,
205
- declaration.name,
206
- ...(declaration.retired === undefined ? [] : [declaration.retired]),
207
- ].map((field) => `/${escapePointer(field)}`),
208
- },
209
- limit: MAXIMUM_WINDOW,
210
- ...(after === undefined ? {} : { after }),
211
- }, roomFacts())),
212
251
  call: (name, input) => invokeAction(name, input),
252
+ history: (request) => readConversationHistory(request),
213
253
  durably,
214
254
  stream: () => options.adapter.conversations,
215
255
  publish: publishChangeSet,
216
256
  online: () => connectionOf() !== 'unreachable' && refusal === null,
257
+ full: () => full,
217
258
  closed: () => closed,
218
259
  unanswered: resendLater,
219
260
  expired: (operation) => pastHorizon(operation),
220
261
  });
221
262
  const journal = createLog();
222
- const keptFiles = openedFiles(options.database, isFenced);
223
- let notifyReady = () => { };
263
+ const keptFiles = openedFiles(database, isFenced);
264
+ let readyFromStore = () => { };
224
265
  const ready = new Promise((resolve) => {
225
- notifyReady = resolve;
266
+ readyFromStore = resolve;
267
+ });
268
+ let notifyDiscovered = () => { };
269
+ const firstDiscovery = new Promise((resolve) => {
270
+ notifyDiscovered = resolve;
226
271
  });
272
+ let roomsNamed = false;
273
+ function notifyReady() {
274
+ notifyDiscovered();
275
+ readyFromStore();
276
+ }
227
277
  function rememberRoomDraft(record) {
228
278
  const room = roomDrafts.get(record.locator) ?? new Map();
229
279
  room.set(record.path, record);
@@ -243,7 +293,7 @@ export function composeRuntime(options) {
243
293
  return undefined;
244
294
  const local = await openLocal();
245
295
  const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
246
- return { ...slice, collection: collectionOf(slice.documents, compositionOf(held.record)) };
296
+ return { ...slice, collection: collectionOf(slice.documents, composed(held.record)) };
247
297
  }
248
298
  function roomDraftOf(record, visible) {
249
299
  return structuredClone({
@@ -299,14 +349,40 @@ export function composeRuntime(options) {
299
349
  const value = presentations.get(app);
300
350
  return value === undefined ? null : structuredClone(value);
301
351
  }
302
- function keepPresentation(app, next) {
303
- return exclusive(async () => {
304
- const local = await openLocal();
305
- const stored = next(presentations.get(app));
306
- await local.present({ app, value: stored });
307
- presentations.set(app, stored);
308
- publishChangeSet(changeSet('', { presentation: app }));
309
- });
352
+ async function writtenRoomDraft(room, path, revision, input, next) {
353
+ const parts = readParts(input);
354
+ if (path === '' || normalize(path) !== path)
355
+ throw runtimeError('room-draft-malformed');
356
+ const local = await openLocal();
357
+ const current = roomDrafts.get(room)?.get(path);
358
+ if ((current?.revision ?? null) !== revision) {
359
+ return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
360
+ }
361
+ const file = parts.file === undefined
362
+ ? undefined
363
+ : await heldFile(parts.file.blob, parts.file.base === undefined
364
+ ? binaryBase(await visibleOf(room, [path]), path)
365
+ : parts.file.base);
366
+ const record = {
367
+ locator: room,
368
+ path,
369
+ revision: next,
370
+ parts: held(current?.parts, parts, file),
371
+ returned: current?.returned ?? null,
372
+ };
373
+ await local.keepRoomDraft(record, parts.file === undefined || file === undefined
374
+ ? []
375
+ : [{ key: file.localId, blob: parts.file.blob }]);
376
+ rememberRoomDraft(record);
377
+ publishChangeSet(changeSet(room, { drafts: [path] }));
378
+ return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
379
+ }
380
+ async function keptPresentation(app, next) {
381
+ const local = await openLocal();
382
+ const stored = next(presentations.get(app));
383
+ await local.present({ app, value: stored });
384
+ presentations.set(app, stored);
385
+ publishChangeSet(changeSet('', { presentation: app }));
310
386
  }
311
387
  function emitPresentation(app) {
312
388
  if (closed)
@@ -321,6 +397,20 @@ export function composeRuntime(options) {
321
397
  (queuesWithoutReplica.size > 0 ||
322
398
  [...replicas.values()].some((replica) => replica.queue.length > 0)));
323
399
  }
400
+ function unsent() {
401
+ const changed = new Map();
402
+ for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
403
+ for (const { batch, state } of entriesOf(room)) {
404
+ if (state === 'local')
405
+ continue;
406
+ for (const { path } of batch.changes)
407
+ changed.set(`${room}\u0000${path}`, { room, path });
408
+ }
409
+ }
410
+ return [...changed]
411
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
412
+ .map(([, change]) => change);
413
+ }
324
414
  function attention() {
325
415
  const attentionRequired = [];
326
416
  for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
@@ -332,16 +422,9 @@ export function composeRuntime(options) {
332
422
  returned: structuredClone(record.returned),
333
423
  });
334
424
  }
335
- for (const [room, refusal] of sourceRefusals)
336
- attentionRequired.push({ room, refusal: structuredClone(refusal) });
337
425
  attentionRequired.sort((left, right) => {
338
426
  if (left.room !== right.room)
339
427
  return left.room < right.room ? -1 : 1;
340
- if ('refusal' in left || 'refusal' in right) {
341
- if ('refusal' in left && 'refusal' in right)
342
- return 0;
343
- return 'refusal' in left ? -1 : 1;
344
- }
345
428
  if (left.path !== right.path)
346
429
  return left.path < right.path ? -1 : 1;
347
430
  return 0;
@@ -353,7 +436,7 @@ export function composeRuntime(options) {
353
436
  if (reset === null || submission.reset < reset)
354
437
  reset = submission.reset;
355
438
  }
356
- return { attentionRequired, reset };
439
+ return { attentionRequired, unsent: unsent(), reset };
357
440
  }
358
441
  function documentsIn(locator) {
359
442
  return replicas.get(locator)?.count ?? 0;
@@ -426,7 +509,7 @@ export function composeRuntime(options) {
426
509
  const governed = governs(reach, room);
427
510
  const attached = [
428
511
  ...(governed.collection ? [modelOf(room.locator)] : []),
429
- ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
512
+ ...(governed.selection ? [builtIn] : []),
430
513
  ];
431
514
  return {
432
515
  locator: room.locator,
@@ -442,11 +525,26 @@ export function composeRuntime(options) {
442
525
  const value = listed();
443
526
  for (const listener of roomListeners)
444
527
  listener(value);
528
+ emitKnownRooms();
529
+ }
530
+ function emitKnownRooms() {
531
+ conversations.learned();
532
+ if (unlistedRooms.size === 0)
533
+ return;
534
+ if (!roomsListed && rooms.size === 0 && refusal === null && !status.providerUnreachable)
535
+ return;
536
+ const value = listed();
537
+ for (const listener of [...unlistedRooms]) {
538
+ unlistedRooms.delete(listener);
539
+ roomListeners.add(listener);
540
+ listener(value);
541
+ }
445
542
  }
446
543
  function emitRefusal() {
447
544
  queueReadiness();
448
545
  for (const listener of refusalListeners)
449
546
  listener(refusal);
547
+ emitKnownRooms();
450
548
  }
451
549
  function emitStorageLoss() {
452
550
  queueReadiness();
@@ -467,6 +565,7 @@ export function composeRuntime(options) {
467
565
  if (closed)
468
566
  return;
469
567
  queueReadiness();
568
+ emitKnownRooms();
470
569
  const next = status.state === 'synced' && !conversations.current()
471
570
  ? { ...status, state: 'syncing' }
472
571
  : status;
@@ -480,9 +579,6 @@ export function composeRuntime(options) {
480
579
  if (localSaves)
481
580
  return Object.freeze({ batch, state: 'local' });
482
581
  const state = pendings.get(pendingKey(locator, batch.id));
483
- if (state?.accepted !== undefined) {
484
- return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
485
- }
486
582
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
487
583
  }
488
584
  function entriesOf(locator) {
@@ -501,18 +597,25 @@ export function composeRuntime(options) {
501
597
  function unpublished(error) {
502
598
  journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
503
599
  }
600
+ function indexed(locator) {
601
+ return replicas.get(locator)?.indexed === true;
602
+ }
504
603
  async function prepareReplica(locator) {
505
604
  for (;;) {
506
605
  const held = replicas.get(locator);
507
- if (held === undefined || closed || prepared.has(locator))
606
+ if (held === undefined || closed || held.indexed)
508
607
  return;
509
608
  const key = held.record.key;
510
- const done = await exclusive(async () => (await openLocal()).rederive(key, REDERIVE_BATCH));
511
- if (replicas.get(locator)?.record.key !== key)
609
+ const done = await exclusive(async () => {
610
+ const local = await openLocal();
611
+ return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR));
612
+ });
613
+ const now = replicas.get(locator);
614
+ if (now?.record.key !== key)
512
615
  continue;
513
- if (!done)
616
+ if (!done.done)
514
617
  continue;
515
- prepared.add(locator);
618
+ replicas.set(locator, { ...now, indexed: true });
516
619
  publishChangeSet(changeSet(locator, { documents: null }));
517
620
  liveQueries.recover();
518
621
  queueReadiness();
@@ -520,7 +623,7 @@ export function composeRuntime(options) {
520
623
  }
521
624
  }
522
625
  function unprepared() {
523
- return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unpreparable.has(locator));
626
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
524
627
  }
525
628
  function unpreparedBy(locator, error) {
526
629
  unpreparable.add(locator);
@@ -581,14 +684,14 @@ export function composeRuntime(options) {
581
684
  pending.push({ room: locator, awaits: 'acquisition' });
582
685
  continue;
583
686
  }
584
- if (!prepared.has(locator)) {
687
+ if (!replica.indexed) {
585
688
  pending.push({ room: locator, awaits: 'preparation' });
586
689
  continue;
587
690
  }
588
691
  coverage.push({
589
692
  room: locator,
590
693
  generation: replica.record.transfer,
591
- current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
694
+ current: !(catching.has(locator) || transfers.has(locator)),
592
695
  });
593
696
  read.push({ locator, key: replica.record.key });
594
697
  for (const batch of replica.queue)
@@ -615,8 +718,6 @@ export function composeRuntime(options) {
615
718
  return { listed: named.every((room) => replicas.has(room)), rooms: named };
616
719
  }
617
720
  function roomState(locator) {
618
- if (sourceRefusals.has(locator))
619
- return 'refused';
620
721
  const listedRoom = discovered.get(locator);
621
722
  if (roomsListed &&
622
723
  (listedRoom === undefined ||
@@ -625,13 +726,13 @@ export function composeRuntime(options) {
625
726
  return 'unadmitted';
626
727
  if (!replicas.has(locator))
627
728
  return 'acquiring';
628
- if (!prepared.has(locator))
729
+ if (!indexed(locator))
629
730
  return 'preparing';
630
731
  return 'complete';
631
732
  }
632
733
  function conversationCoverage(reach) {
633
734
  const declared = reach === null || reach.conversations;
634
- if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
735
+ if (!declared || options.adapter.conversations === undefined)
635
736
  return 'undeclared';
636
737
  if (!conversations.known() || !conversations.current())
637
738
  return 'acquiring';
@@ -646,21 +747,47 @@ export function composeRuntime(options) {
646
747
  ? 'connected'
647
748
  : 'connecting';
648
749
  }
649
- function refusedIn(locators) {
650
- return locators.flatMap((room) => {
651
- const held = sourceRefusals.get(room);
652
- if (held === undefined)
653
- return [];
654
- const { file, location } = held.fields;
655
- return [
656
- {
657
- room,
658
- file: typeof file === 'string' ? file : '',
659
- rule: held.code,
660
- location: typeof location === 'string' ? location : null,
661
- },
662
- ];
663
- });
750
+ function bytesOf(room, underway) {
751
+ const transfer = transfers.get(room);
752
+ if (transfer !== undefined) {
753
+ const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
754
+ return {
755
+ done: transfer.done.bytes - Math.ceil(owed),
756
+ total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
757
+ };
758
+ }
759
+ const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
760
+ return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
761
+ }
762
+ function installationOf(coverage) {
763
+ const rooms = { done: 0, total: 0 };
764
+ const bytes = { done: 0, total: 0 };
765
+ let installing = 0;
766
+ let downloading = false;
767
+ for (const { room, state } of coverage) {
768
+ if (state === 'unadmitted')
769
+ continue;
770
+ rooms.total += 1;
771
+ if (state === 'complete')
772
+ rooms.done += 1;
773
+ const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
774
+ if (underway)
775
+ installing += 1;
776
+ if (underway && (streaming.has(room) || !arrivals.has(room)))
777
+ downloading = true;
778
+ const now = bytesOf(room, underway);
779
+ const earlier = underway ? startedOver.get(room) : undefined;
780
+ bytes.done += Math.max(now.done, earlier?.done ?? 0);
781
+ bytes.total += Math.max(now.total, earlier?.total ?? 0);
782
+ }
783
+ const step = installing === 0
784
+ ? null
785
+ : downloading
786
+ ? 'downloading'
787
+ : bytes.done < bytes.total
788
+ ? 'writing'
789
+ : 'replaying';
790
+ return { step, rooms, bytes };
664
791
  }
665
792
  function readinessFor(port, reach) {
666
793
  const required = requiredRooms(reach);
@@ -682,18 +809,17 @@ export function composeRuntime(options) {
682
809
  persistence,
683
810
  connection: connectionOf(),
684
811
  house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
685
- capacity: capacity !== null &&
686
- coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
687
- ? capacity
688
- : port.capacity,
689
- refused: refusedIn(required.rooms),
812
+ space: full ? 'full' : port.space,
813
+ installation: installationOf(coverage),
690
814
  lost: storageLoss !== null,
691
- established: targets[port.target]?.established === true,
815
+ established: targets[port.target]?.established === true ||
816
+ (usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
692
817
  });
693
818
  }
694
819
  function establish(name) {
695
820
  targets = { ...targets, [name]: { established: true } };
696
821
  queueReadiness();
822
+ probeEachMinute();
697
823
  void durably((local) => local.fact('targets', targets)).catch(() => {
698
824
  journal.record(`target record failed ${name}`);
699
825
  });
@@ -731,87 +857,56 @@ export function composeRuntime(options) {
731
857
  readinessWatches.delete(watch);
732
858
  };
733
859
  }
734
- function pageBytes(page) {
735
- let bytes = 0;
736
- for (const { path, content } of page.entries)
737
- bytes += (path.length + (content?.length ?? 0)) * 2;
738
- return bytes;
739
- }
740
- function exceeded(error) {
741
- return (typeof error === 'object' &&
742
- error !== null &&
743
- error.name === 'QuotaExceededError');
744
- }
745
- async function estimated() {
746
- const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
747
- return estimate === null ? null : Math.max(0, estimate.total - estimate.usage);
748
- }
749
- function pause(locator, cause) {
750
- paused.add(locator);
751
- capacity = cause;
752
- journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
753
- queueReadiness();
754
- if (rechecking !== null)
860
+ function filled() {
861
+ if (full || closed)
755
862
  return;
756
- // A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
757
- rechecking = setInterval(() => {
758
- void recheck();
759
- }, 60_000);
863
+ full = true;
864
+ journal.record('space full');
865
+ queueReadiness();
866
+ probeEachMinute();
760
867
  }
761
- async function recheck() {
762
- const held = capacity;
763
- if (held === null || closed)
764
- return;
765
- const available = await estimated().catch(() => null);
766
- if (available === null || available < (held.needed ?? 0))
767
- return;
768
- if (held.cause === 'storage-exceeded' &&
769
- (held.available === null || available <= held.available))
770
- return;
771
- resume();
868
+ function installedTarget() {
869
+ return ((usableWhenHeld.length > 0 && reopened.size > 0) ||
870
+ Object.values(targets).some((target) => target.established));
772
871
  }
773
- function resume() {
774
- if (rechecking !== null)
775
- clearInterval(rechecking);
776
- rechecking = null;
777
- if (paused.size === 0 && capacity === null)
872
+ function probeEachMinute() {
873
+ if (!full || probing !== null || closed || !installedTarget())
778
874
  return;
779
- paused.clear();
780
- capacity = null;
781
- journal.record('capacity resumed');
782
- queueReadiness();
783
- if (!closed && authorization !== undefined)
784
- void sync('unstreamed');
785
- }
786
- async function reclaimed(local) {
787
- const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
788
- for (const locator of unreferenced) {
789
- await local.abandon(locator);
790
- transfers.delete(locator);
791
- }
792
- if (unreferenced.length > 0)
793
- journal.record(`capacity reclaimed ${unreferenced.length}`);
794
- return unreferenced.length > 0;
875
+ // A device whose storage filled after its install tries a write each minute, so space freed elsewhere ends its read-only mode without a Retry.
876
+ probing = setInterval(() => {
877
+ void probed();
878
+ }, 60_000);
795
879
  }
796
- async function admitted(local, locator, needed) {
797
- if (paused.has(locator))
798
- return false;
799
- let available = await estimated().catch(() => null);
800
- if (available === null || available >= needed)
880
+ function probed() {
881
+ testing ??= database.ask({ kind: 'space.probe' }).then(() => {
882
+ testing = null;
883
+ spaceFreed();
801
884
  return true;
802
- if (await reclaimed(local))
803
- available = await estimated().catch(() => null);
804
- if (available === null || available >= needed)
805
- return true;
806
- pause(locator, { cause: 'insufficient-space', needed, available });
807
- return false;
885
+ }, () => {
886
+ testing = null;
887
+ return false;
888
+ });
889
+ return testing;
808
890
  }
809
- async function exceededOn(locator, bytes) {
810
- const available = await estimated().catch(() => null);
811
- pause(locator, { cause: 'storage-exceeded', needed: bytes, available });
891
+ function spaceFreed() {
892
+ if (!full)
893
+ return;
894
+ full = false;
895
+ if (probing !== null)
896
+ clearInterval(probing);
897
+ probing = null;
898
+ unpreparable.clear();
899
+ journal.record('space freed');
900
+ queueReadiness();
901
+ if (closed || authorization === undefined)
902
+ return;
903
+ if (options.adapter.roomStream === undefined)
904
+ void sync('unstreamed', null);
905
+ else
906
+ void resynchronize();
812
907
  }
813
908
  function seedRoomHeld(room, version) {
814
- return prepared.has(room) && reachedBy(room, version);
909
+ return indexed(room) && reachedBy(room, version);
815
910
  }
816
911
  function seedHeld(at) {
817
912
  return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
@@ -934,6 +1029,10 @@ export function composeRuntime(options) {
934
1029
  }
935
1030
  function publishChangeSet(set) {
936
1031
  applyChangeSet(set);
1032
+ for (const { locator, path } of set.documents) {
1033
+ if (path !== null)
1034
+ attachedFiles.get(locator)?.delete(path);
1035
+ }
937
1036
  if (set.rooms.length > 0 ||
938
1037
  [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
939
1038
  emitRooms();
@@ -952,6 +1051,8 @@ export function composeRuntime(options) {
952
1051
  }
953
1052
  async function refuse(reason, provider = null) {
954
1053
  discoveryPaused = true;
1054
+ inFlight.abort();
1055
+ inFlight = new AbortController();
955
1056
  try {
956
1057
  if (reason === 'revoked-authorization') {
957
1058
  await exclusive(async () => {
@@ -959,27 +1060,24 @@ export function composeRuntime(options) {
959
1060
  const records = [...replicas].map(([locator, held]) => ({
960
1061
  locator,
961
1062
  held,
962
- record: { ...held.record, model: revoked, selectionModel: revoked },
1063
+ record: { ...held.record, model: revoked },
963
1064
  }));
1065
+ let answer = null;
964
1066
  try {
965
1067
  const local = await openLocal();
966
- await local.commit({
1068
+ answer = await local.commit({
967
1069
  rooms: records.map(({ locator, record }) => ({
968
1070
  locator,
969
1071
  key: record.key,
970
1072
  record: storedRecord(record),
971
- composition: compositionOf(record),
1073
+ composed: composedOf(composed(record)),
972
1074
  })),
973
1075
  });
974
1076
  }
975
1077
  catch {
976
1078
  journal.record('model revocation persistence failed');
977
1079
  }
978
- const keys = new Set(models.keys());
979
- for (const locator of rooms.keys()) {
980
- keys.add(modelKey(locator, 'collection'));
981
- keys.add(modelKey(locator, 'typed-selection'));
982
- }
1080
+ const keys = new Set([...models.keys(), ...rooms.keys()]);
983
1081
  for (const key of keys) {
984
1082
  invalidModels.add(key);
985
1083
  models.set(key, revoked);
@@ -989,8 +1087,8 @@ export function composeRuntime(options) {
989
1087
  for (const { locator, held, record } of records) {
990
1088
  if (replicas.get(locator) !== held)
991
1089
  continue;
992
- recomposed(locator, held.record, record);
993
- replicas.set(locator, { ...held, record });
1090
+ const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
1091
+ replicas.set(locator, { ...held, record, indexed });
994
1092
  }
995
1093
  for (const locator of [...rooms.keys()]) {
996
1094
  const room = rooms.get(locator);
@@ -1034,15 +1132,15 @@ export function composeRuntime(options) {
1034
1132
  ? null
1035
1133
  : {
1036
1134
  roots: selection.roots,
1037
- model: modelTypes(modelOf(room.locator, 'typed-selection')),
1135
+ model: modelTypes(builtIn),
1038
1136
  },
1039
1137
  providerOwnedPaths: room.providerOwnedPaths,
1040
1138
  roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
1041
1139
  synchronizationAvailable,
1042
- refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
1140
+ refusal: scopedRefusals.get(room.locator) ?? null,
1043
1141
  });
1044
1142
  }
1045
- async function applyDiscovery(found, started, covered) {
1143
+ async function applyDiscovery(found, started, left) {
1046
1144
  const lockedBefore = lockedRooms();
1047
1145
  for (const room of found) {
1048
1146
  const fence = fences.get(room.locator);
@@ -1056,18 +1154,11 @@ export function composeRuntime(options) {
1056
1154
  const listedLocators = new Set(listed.map((room) => room.locator));
1057
1155
  for (const locator of listedLocators)
1058
1156
  scopedRefusals.delete(locator);
1059
- if (covered === undefined)
1157
+ if (left === undefined)
1060
1158
  discovered.clear();
1061
- else
1062
- for (const locator of covered)
1063
- discovered.delete(locator);
1064
1159
  for (const room of listed)
1065
1160
  discovered.set(room.locator, room);
1066
- const unlisted = unlistedHeld(new Set(found.map((room) => room.locator))).filter((locator) => covered === undefined || covered.includes(locator));
1067
- if (options.adapter.exhaustiveDiscovery === true)
1068
- await loseRooms(unlisted);
1069
- else
1070
- unlistedKept(unlisted);
1161
+ await loseRooms(left ?? unlistedHeld(new Set(found.map((room) => room.locator))));
1071
1162
  for (const room of listed) {
1072
1163
  const held = replicas.get(room.locator)?.record;
1073
1164
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -1098,15 +1189,6 @@ export function composeRuntime(options) {
1098
1189
  ]),
1099
1190
  ].filter((locator) => !listed.has(locator));
1100
1191
  }
1101
- function unlistedKept(unlisted) {
1102
- for (const locator of unlisted) {
1103
- const held = replicas.get(locator)?.record;
1104
- if (held !== undefined)
1105
- list(held, false);
1106
- else
1107
- rooms.delete(locator);
1108
- }
1109
- }
1110
1192
  function fence(locators) {
1111
1193
  const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1112
1194
  const removed = fresh.map((locator) => ({
@@ -1120,28 +1202,24 @@ export function composeRuntime(options) {
1120
1202
  for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1121
1203
  pendings.delete(pendingKey(locator, batch.id));
1122
1204
  replicas.delete(locator);
1205
+ highest.delete(locator);
1206
+ beyondHorizon.delete(locator);
1123
1207
  queuesWithoutReplica.delete(locator);
1124
1208
  rooms.delete(locator);
1125
1209
  roomDrafts.delete(locator);
1126
- transfers.delete(locator);
1210
+ attachedFiles.delete(locator);
1211
+ untransferred(locator);
1212
+ startedOver.delete(locator);
1127
1213
  catching.delete(locator);
1128
1214
  discovered.delete(locator);
1129
- announced.delete(locator);
1130
1215
  unreadable.delete(locator);
1131
1216
  stale.delete(locator);
1132
1217
  repairable.delete(locator);
1133
1218
  unpreparable.delete(locator);
1134
- paused.delete(locator);
1135
- prepared.delete(locator);
1136
1219
  scopedRefusals.delete(locator);
1137
- sourceRefusals.delete(locator);
1138
- declarationRefusals.delete(locator);
1139
- for (const attached of ['collection', 'typed-selection']) {
1140
- const key = modelKey(locator, attached);
1141
- models.delete(key);
1142
- liveModels.delete(key);
1143
- invalidModels.delete(key);
1144
- }
1220
+ models.delete(locator);
1221
+ liveModels.delete(locator);
1222
+ invalidModels.delete(locator);
1145
1223
  journal.record(`room lost ${locator}`);
1146
1224
  }
1147
1225
  if (fresh.length === 0)
@@ -1189,7 +1267,14 @@ export function composeRuntime(options) {
1189
1267
  });
1190
1268
  await eraseFenced(fresh);
1191
1269
  }
1192
- function restored(key, persisted) {
1270
+ function retyped(persisted) {
1271
+ if (persisted.status === 'unavailable')
1272
+ return persisted;
1273
+ const { state } = attachedModel({ kind: 'model', snapshot: persisted.snapshot }, NO_MODEL, roomTypes);
1274
+ return state.status === 'unavailable' ? state : { ...persisted, types: state.types };
1275
+ }
1276
+ function restored(key, held) {
1277
+ const persisted = retyped(held);
1193
1278
  const prior = models.get(key);
1194
1279
  if (prior !== undefined &&
1195
1280
  prior.status !== 'unavailable' &&
@@ -1220,8 +1305,9 @@ export function composeRuntime(options) {
1220
1305
  const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1221
1306
  targets = held.targets;
1222
1307
  transfers.clear();
1223
- for (const [locator, record] of held.transfers)
1224
- transfers.set(locator, record);
1308
+ arrivals.clear();
1309
+ streaming.clear();
1310
+ startedOver.clear();
1225
1311
  limits = held.providerLimits;
1226
1312
  capabilities = held.capabilities;
1227
1313
  member = held.member;
@@ -1231,7 +1317,7 @@ export function composeRuntime(options) {
1231
1317
  presentations.clear();
1232
1318
  for (const record of held.presentation)
1233
1319
  presentations.set(record.app, record.value);
1234
- conversations.hydrate(heldConversations, chatDrafts);
1320
+ conversations.hydrate(heldConversations, chatDrafts, held.conversationList, held.conversationCursor);
1235
1321
  pendings.clear();
1236
1322
  for (const queue of queues) {
1237
1323
  for (const { batch, state } of queue.pending) {
@@ -1239,7 +1325,7 @@ export function composeRuntime(options) {
1239
1325
  }
1240
1326
  }
1241
1327
  replicas.clear();
1242
- prepared.clear();
1328
+ reopened.clear();
1243
1329
  queuesWithoutReplica.clear();
1244
1330
  for (const [locator, queue] of queuedBatches) {
1245
1331
  if (!held.rooms.has(locator))
@@ -1248,39 +1334,40 @@ export function composeRuntime(options) {
1248
1334
  records.forEach((stored, index) => {
1249
1335
  if (fences.has(stored.locator))
1250
1336
  return;
1251
- const model = restored(modelKey(stored.locator, 'collection'), stored.model);
1252
- const selectionModel = restored(modelKey(stored.locator, 'typed-selection'), stored.selectionModel);
1253
- const record = { ...stored, model, selectionModel };
1337
+ const record = { ...stored, model: restored(stored.locator, stored.model) };
1338
+ reopened.add(record.locator);
1254
1339
  replicas.set(record.locator, {
1255
1340
  record,
1256
1341
  queue: queuedBatches.get(record.locator) ?? [],
1257
1342
  overlaid: held.overlaid.get(record.key) ?? new Set(),
1258
1343
  count: counts[index] ?? 0,
1259
1344
  confirmed: 0,
1345
+ indexed: !held.unindexed.has(record.key),
1260
1346
  });
1261
- if (!held.unprepared.has(record.key))
1262
- prepared.add(record.locator);
1263
1347
  const existing = rooms.get(record.locator);
1264
1348
  if (existing === undefined)
1265
- list(record, false);
1349
+ list(record, record.roomVersion !== null);
1266
1350
  else {
1267
1351
  rooms.set(record.locator, {
1268
1352
  ...existing,
1269
1353
  roomVersion: record.roomVersion,
1270
1354
  operations: record.operations,
1271
1355
  replica: record.replica,
1272
- model: modelTypes(model),
1356
+ model: modelTypes(record.model),
1273
1357
  typedSelection: record.typedSelection === null
1274
1358
  ? null
1275
- : {
1276
- roots: record.typedSelection.roots,
1277
- model: modelTypes(selectionModel),
1278
- },
1359
+ : { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
1279
1360
  });
1280
1361
  }
1281
1362
  });
1282
- for (const key of held.orphans)
1283
- await opened.release(key);
1363
+ try {
1364
+ for (const key of held.orphans)
1365
+ await opened.release(key);
1366
+ }
1367
+ catch (error) {
1368
+ if (!refusedForSpace(error))
1369
+ throw error;
1370
+ }
1284
1371
  if (held.erasing || held.orphans.length > 0)
1285
1372
  queueMicrotask(sweep);
1286
1373
  return {
@@ -1305,7 +1392,7 @@ export function composeRuntime(options) {
1305
1392
  async function open() {
1306
1393
  if (closed)
1307
1394
  throw new Error('runtime closed');
1308
- const opened = openRuntimeStore(isFenced, options.database);
1395
+ const opened = openRuntimeStore(isFenced, database);
1309
1396
  const interrupted = await opened.fences();
1310
1397
  for (const locator of interrupted) {
1311
1398
  fences.set(locator, { at: discoveries, erased: false });
@@ -1351,10 +1438,29 @@ export function composeRuntime(options) {
1351
1438
  return opening;
1352
1439
  }
1353
1440
  function exclusive(work) {
1441
+ gathering = null;
1354
1442
  const next = mutations.then(work, work);
1355
1443
  mutations = next.then(() => undefined, () => undefined);
1356
1444
  return next;
1357
1445
  }
1446
+ function gathered(key, input, merge, work) {
1447
+ const tail = gathering;
1448
+ if (tail !== null && tail.key === key) {
1449
+ const merged = merge(tail.input, input);
1450
+ if (merged !== null) {
1451
+ tail.input = merged;
1452
+ return tail.done;
1453
+ }
1454
+ }
1455
+ const entry = { key, input, done: Promise.resolve() };
1456
+ entry.done = exclusive(() => {
1457
+ if (gathering === entry)
1458
+ gathering = null;
1459
+ return work(entry.input);
1460
+ });
1461
+ gathering = entry;
1462
+ return entry.done;
1463
+ }
1358
1464
  function withdrawable(locator) {
1359
1465
  return (batch) => {
1360
1466
  const state = pendings.get(pendingKey(locator, batch.id));
@@ -1436,26 +1542,14 @@ export function composeRuntime(options) {
1436
1542
  },
1437
1543
  };
1438
1544
  }
1439
- function recomposed(locator, before, after) {
1440
- if (before !== undefined &&
1441
- before.key === after.key &&
1442
- fingerprint(compositionOf(before)) === fingerprint(compositionOf(after))) {
1443
- return;
1444
- }
1445
- if (before !== undefined && before.key === after.key)
1446
- prepared.delete(locator);
1447
- }
1448
- function committedRoom(locator, record, answer) {
1449
- if (answer.prepared[locator] === false)
1450
- prepared.delete(locator);
1451
- else if (answer.prepared[locator] === true)
1452
- prepared.add(locator);
1453
- if (!prepared.has(locator))
1545
+ function committedRoom(locator, answer) {
1546
+ const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
1547
+ if (!indexed)
1454
1548
  prepare([locator]);
1455
- return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0 };
1549
+ return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
1456
1550
  }
1457
1551
  function replayed(record, confirmedLayer, queue) {
1458
- return stepped(replaying(confirmedLayer, queue, record.locator, compositionOf(record)));
1552
+ return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
1459
1553
  }
1460
1554
  async function overlayDiff(record, before, visible, touched) {
1461
1555
  const kept = before !== undefined && before.record.key === record.key
@@ -1475,8 +1569,6 @@ export function composeRuntime(options) {
1475
1569
  function unpairedIn(locator, batch) {
1476
1570
  if (!queueIn(locator).some((held) => held.id === batch.id))
1477
1571
  return false;
1478
- if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
1479
- return false;
1480
1572
  return !leavingIn(batch, locator) || withdrawable(locator)(batch);
1481
1573
  }
1482
1574
  async function together(local, settlements) {
@@ -1518,7 +1610,7 @@ export function composeRuntime(options) {
1518
1610
  const confirmedLayer = overrides === undefined
1519
1611
  ? read
1520
1612
  : await stepped(slicingWith(read, overrides.set, overrides.removed));
1521
- const composition = compositionOf(record);
1613
+ const composition = composed(record);
1522
1614
  const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
1523
1615
  const founded = kept.filter((batch) => !queue.includes(batch));
1524
1616
  const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
@@ -1561,6 +1653,7 @@ export function composeRuntime(options) {
1561
1653
  key: record.key,
1562
1654
  record: storedRecord(record),
1563
1655
  roots: record.typedSelection?.roots ?? [],
1656
+ composed: composedOf(composition),
1564
1657
  ...settling.commit,
1565
1658
  ...(overrides === undefined
1566
1659
  ? {}
@@ -1602,14 +1695,14 @@ export function composeRuntime(options) {
1602
1695
  const confirmedChanged = settling.commit?.published !== undefined ||
1603
1696
  settling.commit?.refreshed === true ||
1604
1697
  overrides !== undefined;
1605
- recomposed(locator, before?.record, record);
1606
- const { count } = committedRoom(locator, record, answer);
1698
+ const { count, indexed } = committedRoom(locator, answer);
1607
1699
  replicas.set(locator, {
1608
1700
  record,
1609
1701
  queue: kept,
1610
1702
  overlaid: touched,
1611
1703
  count,
1612
1704
  confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
1705
+ indexed,
1613
1706
  });
1614
1707
  return mergeChangeSets([
1615
1708
  changeSet(locator, {
@@ -1624,110 +1717,105 @@ export function composeRuntime(options) {
1624
1717
  };
1625
1718
  }
1626
1719
  function adoptModels(locator, record) {
1627
- for (const [attached, model] of [
1628
- ['collection', record.model],
1629
- ['typed-selection', record.selectionModel],
1630
- ]) {
1631
- const key = modelKey(locator, attached);
1632
- models.set(key, model);
1633
- invalidModels.delete(key);
1634
- if (model.status === 'current')
1635
- liveModels.set(key, JSON.stringify(model.snapshot));
1636
- else if (model.status === 'unavailable')
1637
- liveModels.delete(key);
1638
- }
1720
+ const model = record.model;
1721
+ models.set(locator, model);
1722
+ invalidModels.delete(locator);
1723
+ if (model.status === 'current')
1724
+ liveModels.set(locator, JSON.stringify(model.snapshot));
1725
+ else if (model.status === 'unavailable')
1726
+ liveModels.delete(locator);
1639
1727
  }
1640
1728
  function remodel(locator, answers, held) {
1641
- const taken = (answer, previous, attached) => {
1642
- if (answer === undefined)
1643
- return previous;
1644
- const { state, refused } = attachedModel(answer, previous, attached === 'collection' ? roomTypes : NO_ROOM_TYPES);
1645
- if (refused !== null) {
1646
- const where = refused.location === undefined ? '' : ` ${refused.location}`;
1647
- journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
1648
- }
1649
- models.set(modelKey(locator, attached), state);
1650
- return state;
1651
- };
1652
- return {
1653
- collection: taken(answers?.collection, held.collection, 'collection'),
1654
- selection: taken(answers?.selection, held.selection, 'typed-selection'),
1655
- };
1729
+ const answer = answers?.collection;
1730
+ if (answer === undefined)
1731
+ return held;
1732
+ const { state, refused } = attachedModel(answer, held.collection, roomTypes);
1733
+ if (refused !== null) {
1734
+ const where = refused.location === undefined ? '' : ` ${refused.location}`;
1735
+ journal.record(`model invalid ${locator} collection ${refused.identity} ${refused.rule}${where}`);
1736
+ }
1737
+ models.set(locator, state);
1738
+ return { collection: state };
1656
1739
  }
1657
- function publish(local, room, record, landing, live) {
1658
- return exclusive(async () => {
1740
+ function publish(local, room, reached, landing, live) {
1741
+ const landed = exclusive(async () => {
1659
1742
  if (!live()) {
1660
1743
  caughtUpTo(room.locator);
1661
1744
  return true;
1662
1745
  }
1663
1746
  const prior = replicas.get(room.locator);
1664
- if (landing.kind === 'refreshed' &&
1665
- (prior === undefined || prior.record.key !== record.key || prior.confirmed !== landing.base)) {
1666
- await local.unrefresh(record.key);
1747
+ if (!stands(room.locator, landing.from)) {
1748
+ if (landing.kind === 'refreshed') {
1749
+ await local.unrefresh(reached.key);
1750
+ return false;
1751
+ }
1752
+ caughtUpTo(room.locator);
1753
+ await abandoned(local, room.locator);
1667
1754
  return false;
1668
1755
  }
1756
+ const record = landing.kind === 'refreshed' && prior !== undefined
1757
+ ? {
1758
+ ...prior.record,
1759
+ roomVersion: reached.roomVersion,
1760
+ ...(landing.remodelled ? { model: reached.model } : {}),
1761
+ }
1762
+ : reached;
1669
1763
  adoptModels(room.locator, record);
1670
1764
  const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
1671
- const roomVersion = record.roomVersion;
1672
- const accepted = queue.filter((batch) => {
1673
- const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
1674
- return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
1675
- });
1676
1765
  let set;
1677
1766
  try {
1678
- set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
1767
+ set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
1679
1768
  ? {
1680
1769
  room: true,
1681
1770
  view: 'refreshed',
1682
- commit: { refreshed: true, composition: compositionOf(record) },
1771
+ commit: { refreshed: true },
1683
1772
  }
1684
1773
  : {
1685
1774
  room: true,
1686
1775
  view: landing.refreshed ? 'refreshed' : 'confirmed',
1687
1776
  commit: {
1688
1777
  published: { from: prior?.record.key ?? null },
1689
- composition: compositionOf(record),
1690
1778
  ...(landing.refreshed ? { refreshed: true } : {}),
1691
1779
  },
1692
1780
  });
1693
1781
  }
1694
1782
  catch (error) {
1695
- if (!exceeded(error))
1783
+ if (!refusedForSpace(error))
1696
1784
  throw error;
1697
- await exceededOn(room.locator, null);
1698
1785
  caughtUpTo(room.locator);
1699
1786
  return true;
1700
1787
  }
1701
1788
  if (landing.kind === 'bootstrapped') {
1702
- transfers.delete(room.locator);
1789
+ highest.delete(room.locator);
1790
+ beyondHorizon.delete(room.locator);
1791
+ untransferred(room.locator);
1792
+ startedOver.delete(room.locator);
1703
1793
  catching.delete(room.locator);
1704
- sweep();
1794
+ if (prior !== undefined && prior.record.key !== record.key)
1795
+ sweep();
1796
+ name(room.locator);
1705
1797
  }
1706
- for (const batch of accepted)
1707
- pendings.delete(pendingKey(room.locator, batch.id));
1708
1798
  stale.delete(room.locator);
1709
1799
  unpreparable.delete(room.locator);
1710
1800
  if (unreadable.delete(room.locator))
1711
1801
  scopedRefusals.delete(room.locator);
1712
- sourceRefusals.delete(room.locator);
1713
- declarationRefusals.delete(room.locator);
1714
1802
  list(room, true);
1715
1803
  publishChangeSet(set);
1716
1804
  await unpair(local);
1717
1805
  moveTo({ ...status, ...attention() });
1718
- options.adapter.roomStream?.published(room.locator, roomVersion);
1719
- if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1806
+ if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1720
1807
  submitLater(room.locator);
1721
1808
  }
1722
1809
  return true;
1723
1810
  });
1811
+ return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
1724
1812
  }
1725
1813
  async function savedRoom(locator) {
1726
1814
  const local = await openLocal();
1727
1815
  const replica = replicas.get(locator);
1728
1816
  const room = rooms.get(locator);
1729
1817
  if (replica === undefined || room === undefined) {
1730
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1818
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
1731
1819
  }
1732
1820
  return { local, replica, room };
1733
1821
  }
@@ -1745,12 +1833,7 @@ export function composeRuntime(options) {
1745
1833
  return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
1746
1834
  }
1747
1835
  async function saveParts(locator, named, writtenBy) {
1748
- const read = named.map(({ path, parts }) => ({ path, parts: readParts(parts) }));
1749
- if (read.some(({ path }, at) => path === '' ||
1750
- normalize(path) !== path ||
1751
- read.findIndex((one) => one.path === path) !== at)) {
1752
- throw runtimeError('room-draft-malformed');
1753
- }
1836
+ const read = readSavedParts(named);
1754
1837
  const target = await savedRoom(locator);
1755
1838
  const saved = [];
1756
1839
  const files = [];
@@ -1759,7 +1842,7 @@ export function composeRuntime(options) {
1759
1842
  for (const { path, parts } of read) {
1760
1843
  const file = parts.file === undefined
1761
1844
  ? undefined
1762
- : await heldFile(parts.file.blob, binaryBase(visible, path));
1845
+ : await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
1763
1846
  if (parts.file !== undefined && file !== undefined) {
1764
1847
  files.push({ key: file.localId, blob: parts.file.blob });
1765
1848
  }
@@ -1794,13 +1877,14 @@ export function composeRuntime(options) {
1794
1877
  const referring = removed.length === 0
1795
1878
  ? []
1796
1879
  : (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
1797
- const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
1880
+ const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
1798
1881
  const { visible } = await replayed(record, confirmedLayer, replica.queue);
1799
- const composition = compositionOf(record);
1882
+ const composition = composed(record);
1800
1883
  return {
1801
1884
  record,
1802
1885
  confirmed: confirmedLayer,
1803
1886
  visible,
1887
+ folders: new Set(await local.folders(record.key, 'visible', paths)),
1804
1888
  *visibleOver(queue) {
1805
1889
  return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
1806
1890
  },
@@ -1812,13 +1896,7 @@ export function composeRuntime(options) {
1812
1896
  return false;
1813
1897
  const local = await openLocal();
1814
1898
  const roots = record.typedSelection?.roots ?? [];
1815
- if (composition.model.status === 'unavailable' &&
1816
- (await local.typedWithin(record.key, 'visible', roots, false))) {
1817
- return true;
1818
- }
1819
- return (composition.selection !== null &&
1820
- composition.selection.model.status === 'unavailable' &&
1821
- (await local.typedWithin(record.key, 'visible', roots, true)));
1899
+ return local.typedWithin(record.key, 'visible', roots, false);
1822
1900
  }
1823
1901
  function textDiffers(left, right) {
1824
1902
  return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
@@ -1829,18 +1907,6 @@ export function composeRuntime(options) {
1829
1907
  return undefined;
1830
1908
  return entry.binary !== null ? null : entry.content;
1831
1909
  }
1832
- function walkOf(local, key, view) {
1833
- return async (after) => {
1834
- const page = await local.walk(key, view, after, WALK_BYTES);
1835
- return {
1836
- entries: page.entries.map(({ path, content, binary }) => ({
1837
- path,
1838
- content: binary !== null ? null : content,
1839
- })),
1840
- next: page.next,
1841
- };
1842
- };
1843
- }
1844
1910
  function lookupOf(local, key, view) {
1845
1911
  return async (paths) => {
1846
1912
  if (paths.length === 0)
@@ -1857,37 +1923,6 @@ export function composeRuntime(options) {
1857
1923
  const read = parseFrontmatter(entry.content ?? '');
1858
1924
  return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
1859
1925
  }
1860
- function overriddenWalk(walk, overrides) {
1861
- const sorted = [...overrides.keys()].sort();
1862
- const beyond = (path) => {
1863
- if (path === null)
1864
- return 0;
1865
- let low = 0;
1866
- let high = sorted.length;
1867
- while (low < high) {
1868
- const middle = (low + high) >>> 1;
1869
- if (sorted[middle] <= path)
1870
- low = middle + 1;
1871
- else
1872
- high = middle;
1873
- }
1874
- return low;
1875
- };
1876
- return async (after) => {
1877
- const page = await walk(after);
1878
- const end = page.next;
1879
- const added = sorted
1880
- .slice(beyond(after), end === null ? sorted.length : beyond(end))
1881
- .flatMap((path) => {
1882
- const entry = overrides.get(path);
1883
- return entry === null || entry === undefined
1884
- ? []
1885
- : [{ path, content: entry.binary !== null ? null : entry.content }];
1886
- });
1887
- const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
1888
- return { entries, next: end };
1889
- };
1890
- }
1891
1926
  function overriddenLookup(lookup, overrides) {
1892
1927
  return async (paths) => {
1893
1928
  const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
@@ -1905,9 +1940,8 @@ export function composeRuntime(options) {
1905
1940
  }
1906
1941
  function validatorOf(record, confirmedLayer) {
1907
1942
  return async (resulting, touched) => {
1908
- const composition = compositionFor(record);
1909
1943
  const local = await openLocal();
1910
- const changed = [];
1944
+ const changes = [];
1911
1945
  const overrides = new Map();
1912
1946
  const pace = paced();
1913
1947
  for (const path of new Set([...confirmedLayer.held, ...touched])) {
@@ -1918,19 +1952,13 @@ export function composeRuntime(options) {
1918
1952
  continue;
1919
1953
  overrides.set(path, now);
1920
1954
  if (textOf(was) !== textOf(now))
1921
- changed.push({ path, was: textOf(was), now: textOf(now) });
1955
+ changes.push({ path, was: textOf(was), now: textOf(now) });
1922
1956
  }
1923
- const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
1924
- const clean = available(composition) &&
1925
- changed.length <= CLEAN_CHANGES &&
1926
- (await changedCleanly(changed, compositionOf(record), composition, {
1927
- lookup,
1928
- texts: textsOf(local, record.key, 'confirmed'),
1929
- referrers: referrersOf(local, record.key, 'confirmed'),
1930
- }));
1931
- if (clean)
1932
- return { verdict: 'accept' };
1933
- return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
1957
+ return validatedNeighbourhood(changes, compositionFor(record), {
1958
+ lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
1959
+ texts: textsOf(local, record.key, 'confirmed'),
1960
+ referrers: referrersOf(local, record.key, 'confirmed'),
1961
+ });
1934
1962
  };
1935
1963
  }
1936
1964
  function textsOf(local, key, view) {
@@ -1954,6 +1982,7 @@ export function composeRuntime(options) {
1954
1982
  operations: room.operations,
1955
1983
  providerOwnedPaths: room.providerOwnedPaths,
1956
1984
  visible: saving.visible,
1985
+ folders: saving.folders,
1957
1986
  queued: queuedDocuments(replica.queue),
1958
1987
  unmodeled: await unmodeledIn(record),
1959
1988
  validate: validatorOf(record, saving.confirmed),
@@ -1974,6 +2003,7 @@ export function composeRuntime(options) {
1974
2003
  locator,
1975
2004
  key: saving.record.key,
1976
2005
  roots: saving.record.typedSelection?.roots ?? [],
2006
+ composed: composedOf(composed(saving.record)),
1977
2007
  overlay: diff.overlay,
1978
2008
  });
1979
2009
  }
@@ -2005,13 +2035,13 @@ export function composeRuntime(options) {
2005
2035
  }
2006
2036
  const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
2007
2037
  const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
2008
- const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, sourceSaving.visible)));
2038
+ const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
2009
2039
  if (changed.length > 0)
2010
2040
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2011
2041
  const target = replicas.get(destination);
2012
2042
  const listedRoom = rooms.get(destination);
2013
2043
  if (target === undefined || listedRoom === undefined) {
2014
- throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
2044
+ throw runtimeError('unknown-room', `Unknown Room ${destination}`);
2015
2045
  }
2016
2046
  const arriving = await savingIn(target, [], moves.map(({ to }) => to));
2017
2047
  const built = await buildMove({
@@ -2074,10 +2104,10 @@ export function composeRuntime(options) {
2074
2104
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2075
2105
  const { answer, touched } = committedBoth;
2076
2106
  if (answer.fenced !== null) {
2077
- throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2107
+ throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2078
2108
  }
2079
2109
  if (fences.has(locator) || fences.has(destination)) {
2080
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2110
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2081
2111
  }
2082
2112
  pendings.set(pendingKey(locator, id), {
2083
2113
  drafts: saved,
@@ -2117,6 +2147,8 @@ export function composeRuntime(options) {
2117
2147
  submitLater(locator);
2118
2148
  }
2119
2149
  async function committed(locator, target, saved, consumed, writtenBy, files = []) {
2150
+ if (full)
2151
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
2120
2152
  const into = movedInto(locator, saved);
2121
2153
  if (into !== null) {
2122
2154
  return committedMove(locator, into, target, saved, consumed, writtenBy, files);
@@ -2128,7 +2160,7 @@ export function composeRuntime(options) {
2128
2160
  ...saved.map(({ path }) => path),
2129
2161
  ...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
2130
2162
  ]);
2131
- const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, underneath(held, path, saving.visible)));
2163
+ const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
2132
2164
  if (changed.length > 0)
2133
2165
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2134
2166
  const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
@@ -2156,6 +2188,7 @@ export function composeRuntime(options) {
2156
2188
  providerLimits: limits,
2157
2189
  handle: room.handle,
2158
2190
  visible: saving.visible,
2191
+ folders: saving.folders,
2159
2192
  changes,
2160
2193
  bases: parts.flatMap(({ bases }) => bases),
2161
2194
  queued: queuedDocuments(replica.queue),
@@ -2245,10 +2278,10 @@ export function composeRuntime(options) {
2245
2278
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2246
2279
  const { answer, touched } = done;
2247
2280
  if (answer.fenced !== null) {
2248
- throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2281
+ throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2249
2282
  }
2250
2283
  if (fences.has(locator))
2251
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2284
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2252
2285
  for (const origin of withdrawn.keys())
2253
2286
  pendings.delete(pendingKey(locator, origin));
2254
2287
  for (const [id, drafts] of drafted) {
@@ -2297,7 +2330,7 @@ export function composeRuntime(options) {
2297
2330
  throw new Error('connection refused');
2298
2331
  await local.keepUpload(locator, identifier, upload);
2299
2332
  if (fences.has(locator))
2300
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2333
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2301
2334
  const key = pendingKey(locator, identifier);
2302
2335
  pendings.set(key, { ...pendings.get(key), upload });
2303
2336
  if (!continuing())
@@ -2306,6 +2339,9 @@ export function composeRuntime(options) {
2306
2339
  async function readHistory(locator, path, query = {}) {
2307
2340
  if (closed || refusal !== null)
2308
2341
  return UNCERTAIN;
2342
+ await firstDiscovery;
2343
+ if (!roomsNamed)
2344
+ return UNCERTAIN;
2309
2345
  try {
2310
2346
  await ensureAuthorization();
2311
2347
  }
@@ -2395,14 +2431,12 @@ export function composeRuntime(options) {
2395
2431
  return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
2396
2432
  };
2397
2433
  }
2398
- function binaryRoom(request) {
2399
- if (request.kind === 'path')
2400
- return request.locator;
2401
- return options.adapter.referrerScope(request.referrer)?.locator ?? null;
2402
- }
2403
2434
  async function transferBinary(request, transfer, answer) {
2404
2435
  if (closed || refusal !== null)
2405
2436
  return UNCERTAIN;
2437
+ await firstDiscovery;
2438
+ if (!roomsNamed)
2439
+ return UNCERTAIN;
2406
2440
  try {
2407
2441
  await ensureAuthorization();
2408
2442
  }
@@ -2412,16 +2446,13 @@ export function composeRuntime(options) {
2412
2446
  if (closed || authorization === undefined || refusal !== null)
2413
2447
  return UNCERTAIN;
2414
2448
  const since = fencings;
2415
- journal.record(request.kind === 'path'
2416
- ? `transfer binary ${request.locator} ${request.path}`
2417
- : `transfer binary ${request.fileRef}`);
2449
+ journal.record(`transfer binary ${request.locator} ${request.path}`);
2418
2450
  try {
2419
2451
  const result = await transfer(authorization);
2420
2452
  const settled = result;
2421
2453
  journal.record(`outcome binary ${settled.kind}`);
2422
- const locator = binaryRoom(request);
2423
- if (locator !== null && fencedSince(locator, since))
2424
- return failure({ kind: 'room-not-readable', locator });
2454
+ if (fencedSince(request.locator, since))
2455
+ return failure({ kind: 'room-not-readable', locator: request.locator });
2425
2456
  if (settled.kind === 'scoped-refusal') {
2426
2457
  const room = replicas.get(settled.locator)?.record;
2427
2458
  if (room !== undefined)
@@ -2436,23 +2467,27 @@ export function composeRuntime(options) {
2436
2467
  return UNCERTAIN;
2437
2468
  }
2438
2469
  }
2470
+ function fetchBinary(request, name) {
2471
+ return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2472
+ }
2439
2473
  async function readBinary(request, name) {
2440
2474
  if (closed)
2441
2475
  return UNCERTAIN;
2442
2476
  const held = await keptFiles.find(name);
2443
2477
  if (held !== null)
2444
2478
  return held;
2445
- return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2479
+ return fetchBinary(request, name);
2446
2480
  }
2447
2481
  async function binaryPath(locator, path, revision) {
2448
2482
  if (revision !== undefined)
2449
2483
  return { kind: 'path', locator, path, revision };
2450
2484
  const replica = replicas.get(locator);
2451
- if (replica === undefined)
2452
- return null;
2453
2485
  const local = await openLocal();
2454
- const [held] = await local.entries(replica.record.key, 'visible', [path]);
2455
- if (held?.binary === undefined || held.binary === null)
2486
+ const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
2487
+ const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
2488
+ if (attached !== undefined)
2489
+ return { kind: 'path', locator, path, revision: attached.revision };
2490
+ if (replica === undefined || held?.binary === undefined || held.binary === null)
2456
2491
  return null;
2457
2492
  if (held.binary.state === 'pending')
2458
2493
  return { kind: 'pending', localId: held.binary.localId };
@@ -2468,7 +2503,7 @@ export function composeRuntime(options) {
2468
2503
  const holds = (at) => {
2469
2504
  const kept = confirmedLayer.binaries[at];
2470
2505
  return (kept?.state === 'accepted' &&
2471
- kept.fileRef === record.fileRef &&
2506
+ kept.sha256 === record.sha256 &&
2472
2507
  confirmedLayer.revisions[at] === record.revision);
2473
2508
  };
2474
2509
  let confirmedAt = path;
@@ -2482,58 +2517,93 @@ export function composeRuntime(options) {
2482
2517
  locator,
2483
2518
  path: confirmedAt,
2484
2519
  revision: record.revision,
2485
- fileRef: record.fileRef,
2486
2520
  };
2487
2521
  }
2488
- async function readBinaryPath(locator, path, revision) {
2522
+ async function askedBinary(locator, path, revision) {
2489
2523
  await ready;
2490
2524
  const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2491
- const asked = drafted !== undefined && drafted.localId === revision
2525
+ return drafted !== undefined && drafted.localId === revision
2492
2526
  ? { kind: 'pending', localId: revision }
2493
- : await binaryPath(locator, path, revision);
2527
+ : binaryPath(locator, path, revision);
2528
+ }
2529
+ function pendingBinary(locator, path, localId) {
2530
+ const facts = queuedFacts(locator, path, localId);
2531
+ return facts === null
2532
+ ? absentBinary(path)
2533
+ : {
2534
+ kind: 'binary',
2535
+ file: localId,
2536
+ byteLength: facts.byteLength,
2537
+ mediaType: facts.mediaType,
2538
+ };
2539
+ }
2540
+ async function readBinaryPath(locator, path, revision) {
2541
+ const asked = await askedBinary(locator, path, revision);
2494
2542
  if (asked === null)
2495
2543
  return absentBinary(path);
2496
- if (asked.kind === 'pending') {
2497
- const facts = queuedFacts(locator, path, asked.localId);
2498
- return facts === null
2499
- ? absentBinary(path)
2500
- : {
2501
- kind: 'binary',
2502
- file: asked.localId,
2503
- byteLength: facts.byteLength,
2504
- mediaType: facts.mediaType,
2505
- };
2506
- }
2544
+ if (asked.kind === 'pending')
2545
+ return pendingBinary(locator, path, asked.localId);
2507
2546
  return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
2508
2547
  }
2548
+ async function heldMediaType(locator, path) {
2549
+ const replica = replicas.get(locator);
2550
+ const [held] = replica === undefined
2551
+ ? []
2552
+ : await (await openLocal()).entries(replica.record.key, 'visible', [path]);
2553
+ return held === undefined
2554
+ ? attachedFiles.get(locator)?.get(path)?.mediaType
2555
+ : held.binary?.mediaType;
2556
+ }
2557
+ function linkOf(asked, held) {
2558
+ return transferBinary(asked, (authorization) => options.adapter.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
2559
+ }
2509
2560
  async function linkBinaryPath(locator, path, revision) {
2510
2561
  await ready;
2511
2562
  const asked = await binaryPath(locator, path, revision);
2512
2563
  if (asked === null || asked.kind === 'pending')
2513
2564
  return absentBinary(path);
2514
- return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
2565
+ return linkOf(asked, revision === undefined ? await heldMediaType(locator, path) : undefined);
2515
2566
  }
2516
- async function readBinaryReference(reference) {
2567
+ async function showBinaryPath(locator, path, revision) {
2568
+ const asked = await askedBinary(locator, path, revision);
2569
+ if (asked === null)
2570
+ return absentBinary(path);
2571
+ if (asked.kind === 'pending')
2572
+ return pendingBinary(locator, path, asked.localId);
2573
+ if (closed)
2574
+ return UNCERTAIN;
2575
+ const name = { locator, path: asked.path, revision: asked.revision };
2576
+ const kept = await keptFiles.find(name);
2577
+ if (kept !== null)
2578
+ return kept;
2579
+ const mediaType = await heldMediaType(locator, path);
2580
+ if (mediaType?.startsWith('image/') === true)
2581
+ return fetchBinary(asked, name);
2582
+ return linkOf(asked, revision === undefined ? mediaType : undefined);
2583
+ }
2584
+ async function attachBinary(locator, file) {
2517
2585
  await ready;
2518
- const scope = options.adapter.referrerScope(reference.referrer);
2519
- if (scope === null)
2520
- return absentReference(reference.fileRef);
2521
- return readBinary({ kind: 'reference', fileRef: reference.fileRef, referrer: reference.referrer }, { locator: scope.locator, path: scope.path ?? '', revision: reference.fileRef });
2586
+ await firstDiscovery;
2587
+ if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
2588
+ return OFFLINE;
2589
+ const request = await attachedFile(locator, file);
2590
+ return transferBinary({ locator, path: file.name }, async (held) => {
2591
+ const result = await options.adapter.attach(held, request);
2592
+ if (result.kind === 'attached') {
2593
+ const landed = attachedFiles.get(locator) ?? new Map();
2594
+ landed.set(result.path, { revision: result.revision, mediaType: request.mediaType });
2595
+ attachedFiles.set(locator, landed);
2596
+ }
2597
+ return result;
2598
+ }, attachAnswer);
2522
2599
  }
2523
2600
  function readPart(file, part) {
2524
- return options.database.ask({ kind: 'file.part', key: file, part });
2601
+ return database.ask({ kind: 'file.part', key: file, part });
2525
2602
  }
2526
2603
  async function cleanUp() {
2527
- const freed = await options.database.ask({ kind: 'opened.clean' });
2604
+ const freed = await database.ask({ kind: 'opened.clean' });
2528
2605
  return freed + ((await options.shell.cleanUp?.()) ?? 0);
2529
2606
  }
2530
- function declaredAgents() {
2531
- for (const capability of capabilities) {
2532
- if (capability.conversationAgents !== undefined)
2533
- return capability.conversationAgents;
2534
- }
2535
- return undefined;
2536
- }
2537
2607
  function declaredAction(name) {
2538
2608
  for (const capability of capabilities) {
2539
2609
  for (const action of capability.actions ?? []) {
@@ -2585,6 +2655,24 @@ export function composeRuntime(options) {
2585
2655
  return UNCERTAIN;
2586
2656
  }
2587
2657
  }
2658
+ async function readConversationHistory(request) {
2659
+ await ready;
2660
+ const stream = options.adapter.conversations;
2661
+ if (closed || refusal !== null || stream?.history === undefined)
2662
+ return UNCERTAIN;
2663
+ journal.record('transfer conversation history');
2664
+ try {
2665
+ const result = await stream.history(request);
2666
+ if (result.kind === 'revoked-authorization')
2667
+ await refuse('revoked-authorization');
2668
+ journal.record(`outcome conversation history ${result.kind}`);
2669
+ return calledResult(result);
2670
+ }
2671
+ catch {
2672
+ journal.record('outcome conversation history transport-uncertainty');
2673
+ return UNCERTAIN;
2674
+ }
2675
+ }
2588
2676
  function unsettled(result) {
2589
2677
  return result.kind !== 'action' && result.retryable;
2590
2678
  }
@@ -2646,6 +2734,33 @@ export function composeRuntime(options) {
2646
2734
  const held = replicas.get(locator);
2647
2735
  return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
2648
2736
  }
2737
+ function shownAt(locator) {
2738
+ const held = replicas.get(locator)?.record.roomVersion;
2739
+ const raised = highest.get(locator);
2740
+ if (held === undefined || raised === undefined)
2741
+ return held ?? raised;
2742
+ return options.adapter.reached(locator, held, raised) ? held : raised;
2743
+ }
2744
+ function showing(locator, roomVersion) {
2745
+ const at = shownAt(locator);
2746
+ if (at === undefined || !options.adapter.reached(locator, at, roomVersion)) {
2747
+ highest.set(locator, roomVersion);
2748
+ }
2749
+ }
2750
+ function older(locator, roomVersion) {
2751
+ if (beyondHorizon.has(locator))
2752
+ return false;
2753
+ const at = shownAt(locator);
2754
+ return (at !== undefined &&
2755
+ options.adapter.reached(locator, at, roomVersion) &&
2756
+ !options.adapter.reached(locator, roomVersion, at));
2757
+ }
2758
+ function replaced(locators) {
2759
+ for (const locator of locators) {
2760
+ highest.delete(locator);
2761
+ beyondHorizon.add(locator);
2762
+ }
2763
+ }
2649
2764
  async function writtenAfter(locator, roomVersion, path) {
2650
2765
  const held = replicas.get(locator);
2651
2766
  if (held === undefined)
@@ -2656,169 +2771,233 @@ export function composeRuntime(options) {
2656
2771
  options.adapter.reached(locator, written, roomVersion) &&
2657
2772
  !options.adapter.reached(locator, roomVersion, written));
2658
2773
  }
2659
- function overriddenTexts(texts, overrides) {
2660
- return async (paths) => {
2661
- const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
2662
- for (const path of paths) {
2663
- const entry = overrides.get(path);
2664
- if (entry === undefined)
2665
- continue;
2666
- if (entry === null)
2667
- known.delete(path);
2668
- else
2669
- known.set(path, entry.binary !== null ? null : entry.content);
2670
- }
2671
- return known;
2672
- };
2673
- }
2674
- async function unreadableRoom(texts, walk, paths) {
2675
- if (paths !== null) {
2676
- const text = {};
2677
- for (const [path, content] of await texts(paths))
2678
- if (content !== null)
2679
- text[path] = content;
2680
- return stepped(unreadableIn(text, Object.keys(text)));
2681
- }
2682
- for (let after = null;;) {
2683
- const page = await walk(after);
2684
- const text = {};
2685
- for (const { path, content } of page.entries)
2686
- if (content !== null)
2687
- text[path] = content;
2688
- const refused = await stepped(unreadableIn(text, Object.keys(text)));
2689
- if (refused !== null)
2690
- return refused;
2691
- if (page.next === null)
2692
- return null;
2693
- after = page.next;
2694
- }
2695
- }
2696
- async function validatedRoom(local, record, view, previous, overrides = null) {
2697
- const composition = compositionOf(record);
2698
- const modeled = record.model.status !== 'unavailable' || record.selectionModel.status !== 'unavailable';
2699
- const lookup = overrides === null
2700
- ? lookupOf(local, record.key, view)
2701
- : overriddenLookup(lookupOf(local, record.key, view), overrides);
2702
- const texts = overrides === null
2703
- ? textsOf(local, record.key, view)
2704
- : overriddenTexts(textsOf(local, record.key, view), overrides);
2705
- const walk = overrides === null
2706
- ? walkOf(local, record.key, view)
2707
- : overriddenWalk(walkOf(local, record.key, view), overrides);
2708
- let changes = null;
2709
- let reshaped = false;
2710
- if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
2711
- const [was, now] = await Promise.all([
2712
- local.entries(record.key, 'confirmed', previous.changed),
2713
- overrides === null
2714
- ? local.entries(record.key, view, previous.changed)
2715
- : Promise.resolve([]),
2716
- ]);
2717
- const before = new Map(was.map((entry) => [entry.path, entry]));
2718
- const after = overrides === null
2719
- ? new Map(now.map((entry) => [entry.path, entry]))
2720
- : overrides;
2721
- changes = previous.changed.flatMap((path) => {
2722
- const one = before.get(path) ?? null;
2723
- const other = after.get(path) ?? null;
2724
- reshaped ||= textDiffers(one, other);
2725
- const held = textOf(one);
2726
- const next = textOf(other);
2727
- return held === next ? [] : [{ path, was: held, now: next }];
2728
- });
2729
- }
2730
- const named = changes === null ? null : changes.map(({ path }) => path);
2731
- const textChanged = changes === null || changes.length > 0 || reshaped;
2732
- const modelsChanged = previous === null || !sameComposition(compositionOf(previous.record), composition);
2733
- if (modeled && (textChanged || modelsChanged)) {
2734
- const clean = previous !== null &&
2735
- changes !== null &&
2736
- (await changedCleanly(changes, compositionOf(previous.record), composition, {
2737
- lookup,
2738
- texts,
2739
- referrers: referrersOf(local, record.key, 'confirmed'),
2740
- }));
2741
- if (clean)
2742
- return null;
2743
- const verdict = await validatedCollection(walk, lookup, composition);
2744
- if (verdict.verdict === 'accept')
2745
- return null;
2746
- if (verdict.rule !== 'model-unavailable')
2747
- return { kind: 'refused', refusal: verdict };
2748
- return unreadableRoom(texts, walk, named);
2749
- }
2750
- if (textChanged)
2751
- return unreadableRoom(texts, walk, named);
2752
- return null;
2774
+ async function writtenLater(local, locator, key, paths, roomVersion) {
2775
+ if (paths.length === 0 || !older(locator, roomVersion))
2776
+ return new Set();
2777
+ const entries = await local.entries(key, 'confirmed', paths);
2778
+ return new Set(entries.flatMap(({ path, written }) => written !== null &&
2779
+ options.adapter.reached(locator, written, roomVersion) &&
2780
+ !options.adapter.reached(locator, roomVersion, written)
2781
+ ? [path]
2782
+ : []));
2753
2783
  }
2754
- function refreshingOf(local, key, liveness) {
2784
+ function refreshingOf(local, locator, key, composition, liveness, replaying = false) {
2785
+ const write = replaying ? finishingWrite : exclusive;
2755
2786
  return {
2787
+ turn() {
2788
+ if (replaying)
2789
+ installing(locator);
2790
+ },
2756
2791
  prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
2792
+ later: (paths, roomVersion) => writtenLater(local, locator, key, paths, roomVersion),
2757
2793
  async stage(page) {
2758
- const batches = batched(page.entries);
2759
- for (const [index, entries] of batches.entries()) {
2760
- const last = index === batches.length - 1;
2761
- const staged = await exclusive(async () => {
2794
+ for (let written = 0;;) {
2795
+ const end = batchEnd(page.entries, written);
2796
+ const last = end === page.entries.length;
2797
+ const entries = page.entries.slice(written, end);
2798
+ const staged = await write(async () => {
2762
2799
  if (!liveness())
2763
2800
  return false;
2764
- await local.refresh(key, last
2801
+ await timed(stageBudget, () => local.refresh(key, composition, last
2765
2802
  ? { ...page, entries }
2766
- : { entries, removals: [], folders: [], folderRemovals: [] });
2803
+ : { entries, removals: [], folders: [], folderRemovals: [] }));
2767
2804
  return true;
2768
2805
  });
2769
2806
  if (!staged)
2770
2807
  return false;
2808
+ if (last)
2809
+ return true;
2810
+ written = end;
2771
2811
  }
2772
- return true;
2773
2812
  },
2774
2813
  };
2775
2814
  }
2776
- function batched(entries) {
2777
- const batches = [[]];
2815
+ function batchEnd(entries, from) {
2816
+ let end = from;
2778
2817
  let spent = 0;
2779
- for (const entry of entries) {
2780
- const size = (entry.content?.length ?? 0) + ROW_WEIGHT;
2781
- if (spent + size > STAGE_BYTES && batches.at(-1).length > 0) {
2782
- batches.push([]);
2783
- spent = 0;
2784
- }
2785
- batches.at(-1).push(entry);
2818
+ while (end < entries.length) {
2819
+ const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
2820
+ if (spent + size > stageBudget.bytes && end > from)
2821
+ break;
2786
2822
  spent += size;
2823
+ end += 1;
2824
+ }
2825
+ return end;
2826
+ }
2827
+ async function timed(budget, work) {
2828
+ const started = performance.now();
2829
+ const done = await work();
2830
+ const spent = performance.now() - started;
2831
+ if (spent < budget.aim / 2)
2832
+ budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
2833
+ else if (spent > budget.aim * 2) {
2834
+ budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
2835
+ }
2836
+ return done;
2837
+ }
2838
+ function installWrite(work) {
2839
+ return installTurn(installTurns.starting, work);
2840
+ }
2841
+ function finishingWrite(work) {
2842
+ return installTurn(installTurns.finishing, work);
2843
+ }
2844
+ function installTurn(queue, work) {
2845
+ return new Promise((resolve, reject) => {
2846
+ queue.push(() => void exclusive(work)
2847
+ .then(resolve, reject)
2848
+ .finally(() => {
2849
+ installTurns.busy = false;
2850
+ nextInstallTurn();
2851
+ }));
2852
+ nextInstallTurn();
2853
+ });
2854
+ }
2855
+ function nextInstallTurn() {
2856
+ if (installTurns.busy || installTurns.ahead > 0)
2857
+ return;
2858
+ const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
2859
+ if (turn === undefined)
2860
+ return;
2861
+ installTurns.busy = true;
2862
+ turn();
2863
+ }
2864
+ async function ahead(work) {
2865
+ let taken = false;
2866
+ const hold = {
2867
+ take() {
2868
+ if (taken)
2869
+ return;
2870
+ taken = true;
2871
+ installTurns.ahead += 1;
2872
+ },
2873
+ release() {
2874
+ if (!taken)
2875
+ return;
2876
+ taken = false;
2877
+ installTurns.ahead -= 1;
2878
+ resumeInstall();
2879
+ },
2880
+ };
2881
+ hold.take();
2882
+ try {
2883
+ return await work(hold);
2884
+ }
2885
+ finally {
2886
+ hold.release();
2887
+ }
2888
+ }
2889
+ function resumeInstall() {
2890
+ if (installTurns.ahead > 0)
2891
+ return;
2892
+ void yielded().then(nextInstallTurn);
2893
+ }
2894
+ async function heldStream() {
2895
+ while (streamsFree === 0)
2896
+ await new Promise((admit) => installWaiters.push(admit));
2897
+ streamsFree -= 1;
2898
+ return once(() => {
2899
+ streamsFree += 1;
2900
+ });
2901
+ }
2902
+ async function unqueued() {
2903
+ while (queuedText >= QUEUED_TEXT) {
2904
+ await new Promise((admit) => installWaiters.push(admit));
2787
2905
  }
2788
- return batches;
2789
2906
  }
2790
- async function pullRefresh(authorized, held, liveness) {
2907
+ function once(release) {
2908
+ let released = false;
2909
+ return () => {
2910
+ if (released)
2911
+ return;
2912
+ released = true;
2913
+ release();
2914
+ for (const admit of installWaiters.splice(0))
2915
+ admit();
2916
+ };
2917
+ }
2918
+ function installing(locator) {
2919
+ installs.add(locator);
2920
+ queueReadiness();
2921
+ }
2922
+ function untransferred(locator) {
2923
+ transfers.delete(locator);
2924
+ arrivals.delete(locator);
2925
+ streaming.delete(locator);
2926
+ underived.delete(locator);
2927
+ unindexed.delete(locator);
2928
+ }
2929
+ function setBack(locator) {
2930
+ const now = bytesOf(locator, true);
2931
+ const earlier = startedOver.get(locator);
2932
+ startedOver.set(locator, {
2933
+ done: Math.max(now.done, earlier?.done ?? 0),
2934
+ total: Math.max(now.total, earlier?.total ?? 0),
2935
+ });
2936
+ }
2937
+ function owedIndexes(locator, bytes, text) {
2938
+ if (bytes <= 0 || text <= 0)
2939
+ return;
2940
+ unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
2941
+ }
2942
+ function built(locator, { done, text }) {
2943
+ const owed = unindexed.get(locator) ?? [];
2944
+ let left = text;
2945
+ while (!done && left > 0 && owed.length > 0) {
2946
+ const [first] = owed;
2947
+ if (left >= first.text) {
2948
+ left -= first.text;
2949
+ owed.shift();
2950
+ continue;
2951
+ }
2952
+ owed[0] = {
2953
+ bytes: (first.bytes * (first.text - left)) / first.text,
2954
+ text: first.text - left,
2955
+ };
2956
+ left = 0;
2957
+ }
2958
+ if (done || owed.length === 0)
2959
+ unindexed.delete(locator);
2960
+ queueReadiness();
2961
+ }
2962
+ function installed(locator) {
2963
+ if (installs.delete(locator))
2964
+ queueReadiness();
2965
+ }
2966
+ async function pullRefresh(authorized, held, liveness, hold, refresh) {
2791
2967
  if (!liveness())
2792
2968
  return unconfirmed;
2793
- const turn = generation;
2794
2969
  const { record } = held;
2795
- const base = held.confirmed;
2970
+ const from = standingOf(record.locator);
2796
2971
  const local = await openLocal();
2797
2972
  try {
2798
2973
  await exclusive(() => local.unrefresh(record.key));
2799
- const result = await advancement(reads, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, liveness));
2974
+ const result = await advancement(async (authorization, request) => {
2975
+ hold.release();
2976
+ try {
2977
+ return await refresh(authorization, request);
2978
+ }
2979
+ finally {
2980
+ hold.take();
2981
+ }
2982
+ }, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.locator, record.key, composed(record), liveness));
2800
2983
  if (result.kind !== 'advanced') {
2801
2984
  journal.record(`transfer refresh ${record.locator} ${result.kind}`);
2802
2985
  return result;
2803
2986
  }
2804
- const refused = await validatedRoom(local, result.record, 'refreshed', {
2805
- record,
2806
- changed: [...result.changed],
2807
- });
2808
- if (refused !== null) {
2809
- journal.record(`transfer refresh ${record.locator} refused`);
2810
- return refused;
2811
- }
2812
2987
  journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
2813
- return { kind: 'advanced', record: result.record, base };
2988
+ return {
2989
+ kind: 'advanced',
2990
+ record: result.record,
2991
+ from,
2992
+ remodelled: result.record.model !== record.model,
2993
+ };
2814
2994
  }
2815
2995
  catch (error) {
2996
+ if (refusedForSpace(error))
2997
+ return unconfirmed;
2816
2998
  journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
2817
2999
  throw error;
2818
3000
  }
2819
- finally {
2820
- reads.finished(record.locator, turn);
2821
- }
2822
3001
  }
2823
3002
  const heldContents = new Map();
2824
3003
  function heldContentOf(local, key) {
@@ -2836,63 +3015,115 @@ export function composeRuntime(options) {
2836
3015
  heldContents.set(key, made);
2837
3016
  return made;
2838
3017
  }
3018
+ async function abandoned(local, locator) {
3019
+ await local.abandon(locator);
3020
+ setBack(locator);
3021
+ untransferred(locator);
3022
+ liveQueries.recover();
3023
+ sweep();
3024
+ }
2839
3025
  function staging(local, liveness) {
3026
+ const derived = async (locator, key, composition) => {
3027
+ underived.delete(locator);
3028
+ for (;;) {
3029
+ if (!liveness())
3030
+ return false;
3031
+ const done = await finishingWrite(async () => liveness()
3032
+ ? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
3033
+ : null);
3034
+ if (done === null)
3035
+ return false;
3036
+ built(locator, done);
3037
+ if (done.done)
3038
+ return true;
3039
+ }
3040
+ };
2840
3041
  return {
2841
3042
  held: (locator) => transfers.get(locator) ?? null,
3043
+ async turn(locator, step) {
3044
+ installing(locator);
3045
+ return step === 'downloading' ? heldStream() : () => { };
3046
+ },
3047
+ unqueued,
3048
+ queued(text) {
3049
+ queuedText += text;
3050
+ return once(() => {
3051
+ queuedText -= text;
3052
+ });
3053
+ },
3054
+ arriving(locator) {
3055
+ let ended = false;
3056
+ return {
3057
+ at(bytes) {
3058
+ if (ended)
3059
+ return;
3060
+ arrivals.set(locator, bytes);
3061
+ streaming.add(locator);
3062
+ const now = performance.now();
3063
+ if (now - arrivalReported < ARRIVAL_REPORT_MS)
3064
+ return;
3065
+ arrivalReported = now;
3066
+ queueReadiness();
3067
+ },
3068
+ end() {
3069
+ if (ended)
3070
+ return;
3071
+ ended = true;
3072
+ if (streaming.delete(locator))
3073
+ queueReadiness();
3074
+ },
3075
+ };
3076
+ },
3077
+ streamed: options.adapter.roomStream !== undefined,
2842
3078
  async stage(record, page, composition) {
2843
- const bytes = pageBytes(page);
2844
- const batches = batched(page.entries);
2845
- let key = record.key;
2846
- for (const [index, entries] of batches.entries()) {
2847
- const last = index === batches.length - 1;
2848
- const staged = await exclusive(async () => {
2849
- if (!liveness())
2850
- return null;
2851
- if (index === 0 && !(await admitted(local, record.locator, bytes)))
2852
- return null;
2853
- try {
2854
- return await local.stage(record, composition, { entries, folders: last ? page.folders : [] }, key, last);
2855
- }
2856
- catch (error) {
2857
- if (!exceeded(error))
2858
- throw error;
2859
- await exceededOn(record.locator, bytes);
2860
- return null;
2861
- }
2862
- });
2863
- if (staged === null)
3079
+ const { locator } = record;
3080
+ installing(locator);
3081
+ if (!liveness())
3082
+ return null;
3083
+ const key = await installWrite(async () => {
3084
+ if (!liveness())
2864
3085
  return null;
2865
- key = staged;
2866
- }
2867
- transfers.set(record.locator, { ...record, key });
3086
+ return local.stage(record, composition, page);
3087
+ });
3088
+ if (key === null || !liveness())
3089
+ return null;
3090
+ const arrived = !transfers.has(locator);
3091
+ const prior = transfers.get(locator)?.done.bytes ?? 0;
3092
+ const held = { ...record, key };
3093
+ transfers.set(locator, held);
3094
+ if (arrived)
3095
+ liveQueries.recover();
3096
+ installing(locator);
2868
3097
  liveQueries.recover();
2869
- return key;
3098
+ const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
3099
+ owedIndexes(locator, record.done.bytes - prior, text);
3100
+ const owed = text + (underived.get(locator) ?? 0);
3101
+ underived.set(locator, owed);
3102
+ if (owed < deriveBudget.bytes)
3103
+ return held;
3104
+ return (await derived(locator, key, composition)) ? held : null;
2870
3105
  },
2871
- async refreshing(key) {
3106
+ derive: derived,
3107
+ async refreshing(locator, key, composition) {
2872
3108
  await exclusive(() => local.unrefresh(key));
2873
- return refreshingOf(local, key, liveness);
3109
+ return refreshingOf(local, locator, key, composition, liveness, true);
2874
3110
  },
2875
3111
  abandon: (locator) => exclusive(async () => {
2876
- if (!liveness())
2877
- return;
2878
- await local.abandon(locator);
2879
- transfers.delete(locator);
2880
- liveQueries.recover();
2881
- sweep();
3112
+ if (liveness())
3113
+ await abandoned(local, locator);
2882
3114
  }),
2883
- setAside: (locator) => exclusive(async () => {
3115
+ setAside: async (locator) => {
2884
3116
  if (!liveness())
2885
3117
  return;
2886
- await local.abandon(locator, true);
2887
- transfers.delete(locator);
3118
+ setBack(locator);
3119
+ untransferred(locator);
2888
3120
  liveQueries.recover();
2889
- }),
3121
+ },
2890
3122
  release: (key) => exclusive(async () => {
2891
3123
  await local.release(key);
2892
3124
  sweep();
2893
3125
  }),
2894
3126
  content: (key) => heldContentOf(local, key),
2895
- validate: (record, view) => validatedRoom(local, record, view, null),
2896
3127
  };
2897
3128
  }
2898
3129
  function reusable(room, local) {
@@ -2904,30 +3135,35 @@ export function composeRuntime(options) {
2904
3135
  async function pullBootstrap(authorized, room, local, liveness) {
2905
3136
  if (!liveness())
2906
3137
  return unconfirmed;
2907
- const turn = generation;
2908
3138
  if (replicas.has(room.locator)) {
2909
3139
  catching.add(room.locator);
2910
3140
  liveQueries.recover();
2911
3141
  }
2912
3142
  try {
2913
- const result = await bootstrapRoom(reads, authorized, room, {
2914
- collection: modelOf(room.locator),
2915
- selection: modelOf(room.locator, 'typed-selection'),
2916
- }, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
3143
+ const result = await bootstrapRoom(reads, authorized, room, { collection: modelOf(room.locator) }, builtIn, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
2917
3144
  const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
2918
3145
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
2919
- if (result.kind !== 'staged')
3146
+ if (result.kind === 'staged' && older(room.locator, result.record.roomVersion)) {
3147
+ caughtUpTo(room.locator);
3148
+ installed(room.locator);
3149
+ await abandoned(local, room.locator);
3150
+ journal.record(`transfer bootstrap ${room.locator} older than shown`);
3151
+ return { kind: 'transport-uncertainty' };
3152
+ }
3153
+ if (result.kind !== 'staged') {
2920
3154
  caughtUpTo(room.locator);
3155
+ installed(room.locator);
3156
+ }
2921
3157
  return result;
2922
3158
  }
2923
3159
  catch (error) {
2924
- journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
2925
3160
  caughtUpTo(room.locator);
3161
+ installed(room.locator);
3162
+ if (refusedForSpace(error))
3163
+ return unconfirmed;
3164
+ journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
2926
3165
  throw error;
2927
3166
  }
2928
- finally {
2929
- reads.finished(room.locator, turn);
2930
- }
2931
3167
  }
2932
3168
  function caughtUpTo(locator) {
2933
3169
  if (catching.delete(locator))
@@ -2935,72 +3171,83 @@ export function composeRuntime(options) {
2935
3171
  }
2936
3172
  function behind(room) {
2937
3173
  const held = replicas.get(room.locator)?.record.roomVersion;
2938
- return (held !== undefined &&
2939
- [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
3174
+ return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
2940
3175
  }
2941
- async function advance(room, local, authorized, live) {
2942
- const carried = reads.carried(room.locator);
2943
- const withheld = await advanceOnce(room, local, authorized, live);
2944
- if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
2945
- return withheld;
2946
- }
2947
- return advanceOnce(room, local, authorized, live);
3176
+ function standingOf(locator) {
3177
+ const held = replicas.get(locator);
3178
+ return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
2948
3179
  }
2949
- async function advanceOnce(room, local, authorized, live) {
2950
- if (!live() || paused.has(room.locator))
3180
+ function stands(locator, from) {
3181
+ const now = standingOf(locator);
3182
+ return now.key === from.key && now.confirmed === from.confirmed;
3183
+ }
3184
+ async function advance(room, local, authorized, running) {
3185
+ const live = () => running() && !full;
3186
+ if (!live())
2951
3187
  return unconfirmed;
2952
3188
  const held = replicas.get(room.locator);
2953
- if (held !== undefined) {
2954
- const refreshed = await pullRefresh(authorized, held, live);
2955
- if (!live())
2956
- return unconfirmed;
2957
- if (refreshed.kind === 'still-current') {
2958
- stale.delete(room.locator);
2959
- if (sourceRefusals.has(room.locator))
2960
- return null;
2961
- if (unreadable.delete(room.locator)) {
2962
- scopedRefusals.delete(room.locator);
2963
- list(room, true);
2964
- emitRooms();
2965
- }
2966
- return null;
2967
- }
2968
- if (refreshed.kind === 'advanced') {
2969
- const landing = { kind: 'refreshed', base: refreshed.base };
2970
- if (!(await publish(local, room, refreshed.record, landing, live))) {
2971
- const now = replicas.get(room.locator)?.record.roomVersion;
2972
- const reached = now !== undefined &&
2973
- options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
2974
- return reached ? null : advance(room, local, authorized, live);
2975
- }
2976
- await retainModel(local, room, 'collection', modelOf(room.locator));
2977
- return null;
2978
- }
2979
- if (refreshed.kind !== 'beyond-horizon')
3189
+ if (held !== undefined && options.adapter.roomStream !== undefined) {
3190
+ stale.delete(room.locator);
3191
+ name(room.locator);
3192
+ return null;
3193
+ }
3194
+ const refresh = reads.refresh;
3195
+ if (held !== undefined && refresh !== undefined) {
3196
+ const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
3197
+ if (refreshed === 'behind')
3198
+ return advance(room, local, authorized, live);
3199
+ if (refreshed !== 'beyond-horizon')
2980
3200
  return refreshed;
2981
- if (opened !== null) {
2982
- owed.add(room.locator);
2983
- liveQueries.recover();
2984
- return unconfirmed;
2985
- }
2986
- if (reloadsDue.get(room.locator) === generation)
3201
+ if (reloadsDue.has(room.locator))
2987
3202
  return unconfirmed;
3203
+ replaced([room.locator]);
2988
3204
  }
3205
+ const from = standingOf(room.locator);
2989
3206
  const bootstrapped = await pullBootstrap(authorized, room, local, live);
3207
+ if (bootstrapped.kind !== 'staged')
3208
+ return live() ? bootstrapped : unconfirmed;
3209
+ const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
3210
+ if (await publish(local, room, bootstrapped.record, landing, live)) {
3211
+ return live() ? null : unconfirmed;
3212
+ }
3213
+ return reachedBy(room.locator, bootstrapped.record.roomVersion)
3214
+ ? null
3215
+ : advance(room, local, authorized, live);
3216
+ }
3217
+ async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
3218
+ const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
2990
3219
  if (!live())
2991
3220
  return unconfirmed;
2992
- if (bootstrapped.kind !== 'staged')
2993
- return bootstrapped;
2994
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
2995
- return null;
3221
+ if (refreshed.kind === 'still-current') {
3222
+ stale.delete(room.locator);
3223
+ if (unreadable.delete(room.locator)) {
3224
+ scopedRefusals.delete(room.locator);
3225
+ list(room, true);
3226
+ emitRooms();
3227
+ }
3228
+ return null;
3229
+ }
3230
+ if (refreshed.kind === 'advanced') {
3231
+ const landing = {
3232
+ kind: 'refreshed',
3233
+ from: refreshed.from,
3234
+ remodelled: refreshed.remodelled,
3235
+ };
3236
+ if (!(await publish(local, room, refreshed.record, landing, live))) {
3237
+ const now = replicas.get(room.locator)?.record.roomVersion;
3238
+ const reached = now !== undefined &&
3239
+ options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3240
+ return reached ? null : 'behind';
3241
+ }
3242
+ await retainModel(local, room, modelOf(room.locator));
3243
+ return null;
3244
+ }
3245
+ return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
2996
3246
  }
2997
3247
  async function withhold(withheld, room, local, progress) {
2998
- if (withheld.kind === 'unconfirmed')
3248
+ if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
2999
3249
  return;
3000
- if (withheld.kind === 'refused')
3001
- stale.delete(room.locator);
3002
- else
3003
- stale.add(room.locator);
3250
+ stale.add(room.locator);
3004
3251
  if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
3005
3252
  const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
3006
3253
  repairable.set(room.locator, reset);
@@ -3022,30 +3269,10 @@ export function composeRuntime(options) {
3022
3269
  list(room, false);
3023
3270
  emitRooms();
3024
3271
  }
3025
- else if (withheld.kind === 'refused') {
3026
- refusedSource(room, withheld);
3027
- }
3028
3272
  else if (withheld.kind === 'revoked-authorization') {
3029
3273
  await refuse('revoked-authorization');
3030
3274
  }
3031
3275
  }
3032
- function refusedSource(room, { refusal }) {
3033
- sourceRefusals.set(room.locator, {
3034
- code: refusal.rule,
3035
- retryable: false,
3036
- fields: {
3037
- file: refusal.file,
3038
- ...(refusal.location === undefined ? {} : { location: refusal.location }),
3039
- ...(refusal.referrers === undefined ? {} : { referrers: refusal.referrers }),
3040
- },
3041
- });
3042
- declarationRefusals.delete(room.locator);
3043
- unreadable.add(room.locator);
3044
- list(room, false);
3045
- emitRooms();
3046
- moveTo({ ...status, ...attention() });
3047
- journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
3048
- }
3049
3276
  function leaving(batch, result) {
3050
3277
  if (result.kind === 'expired')
3051
3278
  return { batch, returned: returnedBy('expired') };
@@ -3067,8 +3294,8 @@ export function composeRuntime(options) {
3067
3294
  function settling(acceptance) {
3068
3295
  return acceptance !== null && 'apply' in acceptance;
3069
3296
  }
3070
- function refusalOf(acceptance) {
3071
- return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
3297
+ function failureOf(acceptance) {
3298
+ return settling(acceptance) ? null : acceptance;
3072
3299
  }
3073
3300
  async function acceptance(room, local, submitted, accepted) {
3074
3301
  if (!continuing())
@@ -3081,7 +3308,8 @@ export function composeRuntime(options) {
3081
3308
  logAccepted(room.locator, submitted, accepted);
3082
3309
  let record = current.record;
3083
3310
  let changes;
3084
- if (!options.adapter.reached(room.locator, held, accepted.roomVersion)) {
3311
+ if (!options.adapter.reached(room.locator, held, accepted.roomVersion) &&
3312
+ !older(room.locator, accepted.roomVersion)) {
3085
3313
  const paths = [...returnedPaths(submitted, accepted)];
3086
3314
  const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
3087
3315
  const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
@@ -3091,34 +3319,6 @@ export function composeRuntime(options) {
3091
3319
  ? accepted.roomVersion
3092
3320
  : held,
3093
3321
  };
3094
- const overrides = new Map([
3095
- ...back.set.map((entry) => [entry.path, entry]),
3096
- ...back.removed.map((path) => [path, null]),
3097
- ]);
3098
- const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
3099
- if (refused !== null) {
3100
- const key = pendingKey(room.locator, submitted.id);
3101
- return {
3102
- locator: room.locator,
3103
- left: [],
3104
- commit: {
3105
- rooms: [],
3106
- writes: [
3107
- {
3108
- locator: room.locator,
3109
- accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
3110
- },
3111
- ],
3112
- },
3113
- refused,
3114
- apply() {
3115
- const state = pendings.get(key);
3116
- if (state !== undefined && !fences.has(room.locator))
3117
- pendings.set(key, { ...state, accepted: accepted.roomVersion });
3118
- return changeSet(room.locator, { queue: true });
3119
- },
3120
- };
3121
- }
3122
3322
  changes = back;
3123
3323
  }
3124
3324
  const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
@@ -3126,10 +3326,9 @@ export function composeRuntime(options) {
3126
3326
  ...settled,
3127
3327
  apply(answer) {
3128
3328
  const set = settled.apply(answer);
3329
+ showing(room.locator, accepted.roomVersion);
3129
3330
  pendings.delete(pendingKey(room.locator, submitted.id));
3130
3331
  unreadable.delete(room.locator);
3131
- sourceRefusals.delete(room.locator);
3132
- declarationRefusals.delete(room.locator);
3133
3332
  list(room, true);
3134
3333
  return set;
3135
3334
  },
@@ -3147,13 +3346,9 @@ export function composeRuntime(options) {
3147
3346
  const failed = await exclusive(async () => {
3148
3347
  const adopting = await acceptance(room, local, submitted, accepted);
3149
3348
  await adoptedTogether(local, [adopting]);
3150
- return refusalOf(adopting);
3349
+ return failureOf(adopting);
3151
3350
  });
3152
- if (failed === null)
3153
- return true;
3154
- if (failed.kind === 'refused' && continuing())
3155
- refusedSource(room, failed);
3156
- return false;
3351
+ return failed === null;
3157
3352
  }
3158
3353
  async function adoptedMove(room, partner, local, submitted, accepted) {
3159
3354
  const other = discovered.get(partner);
@@ -3167,18 +3362,10 @@ export function composeRuntime(options) {
3167
3362
  ...accepted.destination,
3168
3363
  });
3169
3364
  await adoptedTogether(local, [left, arrived]);
3170
- const refusedArrival = refusalOf(arrived);
3171
- if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
3172
- refusedSource(other, refusedArrival);
3173
- }
3174
- return refusalOf(left);
3365
+ return failureOf(left);
3175
3366
  });
3176
3367
  submitLater(partner);
3177
- if (failed === null)
3178
- return true;
3179
- if (failed.kind === 'refused' && continuing())
3180
- refusedSource(room, failed);
3181
- return false;
3368
+ return failed === null;
3182
3369
  }
3183
3370
  function queueIn(locator) {
3184
3371
  return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
@@ -3301,8 +3488,6 @@ export function composeRuntime(options) {
3301
3488
  if (replica === undefined || head === undefined)
3302
3489
  return 'done';
3303
3490
  const state = pendings.get(pendingKey(room.locator, head.id));
3304
- if (state?.accepted !== undefined)
3305
- return 'done';
3306
3491
  const partner = partnerOf(head, room.locator);
3307
3492
  if (partner !== null && !leavingIn(head, room.locator)) {
3308
3493
  submitLater(partner);
@@ -3416,12 +3601,12 @@ export function composeRuntime(options) {
3416
3601
  }
3417
3602
  if (result.kind === 'expired') {
3418
3603
  await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
3419
- const there = other === undefined ? null : await advance(other, local, authorized, continuing);
3604
+ const there = other === undefined ? null : await advanced(other, local, authorized);
3420
3605
  if (other !== undefined && there !== null) {
3421
3606
  await withhold(there, other, local, progress);
3422
3607
  return 'done';
3423
3608
  }
3424
- const withheld = await advance(room, local, authorized, continuing);
3609
+ const withheld = await advanced(room, local, authorized);
3425
3610
  if (withheld !== null) {
3426
3611
  await withhold(withheld, room, local, progress);
3427
3612
  return 'done';
@@ -3443,8 +3628,8 @@ export function composeRuntime(options) {
3443
3628
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
3444
3629
  const left = leaving(head, result);
3445
3630
  const refreshed = async () => ({
3446
- there: other === undefined ? null : await advance(other, local, authorized, continuing),
3447
- here: await advance(room, local, authorized, continuing),
3631
+ there: other === undefined ? null : await advanced(other, local, authorized),
3632
+ here: await advanced(room, local, authorized),
3448
3633
  });
3449
3634
  const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
3450
3635
  if (judged !== null && (judged.there !== null || judged.here !== null)) {
@@ -3515,6 +3700,7 @@ export function composeRuntime(options) {
3515
3700
  locator: room.locator,
3516
3701
  key: before.record.key,
3517
3702
  roots: before.record.typedSelection?.roots ?? [],
3703
+ composed: composedOf(composed(before.record)),
3518
3704
  overlay: { set: [], cleared: [...before.overlaid] },
3519
3705
  },
3520
3706
  ]),
@@ -3547,39 +3733,37 @@ export function composeRuntime(options) {
3547
3733
  if (signal.aborted)
3548
3734
  return;
3549
3735
  const progress = { complete: true, reachable: true };
3550
- await roomTask(room.locator, async (live) => {
3551
- await Promise.race([
3552
- reload(room, local, progress, () => live() && !signal.aborted),
3553
- abandonedOn(signal),
3554
- ]);
3555
- });
3736
+ await roomTask(room.locator, (live) => reload(room, local, progress, live));
3556
3737
  if (signal.aborted)
3557
3738
  return;
3558
3739
  await rest(progress);
3559
3740
  }
3560
3741
  }
3561
3742
  function awaitingAnswer(locator) {
3743
+ answering.get(locator)?.release();
3562
3744
  let release = () => { };
3563
- const awaited = new Promise((resolve) => {
3745
+ const answered = new Promise((resolve) => {
3564
3746
  release = resolve;
3565
3747
  });
3566
- answering.set(locator, { generation, answered: awaited });
3748
+ const awaited = { answered, release };
3749
+ answering.set(locator, awaited);
3567
3750
  return () => {
3568
- if (answering.get(locator)?.answered === awaited)
3751
+ if (answering.get(locator) === awaited)
3569
3752
  answering.delete(locator);
3570
3753
  release();
3571
3754
  };
3572
3755
  }
3573
- async function retainModel(local, room, attached, next) {
3574
- const key = modelKey(room.locator, attached);
3575
- const previous = modelOf(room.locator, attached);
3576
- const heldBefore = replicas.get(room.locator)?.record;
3577
- if (JSON.stringify(previous) === JSON.stringify(next) &&
3578
- (heldBefore === undefined ||
3579
- (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
3580
- JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
3581
- JSON.stringify(heldBefore.typedSelection) ===
3582
- JSON.stringify(room.typedSelection ?? null))))
3756
+ function declaredAsHeld(room) {
3757
+ const held = replicas.get(room.locator)?.record;
3758
+ return (held === undefined ||
3759
+ (JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
3760
+ JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
3761
+ JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
3762
+ }
3763
+ async function retainModel(local, room, next) {
3764
+ const key = room.locator;
3765
+ const previous = modelOf(room.locator);
3766
+ if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(room))
3583
3767
  return;
3584
3768
  if (next.status === 'unavailable') {
3585
3769
  invalidModels.add(key);
@@ -3591,26 +3775,13 @@ export function composeRuntime(options) {
3591
3775
  const held = replicas.get(room.locator);
3592
3776
  let changed = null;
3593
3777
  if (held !== undefined) {
3594
- const declared = {
3778
+ const record = {
3595
3779
  ...held.record,
3596
3780
  operations: room.operations,
3597
3781
  replica: room.replica,
3598
3782
  typedSelection: room.typedSelection ?? null,
3599
- ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
3783
+ model: next,
3600
3784
  };
3601
- let read = 'staged';
3602
- if (!sameMembership(declared, held.record)) {
3603
- const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), compositionOf(declared));
3604
- read =
3605
- verdict.verdict !== 'refuse'
3606
- ? 'staged'
3607
- : verdict.rule === 'model-unavailable'
3608
- ? 'deferred'
3609
- : { kind: 'refused', refusal: verdict };
3610
- }
3611
- const record = read === 'staged'
3612
- ? declared
3613
- : { ...declared, typedSelection: held.record.typedSelection };
3614
3785
  const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
3615
3786
  const replayedLayer = await replayed(record, confirmedLayer, held.queue);
3616
3787
  const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
@@ -3621,41 +3792,33 @@ export function composeRuntime(options) {
3621
3792
  key: record.key,
3622
3793
  record: storedRecord(record),
3623
3794
  roots: record.typedSelection?.roots ?? [],
3624
- composition: compositionOf(record),
3795
+ composed: composedOf(composed(record)),
3625
3796
  overlay: diff.overlay,
3626
3797
  },
3627
3798
  ],
3628
3799
  });
3629
- recomposed(room.locator, held.record, record);
3630
- const { count } = committedRoom(room.locator, record, answer);
3631
- replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count });
3800
+ const { count, indexed } = committedRoom(room.locator, answer);
3801
+ replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
3632
3802
  changed = changeSet(room.locator, {
3633
3803
  documents: answer.changed[room.locator] ?? [],
3634
3804
  room: true,
3635
3805
  });
3636
- if (read === 'staged' || read === 'deferred') {
3637
- if (declarationRefusals.delete(room.locator)) {
3638
- sourceRefusals.delete(room.locator);
3639
- unreadable.delete(room.locator);
3640
- }
3641
- }
3642
- else {
3643
- refusedSource(room, read);
3644
- declarationRefusals.add(room.locator);
3645
- }
3646
3806
  }
3647
- models.set(key, next);
3648
- if (next.status === 'current') {
3649
- liveModels.set(key, JSON.stringify(next.snapshot));
3650
- invalidModels.delete(key);
3651
- }
3652
- list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
3807
+ heldModel(room, next);
3653
3808
  if (changed === null)
3654
3809
  emitRooms();
3655
3810
  else
3656
3811
  publishChangeSet(changed);
3657
3812
  });
3658
3813
  }
3814
+ function heldModel(room, next) {
3815
+ models.set(room.locator, next);
3816
+ if (next.status === 'current') {
3817
+ liveModels.set(room.locator, JSON.stringify(next.snapshot));
3818
+ invalidModels.delete(room.locator);
3819
+ }
3820
+ list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
3821
+ }
3659
3822
  async function scoped(result, room, local) {
3660
3823
  if (result.scope === 'content' && !result.retryable) {
3661
3824
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
@@ -3669,14 +3832,14 @@ export function composeRuntime(options) {
3669
3832
  });
3670
3833
  let scopedAway = room;
3671
3834
  if (result.scope === 'model')
3672
- await retainModel(local, room, 'collection', {
3835
+ await retainModel(local, room, {
3673
3836
  status: 'unavailable',
3674
3837
  reason: 'not-readable',
3675
3838
  });
3676
3839
  else if (result.scope !== 'operation') {
3677
3840
  unreadable.add(room.locator);
3678
3841
  scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
3679
- await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
3842
+ await retainModel(local, scopedAway, modelOf(room.locator));
3680
3843
  }
3681
3844
  if (!continuing())
3682
3845
  return;
@@ -3684,6 +3847,17 @@ export function composeRuntime(options) {
3684
3847
  emitRooms();
3685
3848
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
3686
3849
  }
3850
+ function name(locator) {
3851
+ const stream = options.adapter.roomStream;
3852
+ const record = replicas.get(locator)?.record;
3853
+ const room = discovered.get(locator);
3854
+ if (stream === undefined || record === undefined || room === undefined)
3855
+ return;
3856
+ if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
3857
+ return;
3858
+ naming += 1;
3859
+ stream.hold([{ locator, roomVersion: record.roomVersion }]);
3860
+ }
3687
3861
  function heldRooms(listed) {
3688
3862
  return listed.flatMap((room) => {
3689
3863
  const record = replicas.get(room.locator)?.record;
@@ -3701,14 +3875,19 @@ export function composeRuntime(options) {
3701
3875
  return outcome?.status === 'fulfilled' && outcome.value;
3702
3876
  });
3703
3877
  if (attempt === 'opened') {
3878
+ naming += 1;
3704
3879
  options.adapter.roomStream?.hold(heldRooms(ready));
3705
3880
  handed = true;
3706
3881
  }
3707
- discovering?.release();
3708
- const loaded = await Promise.allSettled(ready
3709
- .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
3710
- .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
3711
- const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
3882
+ for (const room of ready) {
3883
+ if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
3884
+ if (attempt === 'authorized')
3885
+ name(room.locator);
3886
+ continue;
3887
+ }
3888
+ void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
3889
+ }
3890
+ const failed = prepared.find((room) => room.status === 'rejected');
3712
3891
  if (failed !== undefined)
3713
3892
  throw failed.reason;
3714
3893
  }
@@ -3726,27 +3905,29 @@ export function composeRuntime(options) {
3726
3905
  return false;
3727
3906
  }
3728
3907
  if (prior !== undefined)
3729
- await retainModel(local, room, 'collection', modelOf(room.locator));
3908
+ await retainModel(local, room, modelOf(room.locator));
3730
3909
  if (discoveryPaused || authorization === undefined)
3731
3910
  return false;
3732
3911
  return room.replica.admission === 'admitted';
3733
3912
  }
3734
3913
  async function caughtUp(room, local, progress, live) {
3735
- if (authorization === undefined || !live() || !behind(room))
3914
+ if (options.adapter.roomStream !== undefined || authorization === undefined)
3915
+ return;
3916
+ if (!live() || !behind(room))
3736
3917
  return;
3737
3918
  const withheld = await advance(room, local, authorization, live);
3738
3919
  if (withheld !== null)
3739
3920
  await withhold(withheld, room, local, progress);
3740
3921
  }
3741
3922
  function revoked(locator) {
3742
- return ['collection', 'typed-selection'].some((attached) => {
3743
- const model = modelOf(locator, attached);
3744
- return model.status === 'unavailable' && model.reason === 'revoked';
3745
- });
3923
+ const model = modelOf(locator);
3924
+ return model.status === 'unavailable' && model.reason === 'revoked';
3746
3925
  }
3747
3926
  async function loadRoom(room, local, progress, live) {
3748
3927
  if (!live() || authorization === undefined)
3749
3928
  return;
3929
+ if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
3930
+ return;
3750
3931
  if (replicas.has(room.locator) && revoked(room.locator)) {
3751
3932
  await reload(room, local, progress, live);
3752
3933
  return;
@@ -3764,21 +3945,53 @@ export function composeRuntime(options) {
3764
3945
  reloadsDue.delete(room.locator);
3765
3946
  if (!live() || authorization === undefined)
3766
3947
  return;
3948
+ const from = standingOf(room.locator);
3767
3949
  const bootstrapped = await pullBootstrap(authorization, room, local, live);
3768
- if (!live())
3769
- return;
3770
3950
  if (bootstrapped.kind !== 'staged') {
3771
- await withhold(bootstrapped, room, local, progress);
3951
+ if (live())
3952
+ await withhold(bootstrapped, room, local, progress);
3772
3953
  return;
3773
3954
  }
3774
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3955
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
3775
3956
  await caughtUp(room, local, progress, live);
3776
3957
  }
3777
3958
  finally {
3778
3959
  caughtUpTo(room.locator);
3779
3960
  }
3780
3961
  }
3781
- async function discoverRooms(local, progress, attempt, live, scope) {
3962
+ async function asked(authorized) {
3963
+ try {
3964
+ return (await options.adapter.discover?.(authorized)) ?? { kind: 'transport-uncertainty' };
3965
+ }
3966
+ catch {
3967
+ return { kind: 'transport-uncertainty' };
3968
+ }
3969
+ }
3970
+ function malformed(listed) {
3971
+ for (const room of listed) {
3972
+ if (room.operations === undefined ||
3973
+ !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
3974
+ room.operations[operation].every((scope) => typeof scope === 'string')) ||
3975
+ room.replica === undefined ||
3976
+ typeof room.replica.selection !== 'string' ||
3977
+ room.replica.selection.length === 0 ||
3978
+ !['admitted', 'unavailable'].includes(room.replica.admission) ||
3979
+ !['locked', 'open'].includes(room.replica.offline)) {
3980
+ return `invalid model or replica declaration ${room.locator}`;
3981
+ }
3982
+ if (!declaredSelection(room))
3983
+ return `invalid typed selection declaration ${room.locator}`;
3984
+ }
3985
+ return null;
3986
+ }
3987
+ async function signIn(local, signedIn) {
3988
+ if (JSON.stringify(signedIn) === JSON.stringify(member))
3989
+ return;
3990
+ await local.fact('member', signedIn);
3991
+ member = signedIn;
3992
+ emitMember();
3993
+ }
3994
+ async function discoverRooms(local, progress, attempt, live, found) {
3782
3995
  if (closed || authorization === undefined) {
3783
3996
  notifyReady();
3784
3997
  return;
@@ -3786,13 +3999,9 @@ export function composeRuntime(options) {
3786
3999
  moveTo(transferring(status));
3787
4000
  discoveries += 1;
3788
4001
  const started = discoveries;
3789
- let result;
3790
- try {
3791
- result = await options.adapter.discover(authorization, scope);
3792
- }
3793
- catch {
3794
- result = { kind: 'transport-uncertainty' };
3795
- }
4002
+ const result = found ?? (await asked(authorization));
4003
+ if (result.kind === 'discovery')
4004
+ roomsNamed = true;
3796
4005
  if (!live()) {
3797
4006
  progress.complete = false;
3798
4007
  notifyReady();
@@ -3800,12 +4009,7 @@ export function composeRuntime(options) {
3800
4009
  }
3801
4010
  journal.record(`connection discover ${result.kind}`);
3802
4011
  if (result.kind === 'transport-uncertainty') {
3803
- for (const room of rooms.values()) {
3804
- await retainModel(local, room, 'collection', cached(modelOf(room.locator)));
3805
- if (room.typedSelection !== null) {
3806
- await retainModel(local, room, 'typed-selection', cached(modelOf(room.locator, 'typed-selection')));
3807
- }
3808
- }
4012
+ await cachedModels(local);
3809
4013
  progress.reachable = false;
3810
4014
  progress.complete = false;
3811
4015
  notifyReady();
@@ -3826,26 +4030,12 @@ export function composeRuntime(options) {
3826
4030
  notifyReady();
3827
4031
  return;
3828
4032
  }
3829
- for (const room of result.rooms) {
3830
- if (room.operations === undefined ||
3831
- !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
3832
- room.operations[operation].every((scope) => typeof scope === 'string')) ||
3833
- room.replica === undefined ||
3834
- typeof room.replica.selection !== 'string' ||
3835
- room.replica.selection.length === 0 ||
3836
- !['admitted', 'unavailable'].includes(room.replica.admission) ||
3837
- !['locked', 'open'].includes(room.replica.offline)) {
3838
- journal.record(`invalid model or replica declaration ${room.locator}`);
3839
- await refuse('missing-required-guarantee');
3840
- notifyReady();
3841
- return;
3842
- }
3843
- if (!declaredSelection(room)) {
3844
- journal.record(`invalid typed selection declaration ${room.locator}`);
3845
- await refuse('missing-required-guarantee');
3846
- notifyReady();
3847
- return;
3848
- }
4033
+ const invalid = malformed(result.rooms);
4034
+ if (invalid !== null) {
4035
+ journal.record(invalid);
4036
+ await refuse('missing-required-guarantee');
4037
+ notifyReady();
4038
+ return;
3849
4039
  }
3850
4040
  if (refusal !== null) {
3851
4041
  refusal = null;
@@ -3861,13 +4051,8 @@ export function composeRuntime(options) {
3861
4051
  await local.fact('providerLimits', limits);
3862
4052
  await local.fact('capabilities', capabilities);
3863
4053
  }
3864
- const signedIn = result.member ?? null;
3865
- if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
3866
- await local.fact('member', signedIn);
3867
- member = signedIn;
3868
- emitMember();
3869
- }
3870
- await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
4054
+ await signIn(local, result.member ?? null);
4055
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
3871
4056
  }
3872
4057
  async function ensureAuthorization() {
3873
4058
  if (authorization !== undefined)
@@ -3882,23 +4067,45 @@ export function composeRuntime(options) {
3882
4067
  }
3883
4068
  }
3884
4069
  async function admittedRead(work) {
3885
- if (roomReads.generation !== generation) {
3886
- roomReads = { generation, free: ROOM_READS, waiting: [] };
4070
+ while (roomReads.free === 0) {
4071
+ await new Promise((admit) => roomReads.waiting.push(admit));
3887
4072
  }
3888
- const reads = roomReads;
3889
- while (reads.free === 0)
3890
- await new Promise((admit) => reads.waiting.push(admit));
3891
- reads.free -= 1;
4073
+ roomReads.free -= 1;
3892
4074
  try {
3893
4075
  await work();
3894
4076
  }
3895
4077
  finally {
3896
- reads.free += 1;
3897
- reads.waiting.shift()?.();
4078
+ roomReads.free += 1;
4079
+ roomReads.waiting.shift()?.();
3898
4080
  }
3899
4081
  }
4082
+ function inRoom(locator, work) {
4083
+ roomWork += 1;
4084
+ const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
4085
+ try {
4086
+ return await work(continuing);
4087
+ }
4088
+ finally {
4089
+ roomWork -= 1;
4090
+ }
4091
+ });
4092
+ const chain = ran.then(() => undefined, () => undefined);
4093
+ roomLanes.set(locator, chain);
4094
+ working.add(chain);
4095
+ void chain.finally(() => {
4096
+ working.delete(chain);
4097
+ if (roomLanes.get(locator) === chain)
4098
+ roomLanes.delete(locator);
4099
+ });
4100
+ return ran;
4101
+ }
3900
4102
  function roomTask(locator, work) {
3901
- return task(roomLane(locator), (live) => admittedRead(() => work(live)));
4103
+ if (stopping)
4104
+ return Promise.resolve();
4105
+ return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
4106
+ }
4107
+ function advanced(room, local, authorized) {
4108
+ return inRoom(room.locator, (live) => advance(room, local, authorized, live));
3902
4109
  }
3903
4110
  function task(lane, work) {
3904
4111
  if (stopping)
@@ -3914,23 +4121,19 @@ export function composeRuntime(options) {
3914
4121
  void run.finally(() => working.delete(run));
3915
4122
  return run;
3916
4123
  }
3917
- function sync(attempt) {
4124
+ function sync(attempt, found) {
3918
4125
  return task(OPENING_LANE, async (live) => {
3919
4126
  if (!live()) {
3920
- notifyReady();
4127
+ readyFromStore();
3921
4128
  return;
3922
4129
  }
3923
4130
  const progress = { complete: true, reachable: true };
3924
- const gate = discover();
3925
- if (attempt === 'opened')
3926
- opened = generation;
3927
- underway = attempt === 'opened' ? { generation, progress } : null;
4131
+ underway = attempt === 'opened' ? progress : null;
3928
4132
  if (attempt === 'opened')
3929
4133
  streamed = progress;
3930
4134
  complete = false;
3931
4135
  recorded = false;
3932
4136
  moveTo(transferring(status));
3933
- handed = false;
3934
4137
  try {
3935
4138
  const local = await openLocal();
3936
4139
  if (!live())
@@ -3938,7 +4141,7 @@ export function composeRuntime(options) {
3938
4141
  await ensureAuthorization();
3939
4142
  if (!live())
3940
4143
  return;
3941
- await discoverRooms(local, progress, attempt, live);
4144
+ await discoverRooms(local, progress, attempt, live, found);
3942
4145
  }
3943
4146
  catch {
3944
4147
  progress.complete = false;
@@ -3946,7 +4149,6 @@ export function composeRuntime(options) {
3946
4149
  progress.reachable = false;
3947
4150
  }
3948
4151
  finally {
3949
- gate.release();
3950
4152
  notifyReady();
3951
4153
  }
3952
4154
  if (!live())
@@ -3958,35 +4160,53 @@ export function composeRuntime(options) {
3958
4160
  if (handed || closed || progress.reachable)
3959
4161
  return;
3960
4162
  underway = null;
3961
- opened = null;
3962
- journal.record('connection reopen');
3963
4163
  await rest(progress, live);
3964
- options.adapter.roomStream?.reopen();
3965
4164
  });
3966
4165
  }
3967
- function rediscover(scope) {
4166
+ function resynchronize() {
3968
4167
  return task(OPENING_LANE, async (live) => {
3969
- if (!live() || !socketOpen)
4168
+ if (!live() || !announced)
3970
4169
  return;
3971
4170
  const progress = { complete: true, reachable: true };
3972
- const gate = discover();
3973
4171
  moveTo(transferring(status));
3974
4172
  try {
3975
4173
  const local = await openLocal();
3976
4174
  if (!live())
3977
4175
  return;
3978
- await ensureAuthorization();
4176
+ await synchronize([...discovered.values()], local, progress, 'unstreamed', live);
4177
+ }
4178
+ catch {
4179
+ progress.complete = false;
4180
+ }
4181
+ if (live())
4182
+ await finishOpening(progress, live);
4183
+ });
4184
+ }
4185
+ function changeAuthority(change) {
4186
+ return task(OPENING_LANE, async (live) => {
4187
+ if (!live() || !announced)
4188
+ return;
4189
+ const progress = { complete: true, reachable: true };
4190
+ moveTo(transferring(status));
4191
+ try {
4192
+ const local = await openLocal();
3979
4193
  if (!live())
3980
4194
  return;
3981
- await discoverRooms(local, progress, 'authorized', live, scope);
4195
+ const invalid = malformed(change.rooms);
4196
+ if (invalid !== null) {
4197
+ journal.record(invalid);
4198
+ await refuse('missing-required-guarantee');
4199
+ return;
4200
+ }
4201
+ if (change.member !== undefined)
4202
+ await signIn(local, change.member);
4203
+ discoveries += 1;
4204
+ const listed = await applyDiscovery(change.rooms, discoveries, change.left);
4205
+ await synchronize(listed, local, progress, 'authorized', live);
3982
4206
  }
3983
4207
  catch {
3984
4208
  progress.complete = false;
3985
4209
  }
3986
- finally {
3987
- gate.release();
3988
- notifyReady();
3989
- }
3990
4210
  if (!live())
3991
4211
  return;
3992
4212
  if (!progress.complete)
@@ -3995,13 +4215,15 @@ export function composeRuntime(options) {
3995
4215
  });
3996
4216
  }
3997
4217
  function closedSocket(revoked, lost) {
3998
- const opened = socketOpen;
4218
+ notifyReady();
4219
+ const opened = announced;
3999
4220
  socketOpen = false;
4221
+ announced = false;
4222
+ handed = false;
4223
+ naming = 0;
4000
4224
  connectionLost();
4001
- if (lost) {
4002
- reads.lost();
4225
+ if (lost)
4003
4226
  conversations.lost();
4004
- }
4005
4227
  if (revoked) {
4006
4228
  void refuse('revoked-authorization');
4007
4229
  return;
@@ -4014,16 +4236,30 @@ export function composeRuntime(options) {
4014
4236
  state: status.state === 'synced' ? 'syncing' : status.state,
4015
4237
  providerUnreachable: status.providerUnreachable || !opened,
4016
4238
  });
4239
+ void closing(opened);
4017
4240
  }
4018
- function applyOpening(expired) {
4019
- const progress = underway?.generation === generation ? underway.progress : null;
4241
+ function closing(opened) {
4242
+ return task(OPENING_LANE, async (live) => {
4243
+ if (!live())
4244
+ return;
4245
+ const local = await openLocal();
4246
+ if (!opened && !socketOpen)
4247
+ await cachedModels(local);
4248
+ await rest(null, live);
4249
+ });
4250
+ }
4251
+ function cachedModels(local) {
4252
+ return settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
4253
+ }
4254
+ function caughtUpRooms(expired) {
4255
+ const progress = underway;
4256
+ underway = null;
4257
+ naming = Math.max(0, naming - 1);
4020
4258
  const cycle = progress ?? { complete: true, reachable: true };
4021
- if (opened === generation)
4022
- opened = null;
4259
+ replaced(expired);
4023
4260
  const reloading = progress === null ? [] : owedReloads(expired);
4024
4261
  const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
4025
4262
  return task(OPENING_LANE, async (live) => {
4026
- underway = null;
4027
4263
  if (!live())
4028
4264
  return;
4029
4265
  if (progress === null) {
@@ -4031,24 +4267,14 @@ export function composeRuntime(options) {
4031
4267
  await rest(cycle, live);
4032
4268
  return;
4033
4269
  }
4034
- await roomsCarried([...expired, ...reloading]);
4035
- if (!live())
4036
- return;
4037
4270
  try {
4038
4271
  const local = await openLocal();
4039
- const reloaded = await replacing;
4272
+ await replacing;
4040
4273
  if (progress.reachable) {
4041
- for (const room of discovered.values()) {
4042
- if (replicas.has(room.locator) &&
4043
- !expired.includes(room.locator) &&
4044
- !reloading.includes(room.locator)) {
4045
- await confirmModels(room, local);
4046
- }
4047
- }
4274
+ await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
4275
+ !expired.includes(room.locator) &&
4276
+ !reloading.includes(room.locator)), 'current', local);
4048
4277
  }
4049
- const failed = reloaded.find((outcome) => outcome.status === 'rejected');
4050
- if (failed !== undefined)
4051
- throw failed.reason;
4052
4278
  }
4053
4279
  catch {
4054
4280
  progress.complete = false;
@@ -4060,11 +4286,8 @@ export function composeRuntime(options) {
4060
4286
  }
4061
4287
  function owedReloads(expired) {
4062
4288
  const reloading = [...discovered.values()]
4063
- .filter((room) => replicas.has(room.locator) &&
4064
- !expired.includes(room.locator) &&
4065
- (owed.has(room.locator) || revoked(room.locator)))
4289
+ .filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
4066
4290
  .map((room) => room.locator);
4067
- owed.clear();
4068
4291
  for (const locator of reloading)
4069
4292
  catching.add(locator);
4070
4293
  liveQueries.recover();
@@ -4073,24 +4296,43 @@ export function composeRuntime(options) {
4073
4296
  async function reloadNow(replaced, progress) {
4074
4297
  const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
4075
4298
  for (const room of reloaded)
4076
- reloadsDue.set(room.locator, generation);
4299
+ reloadsDue.add(room.locator);
4077
4300
  try {
4078
4301
  const local = await openLocal();
4079
- return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
4302
+ for (const room of reloaded) {
4303
+ void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
4304
+ }
4080
4305
  }
4081
4306
  catch {
4082
4307
  for (const room of reloaded)
4083
4308
  reloadsDue.delete(room.locator);
4084
4309
  progress.reachable = false;
4085
- return [];
4086
4310
  }
4087
4311
  }
4088
- async function confirmModels(room, local) {
4089
- for (const attached of ['collection', 'typed-selection']) {
4090
- const model = modelOf(room.locator, attached);
4091
- if (model.status === 'cached') {
4092
- await retainModel(local, room, attached, { ...model, status: 'current' });
4312
+ async function settleModels(held, status, local) {
4313
+ const owed = (room) => {
4314
+ const model = modelOf(room.locator);
4315
+ return model.status !== 'unavailable' && model.status !== status;
4316
+ };
4317
+ const settled = await exclusive(async () => {
4318
+ if (!continuing())
4319
+ return [];
4320
+ const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
4321
+ for (const room of unchanged) {
4322
+ const next = { ...modelOf(room.locator), status };
4323
+ const replica = replicas.get(room.locator);
4324
+ if (replica !== undefined)
4325
+ replicas.set(room.locator, { ...replica, record: { ...replica.record, model: next } });
4326
+ heldModel(room, next);
4093
4327
  }
4328
+ if (unchanged.length > 0)
4329
+ emitRooms();
4330
+ return unchanged;
4331
+ });
4332
+ for (const room of held) {
4333
+ if (settled.includes(room) || !owed(room))
4334
+ continue;
4335
+ await retainModel(local, room, { ...modelOf(room.locator), status });
4094
4336
  }
4095
4337
  }
4096
4338
  function submitLater(locator) {
@@ -4135,14 +4377,10 @@ export function composeRuntime(options) {
4135
4377
  try {
4136
4378
  const local = await openLocal();
4137
4379
  await roomTask(locator, async (live) => {
4138
- const current = () => live() && !signal.aborted;
4139
- if (!current() || authorization === undefined)
4380
+ if (!live() || authorization === undefined)
4140
4381
  return;
4141
- const withheld = await Promise.race([
4142
- advance(room, local, authorization, current),
4143
- abandonedOn(signal),
4144
- ]);
4145
- if (withheld !== null && current())
4382
+ const withheld = await advance(room, local, authorization, live);
4383
+ if (withheld !== null && live())
4146
4384
  await withhold(withheld, room, local, progress);
4147
4385
  });
4148
4386
  }
@@ -4197,14 +4435,11 @@ export function composeRuntime(options) {
4197
4435
  progress.complete = false;
4198
4436
  return;
4199
4437
  }
4200
- reads.applied();
4201
4438
  submitAll();
4202
4439
  for (const locator of stale)
4203
4440
  if (repairable.has(locator))
4204
4441
  repairLater(locator, {});
4205
4442
  await conversations.reconcile();
4206
- if (status.providerUnreachable)
4207
- conversations.reconnected();
4208
4443
  const reading = { complete: true, reachable: true };
4209
4444
  await conversations.catchUp(reading);
4210
4445
  if (progress.reachable && reading.reachable)
@@ -4230,7 +4465,7 @@ export function composeRuntime(options) {
4230
4465
  if (!live())
4231
4466
  return;
4232
4467
  let lastCompleted = status.lastCompleted;
4233
- let synced = complete && stale.size === 0 && !pending();
4468
+ let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
4234
4469
  if (synced && !recorded) {
4235
4470
  const completedAt = new Date().toISOString();
4236
4471
  try {
@@ -4245,74 +4480,69 @@ export function composeRuntime(options) {
4245
4480
  }
4246
4481
  }
4247
4482
  moveTo({
4248
- state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
4483
+ state: pending()
4484
+ ? 'pending'
4485
+ : synced && complete && roomWork === 0 && naming === 0
4486
+ ? 'synced'
4487
+ : 'syncing',
4249
4488
  lastCompleted,
4250
- providerUnreachable: !progress.reachable,
4489
+ providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
4251
4490
  ...attention(),
4252
4491
  });
4253
4492
  }
4254
- function discover() {
4255
- let release = () => { };
4256
- const done = new Promise((resolve) => {
4257
- release = resolve;
4258
- });
4259
- discovering = { generation, done, release };
4260
- return discovering;
4261
- }
4262
- function applyRoom(locator, roomVersion) {
4263
- announced.set(locator, roomVersion);
4264
- return task(roomLane(locator), async (live) => {
4265
- if (discovering?.generation === generation)
4266
- await discovering.done;
4267
- const saving = answering.get(locator);
4268
- if (saving?.generation === generation)
4269
- await saving.answered;
4270
- if (!live() || authorization === undefined)
4271
- return;
4272
- const room = discovered.get(locator);
4273
- const held = replicas.get(locator);
4274
- if (room === undefined || room.replica.admission !== 'admitted')
4275
- return;
4276
- if (held !== undefined &&
4277
- (held.record.replica.selection !== room.replica.selection ||
4278
- options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
4279
- return;
4280
- }
4281
- const during = underway?.generation === generation ? underway.progress : null;
4282
- const progress = during ?? { complete: true, reachable: true };
4283
- if (during === null)
4284
- moveTo(transferring(status));
4493
+ function applyRoom(change) {
4494
+ const { locator } = change;
4495
+ if (stopping)
4496
+ return Promise.resolve();
4497
+ return inRoom(locator, async (live) => {
4498
+ await answering.get(locator)?.answered;
4499
+ const progress = underway ?? { complete: true, reachable: true };
4285
4500
  try {
4286
4501
  const local = await openLocal();
4287
- await admittedRead(async () => {
4288
- if (!live() || authorization === undefined)
4289
- return;
4290
- const withheld = await advance(room, local, authorization, live);
4291
- if (withheld !== null)
4292
- await withhold(withheld, room, local, progress);
4293
- });
4502
+ for (;;) {
4503
+ if (!live())
4504
+ return null;
4505
+ const room = discovered.get(locator);
4506
+ const held = replicas.get(locator);
4507
+ if (room === undefined ||
4508
+ held === undefined ||
4509
+ room.replica.admission !== 'admitted' ||
4510
+ held.record.replica.selection !== room.replica.selection) {
4511
+ return null;
4512
+ }
4513
+ const at = held.record.roomVersion;
4514
+ if (options.adapter.reached(locator, at, change.roomVersion))
4515
+ return progress;
4516
+ if (!options.adapter.reached(locator, at, change.from)) {
4517
+ name(locator);
4518
+ return progress;
4519
+ }
4520
+ if (underway === null)
4521
+ moveTo(transferring(status));
4522
+ const writing = () => live() && !full;
4523
+ const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, () => change.read()));
4524
+ if (refreshed.kind !== 'advanced')
4525
+ return progress;
4526
+ const landing = {
4527
+ kind: 'refreshed',
4528
+ from: refreshed.from,
4529
+ remodelled: refreshed.remodelled,
4530
+ };
4531
+ if (await publish(local, room, refreshed.record, landing, writing)) {
4532
+ stale.delete(locator);
4533
+ await retainModel(local, room, modelOf(locator));
4534
+ return progress;
4535
+ }
4536
+ }
4294
4537
  }
4295
4538
  catch {
4539
+ if (!stale.has(locator))
4540
+ name(locator);
4296
4541
  stale.add(locator);
4542
+ return progress;
4297
4543
  }
4298
- finally {
4299
- if (during === null)
4300
- await rest(progress, live);
4301
- }
4302
- });
4544
+ }).then((progress) => rest(progress), () => undefined);
4303
4545
  }
4304
- function roomsCarried(replacing) {
4305
- const replaced = new Set(replacing.map(roomLane));
4306
- return Promise.all([...lanes]
4307
- .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
4308
- .map(([, held]) => held.chain));
4309
- }
4310
- const receipts = {
4311
- heads: async () => (await openLocal()).receivedHeads(),
4312
- parts: async (key, count) => (await openLocal()).receivedParts(key, count),
4313
- keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
4314
- drop: async (keys) => (await openLocal()).dropReceived(keys),
4315
- };
4316
4546
  function observeStream() {
4317
4547
  const stream = options.adapter.roomStream;
4318
4548
  if (stream === undefined || withdrawStream !== undefined)
@@ -4322,29 +4552,29 @@ export function composeRuntime(options) {
4322
4552
  return;
4323
4553
  if (event.kind === 'opened') {
4324
4554
  socketOpen = true;
4555
+ announced = false;
4556
+ handed = false;
4557
+ naming = 0;
4325
4558
  connectionLive();
4326
4559
  generation += 1;
4327
- reads.opened();
4328
4560
  conversations.reopened();
4329
4561
  recovery.resume();
4330
- void sync('opened');
4562
+ }
4563
+ else if (event.kind === 'discovered') {
4564
+ if (!socketOpen)
4565
+ return;
4566
+ announced = true;
4567
+ void sync('opened', event.discovery);
4331
4568
  }
4332
4569
  else if (event.kind === 'closed')
4333
4570
  closedSocket(event.revoked, event.lost);
4334
- else if (event.kind === 'opening')
4335
- void applyOpening(event.expired);
4571
+ else if (event.kind === 'caught-up')
4572
+ void caughtUpRooms(event.expired);
4336
4573
  else if (event.kind === 'authority-changed')
4337
- void rediscover(event.scope);
4574
+ void changeAuthority(event);
4338
4575
  else
4339
- void applyRoom(event.locator, event.roomVersion);
4340
- }, receipts, heldRevisions);
4341
- }
4342
- async function heldRevisions(locator, paths) {
4343
- const held = replicas.get(locator);
4344
- if (held === undefined || paths.length === 0)
4345
- return new Map();
4346
- const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
4347
- return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
4576
+ void applyRoom(event);
4577
+ });
4348
4578
  }
4349
4579
  function writesHeld() {
4350
4580
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -4414,8 +4644,9 @@ export function composeRuntime(options) {
4414
4644
  if (closed)
4415
4645
  return;
4416
4646
  closed = true;
4417
- if (rechecking !== null)
4418
- clearInterval(rechecking);
4647
+ notifyDiscovered();
4648
+ if (probing !== null)
4649
+ clearInterval(probing);
4419
4650
  connectionLive();
4420
4651
  lockListeners.clear();
4421
4652
  readinessWatches.clear();
@@ -4427,6 +4658,7 @@ export function composeRuntime(options) {
4427
4658
  conversations.close();
4428
4659
  void recovery.retire();
4429
4660
  roomListeners.clear();
4661
+ unlistedRooms.clear();
4430
4662
  refusalListeners.clear();
4431
4663
  storageLossListeners.clear();
4432
4664
  statusListeners.clear();
@@ -4439,6 +4671,7 @@ export function composeRuntime(options) {
4439
4671
  }
4440
4672
  async function stop() {
4441
4673
  stopping = true;
4674
+ inFlight.abort();
4442
4675
  await recovery.retire();
4443
4676
  await Promise.all(working);
4444
4677
  await mutations;
@@ -4452,12 +4685,12 @@ export function composeRuntime(options) {
4452
4685
  observeStream();
4453
4686
  void openLocal().then(() => {
4454
4687
  if (capabilities.length > 0)
4455
- notifyReady();
4688
+ readyFromStore();
4456
4689
  }, () => {
4457
- notifyReady();
4690
+ readyFromStore();
4458
4691
  });
4459
4692
  if (options.adapter.roomStream === undefined)
4460
- void sync('unstreamed');
4693
+ void sync('unstreamed', null);
4461
4694
  function planned(input, reach) {
4462
4695
  const plan = planOf(input, roomFacts(reach), planReach(reach));
4463
4696
  const named = plan.paths !== null &&
@@ -4602,12 +4835,13 @@ export function composeRuntime(options) {
4602
4835
  };
4603
4836
  }
4604
4837
  if (topic === 'readiness') {
4605
- return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
4838
+ return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
4606
4839
  }
4607
4840
  if (topic === 'rooms') {
4608
- roomListeners.add(deliver);
4609
- deliver(listed());
4841
+ unlistedRooms.add(deliver);
4842
+ emitKnownRooms();
4610
4843
  return () => {
4844
+ unlistedRooms.delete(deliver);
4611
4845
  roomListeners.delete(deliver);
4612
4846
  };
4613
4847
  }
@@ -4661,7 +4895,17 @@ export function composeRuntime(options) {
4661
4895
  return openLocal().then(() => undefined);
4662
4896
  },
4663
4897
  rooms() {
4664
- return listed();
4898
+ if (closed)
4899
+ return Promise.resolve([]);
4900
+ return new Promise((settle) => {
4901
+ const answer = (value) => {
4902
+ unlistedRooms.delete(answer);
4903
+ roomListeners.delete(answer);
4904
+ settle(value);
4905
+ };
4906
+ unlistedRooms.add(answer);
4907
+ emitKnownRooms();
4908
+ });
4665
4909
  },
4666
4910
  query: ((input) => rowsQuery(planned(input, null))),
4667
4911
  search: (input) => searched(input, null, []),
@@ -4671,7 +4915,7 @@ export function composeRuntime(options) {
4671
4915
  typedSelection: rooms.get(locator)?.typedSelection === undefined ||
4672
4916
  rooms.get(locator)?.typedSelection === null
4673
4917
  ? null
4674
- : modelTypes(modelOf(locator, 'typed-selection')),
4918
+ : modelTypes(builtIn),
4675
4919
  };
4676
4920
  },
4677
4921
  roomDraft: {
@@ -4688,33 +4932,17 @@ export function composeRuntime(options) {
4688
4932
  const record = roomDrafts.get(room)?.get(path);
4689
4933
  return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
4690
4934
  },
4691
- write(room, path, revision, input) {
4692
- return exclusive(async () => {
4693
- const parts = readParts(input);
4694
- if (path === '' || normalize(path) !== path)
4695
- throw runtimeError('room-draft-malformed');
4696
- const local = await openLocal();
4697
- const current = roomDrafts.get(room)?.get(path);
4698
- if ((current?.revision ?? null) !== revision) {
4699
- return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
4700
- }
4701
- const file = parts.file === undefined
4702
- ? undefined
4703
- : await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
4704
- const record = {
4705
- locator: room,
4706
- path,
4707
- revision: roomDraftRevision(),
4708
- parts: held(current?.parts, parts, file),
4709
- returned: current?.returned ?? null,
4710
- };
4711
- await local.keepRoomDraft(record, parts.file === undefined || file === undefined
4712
- ? []
4713
- : [{ key: file.localId, blob: parts.file.blob }]);
4714
- rememberRoomDraft(record);
4715
- publishChangeSet(changeSet(room, { drafts: [path] }));
4716
- return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
4717
- });
4935
+ write(room, path, revision, input, next = roomDraftRevision()) {
4936
+ if (typeof next !== 'string' || next === '')
4937
+ return Promise.reject(runtimeError('room-draft-malformed'));
4938
+ return gathered(`room-draft\u0000${room}\u0000${path}`, { revision, input, next }, (earlier, later) => {
4939
+ if (later.revision !== earlier.next)
4940
+ return null;
4941
+ const parts = gatheredParts(earlier.input, later.input);
4942
+ return parts === null
4943
+ ? null
4944
+ : { revision: earlier.revision, input: parts, next: later.next };
4945
+ }, (written) => writtenRoomDraft(room, path, written.revision, written.input, written.next));
4718
4946
  },
4719
4947
  discard(room, path, revision) {
4720
4948
  return exclusive(async () => {
@@ -4741,13 +4969,13 @@ export function composeRuntime(options) {
4741
4969
  });
4742
4970
  },
4743
4971
  write(app, value) {
4744
- return keepPresentation(app, () => structuredClone(value));
4972
+ return exclusive(() => keptPresentation(app, () => structuredClone(value)));
4745
4973
  },
4746
4974
  edit(app, members) {
4747
- return keepPresentation(app, (held) => Object.fromEntries([
4748
- ...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(members, member)),
4749
- ...Object.entries(structuredClone(members)).filter(([, value]) => value !== null),
4750
- ]));
4975
+ return gathered(`presentation\u0000${app}`, structuredClone(members), (earlier, later) => ({ ...earlier, ...later }), (edited) => keptPresentation(app, (held) => Object.fromEntries([
4976
+ ...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(edited, member)),
4977
+ ...Object.entries(edited).filter(([, value]) => value !== null),
4978
+ ])));
4751
4979
  },
4752
4980
  },
4753
4981
  save(room, named, writtenBy) {
@@ -4766,7 +4994,8 @@ export function composeRuntime(options) {
4766
4994
  part: readPart,
4767
4995
  read: readBinaryPath,
4768
4996
  link: linkBinaryPath,
4769
- readReference: readBinaryReference,
4997
+ show: showBinaryPath,
4998
+ attach: attachBinary,
4770
4999
  },
4771
5000
  async call(name, input) {
4772
5001
  await ready;
@@ -4789,7 +5018,8 @@ export function composeRuntime(options) {
4789
5018
  journal.record('retry');
4790
5019
  unpreparable.clear();
4791
5020
  queueReadiness();
4792
- resume();
5021
+ if (full)
5022
+ await probed();
4793
5023
  },
4794
5024
  observe: observing(watch),
4795
5025
  async reauthorize() {
@@ -4803,7 +5033,7 @@ export function composeRuntime(options) {
4803
5033
  authorization = undefined;
4804
5034
  discoveryPaused = false;
4805
5035
  if (options.adapter.roomStream === undefined)
4806
- await sync('unstreamed');
5036
+ await sync('unstreamed', null);
4807
5037
  else
4808
5038
  await task(OPENING_LANE, ensureAuthorization);
4809
5039
  },
@@ -4811,7 +5041,6 @@ export function composeRuntime(options) {
4811
5041
  const reachable = {
4812
5042
  md,
4813
5043
  watch,
4814
- referrerScope: (referrer) => options.adapter.referrerScope(referrer),
4815
5044
  plan: planned,
4816
5045
  query: (plan) => rowsQuery(plan),
4817
5046
  watchPlan,
@@ -4821,12 +5050,12 @@ export function composeRuntime(options) {
4821
5050
  lockedRooms,
4822
5051
  watchLocks,
4823
5052
  located,
5053
+ room: (locator) => listed().find((one) => one.locator === locator),
4824
5054
  };
4825
5055
  return {
4826
5056
  md,
4827
5057
  seed,
4828
5058
  stop,
4829
- lose: loseRooms,
4830
5059
  watch,
4831
5060
  persisted(answer) {
4832
5061
  if (answer === persistence)
@@ -4835,6 +5064,10 @@ export function composeRuntime(options) {
4835
5064
  journal.record(`persistence ${answer}`);
4836
5065
  queueReadiness();
4837
5066
  },
4838
- within: (reach, port) => narrowed(reachable, reach, port),
5067
+ within: (reach, port) => {
5068
+ if (full)
5069
+ void probed();
5070
+ return narrowed(reachable, reach, port);
5071
+ },
4839
5072
  };
4840
5073
  }