@agentshouse/mdruntime 0.1.0-cee9c8aa4cd86 → 0.1.0-cf1e7fdb9c600

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 (221) hide show
  1. package/README.md +11 -9
  2. package/dist/adapter.d.ts +96 -63
  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 +17 -9
  11. package/dist/bootstrap.d.ts.map +1 -1
  12. package/dist/bootstrap.js +143 -94
  13. package/dist/chain.d.ts +2 -1
  14. package/dist/chain.d.ts.map +1 -1
  15. package/dist/client.d.ts +12 -12
  16. package/dist/client.d.ts.map +1 -1
  17. package/dist/client.js +54 -8
  18. package/dist/collection.d.ts +7 -4
  19. package/dist/collection.d.ts.map +1 -1
  20. package/dist/collection.js +24 -19
  21. package/dist/compile.d.ts +1 -6
  22. package/dist/compile.d.ts.map +1 -1
  23. package/dist/compile.js +118 -247
  24. package/dist/connection.d.ts +11 -11
  25. package/dist/connection.d.ts.map +1 -1
  26. package/dist/connection.js +1602 -1147
  27. package/dist/context.d.ts +2 -0
  28. package/dist/context.d.ts.map +1 -0
  29. package/dist/{chat/context.js → context.js} +1 -4
  30. package/dist/conversation.d.ts +18 -14
  31. package/dist/conversation.d.ts.map +1 -1
  32. package/dist/conversation.js +266 -150
  33. package/dist/durability.d.ts +1 -5
  34. package/dist/durability.d.ts.map +1 -1
  35. package/dist/durability.js +5 -0
  36. package/dist/entry.d.ts +3 -2
  37. package/dist/entry.d.ts.map +1 -1
  38. package/dist/entry.js +15 -6
  39. package/dist/file.d.ts +2 -11
  40. package/dist/file.d.ts.map +1 -1
  41. package/dist/file.js +6 -4
  42. package/dist/host.d.ts +3 -6
  43. package/dist/host.d.ts.map +1 -1
  44. package/dist/host.js +49 -35
  45. package/dist/index.d.ts +15 -12
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +6 -3
  48. package/dist/job.d.ts +14 -0
  49. package/dist/job.d.ts.map +1 -0
  50. package/dist/job.js +28 -0
  51. package/dist/live.d.ts +2 -1
  52. package/dist/live.d.ts.map +1 -1
  53. package/dist/live.js +9 -5
  54. package/dist/media.d.ts +8 -0
  55. package/dist/media.d.ts.map +1 -0
  56. package/dist/media.js +218 -0
  57. package/dist/memory.d.ts +11 -6
  58. package/dist/memory.d.ts.map +1 -1
  59. package/dist/memory.js +178 -100
  60. package/dist/messages.d.ts +2 -2
  61. package/dist/messages.d.ts.map +1 -1
  62. package/dist/messages.js +15 -16
  63. package/dist/model.d.ts +7 -3
  64. package/dist/model.d.ts.map +1 -1
  65. package/dist/model.js +7 -13
  66. package/dist/observation.d.ts +3 -2
  67. package/dist/observation.d.ts.map +1 -1
  68. package/dist/observation.js +8 -3
  69. package/dist/outcome.d.ts +13 -6
  70. package/dist/outcome.d.ts.map +1 -1
  71. package/dist/outcome.js +16 -5
  72. package/dist/overlay.js +1 -1
  73. package/dist/paths.d.ts +1 -0
  74. package/dist/paths.d.ts.map +1 -1
  75. package/dist/paths.js +4 -0
  76. package/dist/query.d.ts +10 -205
  77. package/dist/query.d.ts.map +1 -1
  78. package/dist/query.js +3 -494
  79. package/dist/reach.d.ts +14 -9
  80. package/dist/reach.d.ts.map +1 -1
  81. package/dist/reach.js +81 -71
  82. package/dist/react.d.ts +3 -1
  83. package/dist/react.d.ts.map +1 -1
  84. package/dist/readiness.d.ts +13 -16
  85. package/dist/readiness.d.ts.map +1 -1
  86. package/dist/readiness.js +2 -8
  87. package/dist/recovery.d.ts +1 -0
  88. package/dist/recovery.d.ts.map +1 -1
  89. package/dist/recovery.js +26 -0
  90. package/dist/refresh.d.ts +10 -4
  91. package/dist/refresh.d.ts.map +1 -1
  92. package/dist/refresh.js +104 -30
  93. package/dist/replica.d.ts +6 -5
  94. package/dist/replica.d.ts.map +1 -1
  95. package/dist/replica.js +8 -7
  96. package/dist/room-draft.d.ts +20 -3
  97. package/dist/room-draft.d.ts.map +1 -1
  98. package/dist/room-draft.js +131 -51
  99. package/dist/rows.d.ts +8 -15
  100. package/dist/rows.d.ts.map +1 -1
  101. package/dist/rows.js +51 -151
  102. package/dist/save.d.ts +2 -0
  103. package/dist/save.d.ts.map +1 -1
  104. package/dist/save.js +13 -7
  105. package/dist/search.d.ts +12 -63
  106. package/dist/search.d.ts.map +1 -1
  107. package/dist/search.js +9 -52
  108. package/dist/shared.d.ts +0 -1
  109. package/dist/shared.d.ts.map +1 -1
  110. package/dist/shared.js +0 -10
  111. package/dist/status.d.ts +4 -3
  112. package/dist/status.d.ts.map +1 -1
  113. package/dist/status.js +3 -1
  114. package/dist/store.d.ts +32 -30
  115. package/dist/store.d.ts.map +1 -1
  116. package/dist/store.js +64 -58
  117. package/dist/stored.d.ts +42 -18
  118. package/dist/stored.d.ts.map +1 -1
  119. package/dist/stored.js +193 -194
  120. package/dist/tables.d.ts +60 -39
  121. package/dist/tables.d.ts.map +1 -1
  122. package/dist/tables.js +77 -59
  123. package/dist/transfer.d.ts +0 -5
  124. package/dist/transfer.d.ts.map +1 -1
  125. package/dist/validation.d.ts +3 -4
  126. package/dist/validation.d.ts.map +1 -1
  127. package/dist/validation.js +39 -97
  128. package/dist/windows.d.ts +1 -12
  129. package/dist/windows.d.ts.map +1 -1
  130. package/dist/windows.js +2 -75
  131. package/dist/wire.d.ts +6 -9
  132. package/dist/wire.d.ts.map +1 -1
  133. package/dist/wire.js +3 -3
  134. package/package.json +11 -18
  135. package/AGENTS.md +0 -320
  136. package/dist/chat/acts.d.ts +0 -33
  137. package/dist/chat/acts.d.ts.map +0 -1
  138. package/dist/chat/acts.js +0 -36
  139. package/dist/chat/context.d.ts +0 -3
  140. package/dist/chat/context.d.ts.map +0 -1
  141. package/dist/chat/earlier.d.ts +0 -10
  142. package/dist/chat/earlier.d.ts.map +0 -1
  143. package/dist/chat/earlier.js +0 -54
  144. package/dist/chat/house.d.ts +0 -25
  145. package/dist/chat/house.d.ts.map +0 -1
  146. package/dist/chat/house.js +0 -38
  147. package/dist/chat/input.d.ts +0 -11
  148. package/dist/chat/input.d.ts.map +0 -1
  149. package/dist/chat/input.js +0 -92
  150. package/dist/chat/kit.d.ts +0 -61
  151. package/dist/chat/kit.d.ts.map +0 -1
  152. package/dist/chat/kit.js +0 -123
  153. package/dist/chat/lasting.d.ts +0 -6
  154. package/dist/chat/lasting.d.ts.map +0 -1
  155. package/dist/chat/lasting.js +0 -19
  156. package/dist/chat/list.d.ts +0 -28
  157. package/dist/chat/list.d.ts.map +0 -1
  158. package/dist/chat/list.js +0 -103
  159. package/dist/chat/markdown.d.ts +0 -21
  160. package/dist/chat/markdown.d.ts.map +0 -1
  161. package/dist/chat/markdown.js +0 -246
  162. package/dist/chat/message.d.ts +0 -7
  163. package/dist/chat/message.d.ts.map +0 -1
  164. package/dist/chat/message.js +0 -254
  165. package/dist/chat/messages.d.ts +0 -35
  166. package/dist/chat/messages.d.ts.map +0 -1
  167. package/dist/chat/messages.js +0 -210
  168. package/dist/chat/name.d.ts +0 -7
  169. package/dist/chat/name.d.ts.map +0 -1
  170. package/dist/chat/name.js +0 -37
  171. package/dist/chat/opened.d.ts +0 -12
  172. package/dist/chat/opened.d.ts.map +0 -1
  173. package/dist/chat/opened.js +0 -19
  174. package/dist/chat/overlay.d.ts +0 -32
  175. package/dist/chat/overlay.d.ts.map +0 -1
  176. package/dist/chat/overlay.js +0 -103
  177. package/dist/chat/parts.d.ts +0 -58
  178. package/dist/chat/parts.d.ts.map +0 -1
  179. package/dist/chat/parts.js +0 -120
  180. package/dist/chat/pieces.d.ts +0 -62
  181. package/dist/chat/pieces.d.ts.map +0 -1
  182. package/dist/chat/pieces.js +0 -1110
  183. package/dist/chat/process.d.ts +0 -20
  184. package/dist/chat/process.d.ts.map +0 -1
  185. package/dist/chat/process.js +0 -32
  186. package/dist/chat/provider.d.ts +0 -70
  187. package/dist/chat/provider.d.ts.map +0 -1
  188. package/dist/chat/provider.js +0 -635
  189. package/dist/chat/question.d.ts +0 -9
  190. package/dist/chat/question.d.ts.map +0 -1
  191. package/dist/chat/question.js +0 -69
  192. package/dist/chat/questions.d.ts +0 -31
  193. package/dist/chat/questions.d.ts.map +0 -1
  194. package/dist/chat/questions.js +0 -111
  195. package/dist/chat/read.d.ts +0 -5
  196. package/dist/chat/read.d.ts.map +0 -1
  197. package/dist/chat/read.js +0 -13
  198. package/dist/chat/ready.d.ts +0 -37
  199. package/dist/chat/ready.d.ts.map +0 -1
  200. package/dist/chat/ready.js +0 -93
  201. package/dist/chat/room-file.d.ts +0 -8
  202. package/dist/chat/room-file.d.ts.map +0 -1
  203. package/dist/chat/room-file.js +0 -72
  204. package/dist/chat/steady.d.ts +0 -3
  205. package/dist/chat/steady.d.ts.map +0 -1
  206. package/dist/chat/steady.js +0 -26
  207. package/dist/chat/style.d.ts +0 -4
  208. package/dist/chat/style.d.ts.map +0 -1
  209. package/dist/chat/style.js +0 -307
  210. package/dist/chat.d.ts +0 -15
  211. package/dist/chat.d.ts.map +0 -1
  212. package/dist/chat.js +0 -8
  213. package/dist/fulltext.d.ts +0 -27
  214. package/dist/fulltext.d.ts.map +0 -1
  215. package/dist/fulltext.js +0 -368
  216. package/dist/reads.d.ts +0 -11
  217. package/dist/reads.d.ts.map +0 -1
  218. package/dist/reads.js +0 -98
  219. package/dist/words.d.ts +0 -19
  220. package/dist/words.d.ts.map +0 -1
  221. package/dist/words.js +0 -150
@@ -1,8 +1,8 @@
1
- import { escapePointer, parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
2
- import { attachedModel, NO_ROOM_TYPES, roomTypesOf } from './attached-model.js';
1
+ import { bodied, identityOf, parseFrontmatter, plain, queryKey, 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
- import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
5
+ import { planWithin, } from './query.js';
6
6
  import { createLiveQueries } from './live.js';
7
7
  import { seededResult } from './seed.js';
8
8
  import { documentKey, roomOfKey } from './rows.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,44 @@ 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
- }
88
+ const TEXT_READS = 4;
80
89
  function pendingKey(locator, id) {
81
90
  return `${locator}\u0000${id}`;
82
91
  }
83
92
  export function composeRuntime(options) {
84
93
  const localSaves = options.adapter.localSaves === true;
85
- const roomTypes = roomTypesOf(options.adapter.roomTypes);
94
+ const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
86
95
  let authorization = options.authorization;
87
96
  let opening;
88
97
  const fences = new Map();
@@ -92,44 +101,53 @@ export function composeRuntime(options) {
92
101
  const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
93
102
  let discoveries = 0;
94
103
  const replicas = new FencedMap(isFenced);
104
+ const reopened = new Set();
105
+ const usableWhenHeld = options.usableWhenHeld ?? [];
95
106
  const queuesWithoutReplica = new FencedMap(isFenced);
96
107
  const pendings = new Map();
108
+ const landedAhead = new Map();
97
109
  const seeds = new Map();
98
110
  const seedWatchers = new Map();
99
111
  const roomDrafts = new FencedMap(isFenced);
112
+ const attachedFiles = new FencedMap(isFenced);
100
113
  const presentations = new Map();
101
114
  let limits = [];
102
115
  let capabilities = [];
103
116
  const rooms = new FencedMap(isFenced);
104
117
  const counted = new Map();
105
118
  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);
119
+ const builtIn = builtInModel(roomTypes);
120
+ const typedPaths = options.adapter.typedPaths ?? [];
121
+ const models = new FencedMap(isFenced);
122
+ const liveModels = new FencedMap(isFenced);
109
123
  const invalidModels = new Set();
110
124
  const discovered = new Map();
111
- const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
125
+ let inFlight = new AbortController();
126
+ const refreshed = options.adapter.refresh?.bind(options.adapter);
127
+ const reads = {
128
+ bootstrap: (authorized, request) => readUntil(options.adapter.bootstrap(authorized, request), inFlight.signal),
129
+ ...(refreshed === undefined
130
+ ? {}
131
+ : {
132
+ refresh: (authorized, request) => readUntil(refreshed(authorized, request), inFlight.signal),
133
+ }),
134
+ reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
135
+ };
112
136
  let roomsListed = false;
113
137
  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;
138
+ function modelOf(locator) {
139
+ return models.get(locator) ?? NO_MODEL;
121
140
  }
122
- function savedModel(locator, attached) {
123
- return invalidModels.has(modelKey(locator, attached))
141
+ function savedModel(locator) {
142
+ return invalidModels.has(locator)
124
143
  ? { status: 'unavailable', reason: 'revoked' }
125
- : modelOf(locator, attached);
144
+ : modelOf(locator);
145
+ }
146
+ function composed(record) {
147
+ return compositionOf(record, builtIn, typedPaths);
126
148
  }
127
149
  function compositionFor(record) {
128
- return compositionOf({
129
- ...record,
130
- model: savedModel(record.locator, 'collection'),
131
- selectionModel: savedModel(record.locator, 'typed-selection'),
132
- });
150
+ return composed({ ...record, model: savedModel(record.locator) });
133
151
  }
134
152
  let refusal = null;
135
153
  let storageLoss = null;
@@ -139,10 +157,10 @@ export function composeRuntime(options) {
139
157
  let closed = false;
140
158
  let stopping = false;
141
159
  let withdrawStream;
142
- const announced = new Map();
143
160
  let underway = null;
144
161
  let streamed = null;
145
162
  let handed = false;
163
+ let naming = 0;
146
164
  const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
147
165
  const repairing = new Set();
148
166
  const calling = new Set();
@@ -153,15 +171,18 @@ export function composeRuntime(options) {
153
171
  let recorded = false;
154
172
  const stale = new Set();
155
173
  let generation = 0;
156
- let discovering = null;
157
- let opened = null;
158
- const owed = new Set();
159
- const reloadsDue = new Map();
174
+ const reloadsDue = new Set();
175
+ const highest = new Map();
176
+ const beyondHorizon = new Set();
160
177
  const lanes = new Map();
161
- let roomReads = { generation, free: ROOM_READS, waiting: [] };
178
+ const roomLanes = new Map();
179
+ let roomWork = 0;
180
+ const roomReads = { free: ROOM_READS, waiting: [] };
162
181
  const working = new Set();
163
182
  let mutations = Promise.resolve();
183
+ let gathering = null;
164
184
  const roomListeners = new Set();
185
+ const unlistedRooms = new Set();
165
186
  const refusalListeners = new Set();
166
187
  const storageLossListeners = new Set();
167
188
  const statusListeners = new Set();
@@ -172,6 +193,8 @@ export function composeRuntime(options) {
172
193
  let withdrawConversations;
173
194
  let currencyQueued = null;
174
195
  let socketOpen = false;
196
+ let announced = false;
197
+ let missedChange = false;
175
198
  const confirmed = new Set();
176
199
  let lockGrace = null;
177
200
  const lockListeners = new Set();
@@ -181,49 +204,84 @@ export function composeRuntime(options) {
181
204
  const serviceModeListeners = new Set();
182
205
  let member = null;
183
206
  const memberListeners = new Set();
184
- const prepared = new Set();
207
+ let authorityChanges = 0;
208
+ const authorityChangeListeners = new Set();
185
209
  const unpreparedDemand = new Set();
186
210
  const unpreparable = new Set();
187
211
  let preparing = false;
188
212
  let targets = {};
189
213
  let persistence = options.shell.persistence;
190
- let capacity = null;
191
- const paused = new Set();
192
- let rechecking = null;
214
+ let full = false;
215
+ let probing = null;
216
+ let testing = null;
217
+ const database = {
218
+ async ask(request) {
219
+ try {
220
+ return await options.database.ask(request);
221
+ }
222
+ catch (error) {
223
+ if (refusedForSpace(error))
224
+ filled();
225
+ throw error;
226
+ }
227
+ },
228
+ };
193
229
  const readinessWatches = new Set();
194
230
  let readinessQueued = false;
195
231
  const catching = new Set();
196
232
  const transfers = new FencedMap(isFenced);
233
+ const installs = new Set();
234
+ const arrivals = new Map();
235
+ const streaming = new Set();
236
+ const underived = new Map();
237
+ const unindexed = new Map();
238
+ const startedOver = new Map();
239
+ let arrivalReported = 0;
240
+ let streamsFree = STREAMS_HELD;
241
+ let queuedText = 0;
242
+ const installWaiters = [];
243
+ const stageBudget = {
244
+ bytes: STAGE_FLOOR * 4,
245
+ floor: STAGE_FLOOR,
246
+ ceiling: STAGE_BYTES,
247
+ aim: STAGE_AIM_MS,
248
+ };
249
+ const deriveBudget = {
250
+ bytes: DERIVE_FLOOR * 4,
251
+ floor: DERIVE_FLOOR,
252
+ ceiling: DERIVE_BYTES,
253
+ aim: DERIVE_AIM_MS,
254
+ };
255
+ const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
197
256
  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
257
  call: (name, input) => invokeAction(name, input),
258
+ history: (request) => readConversationHistory(request),
213
259
  durably,
214
260
  stream: () => options.adapter.conversations,
215
261
  publish: publishChangeSet,
216
262
  online: () => connectionOf() !== 'unreachable' && refusal === null,
263
+ full: () => full,
217
264
  closed: () => closed,
218
265
  unanswered: resendLater,
219
266
  expired: (operation) => pastHorizon(operation),
220
267
  });
221
268
  const journal = createLog();
222
- const keptFiles = openedFiles(options.database, isFenced);
223
- let notifyReady = () => { };
269
+ const keptFiles = openedFiles(database, isFenced);
270
+ const holding = new Map();
271
+ const unholdable = new Map();
272
+ let readyFromStore = () => { };
224
273
  const ready = new Promise((resolve) => {
225
- notifyReady = resolve;
274
+ readyFromStore = resolve;
275
+ });
276
+ let notifyDiscovered = () => { };
277
+ const firstDiscovery = new Promise((resolve) => {
278
+ notifyDiscovered = resolve;
226
279
  });
280
+ let roomsNamed = false;
281
+ function notifyReady() {
282
+ notifyDiscovered();
283
+ readyFromStore();
284
+ }
227
285
  function rememberRoomDraft(record) {
228
286
  const room = roomDrafts.get(record.locator) ?? new Map();
229
287
  room.set(record.path, record);
@@ -243,7 +301,7 @@ export function composeRuntime(options) {
243
301
  return undefined;
244
302
  const local = await openLocal();
245
303
  const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
246
- return { ...slice, collection: collectionOf(slice.documents, compositionOf(held.record)) };
304
+ return { ...slice, collection: collectionOf(slice.documents, composed(held.record)) };
247
305
  }
248
306
  function roomDraftOf(record, visible) {
249
307
  return structuredClone({
@@ -299,14 +357,40 @@ export function composeRuntime(options) {
299
357
  const value = presentations.get(app);
300
358
  return value === undefined ? null : structuredClone(value);
301
359
  }
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
- });
360
+ async function writtenRoomDraft(room, path, revision, input, next) {
361
+ const parts = readParts(input, path);
362
+ if (path === '' || normalize(path) !== path)
363
+ throw runtimeError('room-draft-malformed', `room-draft-malformed at ${path}: a path inside the Room, such as notes/a.md`);
364
+ const local = await openLocal();
365
+ const current = roomDrafts.get(room)?.get(path);
366
+ if ((current?.revision ?? null) !== revision) {
367
+ return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
368
+ }
369
+ const file = parts.file === undefined
370
+ ? undefined
371
+ : await heldFile(parts.file.blob, parts.file.base === undefined
372
+ ? binaryBase(await visibleOf(room, [path]), path)
373
+ : parts.file.base);
374
+ const record = {
375
+ locator: room,
376
+ path,
377
+ revision: next,
378
+ parts: held(current?.parts, parts, file),
379
+ returned: current?.returned ?? null,
380
+ };
381
+ await local.keepRoomDraft(record, parts.file === undefined || file === undefined
382
+ ? []
383
+ : [{ key: file.localId, blob: parts.file.blob }]);
384
+ rememberRoomDraft(record);
385
+ publishChangeSet(changeSet(room, { drafts: [path] }));
386
+ return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
387
+ }
388
+ async function keptPresentation(app, next) {
389
+ const local = await openLocal();
390
+ const stored = next(presentations.get(app));
391
+ await local.present({ app, value: stored });
392
+ presentations.set(app, stored);
393
+ publishChangeSet(changeSet('', { presentation: app }));
310
394
  }
311
395
  function emitPresentation(app) {
312
396
  if (closed)
@@ -321,6 +405,20 @@ export function composeRuntime(options) {
321
405
  (queuesWithoutReplica.size > 0 ||
322
406
  [...replicas.values()].some((replica) => replica.queue.length > 0)));
323
407
  }
408
+ function unsent() {
409
+ const changed = new Map();
410
+ for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
411
+ for (const { batch, state } of entriesOf(room)) {
412
+ if (state === 'local')
413
+ continue;
414
+ for (const { path } of batch.changes)
415
+ changed.set(`${room}\u0000${path}`, { room, path });
416
+ }
417
+ }
418
+ return [...changed]
419
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
420
+ .map(([, change]) => change);
421
+ }
324
422
  function attention() {
325
423
  const attentionRequired = [];
326
424
  for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
@@ -332,16 +430,9 @@ export function composeRuntime(options) {
332
430
  returned: structuredClone(record.returned),
333
431
  });
334
432
  }
335
- for (const [room, refusal] of sourceRefusals)
336
- attentionRequired.push({ room, refusal: structuredClone(refusal) });
337
433
  attentionRequired.sort((left, right) => {
338
434
  if (left.room !== right.room)
339
435
  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
436
  if (left.path !== right.path)
346
437
  return left.path < right.path ? -1 : 1;
347
438
  return 0;
@@ -353,7 +444,7 @@ export function composeRuntime(options) {
353
444
  if (reset === null || submission.reset < reset)
354
445
  reset = submission.reset;
355
446
  }
356
- return { attentionRequired, reset };
447
+ return { attentionRequired, unsent: unsent(), reset };
357
448
  }
358
449
  function documentsIn(locator) {
359
450
  return replicas.get(locator)?.count ?? 0;
@@ -426,7 +517,7 @@ export function composeRuntime(options) {
426
517
  const governed = governs(reach, room);
427
518
  const attached = [
428
519
  ...(governed.collection ? [modelOf(room.locator)] : []),
429
- ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
520
+ ...(governed.selection ? [builtIn] : []),
430
521
  ];
431
522
  return {
432
523
  locator: room.locator,
@@ -442,11 +533,26 @@ export function composeRuntime(options) {
442
533
  const value = listed();
443
534
  for (const listener of roomListeners)
444
535
  listener(value);
536
+ emitKnownRooms();
537
+ }
538
+ function emitKnownRooms() {
539
+ conversations.learned();
540
+ if (unlistedRooms.size === 0)
541
+ return;
542
+ if (!roomsListed && rooms.size === 0 && refusal === null && !status.providerUnreachable)
543
+ return;
544
+ const value = listed();
545
+ for (const listener of [...unlistedRooms]) {
546
+ unlistedRooms.delete(listener);
547
+ roomListeners.add(listener);
548
+ listener(value);
549
+ }
445
550
  }
446
551
  function emitRefusal() {
447
552
  queueReadiness();
448
553
  for (const listener of refusalListeners)
449
554
  listener(refusal);
555
+ emitKnownRooms();
450
556
  }
451
557
  function emitStorageLoss() {
452
558
  queueReadiness();
@@ -467,6 +573,7 @@ export function composeRuntime(options) {
467
573
  if (closed)
468
574
  return;
469
575
  queueReadiness();
576
+ emitKnownRooms();
470
577
  const next = status.state === 'synced' && !conversations.current()
471
578
  ? { ...status, state: 'syncing' }
472
579
  : status;
@@ -480,9 +587,6 @@ export function composeRuntime(options) {
480
587
  if (localSaves)
481
588
  return Object.freeze({ batch, state: 'local' });
482
589
  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
590
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
487
591
  }
488
592
  function entriesOf(locator) {
@@ -501,18 +605,25 @@ export function composeRuntime(options) {
501
605
  function unpublished(error) {
502
606
  journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
503
607
  }
608
+ function indexed(locator) {
609
+ return replicas.get(locator)?.indexed === true;
610
+ }
504
611
  async function prepareReplica(locator) {
505
612
  for (;;) {
506
613
  const held = replicas.get(locator);
507
- if (held === undefined || closed || prepared.has(locator))
614
+ if (held === undefined || closed || held.indexed)
508
615
  return;
509
616
  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)
617
+ const done = await exclusive(async () => {
618
+ const local = await openLocal();
619
+ return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR));
620
+ });
621
+ const now = replicas.get(locator);
622
+ if (now?.record.key !== key)
512
623
  continue;
513
- if (!done)
624
+ if (!done.done)
514
625
  continue;
515
- prepared.add(locator);
626
+ replicas.set(locator, { ...now, indexed: true });
516
627
  publishChangeSet(changeSet(locator, { documents: null }));
517
628
  liveQueries.recover();
518
629
  queueReadiness();
@@ -520,7 +631,7 @@ export function composeRuntime(options) {
520
631
  }
521
632
  }
522
633
  function unprepared() {
523
- return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unpreparable.has(locator));
634
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
524
635
  }
525
636
  function unpreparedBy(locator, error) {
526
637
  unpreparable.add(locator);
@@ -581,14 +692,14 @@ export function composeRuntime(options) {
581
692
  pending.push({ room: locator, awaits: 'acquisition' });
582
693
  continue;
583
694
  }
584
- if (!prepared.has(locator)) {
695
+ if (!replica.indexed) {
585
696
  pending.push({ room: locator, awaits: 'preparation' });
586
697
  continue;
587
698
  }
588
699
  coverage.push({
589
700
  room: locator,
590
701
  generation: replica.record.transfer,
591
- current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
702
+ current: !(catching.has(locator) || transfers.has(locator)),
592
703
  });
593
704
  read.push({ locator, key: replica.record.key });
594
705
  for (const batch of replica.queue)
@@ -615,8 +726,6 @@ export function composeRuntime(options) {
615
726
  return { listed: named.every((room) => replicas.has(room)), rooms: named };
616
727
  }
617
728
  function roomState(locator) {
618
- if (sourceRefusals.has(locator))
619
- return 'refused';
620
729
  const listedRoom = discovered.get(locator);
621
730
  if (roomsListed &&
622
731
  (listedRoom === undefined ||
@@ -625,13 +734,13 @@ export function composeRuntime(options) {
625
734
  return 'unadmitted';
626
735
  if (!replicas.has(locator))
627
736
  return 'acquiring';
628
- if (!prepared.has(locator))
737
+ if (!indexed(locator))
629
738
  return 'preparing';
630
739
  return 'complete';
631
740
  }
632
741
  function conversationCoverage(reach) {
633
742
  const declared = reach === null || reach.conversations;
634
- if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
743
+ if (!declared || options.adapter.conversations === undefined)
635
744
  return 'undeclared';
636
745
  if (!conversations.known() || !conversations.current())
637
746
  return 'acquiring';
@@ -646,25 +755,50 @@ export function composeRuntime(options) {
646
755
  ? 'connected'
647
756
  : 'connecting';
648
757
  }
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
- });
758
+ function bytesOf(room, underway) {
759
+ const transfer = transfers.get(room);
760
+ if (transfer !== undefined) {
761
+ const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
762
+ return {
763
+ done: transfer.done.bytes - Math.ceil(owed),
764
+ total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
765
+ };
766
+ }
767
+ const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
768
+ return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
769
+ }
770
+ function installationOf(coverage) {
771
+ const rooms = { done: 0, total: 0 };
772
+ const bytes = { done: 0, total: 0 };
773
+ let installing = 0;
774
+ let downloading = false;
775
+ for (const { room, state } of coverage) {
776
+ if (state === 'unadmitted')
777
+ continue;
778
+ rooms.total += 1;
779
+ if (state === 'complete')
780
+ rooms.done += 1;
781
+ const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
782
+ if (underway)
783
+ installing += 1;
784
+ if (underway && (streaming.has(room) || !arrivals.has(room)))
785
+ downloading = true;
786
+ const now = bytesOf(room, underway);
787
+ const earlier = underway ? startedOver.get(room) : undefined;
788
+ bytes.done += Math.max(now.done, earlier?.done ?? 0);
789
+ bytes.total += Math.max(now.total, earlier?.total ?? 0);
790
+ }
791
+ const step = installing === 0
792
+ ? null
793
+ : downloading
794
+ ? 'downloading'
795
+ : bytes.done < bytes.total
796
+ ? 'writing'
797
+ : 'replaying';
798
+ return { step, rooms, bytes };
664
799
  }
665
800
  function readinessFor(port, reach) {
666
801
  const required = requiredRooms(reach);
667
- const search = port.search === 'required';
668
802
  const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
669
803
  const preparing = coverage
670
804
  .filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
@@ -677,23 +811,21 @@ export function composeRuntime(options) {
677
811
  listed: required.listed,
678
812
  rooms: coverage,
679
813
  conversations: conversationCoverage(reach),
680
- search: search ? 'required' : 'unused',
681
814
  },
682
815
  persistence,
683
816
  connection: connectionOf(),
684
817
  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),
818
+ space: full ? 'full' : null,
819
+ installation: installationOf(coverage),
690
820
  lost: storageLoss !== null,
691
- established: targets[port.target]?.established === true,
821
+ established: targets[port.target]?.established === true ||
822
+ (usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
692
823
  });
693
824
  }
694
825
  function establish(name) {
695
826
  targets = { ...targets, [name]: { established: true } };
696
827
  queueReadiness();
828
+ probeEachMinute();
697
829
  void durably((local) => local.fact('targets', targets)).catch(() => {
698
830
  journal.record(`target record failed ${name}`);
699
831
  });
@@ -731,87 +863,56 @@ export function composeRuntime(options) {
731
863
  readinessWatches.delete(watch);
732
864
  };
733
865
  }
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)
866
+ function filled() {
867
+ if (full || closed)
755
868
  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);
869
+ full = true;
870
+ journal.record('space full');
871
+ queueReadiness();
872
+ probeEachMinute();
760
873
  }
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();
874
+ function installedTarget() {
875
+ return ((usableWhenHeld.length > 0 && reopened.size > 0) ||
876
+ Object.values(targets).some((target) => target.established));
772
877
  }
773
- function resume() {
774
- if (rechecking !== null)
775
- clearInterval(rechecking);
776
- rechecking = null;
777
- if (paused.size === 0 && capacity === null)
878
+ function probeEachMinute() {
879
+ if (!full || probing !== null || closed || !installedTarget())
778
880
  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;
881
+ // 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.
882
+ probing = setInterval(() => {
883
+ void probed();
884
+ }, 60_000);
795
885
  }
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)
886
+ function probed() {
887
+ testing ??= database.ask({ kind: 'space.probe' }).then(() => {
888
+ testing = null;
889
+ spaceFreed();
805
890
  return true;
806
- pause(locator, { cause: 'insufficient-space', needed, available });
807
- return false;
891
+ }, () => {
892
+ testing = null;
893
+ return false;
894
+ });
895
+ return testing;
808
896
  }
809
- async function exceededOn(locator, bytes) {
810
- const available = await estimated().catch(() => null);
811
- pause(locator, { cause: 'storage-exceeded', needed: bytes, available });
897
+ function spaceFreed() {
898
+ if (!full)
899
+ return;
900
+ full = false;
901
+ if (probing !== null)
902
+ clearInterval(probing);
903
+ probing = null;
904
+ unpreparable.clear();
905
+ journal.record('space freed');
906
+ queueReadiness();
907
+ if (closed || authorization === undefined)
908
+ return;
909
+ if (options.adapter.roomStream === undefined)
910
+ void sync('unstreamed', null);
911
+ else
912
+ void resynchronize();
812
913
  }
813
914
  function seedRoomHeld(room, version) {
814
- return prepared.has(room) && reachedBy(room, version);
915
+ return indexed(room) && reachedBy(room, version);
815
916
  }
816
917
  function seedHeld(at) {
817
918
  return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
@@ -848,7 +949,24 @@ export function composeRuntime(options) {
848
949
  function sameCover(left, right) {
849
950
  return JSON.stringify(left) === JSON.stringify(right);
850
951
  }
952
+ async function unheldRows(local, plan, rows, rooms) {
953
+ if (plan.shape.kind !== 'documents' || !bodied(plan.shape))
954
+ return [];
955
+ if (!('documents' in rows) || !textOnline())
956
+ return [];
957
+ const named = new Map();
958
+ for (const { room, path, textHeld } of rows.documents) {
959
+ if (textHeld === false)
960
+ named.set(room, [...(named.get(room) ?? []), path]);
961
+ }
962
+ const unheld = [];
963
+ for (const { locator, key } of rooms) {
964
+ unheld.push(...(await unheldIn(local, locator, key, named.get(locator) ?? [])));
965
+ }
966
+ return unheld.filter((one) => unholdable.get(textKey(one)) !== one.revision);
967
+ }
851
968
  async function coveredRows(plan, cover) {
969
+ const asked = new Set();
852
970
  for (;;) {
853
971
  const before = cover();
854
972
  if ('pending' in before)
@@ -859,8 +977,14 @@ export function composeRuntime(options) {
859
977
  const current = () => local.changes(keys) === changes;
860
978
  const rows = await windowRows(local, plan, before.rooms, current);
861
979
  const after = cover();
862
- if (rows !== null && sameCover(before, after))
980
+ if (rows === null || !sameCover(before, after))
981
+ continue;
982
+ const unheld = (await unheldRows(local, plan, rows, before.rooms)).filter((one) => !asked.has(textKey(one)));
983
+ if (unheld.length === 0)
863
984
  return { rows, cover: before, current };
985
+ for (const one of unheld)
986
+ asked.add(textKey(one));
987
+ await holdTexts(unheld);
864
988
  }
865
989
  }
866
990
  async function windowRows(local, plan, rooms, unchanged) {
@@ -934,6 +1058,10 @@ export function composeRuntime(options) {
934
1058
  }
935
1059
  function publishChangeSet(set) {
936
1060
  applyChangeSet(set);
1061
+ for (const { locator, path } of set.documents) {
1062
+ if (path !== null)
1063
+ attachedFiles.get(locator)?.delete(path);
1064
+ }
937
1065
  if (set.rooms.length > 0 ||
938
1066
  [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
939
1067
  emitRooms();
@@ -952,6 +1080,8 @@ export function composeRuntime(options) {
952
1080
  }
953
1081
  async function refuse(reason, provider = null) {
954
1082
  discoveryPaused = true;
1083
+ inFlight.abort();
1084
+ inFlight = new AbortController();
955
1085
  try {
956
1086
  if (reason === 'revoked-authorization') {
957
1087
  await exclusive(async () => {
@@ -959,27 +1089,24 @@ export function composeRuntime(options) {
959
1089
  const records = [...replicas].map(([locator, held]) => ({
960
1090
  locator,
961
1091
  held,
962
- record: { ...held.record, model: revoked, selectionModel: revoked },
1092
+ record: { ...held.record, model: revoked },
963
1093
  }));
1094
+ let answer = null;
964
1095
  try {
965
1096
  const local = await openLocal();
966
- await local.commit({
1097
+ answer = await local.commit({
967
1098
  rooms: records.map(({ locator, record }) => ({
968
1099
  locator,
969
1100
  key: record.key,
970
1101
  record: storedRecord(record),
971
- composition: compositionOf(record),
1102
+ composed: composedOf(composed(record)),
972
1103
  })),
973
1104
  });
974
1105
  }
975
1106
  catch {
976
1107
  journal.record('model revocation persistence failed');
977
1108
  }
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
- }
1109
+ const keys = new Set([...models.keys(), ...rooms.keys()]);
983
1110
  for (const key of keys) {
984
1111
  invalidModels.add(key);
985
1112
  models.set(key, revoked);
@@ -989,8 +1116,8 @@ export function composeRuntime(options) {
989
1116
  for (const { locator, held, record } of records) {
990
1117
  if (replicas.get(locator) !== held)
991
1118
  continue;
992
- recomposed(locator, held.record, record);
993
- replicas.set(locator, { ...held, record });
1119
+ const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
1120
+ replicas.set(locator, { ...held, record, indexed });
994
1121
  }
995
1122
  for (const locator of [...rooms.keys()]) {
996
1123
  const room = rooms.get(locator);
@@ -1034,15 +1161,15 @@ export function composeRuntime(options) {
1034
1161
  ? null
1035
1162
  : {
1036
1163
  roots: selection.roots,
1037
- model: modelTypes(modelOf(room.locator, 'typed-selection')),
1164
+ model: modelTypes(builtIn),
1038
1165
  },
1039
1166
  providerOwnedPaths: room.providerOwnedPaths,
1040
1167
  roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
1041
1168
  synchronizationAvailable,
1042
- refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
1169
+ refusal: scopedRefusals.get(room.locator) ?? null,
1043
1170
  });
1044
1171
  }
1045
- async function applyDiscovery(found, started, covered) {
1172
+ async function applyDiscovery(found, started, left) {
1046
1173
  const lockedBefore = lockedRooms();
1047
1174
  for (const room of found) {
1048
1175
  const fence = fences.get(room.locator);
@@ -1056,18 +1183,11 @@ export function composeRuntime(options) {
1056
1183
  const listedLocators = new Set(listed.map((room) => room.locator));
1057
1184
  for (const locator of listedLocators)
1058
1185
  scopedRefusals.delete(locator);
1059
- if (covered === undefined)
1186
+ if (left === undefined)
1060
1187
  discovered.clear();
1061
- else
1062
- for (const locator of covered)
1063
- discovered.delete(locator);
1064
1188
  for (const room of listed)
1065
1189
  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);
1190
+ await loseRooms(left ?? unlistedHeld(new Set(found.map((room) => room.locator))));
1071
1191
  for (const room of listed) {
1072
1192
  const held = replicas.get(room.locator)?.record;
1073
1193
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -1098,15 +1218,6 @@ export function composeRuntime(options) {
1098
1218
  ]),
1099
1219
  ].filter((locator) => !listed.has(locator));
1100
1220
  }
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
1221
  function fence(locators) {
1111
1222
  const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1112
1223
  const removed = fresh.map((locator) => ({
@@ -1117,31 +1228,29 @@ export function composeRuntime(options) {
1117
1228
  fences.set(locator, { at: discoveries, erased: false });
1118
1229
  lastFenced.set(locator, ++fencings);
1119
1230
  confirmed.delete(locator);
1120
- for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1231
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []) {
1121
1232
  pendings.delete(pendingKey(locator, batch.id));
1233
+ landedAhead.delete(pendingKey(locator, batch.id));
1234
+ }
1122
1235
  replicas.delete(locator);
1236
+ highest.delete(locator);
1237
+ beyondHorizon.delete(locator);
1123
1238
  queuesWithoutReplica.delete(locator);
1124
1239
  rooms.delete(locator);
1125
1240
  roomDrafts.delete(locator);
1126
- transfers.delete(locator);
1241
+ attachedFiles.delete(locator);
1242
+ untransferred(locator);
1243
+ startedOver.delete(locator);
1127
1244
  catching.delete(locator);
1128
1245
  discovered.delete(locator);
1129
- announced.delete(locator);
1130
1246
  unreadable.delete(locator);
1131
1247
  stale.delete(locator);
1132
1248
  repairable.delete(locator);
1133
1249
  unpreparable.delete(locator);
1134
- paused.delete(locator);
1135
- prepared.delete(locator);
1136
1250
  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
- }
1251
+ models.delete(locator);
1252
+ liveModels.delete(locator);
1253
+ invalidModels.delete(locator);
1145
1254
  journal.record(`room lost ${locator}`);
1146
1255
  }
1147
1256
  if (fresh.length === 0)
@@ -1189,7 +1298,14 @@ export function composeRuntime(options) {
1189
1298
  });
1190
1299
  await eraseFenced(fresh);
1191
1300
  }
1192
- function restored(key, persisted) {
1301
+ function retyped(persisted) {
1302
+ if (persisted.status === 'unavailable')
1303
+ return persisted;
1304
+ const { state } = attachedModel({ kind: 'model', snapshot: persisted.snapshot }, NO_MODEL, roomTypes);
1305
+ return state.status === 'unavailable' ? state : { ...persisted, types: state.types };
1306
+ }
1307
+ function restored(key, held) {
1308
+ const persisted = retyped(held);
1193
1309
  const prior = models.get(key);
1194
1310
  if (prior !== undefined &&
1195
1311
  prior.status !== 'unavailable' &&
@@ -1220,8 +1336,9 @@ export function composeRuntime(options) {
1220
1336
  const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1221
1337
  targets = held.targets;
1222
1338
  transfers.clear();
1223
- for (const [locator, record] of held.transfers)
1224
- transfers.set(locator, record);
1339
+ arrivals.clear();
1340
+ streaming.clear();
1341
+ startedOver.clear();
1225
1342
  limits = held.providerLimits;
1226
1343
  capabilities = held.capabilities;
1227
1344
  member = held.member;
@@ -1231,15 +1348,16 @@ export function composeRuntime(options) {
1231
1348
  presentations.clear();
1232
1349
  for (const record of held.presentation)
1233
1350
  presentations.set(record.app, record.value);
1234
- conversations.hydrate(heldConversations, chatDrafts);
1351
+ conversations.hydrate(heldConversations, chatDrafts, held.conversationList, held.conversationCursor);
1235
1352
  pendings.clear();
1353
+ landedAhead.clear();
1236
1354
  for (const queue of queues) {
1237
1355
  for (const { batch, state } of queue.pending) {
1238
1356
  pendings.set(pendingKey(queue.locator, batch.id), state);
1239
1357
  }
1240
1358
  }
1241
1359
  replicas.clear();
1242
- prepared.clear();
1360
+ reopened.clear();
1243
1361
  queuesWithoutReplica.clear();
1244
1362
  for (const [locator, queue] of queuedBatches) {
1245
1363
  if (!held.rooms.has(locator))
@@ -1248,39 +1366,40 @@ export function composeRuntime(options) {
1248
1366
  records.forEach((stored, index) => {
1249
1367
  if (fences.has(stored.locator))
1250
1368
  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 };
1369
+ const record = { ...stored, model: restored(stored.locator, stored.model) };
1370
+ reopened.add(record.locator);
1254
1371
  replicas.set(record.locator, {
1255
1372
  record,
1256
1373
  queue: queuedBatches.get(record.locator) ?? [],
1257
1374
  overlaid: held.overlaid.get(record.key) ?? new Set(),
1258
1375
  count: counts[index] ?? 0,
1259
1376
  confirmed: 0,
1377
+ indexed: !held.unindexed.has(record.key),
1260
1378
  });
1261
- if (!held.unprepared.has(record.key))
1262
- prepared.add(record.locator);
1263
1379
  const existing = rooms.get(record.locator);
1264
1380
  if (existing === undefined)
1265
- list(record, false);
1381
+ list(record, record.roomVersion !== null);
1266
1382
  else {
1267
1383
  rooms.set(record.locator, {
1268
1384
  ...existing,
1269
1385
  roomVersion: record.roomVersion,
1270
1386
  operations: record.operations,
1271
1387
  replica: record.replica,
1272
- model: modelTypes(model),
1388
+ model: modelTypes(record.model),
1273
1389
  typedSelection: record.typedSelection === null
1274
1390
  ? null
1275
- : {
1276
- roots: record.typedSelection.roots,
1277
- model: modelTypes(selectionModel),
1278
- },
1391
+ : { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
1279
1392
  });
1280
1393
  }
1281
1394
  });
1282
- for (const key of held.orphans)
1283
- await opened.release(key);
1395
+ try {
1396
+ for (const key of held.orphans)
1397
+ await opened.release(key);
1398
+ }
1399
+ catch (error) {
1400
+ if (!refusedForSpace(error))
1401
+ throw error;
1402
+ }
1284
1403
  if (held.erasing || held.orphans.length > 0)
1285
1404
  queueMicrotask(sweep);
1286
1405
  return {
@@ -1305,7 +1424,7 @@ export function composeRuntime(options) {
1305
1424
  async function open() {
1306
1425
  if (closed)
1307
1426
  throw new Error('runtime closed');
1308
- const opened = openRuntimeStore(isFenced, options.database);
1427
+ const opened = openRuntimeStore(isFenced, database);
1309
1428
  const interrupted = await opened.fences();
1310
1429
  for (const locator of interrupted) {
1311
1430
  fences.set(locator, { at: discoveries, erased: false });
@@ -1351,10 +1470,29 @@ export function composeRuntime(options) {
1351
1470
  return opening;
1352
1471
  }
1353
1472
  function exclusive(work) {
1473
+ gathering = null;
1354
1474
  const next = mutations.then(work, work);
1355
1475
  mutations = next.then(() => undefined, () => undefined);
1356
1476
  return next;
1357
1477
  }
1478
+ function gathered(key, input, merge, work) {
1479
+ const tail = gathering;
1480
+ if (tail !== null && tail.key === key) {
1481
+ const merged = merge(tail.input, input);
1482
+ if (merged !== null) {
1483
+ tail.input = merged;
1484
+ return tail.done;
1485
+ }
1486
+ }
1487
+ const entry = { key, input, done: Promise.resolve() };
1488
+ entry.done = exclusive(() => {
1489
+ if (gathering === entry)
1490
+ gathering = null;
1491
+ return work(entry.input);
1492
+ });
1493
+ gathering = entry;
1494
+ return entry.done;
1495
+ }
1358
1496
  function withdrawable(locator) {
1359
1497
  return (batch) => {
1360
1498
  const state = pendings.get(pendingKey(locator, batch.id));
@@ -1363,6 +1501,16 @@ export function composeRuntime(options) {
1363
1501
  }
1364
1502
  const REWRITE_LINKS = ['field', 'body'];
1365
1503
  const CHECKED_LINKS = ['field', 'section'];
1504
+ const BODY_LINKS = ['body'];
1505
+ async function textsNeeded(local, record, named) {
1506
+ const paths = [...new Set(named.map((one) => (typeof one === 'string' ? one : one.path)))];
1507
+ const unheld = await unheldIn(local, record.locator, record.key, paths);
1508
+ if (unheld.length === 0)
1509
+ return null;
1510
+ await holdTexts(unheld);
1511
+ const [first] = await unheldIn(local, record.locator, record.key, unheld.map(({ path }) => path));
1512
+ return first?.path ?? null;
1513
+ }
1366
1514
  async function sliceAt(record, view, queue, extra = [], moved = []) {
1367
1515
  const local = await openLocal();
1368
1516
  const needed = new Set([...touchedBy(queue), ...extra]);
@@ -1436,26 +1584,14 @@ export function composeRuntime(options) {
1436
1584
  },
1437
1585
  };
1438
1586
  }
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))
1587
+ function committedRoom(locator, answer) {
1588
+ const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
1589
+ if (!indexed)
1454
1590
  prepare([locator]);
1455
- return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0 };
1591
+ return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
1456
1592
  }
1457
1593
  function replayed(record, confirmedLayer, queue) {
1458
- return stepped(replaying(confirmedLayer, queue, record.locator, compositionOf(record)));
1594
+ return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
1459
1595
  }
1460
1596
  async function overlayDiff(record, before, visible, touched) {
1461
1597
  const kept = before !== undefined && before.record.key === record.key
@@ -1475,8 +1611,6 @@ export function composeRuntime(options) {
1475
1611
  function unpairedIn(locator, batch) {
1476
1612
  if (!queueIn(locator).some((held) => held.id === batch.id))
1477
1613
  return false;
1478
- if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
1479
- return false;
1480
1614
  return !leavingIn(batch, locator) || withdrawable(locator)(batch);
1481
1615
  }
1482
1616
  async function together(local, settlements) {
@@ -1518,11 +1652,17 @@ export function composeRuntime(options) {
1518
1652
  const confirmedLayer = overrides === undefined
1519
1653
  ? read
1520
1654
  : await stepped(slicingWith(read, overrides.set, overrides.removed));
1521
- const composition = compositionOf(record);
1522
- const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
1523
- const founded = kept.filter((batch) => !queue.includes(batch));
1655
+ const composition = composed(record);
1656
+ const shown = queue.filter((batch) => {
1657
+ const held = landedAhead.get(pendingKey(locator, batch.id));
1658
+ return (held !== undefined &&
1659
+ Object.entries(held).some(([path, revision]) => confirmedLayer.revisions[path] !== revision));
1660
+ });
1661
+ const unshown = queue.filter((batch) => !shown.includes(batch));
1662
+ const { withdrawn, kept } = await stepped(standing(confirmedLayer, unshown, withdrawable(locator), localSaves, locator, composition));
1663
+ const founded = kept.filter((batch) => !unshown.includes(batch));
1524
1664
  const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
1525
- const unqueued = [];
1665
+ const unqueued = shown.map((batch) => ({ batch, landed: true }));
1526
1666
  for (const batch of withdrawn) {
1527
1667
  unqueued.push({ batch, landed: await landedWhole(locator, batch, confirmedLayer) });
1528
1668
  }
@@ -1561,6 +1701,7 @@ export function composeRuntime(options) {
1561
1701
  key: record.key,
1562
1702
  record: storedRecord(record),
1563
1703
  roots: record.typedSelection?.roots ?? [],
1704
+ composed: composedOf(composition),
1564
1705
  ...settling.commit,
1565
1706
  ...(overrides === undefined
1566
1707
  ? {}
@@ -1586,8 +1727,10 @@ export function composeRuntime(options) {
1586
1727
  commit,
1587
1728
  apply: (answer) => {
1588
1729
  for (const batch of previousQueue) {
1589
- if (!staying.has(batch.id))
1590
- pendings.delete(pendingKey(locator, batch.id));
1730
+ if (staying.has(batch.id))
1731
+ continue;
1732
+ pendings.delete(pendingKey(locator, batch.id));
1733
+ landedAhead.delete(pendingKey(locator, batch.id));
1591
1734
  }
1592
1735
  for (const draft of written.values())
1593
1736
  rememberRoomDraft(draft);
@@ -1602,14 +1745,14 @@ export function composeRuntime(options) {
1602
1745
  const confirmedChanged = settling.commit?.published !== undefined ||
1603
1746
  settling.commit?.refreshed === true ||
1604
1747
  overrides !== undefined;
1605
- recomposed(locator, before?.record, record);
1606
- const { count } = committedRoom(locator, record, answer);
1748
+ const { count, indexed } = committedRoom(locator, answer);
1607
1749
  replicas.set(locator, {
1608
1750
  record,
1609
1751
  queue: kept,
1610
1752
  overlaid: touched,
1611
1753
  count,
1612
1754
  confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
1755
+ indexed,
1613
1756
  });
1614
1757
  return mergeChangeSets([
1615
1758
  changeSet(locator, {
@@ -1624,110 +1767,107 @@ export function composeRuntime(options) {
1624
1767
  };
1625
1768
  }
1626
1769
  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
- }
1770
+ const model = record.model;
1771
+ models.set(locator, model);
1772
+ invalidModels.delete(locator);
1773
+ if (model.status === 'current')
1774
+ liveModels.set(locator, JSON.stringify(model.snapshot));
1775
+ else if (model.status === 'unavailable')
1776
+ liveModels.delete(locator);
1639
1777
  }
1640
1778
  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
- };
1779
+ const answer = answers?.collection;
1780
+ if (answer === undefined)
1781
+ return held;
1782
+ const { state, refused } = attachedModel(answer, held.collection, roomTypes);
1783
+ if (refused !== null) {
1784
+ const where = refused.location === undefined ? '' : ` ${refused.location}`;
1785
+ journal.record(`model invalid ${locator} collection ${refused.identity} ${refused.rule}${where}`);
1786
+ }
1787
+ models.set(locator, state);
1788
+ return { collection: state };
1656
1789
  }
1657
- function publish(local, room, record, landing, live) {
1658
- return exclusive(async () => {
1790
+ function publish(local, room, reached, landing, live) {
1791
+ const landed = exclusive(async () => {
1659
1792
  if (!live()) {
1660
1793
  caughtUpTo(room.locator);
1661
1794
  return true;
1662
1795
  }
1663
1796
  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);
1797
+ if (!stands(room.locator, landing.from)) {
1798
+ if (landing.kind === 'refreshed') {
1799
+ await local.unrefresh(reached.key);
1800
+ return false;
1801
+ }
1802
+ caughtUpTo(room.locator);
1803
+ await abandoned(local, room.locator);
1667
1804
  return false;
1668
1805
  }
1806
+ const record = landing.kind === 'refreshed' && prior !== undefined
1807
+ ? {
1808
+ ...prior.record,
1809
+ roomVersion: reached.roomVersion,
1810
+ ...(landing.remodelled ? { model: reached.model } : {}),
1811
+ }
1812
+ : reached;
1669
1813
  adoptModels(room.locator, record);
1670
1814
  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
1815
  let set;
1677
1816
  try {
1678
- set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
1817
+ set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
1679
1818
  ? {
1680
1819
  room: true,
1681
1820
  view: 'refreshed',
1682
- commit: { refreshed: true, composition: compositionOf(record) },
1821
+ commit: { refreshed: true },
1683
1822
  }
1684
1823
  : {
1685
1824
  room: true,
1686
1825
  view: landing.refreshed ? 'refreshed' : 'confirmed',
1687
1826
  commit: {
1688
1827
  published: { from: prior?.record.key ?? null },
1689
- composition: compositionOf(record),
1690
1828
  ...(landing.refreshed ? { refreshed: true } : {}),
1691
1829
  },
1692
1830
  });
1693
1831
  }
1694
1832
  catch (error) {
1695
- if (!exceeded(error))
1833
+ if (!refusedForSpace(error))
1696
1834
  throw error;
1697
- await exceededOn(room.locator, null);
1698
1835
  caughtUpTo(room.locator);
1699
1836
  return true;
1700
1837
  }
1701
1838
  if (landing.kind === 'bootstrapped') {
1702
- transfers.delete(room.locator);
1839
+ highest.delete(room.locator);
1840
+ beyondHorizon.delete(room.locator);
1841
+ untransferred(room.locator);
1842
+ startedOver.delete(room.locator);
1703
1843
  catching.delete(room.locator);
1704
- sweep();
1844
+ if (prior !== undefined && prior.record.key !== record.key)
1845
+ sweep();
1846
+ name(room.locator);
1705
1847
  }
1706
- for (const batch of accepted)
1707
- pendings.delete(pendingKey(room.locator, batch.id));
1708
1848
  stale.delete(room.locator);
1709
1849
  unpreparable.delete(room.locator);
1710
1850
  if (unreadable.delete(room.locator))
1711
1851
  scopedRefusals.delete(room.locator);
1712
- sourceRefusals.delete(room.locator);
1713
- declarationRefusals.delete(room.locator);
1714
1852
  list(room, true);
1715
1853
  publishChangeSet(set);
1854
+ if (landing.kind === 'bootstrapped')
1855
+ void heldAgain(room.locator);
1716
1856
  await unpair(local);
1717
1857
  moveTo({ ...status, ...attention() });
1718
- options.adapter.roomStream?.published(room.locator, roomVersion);
1719
- if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1858
+ if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1720
1859
  submitLater(room.locator);
1721
1860
  }
1722
1861
  return true;
1723
1862
  });
1863
+ return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
1724
1864
  }
1725
1865
  async function savedRoom(locator) {
1726
1866
  const local = await openLocal();
1727
1867
  const replica = replicas.get(locator);
1728
1868
  const room = rooms.get(locator);
1729
1869
  if (replica === undefined || room === undefined) {
1730
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1870
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
1731
1871
  }
1732
1872
  return { local, replica, room };
1733
1873
  }
@@ -1745,12 +1885,7 @@ export function composeRuntime(options) {
1745
1885
  return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
1746
1886
  }
1747
1887
  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
- }
1888
+ const read = readSavedParts(named);
1754
1889
  const target = await savedRoom(locator);
1755
1890
  const saved = [];
1756
1891
  const files = [];
@@ -1759,7 +1894,7 @@ export function composeRuntime(options) {
1759
1894
  for (const { path, parts } of read) {
1760
1895
  const file = parts.file === undefined
1761
1896
  ? undefined
1762
- : await heldFile(parts.file.blob, binaryBase(visible, path));
1897
+ : await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
1763
1898
  if (parts.file !== undefined && file !== undefined) {
1764
1899
  files.push({ key: file.localId, blob: parts.file.blob });
1765
1900
  }
@@ -1794,13 +1929,27 @@ export function composeRuntime(options) {
1794
1929
  const referring = removed.length === 0
1795
1930
  ? []
1796
1931
  : (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
1797
- const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
1932
+ const unheld = await textsNeeded(local, record, [
1933
+ ...paths,
1934
+ ...referring,
1935
+ ...(moved.length === 0
1936
+ ? []
1937
+ : await local.referrers(record.key, 'confirmed', moved, REWRITE_LINKS)),
1938
+ ...(removed.length === 0
1939
+ ? []
1940
+ : await local.referrers(record.key, 'confirmed', removed, BODY_LINKS)),
1941
+ ]);
1942
+ if (unheld !== null) {
1943
+ return { refusal: { code: 'transport-uncertainty', retryable: true, file: unheld } };
1944
+ }
1945
+ const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
1798
1946
  const { visible } = await replayed(record, confirmedLayer, replica.queue);
1799
- const composition = compositionOf(record);
1947
+ const composition = composed(record);
1800
1948
  return {
1801
1949
  record,
1802
1950
  confirmed: confirmedLayer,
1803
1951
  visible,
1952
+ folders: new Set(await local.folders(record.key, 'visible', paths)),
1804
1953
  *visibleOver(queue) {
1805
1954
  return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
1806
1955
  },
@@ -1812,13 +1961,7 @@ export function composeRuntime(options) {
1812
1961
  return false;
1813
1962
  const local = await openLocal();
1814
1963
  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)));
1964
+ return local.typedWithin(record.key, 'visible', roots, false);
1822
1965
  }
1823
1966
  function textDiffers(left, right) {
1824
1967
  return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
@@ -1829,18 +1972,6 @@ export function composeRuntime(options) {
1829
1972
  return undefined;
1830
1973
  return entry.binary !== null ? null : entry.content;
1831
1974
  }
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
1975
  function lookupOf(local, key, view) {
1845
1976
  return async (paths) => {
1846
1977
  if (paths.length === 0)
@@ -1857,37 +1988,6 @@ export function composeRuntime(options) {
1857
1988
  const read = parseFrontmatter(entry.content ?? '');
1858
1989
  return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
1859
1990
  }
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
1991
  function overriddenLookup(lookup, overrides) {
1892
1992
  return async (paths) => {
1893
1993
  const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
@@ -1905,9 +2005,8 @@ export function composeRuntime(options) {
1905
2005
  }
1906
2006
  function validatorOf(record, confirmedLayer) {
1907
2007
  return async (resulting, touched) => {
1908
- const composition = compositionFor(record);
1909
2008
  const local = await openLocal();
1910
- const changed = [];
2009
+ const changes = [];
1911
2010
  const overrides = new Map();
1912
2011
  const pace = paced();
1913
2012
  for (const path of new Set([...confirmedLayer.held, ...touched])) {
@@ -1918,19 +2017,13 @@ export function composeRuntime(options) {
1918
2017
  continue;
1919
2018
  overrides.set(path, now);
1920
2019
  if (textOf(was) !== textOf(now))
1921
- changed.push({ path, was: textOf(was), now: textOf(now) });
2020
+ changes.push({ path, was: textOf(was), now: textOf(now) });
1922
2021
  }
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);
2022
+ return validatedNeighbourhood(changes, compositionFor(record), {
2023
+ lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
2024
+ texts: textsOf(local, record.key, 'confirmed'),
2025
+ referrers: referrersOf(local, record.key, 'confirmed'),
2026
+ });
1934
2027
  };
1935
2028
  }
1936
2029
  function textsOf(local, key, view) {
@@ -1954,6 +2047,7 @@ export function composeRuntime(options) {
1954
2047
  operations: room.operations,
1955
2048
  providerOwnedPaths: room.providerOwnedPaths,
1956
2049
  visible: saving.visible,
2050
+ folders: saving.folders,
1957
2051
  queued: queuedDocuments(replica.queue),
1958
2052
  unmodeled: await unmodeledIn(record),
1959
2053
  validate: validatorOf(record, saving.confirmed),
@@ -1974,6 +2068,7 @@ export function composeRuntime(options) {
1974
2068
  locator,
1975
2069
  key: saving.record.key,
1976
2070
  roots: saving.record.typedSelection?.roots ?? [],
2071
+ composed: composedOf(composed(saving.record)),
1977
2072
  overlay: diff.overlay,
1978
2073
  });
1979
2074
  }
@@ -2005,15 +2100,19 @@ export function composeRuntime(options) {
2005
2100
  }
2006
2101
  const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
2007
2102
  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)));
2103
+ if ('refusal' in sourceSaving)
2104
+ return { outcome: 'refused', ...sourceSaving.refusal };
2105
+ const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
2009
2106
  if (changed.length > 0)
2010
2107
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2011
2108
  const target = replicas.get(destination);
2012
2109
  const listedRoom = rooms.get(destination);
2013
2110
  if (target === undefined || listedRoom === undefined) {
2014
- throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
2111
+ throw runtimeError('unknown-room', `Unknown Room ${destination}`);
2015
2112
  }
2016
2113
  const arriving = await savingIn(target, [], moves.map(({ to }) => to));
2114
+ if ('refusal' in arriving)
2115
+ return { outcome: 'refused', ...arriving.refusal };
2017
2116
  const built = await buildMove({
2018
2117
  source: await sideOf(locator, replica, room, sourceSaving),
2019
2118
  destination: await sideOf(destination, target, listedRoom, arriving),
@@ -2074,10 +2173,10 @@ export function composeRuntime(options) {
2074
2173
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2075
2174
  const { answer, touched } = committedBoth;
2076
2175
  if (answer.fenced !== null) {
2077
- throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2176
+ throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2078
2177
  }
2079
2178
  if (fences.has(locator) || fences.has(destination)) {
2080
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2179
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2081
2180
  }
2082
2181
  pendings.set(pendingKey(locator, id), {
2083
2182
  drafts: saved,
@@ -2117,6 +2216,8 @@ export function composeRuntime(options) {
2117
2216
  submitLater(locator);
2118
2217
  }
2119
2218
  async function committed(locator, target, saved, consumed, writtenBy, files = []) {
2219
+ if (full)
2220
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
2120
2221
  const into = movedInto(locator, saved);
2121
2222
  if (into !== null) {
2122
2223
  return committedMove(locator, into, target, saved, consumed, writtenBy, files);
@@ -2128,7 +2229,9 @@ export function composeRuntime(options) {
2128
2229
  ...saved.map(({ path }) => path),
2129
2230
  ...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
2130
2231
  ]);
2131
- const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, underneath(held, path, saving.visible)));
2232
+ if ('refusal' in saving)
2233
+ return { outcome: 'refused', ...saving.refusal };
2234
+ const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
2132
2235
  if (changed.length > 0)
2133
2236
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2134
2237
  const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
@@ -2156,6 +2259,7 @@ export function composeRuntime(options) {
2156
2259
  providerLimits: limits,
2157
2260
  handle: room.handle,
2158
2261
  visible: saving.visible,
2262
+ folders: saving.folders,
2159
2263
  changes,
2160
2264
  bases: parts.flatMap(({ bases }) => bases),
2161
2265
  queued: queuedDocuments(replica.queue),
@@ -2245,10 +2349,10 @@ export function composeRuntime(options) {
2245
2349
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2246
2350
  const { answer, touched } = done;
2247
2351
  if (answer.fenced !== null) {
2248
- throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2352
+ throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2249
2353
  }
2250
2354
  if (fences.has(locator))
2251
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2355
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2252
2356
  for (const origin of withdrawn.keys())
2253
2357
  pendings.delete(pendingKey(locator, origin));
2254
2358
  for (const [id, drafts] of drafted) {
@@ -2297,7 +2401,7 @@ export function composeRuntime(options) {
2297
2401
  throw new Error('connection refused');
2298
2402
  await local.keepUpload(locator, identifier, upload);
2299
2403
  if (fences.has(locator))
2300
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2404
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2301
2405
  const key = pendingKey(locator, identifier);
2302
2406
  pendings.set(key, { ...pendings.get(key), upload });
2303
2407
  if (!continuing())
@@ -2306,6 +2410,9 @@ export function composeRuntime(options) {
2306
2410
  async function readHistory(locator, path, query = {}) {
2307
2411
  if (closed || refusal !== null)
2308
2412
  return UNCERTAIN;
2413
+ await firstDiscovery;
2414
+ if (!roomsNamed)
2415
+ return UNCERTAIN;
2309
2416
  try {
2310
2417
  await ensureAuthorization();
2311
2418
  }
@@ -2395,14 +2502,12 @@ export function composeRuntime(options) {
2395
2502
  return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
2396
2503
  };
2397
2504
  }
2398
- function binaryRoom(request) {
2399
- if (request.kind === 'path')
2400
- return request.locator;
2401
- return options.adapter.referrerScope(request.referrer)?.locator ?? null;
2402
- }
2403
2505
  async function transferBinary(request, transfer, answer) {
2404
2506
  if (closed || refusal !== null)
2405
2507
  return UNCERTAIN;
2508
+ await firstDiscovery;
2509
+ if (!roomsNamed)
2510
+ return UNCERTAIN;
2406
2511
  try {
2407
2512
  await ensureAuthorization();
2408
2513
  }
@@ -2412,16 +2517,13 @@ export function composeRuntime(options) {
2412
2517
  if (closed || authorization === undefined || refusal !== null)
2413
2518
  return UNCERTAIN;
2414
2519
  const since = fencings;
2415
- journal.record(request.kind === 'path'
2416
- ? `transfer binary ${request.locator} ${request.path}`
2417
- : `transfer binary ${request.fileRef}`);
2520
+ journal.record(`transfer binary ${request.locator} ${request.path}`);
2418
2521
  try {
2419
2522
  const result = await transfer(authorization);
2420
2523
  const settled = result;
2421
2524
  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 });
2525
+ if (fencedSince(request.locator, since))
2526
+ return failure({ kind: 'room-not-readable', locator: request.locator });
2425
2527
  if (settled.kind === 'scoped-refusal') {
2426
2528
  const room = replicas.get(settled.locator)?.record;
2427
2529
  if (room !== undefined)
@@ -2436,23 +2538,104 @@ export function composeRuntime(options) {
2436
2538
  return UNCERTAIN;
2437
2539
  }
2438
2540
  }
2541
+ async function heldAgain(locator) {
2542
+ const replica = replicas.get(locator);
2543
+ if (replica === undefined || !textOnline())
2544
+ return;
2545
+ try {
2546
+ const wanted = await (await openLocal()).wanted(locator, replica.record.key);
2547
+ await holdTexts(wanted.map(({ path, revision }) => ({ locator, path, revision })));
2548
+ }
2549
+ catch {
2550
+ journal.record(`hold text ${locator} unwritten`);
2551
+ }
2552
+ }
2553
+ function textOnline() {
2554
+ return !closed && refusal === null && connectionOf() === 'connected';
2555
+ }
2556
+ function textKey({ locator, path }) {
2557
+ return documentKey(locator, path);
2558
+ }
2559
+ async function readText({ locator, path, revision }) {
2560
+ const replica = replicas.get(locator);
2561
+ if (replica === undefined || !textOnline())
2562
+ return;
2563
+ try {
2564
+ await ensureAuthorization();
2565
+ }
2566
+ catch {
2567
+ return;
2568
+ }
2569
+ if (closed || authorization === undefined || refusal !== null)
2570
+ return;
2571
+ const since = fencings;
2572
+ journal.record(`hold text ${locator} ${path}`);
2573
+ const read = await reads
2574
+ .bootstrap(authorization, { locator, selection: replica.record.replica.selection, path })
2575
+ .catch(() => null);
2576
+ journal.record(`outcome text ${read?.kind ?? 'transport-uncertainty'}`);
2577
+ if (read === null || read.kind !== 'bootstrap' || fencedSince(locator, since))
2578
+ return;
2579
+ const content = read.documents[path];
2580
+ const now = replicas.get(locator);
2581
+ if (content === undefined || read.revisions[path] !== revision || now === undefined) {
2582
+ unholdable.set(textKey({ locator, path, revision }), revision);
2583
+ return;
2584
+ }
2585
+ const held = await (await openLocal()).hold(locator, now.record.key, compositionFor(now.record), [{ path, revision, content }]);
2586
+ if (held.length === 0)
2587
+ unholdable.set(textKey({ locator, path, revision }), revision);
2588
+ else
2589
+ publishChangeSet(changeSet(locator, { documents: held }));
2590
+ }
2591
+ function holdText(unheld) {
2592
+ const key = textKey(unheld);
2593
+ const running = holding.get(key);
2594
+ if (running !== undefined)
2595
+ return running;
2596
+ const started = readText(unheld).finally(() => holding.delete(key));
2597
+ holding.set(key, started);
2598
+ return started;
2599
+ }
2600
+ async function holdTexts(wanted) {
2601
+ const queue = wanted.filter((one) => unholdable.get(textKey(one)) !== one.revision);
2602
+ await Promise.all(Array.from({ length: Math.min(TEXT_READS, queue.length) }, async () => {
2603
+ for (let next = queue.shift(); next !== undefined; next = queue.shift()) {
2604
+ await holdText(next);
2605
+ }
2606
+ }));
2607
+ }
2608
+ async function unheldIn(local, locator, key, paths) {
2609
+ if (paths.length === 0)
2610
+ return [];
2611
+ return (await local.entries(key, 'confirmed', paths)).flatMap((entry) => entry.content === null &&
2612
+ entry.binary === null &&
2613
+ entry.outline !== null &&
2614
+ entry.revision !== null
2615
+ ? [{ locator, path: entry.path, revision: entry.revision }]
2616
+ : []);
2617
+ }
2618
+ function fetchBinary(request, name) {
2619
+ return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2620
+ }
2439
2621
  async function readBinary(request, name) {
2440
2622
  if (closed)
2441
2623
  return UNCERTAIN;
2442
2624
  const held = await keptFiles.find(name);
2443
2625
  if (held !== null)
2444
2626
  return held;
2445
- return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2627
+ return fetchBinary(request, name);
2446
2628
  }
2447
2629
  async function binaryPath(locator, path, revision) {
2448
2630
  if (revision !== undefined)
2449
2631
  return { kind: 'path', locator, path, revision };
2450
2632
  const replica = replicas.get(locator);
2451
- if (replica === undefined)
2452
- return null;
2453
2633
  const local = await openLocal();
2454
- const [held] = await local.entries(replica.record.key, 'visible', [path]);
2455
- if (held?.binary === undefined || held.binary === null)
2634
+ const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
2635
+ const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
2636
+ if (attached !== undefined)
2637
+ return { kind: 'path', locator, path, revision: attached.revision };
2638
+ if (replica === undefined || held?.binary === undefined || held.binary === null)
2456
2639
  return null;
2457
2640
  if (held.binary.state === 'pending')
2458
2641
  return { kind: 'pending', localId: held.binary.localId };
@@ -2468,7 +2651,7 @@ export function composeRuntime(options) {
2468
2651
  const holds = (at) => {
2469
2652
  const kept = confirmedLayer.binaries[at];
2470
2653
  return (kept?.state === 'accepted' &&
2471
- kept.fileRef === record.fileRef &&
2654
+ kept.sha256 === record.sha256 &&
2472
2655
  confirmedLayer.revisions[at] === record.revision);
2473
2656
  };
2474
2657
  let confirmedAt = path;
@@ -2482,57 +2665,101 @@ export function composeRuntime(options) {
2482
2665
  locator,
2483
2666
  path: confirmedAt,
2484
2667
  revision: record.revision,
2485
- fileRef: record.fileRef,
2486
2668
  };
2487
2669
  }
2488
- async function readBinaryPath(locator, path, revision) {
2670
+ async function askedBinary(locator, path, revision) {
2489
2671
  await ready;
2490
2672
  const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2491
- const asked = drafted !== undefined && drafted.localId === revision
2673
+ return drafted !== undefined && drafted.localId === revision
2492
2674
  ? { kind: 'pending', localId: revision }
2493
- : await binaryPath(locator, path, revision);
2675
+ : binaryPath(locator, path, revision);
2676
+ }
2677
+ function pendingBinary(locator, path, localId) {
2678
+ const facts = queuedFacts(locator, path, localId);
2679
+ return facts === null
2680
+ ? absentBinary(path)
2681
+ : {
2682
+ kind: 'binary',
2683
+ file: localId,
2684
+ byteLength: facts.byteLength,
2685
+ mediaType: facts.mediaType,
2686
+ };
2687
+ }
2688
+ async function readBinaryPath(locator, path, revision) {
2689
+ const asked = await askedBinary(locator, path, revision);
2494
2690
  if (asked === null)
2495
2691
  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
- }
2692
+ if (asked.kind === 'pending')
2693
+ return pendingBinary(locator, path, asked.localId);
2507
2694
  return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
2508
2695
  }
2696
+ async function heldMediaType(locator, path) {
2697
+ const replica = replicas.get(locator);
2698
+ const [held] = replica === undefined
2699
+ ? []
2700
+ : await (await openLocal()).entries(replica.record.key, 'visible', [path]);
2701
+ return held === undefined
2702
+ ? attachedFiles.get(locator)?.get(path)?.mediaType
2703
+ : held.binary?.mediaType;
2704
+ }
2705
+ function linkOf(asked, held) {
2706
+ return transferBinary(asked, (authorization) => options.adapter.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
2707
+ }
2509
2708
  async function linkBinaryPath(locator, path, revision) {
2510
2709
  await ready;
2511
2710
  const asked = await binaryPath(locator, path, revision);
2512
2711
  if (asked === null || asked.kind === 'pending')
2513
2712
  return absentBinary(path);
2514
- return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
2713
+ return linkOf(asked, revision === undefined ? await heldMediaType(locator, path) : undefined);
2515
2714
  }
2516
- async function readBinaryReference(reference) {
2715
+ async function showBinaryPath(locator, path, revision) {
2716
+ const asked = await askedBinary(locator, path, revision);
2717
+ if (asked === null)
2718
+ return absentBinary(path);
2719
+ if (asked.kind === 'pending')
2720
+ return pendingBinary(locator, path, asked.localId);
2721
+ if (closed)
2722
+ return UNCERTAIN;
2723
+ const name = { locator, path: asked.path, revision: asked.revision };
2724
+ const kept = await keptFiles.find(name);
2725
+ if (kept !== null)
2726
+ return kept;
2727
+ const mediaType = await heldMediaType(locator, path);
2728
+ if (mediaType?.startsWith('image/') === true)
2729
+ return fetchBinary(asked, name);
2730
+ return linkOf(asked, revision === undefined ? mediaType : undefined);
2731
+ }
2732
+ async function attachBinary(locator, file) {
2517
2733
  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 });
2734
+ await firstDiscovery;
2735
+ if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
2736
+ return OFFLINE;
2737
+ const request = await attachedFile(locator, file);
2738
+ return transferBinary({ locator, path: file.name }, async (held) => {
2739
+ const result = await options.adapter.attach(held, request);
2740
+ if (result.kind === 'attached') {
2741
+ const landed = attachedFiles.get(locator) ?? new Map();
2742
+ landed.set(result.path, { revision: result.revision, mediaType: request.mediaType });
2743
+ attachedFiles.set(locator, landed);
2744
+ }
2745
+ return result;
2746
+ }, attachAnswer);
2522
2747
  }
2523
2748
  function readPart(file, part) {
2524
- return options.database.ask({ kind: 'file.part', key: file, part });
2749
+ return database.ask({ kind: 'file.part', key: file, part });
2525
2750
  }
2526
2751
  async function cleanUp() {
2527
- const freed = await options.database.ask({ kind: 'opened.clean' });
2528
- return freed + ((await options.shell.cleanUp?.()) ?? 0);
2529
- }
2530
- function declaredAgents() {
2531
- for (const capability of capabilities) {
2532
- if (capability.conversationAgents !== undefined)
2533
- return capability.conversationAgents;
2752
+ const freed = await database.ask({ kind: 'opened.clean' });
2753
+ const local = await openLocal();
2754
+ let texts = 0;
2755
+ for (const [locator, replica] of replicas) {
2756
+ const released = await local.unhold(locator, replica.record.key, compositionFor(replica.record));
2757
+ texts += released.bytes;
2758
+ if (released.paths.length > 0) {
2759
+ publishChangeSet(changeSet(locator, { documents: released.paths }));
2760
+ }
2534
2761
  }
2535
- return undefined;
2762
+ return freed + texts + ((await options.shell.cleanUp?.()) ?? 0);
2536
2763
  }
2537
2764
  function declaredAction(name) {
2538
2765
  for (const capability of capabilities) {
@@ -2585,6 +2812,24 @@ export function composeRuntime(options) {
2585
2812
  return UNCERTAIN;
2586
2813
  }
2587
2814
  }
2815
+ async function readConversationHistory(request) {
2816
+ await ready;
2817
+ const stream = options.adapter.conversations;
2818
+ if (closed || refusal !== null || stream?.history === undefined)
2819
+ return UNCERTAIN;
2820
+ journal.record('transfer conversation history');
2821
+ try {
2822
+ const result = await stream.history(request);
2823
+ if (result.kind === 'revoked-authorization')
2824
+ await refuse('revoked-authorization');
2825
+ journal.record(`outcome conversation history ${result.kind}`);
2826
+ return calledResult(result);
2827
+ }
2828
+ catch {
2829
+ journal.record('outcome conversation history transport-uncertainty');
2830
+ return UNCERTAIN;
2831
+ }
2832
+ }
2588
2833
  function unsettled(result) {
2589
2834
  return result.kind !== 'action' && result.retryable;
2590
2835
  }
@@ -2646,6 +2891,33 @@ export function composeRuntime(options) {
2646
2891
  const held = replicas.get(locator);
2647
2892
  return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
2648
2893
  }
2894
+ function shownAt(locator) {
2895
+ const held = replicas.get(locator)?.record.roomVersion;
2896
+ const raised = highest.get(locator);
2897
+ if (held === undefined || raised === undefined)
2898
+ return held ?? raised;
2899
+ return options.adapter.reached(locator, held, raised) ? held : raised;
2900
+ }
2901
+ function showing(locator, roomVersion) {
2902
+ const at = shownAt(locator);
2903
+ if (at === undefined || !options.adapter.reached(locator, at, roomVersion)) {
2904
+ highest.set(locator, roomVersion);
2905
+ }
2906
+ }
2907
+ function older(locator, roomVersion) {
2908
+ if (beyondHorizon.has(locator))
2909
+ return false;
2910
+ const at = shownAt(locator);
2911
+ return (at !== undefined &&
2912
+ options.adapter.reached(locator, at, roomVersion) &&
2913
+ !options.adapter.reached(locator, roomVersion, at));
2914
+ }
2915
+ function replaced(locators) {
2916
+ for (const locator of locators) {
2917
+ highest.delete(locator);
2918
+ beyondHorizon.add(locator);
2919
+ }
2920
+ }
2649
2921
  async function writtenAfter(locator, roomVersion, path) {
2650
2922
  const held = replicas.get(locator);
2651
2923
  if (held === undefined)
@@ -2656,169 +2928,233 @@ export function composeRuntime(options) {
2656
2928
  options.adapter.reached(locator, written, roomVersion) &&
2657
2929
  !options.adapter.reached(locator, roomVersion, written));
2658
2930
  }
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;
2931
+ async function writtenLater(local, locator, key, paths, roomVersion) {
2932
+ if (paths.length === 0 || !older(locator, roomVersion))
2933
+ return new Set();
2934
+ const entries = await local.entries(key, 'confirmed', paths);
2935
+ return new Set(entries.flatMap(({ path, written }) => written !== null &&
2936
+ options.adapter.reached(locator, written, roomVersion) &&
2937
+ !options.adapter.reached(locator, roomVersion, written)
2938
+ ? [path]
2939
+ : []));
2753
2940
  }
2754
- function refreshingOf(local, key, liveness) {
2941
+ function refreshingOf(local, locator, key, composition, liveness, replaying = false) {
2942
+ const write = replaying ? finishingWrite : exclusive;
2755
2943
  return {
2944
+ turn() {
2945
+ if (replaying)
2946
+ installing(locator);
2947
+ },
2756
2948
  prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
2949
+ later: (paths, roomVersion) => writtenLater(local, locator, key, paths, roomVersion),
2757
2950
  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 () => {
2951
+ for (let written = 0;;) {
2952
+ const end = batchEnd(page.entries, written);
2953
+ const last = end === page.entries.length;
2954
+ const entries = page.entries.slice(written, end);
2955
+ const staged = await write(async () => {
2762
2956
  if (!liveness())
2763
2957
  return false;
2764
- await local.refresh(key, last
2958
+ await timed(stageBudget, () => local.refresh(key, composition, last
2765
2959
  ? { ...page, entries }
2766
- : { entries, removals: [], folders: [], folderRemovals: [] });
2960
+ : { entries, removals: [], folders: [], folderRemovals: [] }));
2767
2961
  return true;
2768
2962
  });
2769
2963
  if (!staged)
2770
2964
  return false;
2965
+ if (last)
2966
+ return true;
2967
+ written = end;
2771
2968
  }
2772
- return true;
2773
2969
  },
2774
2970
  };
2775
2971
  }
2776
- function batched(entries) {
2777
- const batches = [[]];
2972
+ function batchEnd(entries, from) {
2973
+ let end = from;
2778
2974
  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);
2975
+ while (end < entries.length) {
2976
+ const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
2977
+ if (spent + size > stageBudget.bytes && end > from)
2978
+ break;
2786
2979
  spent += size;
2787
- }
2788
- return batches;
2980
+ end += 1;
2981
+ }
2982
+ return end;
2983
+ }
2984
+ async function timed(budget, work) {
2985
+ const started = performance.now();
2986
+ const done = await work();
2987
+ const spent = performance.now() - started;
2988
+ if (spent < budget.aim / 2)
2989
+ budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
2990
+ else if (spent > budget.aim * 2) {
2991
+ budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
2992
+ }
2993
+ return done;
2994
+ }
2995
+ function installWrite(work) {
2996
+ return installTurn(installTurns.starting, work);
2997
+ }
2998
+ function finishingWrite(work) {
2999
+ return installTurn(installTurns.finishing, work);
3000
+ }
3001
+ function installTurn(queue, work) {
3002
+ return new Promise((resolve, reject) => {
3003
+ queue.push(() => void exclusive(work)
3004
+ .then(resolve, reject)
3005
+ .finally(() => {
3006
+ installTurns.busy = false;
3007
+ nextInstallTurn();
3008
+ }));
3009
+ nextInstallTurn();
3010
+ });
2789
3011
  }
2790
- async function pullRefresh(authorized, held, liveness) {
2791
- if (!liveness())
2792
- return unconfirmed;
2793
- const turn = generation;
2794
- const { record } = held;
2795
- const base = held.confirmed;
2796
- const local = await openLocal();
3012
+ function nextInstallTurn() {
3013
+ if (installTurns.busy || installTurns.ahead > 0)
3014
+ return;
3015
+ const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
3016
+ if (turn === undefined)
3017
+ return;
3018
+ installTurns.busy = true;
3019
+ turn();
3020
+ }
3021
+ async function ahead(work) {
3022
+ let taken = false;
3023
+ const hold = {
3024
+ take() {
3025
+ if (taken)
3026
+ return;
3027
+ taken = true;
3028
+ installTurns.ahead += 1;
3029
+ },
3030
+ release() {
3031
+ if (!taken)
3032
+ return;
3033
+ taken = false;
3034
+ installTurns.ahead -= 1;
3035
+ resumeInstall();
3036
+ },
3037
+ };
3038
+ hold.take();
3039
+ try {
3040
+ return await work(hold);
3041
+ }
3042
+ finally {
3043
+ hold.release();
3044
+ }
3045
+ }
3046
+ function resumeInstall() {
3047
+ if (installTurns.ahead > 0)
3048
+ return;
3049
+ void yielded().then(nextInstallTurn);
3050
+ }
3051
+ async function heldStream() {
3052
+ while (streamsFree === 0)
3053
+ await new Promise((admit) => installWaiters.push(admit));
3054
+ streamsFree -= 1;
3055
+ return once(() => {
3056
+ streamsFree += 1;
3057
+ });
3058
+ }
3059
+ async function unqueued() {
3060
+ while (queuedText >= QUEUED_TEXT) {
3061
+ await new Promise((admit) => installWaiters.push(admit));
3062
+ }
3063
+ }
3064
+ function once(release) {
3065
+ let released = false;
3066
+ return () => {
3067
+ if (released)
3068
+ return;
3069
+ released = true;
3070
+ release();
3071
+ for (const admit of installWaiters.splice(0))
3072
+ admit();
3073
+ };
3074
+ }
3075
+ function installing(locator) {
3076
+ installs.add(locator);
3077
+ queueReadiness();
3078
+ }
3079
+ function untransferred(locator) {
3080
+ transfers.delete(locator);
3081
+ arrivals.delete(locator);
3082
+ streaming.delete(locator);
3083
+ underived.delete(locator);
3084
+ unindexed.delete(locator);
3085
+ }
3086
+ function setBack(locator) {
3087
+ const now = bytesOf(locator, true);
3088
+ const earlier = startedOver.get(locator);
3089
+ startedOver.set(locator, {
3090
+ done: Math.max(now.done, earlier?.done ?? 0),
3091
+ total: Math.max(now.total, earlier?.total ?? 0),
3092
+ });
3093
+ }
3094
+ function owedIndexes(locator, bytes, text) {
3095
+ if (bytes <= 0 || text <= 0)
3096
+ return;
3097
+ unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
3098
+ }
3099
+ function built(locator, { done, text }) {
3100
+ const owed = unindexed.get(locator) ?? [];
3101
+ let left = text;
3102
+ while (!done && left > 0 && owed.length > 0) {
3103
+ const [first] = owed;
3104
+ if (left >= first.text) {
3105
+ left -= first.text;
3106
+ owed.shift();
3107
+ continue;
3108
+ }
3109
+ owed[0] = {
3110
+ bytes: (first.bytes * (first.text - left)) / first.text,
3111
+ text: first.text - left,
3112
+ };
3113
+ left = 0;
3114
+ }
3115
+ if (done || owed.length === 0)
3116
+ unindexed.delete(locator);
3117
+ queueReadiness();
3118
+ }
3119
+ function installed(locator) {
3120
+ if (installs.delete(locator))
3121
+ queueReadiness();
3122
+ }
3123
+ async function pullRefresh(authorized, held, liveness, hold, refresh) {
3124
+ if (!liveness())
3125
+ return unconfirmed;
3126
+ const { record } = held;
3127
+ const from = standingOf(record.locator);
3128
+ const local = await openLocal();
2797
3129
  try {
2798
3130
  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));
3131
+ const result = await advancement(async (authorization, request) => {
3132
+ hold.release();
3133
+ try {
3134
+ return await refresh(authorization, request);
3135
+ }
3136
+ finally {
3137
+ hold.take();
3138
+ }
3139
+ }, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.locator, record.key, composed(record), liveness));
2800
3140
  if (result.kind !== 'advanced') {
2801
3141
  journal.record(`transfer refresh ${record.locator} ${result.kind}`);
2802
3142
  return result;
2803
3143
  }
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
3144
  journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
2813
- return { kind: 'advanced', record: result.record, base };
3145
+ return {
3146
+ kind: 'advanced',
3147
+ record: result.record,
3148
+ from,
3149
+ remodelled: result.record.model !== record.model,
3150
+ };
2814
3151
  }
2815
3152
  catch (error) {
3153
+ if (refusedForSpace(error))
3154
+ return unconfirmed;
2816
3155
  journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
2817
3156
  throw error;
2818
3157
  }
2819
- finally {
2820
- reads.finished(record.locator, turn);
2821
- }
2822
3158
  }
2823
3159
  const heldContents = new Map();
2824
3160
  function heldContentOf(local, key) {
@@ -2836,63 +3172,115 @@ export function composeRuntime(options) {
2836
3172
  heldContents.set(key, made);
2837
3173
  return made;
2838
3174
  }
3175
+ async function abandoned(local, locator) {
3176
+ await local.abandon(locator);
3177
+ setBack(locator);
3178
+ untransferred(locator);
3179
+ liveQueries.recover();
3180
+ sweep();
3181
+ }
2839
3182
  function staging(local, liveness) {
3183
+ const derived = async (locator, key, composition) => {
3184
+ underived.delete(locator);
3185
+ for (;;) {
3186
+ if (!liveness())
3187
+ return false;
3188
+ const done = await finishingWrite(async () => liveness()
3189
+ ? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
3190
+ : null);
3191
+ if (done === null)
3192
+ return false;
3193
+ built(locator, done);
3194
+ if (done.done)
3195
+ return true;
3196
+ }
3197
+ };
2840
3198
  return {
2841
3199
  held: (locator) => transfers.get(locator) ?? null,
3200
+ async turn(locator, step) {
3201
+ installing(locator);
3202
+ return step === 'downloading' ? heldStream() : () => { };
3203
+ },
3204
+ unqueued,
3205
+ queued(text) {
3206
+ queuedText += text;
3207
+ return once(() => {
3208
+ queuedText -= text;
3209
+ });
3210
+ },
3211
+ arriving(locator) {
3212
+ let ended = false;
3213
+ return {
3214
+ at(bytes) {
3215
+ if (ended)
3216
+ return;
3217
+ arrivals.set(locator, bytes);
3218
+ streaming.add(locator);
3219
+ const now = performance.now();
3220
+ if (now - arrivalReported < ARRIVAL_REPORT_MS)
3221
+ return;
3222
+ arrivalReported = now;
3223
+ queueReadiness();
3224
+ },
3225
+ end() {
3226
+ if (ended)
3227
+ return;
3228
+ ended = true;
3229
+ if (streaming.delete(locator))
3230
+ queueReadiness();
3231
+ },
3232
+ };
3233
+ },
3234
+ streamed: options.adapter.roomStream !== undefined,
2842
3235
  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)
3236
+ const { locator } = record;
3237
+ installing(locator);
3238
+ if (!liveness())
3239
+ return null;
3240
+ const key = await installWrite(async () => {
3241
+ if (!liveness())
2864
3242
  return null;
2865
- key = staged;
2866
- }
2867
- transfers.set(record.locator, { ...record, key });
3243
+ return local.stage(record, composition, page);
3244
+ });
3245
+ if (key === null || !liveness())
3246
+ return null;
3247
+ const arrived = !transfers.has(locator);
3248
+ const prior = transfers.get(locator)?.done.bytes ?? 0;
3249
+ const held = { ...record, key };
3250
+ transfers.set(locator, held);
3251
+ if (arrived)
3252
+ liveQueries.recover();
3253
+ installing(locator);
2868
3254
  liveQueries.recover();
2869
- return key;
3255
+ const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
3256
+ owedIndexes(locator, record.done.bytes - prior, text);
3257
+ const owed = text + (underived.get(locator) ?? 0);
3258
+ underived.set(locator, owed);
3259
+ if (owed < deriveBudget.bytes)
3260
+ return held;
3261
+ return (await derived(locator, key, composition)) ? held : null;
2870
3262
  },
2871
- async refreshing(key) {
3263
+ derive: derived,
3264
+ async refreshing(locator, key, composition) {
2872
3265
  await exclusive(() => local.unrefresh(key));
2873
- return refreshingOf(local, key, liveness);
3266
+ return refreshingOf(local, locator, key, composition, liveness, true);
2874
3267
  },
2875
3268
  abandon: (locator) => exclusive(async () => {
2876
- if (!liveness())
2877
- return;
2878
- await local.abandon(locator);
2879
- transfers.delete(locator);
2880
- liveQueries.recover();
2881
- sweep();
3269
+ if (liveness())
3270
+ await abandoned(local, locator);
2882
3271
  }),
2883
- setAside: (locator) => exclusive(async () => {
3272
+ setAside: async (locator) => {
2884
3273
  if (!liveness())
2885
3274
  return;
2886
- await local.abandon(locator, true);
2887
- transfers.delete(locator);
3275
+ setBack(locator);
3276
+ untransferred(locator);
2888
3277
  liveQueries.recover();
2889
- }),
3278
+ },
2890
3279
  release: (key) => exclusive(async () => {
2891
3280
  await local.release(key);
2892
3281
  sweep();
2893
3282
  }),
2894
3283
  content: (key) => heldContentOf(local, key),
2895
- validate: (record, view) => validatedRoom(local, record, view, null),
2896
3284
  };
2897
3285
  }
2898
3286
  function reusable(room, local) {
@@ -2904,30 +3292,35 @@ export function composeRuntime(options) {
2904
3292
  async function pullBootstrap(authorized, room, local, liveness) {
2905
3293
  if (!liveness())
2906
3294
  return unconfirmed;
2907
- const turn = generation;
2908
3295
  if (replicas.has(room.locator)) {
2909
3296
  catching.add(room.locator);
2910
3297
  liveQueries.recover();
2911
3298
  }
2912
3299
  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));
3300
+ const result = await bootstrapRoom(reads, authorized, room, { collection: modelOf(room.locator) }, builtIn, typedPaths, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
2917
3301
  const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
2918
3302
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
2919
- if (result.kind !== 'staged')
3303
+ if (result.kind === 'staged' && older(room.locator, result.record.roomVersion)) {
2920
3304
  caughtUpTo(room.locator);
3305
+ installed(room.locator);
3306
+ await abandoned(local, room.locator);
3307
+ journal.record(`transfer bootstrap ${room.locator} older than shown`);
3308
+ return { kind: 'transport-uncertainty' };
3309
+ }
3310
+ if (result.kind !== 'staged') {
3311
+ caughtUpTo(room.locator);
3312
+ installed(room.locator);
3313
+ }
2921
3314
  return result;
2922
3315
  }
2923
3316
  catch (error) {
2924
- journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
2925
3317
  caughtUpTo(room.locator);
3318
+ installed(room.locator);
3319
+ if (refusedForSpace(error))
3320
+ return unconfirmed;
3321
+ journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
2926
3322
  throw error;
2927
3323
  }
2928
- finally {
2929
- reads.finished(room.locator, turn);
2930
- }
2931
3324
  }
2932
3325
  function caughtUpTo(locator) {
2933
3326
  if (catching.delete(locator))
@@ -2935,72 +3328,83 @@ export function composeRuntime(options) {
2935
3328
  }
2936
3329
  function behind(room) {
2937
3330
  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)));
3331
+ return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
2940
3332
  }
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);
3333
+ function standingOf(locator) {
3334
+ const held = replicas.get(locator);
3335
+ return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
3336
+ }
3337
+ function stands(locator, from) {
3338
+ const now = standingOf(locator);
3339
+ return now.key === from.key && now.confirmed === from.confirmed;
2948
3340
  }
2949
- async function advanceOnce(room, local, authorized, live) {
2950
- if (!live() || paused.has(room.locator))
3341
+ async function advance(room, local, authorized, running) {
3342
+ const live = () => running() && !full;
3343
+ if (!live())
2951
3344
  return unconfirmed;
2952
3345
  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')
3346
+ if (held !== undefined && options.adapter.roomStream !== undefined) {
3347
+ stale.delete(room.locator);
3348
+ name(room.locator);
3349
+ return null;
3350
+ }
3351
+ const refresh = reads.refresh;
3352
+ if (held !== undefined && refresh !== undefined) {
3353
+ const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
3354
+ if (refreshed === 'behind')
3355
+ return advance(room, local, authorized, live);
3356
+ if (refreshed !== 'beyond-horizon')
2980
3357
  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)
3358
+ if (reloadsDue.has(room.locator))
2987
3359
  return unconfirmed;
3360
+ replaced([room.locator]);
2988
3361
  }
3362
+ const from = standingOf(room.locator);
2989
3363
  const bootstrapped = await pullBootstrap(authorized, room, local, live);
3364
+ if (bootstrapped.kind !== 'staged')
3365
+ return live() ? bootstrapped : unconfirmed;
3366
+ const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
3367
+ if (await publish(local, room, bootstrapped.record, landing, live)) {
3368
+ return live() ? null : unconfirmed;
3369
+ }
3370
+ return reachedBy(room.locator, bootstrapped.record.roomVersion)
3371
+ ? null
3372
+ : advance(room, local, authorized, live);
3373
+ }
3374
+ async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
3375
+ const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
2990
3376
  if (!live())
2991
3377
  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;
3378
+ if (refreshed.kind === 'still-current') {
3379
+ stale.delete(room.locator);
3380
+ if (unreadable.delete(room.locator)) {
3381
+ scopedRefusals.delete(room.locator);
3382
+ list(room, true);
3383
+ emitRooms();
3384
+ }
3385
+ return null;
3386
+ }
3387
+ if (refreshed.kind === 'advanced') {
3388
+ const landing = {
3389
+ kind: 'refreshed',
3390
+ from: refreshed.from,
3391
+ remodelled: refreshed.remodelled,
3392
+ };
3393
+ if (!(await publish(local, room, refreshed.record, landing, live))) {
3394
+ const now = replicas.get(room.locator)?.record.roomVersion;
3395
+ const reached = now !== undefined &&
3396
+ options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3397
+ return reached ? null : 'behind';
3398
+ }
3399
+ await retainModel(local, room, modelOf(room.locator));
3400
+ return null;
3401
+ }
3402
+ return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
2996
3403
  }
2997
3404
  async function withhold(withheld, room, local, progress) {
2998
- if (withheld.kind === 'unconfirmed')
3405
+ if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
2999
3406
  return;
3000
- if (withheld.kind === 'refused')
3001
- stale.delete(room.locator);
3002
- else
3003
- stale.add(room.locator);
3407
+ stale.add(room.locator);
3004
3408
  if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
3005
3409
  const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
3006
3410
  repairable.set(room.locator, reset);
@@ -3022,30 +3426,10 @@ export function composeRuntime(options) {
3022
3426
  list(room, false);
3023
3427
  emitRooms();
3024
3428
  }
3025
- else if (withheld.kind === 'refused') {
3026
- refusedSource(room, withheld);
3027
- }
3028
3429
  else if (withheld.kind === 'revoked-authorization') {
3029
3430
  await refuse('revoked-authorization');
3030
3431
  }
3031
3432
  }
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
3433
  function leaving(batch, result) {
3050
3434
  if (result.kind === 'expired')
3051
3435
  return { batch, returned: returnedBy('expired') };
@@ -3067,8 +3451,8 @@ export function composeRuntime(options) {
3067
3451
  function settling(acceptance) {
3068
3452
  return acceptance !== null && 'apply' in acceptance;
3069
3453
  }
3070
- function refusalOf(acceptance) {
3071
- return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
3454
+ function failureOf(acceptance) {
3455
+ return settling(acceptance) ? null : acceptance;
3072
3456
  }
3073
3457
  async function acceptance(room, local, submitted, accepted) {
3074
3458
  if (!continuing())
@@ -3081,7 +3465,8 @@ export function composeRuntime(options) {
3081
3465
  logAccepted(room.locator, submitted, accepted);
3082
3466
  let record = current.record;
3083
3467
  let changes;
3084
- if (!options.adapter.reached(room.locator, held, accepted.roomVersion)) {
3468
+ if (!options.adapter.reached(room.locator, held, accepted.roomVersion) &&
3469
+ !older(room.locator, accepted.roomVersion)) {
3085
3470
  const paths = [...returnedPaths(submitted, accepted)];
3086
3471
  const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
3087
3472
  const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
@@ -3091,34 +3476,6 @@ export function composeRuntime(options) {
3091
3476
  ? accepted.roomVersion
3092
3477
  : held,
3093
3478
  };
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
3479
  changes = back;
3123
3480
  }
3124
3481
  const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
@@ -3126,10 +3483,9 @@ export function composeRuntime(options) {
3126
3483
  ...settled,
3127
3484
  apply(answer) {
3128
3485
  const set = settled.apply(answer);
3486
+ showing(room.locator, accepted.roomVersion);
3129
3487
  pendings.delete(pendingKey(room.locator, submitted.id));
3130
3488
  unreadable.delete(room.locator);
3131
- sourceRefusals.delete(room.locator);
3132
- declarationRefusals.delete(room.locator);
3133
3489
  list(room, true);
3134
3490
  return set;
3135
3491
  },
@@ -3147,13 +3503,9 @@ export function composeRuntime(options) {
3147
3503
  const failed = await exclusive(async () => {
3148
3504
  const adopting = await acceptance(room, local, submitted, accepted);
3149
3505
  await adoptedTogether(local, [adopting]);
3150
- return refusalOf(adopting);
3506
+ return failureOf(adopting);
3151
3507
  });
3152
- if (failed === null)
3153
- return true;
3154
- if (failed.kind === 'refused' && continuing())
3155
- refusedSource(room, failed);
3156
- return false;
3508
+ return failed === null;
3157
3509
  }
3158
3510
  async function adoptedMove(room, partner, local, submitted, accepted) {
3159
3511
  const other = discovered.get(partner);
@@ -3167,18 +3519,10 @@ export function composeRuntime(options) {
3167
3519
  ...accepted.destination,
3168
3520
  });
3169
3521
  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);
3522
+ return failureOf(left);
3175
3523
  });
3176
3524
  submitLater(partner);
3177
- if (failed === null)
3178
- return true;
3179
- if (failed.kind === 'refused' && continuing())
3180
- refusedSource(room, failed);
3181
- return false;
3525
+ return failed === null;
3182
3526
  }
3183
3527
  function queueIn(locator) {
3184
3528
  return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
@@ -3297,12 +3641,10 @@ export function composeRuntime(options) {
3297
3641
  async function submitRoom(room, local, authorized, progress, signal) {
3298
3642
  while (continuing() && authorization !== undefined && !signal.aborted) {
3299
3643
  const replica = replicas.get(room.locator);
3300
- const head = replica?.queue[0];
3644
+ const head = replica?.queue.find((batch) => !landedAhead.has(pendingKey(room.locator, batch.id)));
3301
3645
  if (replica === undefined || head === undefined)
3302
3646
  return 'done';
3303
3647
  const state = pendings.get(pendingKey(room.locator, head.id));
3304
- if (state?.accepted !== undefined)
3305
- return 'done';
3306
3648
  const partner = partnerOf(head, room.locator);
3307
3649
  if (partner !== null && !leavingIn(head, room.locator)) {
3308
3650
  submitLater(partner);
@@ -3416,12 +3758,12 @@ export function composeRuntime(options) {
3416
3758
  }
3417
3759
  if (result.kind === 'expired') {
3418
3760
  await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
3419
- const there = other === undefined ? null : await advance(other, local, authorized, continuing);
3761
+ const there = other === undefined ? null : await advanced(other, local, authorized);
3420
3762
  if (other !== undefined && there !== null) {
3421
3763
  await withhold(there, other, local, progress);
3422
3764
  return 'done';
3423
3765
  }
3424
- const withheld = await advance(room, local, authorized, continuing);
3766
+ const withheld = await advanced(room, local, authorized);
3425
3767
  if (withheld !== null) {
3426
3768
  await withhold(withheld, room, local, progress);
3427
3769
  return 'done';
@@ -3443,8 +3785,8 @@ export function composeRuntime(options) {
3443
3785
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
3444
3786
  const left = leaving(head, result);
3445
3787
  const refreshed = async () => ({
3446
- there: other === undefined ? null : await advance(other, local, authorized, continuing),
3447
- here: await advance(room, local, authorized, continuing),
3788
+ there: other === undefined ? null : await advanced(other, local, authorized),
3789
+ here: await advanced(room, local, authorized),
3448
3790
  });
3449
3791
  const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
3450
3792
  if (judged !== null && (judged.there !== null || judged.here !== null)) {
@@ -3468,6 +3810,13 @@ export function composeRuntime(options) {
3468
3810
  (await landedWhole(room.locator, head, confirmedLayer))) ||
3469
3811
  (result.kind === 'conflicted' &&
3470
3812
  (await landedWhole(room.locator, head, confirmedLayer, result.affected)));
3813
+ if (held &&
3814
+ result.kind === 'conflicted' &&
3815
+ partner === null &&
3816
+ !landed(head, confirmedLayer)) {
3817
+ landedAhead.set(pendingKey(room.locator, head.id), Object.fromEntries([...touchedBy([head])].map((path) => [path, confirmedLayer.revisions[path]])));
3818
+ return;
3819
+ }
3471
3820
  publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
3472
3821
  await unpair(local);
3473
3822
  moveTo({ ...status, ...attention() });
@@ -3515,6 +3864,7 @@ export function composeRuntime(options) {
3515
3864
  locator: room.locator,
3516
3865
  key: before.record.key,
3517
3866
  roots: before.record.typedSelection?.roots ?? [],
3867
+ composed: composedOf(composed(before.record)),
3518
3868
  overlay: { set: [], cleared: [...before.overlaid] },
3519
3869
  },
3520
3870
  ]),
@@ -3523,8 +3873,10 @@ export function composeRuntime(options) {
3523
3873
  for (const { room, before, queue } of held) {
3524
3874
  const { locator } = room;
3525
3875
  lastFenced.set(locator, ++fencings);
3526
- for (const batch of queue)
3876
+ for (const batch of queue) {
3527
3877
  pendings.delete(pendingKey(locator, batch.id));
3878
+ landedAhead.delete(pendingKey(locator, batch.id));
3879
+ }
3528
3880
  queuesWithoutReplica.delete(locator);
3529
3881
  if (before !== undefined) {
3530
3882
  replicas.set(locator, {
@@ -3547,39 +3899,61 @@ export function composeRuntime(options) {
3547
3899
  if (signal.aborted)
3548
3900
  return;
3549
3901
  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
- });
3902
+ await roomTask(room.locator, (live) => reload(room, local, progress, live));
3556
3903
  if (signal.aborted)
3557
3904
  return;
3558
3905
  await rest(progress);
3559
3906
  }
3560
3907
  }
3561
3908
  function awaitingAnswer(locator) {
3909
+ answering.get(locator)?.release();
3562
3910
  let release = () => { };
3563
- const awaited = new Promise((resolve) => {
3911
+ const answered = new Promise((resolve) => {
3564
3912
  release = resolve;
3565
3913
  });
3566
- answering.set(locator, { generation, answered: awaited });
3914
+ const awaited = { answered, release };
3915
+ answering.set(locator, awaited);
3567
3916
  return () => {
3568
- if (answering.get(locator)?.answered === awaited)
3917
+ if (answering.get(locator) === awaited)
3569
3918
  answering.delete(locator);
3570
3919
  release();
3571
3920
  };
3572
3921
  }
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))))
3922
+ function declaredAsHeld(room) {
3923
+ const held = replicas.get(room.locator)?.record;
3924
+ return (held === undefined ||
3925
+ (JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
3926
+ JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
3927
+ JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
3928
+ }
3929
+ async function keepNaming(local, room) {
3930
+ const held = replicas.get(room.locator)?.record;
3931
+ if (held === undefined || (held.name === room.name && held.handle === room.handle))
3932
+ return;
3933
+ await exclusive(async () => {
3934
+ if (!continuing())
3935
+ return;
3936
+ const replica = replicas.get(room.locator);
3937
+ if (replica === undefined)
3938
+ return;
3939
+ const record = { ...replica.record, name: room.name, handle: room.handle };
3940
+ await local.commit({
3941
+ rooms: [
3942
+ {
3943
+ locator: room.locator,
3944
+ key: record.key,
3945
+ record: storedRecord(record),
3946
+ composed: composedOf(composed(record)),
3947
+ },
3948
+ ],
3949
+ });
3950
+ replicas.set(room.locator, { ...replica, record });
3951
+ });
3952
+ }
3953
+ async function retainModel(local, room, next) {
3954
+ const key = room.locator;
3955
+ const previous = modelOf(room.locator);
3956
+ if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(room))
3583
3957
  return;
3584
3958
  if (next.status === 'unavailable') {
3585
3959
  invalidModels.add(key);
@@ -3591,26 +3965,15 @@ export function composeRuntime(options) {
3591
3965
  const held = replicas.get(room.locator);
3592
3966
  let changed = null;
3593
3967
  if (held !== undefined) {
3594
- const declared = {
3968
+ const record = {
3595
3969
  ...held.record,
3970
+ name: room.name,
3971
+ handle: room.handle,
3596
3972
  operations: room.operations,
3597
3973
  replica: room.replica,
3598
3974
  typedSelection: room.typedSelection ?? null,
3599
- ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
3975
+ model: next,
3600
3976
  };
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
3977
  const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
3615
3978
  const replayedLayer = await replayed(record, confirmedLayer, held.queue);
3616
3979
  const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
@@ -3621,41 +3984,33 @@ export function composeRuntime(options) {
3621
3984
  key: record.key,
3622
3985
  record: storedRecord(record),
3623
3986
  roots: record.typedSelection?.roots ?? [],
3624
- composition: compositionOf(record),
3987
+ composed: composedOf(composed(record)),
3625
3988
  overlay: diff.overlay,
3626
3989
  },
3627
3990
  ],
3628
3991
  });
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 });
3992
+ const { count, indexed } = committedRoom(room.locator, answer);
3993
+ replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
3632
3994
  changed = changeSet(room.locator, {
3633
3995
  documents: answer.changed[room.locator] ?? [],
3634
3996
  room: true,
3635
3997
  });
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
- }
3647
- models.set(key, next);
3648
- if (next.status === 'current') {
3649
- liveModels.set(key, JSON.stringify(next.snapshot));
3650
- invalidModels.delete(key);
3651
3998
  }
3652
- list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
3999
+ heldModel(room, next);
3653
4000
  if (changed === null)
3654
4001
  emitRooms();
3655
4002
  else
3656
4003
  publishChangeSet(changed);
3657
4004
  });
3658
4005
  }
4006
+ function heldModel(room, next) {
4007
+ models.set(room.locator, next);
4008
+ if (next.status === 'current') {
4009
+ liveModels.set(room.locator, JSON.stringify(next.snapshot));
4010
+ invalidModels.delete(room.locator);
4011
+ }
4012
+ list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
4013
+ }
3659
4014
  async function scoped(result, room, local) {
3660
4015
  if (result.scope === 'content' && !result.retryable) {
3661
4016
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
@@ -3669,14 +4024,14 @@ export function composeRuntime(options) {
3669
4024
  });
3670
4025
  let scopedAway = room;
3671
4026
  if (result.scope === 'model')
3672
- await retainModel(local, room, 'collection', {
4027
+ await retainModel(local, room, {
3673
4028
  status: 'unavailable',
3674
4029
  reason: 'not-readable',
3675
4030
  });
3676
4031
  else if (result.scope !== 'operation') {
3677
4032
  unreadable.add(room.locator);
3678
4033
  scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
3679
- await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
4034
+ await retainModel(local, scopedAway, modelOf(room.locator));
3680
4035
  }
3681
4036
  if (!continuing())
3682
4037
  return;
@@ -3684,6 +4039,17 @@ export function composeRuntime(options) {
3684
4039
  emitRooms();
3685
4040
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
3686
4041
  }
4042
+ function name(locator) {
4043
+ const stream = options.adapter.roomStream;
4044
+ const record = replicas.get(locator)?.record;
4045
+ const room = discovered.get(locator);
4046
+ if (stream === undefined || record === undefined || room === undefined)
4047
+ return;
4048
+ if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
4049
+ return;
4050
+ naming += 1;
4051
+ stream.hold([{ locator, roomVersion: record.roomVersion }]);
4052
+ }
3687
4053
  function heldRooms(listed) {
3688
4054
  return listed.flatMap((room) => {
3689
4055
  const record = replicas.get(room.locator)?.record;
@@ -3701,14 +4067,19 @@ export function composeRuntime(options) {
3701
4067
  return outcome?.status === 'fulfilled' && outcome.value;
3702
4068
  });
3703
4069
  if (attempt === 'opened') {
4070
+ naming += 1;
3704
4071
  options.adapter.roomStream?.hold(heldRooms(ready));
3705
4072
  handed = true;
3706
4073
  }
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');
4074
+ for (const room of ready) {
4075
+ if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
4076
+ if (attempt === 'authorized')
4077
+ name(room.locator);
4078
+ continue;
4079
+ }
4080
+ void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
4081
+ }
4082
+ const failed = prepared.find((room) => room.status === 'rejected');
3712
4083
  if (failed !== undefined)
3713
4084
  throw failed.reason;
3714
4085
  }
@@ -3726,27 +4097,31 @@ export function composeRuntime(options) {
3726
4097
  return false;
3727
4098
  }
3728
4099
  if (prior !== undefined)
3729
- await retainModel(local, room, 'collection', modelOf(room.locator));
4100
+ await retainModel(local, room, modelOf(room.locator));
4101
+ if (prior !== undefined)
4102
+ await keepNaming(local, room);
3730
4103
  if (discoveryPaused || authorization === undefined)
3731
4104
  return false;
3732
4105
  return room.replica.admission === 'admitted';
3733
4106
  }
3734
4107
  async function caughtUp(room, local, progress, live) {
3735
- if (authorization === undefined || !live() || !behind(room))
4108
+ if (options.adapter.roomStream !== undefined || authorization === undefined)
4109
+ return;
4110
+ if (!live() || !behind(room))
3736
4111
  return;
3737
4112
  const withheld = await advance(room, local, authorization, live);
3738
4113
  if (withheld !== null)
3739
4114
  await withhold(withheld, room, local, progress);
3740
4115
  }
3741
4116
  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
- });
4117
+ const model = modelOf(locator);
4118
+ return model.status === 'unavailable' && model.reason === 'revoked';
3746
4119
  }
3747
4120
  async function loadRoom(room, local, progress, live) {
3748
4121
  if (!live() || authorization === undefined)
3749
4122
  return;
4123
+ if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
4124
+ return;
3750
4125
  if (replicas.has(room.locator) && revoked(room.locator)) {
3751
4126
  await reload(room, local, progress, live);
3752
4127
  return;
@@ -3764,21 +4139,53 @@ export function composeRuntime(options) {
3764
4139
  reloadsDue.delete(room.locator);
3765
4140
  if (!live() || authorization === undefined)
3766
4141
  return;
4142
+ const from = standingOf(room.locator);
3767
4143
  const bootstrapped = await pullBootstrap(authorization, room, local, live);
3768
- if (!live())
3769
- return;
3770
4144
  if (bootstrapped.kind !== 'staged') {
3771
- await withhold(bootstrapped, room, local, progress);
4145
+ if (live())
4146
+ await withhold(bootstrapped, room, local, progress);
3772
4147
  return;
3773
4148
  }
3774
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
4149
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
3775
4150
  await caughtUp(room, local, progress, live);
3776
4151
  }
3777
4152
  finally {
3778
4153
  caughtUpTo(room.locator);
3779
4154
  }
3780
4155
  }
3781
- async function discoverRooms(local, progress, attempt, live, scope) {
4156
+ async function asked(authorized) {
4157
+ try {
4158
+ return (await options.adapter.discover?.(authorized)) ?? { kind: 'transport-uncertainty' };
4159
+ }
4160
+ catch {
4161
+ return { kind: 'transport-uncertainty' };
4162
+ }
4163
+ }
4164
+ function malformed(listed) {
4165
+ for (const room of listed) {
4166
+ if (room.operations === undefined ||
4167
+ !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
4168
+ room.operations[operation].every((scope) => typeof scope === 'string')) ||
4169
+ room.replica === undefined ||
4170
+ typeof room.replica.selection !== 'string' ||
4171
+ room.replica.selection.length === 0 ||
4172
+ !['admitted', 'unavailable'].includes(room.replica.admission) ||
4173
+ !['locked', 'open'].includes(room.replica.offline)) {
4174
+ return `invalid model or replica declaration ${room.locator}`;
4175
+ }
4176
+ if (!declaredSelection(room))
4177
+ return `invalid typed selection declaration ${room.locator}`;
4178
+ }
4179
+ return null;
4180
+ }
4181
+ async function signIn(local, signedIn) {
4182
+ if (JSON.stringify(signedIn) === JSON.stringify(member))
4183
+ return;
4184
+ await local.fact('member', signedIn);
4185
+ member = signedIn;
4186
+ emitMember();
4187
+ }
4188
+ async function discoverRooms(local, progress, attempt, live, found) {
3782
4189
  if (closed || authorization === undefined) {
3783
4190
  notifyReady();
3784
4191
  return;
@@ -3786,13 +4193,9 @@ export function composeRuntime(options) {
3786
4193
  moveTo(transferring(status));
3787
4194
  discoveries += 1;
3788
4195
  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
- }
4196
+ const result = found ?? (await asked(authorization));
4197
+ if (result.kind === 'discovery')
4198
+ roomsNamed = true;
3796
4199
  if (!live()) {
3797
4200
  progress.complete = false;
3798
4201
  notifyReady();
@@ -3800,12 +4203,7 @@ export function composeRuntime(options) {
3800
4203
  }
3801
4204
  journal.record(`connection discover ${result.kind}`);
3802
4205
  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
- }
4206
+ await cachedModels(local);
3809
4207
  progress.reachable = false;
3810
4208
  progress.complete = false;
3811
4209
  notifyReady();
@@ -3826,26 +4224,12 @@ export function composeRuntime(options) {
3826
4224
  notifyReady();
3827
4225
  return;
3828
4226
  }
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
- }
4227
+ const invalid = malformed(result.rooms);
4228
+ if (invalid !== null) {
4229
+ journal.record(invalid);
4230
+ await refuse('missing-required-guarantee');
4231
+ notifyReady();
4232
+ return;
3849
4233
  }
3850
4234
  if (refusal !== null) {
3851
4235
  refusal = null;
@@ -3861,13 +4245,8 @@ export function composeRuntime(options) {
3861
4245
  await local.fact('providerLimits', limits);
3862
4246
  await local.fact('capabilities', capabilities);
3863
4247
  }
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);
4248
+ await signIn(local, result.member ?? null);
4249
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
3871
4250
  }
3872
4251
  async function ensureAuthorization() {
3873
4252
  if (authorization !== undefined)
@@ -3882,23 +4261,45 @@ export function composeRuntime(options) {
3882
4261
  }
3883
4262
  }
3884
4263
  async function admittedRead(work) {
3885
- if (roomReads.generation !== generation) {
3886
- roomReads = { generation, free: ROOM_READS, waiting: [] };
4264
+ while (roomReads.free === 0) {
4265
+ await new Promise((admit) => roomReads.waiting.push(admit));
3887
4266
  }
3888
- const reads = roomReads;
3889
- while (reads.free === 0)
3890
- await new Promise((admit) => reads.waiting.push(admit));
3891
- reads.free -= 1;
4267
+ roomReads.free -= 1;
3892
4268
  try {
3893
4269
  await work();
3894
4270
  }
3895
4271
  finally {
3896
- reads.free += 1;
3897
- reads.waiting.shift()?.();
4272
+ roomReads.free += 1;
4273
+ roomReads.waiting.shift()?.();
3898
4274
  }
3899
4275
  }
4276
+ function inRoom(locator, work) {
4277
+ roomWork += 1;
4278
+ const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
4279
+ try {
4280
+ return await work(continuing);
4281
+ }
4282
+ finally {
4283
+ roomWork -= 1;
4284
+ }
4285
+ });
4286
+ const chain = ran.then(() => undefined, () => undefined);
4287
+ roomLanes.set(locator, chain);
4288
+ working.add(chain);
4289
+ void chain.finally(() => {
4290
+ working.delete(chain);
4291
+ if (roomLanes.get(locator) === chain)
4292
+ roomLanes.delete(locator);
4293
+ });
4294
+ return ran;
4295
+ }
3900
4296
  function roomTask(locator, work) {
3901
- return task(roomLane(locator), (live) => admittedRead(() => work(live)));
4297
+ if (stopping)
4298
+ return Promise.resolve();
4299
+ return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
4300
+ }
4301
+ function advanced(room, local, authorized) {
4302
+ return inRoom(room.locator, (live) => advance(room, local, authorized, live));
3902
4303
  }
3903
4304
  function task(lane, work) {
3904
4305
  if (stopping)
@@ -3914,23 +4315,19 @@ export function composeRuntime(options) {
3914
4315
  void run.finally(() => working.delete(run));
3915
4316
  return run;
3916
4317
  }
3917
- function sync(attempt) {
4318
+ function sync(attempt, found) {
3918
4319
  return task(OPENING_LANE, async (live) => {
3919
4320
  if (!live()) {
3920
- notifyReady();
4321
+ readyFromStore();
3921
4322
  return;
3922
4323
  }
3923
4324
  const progress = { complete: true, reachable: true };
3924
- const gate = discover();
3925
- if (attempt === 'opened')
3926
- opened = generation;
3927
- underway = attempt === 'opened' ? { generation, progress } : null;
4325
+ underway = attempt === 'opened' ? progress : null;
3928
4326
  if (attempt === 'opened')
3929
4327
  streamed = progress;
3930
4328
  complete = false;
3931
4329
  recorded = false;
3932
4330
  moveTo(transferring(status));
3933
- handed = false;
3934
4331
  try {
3935
4332
  const local = await openLocal();
3936
4333
  if (!live())
@@ -3938,7 +4335,7 @@ export function composeRuntime(options) {
3938
4335
  await ensureAuthorization();
3939
4336
  if (!live())
3940
4337
  return;
3941
- await discoverRooms(local, progress, attempt, live);
4338
+ await discoverRooms(local, progress, attempt, live, found);
3942
4339
  }
3943
4340
  catch {
3944
4341
  progress.complete = false;
@@ -3946,7 +4343,6 @@ export function composeRuntime(options) {
3946
4343
  progress.reachable = false;
3947
4344
  }
3948
4345
  finally {
3949
- gate.release();
3950
4346
  notifyReady();
3951
4347
  }
3952
4348
  if (!live())
@@ -3958,35 +4354,59 @@ export function composeRuntime(options) {
3958
4354
  if (handed || closed || progress.reachable)
3959
4355
  return;
3960
4356
  underway = null;
3961
- opened = null;
3962
- journal.record('connection reopen');
3963
4357
  await rest(progress, live);
3964
- options.adapter.roomStream?.reopen();
3965
4358
  });
3966
4359
  }
3967
- function rediscover(scope) {
4360
+ function resynchronize() {
3968
4361
  return task(OPENING_LANE, async (live) => {
3969
- if (!live() || !socketOpen)
4362
+ if (!live() || !announced)
3970
4363
  return;
3971
4364
  const progress = { complete: true, reachable: true };
3972
- const gate = discover();
3973
4365
  moveTo(transferring(status));
3974
4366
  try {
3975
4367
  const local = await openLocal();
3976
4368
  if (!live())
3977
4369
  return;
3978
- await ensureAuthorization();
4370
+ await synchronize([...discovered.values()], local, progress, 'unstreamed', live);
4371
+ }
4372
+ catch {
4373
+ progress.complete = false;
4374
+ }
4375
+ if (live())
4376
+ await finishOpening(progress, live);
4377
+ });
4378
+ }
4379
+ function countAuthorityChange() {
4380
+ authorityChanges += 1;
4381
+ for (const listener of authorityChangeListeners)
4382
+ listener(authorityChanges);
4383
+ }
4384
+ function changeAuthority(change) {
4385
+ return task(OPENING_LANE, async (live) => {
4386
+ if (!live() || !announced)
4387
+ return;
4388
+ countAuthorityChange();
4389
+ const progress = { complete: true, reachable: true };
4390
+ moveTo(transferring(status));
4391
+ try {
4392
+ const local = await openLocal();
3979
4393
  if (!live())
3980
4394
  return;
3981
- await discoverRooms(local, progress, 'authorized', live, scope);
4395
+ const invalid = malformed(change.rooms);
4396
+ if (invalid !== null) {
4397
+ journal.record(invalid);
4398
+ await refuse('missing-required-guarantee');
4399
+ return;
4400
+ }
4401
+ if (change.member !== undefined)
4402
+ await signIn(local, change.member);
4403
+ discoveries += 1;
4404
+ const listed = await applyDiscovery(change.rooms, discoveries, change.left);
4405
+ await synchronize(listed, local, progress, 'authorized', live);
3982
4406
  }
3983
4407
  catch {
3984
4408
  progress.complete = false;
3985
4409
  }
3986
- finally {
3987
- gate.release();
3988
- notifyReady();
3989
- }
3990
4410
  if (!live())
3991
4411
  return;
3992
4412
  if (!progress.complete)
@@ -3994,14 +4414,17 @@ export function composeRuntime(options) {
3994
4414
  await rest(progress, live);
3995
4415
  });
3996
4416
  }
3997
- function closedSocket(revoked, lost) {
3998
- const opened = socketOpen;
4417
+ function closedSocket(revoked, lost, authorityChanged) {
4418
+ notifyReady();
4419
+ const opened = announced;
3999
4420
  socketOpen = false;
4421
+ announced = false;
4422
+ missedChange ||= authorityChanged;
4423
+ handed = false;
4424
+ naming = 0;
4000
4425
  connectionLost();
4001
- if (lost) {
4002
- reads.lost();
4426
+ if (lost)
4003
4427
  conversations.lost();
4004
- }
4005
4428
  if (revoked) {
4006
4429
  void refuse('revoked-authorization');
4007
4430
  return;
@@ -4012,18 +4435,32 @@ export function composeRuntime(options) {
4012
4435
  moveTo({
4013
4436
  ...status,
4014
4437
  state: status.state === 'synced' ? 'syncing' : status.state,
4015
- providerUnreachable: status.providerUnreachable || !opened,
4438
+ providerUnreachable: status.providerUnreachable || (!opened && lost),
4439
+ });
4440
+ void closing(opened);
4441
+ }
4442
+ function closing(opened) {
4443
+ return task(OPENING_LANE, async (live) => {
4444
+ if (!live())
4445
+ return;
4446
+ const local = await openLocal();
4447
+ if (!opened && !socketOpen)
4448
+ await cachedModels(local);
4449
+ await rest(null, live);
4016
4450
  });
4017
4451
  }
4018
- function applyOpening(expired) {
4019
- const progress = underway?.generation === generation ? underway.progress : null;
4452
+ function cachedModels(local) {
4453
+ return settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
4454
+ }
4455
+ function caughtUpRooms(expired) {
4456
+ const progress = underway;
4457
+ underway = null;
4458
+ naming = Math.max(0, naming - 1);
4020
4459
  const cycle = progress ?? { complete: true, reachable: true };
4021
- if (opened === generation)
4022
- opened = null;
4460
+ replaced(expired);
4023
4461
  const reloading = progress === null ? [] : owedReloads(expired);
4024
4462
  const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
4025
4463
  return task(OPENING_LANE, async (live) => {
4026
- underway = null;
4027
4464
  if (!live())
4028
4465
  return;
4029
4466
  if (progress === null) {
@@ -4031,24 +4468,14 @@ export function composeRuntime(options) {
4031
4468
  await rest(cycle, live);
4032
4469
  return;
4033
4470
  }
4034
- await roomsCarried([...expired, ...reloading]);
4035
- if (!live())
4036
- return;
4037
4471
  try {
4038
4472
  const local = await openLocal();
4039
- const reloaded = await replacing;
4473
+ await replacing;
4040
4474
  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
- }
4475
+ await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
4476
+ !expired.includes(room.locator) &&
4477
+ !reloading.includes(room.locator)), 'current', local);
4048
4478
  }
4049
- const failed = reloaded.find((outcome) => outcome.status === 'rejected');
4050
- if (failed !== undefined)
4051
- throw failed.reason;
4052
4479
  }
4053
4480
  catch {
4054
4481
  progress.complete = false;
@@ -4060,11 +4487,8 @@ export function composeRuntime(options) {
4060
4487
  }
4061
4488
  function owedReloads(expired) {
4062
4489
  const reloading = [...discovered.values()]
4063
- .filter((room) => replicas.has(room.locator) &&
4064
- !expired.includes(room.locator) &&
4065
- (owed.has(room.locator) || revoked(room.locator)))
4490
+ .filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
4066
4491
  .map((room) => room.locator);
4067
- owed.clear();
4068
4492
  for (const locator of reloading)
4069
4493
  catching.add(locator);
4070
4494
  liveQueries.recover();
@@ -4073,24 +4497,43 @@ export function composeRuntime(options) {
4073
4497
  async function reloadNow(replaced, progress) {
4074
4498
  const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
4075
4499
  for (const room of reloaded)
4076
- reloadsDue.set(room.locator, generation);
4500
+ reloadsDue.add(room.locator);
4077
4501
  try {
4078
4502
  const local = await openLocal();
4079
- return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
4503
+ for (const room of reloaded) {
4504
+ void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
4505
+ }
4080
4506
  }
4081
4507
  catch {
4082
4508
  for (const room of reloaded)
4083
4509
  reloadsDue.delete(room.locator);
4084
4510
  progress.reachable = false;
4085
- return [];
4086
4511
  }
4087
4512
  }
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' });
4513
+ async function settleModels(held, status, local) {
4514
+ const owed = (room) => {
4515
+ const model = modelOf(room.locator);
4516
+ return model.status !== 'unavailable' && model.status !== status;
4517
+ };
4518
+ const settled = await exclusive(async () => {
4519
+ if (!continuing())
4520
+ return [];
4521
+ const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
4522
+ for (const room of unchanged) {
4523
+ const next = { ...modelOf(room.locator), status };
4524
+ const replica = replicas.get(room.locator);
4525
+ if (replica !== undefined)
4526
+ replicas.set(room.locator, { ...replica, record: { ...replica.record, model: next } });
4527
+ heldModel(room, next);
4093
4528
  }
4529
+ if (unchanged.length > 0)
4530
+ emitRooms();
4531
+ return unchanged;
4532
+ });
4533
+ for (const room of held) {
4534
+ if (settled.includes(room) || !owed(room))
4535
+ continue;
4536
+ await retainModel(local, room, { ...modelOf(room.locator), status });
4094
4537
  }
4095
4538
  }
4096
4539
  function submitLater(locator) {
@@ -4135,14 +4578,10 @@ export function composeRuntime(options) {
4135
4578
  try {
4136
4579
  const local = await openLocal();
4137
4580
  await roomTask(locator, async (live) => {
4138
- const current = () => live() && !signal.aborted;
4139
- if (!current() || authorization === undefined)
4581
+ if (!live() || authorization === undefined)
4140
4582
  return;
4141
- const withheld = await Promise.race([
4142
- advance(room, local, authorization, current),
4143
- abandonedOn(signal),
4144
- ]);
4145
- if (withheld !== null && current())
4583
+ const withheld = await advance(room, local, authorization, live);
4584
+ if (withheld !== null && live())
4146
4585
  await withhold(withheld, room, local, progress);
4147
4586
  });
4148
4587
  }
@@ -4197,14 +4636,11 @@ export function composeRuntime(options) {
4197
4636
  progress.complete = false;
4198
4637
  return;
4199
4638
  }
4200
- reads.applied();
4201
4639
  submitAll();
4202
4640
  for (const locator of stale)
4203
4641
  if (repairable.has(locator))
4204
4642
  repairLater(locator, {});
4205
4643
  await conversations.reconcile();
4206
- if (status.providerUnreachable)
4207
- conversations.reconnected();
4208
4644
  const reading = { complete: true, reachable: true };
4209
4645
  await conversations.catchUp(reading);
4210
4646
  if (progress.reachable && reading.reachable)
@@ -4230,7 +4666,7 @@ export function composeRuntime(options) {
4230
4666
  if (!live())
4231
4667
  return;
4232
4668
  let lastCompleted = status.lastCompleted;
4233
- let synced = complete && stale.size === 0 && !pending();
4669
+ let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
4234
4670
  if (synced && !recorded) {
4235
4671
  const completedAt = new Date().toISOString();
4236
4672
  try {
@@ -4245,74 +4681,78 @@ export function composeRuntime(options) {
4245
4681
  }
4246
4682
  }
4247
4683
  moveTo({
4248
- state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
4684
+ state: pending()
4685
+ ? 'pending'
4686
+ : synced && complete && roomWork === 0 && naming === 0
4687
+ ? 'synced'
4688
+ : 'syncing',
4249
4689
  lastCompleted,
4250
- providerUnreachable: !progress.reachable,
4690
+ providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
4251
4691
  ...attention(),
4252
4692
  });
4253
4693
  }
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));
4694
+ function applyRoom(change) {
4695
+ const { locator } = change;
4696
+ if (stopping)
4697
+ return Promise.resolve();
4698
+ return inRoom(locator, async (live) => {
4699
+ await answering.get(locator)?.answered;
4700
+ const progress = underway ?? { complete: true, reachable: true };
4285
4701
  try {
4286
4702
  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
- });
4703
+ for (;;) {
4704
+ if (!live())
4705
+ return null;
4706
+ const room = discovered.get(locator);
4707
+ const held = replicas.get(locator);
4708
+ if (room === undefined ||
4709
+ held === undefined ||
4710
+ room.replica.admission !== 'admitted' ||
4711
+ held.record.replica.selection !== room.replica.selection) {
4712
+ return null;
4713
+ }
4714
+ const at = held.record.roomVersion;
4715
+ if (options.adapter.reached(locator, at, change.roomVersion))
4716
+ return progress;
4717
+ if (!options.adapter.reached(locator, at, change.from)) {
4718
+ name(locator);
4719
+ return progress;
4720
+ }
4721
+ if (underway === null)
4722
+ moveTo(transferring(status));
4723
+ const writing = () => live() && !full;
4724
+ let outlined = false;
4725
+ const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, async () => {
4726
+ const page = await change.read();
4727
+ const paths = Object.keys(page.outlines ?? {});
4728
+ outlined = paths.length > 0;
4729
+ await local.remember(locator, held.record.key, paths);
4730
+ return page;
4731
+ }));
4732
+ if (refreshed.kind !== 'advanced')
4733
+ return progress;
4734
+ const landing = {
4735
+ kind: 'refreshed',
4736
+ from: refreshed.from,
4737
+ remodelled: refreshed.remodelled,
4738
+ };
4739
+ if (await publish(local, room, refreshed.record, landing, writing)) {
4740
+ stale.delete(locator);
4741
+ await retainModel(local, room, modelOf(locator));
4742
+ if (outlined)
4743
+ void heldAgain(locator);
4744
+ return progress;
4745
+ }
4746
+ }
4294
4747
  }
4295
4748
  catch {
4749
+ if (!stale.has(locator))
4750
+ name(locator);
4296
4751
  stale.add(locator);
4752
+ return progress;
4297
4753
  }
4298
- finally {
4299
- if (during === null)
4300
- await rest(progress, live);
4301
- }
4302
- });
4754
+ }).then((progress) => rest(progress), () => undefined);
4303
4755
  }
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
4756
  function observeStream() {
4317
4757
  const stream = options.adapter.roomStream;
4318
4758
  if (stream === undefined || withdrawStream !== undefined)
@@ -4322,29 +4762,35 @@ export function composeRuntime(options) {
4322
4762
  return;
4323
4763
  if (event.kind === 'opened') {
4324
4764
  socketOpen = true;
4765
+ announced = false;
4766
+ handed = false;
4767
+ naming = 0;
4325
4768
  connectionLive();
4326
4769
  generation += 1;
4327
- reads.opened();
4328
4770
  conversations.reopened();
4329
4771
  recovery.resume();
4330
- void sync('opened');
4772
+ }
4773
+ else if (event.kind === 'discovered') {
4774
+ if (!socketOpen)
4775
+ return;
4776
+ announced = true;
4777
+ if (missedChange) {
4778
+ missedChange = false;
4779
+ countAuthorityChange();
4780
+ }
4781
+ void sync('opened', event.discovery);
4782
+ for (const locator of replicas.keys())
4783
+ void heldAgain(locator);
4331
4784
  }
4332
4785
  else if (event.kind === 'closed')
4333
- closedSocket(event.revoked, event.lost);
4334
- else if (event.kind === 'opening')
4335
- void applyOpening(event.expired);
4786
+ closedSocket(event.revoked, event.lost, event.authorityChanged);
4787
+ else if (event.kind === 'caught-up')
4788
+ void caughtUpRooms(event.expired);
4336
4789
  else if (event.kind === 'authority-changed')
4337
- void rediscover(event.scope);
4790
+ void changeAuthority(event);
4338
4791
  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]]));
4792
+ void applyRoom(event);
4793
+ });
4348
4794
  }
4349
4795
  function writesHeld() {
4350
4796
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -4414,8 +4860,9 @@ export function composeRuntime(options) {
4414
4860
  if (closed)
4415
4861
  return;
4416
4862
  closed = true;
4417
- if (rechecking !== null)
4418
- clearInterval(rechecking);
4863
+ notifyDiscovered();
4864
+ if (probing !== null)
4865
+ clearInterval(probing);
4419
4866
  connectionLive();
4420
4867
  lockListeners.clear();
4421
4868
  readinessWatches.clear();
@@ -4427,11 +4874,13 @@ export function composeRuntime(options) {
4427
4874
  conversations.close();
4428
4875
  void recovery.retire();
4429
4876
  roomListeners.clear();
4877
+ unlistedRooms.clear();
4430
4878
  refusalListeners.clear();
4431
4879
  storageLossListeners.clear();
4432
4880
  statusListeners.clear();
4433
4881
  serviceModeListeners.clear();
4434
4882
  memberListeners.clear();
4883
+ authorityChangeListeners.clear();
4435
4884
  liveQueries.close();
4436
4885
  queueSubscriptions.clear();
4437
4886
  roomDraftListeners.clear();
@@ -4439,6 +4888,7 @@ export function composeRuntime(options) {
4439
4888
  }
4440
4889
  async function stop() {
4441
4890
  stopping = true;
4891
+ inFlight.abort();
4442
4892
  await recovery.retire();
4443
4893
  await Promise.all(working);
4444
4894
  await mutations;
@@ -4452,20 +4902,20 @@ export function composeRuntime(options) {
4452
4902
  observeStream();
4453
4903
  void openLocal().then(() => {
4454
4904
  if (capabilities.length > 0)
4455
- notifyReady();
4905
+ readyFromStore();
4456
4906
  }, () => {
4457
- notifyReady();
4907
+ readyFromStore();
4458
4908
  });
4459
4909
  if (options.adapter.roomStream === undefined)
4460
- void sync('unstreamed');
4910
+ void sync('unstreamed', null);
4461
4911
  function planned(input, reach) {
4462
- const plan = planOf(input, roomFacts(reach), planReach(reach));
4912
+ const plan = planWithin(input, roomFacts(reach), planReach(reach));
4463
4913
  const named = plan.paths !== null &&
4464
4914
  plan.rooms !== null &&
4465
4915
  plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
4466
4916
  return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
4467
4917
  }
4468
- function watchPlan(plan, deliver) {
4918
+ function watchPlan(plan, deliver, fail) {
4469
4919
  let seeded = false;
4470
4920
  let pending = true;
4471
4921
  const shown = () => {
@@ -4491,7 +4941,7 @@ export function composeRuntime(options) {
4491
4941
  }
4492
4942
  seeded = false;
4493
4943
  deliver(message);
4494
- });
4944
+ }, fail);
4495
4945
  return () => {
4496
4946
  watchers.delete(shown);
4497
4947
  if (watchers.size === 0)
@@ -4499,61 +4949,59 @@ export function composeRuntime(options) {
4499
4949
  stop();
4500
4950
  };
4501
4951
  }
4502
- async function searched(input, reach, locked) {
4952
+ async function searched(input, reach) {
4503
4953
  const asked = searchPlanOf(input);
4504
- const filter = {
4505
- ...filterOf(asked.documents ?? {}, roomFacts(reach), planReach(reach)),
4506
- locked,
4507
- };
4508
- const unlocking = (filter.rooms ?? [])
4509
- .filter((room) => locked.includes(room))
4510
- .map((room) => ({ room, awaits: 'unlock' }));
4511
- for (;;) {
4512
- const cover = covered(filter);
4513
- awaitPreparation(cover);
4514
- const pending = [...unlocking, ...('pending' in cover ? cover.pending : [])];
4515
- const complete = pending.length === 0 && !('pending' in cover);
4516
- const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
4517
- const answering = complete
4518
- ? filter
4519
- : { ...filter, rooms: reachedRooms(filter).filter((room) => !awaited.has(room)) };
4520
- const answered = covered(answering);
4521
- if ('pending' in answered)
4522
- continue;
4523
- const listed = conversations.searchable();
4524
- const named = reach !== null && !reach.conversations
4525
- ? []
4526
- : (asked.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
4527
- const hits = await (await openLocal()).search({
4528
- text: asked.text,
4529
- limit: capped(asked.limit),
4530
- after: asked.after,
4531
- total: asked.total,
4532
- documents: {
4533
- plan: answering,
4534
- rooms: answered.rooms,
4535
- beneath: asked.collection
4536
- ? answered.rooms.map(({ locator, key }) => ({
4537
- key,
4538
- roots: replicas.get(locator)?.record.typedSelection?.roots ?? [],
4539
- }))
4540
- : [],
4541
- },
4542
- conversations: named,
4543
- });
4544
- if (JSON.stringify(covered(answering)) !== JSON.stringify(answered))
4545
- continue;
4546
- const coverage = {
4547
- rooms: answered.coverage.rooms,
4548
- conversations: named,
4549
- overlay: answered.coverage.overlay,
4954
+ await ready;
4955
+ const search = options.adapter.search;
4956
+ if (search === undefined) {
4957
+ return {
4958
+ kind: 'undeclared',
4959
+ code: 'action-undeclared',
4960
+ fields: { name: 'search' },
4961
+ retryable: false,
4550
4962
  };
4551
- return complete
4552
- ? { ...hits, coverage }
4553
- : { pending, documents: hits.documents, messages: hits.messages, coverage };
4963
+ }
4964
+ if (closed || refusal !== null || connectionOf() !== 'connected')
4965
+ return OFFLINE;
4966
+ const reached = reach === null ? null : new Set(reach.rooms);
4967
+ const named = asked.rooms === null
4968
+ ? reached === null
4969
+ ? null
4970
+ : [...reached]
4971
+ : asked.rooms.filter((room) => reached === null || reached.has(room));
4972
+ const rooms = named === null ? null : named.filter((room) => replicas.has(room)).sort();
4973
+ if (rooms !== null && rooms.length === 0)
4974
+ return { kind: 'found', documents: [] };
4975
+ try {
4976
+ await ensureAuthorization();
4977
+ }
4978
+ catch {
4979
+ return UNCERTAIN;
4980
+ }
4981
+ if (closed || authorization === undefined || refusal !== null)
4982
+ return UNCERTAIN;
4983
+ journal.record('transfer search');
4984
+ try {
4985
+ const result = await search(authorization, { text: asked.text, rooms });
4986
+ if (result.kind === 'revoked-authorization')
4987
+ await refuse('revoked-authorization');
4988
+ journal.record(`outcome search ${result.kind}`);
4989
+ if (result.kind === 'rejected')
4990
+ return result;
4991
+ if (result.kind !== 'found')
4992
+ return failure(result);
4993
+ const kept = rooms === null ? null : new Set(rooms);
4994
+ return {
4995
+ kind: 'found',
4996
+ documents: result.documents.filter((hit) => kept === null || kept.has(hit.room)),
4997
+ };
4998
+ }
4999
+ catch {
5000
+ journal.record('outcome search transport-uncertainty');
5001
+ return UNCERTAIN;
4554
5002
  }
4555
5003
  }
4556
- const watch = (topic, input, deliver) => {
5004
+ const watch = (topic, input, deliver, fail) => {
4557
5005
  if (topic === 'room-drafts') {
4558
5006
  const listener = { notify: deliver, ready: false };
4559
5007
  roomDraftListeners.add(listener);
@@ -4602,12 +5050,13 @@ export function composeRuntime(options) {
4602
5050
  };
4603
5051
  }
4604
5052
  if (topic === 'readiness') {
4605
- return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
5053
+ return watchReadiness({ target: '', code: 'stored' }, null, deliver);
4606
5054
  }
4607
5055
  if (topic === 'rooms') {
4608
- roomListeners.add(deliver);
4609
- deliver(listed());
5056
+ unlistedRooms.add(deliver);
5057
+ emitKnownRooms();
4610
5058
  return () => {
5059
+ unlistedRooms.delete(deliver);
4611
5060
  roomListeners.delete(deliver);
4612
5061
  };
4613
5062
  }
@@ -4646,6 +5095,13 @@ export function composeRuntime(options) {
4646
5095
  memberListeners.delete(deliver);
4647
5096
  };
4648
5097
  }
5098
+ if (topic === 'authority-changes') {
5099
+ authorityChangeListeners.add(deliver);
5100
+ deliver(authorityChanges);
5101
+ return () => {
5102
+ authorityChangeListeners.delete(deliver);
5103
+ };
5104
+ }
4649
5105
  if (topic === 'queue') {
4650
5106
  const queue = { locator: input[0], notify: deliver };
4651
5107
  queueSubscriptions.add(queue);
@@ -4654,24 +5110,34 @@ export function composeRuntime(options) {
4654
5110
  queueSubscriptions.delete(queue);
4655
5111
  };
4656
5112
  }
4657
- return watchPlan(planned(input[0], null), deliver);
5113
+ return watchPlan(planned(input[0], null), deliver, fail);
4658
5114
  };
4659
5115
  const md = {
4660
5116
  loaded() {
4661
5117
  return openLocal().then(() => undefined);
4662
5118
  },
4663
5119
  rooms() {
4664
- return listed();
5120
+ if (closed)
5121
+ return Promise.resolve([]);
5122
+ return new Promise((settle) => {
5123
+ const answer = (value) => {
5124
+ unlistedRooms.delete(answer);
5125
+ roomListeners.delete(answer);
5126
+ settle(value);
5127
+ };
5128
+ unlistedRooms.add(answer);
5129
+ emitKnownRooms();
5130
+ });
4665
5131
  },
4666
5132
  query: ((input) => rowsQuery(planned(input, null))),
4667
- search: (input) => searched(input, null, []),
5133
+ search: (input) => searched(input, null),
4668
5134
  types(locator) {
4669
5135
  return {
4670
5136
  collection: modelTypes(modelOf(locator)),
4671
5137
  typedSelection: rooms.get(locator)?.typedSelection === undefined ||
4672
5138
  rooms.get(locator)?.typedSelection === null
4673
5139
  ? null
4674
- : modelTypes(modelOf(locator, 'typed-selection')),
5140
+ : modelTypes(builtIn),
4675
5141
  };
4676
5142
  },
4677
5143
  roomDraft: {
@@ -4688,33 +5154,17 @@ export function composeRuntime(options) {
4688
5154
  const record = roomDrafts.get(room)?.get(path);
4689
5155
  return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
4690
5156
  },
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
- });
5157
+ write(room, path, revision, input, next = roomDraftRevision()) {
5158
+ if (typeof next !== 'string' || next === '')
5159
+ return Promise.reject(runtimeError('room-draft-malformed'));
5160
+ return gathered(`room-draft\u0000${room}\u0000${path}`, { revision, input, next }, (earlier, later) => {
5161
+ if (later.revision !== earlier.next)
5162
+ return null;
5163
+ const parts = gatheredParts(earlier.input, later.input);
5164
+ return parts === null
5165
+ ? null
5166
+ : { revision: earlier.revision, input: parts, next: later.next };
5167
+ }, (written) => writtenRoomDraft(room, path, written.revision, written.input, written.next));
4718
5168
  },
4719
5169
  discard(room, path, revision) {
4720
5170
  return exclusive(async () => {
@@ -4741,13 +5191,13 @@ export function composeRuntime(options) {
4741
5191
  });
4742
5192
  },
4743
5193
  write(app, value) {
4744
- return keepPresentation(app, () => structuredClone(value));
5194
+ return exclusive(() => keptPresentation(app, () => structuredClone(value)));
4745
5195
  },
4746
5196
  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
- ]));
5197
+ return gathered(`presentation\u0000${app}`, structuredClone(members), (earlier, later) => ({ ...earlier, ...later }), (edited) => keptPresentation(app, (held) => Object.fromEntries([
5198
+ ...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(edited, member)),
5199
+ ...Object.entries(edited).filter(([, value]) => value !== null),
5200
+ ])));
4751
5201
  },
4752
5202
  },
4753
5203
  save(room, named, writtenBy) {
@@ -4766,7 +5216,8 @@ export function composeRuntime(options) {
4766
5216
  part: readPart,
4767
5217
  read: readBinaryPath,
4768
5218
  link: linkBinaryPath,
4769
- readReference: readBinaryReference,
5219
+ show: showBinaryPath,
5220
+ attach: attachBinary,
4770
5221
  },
4771
5222
  async call(name, input) {
4772
5223
  await ready;
@@ -4789,7 +5240,8 @@ export function composeRuntime(options) {
4789
5240
  journal.record('retry');
4790
5241
  unpreparable.clear();
4791
5242
  queueReadiness();
4792
- resume();
5243
+ if (full)
5244
+ await probed();
4793
5245
  },
4794
5246
  observe: observing(watch),
4795
5247
  async reauthorize() {
@@ -4803,7 +5255,7 @@ export function composeRuntime(options) {
4803
5255
  authorization = undefined;
4804
5256
  discoveryPaused = false;
4805
5257
  if (options.adapter.roomStream === undefined)
4806
- await sync('unstreamed');
5258
+ await sync('unstreamed', null);
4807
5259
  else
4808
5260
  await task(OPENING_LANE, ensureAuthorization);
4809
5261
  },
@@ -4811,7 +5263,6 @@ export function composeRuntime(options) {
4811
5263
  const reachable = {
4812
5264
  md,
4813
5265
  watch,
4814
- referrerScope: (referrer) => options.adapter.referrerScope(referrer),
4815
5266
  plan: planned,
4816
5267
  query: (plan) => rowsQuery(plan),
4817
5268
  watchPlan,
@@ -4821,12 +5272,12 @@ export function composeRuntime(options) {
4821
5272
  lockedRooms,
4822
5273
  watchLocks,
4823
5274
  located,
5275
+ room: (locator) => listed().find((one) => one.locator === locator),
4824
5276
  };
4825
5277
  return {
4826
5278
  md,
4827
5279
  seed,
4828
5280
  stop,
4829
- lose: loseRooms,
4830
5281
  watch,
4831
5282
  persisted(answer) {
4832
5283
  if (answer === persistence)
@@ -4835,6 +5286,10 @@ export function composeRuntime(options) {
4835
5286
  journal.record(`persistence ${answer}`);
4836
5287
  queueReadiness();
4837
5288
  },
4838
- within: (reach, port) => narrowed(reachable, reach, port),
5289
+ within: (reach, port) => {
5290
+ if (full)
5291
+ void probed();
5292
+ return narrowed(reachable, reach, port);
5293
+ },
4839
5294
  };
4840
5295
  }