@agentshouse/mdruntime 0.1.0-cf890bb538119 → 0.1.0-cf9a84afc6def

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 +59 -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 +1242 -1064
  24. package/dist/conversation.d.ts +12 -11
  25. package/dist/conversation.d.ts.map +1 -1
  26. package/dist/conversation.js +201 -131
  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 +8 -6
  48. package/dist/memory.d.ts.map +1 -1
  49. package/dist/memory.js +165 -96
  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 +66 -50
  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 +9 -4
  75. package/dist/refresh.d.ts.map +1 -1
  76. package/dist/refresh.js +64 -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 +19 -26
  96. package/dist/store.d.ts.map +1 -1
  97. package/dist/store.js +26 -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,16 @@ 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();
160
172
  const lanes = new Map();
161
- let roomReads = { generation, free: ROOM_READS, waiting: [] };
173
+ const roomLanes = new Map();
174
+ let roomWork = 0;
175
+ const roomReads = { free: ROOM_READS, waiting: [] };
162
176
  const working = new Set();
163
177
  let mutations = Promise.resolve();
178
+ let gathering = null;
164
179
  const roomListeners = new Set();
180
+ const unlistedRooms = new Set();
165
181
  const refusalListeners = new Set();
166
182
  const storageLossListeners = new Set();
167
183
  const statusListeners = new Set();
@@ -172,6 +188,7 @@ export function composeRuntime(options) {
172
188
  let withdrawConversations;
173
189
  let currencyQueued = null;
174
190
  let socketOpen = false;
191
+ let announced = false;
175
192
  const confirmed = new Set();
176
193
  let lockGrace = null;
177
194
  const lockListeners = new Set();
@@ -181,49 +198,80 @@ export function composeRuntime(options) {
181
198
  const serviceModeListeners = new Set();
182
199
  let member = null;
183
200
  const memberListeners = new Set();
184
- const prepared = new Set();
185
201
  const unpreparedDemand = new Set();
186
202
  const unpreparable = new Set();
187
203
  let preparing = false;
188
204
  let targets = {};
189
205
  let persistence = options.shell.persistence;
190
- let capacity = null;
191
- const paused = new Set();
192
- let rechecking = null;
206
+ let full = false;
207
+ let probing = null;
208
+ let testing = null;
209
+ const database = {
210
+ async ask(request) {
211
+ try {
212
+ return await options.database.ask(request);
213
+ }
214
+ catch (error) {
215
+ if (refusedForSpace(error))
216
+ filled();
217
+ throw error;
218
+ }
219
+ },
220
+ };
193
221
  const readinessWatches = new Set();
194
222
  let readinessQueued = false;
195
223
  const catching = new Set();
196
224
  const transfers = new FencedMap(isFenced);
225
+ const installs = new Set();
226
+ const arrivals = new Map();
227
+ const streaming = new Set();
228
+ const underived = new Map();
229
+ const unindexed = new Map();
230
+ const startedOver = new Map();
231
+ let arrivalReported = 0;
232
+ let streamsFree = STREAMS_HELD;
233
+ let queuedText = 0;
234
+ const installWaiters = [];
235
+ const stageBudget = {
236
+ bytes: STAGE_FLOOR * 4,
237
+ floor: STAGE_FLOOR,
238
+ ceiling: STAGE_BYTES,
239
+ aim: STAGE_AIM_MS,
240
+ };
241
+ const deriveBudget = {
242
+ bytes: DERIVE_FLOOR * 4,
243
+ floor: DERIVE_FLOOR,
244
+ ceiling: DERIVE_BYTES,
245
+ aim: DERIVE_AIM_MS,
246
+ };
247
+ const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
197
248
  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
249
  call: (name, input) => invokeAction(name, input),
250
+ history: (request) => readConversationHistory(request),
213
251
  durably,
214
252
  stream: () => options.adapter.conversations,
215
253
  publish: publishChangeSet,
216
254
  online: () => connectionOf() !== 'unreachable' && refusal === null,
255
+ full: () => full,
217
256
  closed: () => closed,
218
257
  unanswered: resendLater,
219
258
  expired: (operation) => pastHorizon(operation),
220
259
  });
221
260
  const journal = createLog();
222
- const keptFiles = openedFiles(options.database, isFenced);
223
- let notifyReady = () => { };
261
+ const keptFiles = openedFiles(database, isFenced);
262
+ let readyFromStore = () => { };
224
263
  const ready = new Promise((resolve) => {
225
- notifyReady = resolve;
264
+ readyFromStore = resolve;
265
+ });
266
+ let notifyDiscovered = () => { };
267
+ const firstDiscovery = new Promise((resolve) => {
268
+ notifyDiscovered = resolve;
226
269
  });
270
+ let roomsNamed = false;
271
+ function notifyReady() {
272
+ notifyDiscovered();
273
+ readyFromStore();
274
+ }
227
275
  function rememberRoomDraft(record) {
228
276
  const room = roomDrafts.get(record.locator) ?? new Map();
229
277
  room.set(record.path, record);
@@ -243,7 +291,7 @@ export function composeRuntime(options) {
243
291
  return undefined;
244
292
  const local = await openLocal();
245
293
  const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
246
- return { ...slice, collection: collectionOf(slice.documents, compositionOf(held.record)) };
294
+ return { ...slice, collection: collectionOf(slice.documents, composed(held.record)) };
247
295
  }
248
296
  function roomDraftOf(record, visible) {
249
297
  return structuredClone({
@@ -299,14 +347,40 @@ export function composeRuntime(options) {
299
347
  const value = presentations.get(app);
300
348
  return value === undefined ? null : structuredClone(value);
301
349
  }
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
- });
350
+ async function writtenRoomDraft(room, path, revision, input, next) {
351
+ const parts = readParts(input);
352
+ if (path === '' || normalize(path) !== path)
353
+ throw runtimeError('room-draft-malformed');
354
+ const local = await openLocal();
355
+ const current = roomDrafts.get(room)?.get(path);
356
+ if ((current?.revision ?? null) !== revision) {
357
+ return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
358
+ }
359
+ const file = parts.file === undefined
360
+ ? undefined
361
+ : await heldFile(parts.file.blob, parts.file.base === undefined
362
+ ? binaryBase(await visibleOf(room, [path]), path)
363
+ : parts.file.base);
364
+ const record = {
365
+ locator: room,
366
+ path,
367
+ revision: next,
368
+ parts: held(current?.parts, parts, file),
369
+ returned: current?.returned ?? null,
370
+ };
371
+ await local.keepRoomDraft(record, parts.file === undefined || file === undefined
372
+ ? []
373
+ : [{ key: file.localId, blob: parts.file.blob }]);
374
+ rememberRoomDraft(record);
375
+ publishChangeSet(changeSet(room, { drafts: [path] }));
376
+ return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
377
+ }
378
+ async function keptPresentation(app, next) {
379
+ const local = await openLocal();
380
+ const stored = next(presentations.get(app));
381
+ await local.present({ app, value: stored });
382
+ presentations.set(app, stored);
383
+ publishChangeSet(changeSet('', { presentation: app }));
310
384
  }
311
385
  function emitPresentation(app) {
312
386
  if (closed)
@@ -321,6 +395,20 @@ export function composeRuntime(options) {
321
395
  (queuesWithoutReplica.size > 0 ||
322
396
  [...replicas.values()].some((replica) => replica.queue.length > 0)));
323
397
  }
398
+ function unsent() {
399
+ const changed = new Map();
400
+ for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
401
+ for (const { batch, state } of entriesOf(room)) {
402
+ if (state === 'local')
403
+ continue;
404
+ for (const { path } of batch.changes)
405
+ changed.set(`${room}\u0000${path}`, { room, path });
406
+ }
407
+ }
408
+ return [...changed]
409
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
410
+ .map(([, change]) => change);
411
+ }
324
412
  function attention() {
325
413
  const attentionRequired = [];
326
414
  for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
@@ -332,16 +420,9 @@ export function composeRuntime(options) {
332
420
  returned: structuredClone(record.returned),
333
421
  });
334
422
  }
335
- for (const [room, refusal] of sourceRefusals)
336
- attentionRequired.push({ room, refusal: structuredClone(refusal) });
337
423
  attentionRequired.sort((left, right) => {
338
424
  if (left.room !== right.room)
339
425
  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
426
  if (left.path !== right.path)
346
427
  return left.path < right.path ? -1 : 1;
347
428
  return 0;
@@ -353,7 +434,7 @@ export function composeRuntime(options) {
353
434
  if (reset === null || submission.reset < reset)
354
435
  reset = submission.reset;
355
436
  }
356
- return { attentionRequired, reset };
437
+ return { attentionRequired, unsent: unsent(), reset };
357
438
  }
358
439
  function documentsIn(locator) {
359
440
  return replicas.get(locator)?.count ?? 0;
@@ -426,7 +507,7 @@ export function composeRuntime(options) {
426
507
  const governed = governs(reach, room);
427
508
  const attached = [
428
509
  ...(governed.collection ? [modelOf(room.locator)] : []),
429
- ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
510
+ ...(governed.selection ? [builtIn] : []),
430
511
  ];
431
512
  return {
432
513
  locator: room.locator,
@@ -442,11 +523,26 @@ export function composeRuntime(options) {
442
523
  const value = listed();
443
524
  for (const listener of roomListeners)
444
525
  listener(value);
526
+ emitKnownRooms();
527
+ }
528
+ function emitKnownRooms() {
529
+ conversations.learned();
530
+ if (unlistedRooms.size === 0)
531
+ return;
532
+ if (!roomsListed && rooms.size === 0 && refusal === null && !status.providerUnreachable)
533
+ return;
534
+ const value = listed();
535
+ for (const listener of [...unlistedRooms]) {
536
+ unlistedRooms.delete(listener);
537
+ roomListeners.add(listener);
538
+ listener(value);
539
+ }
445
540
  }
446
541
  function emitRefusal() {
447
542
  queueReadiness();
448
543
  for (const listener of refusalListeners)
449
544
  listener(refusal);
545
+ emitKnownRooms();
450
546
  }
451
547
  function emitStorageLoss() {
452
548
  queueReadiness();
@@ -467,6 +563,7 @@ export function composeRuntime(options) {
467
563
  if (closed)
468
564
  return;
469
565
  queueReadiness();
566
+ emitKnownRooms();
470
567
  const next = status.state === 'synced' && !conversations.current()
471
568
  ? { ...status, state: 'syncing' }
472
569
  : status;
@@ -480,9 +577,6 @@ export function composeRuntime(options) {
480
577
  if (localSaves)
481
578
  return Object.freeze({ batch, state: 'local' });
482
579
  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
580
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
487
581
  }
488
582
  function entriesOf(locator) {
@@ -501,18 +595,25 @@ export function composeRuntime(options) {
501
595
  function unpublished(error) {
502
596
  journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
503
597
  }
598
+ function indexed(locator) {
599
+ return replicas.get(locator)?.indexed === true;
600
+ }
504
601
  async function prepareReplica(locator) {
505
602
  for (;;) {
506
603
  const held = replicas.get(locator);
507
- if (held === undefined || closed || prepared.has(locator))
604
+ if (held === undefined || closed || held.indexed)
508
605
  return;
509
606
  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)
607
+ const done = await exclusive(async () => {
608
+ const local = await openLocal();
609
+ return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR));
610
+ });
611
+ const now = replicas.get(locator);
612
+ if (now?.record.key !== key)
512
613
  continue;
513
- if (!done)
614
+ if (!done.done)
514
615
  continue;
515
- prepared.add(locator);
616
+ replicas.set(locator, { ...now, indexed: true });
516
617
  publishChangeSet(changeSet(locator, { documents: null }));
517
618
  liveQueries.recover();
518
619
  queueReadiness();
@@ -520,7 +621,7 @@ export function composeRuntime(options) {
520
621
  }
521
622
  }
522
623
  function unprepared() {
523
- return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unpreparable.has(locator));
624
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
524
625
  }
525
626
  function unpreparedBy(locator, error) {
526
627
  unpreparable.add(locator);
@@ -581,14 +682,14 @@ export function composeRuntime(options) {
581
682
  pending.push({ room: locator, awaits: 'acquisition' });
582
683
  continue;
583
684
  }
584
- if (!prepared.has(locator)) {
685
+ if (!replica.indexed) {
585
686
  pending.push({ room: locator, awaits: 'preparation' });
586
687
  continue;
587
688
  }
588
689
  coverage.push({
589
690
  room: locator,
590
691
  generation: replica.record.transfer,
591
- current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
692
+ current: !(catching.has(locator) || transfers.has(locator)),
592
693
  });
593
694
  read.push({ locator, key: replica.record.key });
594
695
  for (const batch of replica.queue)
@@ -615,8 +716,6 @@ export function composeRuntime(options) {
615
716
  return { listed: named.every((room) => replicas.has(room)), rooms: named };
616
717
  }
617
718
  function roomState(locator) {
618
- if (sourceRefusals.has(locator))
619
- return 'refused';
620
719
  const listedRoom = discovered.get(locator);
621
720
  if (roomsListed &&
622
721
  (listedRoom === undefined ||
@@ -625,13 +724,13 @@ export function composeRuntime(options) {
625
724
  return 'unadmitted';
626
725
  if (!replicas.has(locator))
627
726
  return 'acquiring';
628
- if (!prepared.has(locator))
727
+ if (!indexed(locator))
629
728
  return 'preparing';
630
729
  return 'complete';
631
730
  }
632
731
  function conversationCoverage(reach) {
633
732
  const declared = reach === null || reach.conversations;
634
- if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
733
+ if (!declared || options.adapter.conversations === undefined)
635
734
  return 'undeclared';
636
735
  if (!conversations.known() || !conversations.current())
637
736
  return 'acquiring';
@@ -646,21 +745,47 @@ export function composeRuntime(options) {
646
745
  ? 'connected'
647
746
  : 'connecting';
648
747
  }
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
- });
748
+ function bytesOf(room, underway) {
749
+ const transfer = transfers.get(room);
750
+ if (transfer !== undefined) {
751
+ const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
752
+ return {
753
+ done: transfer.done.bytes - Math.ceil(owed),
754
+ total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
755
+ };
756
+ }
757
+ const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
758
+ return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
759
+ }
760
+ function installationOf(coverage) {
761
+ const rooms = { done: 0, total: 0 };
762
+ const bytes = { done: 0, total: 0 };
763
+ let installing = 0;
764
+ let downloading = false;
765
+ for (const { room, state } of coverage) {
766
+ if (state === 'unadmitted')
767
+ continue;
768
+ rooms.total += 1;
769
+ if (state === 'complete')
770
+ rooms.done += 1;
771
+ const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
772
+ if (underway)
773
+ installing += 1;
774
+ if (underway && (streaming.has(room) || !arrivals.has(room)))
775
+ downloading = true;
776
+ const now = bytesOf(room, underway);
777
+ const earlier = underway ? startedOver.get(room) : undefined;
778
+ bytes.done += Math.max(now.done, earlier?.done ?? 0);
779
+ bytes.total += Math.max(now.total, earlier?.total ?? 0);
780
+ }
781
+ const step = installing === 0
782
+ ? null
783
+ : downloading
784
+ ? 'downloading'
785
+ : bytes.done < bytes.total
786
+ ? 'writing'
787
+ : 'replaying';
788
+ return { step, rooms, bytes };
664
789
  }
665
790
  function readinessFor(port, reach) {
666
791
  const required = requiredRooms(reach);
@@ -682,18 +807,17 @@ export function composeRuntime(options) {
682
807
  persistence,
683
808
  connection: connectionOf(),
684
809
  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),
810
+ space: full ? 'full' : port.space,
811
+ installation: installationOf(coverage),
690
812
  lost: storageLoss !== null,
691
- established: targets[port.target]?.established === true,
813
+ established: targets[port.target]?.established === true ||
814
+ (usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
692
815
  });
693
816
  }
694
817
  function establish(name) {
695
818
  targets = { ...targets, [name]: { established: true } };
696
819
  queueReadiness();
820
+ probeEachMinute();
697
821
  void durably((local) => local.fact('targets', targets)).catch(() => {
698
822
  journal.record(`target record failed ${name}`);
699
823
  });
@@ -731,87 +855,56 @@ export function composeRuntime(options) {
731
855
  readinessWatches.delete(watch);
732
856
  };
733
857
  }
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)
858
+ function filled() {
859
+ if (full || closed)
755
860
  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);
861
+ full = true;
862
+ journal.record('space full');
863
+ queueReadiness();
864
+ probeEachMinute();
760
865
  }
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();
866
+ function installedTarget() {
867
+ return ((usableWhenHeld.length > 0 && reopened.size > 0) ||
868
+ Object.values(targets).some((target) => target.established));
772
869
  }
773
- function resume() {
774
- if (rechecking !== null)
775
- clearInterval(rechecking);
776
- rechecking = null;
777
- if (paused.size === 0 && capacity === null)
870
+ function probeEachMinute() {
871
+ if (!full || probing !== null || closed || !installedTarget())
778
872
  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;
873
+ // 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.
874
+ probing = setInterval(() => {
875
+ void probed();
876
+ }, 60_000);
795
877
  }
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)
801
- return true;
802
- if (await reclaimed(local))
803
- available = await estimated().catch(() => null);
804
- if (available === null || available >= needed)
878
+ function probed() {
879
+ testing ??= database.ask({ kind: 'space.probe' }).then(() => {
880
+ testing = null;
881
+ spaceFreed();
805
882
  return true;
806
- pause(locator, { cause: 'insufficient-space', needed, available });
807
- return false;
883
+ }, () => {
884
+ testing = null;
885
+ return false;
886
+ });
887
+ return testing;
808
888
  }
809
- async function exceededOn(locator, bytes) {
810
- const available = await estimated().catch(() => null);
811
- pause(locator, { cause: 'storage-exceeded', needed: bytes, available });
889
+ function spaceFreed() {
890
+ if (!full)
891
+ return;
892
+ full = false;
893
+ if (probing !== null)
894
+ clearInterval(probing);
895
+ probing = null;
896
+ unpreparable.clear();
897
+ journal.record('space freed');
898
+ queueReadiness();
899
+ if (closed || authorization === undefined)
900
+ return;
901
+ if (options.adapter.roomStream === undefined)
902
+ void sync('unstreamed', null);
903
+ else
904
+ void resynchronize();
812
905
  }
813
906
  function seedRoomHeld(room, version) {
814
- return prepared.has(room) && reachedBy(room, version);
907
+ return indexed(room) && reachedBy(room, version);
815
908
  }
816
909
  function seedHeld(at) {
817
910
  return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
@@ -934,6 +1027,10 @@ export function composeRuntime(options) {
934
1027
  }
935
1028
  function publishChangeSet(set) {
936
1029
  applyChangeSet(set);
1030
+ for (const { locator, path } of set.documents) {
1031
+ if (path !== null)
1032
+ attachedFiles.get(locator)?.delete(path);
1033
+ }
937
1034
  if (set.rooms.length > 0 ||
938
1035
  [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
939
1036
  emitRooms();
@@ -952,6 +1049,8 @@ export function composeRuntime(options) {
952
1049
  }
953
1050
  async function refuse(reason, provider = null) {
954
1051
  discoveryPaused = true;
1052
+ inFlight.abort();
1053
+ inFlight = new AbortController();
955
1054
  try {
956
1055
  if (reason === 'revoked-authorization') {
957
1056
  await exclusive(async () => {
@@ -959,27 +1058,24 @@ export function composeRuntime(options) {
959
1058
  const records = [...replicas].map(([locator, held]) => ({
960
1059
  locator,
961
1060
  held,
962
- record: { ...held.record, model: revoked, selectionModel: revoked },
1061
+ record: { ...held.record, model: revoked },
963
1062
  }));
1063
+ let answer = null;
964
1064
  try {
965
1065
  const local = await openLocal();
966
- await local.commit({
1066
+ answer = await local.commit({
967
1067
  rooms: records.map(({ locator, record }) => ({
968
1068
  locator,
969
1069
  key: record.key,
970
1070
  record: storedRecord(record),
971
- composition: compositionOf(record),
1071
+ composed: composedOf(composed(record)),
972
1072
  })),
973
1073
  });
974
1074
  }
975
1075
  catch {
976
1076
  journal.record('model revocation persistence failed');
977
1077
  }
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
- }
1078
+ const keys = new Set([...models.keys(), ...rooms.keys()]);
983
1079
  for (const key of keys) {
984
1080
  invalidModels.add(key);
985
1081
  models.set(key, revoked);
@@ -989,8 +1085,8 @@ export function composeRuntime(options) {
989
1085
  for (const { locator, held, record } of records) {
990
1086
  if (replicas.get(locator) !== held)
991
1087
  continue;
992
- recomposed(locator, held.record, record);
993
- replicas.set(locator, { ...held, record });
1088
+ const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
1089
+ replicas.set(locator, { ...held, record, indexed });
994
1090
  }
995
1091
  for (const locator of [...rooms.keys()]) {
996
1092
  const room = rooms.get(locator);
@@ -1034,15 +1130,15 @@ export function composeRuntime(options) {
1034
1130
  ? null
1035
1131
  : {
1036
1132
  roots: selection.roots,
1037
- model: modelTypes(modelOf(room.locator, 'typed-selection')),
1133
+ model: modelTypes(builtIn),
1038
1134
  },
1039
1135
  providerOwnedPaths: room.providerOwnedPaths,
1040
1136
  roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
1041
1137
  synchronizationAvailable,
1042
- refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
1138
+ refusal: scopedRefusals.get(room.locator) ?? null,
1043
1139
  });
1044
1140
  }
1045
- async function applyDiscovery(found, started, covered) {
1141
+ async function applyDiscovery(found, started, left) {
1046
1142
  const lockedBefore = lockedRooms();
1047
1143
  for (const room of found) {
1048
1144
  const fence = fences.get(room.locator);
@@ -1056,18 +1152,11 @@ export function composeRuntime(options) {
1056
1152
  const listedLocators = new Set(listed.map((room) => room.locator));
1057
1153
  for (const locator of listedLocators)
1058
1154
  scopedRefusals.delete(locator);
1059
- if (covered === undefined)
1155
+ if (left === undefined)
1060
1156
  discovered.clear();
1061
- else
1062
- for (const locator of covered)
1063
- discovered.delete(locator);
1064
1157
  for (const room of listed)
1065
1158
  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);
1159
+ await loseRooms(left ?? unlistedHeld(new Set(found.map((room) => room.locator))));
1071
1160
  for (const room of listed) {
1072
1161
  const held = replicas.get(room.locator)?.record;
1073
1162
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -1098,15 +1187,6 @@ export function composeRuntime(options) {
1098
1187
  ]),
1099
1188
  ].filter((locator) => !listed.has(locator));
1100
1189
  }
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
1190
  function fence(locators) {
1111
1191
  const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1112
1192
  const removed = fresh.map((locator) => ({
@@ -1123,25 +1203,19 @@ export function composeRuntime(options) {
1123
1203
  queuesWithoutReplica.delete(locator);
1124
1204
  rooms.delete(locator);
1125
1205
  roomDrafts.delete(locator);
1126
- transfers.delete(locator);
1206
+ attachedFiles.delete(locator);
1207
+ untransferred(locator);
1208
+ startedOver.delete(locator);
1127
1209
  catching.delete(locator);
1128
1210
  discovered.delete(locator);
1129
- announced.delete(locator);
1130
1211
  unreadable.delete(locator);
1131
1212
  stale.delete(locator);
1132
1213
  repairable.delete(locator);
1133
1214
  unpreparable.delete(locator);
1134
- paused.delete(locator);
1135
- prepared.delete(locator);
1136
1215
  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
- }
1216
+ models.delete(locator);
1217
+ liveModels.delete(locator);
1218
+ invalidModels.delete(locator);
1145
1219
  journal.record(`room lost ${locator}`);
1146
1220
  }
1147
1221
  if (fresh.length === 0)
@@ -1189,7 +1263,14 @@ export function composeRuntime(options) {
1189
1263
  });
1190
1264
  await eraseFenced(fresh);
1191
1265
  }
1192
- function restored(key, persisted) {
1266
+ function retyped(persisted) {
1267
+ if (persisted.status === 'unavailable')
1268
+ return persisted;
1269
+ const { state } = attachedModel({ kind: 'model', snapshot: persisted.snapshot }, NO_MODEL, roomTypes);
1270
+ return state.status === 'unavailable' ? state : { ...persisted, types: state.types };
1271
+ }
1272
+ function restored(key, held) {
1273
+ const persisted = retyped(held);
1193
1274
  const prior = models.get(key);
1194
1275
  if (prior !== undefined &&
1195
1276
  prior.status !== 'unavailable' &&
@@ -1220,8 +1301,9 @@ export function composeRuntime(options) {
1220
1301
  const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1221
1302
  targets = held.targets;
1222
1303
  transfers.clear();
1223
- for (const [locator, record] of held.transfers)
1224
- transfers.set(locator, record);
1304
+ arrivals.clear();
1305
+ streaming.clear();
1306
+ startedOver.clear();
1225
1307
  limits = held.providerLimits;
1226
1308
  capabilities = held.capabilities;
1227
1309
  member = held.member;
@@ -1231,7 +1313,7 @@ export function composeRuntime(options) {
1231
1313
  presentations.clear();
1232
1314
  for (const record of held.presentation)
1233
1315
  presentations.set(record.app, record.value);
1234
- conversations.hydrate(heldConversations, chatDrafts);
1316
+ conversations.hydrate(heldConversations, chatDrafts, held.conversationList);
1235
1317
  pendings.clear();
1236
1318
  for (const queue of queues) {
1237
1319
  for (const { batch, state } of queue.pending) {
@@ -1239,7 +1321,7 @@ export function composeRuntime(options) {
1239
1321
  }
1240
1322
  }
1241
1323
  replicas.clear();
1242
- prepared.clear();
1324
+ reopened.clear();
1243
1325
  queuesWithoutReplica.clear();
1244
1326
  for (const [locator, queue] of queuedBatches) {
1245
1327
  if (!held.rooms.has(locator))
@@ -1248,39 +1330,40 @@ export function composeRuntime(options) {
1248
1330
  records.forEach((stored, index) => {
1249
1331
  if (fences.has(stored.locator))
1250
1332
  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 };
1333
+ const record = { ...stored, model: restored(stored.locator, stored.model) };
1334
+ reopened.add(record.locator);
1254
1335
  replicas.set(record.locator, {
1255
1336
  record,
1256
1337
  queue: queuedBatches.get(record.locator) ?? [],
1257
1338
  overlaid: held.overlaid.get(record.key) ?? new Set(),
1258
1339
  count: counts[index] ?? 0,
1259
1340
  confirmed: 0,
1341
+ indexed: !held.unindexed.has(record.key),
1260
1342
  });
1261
- if (!held.unprepared.has(record.key))
1262
- prepared.add(record.locator);
1263
1343
  const existing = rooms.get(record.locator);
1264
1344
  if (existing === undefined)
1265
- list(record, false);
1345
+ list(record, record.roomVersion !== null);
1266
1346
  else {
1267
1347
  rooms.set(record.locator, {
1268
1348
  ...existing,
1269
1349
  roomVersion: record.roomVersion,
1270
1350
  operations: record.operations,
1271
1351
  replica: record.replica,
1272
- model: modelTypes(model),
1352
+ model: modelTypes(record.model),
1273
1353
  typedSelection: record.typedSelection === null
1274
1354
  ? null
1275
- : {
1276
- roots: record.typedSelection.roots,
1277
- model: modelTypes(selectionModel),
1278
- },
1355
+ : { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
1279
1356
  });
1280
1357
  }
1281
1358
  });
1282
- for (const key of held.orphans)
1283
- await opened.release(key);
1359
+ try {
1360
+ for (const key of held.orphans)
1361
+ await opened.release(key);
1362
+ }
1363
+ catch (error) {
1364
+ if (!refusedForSpace(error))
1365
+ throw error;
1366
+ }
1284
1367
  if (held.erasing || held.orphans.length > 0)
1285
1368
  queueMicrotask(sweep);
1286
1369
  return {
@@ -1305,7 +1388,7 @@ export function composeRuntime(options) {
1305
1388
  async function open() {
1306
1389
  if (closed)
1307
1390
  throw new Error('runtime closed');
1308
- const opened = openRuntimeStore(isFenced, options.database);
1391
+ const opened = openRuntimeStore(isFenced, database);
1309
1392
  const interrupted = await opened.fences();
1310
1393
  for (const locator of interrupted) {
1311
1394
  fences.set(locator, { at: discoveries, erased: false });
@@ -1351,10 +1434,29 @@ export function composeRuntime(options) {
1351
1434
  return opening;
1352
1435
  }
1353
1436
  function exclusive(work) {
1437
+ gathering = null;
1354
1438
  const next = mutations.then(work, work);
1355
1439
  mutations = next.then(() => undefined, () => undefined);
1356
1440
  return next;
1357
1441
  }
1442
+ function gathered(key, input, merge, work) {
1443
+ const tail = gathering;
1444
+ if (tail !== null && tail.key === key) {
1445
+ const merged = merge(tail.input, input);
1446
+ if (merged !== null) {
1447
+ tail.input = merged;
1448
+ return tail.done;
1449
+ }
1450
+ }
1451
+ const entry = { key, input, done: Promise.resolve() };
1452
+ entry.done = exclusive(() => {
1453
+ if (gathering === entry)
1454
+ gathering = null;
1455
+ return work(entry.input);
1456
+ });
1457
+ gathering = entry;
1458
+ return entry.done;
1459
+ }
1358
1460
  function withdrawable(locator) {
1359
1461
  return (batch) => {
1360
1462
  const state = pendings.get(pendingKey(locator, batch.id));
@@ -1436,26 +1538,14 @@ export function composeRuntime(options) {
1436
1538
  },
1437
1539
  };
1438
1540
  }
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))
1541
+ function committedRoom(locator, answer) {
1542
+ const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
1543
+ if (!indexed)
1454
1544
  prepare([locator]);
1455
- return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0 };
1545
+ return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
1456
1546
  }
1457
1547
  function replayed(record, confirmedLayer, queue) {
1458
- return stepped(replaying(confirmedLayer, queue, record.locator, compositionOf(record)));
1548
+ return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
1459
1549
  }
1460
1550
  async function overlayDiff(record, before, visible, touched) {
1461
1551
  const kept = before !== undefined && before.record.key === record.key
@@ -1475,8 +1565,6 @@ export function composeRuntime(options) {
1475
1565
  function unpairedIn(locator, batch) {
1476
1566
  if (!queueIn(locator).some((held) => held.id === batch.id))
1477
1567
  return false;
1478
- if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
1479
- return false;
1480
1568
  return !leavingIn(batch, locator) || withdrawable(locator)(batch);
1481
1569
  }
1482
1570
  async function together(local, settlements) {
@@ -1518,7 +1606,7 @@ export function composeRuntime(options) {
1518
1606
  const confirmedLayer = overrides === undefined
1519
1607
  ? read
1520
1608
  : await stepped(slicingWith(read, overrides.set, overrides.removed));
1521
- const composition = compositionOf(record);
1609
+ const composition = composed(record);
1522
1610
  const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
1523
1611
  const founded = kept.filter((batch) => !queue.includes(batch));
1524
1612
  const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
@@ -1561,6 +1649,7 @@ export function composeRuntime(options) {
1561
1649
  key: record.key,
1562
1650
  record: storedRecord(record),
1563
1651
  roots: record.typedSelection?.roots ?? [],
1652
+ composed: composedOf(composition),
1564
1653
  ...settling.commit,
1565
1654
  ...(overrides === undefined
1566
1655
  ? {}
@@ -1602,14 +1691,14 @@ export function composeRuntime(options) {
1602
1691
  const confirmedChanged = settling.commit?.published !== undefined ||
1603
1692
  settling.commit?.refreshed === true ||
1604
1693
  overrides !== undefined;
1605
- recomposed(locator, before?.record, record);
1606
- const { count } = committedRoom(locator, record, answer);
1694
+ const { count, indexed } = committedRoom(locator, answer);
1607
1695
  replicas.set(locator, {
1608
1696
  record,
1609
1697
  queue: kept,
1610
1698
  overlaid: touched,
1611
1699
  count,
1612
1700
  confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
1701
+ indexed,
1613
1702
  });
1614
1703
  return mergeChangeSets([
1615
1704
  changeSet(locator, {
@@ -1624,103 +1713,96 @@ export function composeRuntime(options) {
1624
1713
  };
1625
1714
  }
1626
1715
  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
- }
1716
+ const model = record.model;
1717
+ models.set(locator, model);
1718
+ invalidModels.delete(locator);
1719
+ if (model.status === 'current')
1720
+ liveModels.set(locator, JSON.stringify(model.snapshot));
1721
+ else if (model.status === 'unavailable')
1722
+ liveModels.delete(locator);
1639
1723
  }
1640
1724
  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
- };
1725
+ const answer = answers?.collection;
1726
+ if (answer === undefined)
1727
+ return held;
1728
+ const { state, refused } = attachedModel(answer, held.collection, roomTypes);
1729
+ if (refused !== null) {
1730
+ const where = refused.location === undefined ? '' : ` ${refused.location}`;
1731
+ journal.record(`model invalid ${locator} collection ${refused.identity} ${refused.rule}${where}`);
1732
+ }
1733
+ models.set(locator, state);
1734
+ return { collection: state };
1656
1735
  }
1657
- function publish(local, room, record, landing, live) {
1658
- return exclusive(async () => {
1736
+ function publish(local, room, reached, landing, live) {
1737
+ const landed = exclusive(async () => {
1659
1738
  if (!live()) {
1660
1739
  caughtUpTo(room.locator);
1661
1740
  return true;
1662
1741
  }
1663
1742
  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);
1743
+ if (!stands(room.locator, landing.from)) {
1744
+ if (landing.kind === 'refreshed') {
1745
+ await local.unrefresh(reached.key);
1746
+ return false;
1747
+ }
1748
+ caughtUpTo(room.locator);
1749
+ await abandoned(local, room.locator);
1667
1750
  return false;
1668
1751
  }
1752
+ const record = landing.kind === 'refreshed' && prior !== undefined
1753
+ ? {
1754
+ ...prior.record,
1755
+ roomVersion: reached.roomVersion,
1756
+ ...(landing.remodelled ? { model: reached.model } : {}),
1757
+ }
1758
+ : reached;
1669
1759
  adoptModels(room.locator, record);
1670
1760
  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
1761
  let set;
1677
1762
  try {
1678
- set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
1763
+ set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
1679
1764
  ? {
1680
1765
  room: true,
1681
1766
  view: 'refreshed',
1682
- commit: { refreshed: true, composition: compositionOf(record) },
1767
+ commit: { refreshed: true },
1683
1768
  }
1684
1769
  : {
1685
1770
  room: true,
1686
1771
  view: landing.refreshed ? 'refreshed' : 'confirmed',
1687
1772
  commit: {
1688
1773
  published: { from: prior?.record.key ?? null },
1689
- composition: compositionOf(record),
1690
1774
  ...(landing.refreshed ? { refreshed: true } : {}),
1691
1775
  },
1692
1776
  });
1693
1777
  }
1694
1778
  catch (error) {
1695
- if (!exceeded(error))
1779
+ if (!refusedForSpace(error))
1696
1780
  throw error;
1697
- await exceededOn(room.locator, null);
1698
1781
  caughtUpTo(room.locator);
1699
1782
  return true;
1700
1783
  }
1701
1784
  if (landing.kind === 'bootstrapped') {
1702
- transfers.delete(room.locator);
1785
+ untransferred(room.locator);
1786
+ startedOver.delete(room.locator);
1703
1787
  catching.delete(room.locator);
1704
- sweep();
1788
+ if (prior !== undefined && prior.record.key !== record.key)
1789
+ sweep();
1790
+ name(room.locator);
1705
1791
  }
1706
- for (const batch of accepted)
1707
- pendings.delete(pendingKey(room.locator, batch.id));
1708
1792
  stale.delete(room.locator);
1709
1793
  unpreparable.delete(room.locator);
1710
1794
  if (unreadable.delete(room.locator))
1711
1795
  scopedRefusals.delete(room.locator);
1712
- sourceRefusals.delete(room.locator);
1713
- declarationRefusals.delete(room.locator);
1714
1796
  list(room, true);
1715
1797
  publishChangeSet(set);
1716
1798
  await unpair(local);
1717
1799
  moveTo({ ...status, ...attention() });
1718
- options.adapter.roomStream?.published(room.locator, roomVersion);
1719
- if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1800
+ if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1720
1801
  submitLater(room.locator);
1721
1802
  }
1722
1803
  return true;
1723
1804
  });
1805
+ return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
1724
1806
  }
1725
1807
  async function savedRoom(locator) {
1726
1808
  const local = await openLocal();
@@ -1745,12 +1827,7 @@ export function composeRuntime(options) {
1745
1827
  return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
1746
1828
  }
1747
1829
  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
- }
1830
+ const read = readSavedParts(named);
1754
1831
  const target = await savedRoom(locator);
1755
1832
  const saved = [];
1756
1833
  const files = [];
@@ -1759,7 +1836,7 @@ export function composeRuntime(options) {
1759
1836
  for (const { path, parts } of read) {
1760
1837
  const file = parts.file === undefined
1761
1838
  ? undefined
1762
- : await heldFile(parts.file.blob, binaryBase(visible, path));
1839
+ : await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
1763
1840
  if (parts.file !== undefined && file !== undefined) {
1764
1841
  files.push({ key: file.localId, blob: parts.file.blob });
1765
1842
  }
@@ -1794,13 +1871,14 @@ export function composeRuntime(options) {
1794
1871
  const referring = removed.length === 0
1795
1872
  ? []
1796
1873
  : (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
1797
- const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
1874
+ const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
1798
1875
  const { visible } = await replayed(record, confirmedLayer, replica.queue);
1799
- const composition = compositionOf(record);
1876
+ const composition = composed(record);
1800
1877
  return {
1801
1878
  record,
1802
1879
  confirmed: confirmedLayer,
1803
1880
  visible,
1881
+ folders: new Set(await local.folders(record.key, 'visible', paths)),
1804
1882
  *visibleOver(queue) {
1805
1883
  return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
1806
1884
  },
@@ -1812,13 +1890,7 @@ export function composeRuntime(options) {
1812
1890
  return false;
1813
1891
  const local = await openLocal();
1814
1892
  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)));
1893
+ return local.typedWithin(record.key, 'visible', roots, false);
1822
1894
  }
1823
1895
  function textDiffers(left, right) {
1824
1896
  return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
@@ -1829,18 +1901,6 @@ export function composeRuntime(options) {
1829
1901
  return undefined;
1830
1902
  return entry.binary !== null ? null : entry.content;
1831
1903
  }
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
1904
  function lookupOf(local, key, view) {
1845
1905
  return async (paths) => {
1846
1906
  if (paths.length === 0)
@@ -1857,37 +1917,6 @@ export function composeRuntime(options) {
1857
1917
  const read = parseFrontmatter(entry.content ?? '');
1858
1918
  return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
1859
1919
  }
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
1920
  function overriddenLookup(lookup, overrides) {
1892
1921
  return async (paths) => {
1893
1922
  const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
@@ -1905,9 +1934,8 @@ export function composeRuntime(options) {
1905
1934
  }
1906
1935
  function validatorOf(record, confirmedLayer) {
1907
1936
  return async (resulting, touched) => {
1908
- const composition = compositionFor(record);
1909
1937
  const local = await openLocal();
1910
- const changed = [];
1938
+ const changes = [];
1911
1939
  const overrides = new Map();
1912
1940
  const pace = paced();
1913
1941
  for (const path of new Set([...confirmedLayer.held, ...touched])) {
@@ -1918,19 +1946,13 @@ export function composeRuntime(options) {
1918
1946
  continue;
1919
1947
  overrides.set(path, now);
1920
1948
  if (textOf(was) !== textOf(now))
1921
- changed.push({ path, was: textOf(was), now: textOf(now) });
1949
+ changes.push({ path, was: textOf(was), now: textOf(now) });
1922
1950
  }
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);
1951
+ return validatedNeighbourhood(changes, compositionFor(record), {
1952
+ lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
1953
+ texts: textsOf(local, record.key, 'confirmed'),
1954
+ referrers: referrersOf(local, record.key, 'confirmed'),
1955
+ });
1934
1956
  };
1935
1957
  }
1936
1958
  function textsOf(local, key, view) {
@@ -1954,6 +1976,7 @@ export function composeRuntime(options) {
1954
1976
  operations: room.operations,
1955
1977
  providerOwnedPaths: room.providerOwnedPaths,
1956
1978
  visible: saving.visible,
1979
+ folders: saving.folders,
1957
1980
  queued: queuedDocuments(replica.queue),
1958
1981
  unmodeled: await unmodeledIn(record),
1959
1982
  validate: validatorOf(record, saving.confirmed),
@@ -1974,6 +1997,7 @@ export function composeRuntime(options) {
1974
1997
  locator,
1975
1998
  key: saving.record.key,
1976
1999
  roots: saving.record.typedSelection?.roots ?? [],
2000
+ composed: composedOf(composed(saving.record)),
1977
2001
  overlay: diff.overlay,
1978
2002
  });
1979
2003
  }
@@ -2005,7 +2029,7 @@ export function composeRuntime(options) {
2005
2029
  }
2006
2030
  const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
2007
2031
  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)));
2032
+ const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
2009
2033
  if (changed.length > 0)
2010
2034
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2011
2035
  const target = replicas.get(destination);
@@ -2117,6 +2141,8 @@ export function composeRuntime(options) {
2117
2141
  submitLater(locator);
2118
2142
  }
2119
2143
  async function committed(locator, target, saved, consumed, writtenBy, files = []) {
2144
+ if (full)
2145
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
2120
2146
  const into = movedInto(locator, saved);
2121
2147
  if (into !== null) {
2122
2148
  return committedMove(locator, into, target, saved, consumed, writtenBy, files);
@@ -2128,7 +2154,7 @@ export function composeRuntime(options) {
2128
2154
  ...saved.map(({ path }) => path),
2129
2155
  ...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
2130
2156
  ]);
2131
- const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, underneath(held, path, saving.visible)));
2157
+ const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
2132
2158
  if (changed.length > 0)
2133
2159
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2134
2160
  const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
@@ -2156,6 +2182,7 @@ export function composeRuntime(options) {
2156
2182
  providerLimits: limits,
2157
2183
  handle: room.handle,
2158
2184
  visible: saving.visible,
2185
+ folders: saving.folders,
2159
2186
  changes,
2160
2187
  bases: parts.flatMap(({ bases }) => bases),
2161
2188
  queued: queuedDocuments(replica.queue),
@@ -2306,6 +2333,9 @@ export function composeRuntime(options) {
2306
2333
  async function readHistory(locator, path, query = {}) {
2307
2334
  if (closed || refusal !== null)
2308
2335
  return UNCERTAIN;
2336
+ await firstDiscovery;
2337
+ if (!roomsNamed)
2338
+ return UNCERTAIN;
2309
2339
  try {
2310
2340
  await ensureAuthorization();
2311
2341
  }
@@ -2395,14 +2425,12 @@ export function composeRuntime(options) {
2395
2425
  return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
2396
2426
  };
2397
2427
  }
2398
- function binaryRoom(request) {
2399
- if (request.kind === 'path')
2400
- return request.locator;
2401
- return options.adapter.referrerScope(request.referrer)?.locator ?? null;
2402
- }
2403
2428
  async function transferBinary(request, transfer, answer) {
2404
2429
  if (closed || refusal !== null)
2405
2430
  return UNCERTAIN;
2431
+ await firstDiscovery;
2432
+ if (!roomsNamed)
2433
+ return UNCERTAIN;
2406
2434
  try {
2407
2435
  await ensureAuthorization();
2408
2436
  }
@@ -2412,16 +2440,13 @@ export function composeRuntime(options) {
2412
2440
  if (closed || authorization === undefined || refusal !== null)
2413
2441
  return UNCERTAIN;
2414
2442
  const since = fencings;
2415
- journal.record(request.kind === 'path'
2416
- ? `transfer binary ${request.locator} ${request.path}`
2417
- : `transfer binary ${request.fileRef}`);
2443
+ journal.record(`transfer binary ${request.locator} ${request.path}`);
2418
2444
  try {
2419
2445
  const result = await transfer(authorization);
2420
2446
  const settled = result;
2421
2447
  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 });
2448
+ if (fencedSince(request.locator, since))
2449
+ return failure({ kind: 'room-not-readable', locator: request.locator });
2425
2450
  if (settled.kind === 'scoped-refusal') {
2426
2451
  const room = replicas.get(settled.locator)?.record;
2427
2452
  if (room !== undefined)
@@ -2436,23 +2461,27 @@ export function composeRuntime(options) {
2436
2461
  return UNCERTAIN;
2437
2462
  }
2438
2463
  }
2464
+ function fetchBinary(request, name) {
2465
+ return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2466
+ }
2439
2467
  async function readBinary(request, name) {
2440
2468
  if (closed)
2441
2469
  return UNCERTAIN;
2442
2470
  const held = await keptFiles.find(name);
2443
2471
  if (held !== null)
2444
2472
  return held;
2445
- return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2473
+ return fetchBinary(request, name);
2446
2474
  }
2447
2475
  async function binaryPath(locator, path, revision) {
2448
2476
  if (revision !== undefined)
2449
2477
  return { kind: 'path', locator, path, revision };
2450
2478
  const replica = replicas.get(locator);
2451
- if (replica === undefined)
2452
- return null;
2453
2479
  const local = await openLocal();
2454
- const [held] = await local.entries(replica.record.key, 'visible', [path]);
2455
- if (held?.binary === undefined || held.binary === null)
2480
+ const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
2481
+ const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
2482
+ if (attached !== undefined)
2483
+ return { kind: 'path', locator, path, revision: attached.revision };
2484
+ if (replica === undefined || held?.binary === undefined || held.binary === null)
2456
2485
  return null;
2457
2486
  if (held.binary.state === 'pending')
2458
2487
  return { kind: 'pending', localId: held.binary.localId };
@@ -2468,7 +2497,7 @@ export function composeRuntime(options) {
2468
2497
  const holds = (at) => {
2469
2498
  const kept = confirmedLayer.binaries[at];
2470
2499
  return (kept?.state === 'accepted' &&
2471
- kept.fileRef === record.fileRef &&
2500
+ kept.sha256 === record.sha256 &&
2472
2501
  confirmedLayer.revisions[at] === record.revision);
2473
2502
  };
2474
2503
  let confirmedAt = path;
@@ -2482,58 +2511,93 @@ export function composeRuntime(options) {
2482
2511
  locator,
2483
2512
  path: confirmedAt,
2484
2513
  revision: record.revision,
2485
- fileRef: record.fileRef,
2486
2514
  };
2487
2515
  }
2488
- async function readBinaryPath(locator, path, revision) {
2516
+ async function askedBinary(locator, path, revision) {
2489
2517
  await ready;
2490
2518
  const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2491
- const asked = drafted !== undefined && drafted.localId === revision
2519
+ return drafted !== undefined && drafted.localId === revision
2492
2520
  ? { kind: 'pending', localId: revision }
2493
- : await binaryPath(locator, path, revision);
2521
+ : binaryPath(locator, path, revision);
2522
+ }
2523
+ function pendingBinary(locator, path, localId) {
2524
+ const facts = queuedFacts(locator, path, localId);
2525
+ return facts === null
2526
+ ? absentBinary(path)
2527
+ : {
2528
+ kind: 'binary',
2529
+ file: localId,
2530
+ byteLength: facts.byteLength,
2531
+ mediaType: facts.mediaType,
2532
+ };
2533
+ }
2534
+ async function readBinaryPath(locator, path, revision) {
2535
+ const asked = await askedBinary(locator, path, revision);
2494
2536
  if (asked === null)
2495
2537
  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
- }
2538
+ if (asked.kind === 'pending')
2539
+ return pendingBinary(locator, path, asked.localId);
2507
2540
  return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
2508
2541
  }
2542
+ async function heldMediaType(locator, path) {
2543
+ const replica = replicas.get(locator);
2544
+ const [held] = replica === undefined
2545
+ ? []
2546
+ : await (await openLocal()).entries(replica.record.key, 'visible', [path]);
2547
+ return held === undefined
2548
+ ? attachedFiles.get(locator)?.get(path)?.mediaType
2549
+ : held.binary?.mediaType;
2550
+ }
2551
+ function linkOf(asked, held) {
2552
+ return transferBinary(asked, (authorization) => options.adapter.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
2553
+ }
2509
2554
  async function linkBinaryPath(locator, path, revision) {
2510
2555
  await ready;
2511
2556
  const asked = await binaryPath(locator, path, revision);
2512
2557
  if (asked === null || asked.kind === 'pending')
2513
2558
  return absentBinary(path);
2514
- return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
2559
+ return linkOf(asked, revision === undefined ? await heldMediaType(locator, path) : undefined);
2515
2560
  }
2516
- async function readBinaryReference(reference) {
2561
+ async function showBinaryPath(locator, path, revision) {
2562
+ const asked = await askedBinary(locator, path, revision);
2563
+ if (asked === null)
2564
+ return absentBinary(path);
2565
+ if (asked.kind === 'pending')
2566
+ return pendingBinary(locator, path, asked.localId);
2567
+ if (closed)
2568
+ return UNCERTAIN;
2569
+ const name = { locator, path: asked.path, revision: asked.revision };
2570
+ const kept = await keptFiles.find(name);
2571
+ if (kept !== null)
2572
+ return kept;
2573
+ const mediaType = await heldMediaType(locator, path);
2574
+ if (mediaType?.startsWith('image/') === true)
2575
+ return fetchBinary(asked, name);
2576
+ return linkOf(asked, revision === undefined ? mediaType : undefined);
2577
+ }
2578
+ async function attachBinary(locator, file) {
2517
2579
  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 });
2580
+ await firstDiscovery;
2581
+ if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
2582
+ return OFFLINE;
2583
+ const request = await attachedFile(locator, file);
2584
+ return transferBinary({ locator, path: file.name }, async (held) => {
2585
+ const result = await options.adapter.attach(held, request);
2586
+ if (result.kind === 'attached') {
2587
+ const landed = attachedFiles.get(locator) ?? new Map();
2588
+ landed.set(result.path, { revision: result.revision, mediaType: request.mediaType });
2589
+ attachedFiles.set(locator, landed);
2590
+ }
2591
+ return result;
2592
+ }, attachAnswer);
2522
2593
  }
2523
2594
  function readPart(file, part) {
2524
- return options.database.ask({ kind: 'file.part', key: file, part });
2595
+ return database.ask({ kind: 'file.part', key: file, part });
2525
2596
  }
2526
2597
  async function cleanUp() {
2527
- const freed = await options.database.ask({ kind: 'opened.clean' });
2598
+ const freed = await database.ask({ kind: 'opened.clean' });
2528
2599
  return freed + ((await options.shell.cleanUp?.()) ?? 0);
2529
2600
  }
2530
- function declaredAgents() {
2531
- for (const capability of capabilities) {
2532
- if (capability.conversationAgents !== undefined)
2533
- return capability.conversationAgents;
2534
- }
2535
- return undefined;
2536
- }
2537
2601
  function declaredAction(name) {
2538
2602
  for (const capability of capabilities) {
2539
2603
  for (const action of capability.actions ?? []) {
@@ -2585,6 +2649,24 @@ export function composeRuntime(options) {
2585
2649
  return UNCERTAIN;
2586
2650
  }
2587
2651
  }
2652
+ async function readConversationHistory(request) {
2653
+ await ready;
2654
+ const stream = options.adapter.conversations;
2655
+ if (closed || refusal !== null || stream?.history === undefined)
2656
+ return UNCERTAIN;
2657
+ journal.record('transfer conversation history');
2658
+ try {
2659
+ const result = await stream.history(request);
2660
+ if (result.kind === 'revoked-authorization')
2661
+ await refuse('revoked-authorization');
2662
+ journal.record(`outcome conversation history ${result.kind}`);
2663
+ return calledResult(result);
2664
+ }
2665
+ catch {
2666
+ journal.record('outcome conversation history transport-uncertainty');
2667
+ return UNCERTAIN;
2668
+ }
2669
+ }
2588
2670
  function unsettled(result) {
2589
2671
  return result.kind !== 'action' && result.retryable;
2590
2672
  }
@@ -2656,169 +2738,222 @@ export function composeRuntime(options) {
2656
2738
  options.adapter.reached(locator, written, roomVersion) &&
2657
2739
  !options.adapter.reached(locator, roomVersion, written));
2658
2740
  }
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;
2753
- }
2754
- function refreshingOf(local, key, liveness) {
2741
+ function refreshingOf(local, key, composition, liveness, replaying = null) {
2742
+ const write = replaying === null ? exclusive : finishingWrite;
2755
2743
  return {
2744
+ turn() {
2745
+ if (replaying !== null)
2746
+ installing(replaying);
2747
+ },
2756
2748
  prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
2757
2749
  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 () => {
2750
+ for (let written = 0;;) {
2751
+ const end = batchEnd(page.entries, written);
2752
+ const last = end === page.entries.length;
2753
+ const entries = page.entries.slice(written, end);
2754
+ const staged = await write(async () => {
2762
2755
  if (!liveness())
2763
2756
  return false;
2764
- await local.refresh(key, last
2757
+ await timed(stageBudget, () => local.refresh(key, composition, last
2765
2758
  ? { ...page, entries }
2766
- : { entries, removals: [], folders: [], folderRemovals: [] });
2759
+ : { entries, removals: [], folders: [], folderRemovals: [] }));
2767
2760
  return true;
2768
2761
  });
2769
2762
  if (!staged)
2770
2763
  return false;
2764
+ if (last)
2765
+ return true;
2766
+ written = end;
2771
2767
  }
2772
- return true;
2773
2768
  },
2774
2769
  };
2775
2770
  }
2776
- function batched(entries) {
2777
- const batches = [[]];
2771
+ function batchEnd(entries, from) {
2772
+ let end = from;
2778
2773
  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);
2774
+ while (end < entries.length) {
2775
+ const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
2776
+ if (spent + size > stageBudget.bytes && end > from)
2777
+ break;
2786
2778
  spent += size;
2779
+ end += 1;
2780
+ }
2781
+ return end;
2782
+ }
2783
+ async function timed(budget, work) {
2784
+ const started = performance.now();
2785
+ const done = await work();
2786
+ const spent = performance.now() - started;
2787
+ if (spent < budget.aim / 2)
2788
+ budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
2789
+ else if (spent > budget.aim * 2) {
2790
+ budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
2791
+ }
2792
+ return done;
2793
+ }
2794
+ function installWrite(work) {
2795
+ return installTurn(installTurns.starting, work);
2796
+ }
2797
+ function finishingWrite(work) {
2798
+ return installTurn(installTurns.finishing, work);
2799
+ }
2800
+ function installTurn(queue, work) {
2801
+ return new Promise((resolve, reject) => {
2802
+ queue.push(() => void exclusive(work)
2803
+ .then(resolve, reject)
2804
+ .finally(() => {
2805
+ installTurns.busy = false;
2806
+ nextInstallTurn();
2807
+ }));
2808
+ nextInstallTurn();
2809
+ });
2810
+ }
2811
+ function nextInstallTurn() {
2812
+ if (installTurns.busy || installTurns.ahead > 0)
2813
+ return;
2814
+ const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
2815
+ if (turn === undefined)
2816
+ return;
2817
+ installTurns.busy = true;
2818
+ turn();
2819
+ }
2820
+ async function ahead(work) {
2821
+ let taken = false;
2822
+ const hold = {
2823
+ take() {
2824
+ if (taken)
2825
+ return;
2826
+ taken = true;
2827
+ installTurns.ahead += 1;
2828
+ },
2829
+ release() {
2830
+ if (!taken)
2831
+ return;
2832
+ taken = false;
2833
+ installTurns.ahead -= 1;
2834
+ resumeInstall();
2835
+ },
2836
+ };
2837
+ hold.take();
2838
+ try {
2839
+ return await work(hold);
2840
+ }
2841
+ finally {
2842
+ hold.release();
2843
+ }
2844
+ }
2845
+ function resumeInstall() {
2846
+ if (installTurns.ahead > 0)
2847
+ return;
2848
+ void yielded().then(nextInstallTurn);
2849
+ }
2850
+ async function heldStream() {
2851
+ while (streamsFree === 0)
2852
+ await new Promise((admit) => installWaiters.push(admit));
2853
+ streamsFree -= 1;
2854
+ return once(() => {
2855
+ streamsFree += 1;
2856
+ });
2857
+ }
2858
+ async function unqueued() {
2859
+ while (queuedText >= QUEUED_TEXT) {
2860
+ await new Promise((admit) => installWaiters.push(admit));
2861
+ }
2862
+ }
2863
+ function once(release) {
2864
+ let released = false;
2865
+ return () => {
2866
+ if (released)
2867
+ return;
2868
+ released = true;
2869
+ release();
2870
+ for (const admit of installWaiters.splice(0))
2871
+ admit();
2872
+ };
2873
+ }
2874
+ function installing(locator) {
2875
+ installs.add(locator);
2876
+ queueReadiness();
2877
+ }
2878
+ function untransferred(locator) {
2879
+ transfers.delete(locator);
2880
+ arrivals.delete(locator);
2881
+ streaming.delete(locator);
2882
+ underived.delete(locator);
2883
+ unindexed.delete(locator);
2884
+ }
2885
+ function setBack(locator) {
2886
+ const now = bytesOf(locator, true);
2887
+ const earlier = startedOver.get(locator);
2888
+ startedOver.set(locator, {
2889
+ done: Math.max(now.done, earlier?.done ?? 0),
2890
+ total: Math.max(now.total, earlier?.total ?? 0),
2891
+ });
2892
+ }
2893
+ function owedIndexes(locator, bytes, text) {
2894
+ if (bytes <= 0 || text <= 0)
2895
+ return;
2896
+ unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
2897
+ }
2898
+ function built(locator, { done, text }) {
2899
+ const owed = unindexed.get(locator) ?? [];
2900
+ let left = text;
2901
+ while (!done && left > 0 && owed.length > 0) {
2902
+ const [first] = owed;
2903
+ if (left >= first.text) {
2904
+ left -= first.text;
2905
+ owed.shift();
2906
+ continue;
2907
+ }
2908
+ owed[0] = {
2909
+ bytes: (first.bytes * (first.text - left)) / first.text,
2910
+ text: first.text - left,
2911
+ };
2912
+ left = 0;
2787
2913
  }
2788
- return batches;
2914
+ if (done || owed.length === 0)
2915
+ unindexed.delete(locator);
2916
+ queueReadiness();
2789
2917
  }
2790
- async function pullRefresh(authorized, held, liveness) {
2918
+ function installed(locator) {
2919
+ if (installs.delete(locator))
2920
+ queueReadiness();
2921
+ }
2922
+ async function pullRefresh(authorized, held, liveness, hold, refresh) {
2791
2923
  if (!liveness())
2792
2924
  return unconfirmed;
2793
- const turn = generation;
2794
2925
  const { record } = held;
2795
- const base = held.confirmed;
2926
+ const from = standingOf(record.locator);
2796
2927
  const local = await openLocal();
2797
2928
  try {
2798
2929
  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));
2930
+ const result = await advancement(async (authorization, request) => {
2931
+ hold.release();
2932
+ try {
2933
+ return await refresh(authorization, request);
2934
+ }
2935
+ finally {
2936
+ hold.take();
2937
+ }
2938
+ }, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, composed(record), liveness));
2800
2939
  if (result.kind !== 'advanced') {
2801
2940
  journal.record(`transfer refresh ${record.locator} ${result.kind}`);
2802
2941
  return result;
2803
2942
  }
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
2943
  journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
2813
- return { kind: 'advanced', record: result.record, base };
2944
+ return {
2945
+ kind: 'advanced',
2946
+ record: result.record,
2947
+ from,
2948
+ remodelled: result.record.model !== record.model,
2949
+ };
2814
2950
  }
2815
2951
  catch (error) {
2952
+ if (refusedForSpace(error))
2953
+ return unconfirmed;
2816
2954
  journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
2817
2955
  throw error;
2818
2956
  }
2819
- finally {
2820
- reads.finished(record.locator, turn);
2821
- }
2822
2957
  }
2823
2958
  const heldContents = new Map();
2824
2959
  function heldContentOf(local, key) {
@@ -2836,63 +2971,115 @@ export function composeRuntime(options) {
2836
2971
  heldContents.set(key, made);
2837
2972
  return made;
2838
2973
  }
2974
+ async function abandoned(local, locator) {
2975
+ await local.abandon(locator);
2976
+ setBack(locator);
2977
+ untransferred(locator);
2978
+ liveQueries.recover();
2979
+ sweep();
2980
+ }
2839
2981
  function staging(local, liveness) {
2982
+ const derived = async (locator, key, composition) => {
2983
+ underived.delete(locator);
2984
+ for (;;) {
2985
+ if (!liveness())
2986
+ return false;
2987
+ const done = await finishingWrite(async () => liveness()
2988
+ ? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
2989
+ : null);
2990
+ if (done === null)
2991
+ return false;
2992
+ built(locator, done);
2993
+ if (done.done)
2994
+ return true;
2995
+ }
2996
+ };
2840
2997
  return {
2841
2998
  held: (locator) => transfers.get(locator) ?? null,
2999
+ async turn(locator, step) {
3000
+ installing(locator);
3001
+ return step === 'downloading' ? heldStream() : () => { };
3002
+ },
3003
+ unqueued,
3004
+ queued(text) {
3005
+ queuedText += text;
3006
+ return once(() => {
3007
+ queuedText -= text;
3008
+ });
3009
+ },
3010
+ arriving(locator) {
3011
+ let ended = false;
3012
+ return {
3013
+ at(bytes) {
3014
+ if (ended)
3015
+ return;
3016
+ arrivals.set(locator, bytes);
3017
+ streaming.add(locator);
3018
+ const now = performance.now();
3019
+ if (now - arrivalReported < ARRIVAL_REPORT_MS)
3020
+ return;
3021
+ arrivalReported = now;
3022
+ queueReadiness();
3023
+ },
3024
+ end() {
3025
+ if (ended)
3026
+ return;
3027
+ ended = true;
3028
+ if (streaming.delete(locator))
3029
+ queueReadiness();
3030
+ },
3031
+ };
3032
+ },
3033
+ streamed: options.adapter.roomStream !== undefined,
2842
3034
  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)
3035
+ const { locator } = record;
3036
+ installing(locator);
3037
+ if (!liveness())
3038
+ return null;
3039
+ const key = await installWrite(async () => {
3040
+ if (!liveness())
2864
3041
  return null;
2865
- key = staged;
2866
- }
2867
- transfers.set(record.locator, { ...record, key });
3042
+ return local.stage(record, composition, page);
3043
+ });
3044
+ if (key === null || !liveness())
3045
+ return null;
3046
+ const arrived = !transfers.has(locator);
3047
+ const prior = transfers.get(locator)?.done.bytes ?? 0;
3048
+ const held = { ...record, key };
3049
+ transfers.set(locator, held);
3050
+ if (arrived)
3051
+ liveQueries.recover();
3052
+ installing(locator);
2868
3053
  liveQueries.recover();
2869
- return key;
3054
+ const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
3055
+ owedIndexes(locator, record.done.bytes - prior, text);
3056
+ const owed = text + (underived.get(locator) ?? 0);
3057
+ underived.set(locator, owed);
3058
+ if (owed < deriveBudget.bytes)
3059
+ return held;
3060
+ return (await derived(locator, key, composition)) ? held : null;
2870
3061
  },
2871
- async refreshing(key) {
3062
+ derive: derived,
3063
+ async refreshing(locator, key, composition) {
2872
3064
  await exclusive(() => local.unrefresh(key));
2873
- return refreshingOf(local, key, liveness);
3065
+ return refreshingOf(local, key, composition, liveness, locator);
2874
3066
  },
2875
3067
  abandon: (locator) => exclusive(async () => {
2876
- if (!liveness())
2877
- return;
2878
- await local.abandon(locator);
2879
- transfers.delete(locator);
2880
- liveQueries.recover();
2881
- sweep();
3068
+ if (liveness())
3069
+ await abandoned(local, locator);
2882
3070
  }),
2883
- setAside: (locator) => exclusive(async () => {
3071
+ setAside: async (locator) => {
2884
3072
  if (!liveness())
2885
3073
  return;
2886
- await local.abandon(locator, true);
2887
- transfers.delete(locator);
3074
+ setBack(locator);
3075
+ untransferred(locator);
2888
3076
  liveQueries.recover();
2889
- }),
3077
+ },
2890
3078
  release: (key) => exclusive(async () => {
2891
3079
  await local.release(key);
2892
3080
  sweep();
2893
3081
  }),
2894
3082
  content: (key) => heldContentOf(local, key),
2895
- validate: (record, view) => validatedRoom(local, record, view, null),
2896
3083
  };
2897
3084
  }
2898
3085
  function reusable(room, local) {
@@ -2904,30 +3091,28 @@ export function composeRuntime(options) {
2904
3091
  async function pullBootstrap(authorized, room, local, liveness) {
2905
3092
  if (!liveness())
2906
3093
  return unconfirmed;
2907
- const turn = generation;
2908
3094
  if (replicas.has(room.locator)) {
2909
3095
  catching.add(room.locator);
2910
3096
  liveQueries.recover();
2911
3097
  }
2912
3098
  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));
3099
+ 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
3100
  const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
2918
3101
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
2919
- if (result.kind !== 'staged')
3102
+ if (result.kind !== 'staged') {
2920
3103
  caughtUpTo(room.locator);
3104
+ installed(room.locator);
3105
+ }
2921
3106
  return result;
2922
3107
  }
2923
3108
  catch (error) {
2924
- journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
2925
3109
  caughtUpTo(room.locator);
3110
+ installed(room.locator);
3111
+ if (refusedForSpace(error))
3112
+ return unconfirmed;
3113
+ journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
2926
3114
  throw error;
2927
3115
  }
2928
- finally {
2929
- reads.finished(room.locator, turn);
2930
- }
2931
3116
  }
2932
3117
  function caughtUpTo(locator) {
2933
3118
  if (catching.delete(locator))
@@ -2935,72 +3120,82 @@ export function composeRuntime(options) {
2935
3120
  }
2936
3121
  function behind(room) {
2937
3122
  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)));
3123
+ return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
2940
3124
  }
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);
3125
+ function standingOf(locator) {
3126
+ const held = replicas.get(locator);
3127
+ return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
3128
+ }
3129
+ function stands(locator, from) {
3130
+ const now = standingOf(locator);
3131
+ return now.key === from.key && now.confirmed === from.confirmed;
2948
3132
  }
2949
- async function advanceOnce(room, local, authorized, live) {
2950
- if (!live() || paused.has(room.locator))
3133
+ async function advance(room, local, authorized, running) {
3134
+ const live = () => running() && !full;
3135
+ if (!live())
2951
3136
  return unconfirmed;
2952
3137
  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')
3138
+ if (held !== undefined && options.adapter.roomStream !== undefined) {
3139
+ stale.delete(room.locator);
3140
+ name(room.locator);
3141
+ return null;
3142
+ }
3143
+ const refresh = reads.refresh;
3144
+ if (held !== undefined && refresh !== undefined) {
3145
+ const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
3146
+ if (refreshed === 'behind')
3147
+ return advance(room, local, authorized, live);
3148
+ if (refreshed !== 'beyond-horizon')
2980
3149
  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)
3150
+ if (reloadsDue.has(room.locator))
2987
3151
  return unconfirmed;
2988
3152
  }
3153
+ const from = standingOf(room.locator);
2989
3154
  const bootstrapped = await pullBootstrap(authorized, room, local, live);
3155
+ if (bootstrapped.kind !== 'staged')
3156
+ return live() ? bootstrapped : unconfirmed;
3157
+ const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
3158
+ if (await publish(local, room, bootstrapped.record, landing, live)) {
3159
+ return live() ? null : unconfirmed;
3160
+ }
3161
+ return reachedBy(room.locator, bootstrapped.record.roomVersion)
3162
+ ? null
3163
+ : advance(room, local, authorized, live);
3164
+ }
3165
+ async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
3166
+ const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
2990
3167
  if (!live())
2991
3168
  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;
3169
+ if (refreshed.kind === 'still-current') {
3170
+ stale.delete(room.locator);
3171
+ if (unreadable.delete(room.locator)) {
3172
+ scopedRefusals.delete(room.locator);
3173
+ list(room, true);
3174
+ emitRooms();
3175
+ }
3176
+ return null;
3177
+ }
3178
+ if (refreshed.kind === 'advanced') {
3179
+ const landing = {
3180
+ kind: 'refreshed',
3181
+ from: refreshed.from,
3182
+ remodelled: refreshed.remodelled,
3183
+ };
3184
+ if (!(await publish(local, room, refreshed.record, landing, live))) {
3185
+ const now = replicas.get(room.locator)?.record.roomVersion;
3186
+ const reached = now !== undefined &&
3187
+ options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3188
+ return reached ? null : 'behind';
3189
+ }
3190
+ await retainModel(local, room, modelOf(room.locator));
3191
+ return null;
3192
+ }
3193
+ return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
2996
3194
  }
2997
3195
  async function withhold(withheld, room, local, progress) {
2998
- if (withheld.kind === 'unconfirmed')
3196
+ if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
2999
3197
  return;
3000
- if (withheld.kind === 'refused')
3001
- stale.delete(room.locator);
3002
- else
3003
- stale.add(room.locator);
3198
+ stale.add(room.locator);
3004
3199
  if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
3005
3200
  const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
3006
3201
  repairable.set(room.locator, reset);
@@ -3022,30 +3217,10 @@ export function composeRuntime(options) {
3022
3217
  list(room, false);
3023
3218
  emitRooms();
3024
3219
  }
3025
- else if (withheld.kind === 'refused') {
3026
- refusedSource(room, withheld);
3027
- }
3028
3220
  else if (withheld.kind === 'revoked-authorization') {
3029
3221
  await refuse('revoked-authorization');
3030
3222
  }
3031
3223
  }
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
3224
  function leaving(batch, result) {
3050
3225
  if (result.kind === 'expired')
3051
3226
  return { batch, returned: returnedBy('expired') };
@@ -3067,8 +3242,8 @@ export function composeRuntime(options) {
3067
3242
  function settling(acceptance) {
3068
3243
  return acceptance !== null && 'apply' in acceptance;
3069
3244
  }
3070
- function refusalOf(acceptance) {
3071
- return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
3245
+ function failureOf(acceptance) {
3246
+ return settling(acceptance) ? null : acceptance;
3072
3247
  }
3073
3248
  async function acceptance(room, local, submitted, accepted) {
3074
3249
  if (!continuing())
@@ -3091,34 +3266,6 @@ export function composeRuntime(options) {
3091
3266
  ? accepted.roomVersion
3092
3267
  : held,
3093
3268
  };
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
3269
  changes = back;
3123
3270
  }
3124
3271
  const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
@@ -3128,8 +3275,6 @@ export function composeRuntime(options) {
3128
3275
  const set = settled.apply(answer);
3129
3276
  pendings.delete(pendingKey(room.locator, submitted.id));
3130
3277
  unreadable.delete(room.locator);
3131
- sourceRefusals.delete(room.locator);
3132
- declarationRefusals.delete(room.locator);
3133
3278
  list(room, true);
3134
3279
  return set;
3135
3280
  },
@@ -3147,13 +3292,9 @@ export function composeRuntime(options) {
3147
3292
  const failed = await exclusive(async () => {
3148
3293
  const adopting = await acceptance(room, local, submitted, accepted);
3149
3294
  await adoptedTogether(local, [adopting]);
3150
- return refusalOf(adopting);
3295
+ return failureOf(adopting);
3151
3296
  });
3152
- if (failed === null)
3153
- return true;
3154
- if (failed.kind === 'refused' && continuing())
3155
- refusedSource(room, failed);
3156
- return false;
3297
+ return failed === null;
3157
3298
  }
3158
3299
  async function adoptedMove(room, partner, local, submitted, accepted) {
3159
3300
  const other = discovered.get(partner);
@@ -3167,18 +3308,10 @@ export function composeRuntime(options) {
3167
3308
  ...accepted.destination,
3168
3309
  });
3169
3310
  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);
3311
+ return failureOf(left);
3175
3312
  });
3176
3313
  submitLater(partner);
3177
- if (failed === null)
3178
- return true;
3179
- if (failed.kind === 'refused' && continuing())
3180
- refusedSource(room, failed);
3181
- return false;
3314
+ return failed === null;
3182
3315
  }
3183
3316
  function queueIn(locator) {
3184
3317
  return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
@@ -3301,8 +3434,6 @@ export function composeRuntime(options) {
3301
3434
  if (replica === undefined || head === undefined)
3302
3435
  return 'done';
3303
3436
  const state = pendings.get(pendingKey(room.locator, head.id));
3304
- if (state?.accepted !== undefined)
3305
- return 'done';
3306
3437
  const partner = partnerOf(head, room.locator);
3307
3438
  if (partner !== null && !leavingIn(head, room.locator)) {
3308
3439
  submitLater(partner);
@@ -3416,12 +3547,12 @@ export function composeRuntime(options) {
3416
3547
  }
3417
3548
  if (result.kind === 'expired') {
3418
3549
  await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
3419
- const there = other === undefined ? null : await advance(other, local, authorized, continuing);
3550
+ const there = other === undefined ? null : await advanced(other, local, authorized);
3420
3551
  if (other !== undefined && there !== null) {
3421
3552
  await withhold(there, other, local, progress);
3422
3553
  return 'done';
3423
3554
  }
3424
- const withheld = await advance(room, local, authorized, continuing);
3555
+ const withheld = await advanced(room, local, authorized);
3425
3556
  if (withheld !== null) {
3426
3557
  await withhold(withheld, room, local, progress);
3427
3558
  return 'done';
@@ -3443,8 +3574,8 @@ export function composeRuntime(options) {
3443
3574
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
3444
3575
  const left = leaving(head, result);
3445
3576
  const refreshed = async () => ({
3446
- there: other === undefined ? null : await advance(other, local, authorized, continuing),
3447
- here: await advance(room, local, authorized, continuing),
3577
+ there: other === undefined ? null : await advanced(other, local, authorized),
3578
+ here: await advanced(room, local, authorized),
3448
3579
  });
3449
3580
  const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
3450
3581
  if (judged !== null && (judged.there !== null || judged.here !== null)) {
@@ -3515,6 +3646,7 @@ export function composeRuntime(options) {
3515
3646
  locator: room.locator,
3516
3647
  key: before.record.key,
3517
3648
  roots: before.record.typedSelection?.roots ?? [],
3649
+ composed: composedOf(composed(before.record)),
3518
3650
  overlay: { set: [], cleared: [...before.overlaid] },
3519
3651
  },
3520
3652
  ]),
@@ -3547,39 +3679,37 @@ export function composeRuntime(options) {
3547
3679
  if (signal.aborted)
3548
3680
  return;
3549
3681
  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
- });
3682
+ await roomTask(room.locator, (live) => reload(room, local, progress, live));
3556
3683
  if (signal.aborted)
3557
3684
  return;
3558
3685
  await rest(progress);
3559
3686
  }
3560
3687
  }
3561
3688
  function awaitingAnswer(locator) {
3689
+ answering.get(locator)?.release();
3562
3690
  let release = () => { };
3563
- const awaited = new Promise((resolve) => {
3691
+ const answered = new Promise((resolve) => {
3564
3692
  release = resolve;
3565
3693
  });
3566
- answering.set(locator, { generation, answered: awaited });
3694
+ const awaited = { answered, release };
3695
+ answering.set(locator, awaited);
3567
3696
  return () => {
3568
- if (answering.get(locator)?.answered === awaited)
3697
+ if (answering.get(locator) === awaited)
3569
3698
  answering.delete(locator);
3570
3699
  release();
3571
3700
  };
3572
3701
  }
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))))
3702
+ function declaredAsHeld(room) {
3703
+ const held = replicas.get(room.locator)?.record;
3704
+ return (held === undefined ||
3705
+ (JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
3706
+ JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
3707
+ JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
3708
+ }
3709
+ async function retainModel(local, room, next) {
3710
+ const key = room.locator;
3711
+ const previous = modelOf(room.locator);
3712
+ if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(room))
3583
3713
  return;
3584
3714
  if (next.status === 'unavailable') {
3585
3715
  invalidModels.add(key);
@@ -3591,26 +3721,13 @@ export function composeRuntime(options) {
3591
3721
  const held = replicas.get(room.locator);
3592
3722
  let changed = null;
3593
3723
  if (held !== undefined) {
3594
- const declared = {
3724
+ const record = {
3595
3725
  ...held.record,
3596
3726
  operations: room.operations,
3597
3727
  replica: room.replica,
3598
3728
  typedSelection: room.typedSelection ?? null,
3599
- ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
3729
+ model: next,
3600
3730
  };
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
3731
  const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
3615
3732
  const replayedLayer = await replayed(record, confirmedLayer, held.queue);
3616
3733
  const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
@@ -3621,41 +3738,33 @@ export function composeRuntime(options) {
3621
3738
  key: record.key,
3622
3739
  record: storedRecord(record),
3623
3740
  roots: record.typedSelection?.roots ?? [],
3624
- composition: compositionOf(record),
3741
+ composed: composedOf(composed(record)),
3625
3742
  overlay: diff.overlay,
3626
3743
  },
3627
3744
  ],
3628
3745
  });
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 });
3746
+ const { count, indexed } = committedRoom(room.locator, answer);
3747
+ replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
3632
3748
  changed = changeSet(room.locator, {
3633
3749
  documents: answer.changed[room.locator] ?? [],
3634
3750
  room: true,
3635
3751
  });
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
3752
  }
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));
3753
+ heldModel(room, next);
3653
3754
  if (changed === null)
3654
3755
  emitRooms();
3655
3756
  else
3656
3757
  publishChangeSet(changed);
3657
3758
  });
3658
3759
  }
3760
+ function heldModel(room, next) {
3761
+ models.set(room.locator, next);
3762
+ if (next.status === 'current') {
3763
+ liveModels.set(room.locator, JSON.stringify(next.snapshot));
3764
+ invalidModels.delete(room.locator);
3765
+ }
3766
+ list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
3767
+ }
3659
3768
  async function scoped(result, room, local) {
3660
3769
  if (result.scope === 'content' && !result.retryable) {
3661
3770
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
@@ -3669,14 +3778,14 @@ export function composeRuntime(options) {
3669
3778
  });
3670
3779
  let scopedAway = room;
3671
3780
  if (result.scope === 'model')
3672
- await retainModel(local, room, 'collection', {
3781
+ await retainModel(local, room, {
3673
3782
  status: 'unavailable',
3674
3783
  reason: 'not-readable',
3675
3784
  });
3676
3785
  else if (result.scope !== 'operation') {
3677
3786
  unreadable.add(room.locator);
3678
3787
  scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
3679
- await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
3788
+ await retainModel(local, scopedAway, modelOf(room.locator));
3680
3789
  }
3681
3790
  if (!continuing())
3682
3791
  return;
@@ -3684,6 +3793,17 @@ export function composeRuntime(options) {
3684
3793
  emitRooms();
3685
3794
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
3686
3795
  }
3796
+ function name(locator) {
3797
+ const stream = options.adapter.roomStream;
3798
+ const record = replicas.get(locator)?.record;
3799
+ const room = discovered.get(locator);
3800
+ if (stream === undefined || record === undefined || room === undefined)
3801
+ return;
3802
+ if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
3803
+ return;
3804
+ naming += 1;
3805
+ stream.hold([{ locator, roomVersion: record.roomVersion }]);
3806
+ }
3687
3807
  function heldRooms(listed) {
3688
3808
  return listed.flatMap((room) => {
3689
3809
  const record = replicas.get(room.locator)?.record;
@@ -3701,14 +3821,19 @@ export function composeRuntime(options) {
3701
3821
  return outcome?.status === 'fulfilled' && outcome.value;
3702
3822
  });
3703
3823
  if (attempt === 'opened') {
3824
+ naming += 1;
3704
3825
  options.adapter.roomStream?.hold(heldRooms(ready));
3705
3826
  handed = true;
3706
3827
  }
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');
3828
+ for (const room of ready) {
3829
+ if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
3830
+ if (attempt === 'authorized')
3831
+ name(room.locator);
3832
+ continue;
3833
+ }
3834
+ void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
3835
+ }
3836
+ const failed = prepared.find((room) => room.status === 'rejected');
3712
3837
  if (failed !== undefined)
3713
3838
  throw failed.reason;
3714
3839
  }
@@ -3726,27 +3851,29 @@ export function composeRuntime(options) {
3726
3851
  return false;
3727
3852
  }
3728
3853
  if (prior !== undefined)
3729
- await retainModel(local, room, 'collection', modelOf(room.locator));
3854
+ await retainModel(local, room, modelOf(room.locator));
3730
3855
  if (discoveryPaused || authorization === undefined)
3731
3856
  return false;
3732
3857
  return room.replica.admission === 'admitted';
3733
3858
  }
3734
3859
  async function caughtUp(room, local, progress, live) {
3735
- if (authorization === undefined || !live() || !behind(room))
3860
+ if (options.adapter.roomStream !== undefined || authorization === undefined)
3861
+ return;
3862
+ if (!live() || !behind(room))
3736
3863
  return;
3737
3864
  const withheld = await advance(room, local, authorization, live);
3738
3865
  if (withheld !== null)
3739
3866
  await withhold(withheld, room, local, progress);
3740
3867
  }
3741
3868
  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
- });
3869
+ const model = modelOf(locator);
3870
+ return model.status === 'unavailable' && model.reason === 'revoked';
3746
3871
  }
3747
3872
  async function loadRoom(room, local, progress, live) {
3748
3873
  if (!live() || authorization === undefined)
3749
3874
  return;
3875
+ if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
3876
+ return;
3750
3877
  if (replicas.has(room.locator) && revoked(room.locator)) {
3751
3878
  await reload(room, local, progress, live);
3752
3879
  return;
@@ -3764,21 +3891,53 @@ export function composeRuntime(options) {
3764
3891
  reloadsDue.delete(room.locator);
3765
3892
  if (!live() || authorization === undefined)
3766
3893
  return;
3894
+ const from = standingOf(room.locator);
3767
3895
  const bootstrapped = await pullBootstrap(authorization, room, local, live);
3768
- if (!live())
3769
- return;
3770
3896
  if (bootstrapped.kind !== 'staged') {
3771
- await withhold(bootstrapped, room, local, progress);
3897
+ if (live())
3898
+ await withhold(bootstrapped, room, local, progress);
3772
3899
  return;
3773
3900
  }
3774
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3901
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
3775
3902
  await caughtUp(room, local, progress, live);
3776
3903
  }
3777
3904
  finally {
3778
3905
  caughtUpTo(room.locator);
3779
3906
  }
3780
3907
  }
3781
- async function discoverRooms(local, progress, attempt, live, scope) {
3908
+ async function asked(authorized) {
3909
+ try {
3910
+ return (await options.adapter.discover?.(authorized)) ?? { kind: 'transport-uncertainty' };
3911
+ }
3912
+ catch {
3913
+ return { kind: 'transport-uncertainty' };
3914
+ }
3915
+ }
3916
+ function malformed(listed) {
3917
+ for (const room of listed) {
3918
+ if (room.operations === undefined ||
3919
+ !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
3920
+ room.operations[operation].every((scope) => typeof scope === 'string')) ||
3921
+ room.replica === undefined ||
3922
+ typeof room.replica.selection !== 'string' ||
3923
+ room.replica.selection.length === 0 ||
3924
+ !['admitted', 'unavailable'].includes(room.replica.admission) ||
3925
+ !['locked', 'open'].includes(room.replica.offline)) {
3926
+ return `invalid model or replica declaration ${room.locator}`;
3927
+ }
3928
+ if (!declaredSelection(room))
3929
+ return `invalid typed selection declaration ${room.locator}`;
3930
+ }
3931
+ return null;
3932
+ }
3933
+ async function signIn(local, signedIn) {
3934
+ if (JSON.stringify(signedIn) === JSON.stringify(member))
3935
+ return;
3936
+ await local.fact('member', signedIn);
3937
+ member = signedIn;
3938
+ emitMember();
3939
+ }
3940
+ async function discoverRooms(local, progress, attempt, live, found) {
3782
3941
  if (closed || authorization === undefined) {
3783
3942
  notifyReady();
3784
3943
  return;
@@ -3786,13 +3945,9 @@ export function composeRuntime(options) {
3786
3945
  moveTo(transferring(status));
3787
3946
  discoveries += 1;
3788
3947
  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
- }
3948
+ const result = found ?? (await asked(authorization));
3949
+ if (result.kind === 'discovery')
3950
+ roomsNamed = true;
3796
3951
  if (!live()) {
3797
3952
  progress.complete = false;
3798
3953
  notifyReady();
@@ -3800,12 +3955,7 @@ export function composeRuntime(options) {
3800
3955
  }
3801
3956
  journal.record(`connection discover ${result.kind}`);
3802
3957
  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
- }
3958
+ await cachedModels(local);
3809
3959
  progress.reachable = false;
3810
3960
  progress.complete = false;
3811
3961
  notifyReady();
@@ -3826,26 +3976,12 @@ export function composeRuntime(options) {
3826
3976
  notifyReady();
3827
3977
  return;
3828
3978
  }
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
- }
3979
+ const invalid = malformed(result.rooms);
3980
+ if (invalid !== null) {
3981
+ journal.record(invalid);
3982
+ await refuse('missing-required-guarantee');
3983
+ notifyReady();
3984
+ return;
3849
3985
  }
3850
3986
  if (refusal !== null) {
3851
3987
  refusal = null;
@@ -3861,13 +3997,8 @@ export function composeRuntime(options) {
3861
3997
  await local.fact('providerLimits', limits);
3862
3998
  await local.fact('capabilities', capabilities);
3863
3999
  }
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);
4000
+ await signIn(local, result.member ?? null);
4001
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
3871
4002
  }
3872
4003
  async function ensureAuthorization() {
3873
4004
  if (authorization !== undefined)
@@ -3882,23 +4013,45 @@ export function composeRuntime(options) {
3882
4013
  }
3883
4014
  }
3884
4015
  async function admittedRead(work) {
3885
- if (roomReads.generation !== generation) {
3886
- roomReads = { generation, free: ROOM_READS, waiting: [] };
4016
+ while (roomReads.free === 0) {
4017
+ await new Promise((admit) => roomReads.waiting.push(admit));
3887
4018
  }
3888
- const reads = roomReads;
3889
- while (reads.free === 0)
3890
- await new Promise((admit) => reads.waiting.push(admit));
3891
- reads.free -= 1;
4019
+ roomReads.free -= 1;
3892
4020
  try {
3893
4021
  await work();
3894
4022
  }
3895
4023
  finally {
3896
- reads.free += 1;
3897
- reads.waiting.shift()?.();
4024
+ roomReads.free += 1;
4025
+ roomReads.waiting.shift()?.();
3898
4026
  }
3899
4027
  }
4028
+ function inRoom(locator, work) {
4029
+ roomWork += 1;
4030
+ const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
4031
+ try {
4032
+ return await work(continuing);
4033
+ }
4034
+ finally {
4035
+ roomWork -= 1;
4036
+ }
4037
+ });
4038
+ const chain = ran.then(() => undefined, () => undefined);
4039
+ roomLanes.set(locator, chain);
4040
+ working.add(chain);
4041
+ void chain.finally(() => {
4042
+ working.delete(chain);
4043
+ if (roomLanes.get(locator) === chain)
4044
+ roomLanes.delete(locator);
4045
+ });
4046
+ return ran;
4047
+ }
3900
4048
  function roomTask(locator, work) {
3901
- return task(roomLane(locator), (live) => admittedRead(() => work(live)));
4049
+ if (stopping)
4050
+ return Promise.resolve();
4051
+ return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
4052
+ }
4053
+ function advanced(room, local, authorized) {
4054
+ return inRoom(room.locator, (live) => advance(room, local, authorized, live));
3902
4055
  }
3903
4056
  function task(lane, work) {
3904
4057
  if (stopping)
@@ -3914,23 +4067,19 @@ export function composeRuntime(options) {
3914
4067
  void run.finally(() => working.delete(run));
3915
4068
  return run;
3916
4069
  }
3917
- function sync(attempt) {
4070
+ function sync(attempt, found) {
3918
4071
  return task(OPENING_LANE, async (live) => {
3919
4072
  if (!live()) {
3920
- notifyReady();
4073
+ readyFromStore();
3921
4074
  return;
3922
4075
  }
3923
4076
  const progress = { complete: true, reachable: true };
3924
- const gate = discover();
3925
- if (attempt === 'opened')
3926
- opened = generation;
3927
- underway = attempt === 'opened' ? { generation, progress } : null;
4077
+ underway = attempt === 'opened' ? progress : null;
3928
4078
  if (attempt === 'opened')
3929
4079
  streamed = progress;
3930
4080
  complete = false;
3931
4081
  recorded = false;
3932
4082
  moveTo(transferring(status));
3933
- handed = false;
3934
4083
  try {
3935
4084
  const local = await openLocal();
3936
4085
  if (!live())
@@ -3938,7 +4087,7 @@ export function composeRuntime(options) {
3938
4087
  await ensureAuthorization();
3939
4088
  if (!live())
3940
4089
  return;
3941
- await discoverRooms(local, progress, attempt, live);
4090
+ await discoverRooms(local, progress, attempt, live, found);
3942
4091
  }
3943
4092
  catch {
3944
4093
  progress.complete = false;
@@ -3946,7 +4095,6 @@ export function composeRuntime(options) {
3946
4095
  progress.reachable = false;
3947
4096
  }
3948
4097
  finally {
3949
- gate.release();
3950
4098
  notifyReady();
3951
4099
  }
3952
4100
  if (!live())
@@ -3958,35 +4106,53 @@ export function composeRuntime(options) {
3958
4106
  if (handed || closed || progress.reachable)
3959
4107
  return;
3960
4108
  underway = null;
3961
- opened = null;
3962
- journal.record('connection reopen');
3963
4109
  await rest(progress, live);
3964
- options.adapter.roomStream?.reopen();
3965
4110
  });
3966
4111
  }
3967
- function rediscover(scope) {
4112
+ function resynchronize() {
3968
4113
  return task(OPENING_LANE, async (live) => {
3969
- if (!live() || !socketOpen)
4114
+ if (!live() || !announced)
3970
4115
  return;
3971
4116
  const progress = { complete: true, reachable: true };
3972
- const gate = discover();
3973
4117
  moveTo(transferring(status));
3974
4118
  try {
3975
4119
  const local = await openLocal();
3976
4120
  if (!live())
3977
4121
  return;
3978
- await ensureAuthorization();
4122
+ await synchronize([...discovered.values()], local, progress, 'unstreamed', live);
4123
+ }
4124
+ catch {
4125
+ progress.complete = false;
4126
+ }
4127
+ if (live())
4128
+ await finishOpening(progress, live);
4129
+ });
4130
+ }
4131
+ function changeAuthority(change) {
4132
+ return task(OPENING_LANE, async (live) => {
4133
+ if (!live() || !announced)
4134
+ return;
4135
+ const progress = { complete: true, reachable: true };
4136
+ moveTo(transferring(status));
4137
+ try {
4138
+ const local = await openLocal();
3979
4139
  if (!live())
3980
4140
  return;
3981
- await discoverRooms(local, progress, 'authorized', live, scope);
4141
+ const invalid = malformed(change.rooms);
4142
+ if (invalid !== null) {
4143
+ journal.record(invalid);
4144
+ await refuse('missing-required-guarantee');
4145
+ return;
4146
+ }
4147
+ if (change.member !== undefined)
4148
+ await signIn(local, change.member);
4149
+ discoveries += 1;
4150
+ const listed = await applyDiscovery(change.rooms, discoveries, change.left);
4151
+ await synchronize(listed, local, progress, 'authorized', live);
3982
4152
  }
3983
4153
  catch {
3984
4154
  progress.complete = false;
3985
4155
  }
3986
- finally {
3987
- gate.release();
3988
- notifyReady();
3989
- }
3990
4156
  if (!live())
3991
4157
  return;
3992
4158
  if (!progress.complete)
@@ -3995,13 +4161,15 @@ export function composeRuntime(options) {
3995
4161
  });
3996
4162
  }
3997
4163
  function closedSocket(revoked, lost) {
3998
- const opened = socketOpen;
4164
+ notifyReady();
4165
+ const opened = announced;
3999
4166
  socketOpen = false;
4167
+ announced = false;
4168
+ handed = false;
4169
+ naming = 0;
4000
4170
  connectionLost();
4001
- if (lost) {
4002
- reads.lost();
4171
+ if (lost)
4003
4172
  conversations.lost();
4004
- }
4005
4173
  if (revoked) {
4006
4174
  void refuse('revoked-authorization');
4007
4175
  return;
@@ -4014,16 +4182,29 @@ export function composeRuntime(options) {
4014
4182
  state: status.state === 'synced' ? 'syncing' : status.state,
4015
4183
  providerUnreachable: status.providerUnreachable || !opened,
4016
4184
  });
4185
+ void closing(opened);
4186
+ }
4187
+ function closing(opened) {
4188
+ return task(OPENING_LANE, async (live) => {
4189
+ if (!live())
4190
+ return;
4191
+ const local = await openLocal();
4192
+ if (!opened && !socketOpen)
4193
+ await cachedModels(local);
4194
+ await rest(null, live);
4195
+ });
4017
4196
  }
4018
- function applyOpening(expired) {
4019
- const progress = underway?.generation === generation ? underway.progress : null;
4197
+ function cachedModels(local) {
4198
+ return settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
4199
+ }
4200
+ function caughtUpRooms(expired) {
4201
+ const progress = underway;
4202
+ underway = null;
4203
+ naming = Math.max(0, naming - 1);
4020
4204
  const cycle = progress ?? { complete: true, reachable: true };
4021
- if (opened === generation)
4022
- opened = null;
4023
4205
  const reloading = progress === null ? [] : owedReloads(expired);
4024
4206
  const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
4025
4207
  return task(OPENING_LANE, async (live) => {
4026
- underway = null;
4027
4208
  if (!live())
4028
4209
  return;
4029
4210
  if (progress === null) {
@@ -4031,24 +4212,14 @@ export function composeRuntime(options) {
4031
4212
  await rest(cycle, live);
4032
4213
  return;
4033
4214
  }
4034
- await roomsCarried([...expired, ...reloading]);
4035
- if (!live())
4036
- return;
4037
4215
  try {
4038
4216
  const local = await openLocal();
4039
- const reloaded = await replacing;
4217
+ await replacing;
4040
4218
  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
- }
4219
+ await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
4220
+ !expired.includes(room.locator) &&
4221
+ !reloading.includes(room.locator)), 'current', local);
4048
4222
  }
4049
- const failed = reloaded.find((outcome) => outcome.status === 'rejected');
4050
- if (failed !== undefined)
4051
- throw failed.reason;
4052
4223
  }
4053
4224
  catch {
4054
4225
  progress.complete = false;
@@ -4060,11 +4231,8 @@ export function composeRuntime(options) {
4060
4231
  }
4061
4232
  function owedReloads(expired) {
4062
4233
  const reloading = [...discovered.values()]
4063
- .filter((room) => replicas.has(room.locator) &&
4064
- !expired.includes(room.locator) &&
4065
- (owed.has(room.locator) || revoked(room.locator)))
4234
+ .filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
4066
4235
  .map((room) => room.locator);
4067
- owed.clear();
4068
4236
  for (const locator of reloading)
4069
4237
  catching.add(locator);
4070
4238
  liveQueries.recover();
@@ -4073,24 +4241,43 @@ export function composeRuntime(options) {
4073
4241
  async function reloadNow(replaced, progress) {
4074
4242
  const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
4075
4243
  for (const room of reloaded)
4076
- reloadsDue.set(room.locator, generation);
4244
+ reloadsDue.add(room.locator);
4077
4245
  try {
4078
4246
  const local = await openLocal();
4079
- return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
4247
+ for (const room of reloaded) {
4248
+ void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
4249
+ }
4080
4250
  }
4081
4251
  catch {
4082
4252
  for (const room of reloaded)
4083
4253
  reloadsDue.delete(room.locator);
4084
4254
  progress.reachable = false;
4085
- return [];
4086
4255
  }
4087
4256
  }
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' });
4257
+ async function settleModels(held, status, local) {
4258
+ const owed = (room) => {
4259
+ const model = modelOf(room.locator);
4260
+ return model.status !== 'unavailable' && model.status !== status;
4261
+ };
4262
+ const settled = await exclusive(async () => {
4263
+ if (!continuing())
4264
+ return [];
4265
+ const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
4266
+ for (const room of unchanged) {
4267
+ const next = { ...modelOf(room.locator), status };
4268
+ const replica = replicas.get(room.locator);
4269
+ if (replica !== undefined)
4270
+ replicas.set(room.locator, { ...replica, record: { ...replica.record, model: next } });
4271
+ heldModel(room, next);
4093
4272
  }
4273
+ if (unchanged.length > 0)
4274
+ emitRooms();
4275
+ return unchanged;
4276
+ });
4277
+ for (const room of held) {
4278
+ if (settled.includes(room) || !owed(room))
4279
+ continue;
4280
+ await retainModel(local, room, { ...modelOf(room.locator), status });
4094
4281
  }
4095
4282
  }
4096
4283
  function submitLater(locator) {
@@ -4135,14 +4322,10 @@ export function composeRuntime(options) {
4135
4322
  try {
4136
4323
  const local = await openLocal();
4137
4324
  await roomTask(locator, async (live) => {
4138
- const current = () => live() && !signal.aborted;
4139
- if (!current() || authorization === undefined)
4325
+ if (!live() || authorization === undefined)
4140
4326
  return;
4141
- const withheld = await Promise.race([
4142
- advance(room, local, authorization, current),
4143
- abandonedOn(signal),
4144
- ]);
4145
- if (withheld !== null && current())
4327
+ const withheld = await advance(room, local, authorization, live);
4328
+ if (withheld !== null && live())
4146
4329
  await withhold(withheld, room, local, progress);
4147
4330
  });
4148
4331
  }
@@ -4197,14 +4380,11 @@ export function composeRuntime(options) {
4197
4380
  progress.complete = false;
4198
4381
  return;
4199
4382
  }
4200
- reads.applied();
4201
4383
  submitAll();
4202
4384
  for (const locator of stale)
4203
4385
  if (repairable.has(locator))
4204
4386
  repairLater(locator, {});
4205
4387
  await conversations.reconcile();
4206
- if (status.providerUnreachable)
4207
- conversations.reconnected();
4208
4388
  const reading = { complete: true, reachable: true };
4209
4389
  await conversations.catchUp(reading);
4210
4390
  if (progress.reachable && reading.reachable)
@@ -4230,7 +4410,7 @@ export function composeRuntime(options) {
4230
4410
  if (!live())
4231
4411
  return;
4232
4412
  let lastCompleted = status.lastCompleted;
4233
- let synced = complete && stale.size === 0 && !pending();
4413
+ let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
4234
4414
  if (synced && !recorded) {
4235
4415
  const completedAt = new Date().toISOString();
4236
4416
  try {
@@ -4245,74 +4425,69 @@ export function composeRuntime(options) {
4245
4425
  }
4246
4426
  }
4247
4427
  moveTo({
4248
- state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
4428
+ state: pending()
4429
+ ? 'pending'
4430
+ : synced && complete && roomWork === 0 && naming === 0
4431
+ ? 'synced'
4432
+ : 'syncing',
4249
4433
  lastCompleted,
4250
- providerUnreachable: !progress.reachable,
4434
+ providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
4251
4435
  ...attention(),
4252
4436
  });
4253
4437
  }
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));
4438
+ function applyRoom(change) {
4439
+ const { locator } = change;
4440
+ if (stopping)
4441
+ return Promise.resolve();
4442
+ return inRoom(locator, async (live) => {
4443
+ await answering.get(locator)?.answered;
4444
+ const progress = underway ?? { complete: true, reachable: true };
4285
4445
  try {
4286
4446
  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
- });
4447
+ for (;;) {
4448
+ if (!live())
4449
+ return null;
4450
+ const room = discovered.get(locator);
4451
+ const held = replicas.get(locator);
4452
+ if (room === undefined ||
4453
+ held === undefined ||
4454
+ room.replica.admission !== 'admitted' ||
4455
+ held.record.replica.selection !== room.replica.selection) {
4456
+ return null;
4457
+ }
4458
+ const at = held.record.roomVersion;
4459
+ if (options.adapter.reached(locator, at, change.roomVersion))
4460
+ return progress;
4461
+ if (!options.adapter.reached(locator, at, change.from)) {
4462
+ name(locator);
4463
+ return progress;
4464
+ }
4465
+ if (underway === null)
4466
+ moveTo(transferring(status));
4467
+ const writing = () => live() && !full;
4468
+ const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, () => change.read()));
4469
+ if (refreshed.kind !== 'advanced')
4470
+ return progress;
4471
+ const landing = {
4472
+ kind: 'refreshed',
4473
+ from: refreshed.from,
4474
+ remodelled: refreshed.remodelled,
4475
+ };
4476
+ if (await publish(local, room, refreshed.record, landing, writing)) {
4477
+ stale.delete(locator);
4478
+ await retainModel(local, room, modelOf(locator));
4479
+ return progress;
4480
+ }
4481
+ }
4294
4482
  }
4295
4483
  catch {
4484
+ if (!stale.has(locator))
4485
+ name(locator);
4296
4486
  stale.add(locator);
4487
+ return progress;
4297
4488
  }
4298
- finally {
4299
- if (during === null)
4300
- await rest(progress, live);
4301
- }
4302
- });
4489
+ }).then((progress) => rest(progress), () => undefined);
4303
4490
  }
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
4491
  function observeStream() {
4317
4492
  const stream = options.adapter.roomStream;
4318
4493
  if (stream === undefined || withdrawStream !== undefined)
@@ -4322,29 +4497,29 @@ export function composeRuntime(options) {
4322
4497
  return;
4323
4498
  if (event.kind === 'opened') {
4324
4499
  socketOpen = true;
4500
+ announced = false;
4501
+ handed = false;
4502
+ naming = 0;
4325
4503
  connectionLive();
4326
4504
  generation += 1;
4327
- reads.opened();
4328
4505
  conversations.reopened();
4329
4506
  recovery.resume();
4330
- void sync('opened');
4507
+ }
4508
+ else if (event.kind === 'discovered') {
4509
+ if (!socketOpen)
4510
+ return;
4511
+ announced = true;
4512
+ void sync('opened', event.discovery);
4331
4513
  }
4332
4514
  else if (event.kind === 'closed')
4333
4515
  closedSocket(event.revoked, event.lost);
4334
- else if (event.kind === 'opening')
4335
- void applyOpening(event.expired);
4516
+ else if (event.kind === 'caught-up')
4517
+ void caughtUpRooms(event.expired);
4336
4518
  else if (event.kind === 'authority-changed')
4337
- void rediscover(event.scope);
4519
+ void changeAuthority(event);
4338
4520
  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]]));
4521
+ void applyRoom(event);
4522
+ });
4348
4523
  }
4349
4524
  function writesHeld() {
4350
4525
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -4414,8 +4589,9 @@ export function composeRuntime(options) {
4414
4589
  if (closed)
4415
4590
  return;
4416
4591
  closed = true;
4417
- if (rechecking !== null)
4418
- clearInterval(rechecking);
4592
+ notifyDiscovered();
4593
+ if (probing !== null)
4594
+ clearInterval(probing);
4419
4595
  connectionLive();
4420
4596
  lockListeners.clear();
4421
4597
  readinessWatches.clear();
@@ -4427,6 +4603,7 @@ export function composeRuntime(options) {
4427
4603
  conversations.close();
4428
4604
  void recovery.retire();
4429
4605
  roomListeners.clear();
4606
+ unlistedRooms.clear();
4430
4607
  refusalListeners.clear();
4431
4608
  storageLossListeners.clear();
4432
4609
  statusListeners.clear();
@@ -4439,6 +4616,7 @@ export function composeRuntime(options) {
4439
4616
  }
4440
4617
  async function stop() {
4441
4618
  stopping = true;
4619
+ inFlight.abort();
4442
4620
  await recovery.retire();
4443
4621
  await Promise.all(working);
4444
4622
  await mutations;
@@ -4452,12 +4630,12 @@ export function composeRuntime(options) {
4452
4630
  observeStream();
4453
4631
  void openLocal().then(() => {
4454
4632
  if (capabilities.length > 0)
4455
- notifyReady();
4633
+ readyFromStore();
4456
4634
  }, () => {
4457
- notifyReady();
4635
+ readyFromStore();
4458
4636
  });
4459
4637
  if (options.adapter.roomStream === undefined)
4460
- void sync('unstreamed');
4638
+ void sync('unstreamed', null);
4461
4639
  function planned(input, reach) {
4462
4640
  const plan = planOf(input, roomFacts(reach), planReach(reach));
4463
4641
  const named = plan.paths !== null &&
@@ -4602,12 +4780,13 @@ export function composeRuntime(options) {
4602
4780
  };
4603
4781
  }
4604
4782
  if (topic === 'readiness') {
4605
- return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
4783
+ return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
4606
4784
  }
4607
4785
  if (topic === 'rooms') {
4608
- roomListeners.add(deliver);
4609
- deliver(listed());
4786
+ unlistedRooms.add(deliver);
4787
+ emitKnownRooms();
4610
4788
  return () => {
4789
+ unlistedRooms.delete(deliver);
4611
4790
  roomListeners.delete(deliver);
4612
4791
  };
4613
4792
  }
@@ -4661,7 +4840,17 @@ export function composeRuntime(options) {
4661
4840
  return openLocal().then(() => undefined);
4662
4841
  },
4663
4842
  rooms() {
4664
- return listed();
4843
+ if (closed)
4844
+ return Promise.resolve([]);
4845
+ return new Promise((settle) => {
4846
+ const answer = (value) => {
4847
+ unlistedRooms.delete(answer);
4848
+ roomListeners.delete(answer);
4849
+ settle(value);
4850
+ };
4851
+ unlistedRooms.add(answer);
4852
+ emitKnownRooms();
4853
+ });
4665
4854
  },
4666
4855
  query: ((input) => rowsQuery(planned(input, null))),
4667
4856
  search: (input) => searched(input, null, []),
@@ -4671,7 +4860,7 @@ export function composeRuntime(options) {
4671
4860
  typedSelection: rooms.get(locator)?.typedSelection === undefined ||
4672
4861
  rooms.get(locator)?.typedSelection === null
4673
4862
  ? null
4674
- : modelTypes(modelOf(locator, 'typed-selection')),
4863
+ : modelTypes(builtIn),
4675
4864
  };
4676
4865
  },
4677
4866
  roomDraft: {
@@ -4688,33 +4877,17 @@ export function composeRuntime(options) {
4688
4877
  const record = roomDrafts.get(room)?.get(path);
4689
4878
  return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
4690
4879
  },
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
- });
4880
+ write(room, path, revision, input, next = roomDraftRevision()) {
4881
+ if (typeof next !== 'string' || next === '')
4882
+ return Promise.reject(runtimeError('room-draft-malformed'));
4883
+ return gathered(`room-draft\u0000${room}\u0000${path}`, { revision, input, next }, (earlier, later) => {
4884
+ if (later.revision !== earlier.next)
4885
+ return null;
4886
+ const parts = gatheredParts(earlier.input, later.input);
4887
+ return parts === null
4888
+ ? null
4889
+ : { revision: earlier.revision, input: parts, next: later.next };
4890
+ }, (written) => writtenRoomDraft(room, path, written.revision, written.input, written.next));
4718
4891
  },
4719
4892
  discard(room, path, revision) {
4720
4893
  return exclusive(async () => {
@@ -4741,13 +4914,13 @@ export function composeRuntime(options) {
4741
4914
  });
4742
4915
  },
4743
4916
  write(app, value) {
4744
- return keepPresentation(app, () => structuredClone(value));
4917
+ return exclusive(() => keptPresentation(app, () => structuredClone(value)));
4745
4918
  },
4746
4919
  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
- ]));
4920
+ return gathered(`presentation\u0000${app}`, structuredClone(members), (earlier, later) => ({ ...earlier, ...later }), (edited) => keptPresentation(app, (held) => Object.fromEntries([
4921
+ ...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(edited, member)),
4922
+ ...Object.entries(edited).filter(([, value]) => value !== null),
4923
+ ])));
4751
4924
  },
4752
4925
  },
4753
4926
  save(room, named, writtenBy) {
@@ -4766,7 +4939,8 @@ export function composeRuntime(options) {
4766
4939
  part: readPart,
4767
4940
  read: readBinaryPath,
4768
4941
  link: linkBinaryPath,
4769
- readReference: readBinaryReference,
4942
+ show: showBinaryPath,
4943
+ attach: attachBinary,
4770
4944
  },
4771
4945
  async call(name, input) {
4772
4946
  await ready;
@@ -4789,7 +4963,8 @@ export function composeRuntime(options) {
4789
4963
  journal.record('retry');
4790
4964
  unpreparable.clear();
4791
4965
  queueReadiness();
4792
- resume();
4966
+ if (full)
4967
+ await probed();
4793
4968
  },
4794
4969
  observe: observing(watch),
4795
4970
  async reauthorize() {
@@ -4803,7 +4978,7 @@ export function composeRuntime(options) {
4803
4978
  authorization = undefined;
4804
4979
  discoveryPaused = false;
4805
4980
  if (options.adapter.roomStream === undefined)
4806
- await sync('unstreamed');
4981
+ await sync('unstreamed', null);
4807
4982
  else
4808
4983
  await task(OPENING_LANE, ensureAuthorization);
4809
4984
  },
@@ -4811,7 +4986,6 @@ export function composeRuntime(options) {
4811
4986
  const reachable = {
4812
4987
  md,
4813
4988
  watch,
4814
- referrerScope: (referrer) => options.adapter.referrerScope(referrer),
4815
4989
  plan: planned,
4816
4990
  query: (plan) => rowsQuery(plan),
4817
4991
  watchPlan,
@@ -4821,12 +4995,12 @@ export function composeRuntime(options) {
4821
4995
  lockedRooms,
4822
4996
  watchLocks,
4823
4997
  located,
4998
+ room: (locator) => listed().find((one) => one.locator === locator),
4824
4999
  };
4825
5000
  return {
4826
5001
  md,
4827
5002
  seed,
4828
5003
  stop,
4829
- lose: loseRooms,
4830
5004
  watch,
4831
5005
  persisted(answer) {
4832
5006
  if (answer === persistence)
@@ -4835,6 +5009,10 @@ export function composeRuntime(options) {
4835
5009
  journal.record(`persistence ${answer}`);
4836
5010
  queueReadiness();
4837
5011
  },
4838
- within: (reach, port) => narrowed(reachable, reach, port),
5012
+ within: (reach, port) => {
5013
+ if (full)
5014
+ void probed();
5015
+ return narrowed(reachable, reach, port);
5016
+ },
4839
5017
  };
4840
5018
  }