@agentshouse/mdruntime 0.1.0-c1ffd2bc60355 → 0.1.0-c20970f71a6a9

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 (206) hide show
  1. package/README.md +11 -9
  2. package/dist/adapter.d.ts +89 -53
  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/bootstrap.d.ts +10 -13
  8. package/dist/bootstrap.d.ts.map +1 -1
  9. package/dist/bootstrap.js +100 -89
  10. package/dist/client.d.ts +8 -8
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/client.js +52 -6
  13. package/dist/collection.d.ts +5 -3
  14. package/dist/collection.d.ts.map +1 -1
  15. package/dist/collection.js +24 -19
  16. package/dist/compile.d.ts +1 -1
  17. package/dist/compile.d.ts.map +1 -1
  18. package/dist/compile.js +113 -45
  19. package/dist/connection.d.ts +8 -7
  20. package/dist/connection.d.ts.map +1 -1
  21. package/dist/connection.js +1311 -1067
  22. package/dist/conversation.d.ts +15 -14
  23. package/dist/conversation.d.ts.map +1 -1
  24. package/dist/conversation.js +222 -140
  25. package/dist/durability.d.ts +0 -1
  26. package/dist/durability.d.ts.map +1 -1
  27. package/dist/entry.d.ts +1 -0
  28. package/dist/entry.d.ts.map +1 -1
  29. package/dist/entry.js +2 -1
  30. package/dist/file.d.ts +2 -11
  31. package/dist/file.d.ts.map +1 -1
  32. package/dist/file.js +6 -4
  33. package/dist/host.d.ts +3 -6
  34. package/dist/host.d.ts.map +1 -1
  35. package/dist/host.js +12 -19
  36. package/dist/index.d.ts +7 -7
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +2 -1
  39. package/dist/media.d.ts +8 -0
  40. package/dist/media.d.ts.map +1 -0
  41. package/dist/media.js +218 -0
  42. package/dist/memory.d.ts +10 -6
  43. package/dist/memory.d.ts.map +1 -1
  44. package/dist/memory.js +178 -90
  45. package/dist/messages.d.ts +2 -2
  46. package/dist/messages.d.ts.map +1 -1
  47. package/dist/messages.js +15 -16
  48. package/dist/model.d.ts +6 -1
  49. package/dist/model.d.ts.map +1 -1
  50. package/dist/model.js +7 -10
  51. package/dist/observation.d.ts +1 -0
  52. package/dist/observation.d.ts.map +1 -1
  53. package/dist/observation.js +1 -1
  54. package/dist/outcome.d.ts +13 -5
  55. package/dist/outcome.d.ts.map +1 -1
  56. package/dist/outcome.js +16 -2
  57. package/dist/paths.d.ts +1 -0
  58. package/dist/paths.d.ts.map +1 -1
  59. package/dist/paths.js +4 -0
  60. package/dist/phrase.d.ts +3 -0
  61. package/dist/phrase.d.ts.map +1 -0
  62. package/dist/phrase.js +8 -0
  63. package/dist/query.d.ts +10 -0
  64. package/dist/query.d.ts.map +1 -1
  65. package/dist/query.js +20 -1
  66. package/dist/reach.d.ts +10 -3
  67. package/dist/reach.d.ts.map +1 -1
  68. package/dist/reach.js +53 -29
  69. package/dist/react.d.ts +1 -0
  70. package/dist/react.d.ts.map +1 -1
  71. package/dist/readiness.d.ts +3 -17
  72. package/dist/readiness.d.ts.map +1 -1
  73. package/dist/readiness.js +4 -10
  74. package/dist/recovery.d.ts +1 -0
  75. package/dist/recovery.d.ts.map +1 -1
  76. package/dist/recovery.js +26 -0
  77. package/dist/refresh.d.ts +7 -5
  78. package/dist/refresh.d.ts.map +1 -1
  79. package/dist/refresh.js +47 -11
  80. package/dist/replica.d.ts +3 -5
  81. package/dist/replica.d.ts.map +1 -1
  82. package/dist/replica.js +7 -7
  83. package/dist/room-draft.d.ts +18 -3
  84. package/dist/room-draft.d.ts.map +1 -1
  85. package/dist/room-draft.js +94 -47
  86. package/dist/rows.d.ts +17 -7
  87. package/dist/rows.d.ts.map +1 -1
  88. package/dist/rows.js +120 -87
  89. package/dist/save.d.ts +2 -0
  90. package/dist/save.d.ts.map +1 -1
  91. package/dist/save.js +11 -5
  92. package/dist/search.d.ts +12 -63
  93. package/dist/search.d.ts.map +1 -1
  94. package/dist/search.js +9 -52
  95. package/dist/shared.d.ts +0 -1
  96. package/dist/shared.d.ts.map +1 -1
  97. package/dist/shared.js +0 -10
  98. package/dist/status.d.ts +4 -3
  99. package/dist/status.d.ts.map +1 -1
  100. package/dist/status.js +3 -1
  101. package/dist/store.d.ts +30 -37
  102. package/dist/store.d.ts.map +1 -1
  103. package/dist/store.js +61 -66
  104. package/dist/stored.d.ts +41 -24
  105. package/dist/stored.d.ts.map +1 -1
  106. package/dist/stored.js +196 -214
  107. package/dist/tables.d.ts +48 -49
  108. package/dist/tables.d.ts.map +1 -1
  109. package/dist/tables.js +23 -62
  110. package/dist/transfer.d.ts +0 -5
  111. package/dist/transfer.d.ts.map +1 -1
  112. package/dist/validation.d.ts +3 -4
  113. package/dist/validation.d.ts.map +1 -1
  114. package/dist/validation.js +35 -96
  115. package/dist/wire.d.ts +9 -8
  116. package/dist/wire.d.ts.map +1 -1
  117. package/dist/wire.js +3 -2
  118. package/package.json +10 -17
  119. package/AGENTS.md +0 -317
  120. package/dist/chat/acts.d.ts +0 -33
  121. package/dist/chat/acts.d.ts.map +0 -1
  122. package/dist/chat/acts.js +0 -36
  123. package/dist/chat/context.d.ts +0 -3
  124. package/dist/chat/context.d.ts.map +0 -1
  125. package/dist/chat/context.js +0 -10
  126. package/dist/chat/earlier.d.ts +0 -10
  127. package/dist/chat/earlier.d.ts.map +0 -1
  128. package/dist/chat/earlier.js +0 -54
  129. package/dist/chat/house.d.ts +0 -25
  130. package/dist/chat/house.d.ts.map +0 -1
  131. package/dist/chat/house.js +0 -38
  132. package/dist/chat/input.d.ts +0 -11
  133. package/dist/chat/input.d.ts.map +0 -1
  134. package/dist/chat/input.js +0 -92
  135. package/dist/chat/kit.d.ts +0 -61
  136. package/dist/chat/kit.d.ts.map +0 -1
  137. package/dist/chat/kit.js +0 -123
  138. package/dist/chat/lasting.d.ts +0 -6
  139. package/dist/chat/lasting.d.ts.map +0 -1
  140. package/dist/chat/lasting.js +0 -19
  141. package/dist/chat/list.d.ts +0 -28
  142. package/dist/chat/list.d.ts.map +0 -1
  143. package/dist/chat/list.js +0 -103
  144. package/dist/chat/markdown.d.ts +0 -20
  145. package/dist/chat/markdown.d.ts.map +0 -1
  146. package/dist/chat/markdown.js +0 -229
  147. package/dist/chat/message.d.ts +0 -7
  148. package/dist/chat/message.d.ts.map +0 -1
  149. package/dist/chat/message.js +0 -254
  150. package/dist/chat/messages.d.ts +0 -35
  151. package/dist/chat/messages.d.ts.map +0 -1
  152. package/dist/chat/messages.js +0 -210
  153. package/dist/chat/name.d.ts +0 -7
  154. package/dist/chat/name.d.ts.map +0 -1
  155. package/dist/chat/name.js +0 -37
  156. package/dist/chat/opened.d.ts +0 -12
  157. package/dist/chat/opened.d.ts.map +0 -1
  158. package/dist/chat/opened.js +0 -19
  159. package/dist/chat/overlay.d.ts +0 -32
  160. package/dist/chat/overlay.d.ts.map +0 -1
  161. package/dist/chat/overlay.js +0 -103
  162. package/dist/chat/parts.d.ts +0 -58
  163. package/dist/chat/parts.d.ts.map +0 -1
  164. package/dist/chat/parts.js +0 -120
  165. package/dist/chat/pieces.d.ts +0 -62
  166. package/dist/chat/pieces.d.ts.map +0 -1
  167. package/dist/chat/pieces.js +0 -1110
  168. package/dist/chat/process.d.ts +0 -20
  169. package/dist/chat/process.d.ts.map +0 -1
  170. package/dist/chat/process.js +0 -32
  171. package/dist/chat/provider.d.ts +0 -70
  172. package/dist/chat/provider.d.ts.map +0 -1
  173. package/dist/chat/provider.js +0 -635
  174. package/dist/chat/question.d.ts +0 -9
  175. package/dist/chat/question.d.ts.map +0 -1
  176. package/dist/chat/question.js +0 -69
  177. package/dist/chat/questions.d.ts +0 -31
  178. package/dist/chat/questions.d.ts.map +0 -1
  179. package/dist/chat/questions.js +0 -111
  180. package/dist/chat/read.d.ts +0 -5
  181. package/dist/chat/read.d.ts.map +0 -1
  182. package/dist/chat/read.js +0 -13
  183. package/dist/chat/ready.d.ts +0 -37
  184. package/dist/chat/ready.d.ts.map +0 -1
  185. package/dist/chat/ready.js +0 -93
  186. package/dist/chat/room-file.d.ts +0 -8
  187. package/dist/chat/room-file.d.ts.map +0 -1
  188. package/dist/chat/room-file.js +0 -72
  189. package/dist/chat/steady.d.ts +0 -3
  190. package/dist/chat/steady.d.ts.map +0 -1
  191. package/dist/chat/steady.js +0 -26
  192. package/dist/chat/style.d.ts +0 -4
  193. package/dist/chat/style.d.ts.map +0 -1
  194. package/dist/chat/style.js +0 -307
  195. package/dist/chat.d.ts +0 -15
  196. package/dist/chat.d.ts.map +0 -1
  197. package/dist/chat.js +0 -8
  198. package/dist/fulltext.d.ts +0 -27
  199. package/dist/fulltext.d.ts.map +0 -1
  200. package/dist/fulltext.js +0 -368
  201. package/dist/reads.d.ts +0 -11
  202. package/dist/reads.d.ts.map +0 -1
  203. package/dist/reads.js +0 -100
  204. package/dist/words.d.ts +0 -19
  205. package/dist/words.d.ts.map +0 -1
  206. 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 { parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
2
+ import { attachedModel, builtInModel, roomTypesOf } from './attached-model.js';
3
3
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
4
4
  import { observing, whole } from './observation.js';
5
- import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
5
+ import { bodied, identityOf, planOf, plain, queryKey, } 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,18 +12,18 @@ 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, 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, gatheredParts, 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
28
  import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
29
29
  import { readinessOf, } from './readiness.js';
@@ -31,9 +31,9 @@ import { FencedMap } from './fence.js';
31
31
  import { sameBinary } from './overlay.js';
32
32
  import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
33
33
  import { identityTime, roomDraftRevision } from './identity.js';
34
- import { beneath, normalize } from './paths.js';
34
+ import { ancestors, beneath, normalize } from './paths.js';
35
35
  import { openRuntimeStore, } from './store.js';
36
- import { changedCleanly, sameComposition, validatedCollection, } from './validation.js';
36
+ import { validatedNeighbourhood } from './validation.js';
37
37
  import { unconfirmed } from './transfer.js';
38
38
  import { governs, holds, narrowed, planReach, } from './reach.js';
39
39
  function within(root, path) {
@@ -54,46 +54,44 @@ const CONVERSATION_LANE = 'conversations';
54
54
  const ROOM_READS = 256;
55
55
  const ROOM_ATTEMPTS = 16;
56
56
  const ERASE_BATCH = 512;
57
- const REDERIVE_BATCH = 256;
58
- 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;
59
63
  const QUERY_BYTES = 1 << 20;
60
64
  const STAGE_BYTES = 1 << 20;
61
65
  const STAGE_FLOOR = 1 << 16;
62
66
  // A batch aims at this much database work, so a Save or a query waits behind at most one short write.
63
67
  const STAGE_AIM_MS = 50;
64
- const PARTS_HELD = 4;
65
- // Bytes still arriving are reported at most this often, well inside the 2 s progress bound, so readiness is not rebuilt for every chunk the network hands over.
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.
66
72
  const ARRIVAL_REPORT_MS = 250;
67
- const INSTALL_STEPS = [
68
- 'writing',
69
- 'downloading',
70
- 'replaying',
71
- 'listing',
72
- ];
73
- const CLEAN_CHANGES = 2_000;
74
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.
75
74
  const LOCK_GRACE_MS = 120_000;
76
75
  function operationOf(input) {
77
76
  const operation = input?.operation_id;
78
77
  return typeof operation === 'string' ? operation : null;
79
78
  }
80
- function abandonedOn(signal) {
81
- return new Promise((resolve) => {
82
- if (signal.aborted)
83
- resolve(null);
84
- else
85
- 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));
86
86
  });
87
87
  }
88
- function roomLane(locator) {
89
- return `room ${locator}`;
90
- }
88
+ const TEXT_READS = 4;
91
89
  function pendingKey(locator, id) {
92
90
  return `${locator}\u0000${id}`;
93
91
  }
94
92
  export function composeRuntime(options) {
95
93
  const localSaves = options.adapter.localSaves === true;
96
- const roomTypes = roomTypesOf(options.adapter.roomTypes);
94
+ const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
97
95
  let authorization = options.authorization;
98
96
  let opening;
99
97
  const fences = new Map();
@@ -103,44 +101,51 @@ export function composeRuntime(options) {
103
101
  const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
104
102
  let discoveries = 0;
105
103
  const replicas = new FencedMap(isFenced);
104
+ const reopened = new Set();
105
+ const usableWhenHeld = options.usableWhenHeld ?? [];
106
106
  const queuesWithoutReplica = new FencedMap(isFenced);
107
107
  const pendings = new Map();
108
108
  const seeds = new Map();
109
109
  const seedWatchers = new Map();
110
110
  const roomDrafts = new FencedMap(isFenced);
111
+ const attachedFiles = new FencedMap(isFenced);
111
112
  const presentations = new Map();
112
113
  let limits = [];
113
114
  let capabilities = [];
114
115
  const rooms = new FencedMap(isFenced);
115
116
  const counted = new Map();
116
117
  const unreadable = new Set();
117
- const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
118
- const models = new FencedMap(isModelFenced);
119
- const liveModels = new FencedMap(isModelFenced);
118
+ const builtIn = builtInModel(roomTypes);
119
+ const models = new FencedMap(isFenced);
120
+ const liveModels = new FencedMap(isFenced);
120
121
  const invalidModels = new Set();
121
122
  const discovered = new Map();
122
- const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
123
+ let inFlight = new AbortController();
124
+ const refreshed = options.adapter.refresh?.bind(options.adapter);
125
+ const reads = {
126
+ bootstrap: (authorized, request) => readUntil(options.adapter.bootstrap(authorized, request), inFlight.signal),
127
+ ...(refreshed === undefined
128
+ ? {}
129
+ : {
130
+ refresh: (authorized, request) => readUntil(refreshed(authorized, request), inFlight.signal),
131
+ }),
132
+ reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
133
+ };
123
134
  let roomsListed = false;
124
135
  const scopedRefusals = new Map();
125
- const sourceRefusals = new Map();
126
- const declarationRefusals = new Set();
127
- function modelKey(locator, attached) {
128
- return `${attached} ${locator}`;
129
- }
130
- function modelOf(locator, attached = 'collection') {
131
- return models.get(modelKey(locator, attached)) ?? NO_MODEL;
136
+ function modelOf(locator) {
137
+ return models.get(locator) ?? NO_MODEL;
132
138
  }
133
- function savedModel(locator, attached) {
134
- return invalidModels.has(modelKey(locator, attached))
139
+ function savedModel(locator) {
140
+ return invalidModels.has(locator)
135
141
  ? { status: 'unavailable', reason: 'revoked' }
136
- : modelOf(locator, attached);
142
+ : modelOf(locator);
143
+ }
144
+ function composed(record) {
145
+ return compositionOf(record, builtIn);
137
146
  }
138
147
  function compositionFor(record) {
139
- return compositionOf({
140
- ...record,
141
- model: savedModel(record.locator, 'collection'),
142
- selectionModel: savedModel(record.locator, 'typed-selection'),
143
- });
148
+ return composed({ ...record, model: savedModel(record.locator) });
144
149
  }
145
150
  let refusal = null;
146
151
  let storageLoss = null;
@@ -150,10 +155,10 @@ export function composeRuntime(options) {
150
155
  let closed = false;
151
156
  let stopping = false;
152
157
  let withdrawStream;
153
- const announced = new Map();
154
158
  let underway = null;
155
159
  let streamed = null;
156
160
  let handed = false;
161
+ let naming = 0;
157
162
  const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
158
163
  const repairing = new Set();
159
164
  const calling = new Set();
@@ -164,16 +169,18 @@ export function composeRuntime(options) {
164
169
  let recorded = false;
165
170
  const stale = new Set();
166
171
  let generation = 0;
167
- let discovering = null;
168
- let opened = null;
169
- const owed = new Set();
170
- const reloadsDue = new Map();
172
+ const reloadsDue = new Set();
173
+ const highest = new Map();
174
+ const beyondHorizon = new Set();
171
175
  const lanes = new Map();
172
- let roomReads = { generation, free: ROOM_READS, waiting: [] };
176
+ const roomLanes = new Map();
177
+ let roomWork = 0;
178
+ const roomReads = { free: ROOM_READS, waiting: [] };
173
179
  const working = new Set();
174
180
  let mutations = Promise.resolve();
175
181
  let gathering = null;
176
182
  const roomListeners = new Set();
183
+ const unlistedRooms = new Set();
177
184
  const refusalListeners = new Set();
178
185
  const storageLossListeners = new Set();
179
186
  const statusListeners = new Set();
@@ -184,6 +191,8 @@ export function composeRuntime(options) {
184
191
  let withdrawConversations;
185
192
  let currencyQueued = null;
186
193
  let socketOpen = false;
194
+ let announced = false;
195
+ let missedChange = false;
187
196
  const confirmed = new Set();
188
197
  let lockGrace = null;
189
198
  const lockListeners = new Set();
@@ -193,6 +202,8 @@ export function composeRuntime(options) {
193
202
  const serviceModeListeners = new Set();
194
203
  let member = null;
195
204
  const memberListeners = new Set();
205
+ let authorityChanges = 0;
206
+ const authorityChangeListeners = new Set();
196
207
  const unpreparedDemand = new Set();
197
208
  const unpreparable = new Set();
198
209
  let preparing = false;
@@ -217,31 +228,32 @@ export function composeRuntime(options) {
217
228
  let readinessQueued = false;
218
229
  const catching = new Set();
219
230
  const transfers = new FencedMap(isFenced);
220
- const installs = new Map();
231
+ const installs = new Set();
221
232
  const arrivals = new Map();
233
+ const streaming = new Set();
234
+ const underived = new Map();
235
+ const unindexed = new Map();
236
+ const startedOver = new Map();
222
237
  let arrivalReported = 0;
223
- let onScreen = options.shell.onScreen;
224
- const shownWaiters = [];
225
- let partsFree = PARTS_HELD;
226
- const partWaiters = [];
227
- let stageBytes = STAGE_FLOOR * 4;
228
- let installWrites = Promise.resolve();
238
+ let streamsFree = STREAMS_HELD;
239
+ let queuedText = 0;
240
+ const installWaiters = [];
241
+ const stageBudget = {
242
+ bytes: STAGE_FLOOR * 4,
243
+ floor: STAGE_FLOOR,
244
+ ceiling: STAGE_BYTES,
245
+ aim: STAGE_AIM_MS,
246
+ };
247
+ const deriveBudget = {
248
+ bytes: DERIVE_FLOOR * 4,
249
+ floor: DERIVE_FLOOR,
250
+ ceiling: DERIVE_BYTES,
251
+ aim: DERIVE_AIM_MS,
252
+ };
253
+ const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
229
254
  const conversations = createConversationStore({
230
- agents: declaredAgents,
231
- documents: (declaration, after) => rowsQuery(planOf({
232
- rooms: [declaration.locator],
233
- types: [declaration.type],
234
- select: {
235
- fields: [
236
- declaration.identity,
237
- declaration.name,
238
- ...(declaration.retired === undefined ? [] : [declaration.retired]),
239
- ].map((field) => `/${escapePointer(field)}`),
240
- },
241
- limit: MAXIMUM_WINDOW,
242
- ...(after === undefined ? {} : { after }),
243
- }, roomFacts())),
244
255
  call: (name, input) => invokeAction(name, input),
256
+ history: (request) => readConversationHistory(request),
245
257
  durably,
246
258
  stream: () => options.adapter.conversations,
247
259
  publish: publishChangeSet,
@@ -253,10 +265,21 @@ export function composeRuntime(options) {
253
265
  });
254
266
  const journal = createLog();
255
267
  const keptFiles = openedFiles(database, isFenced);
256
- let notifyReady = () => { };
268
+ const holding = new Map();
269
+ const unholdable = new Map();
270
+ let readyFromStore = () => { };
257
271
  const ready = new Promise((resolve) => {
258
- notifyReady = resolve;
272
+ readyFromStore = resolve;
273
+ });
274
+ let notifyDiscovered = () => { };
275
+ const firstDiscovery = new Promise((resolve) => {
276
+ notifyDiscovered = resolve;
259
277
  });
278
+ let roomsNamed = false;
279
+ function notifyReady() {
280
+ notifyDiscovered();
281
+ readyFromStore();
282
+ }
260
283
  function rememberRoomDraft(record) {
261
284
  const room = roomDrafts.get(record.locator) ?? new Map();
262
285
  room.set(record.path, record);
@@ -276,7 +299,7 @@ export function composeRuntime(options) {
276
299
  return undefined;
277
300
  const local = await openLocal();
278
301
  const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
279
- return { ...slice, collection: collectionOf(slice.documents, compositionOf(held.record)) };
302
+ return { ...slice, collection: collectionOf(slice.documents, composed(held.record)) };
280
303
  }
281
304
  function roomDraftOf(record, visible) {
282
305
  return structuredClone({
@@ -333,9 +356,9 @@ export function composeRuntime(options) {
333
356
  return value === undefined ? null : structuredClone(value);
334
357
  }
335
358
  async function writtenRoomDraft(room, path, revision, input, next) {
336
- const parts = readParts(input);
359
+ const parts = readParts(input, path);
337
360
  if (path === '' || normalize(path) !== path)
338
- throw runtimeError('room-draft-malformed');
361
+ throw runtimeError('room-draft-malformed', `room-draft-malformed at ${path}: a path inside the Room, such as notes/a.md`);
339
362
  const local = await openLocal();
340
363
  const current = roomDrafts.get(room)?.get(path);
341
364
  if ((current?.revision ?? null) !== revision) {
@@ -343,7 +366,9 @@ export function composeRuntime(options) {
343
366
  }
344
367
  const file = parts.file === undefined
345
368
  ? undefined
346
- : await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
369
+ : await heldFile(parts.file.blob, parts.file.base === undefined
370
+ ? binaryBase(await visibleOf(room, [path]), path)
371
+ : parts.file.base);
347
372
  const record = {
348
373
  locator: room,
349
374
  path,
@@ -378,6 +403,20 @@ export function composeRuntime(options) {
378
403
  (queuesWithoutReplica.size > 0 ||
379
404
  [...replicas.values()].some((replica) => replica.queue.length > 0)));
380
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
+ }
381
420
  function attention() {
382
421
  const attentionRequired = [];
383
422
  for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
@@ -389,16 +428,9 @@ export function composeRuntime(options) {
389
428
  returned: structuredClone(record.returned),
390
429
  });
391
430
  }
392
- for (const [room, refusal] of sourceRefusals)
393
- attentionRequired.push({ room, refusal: structuredClone(refusal) });
394
431
  attentionRequired.sort((left, right) => {
395
432
  if (left.room !== right.room)
396
433
  return left.room < right.room ? -1 : 1;
397
- if ('refusal' in left || 'refusal' in right) {
398
- if ('refusal' in left && 'refusal' in right)
399
- return 0;
400
- return 'refusal' in left ? -1 : 1;
401
- }
402
434
  if (left.path !== right.path)
403
435
  return left.path < right.path ? -1 : 1;
404
436
  return 0;
@@ -410,7 +442,7 @@ export function composeRuntime(options) {
410
442
  if (reset === null || submission.reset < reset)
411
443
  reset = submission.reset;
412
444
  }
413
- return { attentionRequired, reset };
445
+ return { attentionRequired, unsent: unsent(), reset };
414
446
  }
415
447
  function documentsIn(locator) {
416
448
  return replicas.get(locator)?.count ?? 0;
@@ -483,7 +515,7 @@ export function composeRuntime(options) {
483
515
  const governed = governs(reach, room);
484
516
  const attached = [
485
517
  ...(governed.collection ? [modelOf(room.locator)] : []),
486
- ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
518
+ ...(governed.selection ? [builtIn] : []),
487
519
  ];
488
520
  return {
489
521
  locator: room.locator,
@@ -499,11 +531,26 @@ export function composeRuntime(options) {
499
531
  const value = listed();
500
532
  for (const listener of roomListeners)
501
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
+ }
502
548
  }
503
549
  function emitRefusal() {
504
550
  queueReadiness();
505
551
  for (const listener of refusalListeners)
506
552
  listener(refusal);
553
+ emitKnownRooms();
507
554
  }
508
555
  function emitStorageLoss() {
509
556
  queueReadiness();
@@ -524,6 +571,7 @@ export function composeRuntime(options) {
524
571
  if (closed)
525
572
  return;
526
573
  queueReadiness();
574
+ emitKnownRooms();
527
575
  const next = status.state === 'synced' && !conversations.current()
528
576
  ? { ...status, state: 'syncing' }
529
577
  : status;
@@ -537,9 +585,6 @@ export function composeRuntime(options) {
537
585
  if (localSaves)
538
586
  return Object.freeze({ batch, state: 'local' });
539
587
  const state = pendings.get(pendingKey(locator, batch.id));
540
- if (state?.accepted !== undefined) {
541
- return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
542
- }
543
588
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
544
589
  }
545
590
  function entriesOf(locator) {
@@ -567,11 +612,14 @@ export function composeRuntime(options) {
567
612
  if (held === undefined || closed || held.indexed)
568
613
  return;
569
614
  const key = held.record.key;
570
- const done = await exclusive(async () => (await openLocal()).rederive(key, REDERIVE_BATCH));
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
+ });
571
619
  const now = replicas.get(locator);
572
620
  if (now?.record.key !== key)
573
621
  continue;
574
- if (!done)
622
+ if (!done.done)
575
623
  continue;
576
624
  replicas.set(locator, { ...now, indexed: true });
577
625
  publishChangeSet(changeSet(locator, { documents: null }));
@@ -649,7 +697,7 @@ export function composeRuntime(options) {
649
697
  coverage.push({
650
698
  room: locator,
651
699
  generation: replica.record.transfer,
652
- current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
700
+ current: !(catching.has(locator) || transfers.has(locator)),
653
701
  });
654
702
  read.push({ locator, key: replica.record.key });
655
703
  for (const batch of replica.queue)
@@ -676,8 +724,6 @@ export function composeRuntime(options) {
676
724
  return { listed: named.every((room) => replicas.has(room)), rooms: named };
677
725
  }
678
726
  function roomState(locator) {
679
- if (sourceRefusals.has(locator))
680
- return 'refused';
681
727
  const listedRoom = discovered.get(locator);
682
728
  if (roomsListed &&
683
729
  (listedRoom === undefined ||
@@ -692,7 +738,7 @@ export function composeRuntime(options) {
692
738
  }
693
739
  function conversationCoverage(reach) {
694
740
  const declared = reach === null || reach.conversations;
695
- if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
741
+ if (!declared || options.adapter.conversations === undefined)
696
742
  return 'undeclared';
697
743
  if (!conversations.known() || !conversations.current())
698
744
  return 'acquiring';
@@ -707,71 +753,50 @@ export function composeRuntime(options) {
707
753
  ? 'connected'
708
754
  : 'connecting';
709
755
  }
710
- function refusedIn(locators) {
711
- return locators.flatMap((room) => {
712
- const held = sourceRefusals.get(room);
713
- if (held === undefined)
714
- return [];
715
- const { file, location } = held.fields;
716
- return [
717
- {
718
- room,
719
- name: discovered.get(room)?.name ?? room,
720
- file: typeof file === 'string' ? file : '',
721
- rule: held.code,
722
- location: typeof location === 'string' ? location : null,
723
- },
724
- ];
725
- });
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 };
726
767
  }
727
768
  function installationOf(coverage) {
728
769
  const rooms = { done: 0, total: 0 };
729
- const parts = { done: 0, total: 0 };
730
770
  const bytes = { done: 0, total: 0 };
731
- const received = { done: 0, total: 0 };
732
- const steps = new Set();
733
- let waiting = false;
771
+ let installing = 0;
772
+ let downloading = false;
734
773
  for (const { room, state } of coverage) {
735
774
  if (state === 'unadmitted')
736
775
  continue;
737
776
  rooms.total += 1;
738
777
  if (state === 'complete')
739
778
  rooms.done += 1;
740
- const transfer = transfers.get(room);
741
- const held = installs.get(room);
742
- const inventory = transfer?.inventory ?? replicas.get(room)?.record.inventory ?? null;
743
- if (inventory !== null) {
744
- const done = transfer?.done ?? inventory;
745
- const share = held?.share ?? null;
746
- parts.total += inventory.parts;
747
- bytes.total += inventory.bytes;
748
- parts.done += done.parts;
749
- bytes.done += done.bytes;
750
- if (share !== null && share.entries > 0) {
751
- bytes.done += Math.floor((share.bytes * share.written) / share.entries);
752
- }
753
- received.total += inventory.bytes;
754
- received.done += done.bytes + (share?.bytes ?? 0);
755
- for (const arrived of arrivals.get(room)?.values() ?? [])
756
- received.done += arrived;
757
- }
758
- const installing = held !== undefined && state !== 'complete';
759
- if (installing)
760
- steps.add(held.step);
761
- waiting ||= installing || state === 'acquiring';
762
- }
763
- return {
764
- step: INSTALL_STEPS.find((step) => steps.has(step)) ?? null,
765
- paused: waiting && !onScreen ? 'hidden' : null,
766
- rooms,
767
- parts,
768
- bytes,
769
- received,
770
- };
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 };
771
797
  }
772
798
  function readinessFor(port, reach) {
773
799
  const required = requiredRooms(reach);
774
- const search = port.search === 'required';
775
800
  const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
776
801
  const preparing = coverage
777
802
  .filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
@@ -784,16 +809,15 @@ export function composeRuntime(options) {
784
809
  listed: required.listed,
785
810
  rooms: coverage,
786
811
  conversations: conversationCoverage(reach),
787
- search: search ? 'required' : 'unused',
788
812
  },
789
813
  persistence,
790
814
  connection: connectionOf(),
791
815
  house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
792
816
  space: full ? 'full' : port.space,
793
817
  installation: installationOf(coverage),
794
- refused: refusedIn(required.rooms),
795
818
  lost: storageLoss !== null,
796
- 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))),
797
821
  });
798
822
  }
799
823
  function establish(name) {
@@ -846,7 +870,8 @@ export function composeRuntime(options) {
846
870
  probeEachMinute();
847
871
  }
848
872
  function installedTarget() {
849
- return Object.values(targets).some((target) => target.established);
873
+ return ((usableWhenHeld.length > 0 && reopened.size > 0) ||
874
+ Object.values(targets).some((target) => target.established));
850
875
  }
851
876
  function probeEachMinute() {
852
877
  if (!full || probing !== null || closed || !installedTarget())
@@ -877,8 +902,12 @@ export function composeRuntime(options) {
877
902
  unpreparable.clear();
878
903
  journal.record('space freed');
879
904
  queueReadiness();
880
- if (!closed && authorization !== undefined)
881
- void sync('unstreamed');
905
+ if (closed || authorization === undefined)
906
+ return;
907
+ if (options.adapter.roomStream === undefined)
908
+ void sync('unstreamed', null);
909
+ else
910
+ void resynchronize();
882
911
  }
883
912
  function seedRoomHeld(room, version) {
884
913
  return indexed(room) && reachedBy(room, version);
@@ -918,7 +947,24 @@ export function composeRuntime(options) {
918
947
  function sameCover(left, right) {
919
948
  return JSON.stringify(left) === JSON.stringify(right);
920
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
+ }
921
966
  async function coveredRows(plan, cover) {
967
+ const asked = new Set();
922
968
  for (;;) {
923
969
  const before = cover();
924
970
  if ('pending' in before)
@@ -929,8 +975,14 @@ export function composeRuntime(options) {
929
975
  const current = () => local.changes(keys) === changes;
930
976
  const rows = await windowRows(local, plan, before.rooms, current);
931
977
  const after = cover();
932
- 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)
933
982
  return { rows, cover: before, current };
983
+ for (const one of unheld)
984
+ asked.add(textKey(one));
985
+ await holdTexts(unheld);
934
986
  }
935
987
  }
936
988
  async function windowRows(local, plan, rooms, unchanged) {
@@ -1004,6 +1056,10 @@ export function composeRuntime(options) {
1004
1056
  }
1005
1057
  function publishChangeSet(set) {
1006
1058
  applyChangeSet(set);
1059
+ for (const { locator, path } of set.documents) {
1060
+ if (path !== null)
1061
+ attachedFiles.get(locator)?.delete(path);
1062
+ }
1007
1063
  if (set.rooms.length > 0 ||
1008
1064
  [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
1009
1065
  emitRooms();
@@ -1022,6 +1078,8 @@ export function composeRuntime(options) {
1022
1078
  }
1023
1079
  async function refuse(reason, provider = null) {
1024
1080
  discoveryPaused = true;
1081
+ inFlight.abort();
1082
+ inFlight = new AbortController();
1025
1083
  try {
1026
1084
  if (reason === 'revoked-authorization') {
1027
1085
  await exclusive(async () => {
@@ -1029,7 +1087,7 @@ export function composeRuntime(options) {
1029
1087
  const records = [...replicas].map(([locator, held]) => ({
1030
1088
  locator,
1031
1089
  held,
1032
- record: { ...held.record, model: revoked, selectionModel: revoked },
1090
+ record: { ...held.record, model: revoked },
1033
1091
  }));
1034
1092
  let answer = null;
1035
1093
  try {
@@ -1039,18 +1097,14 @@ export function composeRuntime(options) {
1039
1097
  locator,
1040
1098
  key: record.key,
1041
1099
  record: storedRecord(record),
1042
- composition: compositionOf(record),
1100
+ composed: composedOf(composed(record)),
1043
1101
  })),
1044
1102
  });
1045
1103
  }
1046
1104
  catch {
1047
1105
  journal.record('model revocation persistence failed');
1048
1106
  }
1049
- const keys = new Set(models.keys());
1050
- for (const locator of rooms.keys()) {
1051
- keys.add(modelKey(locator, 'collection'));
1052
- keys.add(modelKey(locator, 'typed-selection'));
1053
- }
1107
+ const keys = new Set([...models.keys(), ...rooms.keys()]);
1054
1108
  for (const key of keys) {
1055
1109
  invalidModels.add(key);
1056
1110
  models.set(key, revoked);
@@ -1105,15 +1159,15 @@ export function composeRuntime(options) {
1105
1159
  ? null
1106
1160
  : {
1107
1161
  roots: selection.roots,
1108
- model: modelTypes(modelOf(room.locator, 'typed-selection')),
1162
+ model: modelTypes(builtIn),
1109
1163
  },
1110
1164
  providerOwnedPaths: room.providerOwnedPaths,
1111
1165
  roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
1112
1166
  synchronizationAvailable,
1113
- refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
1167
+ refusal: scopedRefusals.get(room.locator) ?? null,
1114
1168
  });
1115
1169
  }
1116
- async function applyDiscovery(found, started, covered) {
1170
+ async function applyDiscovery(found, started, left) {
1117
1171
  const lockedBefore = lockedRooms();
1118
1172
  for (const room of found) {
1119
1173
  const fence = fences.get(room.locator);
@@ -1127,18 +1181,11 @@ export function composeRuntime(options) {
1127
1181
  const listedLocators = new Set(listed.map((room) => room.locator));
1128
1182
  for (const locator of listedLocators)
1129
1183
  scopedRefusals.delete(locator);
1130
- if (covered === undefined)
1184
+ if (left === undefined)
1131
1185
  discovered.clear();
1132
- else
1133
- for (const locator of covered)
1134
- discovered.delete(locator);
1135
1186
  for (const room of listed)
1136
1187
  discovered.set(room.locator, room);
1137
- const unlisted = unlistedHeld(new Set(found.map((room) => room.locator))).filter((locator) => covered === undefined || covered.includes(locator));
1138
- if (options.adapter.exhaustiveDiscovery === true)
1139
- await loseRooms(unlisted);
1140
- else
1141
- unlistedKept(unlisted);
1188
+ await loseRooms(left ?? unlistedHeld(new Set(found.map((room) => room.locator))));
1142
1189
  for (const room of listed) {
1143
1190
  const held = replicas.get(room.locator)?.record;
1144
1191
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -1169,15 +1216,6 @@ export function composeRuntime(options) {
1169
1216
  ]),
1170
1217
  ].filter((locator) => !listed.has(locator));
1171
1218
  }
1172
- function unlistedKept(unlisted) {
1173
- for (const locator of unlisted) {
1174
- const held = replicas.get(locator)?.record;
1175
- if (held !== undefined)
1176
- list(held, false);
1177
- else
1178
- rooms.delete(locator);
1179
- }
1180
- }
1181
1219
  function fence(locators) {
1182
1220
  const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1183
1221
  const removed = fresh.map((locator) => ({
@@ -1191,26 +1229,24 @@ export function composeRuntime(options) {
1191
1229
  for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1192
1230
  pendings.delete(pendingKey(locator, batch.id));
1193
1231
  replicas.delete(locator);
1232
+ highest.delete(locator);
1233
+ beyondHorizon.delete(locator);
1194
1234
  queuesWithoutReplica.delete(locator);
1195
1235
  rooms.delete(locator);
1196
1236
  roomDrafts.delete(locator);
1197
- transfers.delete(locator);
1237
+ attachedFiles.delete(locator);
1238
+ untransferred(locator);
1239
+ startedOver.delete(locator);
1198
1240
  catching.delete(locator);
1199
1241
  discovered.delete(locator);
1200
- announced.delete(locator);
1201
1242
  unreadable.delete(locator);
1202
1243
  stale.delete(locator);
1203
1244
  repairable.delete(locator);
1204
1245
  unpreparable.delete(locator);
1205
1246
  scopedRefusals.delete(locator);
1206
- sourceRefusals.delete(locator);
1207
- declarationRefusals.delete(locator);
1208
- for (const attached of ['collection', 'typed-selection']) {
1209
- const key = modelKey(locator, attached);
1210
- models.delete(key);
1211
- liveModels.delete(key);
1212
- invalidModels.delete(key);
1213
- }
1247
+ models.delete(locator);
1248
+ liveModels.delete(locator);
1249
+ invalidModels.delete(locator);
1214
1250
  journal.record(`room lost ${locator}`);
1215
1251
  }
1216
1252
  if (fresh.length === 0)
@@ -1258,7 +1294,14 @@ export function composeRuntime(options) {
1258
1294
  });
1259
1295
  await eraseFenced(fresh);
1260
1296
  }
1261
- function restored(key, persisted) {
1297
+ function retyped(persisted) {
1298
+ if (persisted.status === 'unavailable')
1299
+ return persisted;
1300
+ const { state } = attachedModel({ kind: 'model', snapshot: persisted.snapshot }, NO_MODEL, roomTypes);
1301
+ return state.status === 'unavailable' ? state : { ...persisted, types: state.types };
1302
+ }
1303
+ function restored(key, held) {
1304
+ const persisted = retyped(held);
1262
1305
  const prior = models.get(key);
1263
1306
  if (prior !== undefined &&
1264
1307
  prior.status !== 'unavailable' &&
@@ -1289,8 +1332,9 @@ export function composeRuntime(options) {
1289
1332
  const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1290
1333
  targets = held.targets;
1291
1334
  transfers.clear();
1292
- for (const [locator, record] of held.transfers)
1293
- transfers.set(locator, record);
1335
+ arrivals.clear();
1336
+ streaming.clear();
1337
+ startedOver.clear();
1294
1338
  limits = held.providerLimits;
1295
1339
  capabilities = held.capabilities;
1296
1340
  member = held.member;
@@ -1300,7 +1344,7 @@ export function composeRuntime(options) {
1300
1344
  presentations.clear();
1301
1345
  for (const record of held.presentation)
1302
1346
  presentations.set(record.app, record.value);
1303
- conversations.hydrate(heldConversations, chatDrafts);
1347
+ conversations.hydrate(heldConversations, chatDrafts, held.conversationList, held.conversationCursor);
1304
1348
  pendings.clear();
1305
1349
  for (const queue of queues) {
1306
1350
  for (const { batch, state } of queue.pending) {
@@ -1308,6 +1352,7 @@ export function composeRuntime(options) {
1308
1352
  }
1309
1353
  }
1310
1354
  replicas.clear();
1355
+ reopened.clear();
1311
1356
  queuesWithoutReplica.clear();
1312
1357
  for (const [locator, queue] of queuedBatches) {
1313
1358
  if (!held.rooms.has(locator))
@@ -1316,9 +1361,8 @@ export function composeRuntime(options) {
1316
1361
  records.forEach((stored, index) => {
1317
1362
  if (fences.has(stored.locator))
1318
1363
  return;
1319
- const model = restored(modelKey(stored.locator, 'collection'), stored.model);
1320
- const selectionModel = restored(modelKey(stored.locator, 'typed-selection'), stored.selectionModel);
1321
- const record = { ...stored, model, selectionModel };
1364
+ const record = { ...stored, model: restored(stored.locator, stored.model) };
1365
+ reopened.add(record.locator);
1322
1366
  replicas.set(record.locator, {
1323
1367
  record,
1324
1368
  queue: queuedBatches.get(record.locator) ?? [],
@@ -1329,25 +1373,28 @@ export function composeRuntime(options) {
1329
1373
  });
1330
1374
  const existing = rooms.get(record.locator);
1331
1375
  if (existing === undefined)
1332
- list(record, false);
1376
+ list(record, record.roomVersion !== null);
1333
1377
  else {
1334
1378
  rooms.set(record.locator, {
1335
1379
  ...existing,
1336
1380
  roomVersion: record.roomVersion,
1337
1381
  operations: record.operations,
1338
1382
  replica: record.replica,
1339
- model: modelTypes(model),
1383
+ model: modelTypes(record.model),
1340
1384
  typedSelection: record.typedSelection === null
1341
1385
  ? null
1342
- : {
1343
- roots: record.typedSelection.roots,
1344
- model: modelTypes(selectionModel),
1345
- },
1386
+ : { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
1346
1387
  });
1347
1388
  }
1348
1389
  });
1349
- for (const key of held.orphans)
1350
- await opened.release(key);
1390
+ try {
1391
+ for (const key of held.orphans)
1392
+ await opened.release(key);
1393
+ }
1394
+ catch (error) {
1395
+ if (!refusedForSpace(error))
1396
+ throw error;
1397
+ }
1351
1398
  if (held.erasing || held.orphans.length > 0)
1352
1399
  queueMicrotask(sweep);
1353
1400
  return {
@@ -1449,6 +1496,16 @@ export function composeRuntime(options) {
1449
1496
  }
1450
1497
  const REWRITE_LINKS = ['field', 'body'];
1451
1498
  const CHECKED_LINKS = ['field', 'section'];
1499
+ const BODY_LINKS = ['body'];
1500
+ async function textsNeeded(local, record, named) {
1501
+ const paths = [...new Set(named.map((one) => (typeof one === 'string' ? one : one.path)))];
1502
+ const unheld = await unheldIn(local, record.locator, record.key, paths);
1503
+ if (unheld.length === 0)
1504
+ return null;
1505
+ await holdTexts(unheld);
1506
+ const [first] = await unheldIn(local, record.locator, record.key, unheld.map(({ path }) => path));
1507
+ return first?.path ?? null;
1508
+ }
1452
1509
  async function sliceAt(record, view, queue, extra = [], moved = []) {
1453
1510
  const local = await openLocal();
1454
1511
  const needed = new Set([...touchedBy(queue), ...extra]);
@@ -1529,7 +1586,7 @@ export function composeRuntime(options) {
1529
1586
  return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
1530
1587
  }
1531
1588
  function replayed(record, confirmedLayer, queue) {
1532
- return stepped(replaying(confirmedLayer, queue, record.locator, compositionOf(record)));
1589
+ return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
1533
1590
  }
1534
1591
  async function overlayDiff(record, before, visible, touched) {
1535
1592
  const kept = before !== undefined && before.record.key === record.key
@@ -1549,8 +1606,6 @@ export function composeRuntime(options) {
1549
1606
  function unpairedIn(locator, batch) {
1550
1607
  if (!queueIn(locator).some((held) => held.id === batch.id))
1551
1608
  return false;
1552
- if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
1553
- return false;
1554
1609
  return !leavingIn(batch, locator) || withdrawable(locator)(batch);
1555
1610
  }
1556
1611
  async function together(local, settlements) {
@@ -1592,7 +1647,7 @@ export function composeRuntime(options) {
1592
1647
  const confirmedLayer = overrides === undefined
1593
1648
  ? read
1594
1649
  : await stepped(slicingWith(read, overrides.set, overrides.removed));
1595
- const composition = compositionOf(record);
1650
+ const composition = composed(record);
1596
1651
  const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
1597
1652
  const founded = kept.filter((batch) => !queue.includes(batch));
1598
1653
  const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
@@ -1635,6 +1690,7 @@ export function composeRuntime(options) {
1635
1690
  key: record.key,
1636
1691
  record: storedRecord(record),
1637
1692
  roots: record.typedSelection?.roots ?? [],
1693
+ composed: composedOf(composition),
1638
1694
  ...settling.commit,
1639
1695
  ...(overrides === undefined
1640
1696
  ? {}
@@ -1698,69 +1754,64 @@ export function composeRuntime(options) {
1698
1754
  };
1699
1755
  }
1700
1756
  function adoptModels(locator, record) {
1701
- for (const [attached, model] of [
1702
- ['collection', record.model],
1703
- ['typed-selection', record.selectionModel],
1704
- ]) {
1705
- const key = modelKey(locator, attached);
1706
- models.set(key, model);
1707
- invalidModels.delete(key);
1708
- if (model.status === 'current')
1709
- liveModels.set(key, JSON.stringify(model.snapshot));
1710
- else if (model.status === 'unavailable')
1711
- liveModels.delete(key);
1712
- }
1757
+ const model = record.model;
1758
+ models.set(locator, model);
1759
+ invalidModels.delete(locator);
1760
+ if (model.status === 'current')
1761
+ liveModels.set(locator, JSON.stringify(model.snapshot));
1762
+ else if (model.status === 'unavailable')
1763
+ liveModels.delete(locator);
1713
1764
  }
1714
1765
  function remodel(locator, answers, held) {
1715
- const taken = (answer, previous, attached) => {
1716
- if (answer === undefined)
1717
- return previous;
1718
- const { state, refused } = attachedModel(answer, previous, attached === 'collection' ? roomTypes : NO_ROOM_TYPES);
1719
- if (refused !== null) {
1720
- const where = refused.location === undefined ? '' : ` ${refused.location}`;
1721
- journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
1722
- }
1723
- models.set(modelKey(locator, attached), state);
1724
- return state;
1725
- };
1726
- return {
1727
- collection: taken(answers?.collection, held.collection, 'collection'),
1728
- selection: taken(answers?.selection, held.selection, 'typed-selection'),
1729
- };
1766
+ const answer = answers?.collection;
1767
+ if (answer === undefined)
1768
+ return held;
1769
+ const { state, refused } = attachedModel(answer, held.collection, roomTypes);
1770
+ if (refused !== null) {
1771
+ const where = refused.location === undefined ? '' : ` ${refused.location}`;
1772
+ journal.record(`model invalid ${locator} collection ${refused.identity} ${refused.rule}${where}`);
1773
+ }
1774
+ models.set(locator, state);
1775
+ return { collection: state };
1730
1776
  }
1731
- function publish(local, room, record, landing, live) {
1777
+ function publish(local, room, reached, landing, live) {
1732
1778
  const landed = exclusive(async () => {
1733
1779
  if (!live()) {
1734
1780
  caughtUpTo(room.locator);
1735
1781
  return true;
1736
1782
  }
1737
1783
  const prior = replicas.get(room.locator);
1738
- if (landing.kind === 'refreshed' &&
1739
- (prior === undefined || prior.record.key !== record.key || prior.confirmed !== landing.base)) {
1740
- await local.unrefresh(record.key);
1784
+ if (!stands(room.locator, landing.from)) {
1785
+ if (landing.kind === 'refreshed') {
1786
+ await local.unrefresh(reached.key);
1787
+ return false;
1788
+ }
1789
+ caughtUpTo(room.locator);
1790
+ await abandoned(local, room.locator);
1741
1791
  return false;
1742
1792
  }
1793
+ const record = landing.kind === 'refreshed' && prior !== undefined
1794
+ ? {
1795
+ ...prior.record,
1796
+ roomVersion: reached.roomVersion,
1797
+ ...(landing.remodelled ? { model: reached.model } : {}),
1798
+ }
1799
+ : reached;
1743
1800
  adoptModels(room.locator, record);
1744
1801
  const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
1745
- const roomVersion = record.roomVersion;
1746
- const accepted = queue.filter((batch) => {
1747
- const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
1748
- return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
1749
- });
1750
1802
  let set;
1751
1803
  try {
1752
- set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
1804
+ set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
1753
1805
  ? {
1754
1806
  room: true,
1755
1807
  view: 'refreshed',
1756
- commit: { refreshed: true, composition: compositionOf(record) },
1808
+ commit: { refreshed: true },
1757
1809
  }
1758
1810
  : {
1759
1811
  room: true,
1760
1812
  view: landing.refreshed ? 'refreshed' : 'confirmed',
1761
1813
  commit: {
1762
1814
  published: { from: prior?.record.key ?? null },
1763
- composition: compositionOf(record),
1764
1815
  ...(landing.refreshed ? { refreshed: true } : {}),
1765
1816
  },
1766
1817
  });
@@ -1772,24 +1823,26 @@ export function composeRuntime(options) {
1772
1823
  return true;
1773
1824
  }
1774
1825
  if (landing.kind === 'bootstrapped') {
1775
- transfers.delete(room.locator);
1826
+ highest.delete(room.locator);
1827
+ beyondHorizon.delete(room.locator);
1828
+ untransferred(room.locator);
1829
+ startedOver.delete(room.locator);
1776
1830
  catching.delete(room.locator);
1777
- sweep();
1831
+ if (prior !== undefined && prior.record.key !== record.key)
1832
+ sweep();
1833
+ name(room.locator);
1778
1834
  }
1779
- for (const batch of accepted)
1780
- pendings.delete(pendingKey(room.locator, batch.id));
1781
1835
  stale.delete(room.locator);
1782
1836
  unpreparable.delete(room.locator);
1783
1837
  if (unreadable.delete(room.locator))
1784
1838
  scopedRefusals.delete(room.locator);
1785
- sourceRefusals.delete(room.locator);
1786
- declarationRefusals.delete(room.locator);
1787
1839
  list(room, true);
1788
1840
  publishChangeSet(set);
1841
+ if (landing.kind === 'bootstrapped')
1842
+ void heldAgain(room.locator);
1789
1843
  await unpair(local);
1790
1844
  moveTo({ ...status, ...attention() });
1791
- options.adapter.roomStream?.published(room.locator, roomVersion);
1792
- if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1845
+ if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1793
1846
  submitLater(room.locator);
1794
1847
  }
1795
1848
  return true;
@@ -1801,7 +1854,7 @@ export function composeRuntime(options) {
1801
1854
  const replica = replicas.get(locator);
1802
1855
  const room = rooms.get(locator);
1803
1856
  if (replica === undefined || room === undefined) {
1804
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1857
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
1805
1858
  }
1806
1859
  return { local, replica, room };
1807
1860
  }
@@ -1819,12 +1872,7 @@ export function composeRuntime(options) {
1819
1872
  return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
1820
1873
  }
1821
1874
  async function saveParts(locator, named, writtenBy) {
1822
- const read = named.map(({ path, parts }) => ({ path, parts: readParts(parts) }));
1823
- if (read.some(({ path }, at) => path === '' ||
1824
- normalize(path) !== path ||
1825
- read.findIndex((one) => one.path === path) !== at)) {
1826
- throw runtimeError('room-draft-malformed');
1827
- }
1875
+ const read = readSavedParts(named);
1828
1876
  const target = await savedRoom(locator);
1829
1877
  const saved = [];
1830
1878
  const files = [];
@@ -1833,7 +1881,7 @@ export function composeRuntime(options) {
1833
1881
  for (const { path, parts } of read) {
1834
1882
  const file = parts.file === undefined
1835
1883
  ? undefined
1836
- : await heldFile(parts.file.blob, binaryBase(visible, path));
1884
+ : await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
1837
1885
  if (parts.file !== undefined && file !== undefined) {
1838
1886
  files.push({ key: file.localId, blob: parts.file.blob });
1839
1887
  }
@@ -1868,13 +1916,27 @@ export function composeRuntime(options) {
1868
1916
  const referring = removed.length === 0
1869
1917
  ? []
1870
1918
  : (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
1871
- const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
1919
+ const unheld = await textsNeeded(local, record, [
1920
+ ...paths,
1921
+ ...referring,
1922
+ ...(moved.length === 0
1923
+ ? []
1924
+ : await local.referrers(record.key, 'confirmed', moved, REWRITE_LINKS)),
1925
+ ...(removed.length === 0
1926
+ ? []
1927
+ : await local.referrers(record.key, 'confirmed', removed, BODY_LINKS)),
1928
+ ]);
1929
+ if (unheld !== null) {
1930
+ return { refusal: { code: 'transport-uncertainty', retryable: true, file: unheld } };
1931
+ }
1932
+ const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
1872
1933
  const { visible } = await replayed(record, confirmedLayer, replica.queue);
1873
- const composition = compositionOf(record);
1934
+ const composition = composed(record);
1874
1935
  return {
1875
1936
  record,
1876
1937
  confirmed: confirmedLayer,
1877
1938
  visible,
1939
+ folders: new Set(await local.folders(record.key, 'visible', paths)),
1878
1940
  *visibleOver(queue) {
1879
1941
  return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
1880
1942
  },
@@ -1886,13 +1948,7 @@ export function composeRuntime(options) {
1886
1948
  return false;
1887
1949
  const local = await openLocal();
1888
1950
  const roots = record.typedSelection?.roots ?? [];
1889
- if (composition.model.status === 'unavailable' &&
1890
- (await local.typedWithin(record.key, 'visible', roots, false))) {
1891
- return true;
1892
- }
1893
- return (composition.selection !== null &&
1894
- composition.selection.model.status === 'unavailable' &&
1895
- (await local.typedWithin(record.key, 'visible', roots, true)));
1951
+ return local.typedWithin(record.key, 'visible', roots, false);
1896
1952
  }
1897
1953
  function textDiffers(left, right) {
1898
1954
  return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
@@ -1903,18 +1959,6 @@ export function composeRuntime(options) {
1903
1959
  return undefined;
1904
1960
  return entry.binary !== null ? null : entry.content;
1905
1961
  }
1906
- function walkOf(local, key, view) {
1907
- return async (after) => {
1908
- const page = await local.walk(key, view, after, WALK_BYTES);
1909
- return {
1910
- entries: page.entries.map(({ path, content, binary }) => ({
1911
- path,
1912
- content: binary !== null ? null : content,
1913
- })),
1914
- next: page.next,
1915
- };
1916
- };
1917
- }
1918
1962
  function lookupOf(local, key, view) {
1919
1963
  return async (paths) => {
1920
1964
  if (paths.length === 0)
@@ -1931,37 +1975,6 @@ export function composeRuntime(options) {
1931
1975
  const read = parseFrontmatter(entry.content ?? '');
1932
1976
  return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
1933
1977
  }
1934
- function overriddenWalk(walk, overrides) {
1935
- const sorted = [...overrides.keys()].sort();
1936
- const beyond = (path) => {
1937
- if (path === null)
1938
- return 0;
1939
- let low = 0;
1940
- let high = sorted.length;
1941
- while (low < high) {
1942
- const middle = (low + high) >>> 1;
1943
- if (sorted[middle] <= path)
1944
- low = middle + 1;
1945
- else
1946
- high = middle;
1947
- }
1948
- return low;
1949
- };
1950
- return async (after) => {
1951
- const page = await walk(after);
1952
- const end = page.next;
1953
- const added = sorted
1954
- .slice(beyond(after), end === null ? sorted.length : beyond(end))
1955
- .flatMap((path) => {
1956
- const entry = overrides.get(path);
1957
- return entry === null || entry === undefined
1958
- ? []
1959
- : [{ path, content: entry.binary !== null ? null : entry.content }];
1960
- });
1961
- const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
1962
- return { entries, next: end };
1963
- };
1964
- }
1965
1978
  function overriddenLookup(lookup, overrides) {
1966
1979
  return async (paths) => {
1967
1980
  const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
@@ -1979,9 +1992,8 @@ export function composeRuntime(options) {
1979
1992
  }
1980
1993
  function validatorOf(record, confirmedLayer) {
1981
1994
  return async (resulting, touched) => {
1982
- const composition = compositionFor(record);
1983
1995
  const local = await openLocal();
1984
- const changed = [];
1996
+ const changes = [];
1985
1997
  const overrides = new Map();
1986
1998
  const pace = paced();
1987
1999
  for (const path of new Set([...confirmedLayer.held, ...touched])) {
@@ -1992,19 +2004,13 @@ export function composeRuntime(options) {
1992
2004
  continue;
1993
2005
  overrides.set(path, now);
1994
2006
  if (textOf(was) !== textOf(now))
1995
- changed.push({ path, was: textOf(was), now: textOf(now) });
2007
+ changes.push({ path, was: textOf(was), now: textOf(now) });
1996
2008
  }
1997
- const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
1998
- const clean = available(composition) &&
1999
- changed.length <= CLEAN_CHANGES &&
2000
- (await changedCleanly(changed, compositionOf(record), composition, {
2001
- lookup,
2002
- texts: textsOf(local, record.key, 'confirmed'),
2003
- referrers: referrersOf(local, record.key, 'confirmed'),
2004
- }));
2005
- if (clean)
2006
- return { verdict: 'accept' };
2007
- return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
2009
+ return validatedNeighbourhood(changes, compositionFor(record), {
2010
+ lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
2011
+ texts: textsOf(local, record.key, 'confirmed'),
2012
+ referrers: referrersOf(local, record.key, 'confirmed'),
2013
+ });
2008
2014
  };
2009
2015
  }
2010
2016
  function textsOf(local, key, view) {
@@ -2028,6 +2034,7 @@ export function composeRuntime(options) {
2028
2034
  operations: room.operations,
2029
2035
  providerOwnedPaths: room.providerOwnedPaths,
2030
2036
  visible: saving.visible,
2037
+ folders: saving.folders,
2031
2038
  queued: queuedDocuments(replica.queue),
2032
2039
  unmodeled: await unmodeledIn(record),
2033
2040
  validate: validatorOf(record, saving.confirmed),
@@ -2048,6 +2055,7 @@ export function composeRuntime(options) {
2048
2055
  locator,
2049
2056
  key: saving.record.key,
2050
2057
  roots: saving.record.typedSelection?.roots ?? [],
2058
+ composed: composedOf(composed(saving.record)),
2051
2059
  overlay: diff.overlay,
2052
2060
  });
2053
2061
  }
@@ -2079,15 +2087,19 @@ export function composeRuntime(options) {
2079
2087
  }
2080
2088
  const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
2081
2089
  const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
2082
- const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, sourceSaving.visible)));
2090
+ if ('refusal' in sourceSaving)
2091
+ return { outcome: 'refused', ...sourceSaving.refusal };
2092
+ const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
2083
2093
  if (changed.length > 0)
2084
2094
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2085
2095
  const target = replicas.get(destination);
2086
2096
  const listedRoom = rooms.get(destination);
2087
2097
  if (target === undefined || listedRoom === undefined) {
2088
- throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
2098
+ throw runtimeError('unknown-room', `Unknown Room ${destination}`);
2089
2099
  }
2090
2100
  const arriving = await savingIn(target, [], moves.map(({ to }) => to));
2101
+ if ('refusal' in arriving)
2102
+ return { outcome: 'refused', ...arriving.refusal };
2091
2103
  const built = await buildMove({
2092
2104
  source: await sideOf(locator, replica, room, sourceSaving),
2093
2105
  destination: await sideOf(destination, target, listedRoom, arriving),
@@ -2148,10 +2160,10 @@ export function composeRuntime(options) {
2148
2160
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2149
2161
  const { answer, touched } = committedBoth;
2150
2162
  if (answer.fenced !== null) {
2151
- throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2163
+ throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2152
2164
  }
2153
2165
  if (fences.has(locator) || fences.has(destination)) {
2154
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2166
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2155
2167
  }
2156
2168
  pendings.set(pendingKey(locator, id), {
2157
2169
  drafts: saved,
@@ -2204,7 +2216,9 @@ export function composeRuntime(options) {
2204
2216
  ...saved.map(({ path }) => path),
2205
2217
  ...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
2206
2218
  ]);
2207
- const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, underneath(held, path, saving.visible)));
2219
+ if ('refusal' in saving)
2220
+ return { outcome: 'refused', ...saving.refusal };
2221
+ const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
2208
2222
  if (changed.length > 0)
2209
2223
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2210
2224
  const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
@@ -2232,6 +2246,7 @@ export function composeRuntime(options) {
2232
2246
  providerLimits: limits,
2233
2247
  handle: room.handle,
2234
2248
  visible: saving.visible,
2249
+ folders: saving.folders,
2235
2250
  changes,
2236
2251
  bases: parts.flatMap(({ bases }) => bases),
2237
2252
  queued: queuedDocuments(replica.queue),
@@ -2321,10 +2336,10 @@ export function composeRuntime(options) {
2321
2336
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2322
2337
  const { answer, touched } = done;
2323
2338
  if (answer.fenced !== null) {
2324
- throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2339
+ throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2325
2340
  }
2326
2341
  if (fences.has(locator))
2327
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2342
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2328
2343
  for (const origin of withdrawn.keys())
2329
2344
  pendings.delete(pendingKey(locator, origin));
2330
2345
  for (const [id, drafts] of drafted) {
@@ -2373,7 +2388,7 @@ export function composeRuntime(options) {
2373
2388
  throw new Error('connection refused');
2374
2389
  await local.keepUpload(locator, identifier, upload);
2375
2390
  if (fences.has(locator))
2376
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2391
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2377
2392
  const key = pendingKey(locator, identifier);
2378
2393
  pendings.set(key, { ...pendings.get(key), upload });
2379
2394
  if (!continuing())
@@ -2382,6 +2397,9 @@ export function composeRuntime(options) {
2382
2397
  async function readHistory(locator, path, query = {}) {
2383
2398
  if (closed || refusal !== null)
2384
2399
  return UNCERTAIN;
2400
+ await firstDiscovery;
2401
+ if (!roomsNamed)
2402
+ return UNCERTAIN;
2385
2403
  try {
2386
2404
  await ensureAuthorization();
2387
2405
  }
@@ -2474,6 +2492,9 @@ export function composeRuntime(options) {
2474
2492
  async function transferBinary(request, transfer, answer) {
2475
2493
  if (closed || refusal !== null)
2476
2494
  return UNCERTAIN;
2495
+ await firstDiscovery;
2496
+ if (!roomsNamed)
2497
+ return UNCERTAIN;
2477
2498
  try {
2478
2499
  await ensureAuthorization();
2479
2500
  }
@@ -2504,23 +2525,104 @@ export function composeRuntime(options) {
2504
2525
  return UNCERTAIN;
2505
2526
  }
2506
2527
  }
2528
+ async function heldAgain(locator) {
2529
+ const replica = replicas.get(locator);
2530
+ if (replica === undefined || !textOnline())
2531
+ return;
2532
+ try {
2533
+ const wanted = await (await openLocal()).wanted(locator, replica.record.key);
2534
+ await holdTexts(wanted.map(({ path, revision }) => ({ locator, path, revision })));
2535
+ }
2536
+ catch {
2537
+ journal.record(`hold text ${locator} unwritten`);
2538
+ }
2539
+ }
2540
+ function textOnline() {
2541
+ return !closed && refusal === null && connectionOf() === 'connected';
2542
+ }
2543
+ function textKey({ locator, path }) {
2544
+ return documentKey(locator, path);
2545
+ }
2546
+ async function readText({ locator, path, revision }) {
2547
+ const replica = replicas.get(locator);
2548
+ if (replica === undefined || !textOnline())
2549
+ return;
2550
+ try {
2551
+ await ensureAuthorization();
2552
+ }
2553
+ catch {
2554
+ return;
2555
+ }
2556
+ if (closed || authorization === undefined || refusal !== null)
2557
+ return;
2558
+ const since = fencings;
2559
+ journal.record(`hold text ${locator} ${path}`);
2560
+ const read = await reads
2561
+ .bootstrap(authorization, { locator, selection: replica.record.replica.selection, path })
2562
+ .catch(() => null);
2563
+ journal.record(`outcome text ${read?.kind ?? 'transport-uncertainty'}`);
2564
+ if (read === null || read.kind !== 'bootstrap' || fencedSince(locator, since))
2565
+ return;
2566
+ const content = read.documents[path];
2567
+ const now = replicas.get(locator);
2568
+ if (content === undefined || read.revisions[path] !== revision || now === undefined) {
2569
+ unholdable.set(textKey({ locator, path, revision }), revision);
2570
+ return;
2571
+ }
2572
+ const held = await (await openLocal()).hold(locator, now.record.key, compositionFor(now.record), [{ path, revision, content }]);
2573
+ if (held.length === 0)
2574
+ unholdable.set(textKey({ locator, path, revision }), revision);
2575
+ else
2576
+ publishChangeSet(changeSet(locator, { documents: held }));
2577
+ }
2578
+ function holdText(unheld) {
2579
+ const key = textKey(unheld);
2580
+ const running = holding.get(key);
2581
+ if (running !== undefined)
2582
+ return running;
2583
+ const started = readText(unheld).finally(() => holding.delete(key));
2584
+ holding.set(key, started);
2585
+ return started;
2586
+ }
2587
+ async function holdTexts(wanted) {
2588
+ const queue = wanted.filter((one) => unholdable.get(textKey(one)) !== one.revision);
2589
+ await Promise.all(Array.from({ length: Math.min(TEXT_READS, queue.length) }, async () => {
2590
+ for (let next = queue.shift(); next !== undefined; next = queue.shift()) {
2591
+ await holdText(next);
2592
+ }
2593
+ }));
2594
+ }
2595
+ async function unheldIn(local, locator, key, paths) {
2596
+ if (paths.length === 0)
2597
+ return [];
2598
+ return (await local.entries(key, 'confirmed', paths)).flatMap((entry) => entry.content === null &&
2599
+ entry.binary === null &&
2600
+ entry.outline !== null &&
2601
+ entry.revision !== null
2602
+ ? [{ locator, path: entry.path, revision: entry.revision }]
2603
+ : []);
2604
+ }
2605
+ function fetchBinary(request, name) {
2606
+ return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2607
+ }
2507
2608
  async function readBinary(request, name) {
2508
2609
  if (closed)
2509
2610
  return UNCERTAIN;
2510
2611
  const held = await keptFiles.find(name);
2511
2612
  if (held !== null)
2512
2613
  return held;
2513
- return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2614
+ return fetchBinary(request, name);
2514
2615
  }
2515
2616
  async function binaryPath(locator, path, revision) {
2516
2617
  if (revision !== undefined)
2517
2618
  return { kind: 'path', locator, path, revision };
2518
2619
  const replica = replicas.get(locator);
2519
- if (replica === undefined)
2520
- return null;
2521
2620
  const local = await openLocal();
2522
- const [held] = await local.entries(replica.record.key, 'visible', [path]);
2523
- if (held?.binary === undefined || held.binary === null)
2621
+ const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
2622
+ const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
2623
+ if (attached !== undefined)
2624
+ return { kind: 'path', locator, path, revision: attached.revision };
2625
+ if (replica === undefined || held?.binary === undefined || held.binary === null)
2524
2626
  return null;
2525
2627
  if (held.binary.state === 'pending')
2526
2628
  return { kind: 'pending', localId: held.binary.localId };
@@ -2552,47 +2654,99 @@ export function composeRuntime(options) {
2552
2654
  revision: record.revision,
2553
2655
  };
2554
2656
  }
2555
- async function readBinaryPath(locator, path, revision) {
2657
+ async function askedBinary(locator, path, revision) {
2556
2658
  await ready;
2557
2659
  const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2558
- const asked = drafted !== undefined && drafted.localId === revision
2660
+ return drafted !== undefined && drafted.localId === revision
2559
2661
  ? { kind: 'pending', localId: revision }
2560
- : await binaryPath(locator, path, revision);
2662
+ : binaryPath(locator, path, revision);
2663
+ }
2664
+ function pendingBinary(locator, path, localId) {
2665
+ const facts = queuedFacts(locator, path, localId);
2666
+ return facts === null
2667
+ ? absentBinary(path)
2668
+ : {
2669
+ kind: 'binary',
2670
+ file: localId,
2671
+ byteLength: facts.byteLength,
2672
+ mediaType: facts.mediaType,
2673
+ };
2674
+ }
2675
+ async function readBinaryPath(locator, path, revision) {
2676
+ const asked = await askedBinary(locator, path, revision);
2561
2677
  if (asked === null)
2562
2678
  return absentBinary(path);
2563
- if (asked.kind === 'pending') {
2564
- const facts = queuedFacts(locator, path, asked.localId);
2565
- return facts === null
2566
- ? absentBinary(path)
2567
- : {
2568
- kind: 'binary',
2569
- file: asked.localId,
2570
- byteLength: facts.byteLength,
2571
- mediaType: facts.mediaType,
2572
- };
2573
- }
2679
+ if (asked.kind === 'pending')
2680
+ return pendingBinary(locator, path, asked.localId);
2574
2681
  return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
2575
2682
  }
2683
+ async function heldMediaType(locator, path) {
2684
+ const replica = replicas.get(locator);
2685
+ const [held] = replica === undefined
2686
+ ? []
2687
+ : await (await openLocal()).entries(replica.record.key, 'visible', [path]);
2688
+ return held === undefined
2689
+ ? attachedFiles.get(locator)?.get(path)?.mediaType
2690
+ : held.binary?.mediaType;
2691
+ }
2692
+ function linkOf(asked, held) {
2693
+ return transferBinary(asked, (authorization) => options.adapter.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
2694
+ }
2576
2695
  async function linkBinaryPath(locator, path, revision) {
2577
2696
  await ready;
2578
2697
  const asked = await binaryPath(locator, path, revision);
2579
2698
  if (asked === null || asked.kind === 'pending')
2580
2699
  return absentBinary(path);
2581
- return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
2700
+ return linkOf(asked, revision === undefined ? await heldMediaType(locator, path) : undefined);
2701
+ }
2702
+ async function showBinaryPath(locator, path, revision) {
2703
+ const asked = await askedBinary(locator, path, revision);
2704
+ if (asked === null)
2705
+ return absentBinary(path);
2706
+ if (asked.kind === 'pending')
2707
+ return pendingBinary(locator, path, asked.localId);
2708
+ if (closed)
2709
+ return UNCERTAIN;
2710
+ const name = { locator, path: asked.path, revision: asked.revision };
2711
+ const kept = await keptFiles.find(name);
2712
+ if (kept !== null)
2713
+ return kept;
2714
+ const mediaType = await heldMediaType(locator, path);
2715
+ if (mediaType?.startsWith('image/') === true)
2716
+ return fetchBinary(asked, name);
2717
+ return linkOf(asked, revision === undefined ? mediaType : undefined);
2718
+ }
2719
+ async function attachBinary(locator, file) {
2720
+ await ready;
2721
+ await firstDiscovery;
2722
+ if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
2723
+ return OFFLINE;
2724
+ const request = await attachedFile(locator, file);
2725
+ return transferBinary({ locator, path: file.name }, async (held) => {
2726
+ const result = await options.adapter.attach(held, request);
2727
+ if (result.kind === 'attached') {
2728
+ const landed = attachedFiles.get(locator) ?? new Map();
2729
+ landed.set(result.path, { revision: result.revision, mediaType: request.mediaType });
2730
+ attachedFiles.set(locator, landed);
2731
+ }
2732
+ return result;
2733
+ }, attachAnswer);
2582
2734
  }
2583
2735
  function readPart(file, part) {
2584
2736
  return database.ask({ kind: 'file.part', key: file, part });
2585
2737
  }
2586
2738
  async function cleanUp() {
2587
2739
  const freed = await database.ask({ kind: 'opened.clean' });
2588
- return freed + ((await options.shell.cleanUp?.()) ?? 0);
2589
- }
2590
- function declaredAgents() {
2591
- for (const capability of capabilities) {
2592
- if (capability.conversationAgents !== undefined)
2593
- return capability.conversationAgents;
2740
+ const local = await openLocal();
2741
+ let texts = 0;
2742
+ for (const [locator, replica] of replicas) {
2743
+ const released = await local.unhold(locator, replica.record.key, compositionFor(replica.record));
2744
+ texts += released.bytes;
2745
+ if (released.paths.length > 0) {
2746
+ publishChangeSet(changeSet(locator, { documents: released.paths }));
2747
+ }
2594
2748
  }
2595
- return undefined;
2749
+ return freed + texts + ((await options.shell.cleanUp?.()) ?? 0);
2596
2750
  }
2597
2751
  function declaredAction(name) {
2598
2752
  for (const capability of capabilities) {
@@ -2645,6 +2799,24 @@ export function composeRuntime(options) {
2645
2799
  return UNCERTAIN;
2646
2800
  }
2647
2801
  }
2802
+ async function readConversationHistory(request) {
2803
+ await ready;
2804
+ const stream = options.adapter.conversations;
2805
+ if (closed || refusal !== null || stream?.history === undefined)
2806
+ return UNCERTAIN;
2807
+ journal.record('transfer conversation history');
2808
+ try {
2809
+ const result = await stream.history(request);
2810
+ if (result.kind === 'revoked-authorization')
2811
+ await refuse('revoked-authorization');
2812
+ journal.record(`outcome conversation history ${result.kind}`);
2813
+ return calledResult(result);
2814
+ }
2815
+ catch {
2816
+ journal.record('outcome conversation history transport-uncertainty');
2817
+ return UNCERTAIN;
2818
+ }
2819
+ }
2648
2820
  function unsettled(result) {
2649
2821
  return result.kind !== 'action' && result.retryable;
2650
2822
  }
@@ -2706,6 +2878,33 @@ export function composeRuntime(options) {
2706
2878
  const held = replicas.get(locator);
2707
2879
  return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
2708
2880
  }
2881
+ function shownAt(locator) {
2882
+ const held = replicas.get(locator)?.record.roomVersion;
2883
+ const raised = highest.get(locator);
2884
+ if (held === undefined || raised === undefined)
2885
+ return held ?? raised;
2886
+ return options.adapter.reached(locator, held, raised) ? held : raised;
2887
+ }
2888
+ function showing(locator, roomVersion) {
2889
+ const at = shownAt(locator);
2890
+ if (at === undefined || !options.adapter.reached(locator, at, roomVersion)) {
2891
+ highest.set(locator, roomVersion);
2892
+ }
2893
+ }
2894
+ function older(locator, roomVersion) {
2895
+ if (beyondHorizon.has(locator))
2896
+ return false;
2897
+ const at = shownAt(locator);
2898
+ return (at !== undefined &&
2899
+ options.adapter.reached(locator, at, roomVersion) &&
2900
+ !options.adapter.reached(locator, roomVersion, at));
2901
+ }
2902
+ function replaced(locators) {
2903
+ for (const locator of locators) {
2904
+ highest.delete(locator);
2905
+ beyondHorizon.add(locator);
2906
+ }
2907
+ }
2709
2908
  async function writtenAfter(locator, roomVersion, path) {
2710
2909
  const held = replicas.get(locator);
2711
2910
  if (held === undefined)
@@ -2716,122 +2915,34 @@ export function composeRuntime(options) {
2716
2915
  options.adapter.reached(locator, written, roomVersion) &&
2717
2916
  !options.adapter.reached(locator, roomVersion, written));
2718
2917
  }
2719
- function overriddenTexts(texts, overrides) {
2720
- return async (paths) => {
2721
- const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
2722
- for (const path of paths) {
2723
- const entry = overrides.get(path);
2724
- if (entry === undefined)
2725
- continue;
2726
- if (entry === null)
2727
- known.delete(path);
2728
- else
2729
- known.set(path, entry.binary !== null ? null : entry.content);
2730
- }
2731
- return known;
2732
- };
2733
- }
2734
- async function unreadableRoom(texts, walk, paths) {
2735
- if (paths !== null) {
2736
- const text = {};
2737
- for (const [path, content] of await texts(paths))
2738
- if (content !== null)
2739
- text[path] = content;
2740
- return stepped(unreadableIn(text, Object.keys(text)));
2741
- }
2742
- for (let after = null;;) {
2743
- const page = await walk(after);
2744
- const text = {};
2745
- for (const { path, content } of page.entries)
2746
- if (content !== null)
2747
- text[path] = content;
2748
- const refused = await stepped(unreadableIn(text, Object.keys(text)));
2749
- if (refused !== null)
2750
- return refused;
2751
- if (page.next === null)
2752
- return null;
2753
- after = page.next;
2754
- }
2755
- }
2756
- async function validatedRoom(local, record, view, previous, overrides = null) {
2757
- const composition = compositionOf(record);
2758
- const modeled = record.model.status !== 'unavailable' || record.selectionModel.status !== 'unavailable';
2759
- const lookup = overrides === null
2760
- ? lookupOf(local, record.key, view)
2761
- : overriddenLookup(lookupOf(local, record.key, view), overrides);
2762
- const texts = overrides === null
2763
- ? textsOf(local, record.key, view)
2764
- : overriddenTexts(textsOf(local, record.key, view), overrides);
2765
- const walk = overrides === null
2766
- ? walkOf(local, record.key, view)
2767
- : overriddenWalk(walkOf(local, record.key, view), overrides);
2768
- let changes = null;
2769
- let reshaped = false;
2770
- if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
2771
- const [was, now] = await Promise.all([
2772
- local.entries(record.key, 'confirmed', previous.changed),
2773
- overrides === null
2774
- ? local.entries(record.key, view, previous.changed)
2775
- : Promise.resolve([]),
2776
- ]);
2777
- const before = new Map(was.map((entry) => [entry.path, entry]));
2778
- const after = overrides === null
2779
- ? new Map(now.map((entry) => [entry.path, entry]))
2780
- : overrides;
2781
- changes = previous.changed.flatMap((path) => {
2782
- const one = before.get(path) ?? null;
2783
- const other = after.get(path) ?? null;
2784
- reshaped ||= textDiffers(one, other);
2785
- const held = textOf(one);
2786
- const next = textOf(other);
2787
- return held === next ? [] : [{ path, was: held, now: next }];
2788
- });
2789
- }
2790
- const named = changes === null ? null : changes.map(({ path }) => path);
2791
- const textChanged = changes === null || changes.length > 0 || reshaped;
2792
- const modelsChanged = previous === null || !sameComposition(compositionOf(previous.record), composition);
2793
- if (modeled && (textChanged || modelsChanged)) {
2794
- const clean = previous !== null &&
2795
- changes !== null &&
2796
- (await changedCleanly(changes, compositionOf(previous.record), composition, {
2797
- lookup,
2798
- texts,
2799
- referrers: referrersOf(local, record.key, 'confirmed'),
2800
- }));
2801
- if (clean)
2802
- return null;
2803
- const verdict = await validatedCollection(walk, lookup, composition);
2804
- if (verdict.verdict === 'accept')
2805
- return null;
2806
- if (verdict.rule !== 'model-unavailable')
2807
- return { kind: 'refused', refusal: verdict };
2808
- return unreadableRoom(texts, walk, named);
2809
- }
2810
- if (textChanged)
2811
- return unreadableRoom(texts, walk, named);
2812
- return null;
2918
+ async function writtenLater(local, locator, key, paths, roomVersion) {
2919
+ if (paths.length === 0 || !older(locator, roomVersion))
2920
+ return new Set();
2921
+ const entries = await local.entries(key, 'confirmed', paths);
2922
+ return new Set(entries.flatMap(({ path, written }) => written !== null &&
2923
+ options.adapter.reached(locator, written, roomVersion) &&
2924
+ !options.adapter.reached(locator, roomVersion, written)
2925
+ ? [path]
2926
+ : []));
2813
2927
  }
2814
- function refreshingOf(local, key, liveness, replaying = null) {
2815
- const write = replaying === null ? exclusive : installWrite;
2928
+ function refreshingOf(local, locator, key, composition, liveness, replaying = false) {
2929
+ const write = replaying ? finishingWrite : exclusive;
2816
2930
  return {
2817
- async turn() {
2818
- if (replaying === null)
2819
- return;
2820
- installing(replaying, 'replaying');
2821
- await screened();
2931
+ turn() {
2932
+ if (replaying)
2933
+ installing(locator);
2822
2934
  },
2823
2935
  prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
2936
+ later: (paths, roomVersion) => writtenLater(local, locator, key, paths, roomVersion),
2824
2937
  async stage(page) {
2825
2938
  for (let written = 0;;) {
2826
- if (replaying !== null)
2827
- await screened();
2828
2939
  const end = batchEnd(page.entries, written);
2829
2940
  const last = end === page.entries.length;
2830
2941
  const entries = page.entries.slice(written, end);
2831
2942
  const staged = await write(async () => {
2832
2943
  if (!liveness())
2833
2944
  return false;
2834
- await timed(() => local.refresh(key, last
2945
+ await timed(stageBudget, () => local.refresh(key, composition, last
2835
2946
  ? { ...page, entries }
2836
2947
  : { entries, removals: [], folders: [], folderRemovals: [] }));
2837
2948
  return true;
@@ -2850,94 +2961,180 @@ export function composeRuntime(options) {
2850
2961
  let spent = 0;
2851
2962
  while (end < entries.length) {
2852
2963
  const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
2853
- if (spent + size > stageBytes && end > from)
2964
+ if (spent + size > stageBudget.bytes && end > from)
2854
2965
  break;
2855
2966
  spent += size;
2856
2967
  end += 1;
2857
2968
  }
2858
2969
  return end;
2859
2970
  }
2860
- async function timed(work) {
2971
+ async function timed(budget, work) {
2861
2972
  const started = performance.now();
2862
2973
  const done = await work();
2863
2974
  const spent = performance.now() - started;
2864
- if (spent < STAGE_AIM_MS / 2)
2865
- stageBytes = Math.min(STAGE_BYTES, Math.round(stageBytes * 1.5));
2866
- else if (spent > STAGE_AIM_MS * 2) {
2867
- stageBytes = Math.max(STAGE_FLOOR, Math.round(stageBytes / 1.5));
2975
+ if (spent < budget.aim / 2)
2976
+ budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
2977
+ else if (spent > budget.aim * 2) {
2978
+ budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
2868
2979
  }
2869
2980
  return done;
2870
2981
  }
2871
2982
  function installWrite(work) {
2872
- const next = installWrites.then(() => exclusive(work), () => exclusive(work));
2873
- installWrites = next.then(() => undefined, () => undefined);
2874
- return next;
2983
+ return installTurn(installTurns.starting, work);
2984
+ }
2985
+ function finishingWrite(work) {
2986
+ return installTurn(installTurns.finishing, work);
2987
+ }
2988
+ function installTurn(queue, work) {
2989
+ return new Promise((resolve, reject) => {
2990
+ queue.push(() => void exclusive(work)
2991
+ .then(resolve, reject)
2992
+ .finally(() => {
2993
+ installTurns.busy = false;
2994
+ nextInstallTurn();
2995
+ }));
2996
+ nextInstallTurn();
2997
+ });
2875
2998
  }
2876
- function screened() {
2877
- if (onScreen || closed)
2878
- return Promise.resolve();
2879
- queueReadiness();
2880
- return new Promise((resolve) => shownWaiters.push(resolve));
2999
+ function nextInstallTurn() {
3000
+ if (installTurns.busy || installTurns.ahead > 0)
3001
+ return;
3002
+ const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
3003
+ if (turn === undefined)
3004
+ return;
3005
+ installTurns.busy = true;
3006
+ turn();
3007
+ }
3008
+ async function ahead(work) {
3009
+ let taken = false;
3010
+ const hold = {
3011
+ take() {
3012
+ if (taken)
3013
+ return;
3014
+ taken = true;
3015
+ installTurns.ahead += 1;
3016
+ },
3017
+ release() {
3018
+ if (!taken)
3019
+ return;
3020
+ taken = false;
3021
+ installTurns.ahead -= 1;
3022
+ resumeInstall();
3023
+ },
3024
+ };
3025
+ hold.take();
3026
+ try {
3027
+ return await work(hold);
3028
+ }
3029
+ finally {
3030
+ hold.release();
3031
+ }
2881
3032
  }
2882
- async function heldPart() {
2883
- while (partsFree === 0)
2884
- await new Promise((admit) => partWaiters.push(admit));
2885
- partsFree -= 1;
2886
- return freed();
3033
+ function resumeInstall() {
3034
+ if (installTurns.ahead > 0)
3035
+ return;
3036
+ void yielded().then(nextInstallTurn);
3037
+ }
3038
+ async function heldStream() {
3039
+ while (streamsFree === 0)
3040
+ await new Promise((admit) => installWaiters.push(admit));
3041
+ streamsFree -= 1;
3042
+ return once(() => {
3043
+ streamsFree += 1;
3044
+ });
3045
+ }
3046
+ async function unqueued() {
3047
+ while (queuedText >= QUEUED_TEXT) {
3048
+ await new Promise((admit) => installWaiters.push(admit));
3049
+ }
2887
3050
  }
2888
- function freed() {
3051
+ function once(release) {
2889
3052
  let released = false;
2890
3053
  return () => {
2891
3054
  if (released)
2892
3055
  return;
2893
3056
  released = true;
2894
- partsFree += 1;
2895
- partWaiters.shift()?.();
3057
+ release();
3058
+ for (const admit of installWaiters.splice(0))
3059
+ admit();
2896
3060
  };
2897
3061
  }
2898
- function installing(locator, step, share = null) {
2899
- installs.set(locator, { step, share });
3062
+ function installing(locator) {
3063
+ installs.add(locator);
2900
3064
  queueReadiness();
2901
3065
  }
2902
- function installed(locator) {
2903
- if (installs.delete(locator))
2904
- queueReadiness();
3066
+ function untransferred(locator) {
3067
+ transfers.delete(locator);
3068
+ arrivals.delete(locator);
3069
+ streaming.delete(locator);
3070
+ underived.delete(locator);
3071
+ unindexed.delete(locator);
3072
+ }
3073
+ function setBack(locator) {
3074
+ const now = bytesOf(locator, true);
3075
+ const earlier = startedOver.get(locator);
3076
+ startedOver.set(locator, {
3077
+ done: Math.max(now.done, earlier?.done ?? 0),
3078
+ total: Math.max(now.total, earlier?.total ?? 0),
3079
+ });
2905
3080
  }
2906
- function screen(value) {
2907
- if (value === onScreen)
3081
+ function owedIndexes(locator, bytes, text) {
3082
+ if (bytes <= 0 || text <= 0)
2908
3083
  return;
2909
- onScreen = value;
2910
- journal.record(`installation ${value ? 'shown' : 'hidden'}`);
3084
+ unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
3085
+ }
3086
+ function built(locator, { done, text }) {
3087
+ const owed = unindexed.get(locator) ?? [];
3088
+ let left = text;
3089
+ while (!done && left > 0 && owed.length > 0) {
3090
+ const [first] = owed;
3091
+ if (left >= first.text) {
3092
+ left -= first.text;
3093
+ owed.shift();
3094
+ continue;
3095
+ }
3096
+ owed[0] = {
3097
+ bytes: (first.bytes * (first.text - left)) / first.text,
3098
+ text: first.text - left,
3099
+ };
3100
+ left = 0;
3101
+ }
3102
+ if (done || owed.length === 0)
3103
+ unindexed.delete(locator);
2911
3104
  queueReadiness();
2912
- if (!value)
2913
- return;
2914
- for (const resolve of shownWaiters.splice(0))
2915
- resolve();
2916
3105
  }
2917
- async function pullRefresh(authorized, held, liveness) {
3106
+ function installed(locator) {
3107
+ if (installs.delete(locator))
3108
+ queueReadiness();
3109
+ }
3110
+ async function pullRefresh(authorized, held, liveness, hold, refresh) {
2918
3111
  if (!liveness())
2919
3112
  return unconfirmed;
2920
- const turn = generation;
2921
3113
  const { record } = held;
2922
- const base = held.confirmed;
3114
+ const from = standingOf(record.locator);
2923
3115
  const local = await openLocal();
2924
3116
  try {
2925
3117
  await exclusive(() => local.unrefresh(record.key));
2926
- const result = await advancement(reads, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, liveness));
3118
+ const result = await advancement(async (authorization, request) => {
3119
+ hold.release();
3120
+ try {
3121
+ return await refresh(authorization, request);
3122
+ }
3123
+ finally {
3124
+ hold.take();
3125
+ }
3126
+ }, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.locator, record.key, composed(record), liveness));
2927
3127
  if (result.kind !== 'advanced') {
2928
3128
  journal.record(`transfer refresh ${record.locator} ${result.kind}`);
2929
3129
  return result;
2930
3130
  }
2931
- const refused = await validatedRoom(local, result.record, 'refreshed', {
2932
- record,
2933
- changed: [...result.changed],
2934
- });
2935
- if (refused !== null) {
2936
- journal.record(`transfer refresh ${record.locator} refused`);
2937
- return refused;
2938
- }
2939
3131
  journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
2940
- return { kind: 'advanced', record: result.record, base };
3132
+ return {
3133
+ kind: 'advanced',
3134
+ record: result.record,
3135
+ from,
3136
+ remodelled: result.record.model !== record.model,
3137
+ };
2941
3138
  }
2942
3139
  catch (error) {
2943
3140
  if (refusedForSpace(error))
@@ -2945,9 +3142,6 @@ export function composeRuntime(options) {
2945
3142
  journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
2946
3143
  throw error;
2947
3144
  }
2948
- finally {
2949
- reads.finished(record.locator, turn);
2950
- }
2951
3145
  }
2952
3146
  const heldContents = new Map();
2953
3147
  function heldContentOf(local, key) {
@@ -2965,33 +3159,50 @@ export function composeRuntime(options) {
2965
3159
  heldContents.set(key, made);
2966
3160
  return made;
2967
3161
  }
3162
+ async function abandoned(local, locator) {
3163
+ await local.abandon(locator);
3164
+ setBack(locator);
3165
+ untransferred(locator);
3166
+ liveQueries.recover();
3167
+ sweep();
3168
+ }
2968
3169
  function staging(local, liveness) {
3170
+ const derived = async (locator, key, composition) => {
3171
+ underived.delete(locator);
3172
+ for (;;) {
3173
+ if (!liveness())
3174
+ return false;
3175
+ const done = await finishingWrite(async () => liveness()
3176
+ ? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
3177
+ : null);
3178
+ if (done === null)
3179
+ return false;
3180
+ built(locator, done);
3181
+ if (done.done)
3182
+ return true;
3183
+ }
3184
+ };
2969
3185
  return {
2970
3186
  held: (locator) => transfers.get(locator) ?? null,
2971
3187
  async turn(locator, step) {
2972
- installing(locator, step);
2973
- for (;;) {
2974
- await screened();
2975
- const release = step === 'downloading' ? await heldPart() : () => { };
2976
- if (onScreen || closed)
2977
- return release;
2978
- release();
2979
- }
3188
+ installing(locator);
3189
+ return step === 'downloading' ? heldStream() : () => { };
2980
3190
  },
2981
- spare() {
2982
- if (!onScreen || closed || partsFree === 0)
2983
- return null;
2984
- partsFree -= 1;
2985
- return freed();
3191
+ unqueued,
3192
+ queued(text) {
3193
+ queuedText += text;
3194
+ return once(() => {
3195
+ queuedText -= text;
3196
+ });
2986
3197
  },
2987
3198
  arriving(locator) {
2988
3199
  let ended = false;
2989
3200
  return {
2990
- at(token, bytes) {
3201
+ at(bytes) {
2991
3202
  if (ended)
2992
3203
  return;
2993
- const held = arrivals.get(locator) ?? new Map();
2994
- arrivals.set(locator, held.set(token, bytes));
3204
+ arrivals.set(locator, bytes);
3205
+ streaming.add(locator);
2995
3206
  const now = performance.now();
2996
3207
  if (now - arrivalReported < ARRIVAL_REPORT_MS)
2997
3208
  return;
@@ -2999,96 +3210,64 @@ export function composeRuntime(options) {
2999
3210
  queueReadiness();
3000
3211
  },
3001
3212
  end() {
3213
+ if (ended)
3214
+ return;
3002
3215
  ended = true;
3003
- if (arrivals.delete(locator))
3216
+ if (streaming.delete(locator))
3004
3217
  queueReadiness();
3005
3218
  },
3006
3219
  };
3007
3220
  },
3008
- received: (locator, token) => local.transferPart(locator, token),
3009
- keep: (locator, token, received) => installWrite(async () => {
3010
- if (liveness())
3011
- await local.keepPart(locator, token, received);
3012
- }),
3013
- announced(locator) {
3014
- const later = announced.get(locator);
3015
- return later === undefined ? [] : [later];
3016
- },
3017
- async stage(record, page, composition, received) {
3221
+ streamed: options.adapter.roomStream !== undefined,
3222
+ async stage(record, page, composition) {
3018
3223
  const { locator } = record;
3019
- const whole = received?.byteLength ?? 0;
3020
- const from = record.writing?.written ?? 0;
3021
- const token = record.writing?.token ?? null;
3022
- const entries = page.entries.length;
3023
- let held = record;
3024
- if (token !== null)
3025
- arrivals.get(locator)?.delete(token);
3026
- installing(locator, 'writing', { written: from, entries, bytes: whole });
3027
- for (let written = from;;) {
3028
- await screened();
3224
+ installing(locator);
3225
+ if (!liveness())
3226
+ return null;
3227
+ const key = await installWrite(async () => {
3029
3228
  if (!liveness())
3030
3229
  return null;
3031
- const end = batchEnd(page.entries, written);
3032
- const last = end === entries;
3033
- const next = last
3034
- ? {
3035
- ...held,
3036
- writing: null,
3037
- done: {
3038
- parts: held.done.parts + (received === null ? 0 : 1),
3039
- bytes: held.done.bytes + whole,
3040
- },
3041
- }
3042
- : { ...held, writing: { token, written: end } };
3043
- const key = await installWrite(async () => {
3044
- if (!liveness())
3045
- return null;
3046
- return timed(() => local.stage(next, composition, {
3047
- entries: page.entries.slice(written, end),
3048
- folders: last ? page.folders : [],
3049
- }, last && received !== null && token !== null ? token : undefined));
3050
- });
3051
- if (key === null)
3052
- return null;
3053
- const arrived = !transfers.has(locator);
3054
- held = { ...next, key };
3055
- transfers.set(locator, held);
3056
- if (arrived)
3057
- liveQueries.recover();
3058
- if (last) {
3059
- installing(locator, held.pageToken === null ? 'replaying' : 'downloading');
3060
- liveQueries.recover();
3061
- return held;
3062
- }
3063
- installing(locator, 'writing', { written: end, entries, bytes: whole });
3064
- written = end;
3065
- }
3230
+ return local.stage(record, composition, page);
3231
+ });
3232
+ if (key === null || !liveness())
3233
+ return null;
3234
+ const arrived = !transfers.has(locator);
3235
+ const prior = transfers.get(locator)?.done.bytes ?? 0;
3236
+ const held = { ...record, key };
3237
+ transfers.set(locator, held);
3238
+ if (arrived)
3239
+ liveQueries.recover();
3240
+ installing(locator);
3241
+ liveQueries.recover();
3242
+ const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
3243
+ owedIndexes(locator, record.done.bytes - prior, text);
3244
+ const owed = text + (underived.get(locator) ?? 0);
3245
+ underived.set(locator, owed);
3246
+ if (owed < deriveBudget.bytes)
3247
+ return held;
3248
+ return (await derived(locator, key, composition)) ? held : null;
3066
3249
  },
3067
- async refreshing(locator, key) {
3250
+ derive: derived,
3251
+ async refreshing(locator, key, composition) {
3068
3252
  await exclusive(() => local.unrefresh(key));
3069
- return refreshingOf(local, key, liveness, locator);
3253
+ return refreshingOf(local, locator, key, composition, liveness, true);
3070
3254
  },
3071
3255
  abandon: (locator) => exclusive(async () => {
3072
- if (!liveness())
3073
- return;
3074
- await local.abandon(locator);
3075
- transfers.delete(locator);
3076
- liveQueries.recover();
3077
- sweep();
3256
+ if (liveness())
3257
+ await abandoned(local, locator);
3078
3258
  }),
3079
- setAside: (locator) => exclusive(async () => {
3259
+ setAside: async (locator) => {
3080
3260
  if (!liveness())
3081
3261
  return;
3082
- await local.abandon(locator, true);
3083
- transfers.delete(locator);
3262
+ setBack(locator);
3263
+ untransferred(locator);
3084
3264
  liveQueries.recover();
3085
- }),
3265
+ },
3086
3266
  release: (key) => exclusive(async () => {
3087
3267
  await local.release(key);
3088
3268
  sweep();
3089
3269
  }),
3090
3270
  content: (key) => heldContentOf(local, key),
3091
- validate: (record, view) => validatedRoom(local, record, view, null),
3092
3271
  };
3093
3272
  }
3094
3273
  function reusable(room, local) {
@@ -3100,18 +3279,21 @@ export function composeRuntime(options) {
3100
3279
  async function pullBootstrap(authorized, room, local, liveness) {
3101
3280
  if (!liveness())
3102
3281
  return unconfirmed;
3103
- const turn = generation;
3104
3282
  if (replicas.has(room.locator)) {
3105
3283
  catching.add(room.locator);
3106
3284
  liveQueries.recover();
3107
3285
  }
3108
3286
  try {
3109
- const result = await bootstrapRoom(reads, authorized, room, {
3110
- collection: modelOf(room.locator),
3111
- selection: modelOf(room.locator, 'typed-selection'),
3112
- }, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
3287
+ const result = await bootstrapRoom(reads, authorized, room, { collection: modelOf(room.locator) }, builtIn, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
3113
3288
  const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
3114
3289
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
3290
+ if (result.kind === 'staged' && older(room.locator, result.record.roomVersion)) {
3291
+ caughtUpTo(room.locator);
3292
+ installed(room.locator);
3293
+ await abandoned(local, room.locator);
3294
+ journal.record(`transfer bootstrap ${room.locator} older than shown`);
3295
+ return { kind: 'transport-uncertainty' };
3296
+ }
3115
3297
  if (result.kind !== 'staged') {
3116
3298
  caughtUpTo(room.locator);
3117
3299
  installed(room.locator);
@@ -3126,9 +3308,6 @@ export function composeRuntime(options) {
3126
3308
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
3127
3309
  throw error;
3128
3310
  }
3129
- finally {
3130
- reads.finished(room.locator, turn);
3131
- }
3132
3311
  }
3133
3312
  function caughtUpTo(locator) {
3134
3313
  if (catching.delete(locator))
@@ -3136,72 +3315,83 @@ export function composeRuntime(options) {
3136
3315
  }
3137
3316
  function behind(room) {
3138
3317
  const held = replicas.get(room.locator)?.record.roomVersion;
3139
- return (held !== undefined &&
3140
- [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
3318
+ return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
3141
3319
  }
3142
- async function advance(room, local, authorized, live) {
3143
- const carried = reads.carried(room.locator);
3144
- const withheld = await advanceOnce(room, local, authorized, live);
3145
- if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
3146
- return withheld;
3147
- }
3148
- return advanceOnce(room, local, authorized, live);
3320
+ function standingOf(locator) {
3321
+ const held = replicas.get(locator);
3322
+ return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
3323
+ }
3324
+ function stands(locator, from) {
3325
+ const now = standingOf(locator);
3326
+ return now.key === from.key && now.confirmed === from.confirmed;
3149
3327
  }
3150
- async function advanceOnce(room, local, authorized, running) {
3328
+ async function advance(room, local, authorized, running) {
3151
3329
  const live = () => running() && !full;
3152
3330
  if (!live())
3153
3331
  return unconfirmed;
3154
3332
  const held = replicas.get(room.locator);
3155
- if (held !== undefined) {
3156
- const refreshed = await pullRefresh(authorized, held, live);
3157
- if (!live())
3158
- return unconfirmed;
3159
- if (refreshed.kind === 'still-current') {
3160
- stale.delete(room.locator);
3161
- if (sourceRefusals.has(room.locator))
3162
- return null;
3163
- if (unreadable.delete(room.locator)) {
3164
- scopedRefusals.delete(room.locator);
3165
- list(room, true);
3166
- emitRooms();
3167
- }
3168
- return null;
3169
- }
3170
- if (refreshed.kind === 'advanced') {
3171
- const landing = { kind: 'refreshed', base: refreshed.base };
3172
- if (!(await publish(local, room, refreshed.record, landing, live))) {
3173
- const now = replicas.get(room.locator)?.record.roomVersion;
3174
- const reached = now !== undefined &&
3175
- options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3176
- return reached ? null : advance(room, local, authorized, live);
3177
- }
3178
- await retainModel(local, room, 'collection', modelOf(room.locator));
3179
- return null;
3180
- }
3181
- if (refreshed.kind !== 'beyond-horizon')
3333
+ if (held !== undefined && options.adapter.roomStream !== undefined) {
3334
+ stale.delete(room.locator);
3335
+ name(room.locator);
3336
+ return null;
3337
+ }
3338
+ const refresh = reads.refresh;
3339
+ if (held !== undefined && refresh !== undefined) {
3340
+ const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
3341
+ if (refreshed === 'behind')
3342
+ return advance(room, local, authorized, live);
3343
+ if (refreshed !== 'beyond-horizon')
3182
3344
  return refreshed;
3183
- if (opened !== null) {
3184
- owed.add(room.locator);
3185
- liveQueries.recover();
3186
- return unconfirmed;
3187
- }
3188
- if (reloadsDue.get(room.locator) === generation)
3345
+ if (reloadsDue.has(room.locator))
3189
3346
  return unconfirmed;
3347
+ replaced([room.locator]);
3190
3348
  }
3349
+ const from = standingOf(room.locator);
3191
3350
  const bootstrapped = await pullBootstrap(authorized, room, local, live);
3192
- if (bootstrapped.kind === 'staged') {
3193
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3351
+ if (bootstrapped.kind !== 'staged')
3352
+ return live() ? bootstrapped : unconfirmed;
3353
+ const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
3354
+ if (await publish(local, room, bootstrapped.record, landing, live)) {
3194
3355
  return live() ? null : unconfirmed;
3195
3356
  }
3196
- return live() ? bootstrapped : unconfirmed;
3357
+ return reachedBy(room.locator, bootstrapped.record.roomVersion)
3358
+ ? null
3359
+ : advance(room, local, authorized, live);
3360
+ }
3361
+ async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
3362
+ const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
3363
+ if (!live())
3364
+ return unconfirmed;
3365
+ if (refreshed.kind === 'still-current') {
3366
+ stale.delete(room.locator);
3367
+ if (unreadable.delete(room.locator)) {
3368
+ scopedRefusals.delete(room.locator);
3369
+ list(room, true);
3370
+ emitRooms();
3371
+ }
3372
+ return null;
3373
+ }
3374
+ if (refreshed.kind === 'advanced') {
3375
+ const landing = {
3376
+ kind: 'refreshed',
3377
+ from: refreshed.from,
3378
+ remodelled: refreshed.remodelled,
3379
+ };
3380
+ if (!(await publish(local, room, refreshed.record, landing, live))) {
3381
+ const now = replicas.get(room.locator)?.record.roomVersion;
3382
+ const reached = now !== undefined &&
3383
+ options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3384
+ return reached ? null : 'behind';
3385
+ }
3386
+ await retainModel(local, room, modelOf(room.locator));
3387
+ return null;
3388
+ }
3389
+ return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
3197
3390
  }
3198
3391
  async function withhold(withheld, room, local, progress) {
3199
- if (withheld.kind === 'unconfirmed')
3392
+ if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
3200
3393
  return;
3201
- if (withheld.kind === 'refused')
3202
- stale.delete(room.locator);
3203
- else
3204
- stale.add(room.locator);
3394
+ stale.add(room.locator);
3205
3395
  if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
3206
3396
  const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
3207
3397
  repairable.set(room.locator, reset);
@@ -3223,30 +3413,10 @@ export function composeRuntime(options) {
3223
3413
  list(room, false);
3224
3414
  emitRooms();
3225
3415
  }
3226
- else if (withheld.kind === 'refused') {
3227
- refusedSource(room, withheld);
3228
- }
3229
3416
  else if (withheld.kind === 'revoked-authorization') {
3230
3417
  await refuse('revoked-authorization');
3231
3418
  }
3232
3419
  }
3233
- function refusedSource(room, { refusal }) {
3234
- sourceRefusals.set(room.locator, {
3235
- code: refusal.rule,
3236
- retryable: false,
3237
- fields: {
3238
- file: refusal.file,
3239
- ...(refusal.location === undefined ? {} : { location: refusal.location }),
3240
- ...(refusal.referrers === undefined ? {} : { referrers: refusal.referrers }),
3241
- },
3242
- });
3243
- declarationRefusals.delete(room.locator);
3244
- unreadable.add(room.locator);
3245
- list(room, false);
3246
- emitRooms();
3247
- moveTo({ ...status, ...attention() });
3248
- journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
3249
- }
3250
3420
  function leaving(batch, result) {
3251
3421
  if (result.kind === 'expired')
3252
3422
  return { batch, returned: returnedBy('expired') };
@@ -3268,8 +3438,8 @@ export function composeRuntime(options) {
3268
3438
  function settling(acceptance) {
3269
3439
  return acceptance !== null && 'apply' in acceptance;
3270
3440
  }
3271
- function refusalOf(acceptance) {
3272
- return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
3441
+ function failureOf(acceptance) {
3442
+ return settling(acceptance) ? null : acceptance;
3273
3443
  }
3274
3444
  async function acceptance(room, local, submitted, accepted) {
3275
3445
  if (!continuing())
@@ -3282,7 +3452,8 @@ export function composeRuntime(options) {
3282
3452
  logAccepted(room.locator, submitted, accepted);
3283
3453
  let record = current.record;
3284
3454
  let changes;
3285
- if (!options.adapter.reached(room.locator, held, accepted.roomVersion)) {
3455
+ if (!options.adapter.reached(room.locator, held, accepted.roomVersion) &&
3456
+ !older(room.locator, accepted.roomVersion)) {
3286
3457
  const paths = [...returnedPaths(submitted, accepted)];
3287
3458
  const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
3288
3459
  const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
@@ -3292,34 +3463,6 @@ export function composeRuntime(options) {
3292
3463
  ? accepted.roomVersion
3293
3464
  : held,
3294
3465
  };
3295
- const overrides = new Map([
3296
- ...back.set.map((entry) => [entry.path, entry]),
3297
- ...back.removed.map((path) => [path, null]),
3298
- ]);
3299
- const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
3300
- if (refused !== null) {
3301
- const key = pendingKey(room.locator, submitted.id);
3302
- return {
3303
- locator: room.locator,
3304
- left: [],
3305
- commit: {
3306
- rooms: [],
3307
- writes: [
3308
- {
3309
- locator: room.locator,
3310
- accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
3311
- },
3312
- ],
3313
- },
3314
- refused,
3315
- apply() {
3316
- const state = pendings.get(key);
3317
- if (state !== undefined && !fences.has(room.locator))
3318
- pendings.set(key, { ...state, accepted: accepted.roomVersion });
3319
- return changeSet(room.locator, { queue: true });
3320
- },
3321
- };
3322
- }
3323
3466
  changes = back;
3324
3467
  }
3325
3468
  const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
@@ -3327,10 +3470,9 @@ export function composeRuntime(options) {
3327
3470
  ...settled,
3328
3471
  apply(answer) {
3329
3472
  const set = settled.apply(answer);
3473
+ showing(room.locator, accepted.roomVersion);
3330
3474
  pendings.delete(pendingKey(room.locator, submitted.id));
3331
3475
  unreadable.delete(room.locator);
3332
- sourceRefusals.delete(room.locator);
3333
- declarationRefusals.delete(room.locator);
3334
3476
  list(room, true);
3335
3477
  return set;
3336
3478
  },
@@ -3348,13 +3490,9 @@ export function composeRuntime(options) {
3348
3490
  const failed = await exclusive(async () => {
3349
3491
  const adopting = await acceptance(room, local, submitted, accepted);
3350
3492
  await adoptedTogether(local, [adopting]);
3351
- return refusalOf(adopting);
3493
+ return failureOf(adopting);
3352
3494
  });
3353
- if (failed === null)
3354
- return true;
3355
- if (failed.kind === 'refused' && continuing())
3356
- refusedSource(room, failed);
3357
- return false;
3495
+ return failed === null;
3358
3496
  }
3359
3497
  async function adoptedMove(room, partner, local, submitted, accepted) {
3360
3498
  const other = discovered.get(partner);
@@ -3368,18 +3506,10 @@ export function composeRuntime(options) {
3368
3506
  ...accepted.destination,
3369
3507
  });
3370
3508
  await adoptedTogether(local, [left, arrived]);
3371
- const refusedArrival = refusalOf(arrived);
3372
- if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
3373
- refusedSource(other, refusedArrival);
3374
- }
3375
- return refusalOf(left);
3509
+ return failureOf(left);
3376
3510
  });
3377
3511
  submitLater(partner);
3378
- if (failed === null)
3379
- return true;
3380
- if (failed.kind === 'refused' && continuing())
3381
- refusedSource(room, failed);
3382
- return false;
3512
+ return failed === null;
3383
3513
  }
3384
3514
  function queueIn(locator) {
3385
3515
  return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
@@ -3502,8 +3632,6 @@ export function composeRuntime(options) {
3502
3632
  if (replica === undefined || head === undefined)
3503
3633
  return 'done';
3504
3634
  const state = pendings.get(pendingKey(room.locator, head.id));
3505
- if (state?.accepted !== undefined)
3506
- return 'done';
3507
3635
  const partner = partnerOf(head, room.locator);
3508
3636
  if (partner !== null && !leavingIn(head, room.locator)) {
3509
3637
  submitLater(partner);
@@ -3617,12 +3745,12 @@ export function composeRuntime(options) {
3617
3745
  }
3618
3746
  if (result.kind === 'expired') {
3619
3747
  await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
3620
- const there = other === undefined ? null : await advance(other, local, authorized, continuing);
3748
+ const there = other === undefined ? null : await advanced(other, local, authorized);
3621
3749
  if (other !== undefined && there !== null) {
3622
3750
  await withhold(there, other, local, progress);
3623
3751
  return 'done';
3624
3752
  }
3625
- const withheld = await advance(room, local, authorized, continuing);
3753
+ const withheld = await advanced(room, local, authorized);
3626
3754
  if (withheld !== null) {
3627
3755
  await withhold(withheld, room, local, progress);
3628
3756
  return 'done';
@@ -3644,8 +3772,8 @@ export function composeRuntime(options) {
3644
3772
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
3645
3773
  const left = leaving(head, result);
3646
3774
  const refreshed = async () => ({
3647
- there: other === undefined ? null : await advance(other, local, authorized, continuing),
3648
- here: await advance(room, local, authorized, continuing),
3775
+ there: other === undefined ? null : await advanced(other, local, authorized),
3776
+ here: await advanced(room, local, authorized),
3649
3777
  });
3650
3778
  const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
3651
3779
  if (judged !== null && (judged.there !== null || judged.here !== null)) {
@@ -3716,6 +3844,7 @@ export function composeRuntime(options) {
3716
3844
  locator: room.locator,
3717
3845
  key: before.record.key,
3718
3846
  roots: before.record.typedSelection?.roots ?? [],
3847
+ composed: composedOf(composed(before.record)),
3719
3848
  overlay: { set: [], cleared: [...before.overlaid] },
3720
3849
  },
3721
3850
  ]),
@@ -3748,39 +3877,61 @@ export function composeRuntime(options) {
3748
3877
  if (signal.aborted)
3749
3878
  return;
3750
3879
  const progress = { complete: true, reachable: true };
3751
- await roomTask(room.locator, async (live) => {
3752
- await Promise.race([
3753
- reload(room, local, progress, () => live() && !signal.aborted),
3754
- abandonedOn(signal),
3755
- ]);
3756
- });
3880
+ await roomTask(room.locator, (live) => reload(room, local, progress, live));
3757
3881
  if (signal.aborted)
3758
3882
  return;
3759
3883
  await rest(progress);
3760
3884
  }
3761
3885
  }
3762
3886
  function awaitingAnswer(locator) {
3887
+ answering.get(locator)?.release();
3763
3888
  let release = () => { };
3764
- const awaited = new Promise((resolve) => {
3889
+ const answered = new Promise((resolve) => {
3765
3890
  release = resolve;
3766
3891
  });
3767
- answering.set(locator, { generation, answered: awaited });
3892
+ const awaited = { answered, release };
3893
+ answering.set(locator, awaited);
3768
3894
  return () => {
3769
- if (answering.get(locator)?.answered === awaited)
3895
+ if (answering.get(locator) === awaited)
3770
3896
  answering.delete(locator);
3771
3897
  release();
3772
3898
  };
3773
3899
  }
3774
- async function retainModel(local, room, attached, next) {
3775
- const key = modelKey(room.locator, attached);
3776
- const previous = modelOf(room.locator, attached);
3777
- const heldBefore = replicas.get(room.locator)?.record;
3778
- if (JSON.stringify(previous) === JSON.stringify(next) &&
3779
- (heldBefore === undefined ||
3780
- (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
3781
- JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
3782
- JSON.stringify(heldBefore.typedSelection) ===
3783
- JSON.stringify(room.typedSelection ?? null))))
3900
+ function declaredAsHeld(room) {
3901
+ const held = replicas.get(room.locator)?.record;
3902
+ return (held === undefined ||
3903
+ (JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
3904
+ JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
3905
+ JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
3906
+ }
3907
+ async function keepNaming(local, room) {
3908
+ const held = replicas.get(room.locator)?.record;
3909
+ if (held === undefined || (held.name === room.name && held.handle === room.handle))
3910
+ return;
3911
+ await exclusive(async () => {
3912
+ if (!continuing())
3913
+ return;
3914
+ const replica = replicas.get(room.locator);
3915
+ if (replica === undefined)
3916
+ return;
3917
+ const record = { ...replica.record, name: room.name, handle: room.handle };
3918
+ await local.commit({
3919
+ rooms: [
3920
+ {
3921
+ locator: room.locator,
3922
+ key: record.key,
3923
+ record: storedRecord(record),
3924
+ composed: composedOf(composed(record)),
3925
+ },
3926
+ ],
3927
+ });
3928
+ replicas.set(room.locator, { ...replica, record });
3929
+ });
3930
+ }
3931
+ async function retainModel(local, room, next) {
3932
+ const key = room.locator;
3933
+ const previous = modelOf(room.locator);
3934
+ if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(room))
3784
3935
  return;
3785
3936
  if (next.status === 'unavailable') {
3786
3937
  invalidModels.add(key);
@@ -3792,26 +3943,15 @@ export function composeRuntime(options) {
3792
3943
  const held = replicas.get(room.locator);
3793
3944
  let changed = null;
3794
3945
  if (held !== undefined) {
3795
- const declared = {
3946
+ const record = {
3796
3947
  ...held.record,
3948
+ name: room.name,
3949
+ handle: room.handle,
3797
3950
  operations: room.operations,
3798
3951
  replica: room.replica,
3799
3952
  typedSelection: room.typedSelection ?? null,
3800
- ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
3953
+ model: next,
3801
3954
  };
3802
- let read = 'staged';
3803
- if (!sameMembership(declared, held.record)) {
3804
- const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), compositionOf(declared));
3805
- read =
3806
- verdict.verdict !== 'refuse'
3807
- ? 'staged'
3808
- : verdict.rule === 'model-unavailable'
3809
- ? 'deferred'
3810
- : { kind: 'refused', refusal: verdict };
3811
- }
3812
- const record = read === 'staged'
3813
- ? declared
3814
- : { ...declared, typedSelection: held.record.typedSelection };
3815
3955
  const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
3816
3956
  const replayedLayer = await replayed(record, confirmedLayer, held.queue);
3817
3957
  const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
@@ -3822,7 +3962,7 @@ export function composeRuntime(options) {
3822
3962
  key: record.key,
3823
3963
  record: storedRecord(record),
3824
3964
  roots: record.typedSelection?.roots ?? [],
3825
- composition: compositionOf(record),
3965
+ composed: composedOf(composed(record)),
3826
3966
  overlay: diff.overlay,
3827
3967
  },
3828
3968
  ],
@@ -3833,29 +3973,22 @@ export function composeRuntime(options) {
3833
3973
  documents: answer.changed[room.locator] ?? [],
3834
3974
  room: true,
3835
3975
  });
3836
- if (read === 'staged' || read === 'deferred') {
3837
- if (declarationRefusals.delete(room.locator)) {
3838
- sourceRefusals.delete(room.locator);
3839
- unreadable.delete(room.locator);
3840
- }
3841
- }
3842
- else {
3843
- refusedSource(room, read);
3844
- declarationRefusals.add(room.locator);
3845
- }
3846
- }
3847
- models.set(key, next);
3848
- if (next.status === 'current') {
3849
- liveModels.set(key, JSON.stringify(next.snapshot));
3850
- invalidModels.delete(key);
3851
3976
  }
3852
- list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
3977
+ heldModel(room, next);
3853
3978
  if (changed === null)
3854
3979
  emitRooms();
3855
3980
  else
3856
3981
  publishChangeSet(changed);
3857
3982
  });
3858
3983
  }
3984
+ function heldModel(room, next) {
3985
+ models.set(room.locator, next);
3986
+ if (next.status === 'current') {
3987
+ liveModels.set(room.locator, JSON.stringify(next.snapshot));
3988
+ invalidModels.delete(room.locator);
3989
+ }
3990
+ list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
3991
+ }
3859
3992
  async function scoped(result, room, local) {
3860
3993
  if (result.scope === 'content' && !result.retryable) {
3861
3994
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
@@ -3869,14 +4002,14 @@ export function composeRuntime(options) {
3869
4002
  });
3870
4003
  let scopedAway = room;
3871
4004
  if (result.scope === 'model')
3872
- await retainModel(local, room, 'collection', {
4005
+ await retainModel(local, room, {
3873
4006
  status: 'unavailable',
3874
4007
  reason: 'not-readable',
3875
4008
  });
3876
4009
  else if (result.scope !== 'operation') {
3877
4010
  unreadable.add(room.locator);
3878
4011
  scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
3879
- await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
4012
+ await retainModel(local, scopedAway, modelOf(room.locator));
3880
4013
  }
3881
4014
  if (!continuing())
3882
4015
  return;
@@ -3884,6 +4017,17 @@ export function composeRuntime(options) {
3884
4017
  emitRooms();
3885
4018
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
3886
4019
  }
4020
+ function name(locator) {
4021
+ const stream = options.adapter.roomStream;
4022
+ const record = replicas.get(locator)?.record;
4023
+ const room = discovered.get(locator);
4024
+ if (stream === undefined || record === undefined || room === undefined)
4025
+ return;
4026
+ if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
4027
+ return;
4028
+ naming += 1;
4029
+ stream.hold([{ locator, roomVersion: record.roomVersion }]);
4030
+ }
3887
4031
  function heldRooms(listed) {
3888
4032
  return listed.flatMap((room) => {
3889
4033
  const record = replicas.get(room.locator)?.record;
@@ -3901,14 +4045,19 @@ export function composeRuntime(options) {
3901
4045
  return outcome?.status === 'fulfilled' && outcome.value;
3902
4046
  });
3903
4047
  if (attempt === 'opened') {
4048
+ naming += 1;
3904
4049
  options.adapter.roomStream?.hold(heldRooms(ready));
3905
4050
  handed = true;
3906
4051
  }
3907
- discovering?.release();
3908
- const loaded = await Promise.allSettled(ready
3909
- .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
3910
- .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
3911
- const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
4052
+ for (const room of ready) {
4053
+ if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
4054
+ if (attempt === 'authorized')
4055
+ name(room.locator);
4056
+ continue;
4057
+ }
4058
+ void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
4059
+ }
4060
+ const failed = prepared.find((room) => room.status === 'rejected');
3912
4061
  if (failed !== undefined)
3913
4062
  throw failed.reason;
3914
4063
  }
@@ -3926,27 +4075,31 @@ export function composeRuntime(options) {
3926
4075
  return false;
3927
4076
  }
3928
4077
  if (prior !== undefined)
3929
- await retainModel(local, room, 'collection', modelOf(room.locator));
4078
+ await retainModel(local, room, modelOf(room.locator));
4079
+ if (prior !== undefined)
4080
+ await keepNaming(local, room);
3930
4081
  if (discoveryPaused || authorization === undefined)
3931
4082
  return false;
3932
4083
  return room.replica.admission === 'admitted';
3933
4084
  }
3934
4085
  async function caughtUp(room, local, progress, live) {
3935
- if (authorization === undefined || !live() || !behind(room))
4086
+ if (options.adapter.roomStream !== undefined || authorization === undefined)
4087
+ return;
4088
+ if (!live() || !behind(room))
3936
4089
  return;
3937
4090
  const withheld = await advance(room, local, authorization, live);
3938
4091
  if (withheld !== null)
3939
4092
  await withhold(withheld, room, local, progress);
3940
4093
  }
3941
4094
  function revoked(locator) {
3942
- return ['collection', 'typed-selection'].some((attached) => {
3943
- const model = modelOf(locator, attached);
3944
- return model.status === 'unavailable' && model.reason === 'revoked';
3945
- });
4095
+ const model = modelOf(locator);
4096
+ return model.status === 'unavailable' && model.reason === 'revoked';
3946
4097
  }
3947
4098
  async function loadRoom(room, local, progress, live) {
3948
4099
  if (!live() || authorization === undefined)
3949
4100
  return;
4101
+ if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
4102
+ return;
3950
4103
  if (replicas.has(room.locator) && revoked(room.locator)) {
3951
4104
  await reload(room, local, progress, live);
3952
4105
  return;
@@ -3964,20 +4117,53 @@ export function composeRuntime(options) {
3964
4117
  reloadsDue.delete(room.locator);
3965
4118
  if (!live() || authorization === undefined)
3966
4119
  return;
4120
+ const from = standingOf(room.locator);
3967
4121
  const bootstrapped = await pullBootstrap(authorization, room, local, live);
3968
4122
  if (bootstrapped.kind !== 'staged') {
3969
4123
  if (live())
3970
4124
  await withhold(bootstrapped, room, local, progress);
3971
4125
  return;
3972
4126
  }
3973
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
4127
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
3974
4128
  await caughtUp(room, local, progress, live);
3975
4129
  }
3976
4130
  finally {
3977
4131
  caughtUpTo(room.locator);
3978
4132
  }
3979
4133
  }
3980
- async function discoverRooms(local, progress, attempt, live, scope) {
4134
+ async function asked(authorized) {
4135
+ try {
4136
+ return (await options.adapter.discover?.(authorized)) ?? { kind: 'transport-uncertainty' };
4137
+ }
4138
+ catch {
4139
+ return { kind: 'transport-uncertainty' };
4140
+ }
4141
+ }
4142
+ function malformed(listed) {
4143
+ for (const room of listed) {
4144
+ if (room.operations === undefined ||
4145
+ !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
4146
+ room.operations[operation].every((scope) => typeof scope === 'string')) ||
4147
+ room.replica === undefined ||
4148
+ typeof room.replica.selection !== 'string' ||
4149
+ room.replica.selection.length === 0 ||
4150
+ !['admitted', 'unavailable'].includes(room.replica.admission) ||
4151
+ !['locked', 'open'].includes(room.replica.offline)) {
4152
+ return `invalid model or replica declaration ${room.locator}`;
4153
+ }
4154
+ if (!declaredSelection(room))
4155
+ return `invalid typed selection declaration ${room.locator}`;
4156
+ }
4157
+ return null;
4158
+ }
4159
+ async function signIn(local, signedIn) {
4160
+ if (JSON.stringify(signedIn) === JSON.stringify(member))
4161
+ return;
4162
+ await local.fact('member', signedIn);
4163
+ member = signedIn;
4164
+ emitMember();
4165
+ }
4166
+ async function discoverRooms(local, progress, attempt, live, found) {
3981
4167
  if (closed || authorization === undefined) {
3982
4168
  notifyReady();
3983
4169
  return;
@@ -3985,13 +4171,9 @@ export function composeRuntime(options) {
3985
4171
  moveTo(transferring(status));
3986
4172
  discoveries += 1;
3987
4173
  const started = discoveries;
3988
- let result;
3989
- try {
3990
- result = await options.adapter.discover(authorization, scope);
3991
- }
3992
- catch {
3993
- result = { kind: 'transport-uncertainty' };
3994
- }
4174
+ const result = found ?? (await asked(authorization));
4175
+ if (result.kind === 'discovery')
4176
+ roomsNamed = true;
3995
4177
  if (!live()) {
3996
4178
  progress.complete = false;
3997
4179
  notifyReady();
@@ -3999,12 +4181,7 @@ export function composeRuntime(options) {
3999
4181
  }
4000
4182
  journal.record(`connection discover ${result.kind}`);
4001
4183
  if (result.kind === 'transport-uncertainty') {
4002
- for (const room of rooms.values()) {
4003
- await retainModel(local, room, 'collection', cached(modelOf(room.locator)));
4004
- if (room.typedSelection !== null) {
4005
- await retainModel(local, room, 'typed-selection', cached(modelOf(room.locator, 'typed-selection')));
4006
- }
4007
- }
4184
+ await cachedModels(local);
4008
4185
  progress.reachable = false;
4009
4186
  progress.complete = false;
4010
4187
  notifyReady();
@@ -4025,26 +4202,12 @@ export function composeRuntime(options) {
4025
4202
  notifyReady();
4026
4203
  return;
4027
4204
  }
4028
- for (const room of result.rooms) {
4029
- if (room.operations === undefined ||
4030
- !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
4031
- room.operations[operation].every((scope) => typeof scope === 'string')) ||
4032
- room.replica === undefined ||
4033
- typeof room.replica.selection !== 'string' ||
4034
- room.replica.selection.length === 0 ||
4035
- !['admitted', 'unavailable'].includes(room.replica.admission) ||
4036
- !['locked', 'open'].includes(room.replica.offline)) {
4037
- journal.record(`invalid model or replica declaration ${room.locator}`);
4038
- await refuse('missing-required-guarantee');
4039
- notifyReady();
4040
- return;
4041
- }
4042
- if (!declaredSelection(room)) {
4043
- journal.record(`invalid typed selection declaration ${room.locator}`);
4044
- await refuse('missing-required-guarantee');
4045
- notifyReady();
4046
- return;
4047
- }
4205
+ const invalid = malformed(result.rooms);
4206
+ if (invalid !== null) {
4207
+ journal.record(invalid);
4208
+ await refuse('missing-required-guarantee');
4209
+ notifyReady();
4210
+ return;
4048
4211
  }
4049
4212
  if (refusal !== null) {
4050
4213
  refusal = null;
@@ -4060,13 +4223,8 @@ export function composeRuntime(options) {
4060
4223
  await local.fact('providerLimits', limits);
4061
4224
  await local.fact('capabilities', capabilities);
4062
4225
  }
4063
- const signedIn = result.member ?? null;
4064
- if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
4065
- await local.fact('member', signedIn);
4066
- member = signedIn;
4067
- emitMember();
4068
- }
4069
- await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
4226
+ await signIn(local, result.member ?? null);
4227
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
4070
4228
  }
4071
4229
  async function ensureAuthorization() {
4072
4230
  if (authorization !== undefined)
@@ -4081,23 +4239,45 @@ export function composeRuntime(options) {
4081
4239
  }
4082
4240
  }
4083
4241
  async function admittedRead(work) {
4084
- if (roomReads.generation !== generation) {
4085
- roomReads = { generation, free: ROOM_READS, waiting: [] };
4242
+ while (roomReads.free === 0) {
4243
+ await new Promise((admit) => roomReads.waiting.push(admit));
4086
4244
  }
4087
- const reads = roomReads;
4088
- while (reads.free === 0)
4089
- await new Promise((admit) => reads.waiting.push(admit));
4090
- reads.free -= 1;
4245
+ roomReads.free -= 1;
4091
4246
  try {
4092
4247
  await work();
4093
4248
  }
4094
4249
  finally {
4095
- reads.free += 1;
4096
- reads.waiting.shift()?.();
4250
+ roomReads.free += 1;
4251
+ roomReads.waiting.shift()?.();
4097
4252
  }
4098
4253
  }
4254
+ function inRoom(locator, work) {
4255
+ roomWork += 1;
4256
+ const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
4257
+ try {
4258
+ return await work(continuing);
4259
+ }
4260
+ finally {
4261
+ roomWork -= 1;
4262
+ }
4263
+ });
4264
+ const chain = ran.then(() => undefined, () => undefined);
4265
+ roomLanes.set(locator, chain);
4266
+ working.add(chain);
4267
+ void chain.finally(() => {
4268
+ working.delete(chain);
4269
+ if (roomLanes.get(locator) === chain)
4270
+ roomLanes.delete(locator);
4271
+ });
4272
+ return ran;
4273
+ }
4099
4274
  function roomTask(locator, work) {
4100
- return task(roomLane(locator), (live) => admittedRead(() => work(live)));
4275
+ if (stopping)
4276
+ return Promise.resolve();
4277
+ return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
4278
+ }
4279
+ function advanced(room, local, authorized) {
4280
+ return inRoom(room.locator, (live) => advance(room, local, authorized, live));
4101
4281
  }
4102
4282
  function task(lane, work) {
4103
4283
  if (stopping)
@@ -4113,23 +4293,19 @@ export function composeRuntime(options) {
4113
4293
  void run.finally(() => working.delete(run));
4114
4294
  return run;
4115
4295
  }
4116
- function sync(attempt) {
4296
+ function sync(attempt, found) {
4117
4297
  return task(OPENING_LANE, async (live) => {
4118
4298
  if (!live()) {
4119
- notifyReady();
4299
+ readyFromStore();
4120
4300
  return;
4121
4301
  }
4122
4302
  const progress = { complete: true, reachable: true };
4123
- const gate = discover();
4124
- if (attempt === 'opened')
4125
- opened = generation;
4126
- underway = attempt === 'opened' ? { generation, progress } : null;
4303
+ underway = attempt === 'opened' ? progress : null;
4127
4304
  if (attempt === 'opened')
4128
4305
  streamed = progress;
4129
4306
  complete = false;
4130
4307
  recorded = false;
4131
4308
  moveTo(transferring(status));
4132
- handed = false;
4133
4309
  try {
4134
4310
  const local = await openLocal();
4135
4311
  if (!live())
@@ -4137,7 +4313,7 @@ export function composeRuntime(options) {
4137
4313
  await ensureAuthorization();
4138
4314
  if (!live())
4139
4315
  return;
4140
- await discoverRooms(local, progress, attempt, live);
4316
+ await discoverRooms(local, progress, attempt, live, found);
4141
4317
  }
4142
4318
  catch {
4143
4319
  progress.complete = false;
@@ -4145,7 +4321,6 @@ export function composeRuntime(options) {
4145
4321
  progress.reachable = false;
4146
4322
  }
4147
4323
  finally {
4148
- gate.release();
4149
4324
  notifyReady();
4150
4325
  }
4151
4326
  if (!live())
@@ -4157,35 +4332,59 @@ export function composeRuntime(options) {
4157
4332
  if (handed || closed || progress.reachable)
4158
4333
  return;
4159
4334
  underway = null;
4160
- opened = null;
4161
- journal.record('connection reopen');
4162
4335
  await rest(progress, live);
4163
- options.adapter.roomStream?.reopen();
4164
4336
  });
4165
4337
  }
4166
- function rediscover(scope) {
4338
+ function resynchronize() {
4167
4339
  return task(OPENING_LANE, async (live) => {
4168
- if (!live() || !socketOpen)
4340
+ if (!live() || !announced)
4169
4341
  return;
4170
4342
  const progress = { complete: true, reachable: true };
4171
- const gate = discover();
4172
4343
  moveTo(transferring(status));
4173
4344
  try {
4174
4345
  const local = await openLocal();
4175
4346
  if (!live())
4176
4347
  return;
4177
- await ensureAuthorization();
4348
+ await synchronize([...discovered.values()], local, progress, 'unstreamed', live);
4349
+ }
4350
+ catch {
4351
+ progress.complete = false;
4352
+ }
4353
+ if (live())
4354
+ await finishOpening(progress, live);
4355
+ });
4356
+ }
4357
+ function countAuthorityChange() {
4358
+ authorityChanges += 1;
4359
+ for (const listener of authorityChangeListeners)
4360
+ listener(authorityChanges);
4361
+ }
4362
+ function changeAuthority(change) {
4363
+ return task(OPENING_LANE, async (live) => {
4364
+ if (!live() || !announced)
4365
+ return;
4366
+ countAuthorityChange();
4367
+ const progress = { complete: true, reachable: true };
4368
+ moveTo(transferring(status));
4369
+ try {
4370
+ const local = await openLocal();
4178
4371
  if (!live())
4179
4372
  return;
4180
- await discoverRooms(local, progress, 'authorized', live, scope);
4373
+ const invalid = malformed(change.rooms);
4374
+ if (invalid !== null) {
4375
+ journal.record(invalid);
4376
+ await refuse('missing-required-guarantee');
4377
+ return;
4378
+ }
4379
+ if (change.member !== undefined)
4380
+ await signIn(local, change.member);
4381
+ discoveries += 1;
4382
+ const listed = await applyDiscovery(change.rooms, discoveries, change.left);
4383
+ await synchronize(listed, local, progress, 'authorized', live);
4181
4384
  }
4182
4385
  catch {
4183
4386
  progress.complete = false;
4184
4387
  }
4185
- finally {
4186
- gate.release();
4187
- notifyReady();
4188
- }
4189
4388
  if (!live())
4190
4389
  return;
4191
4390
  if (!progress.complete)
@@ -4193,14 +4392,17 @@ export function composeRuntime(options) {
4193
4392
  await rest(progress, live);
4194
4393
  });
4195
4394
  }
4196
- function closedSocket(revoked, lost) {
4197
- const opened = socketOpen;
4395
+ function closedSocket(revoked, lost, authorityChanged) {
4396
+ notifyReady();
4397
+ const opened = announced;
4198
4398
  socketOpen = false;
4399
+ announced = false;
4400
+ missedChange ||= authorityChanged;
4401
+ handed = false;
4402
+ naming = 0;
4199
4403
  connectionLost();
4200
- if (lost) {
4201
- reads.lost();
4404
+ if (lost)
4202
4405
  conversations.lost();
4203
- }
4204
4406
  if (revoked) {
4205
4407
  void refuse('revoked-authorization');
4206
4408
  return;
@@ -4211,18 +4413,32 @@ export function composeRuntime(options) {
4211
4413
  moveTo({
4212
4414
  ...status,
4213
4415
  state: status.state === 'synced' ? 'syncing' : status.state,
4214
- providerUnreachable: status.providerUnreachable || !opened,
4416
+ providerUnreachable: status.providerUnreachable || (!opened && lost),
4417
+ });
4418
+ void closing(opened);
4419
+ }
4420
+ function closing(opened) {
4421
+ return task(OPENING_LANE, async (live) => {
4422
+ if (!live())
4423
+ return;
4424
+ const local = await openLocal();
4425
+ if (!opened && !socketOpen)
4426
+ await cachedModels(local);
4427
+ await rest(null, live);
4215
4428
  });
4216
4429
  }
4217
- function applyOpening(expired) {
4218
- const progress = underway?.generation === generation ? underway.progress : null;
4430
+ function cachedModels(local) {
4431
+ return settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
4432
+ }
4433
+ function caughtUpRooms(expired) {
4434
+ const progress = underway;
4435
+ underway = null;
4436
+ naming = Math.max(0, naming - 1);
4219
4437
  const cycle = progress ?? { complete: true, reachable: true };
4220
- if (opened === generation)
4221
- opened = null;
4438
+ replaced(expired);
4222
4439
  const reloading = progress === null ? [] : owedReloads(expired);
4223
4440
  const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
4224
4441
  return task(OPENING_LANE, async (live) => {
4225
- underway = null;
4226
4442
  if (!live())
4227
4443
  return;
4228
4444
  if (progress === null) {
@@ -4230,24 +4446,14 @@ export function composeRuntime(options) {
4230
4446
  await rest(cycle, live);
4231
4447
  return;
4232
4448
  }
4233
- await roomsCarried([...expired, ...reloading]);
4234
- if (!live())
4235
- return;
4236
4449
  try {
4237
4450
  const local = await openLocal();
4238
- const reloaded = await replacing;
4451
+ await replacing;
4239
4452
  if (progress.reachable) {
4240
- for (const room of discovered.values()) {
4241
- if (replicas.has(room.locator) &&
4242
- !expired.includes(room.locator) &&
4243
- !reloading.includes(room.locator)) {
4244
- await confirmModels(room, local);
4245
- }
4246
- }
4453
+ await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
4454
+ !expired.includes(room.locator) &&
4455
+ !reloading.includes(room.locator)), 'current', local);
4247
4456
  }
4248
- const failed = reloaded.find((outcome) => outcome.status === 'rejected');
4249
- if (failed !== undefined)
4250
- throw failed.reason;
4251
4457
  }
4252
4458
  catch {
4253
4459
  progress.complete = false;
@@ -4259,11 +4465,8 @@ export function composeRuntime(options) {
4259
4465
  }
4260
4466
  function owedReloads(expired) {
4261
4467
  const reloading = [...discovered.values()]
4262
- .filter((room) => replicas.has(room.locator) &&
4263
- !expired.includes(room.locator) &&
4264
- (owed.has(room.locator) || revoked(room.locator)))
4468
+ .filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
4265
4469
  .map((room) => room.locator);
4266
- owed.clear();
4267
4470
  for (const locator of reloading)
4268
4471
  catching.add(locator);
4269
4472
  liveQueries.recover();
@@ -4272,24 +4475,43 @@ export function composeRuntime(options) {
4272
4475
  async function reloadNow(replaced, progress) {
4273
4476
  const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
4274
4477
  for (const room of reloaded)
4275
- reloadsDue.set(room.locator, generation);
4478
+ reloadsDue.add(room.locator);
4276
4479
  try {
4277
4480
  const local = await openLocal();
4278
- return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
4481
+ for (const room of reloaded) {
4482
+ void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
4483
+ }
4279
4484
  }
4280
4485
  catch {
4281
4486
  for (const room of reloaded)
4282
4487
  reloadsDue.delete(room.locator);
4283
4488
  progress.reachable = false;
4284
- return [];
4285
4489
  }
4286
4490
  }
4287
- async function confirmModels(room, local) {
4288
- for (const attached of ['collection', 'typed-selection']) {
4289
- const model = modelOf(room.locator, attached);
4290
- if (model.status === 'cached') {
4291
- await retainModel(local, room, attached, { ...model, status: 'current' });
4491
+ async function settleModels(held, status, local) {
4492
+ const owed = (room) => {
4493
+ const model = modelOf(room.locator);
4494
+ return model.status !== 'unavailable' && model.status !== status;
4495
+ };
4496
+ const settled = await exclusive(async () => {
4497
+ if (!continuing())
4498
+ return [];
4499
+ const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
4500
+ for (const room of unchanged) {
4501
+ const next = { ...modelOf(room.locator), status };
4502
+ const replica = replicas.get(room.locator);
4503
+ if (replica !== undefined)
4504
+ replicas.set(room.locator, { ...replica, record: { ...replica.record, model: next } });
4505
+ heldModel(room, next);
4292
4506
  }
4507
+ if (unchanged.length > 0)
4508
+ emitRooms();
4509
+ return unchanged;
4510
+ });
4511
+ for (const room of held) {
4512
+ if (settled.includes(room) || !owed(room))
4513
+ continue;
4514
+ await retainModel(local, room, { ...modelOf(room.locator), status });
4293
4515
  }
4294
4516
  }
4295
4517
  function submitLater(locator) {
@@ -4334,14 +4556,10 @@ export function composeRuntime(options) {
4334
4556
  try {
4335
4557
  const local = await openLocal();
4336
4558
  await roomTask(locator, async (live) => {
4337
- const current = () => live() && !signal.aborted;
4338
- if (!current() || authorization === undefined)
4559
+ if (!live() || authorization === undefined)
4339
4560
  return;
4340
- const withheld = await Promise.race([
4341
- advance(room, local, authorization, current),
4342
- abandonedOn(signal),
4343
- ]);
4344
- if (withheld !== null && current())
4561
+ const withheld = await advance(room, local, authorization, live);
4562
+ if (withheld !== null && live())
4345
4563
  await withhold(withheld, room, local, progress);
4346
4564
  });
4347
4565
  }
@@ -4396,14 +4614,11 @@ export function composeRuntime(options) {
4396
4614
  progress.complete = false;
4397
4615
  return;
4398
4616
  }
4399
- reads.applied();
4400
4617
  submitAll();
4401
4618
  for (const locator of stale)
4402
4619
  if (repairable.has(locator))
4403
4620
  repairLater(locator, {});
4404
4621
  await conversations.reconcile();
4405
- if (status.providerUnreachable)
4406
- conversations.reconnected();
4407
4622
  const reading = { complete: true, reachable: true };
4408
4623
  await conversations.catchUp(reading);
4409
4624
  if (progress.reachable && reading.reachable)
@@ -4429,7 +4644,7 @@ export function composeRuntime(options) {
4429
4644
  if (!live())
4430
4645
  return;
4431
4646
  let lastCompleted = status.lastCompleted;
4432
- let synced = complete && stale.size === 0 && !pending();
4647
+ let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
4433
4648
  if (synced && !recorded) {
4434
4649
  const completedAt = new Date().toISOString();
4435
4650
  try {
@@ -4444,74 +4659,78 @@ export function composeRuntime(options) {
4444
4659
  }
4445
4660
  }
4446
4661
  moveTo({
4447
- state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
4662
+ state: pending()
4663
+ ? 'pending'
4664
+ : synced && complete && roomWork === 0 && naming === 0
4665
+ ? 'synced'
4666
+ : 'syncing',
4448
4667
  lastCompleted,
4449
- providerUnreachable: !progress.reachable,
4668
+ providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
4450
4669
  ...attention(),
4451
4670
  });
4452
4671
  }
4453
- function discover() {
4454
- let release = () => { };
4455
- const done = new Promise((resolve) => {
4456
- release = resolve;
4457
- });
4458
- discovering = { generation, done, release };
4459
- return discovering;
4460
- }
4461
- function applyRoom(locator, roomVersion) {
4462
- announced.set(locator, roomVersion);
4463
- return task(roomLane(locator), async (live) => {
4464
- if (discovering?.generation === generation)
4465
- await discovering.done;
4466
- const saving = answering.get(locator);
4467
- if (saving?.generation === generation)
4468
- await saving.answered;
4469
- if (!live() || authorization === undefined)
4470
- return;
4471
- const room = discovered.get(locator);
4472
- const held = replicas.get(locator);
4473
- if (room === undefined || room.replica.admission !== 'admitted')
4474
- return;
4475
- if (held !== undefined &&
4476
- (held.record.replica.selection !== room.replica.selection ||
4477
- options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
4478
- return;
4479
- }
4480
- const during = underway?.generation === generation ? underway.progress : null;
4481
- const progress = during ?? { complete: true, reachable: true };
4482
- if (during === null)
4483
- moveTo(transferring(status));
4672
+ function applyRoom(change) {
4673
+ const { locator } = change;
4674
+ if (stopping)
4675
+ return Promise.resolve();
4676
+ return inRoom(locator, async (live) => {
4677
+ await answering.get(locator)?.answered;
4678
+ const progress = underway ?? { complete: true, reachable: true };
4484
4679
  try {
4485
4680
  const local = await openLocal();
4486
- await admittedRead(async () => {
4487
- if (!live() || authorization === undefined)
4488
- return;
4489
- const withheld = await advance(room, local, authorization, live);
4490
- if (withheld !== null)
4491
- await withhold(withheld, room, local, progress);
4492
- });
4681
+ for (;;) {
4682
+ if (!live())
4683
+ return null;
4684
+ const room = discovered.get(locator);
4685
+ const held = replicas.get(locator);
4686
+ if (room === undefined ||
4687
+ held === undefined ||
4688
+ room.replica.admission !== 'admitted' ||
4689
+ held.record.replica.selection !== room.replica.selection) {
4690
+ return null;
4691
+ }
4692
+ const at = held.record.roomVersion;
4693
+ if (options.adapter.reached(locator, at, change.roomVersion))
4694
+ return progress;
4695
+ if (!options.adapter.reached(locator, at, change.from)) {
4696
+ name(locator);
4697
+ return progress;
4698
+ }
4699
+ if (underway === null)
4700
+ moveTo(transferring(status));
4701
+ const writing = () => live() && !full;
4702
+ let outlined = false;
4703
+ const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, async () => {
4704
+ const page = await change.read();
4705
+ const paths = Object.keys(page.outlines ?? {});
4706
+ outlined = paths.length > 0;
4707
+ await local.remember(locator, held.record.key, paths);
4708
+ return page;
4709
+ }));
4710
+ if (refreshed.kind !== 'advanced')
4711
+ return progress;
4712
+ const landing = {
4713
+ kind: 'refreshed',
4714
+ from: refreshed.from,
4715
+ remodelled: refreshed.remodelled,
4716
+ };
4717
+ if (await publish(local, room, refreshed.record, landing, writing)) {
4718
+ stale.delete(locator);
4719
+ await retainModel(local, room, modelOf(locator));
4720
+ if (outlined)
4721
+ void heldAgain(locator);
4722
+ return progress;
4723
+ }
4724
+ }
4493
4725
  }
4494
4726
  catch {
4727
+ if (!stale.has(locator))
4728
+ name(locator);
4495
4729
  stale.add(locator);
4730
+ return progress;
4496
4731
  }
4497
- finally {
4498
- if (during === null)
4499
- await rest(progress, live);
4500
- }
4501
- });
4732
+ }).then((progress) => rest(progress), () => undefined);
4502
4733
  }
4503
- function roomsCarried(replacing) {
4504
- const replaced = new Set(replacing.map(roomLane));
4505
- return Promise.all([...lanes]
4506
- .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
4507
- .map(([, held]) => held.chain));
4508
- }
4509
- const receipts = {
4510
- heads: async () => (await openLocal()).receivedHeads(),
4511
- parts: async (key, count) => (await openLocal()).receivedParts(key, count),
4512
- keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
4513
- drop: async (keys) => (await openLocal()).dropReceived(keys),
4514
- };
4515
4734
  function observeStream() {
4516
4735
  const stream = options.adapter.roomStream;
4517
4736
  if (stream === undefined || withdrawStream !== undefined)
@@ -4521,29 +4740,35 @@ export function composeRuntime(options) {
4521
4740
  return;
4522
4741
  if (event.kind === 'opened') {
4523
4742
  socketOpen = true;
4743
+ announced = false;
4744
+ handed = false;
4745
+ naming = 0;
4524
4746
  connectionLive();
4525
4747
  generation += 1;
4526
- reads.opened();
4527
4748
  conversations.reopened();
4528
4749
  recovery.resume();
4529
- void sync('opened');
4750
+ }
4751
+ else if (event.kind === 'discovered') {
4752
+ if (!socketOpen)
4753
+ return;
4754
+ announced = true;
4755
+ if (missedChange) {
4756
+ missedChange = false;
4757
+ countAuthorityChange();
4758
+ }
4759
+ void sync('opened', event.discovery);
4760
+ for (const locator of replicas.keys())
4761
+ void heldAgain(locator);
4530
4762
  }
4531
4763
  else if (event.kind === 'closed')
4532
- closedSocket(event.revoked, event.lost);
4533
- else if (event.kind === 'opening')
4534
- void applyOpening(event.expired);
4764
+ closedSocket(event.revoked, event.lost, event.authorityChanged);
4765
+ else if (event.kind === 'caught-up')
4766
+ void caughtUpRooms(event.expired);
4535
4767
  else if (event.kind === 'authority-changed')
4536
- void rediscover(event.scope);
4768
+ void changeAuthority(event);
4537
4769
  else
4538
- void applyRoom(event.locator, event.roomVersion);
4539
- }, receipts, heldRevisions);
4540
- }
4541
- async function heldRevisions(locator, paths) {
4542
- const held = replicas.get(locator);
4543
- if (held === undefined || paths.length === 0)
4544
- return new Map();
4545
- const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
4546
- return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
4770
+ void applyRoom(event);
4771
+ });
4547
4772
  }
4548
4773
  function writesHeld() {
4549
4774
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -4613,10 +4838,9 @@ export function composeRuntime(options) {
4613
4838
  if (closed)
4614
4839
  return;
4615
4840
  closed = true;
4841
+ notifyDiscovered();
4616
4842
  if (probing !== null)
4617
4843
  clearInterval(probing);
4618
- for (const resolve of shownWaiters.splice(0))
4619
- resolve();
4620
4844
  connectionLive();
4621
4845
  lockListeners.clear();
4622
4846
  readinessWatches.clear();
@@ -4628,11 +4852,13 @@ export function composeRuntime(options) {
4628
4852
  conversations.close();
4629
4853
  void recovery.retire();
4630
4854
  roomListeners.clear();
4855
+ unlistedRooms.clear();
4631
4856
  refusalListeners.clear();
4632
4857
  storageLossListeners.clear();
4633
4858
  statusListeners.clear();
4634
4859
  serviceModeListeners.clear();
4635
4860
  memberListeners.clear();
4861
+ authorityChangeListeners.clear();
4636
4862
  liveQueries.close();
4637
4863
  queueSubscriptions.clear();
4638
4864
  roomDraftListeners.clear();
@@ -4640,6 +4866,7 @@ export function composeRuntime(options) {
4640
4866
  }
4641
4867
  async function stop() {
4642
4868
  stopping = true;
4869
+ inFlight.abort();
4643
4870
  await recovery.retire();
4644
4871
  await Promise.all(working);
4645
4872
  await mutations;
@@ -4653,12 +4880,12 @@ export function composeRuntime(options) {
4653
4880
  observeStream();
4654
4881
  void openLocal().then(() => {
4655
4882
  if (capabilities.length > 0)
4656
- notifyReady();
4883
+ readyFromStore();
4657
4884
  }, () => {
4658
- notifyReady();
4885
+ readyFromStore();
4659
4886
  });
4660
4887
  if (options.adapter.roomStream === undefined)
4661
- void sync('unstreamed');
4888
+ void sync('unstreamed', null);
4662
4889
  function planned(input, reach) {
4663
4890
  const plan = planOf(input, roomFacts(reach), planReach(reach));
4664
4891
  const named = plan.paths !== null &&
@@ -4700,58 +4927,56 @@ export function composeRuntime(options) {
4700
4927
  stop();
4701
4928
  };
4702
4929
  }
4703
- async function searched(input, reach, locked) {
4930
+ async function searched(input, reach) {
4704
4931
  const asked = searchPlanOf(input);
4705
- const filter = {
4706
- ...filterOf(asked.documents ?? {}, roomFacts(reach), planReach(reach)),
4707
- locked,
4708
- };
4709
- const unlocking = (filter.rooms ?? [])
4710
- .filter((room) => locked.includes(room))
4711
- .map((room) => ({ room, awaits: 'unlock' }));
4712
- for (;;) {
4713
- const cover = covered(filter);
4714
- awaitPreparation(cover);
4715
- const pending = [...unlocking, ...('pending' in cover ? cover.pending : [])];
4716
- const complete = pending.length === 0 && !('pending' in cover);
4717
- const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
4718
- const answering = complete
4719
- ? filter
4720
- : { ...filter, rooms: reachedRooms(filter).filter((room) => !awaited.has(room)) };
4721
- const answered = covered(answering);
4722
- if ('pending' in answered)
4723
- continue;
4724
- const listed = conversations.searchable();
4725
- const named = reach !== null && !reach.conversations
4726
- ? []
4727
- : (asked.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
4728
- const hits = await (await openLocal()).search({
4729
- text: asked.text,
4730
- limit: capped(asked.limit),
4731
- after: asked.after,
4732
- total: asked.total,
4733
- documents: {
4734
- plan: answering,
4735
- rooms: answered.rooms,
4736
- beneath: asked.collection
4737
- ? answered.rooms.map(({ locator, key }) => ({
4738
- key,
4739
- roots: replicas.get(locator)?.record.typedSelection?.roots ?? [],
4740
- }))
4741
- : [],
4742
- },
4743
- conversations: named,
4744
- });
4745
- if (JSON.stringify(covered(answering)) !== JSON.stringify(answered))
4746
- continue;
4747
- const coverage = {
4748
- rooms: answered.coverage.rooms,
4749
- conversations: named,
4750
- overlay: answered.coverage.overlay,
4932
+ await ready;
4933
+ const search = options.adapter.search;
4934
+ if (search === undefined) {
4935
+ return {
4936
+ kind: 'undeclared',
4937
+ code: 'action-undeclared',
4938
+ fields: { name: 'search' },
4939
+ retryable: false,
4940
+ };
4941
+ }
4942
+ if (closed || refusal !== null || connectionOf() !== 'connected')
4943
+ return OFFLINE;
4944
+ const reached = reach === null ? null : new Set(reach.rooms);
4945
+ const named = asked.rooms === null
4946
+ ? reached === null
4947
+ ? null
4948
+ : [...reached]
4949
+ : asked.rooms.filter((room) => reached === null || reached.has(room));
4950
+ const rooms = named === null ? null : named.filter((room) => replicas.has(room)).sort();
4951
+ if (rooms !== null && rooms.length === 0)
4952
+ return { kind: 'found', documents: [] };
4953
+ try {
4954
+ await ensureAuthorization();
4955
+ }
4956
+ catch {
4957
+ return UNCERTAIN;
4958
+ }
4959
+ if (closed || authorization === undefined || refusal !== null)
4960
+ return UNCERTAIN;
4961
+ journal.record('transfer search');
4962
+ try {
4963
+ const result = await search(authorization, { text: asked.text, rooms });
4964
+ if (result.kind === 'revoked-authorization')
4965
+ await refuse('revoked-authorization');
4966
+ journal.record(`outcome search ${result.kind}`);
4967
+ if (result.kind === 'rejected')
4968
+ return result;
4969
+ if (result.kind !== 'found')
4970
+ return failure(result);
4971
+ const kept = rooms === null ? null : new Set(rooms);
4972
+ return {
4973
+ kind: 'found',
4974
+ documents: result.documents.filter((hit) => kept === null || kept.has(hit.room)),
4751
4975
  };
4752
- return complete
4753
- ? { ...hits, coverage }
4754
- : { pending, documents: hits.documents, messages: hits.messages, coverage };
4976
+ }
4977
+ catch {
4978
+ journal.record('outcome search transport-uncertainty');
4979
+ return UNCERTAIN;
4755
4980
  }
4756
4981
  }
4757
4982
  const watch = (topic, input, deliver) => {
@@ -4803,12 +5028,13 @@ export function composeRuntime(options) {
4803
5028
  };
4804
5029
  }
4805
5030
  if (topic === 'readiness') {
4806
- return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
5031
+ return watchReadiness({ target: '', code: 'stored', space: null }, null, deliver);
4807
5032
  }
4808
5033
  if (topic === 'rooms') {
4809
- roomListeners.add(deliver);
4810
- deliver(listed());
5034
+ unlistedRooms.add(deliver);
5035
+ emitKnownRooms();
4811
5036
  return () => {
5037
+ unlistedRooms.delete(deliver);
4812
5038
  roomListeners.delete(deliver);
4813
5039
  };
4814
5040
  }
@@ -4847,6 +5073,13 @@ export function composeRuntime(options) {
4847
5073
  memberListeners.delete(deliver);
4848
5074
  };
4849
5075
  }
5076
+ if (topic === 'authority-changes') {
5077
+ authorityChangeListeners.add(deliver);
5078
+ deliver(authorityChanges);
5079
+ return () => {
5080
+ authorityChangeListeners.delete(deliver);
5081
+ };
5082
+ }
4850
5083
  if (topic === 'queue') {
4851
5084
  const queue = { locator: input[0], notify: deliver };
4852
5085
  queueSubscriptions.add(queue);
@@ -4862,17 +5095,27 @@ export function composeRuntime(options) {
4862
5095
  return openLocal().then(() => undefined);
4863
5096
  },
4864
5097
  rooms() {
4865
- return listed();
5098
+ if (closed)
5099
+ return Promise.resolve([]);
5100
+ return new Promise((settle) => {
5101
+ const answer = (value) => {
5102
+ unlistedRooms.delete(answer);
5103
+ roomListeners.delete(answer);
5104
+ settle(value);
5105
+ };
5106
+ unlistedRooms.add(answer);
5107
+ emitKnownRooms();
5108
+ });
4866
5109
  },
4867
5110
  query: ((input) => rowsQuery(planned(input, null))),
4868
- search: (input) => searched(input, null, []),
5111
+ search: (input) => searched(input, null),
4869
5112
  types(locator) {
4870
5113
  return {
4871
5114
  collection: modelTypes(modelOf(locator)),
4872
5115
  typedSelection: rooms.get(locator)?.typedSelection === undefined ||
4873
5116
  rooms.get(locator)?.typedSelection === null
4874
5117
  ? null
4875
- : modelTypes(modelOf(locator, 'typed-selection')),
5118
+ : modelTypes(builtIn),
4876
5119
  };
4877
5120
  },
4878
5121
  roomDraft: {
@@ -4951,6 +5194,8 @@ export function composeRuntime(options) {
4951
5194
  part: readPart,
4952
5195
  read: readBinaryPath,
4953
5196
  link: linkBinaryPath,
5197
+ show: showBinaryPath,
5198
+ attach: attachBinary,
4954
5199
  },
4955
5200
  async call(name, input) {
4956
5201
  await ready;
@@ -4988,7 +5233,7 @@ export function composeRuntime(options) {
4988
5233
  authorization = undefined;
4989
5234
  discoveryPaused = false;
4990
5235
  if (options.adapter.roomStream === undefined)
4991
- await sync('unstreamed');
5236
+ await sync('unstreamed', null);
4992
5237
  else
4993
5238
  await task(OPENING_LANE, ensureAuthorization);
4994
5239
  },
@@ -5005,12 +5250,12 @@ export function composeRuntime(options) {
5005
5250
  lockedRooms,
5006
5251
  watchLocks,
5007
5252
  located,
5253
+ room: (locator) => listed().find((one) => one.locator === locator),
5008
5254
  };
5009
5255
  return {
5010
5256
  md,
5011
5257
  seed,
5012
5258
  stop,
5013
- lose: loseRooms,
5014
5259
  watch,
5015
5260
  persisted(answer) {
5016
5261
  if (answer === persistence)
@@ -5019,7 +5264,6 @@ export function composeRuntime(options) {
5019
5264
  journal.record(`persistence ${answer}`);
5020
5265
  queueReadiness();
5021
5266
  },
5022
- shown: screen,
5023
5267
  within: (reach, port) => {
5024
5268
  if (full)
5025
5269
  void probed();