@agentshouse/mdruntime 0.1.0-cee9c8aa4cd86 → 0.1.0-cef5833f6c689

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