@agentshouse/mdruntime 0.1.0-c5f333dfa0583 → 0.1.0-c5fe3b2cddda7

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 (214) hide show
  1. package/README.md +10 -8
  2. package/dist/adapter.d.ts +39 -40
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/attached-model.d.ts +2 -1
  5. package/dist/attached-model.d.ts.map +1 -1
  6. package/dist/attached-model.js +28 -4
  7. package/dist/binary.d.ts +0 -5
  8. package/dist/binary.d.ts.map +1 -1
  9. package/dist/binary.js +1 -1
  10. package/dist/bootstrap.d.ts +16 -8
  11. package/dist/bootstrap.d.ts.map +1 -1
  12. package/dist/bootstrap.js +142 -94
  13. package/dist/change-set.d.ts +1 -7
  14. package/dist/change-set.d.ts.map +1 -1
  15. package/dist/change-set.js +1 -4
  16. package/dist/client.d.ts +6 -7
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +54 -7
  19. package/dist/collection.d.ts +5 -3
  20. package/dist/collection.d.ts.map +1 -1
  21. package/dist/collection.js +24 -19
  22. package/dist/compile.d.ts +6 -1
  23. package/dist/compile.d.ts.map +1 -1
  24. package/dist/compile.js +94 -33
  25. package/dist/connection.d.ts +8 -7
  26. package/dist/connection.d.ts.map +1 -1
  27. package/dist/connection.js +1160 -1151
  28. package/dist/conversation.d.ts +7 -5
  29. package/dist/conversation.d.ts.map +1 -1
  30. package/dist/conversation.js +171 -55
  31. package/dist/durability.d.ts +1 -5
  32. package/dist/durability.d.ts.map +1 -1
  33. package/dist/durability.js +5 -0
  34. package/dist/entry.d.ts +1 -0
  35. package/dist/entry.d.ts.map +1 -1
  36. package/dist/entry.js +1 -1
  37. package/dist/file.d.ts +2 -11
  38. package/dist/file.d.ts.map +1 -1
  39. package/dist/file.js +6 -4
  40. package/dist/fulltext.d.ts +27 -0
  41. package/dist/fulltext.d.ts.map +1 -0
  42. package/dist/fulltext.js +370 -0
  43. package/dist/host.d.ts +3 -4
  44. package/dist/host.d.ts.map +1 -1
  45. package/dist/host.js +42 -18
  46. package/dist/identity.d.ts +0 -1
  47. package/dist/identity.d.ts.map +1 -1
  48. package/dist/identity.js +0 -3
  49. package/dist/index.d.ts +10 -10
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +4 -3
  52. package/dist/live.d.ts.map +1 -1
  53. package/dist/live.js +2 -1
  54. package/dist/media.d.ts +8 -0
  55. package/dist/media.d.ts.map +1 -0
  56. package/dist/media.js +218 -0
  57. package/dist/memory.d.ts +3 -2
  58. package/dist/memory.d.ts.map +1 -1
  59. package/dist/memory.js +93 -38
  60. package/dist/messages.d.ts +28 -0
  61. package/dist/messages.d.ts.map +1 -0
  62. package/dist/messages.js +90 -0
  63. package/dist/model.d.ts +6 -1
  64. package/dist/model.d.ts.map +1 -1
  65. package/dist/model.js +7 -10
  66. package/dist/observation.d.ts +1 -0
  67. package/dist/observation.d.ts.map +1 -1
  68. package/dist/observation.js +1 -1
  69. package/dist/outcome.d.ts +13 -6
  70. package/dist/outcome.d.ts.map +1 -1
  71. package/dist/outcome.js +16 -5
  72. package/dist/overlay.js +1 -1
  73. package/dist/paths.d.ts +1 -0
  74. package/dist/paths.d.ts.map +1 -1
  75. package/dist/paths.js +4 -0
  76. package/dist/query.d.ts +10 -1
  77. package/dist/query.d.ts.map +1 -1
  78. package/dist/query.js +12 -1
  79. package/dist/reach.d.ts +8 -3
  80. package/dist/reach.d.ts.map +1 -1
  81. package/dist/reach.js +69 -46
  82. package/dist/readiness.d.ts +14 -15
  83. package/dist/readiness.d.ts.map +1 -1
  84. package/dist/readiness.js +2 -8
  85. package/dist/recovery.d.ts +1 -0
  86. package/dist/recovery.d.ts.map +1 -1
  87. package/dist/recovery.js +26 -0
  88. package/dist/refresh.d.ts +9 -4
  89. package/dist/refresh.d.ts.map +1 -1
  90. package/dist/refresh.js +64 -30
  91. package/dist/replica.d.ts +5 -5
  92. package/dist/replica.d.ts.map +1 -1
  93. package/dist/replica.js +4 -7
  94. package/dist/room-draft.d.ts +18 -2
  95. package/dist/room-draft.d.ts.map +1 -1
  96. package/dist/room-draft.js +73 -6
  97. package/dist/rows.d.ts +15 -6
  98. package/dist/rows.d.ts.map +1 -1
  99. package/dist/rows.js +102 -74
  100. package/dist/save.d.ts +2 -0
  101. package/dist/save.d.ts.map +1 -1
  102. package/dist/save.js +13 -7
  103. package/dist/search.d.ts +11 -43
  104. package/dist/search.d.ts.map +1 -1
  105. package/dist/search.js +0 -296
  106. package/dist/shared.d.ts +0 -1
  107. package/dist/shared.d.ts.map +1 -1
  108. package/dist/shared.js +0 -10
  109. package/dist/status.d.ts +4 -3
  110. package/dist/status.d.ts.map +1 -1
  111. package/dist/status.js +3 -1
  112. package/dist/store.d.ts +23 -58
  113. package/dist/store.d.ts.map +1 -1
  114. package/dist/store.js +37 -272
  115. package/dist/stored.d.ts +10 -23
  116. package/dist/stored.d.ts.map +1 -1
  117. package/dist/stored.js +217 -218
  118. package/dist/tables.d.ts +51 -46
  119. package/dist/tables.d.ts.map +1 -1
  120. package/dist/tables.js +94 -53
  121. package/dist/transfer.d.ts +0 -5
  122. package/dist/transfer.d.ts.map +1 -1
  123. package/dist/validation.d.ts +3 -4
  124. package/dist/validation.d.ts.map +1 -1
  125. package/dist/validation.js +35 -96
  126. package/dist/wire.d.ts +5 -5
  127. package/dist/wire.d.ts.map +1 -1
  128. package/dist/wire.js +2 -2
  129. package/dist/words.d.ts +19 -0
  130. package/dist/words.d.ts.map +1 -0
  131. package/dist/words.js +156 -0
  132. package/package.json +11 -19
  133. package/AGENTS.md +0 -312
  134. package/dist/chat/acts.d.ts +0 -33
  135. package/dist/chat/acts.d.ts.map +0 -1
  136. package/dist/chat/acts.js +0 -36
  137. package/dist/chat/context.d.ts +0 -3
  138. package/dist/chat/context.d.ts.map +0 -1
  139. package/dist/chat/context.js +0 -10
  140. package/dist/chat/earlier.d.ts +0 -10
  141. package/dist/chat/earlier.d.ts.map +0 -1
  142. package/dist/chat/earlier.js +0 -54
  143. package/dist/chat/house.d.ts +0 -25
  144. package/dist/chat/house.d.ts.map +0 -1
  145. package/dist/chat/house.js +0 -38
  146. package/dist/chat/input.d.ts +0 -11
  147. package/dist/chat/input.d.ts.map +0 -1
  148. package/dist/chat/input.js +0 -92
  149. package/dist/chat/kit.d.ts +0 -61
  150. package/dist/chat/kit.d.ts.map +0 -1
  151. package/dist/chat/kit.js +0 -123
  152. package/dist/chat/lasting.d.ts +0 -6
  153. package/dist/chat/lasting.d.ts.map +0 -1
  154. package/dist/chat/lasting.js +0 -19
  155. package/dist/chat/list.d.ts +0 -28
  156. package/dist/chat/list.d.ts.map +0 -1
  157. package/dist/chat/list.js +0 -103
  158. package/dist/chat/markdown.d.ts +0 -21
  159. package/dist/chat/markdown.d.ts.map +0 -1
  160. package/dist/chat/markdown.js +0 -246
  161. package/dist/chat/message.d.ts +0 -7
  162. package/dist/chat/message.d.ts.map +0 -1
  163. package/dist/chat/message.js +0 -254
  164. package/dist/chat/messages.d.ts +0 -35
  165. package/dist/chat/messages.d.ts.map +0 -1
  166. package/dist/chat/messages.js +0 -210
  167. package/dist/chat/name.d.ts +0 -7
  168. package/dist/chat/name.d.ts.map +0 -1
  169. package/dist/chat/name.js +0 -37
  170. package/dist/chat/opened.d.ts +0 -12
  171. package/dist/chat/opened.d.ts.map +0 -1
  172. package/dist/chat/opened.js +0 -19
  173. package/dist/chat/overlay.d.ts +0 -32
  174. package/dist/chat/overlay.d.ts.map +0 -1
  175. package/dist/chat/overlay.js +0 -103
  176. package/dist/chat/parts.d.ts +0 -58
  177. package/dist/chat/parts.d.ts.map +0 -1
  178. package/dist/chat/parts.js +0 -120
  179. package/dist/chat/pieces.d.ts +0 -62
  180. package/dist/chat/pieces.d.ts.map +0 -1
  181. package/dist/chat/pieces.js +0 -1110
  182. package/dist/chat/process.d.ts +0 -20
  183. package/dist/chat/process.d.ts.map +0 -1
  184. package/dist/chat/process.js +0 -32
  185. package/dist/chat/provider.d.ts +0 -70
  186. package/dist/chat/provider.d.ts.map +0 -1
  187. package/dist/chat/provider.js +0 -635
  188. package/dist/chat/question.d.ts +0 -9
  189. package/dist/chat/question.d.ts.map +0 -1
  190. package/dist/chat/question.js +0 -69
  191. package/dist/chat/questions.d.ts +0 -31
  192. package/dist/chat/questions.d.ts.map +0 -1
  193. package/dist/chat/questions.js +0 -111
  194. package/dist/chat/read.d.ts +0 -5
  195. package/dist/chat/read.d.ts.map +0 -1
  196. package/dist/chat/read.js +0 -13
  197. package/dist/chat/ready.d.ts +0 -37
  198. package/dist/chat/ready.d.ts.map +0 -1
  199. package/dist/chat/ready.js +0 -93
  200. package/dist/chat/room-file.d.ts +0 -8
  201. package/dist/chat/room-file.d.ts.map +0 -1
  202. package/dist/chat/room-file.js +0 -72
  203. package/dist/chat/steady.d.ts +0 -3
  204. package/dist/chat/steady.d.ts.map +0 -1
  205. package/dist/chat/steady.js +0 -26
  206. package/dist/chat/style.d.ts +0 -4
  207. package/dist/chat/style.d.ts.map +0 -1
  208. package/dist/chat/style.js +0 -307
  209. package/dist/chat.d.ts +0 -15
  210. package/dist/chat.d.ts.map +0 -1
  211. package/dist/chat.js +0 -8
  212. package/dist/reads.d.ts +0 -11
  213. package/dist/reads.d.ts.map +0 -1
  214. package/dist/reads.js +0 -98
@@ -1,51 +1,41 @@
1
1
  import { escapePointer, parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
2
- import { attachedModel, NO_ROOM_TYPES, roomTypesOf } from './attached-model.js';
2
+ import { attachedModel, builtInModel, roomTypesOf } from './attached-model.js';
3
3
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
4
4
  import { observing, whole } from './observation.js';
5
5
  import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
6
6
  import { createLiveQueries } from './live.js';
7
7
  import { seededResult } from './seed.js';
8
8
  import { documentKey, roomOfKey } from './rows.js';
9
- import { createSearchIndex, searchPlanOf, } from './search.js';
9
+ import { searchPlanOf } from './search.js';
10
10
  import { capped, MAXIMUM_WINDOW } from './windows.js';
11
11
  import { bootstrapRoom } from './bootstrap.js';
12
12
  import { changeSet, mergeChangeSets } from './change-set.js';
13
13
  import { createConversationStore, } from './conversation.js';
14
14
  import { leaving as leavingIn, partnerOf, queuedFile, } from './change.js';
15
- import { available, collectionOf, unreadable as unreadableIn, validRoot, } from './collection.js';
15
+ import { available, collectionOf, composedOf, validRoot, } from './collection.js';
16
16
  import { runtimeError } from './failure.js';
17
17
  import { createLog } from './log.js';
18
18
  import { binaryRecord } from './binary.js';
19
+ import { attachedFile } from './file.js';
19
20
  import { openedFiles } from './opened.js';
20
- import { ABSENT_TARGET, absentBinary, absentReference, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
21
+ import { ABSENT_TARGET, absentBinary, attachAnswer, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, OFFLINE, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
21
22
  import { createRecovery } from './recovery.js';
22
- import { doorReads } from './reads.js';
23
23
  import { advancement } from './refresh.js';
24
24
  import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
25
- import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, sameMembership, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
26
- import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
25
+ import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
26
+ import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, readSavedParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
27
27
  import { same, saving, STARTING, transferring, } from './status.js';
28
- import { detectStorageLoss, nextGeneration } from './durability.js';
29
- import { readinessOf, SETTLED_ROOMS, } from './readiness.js';
28
+ import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
29
+ import { readinessOf, } from './readiness.js';
30
30
  import { FencedMap } from './fence.js';
31
- import { fingerprint } from './stored.js';
32
31
  import { sameBinary } from './overlay.js';
33
32
  import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
34
33
  import { identityTime, roomDraftRevision } from './identity.js';
35
- import { beneath, normalize } from './paths.js';
34
+ import { ancestors, beneath, normalize } from './paths.js';
36
35
  import { openRuntimeStore, } from './store.js';
37
- import { changedCleanly, sameComposition, validatedCollection, } from './validation.js';
36
+ import { validatedNeighbourhood } from './validation.js';
38
37
  import { unconfirmed } from './transfer.js';
39
38
  import { governs, holds, narrowed, planReach, } from './reach.js';
40
- function roomsAlone(plan) {
41
- return (plan.reach === null &&
42
- plan.root.types === null &&
43
- plan.root.where === null &&
44
- plan.root.pathPrefix === null &&
45
- plan.paths === null &&
46
- plan.hop === null &&
47
- plan.backlinks === null);
48
- }
49
39
  function within(root, path) {
50
40
  return root === '' || beneath(root, path);
51
41
  }
@@ -63,39 +53,46 @@ const OPENING_LANE = 'opening';
63
53
  const CONVERSATION_LANE = 'conversations';
64
54
  const ROOM_READS = 256;
65
55
  const ROOM_ATTEMPTS = 16;
66
- const SEARCH_BATCH = 256;
67
56
  const ERASE_BATCH = 512;
68
- const REDERIVE_BATCH = 256;
69
- 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;
70
63
  const QUERY_BYTES = 1 << 20;
71
64
  const STAGE_BYTES = 1 << 20;
72
- const CLEAN_CHANGES = 2_000;
65
+ const STAGE_FLOOR = 1 << 16;
66
+ // A batch aims at this much database work, so a Save or a query waits behind at most one short write.
67
+ const STAGE_AIM_MS = 50;
68
+ const STREAMS_HELD = 4;
69
+ const NOTHING_HELD = { bytes: 0 };
70
+ const QUEUED_TEXT = 1 << 23;
71
+ // Bytes still arriving are reported at most this often, so readiness is not rebuilt for every chunk the network hands over while a slow link still moves the figure.
72
+ const ARRIVAL_REPORT_MS = 250;
73
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.
74
74
  const LOCK_GRACE_MS = 120_000;
75
75
  function operationOf(input) {
76
76
  const operation = input?.operation_id;
77
77
  return typeof operation === 'string' ? operation : null;
78
78
  }
79
- function abandonedOn(signal) {
80
- return new Promise((resolve) => {
81
- if (signal.aborted)
82
- resolve(null);
83
- else
84
- 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));
85
86
  });
86
87
  }
87
- function roomLane(locator) {
88
- return `room ${locator}`;
89
- }
90
88
  function pendingKey(locator, id) {
91
89
  return `${locator}\u0000${id}`;
92
90
  }
93
91
  export function composeRuntime(options) {
94
92
  const localSaves = options.adapter.localSaves === true;
95
- const roomTypes = roomTypesOf(options.adapter.roomTypes);
93
+ const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
96
94
  let authorization = options.authorization;
97
95
  let opening;
98
- let store;
99
96
  const fences = new Map();
100
97
  const isFenced = (locator) => fences.has(locator);
101
98
  let fencings = 0;
@@ -103,44 +100,51 @@ export function composeRuntime(options) {
103
100
  const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
104
101
  let discoveries = 0;
105
102
  const replicas = new FencedMap(isFenced);
103
+ const reopened = new Set();
104
+ const usableWhenHeld = options.usableWhenHeld ?? [];
106
105
  const queuesWithoutReplica = new FencedMap(isFenced);
107
106
  const pendings = new Map();
108
107
  const seeds = new Map();
109
108
  const seedWatchers = new Map();
110
109
  const roomDrafts = new FencedMap(isFenced);
110
+ const attachedFiles = new FencedMap(isFenced);
111
111
  const presentations = new Map();
112
112
  let limits = [];
113
113
  let capabilities = [];
114
114
  const rooms = new FencedMap(isFenced);
115
115
  const counted = new Map();
116
116
  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);
117
+ const builtIn = builtInModel(roomTypes);
118
+ const models = new FencedMap(isFenced);
119
+ const liveModels = new FencedMap(isFenced);
120
120
  const invalidModels = new Set();
121
121
  const discovered = new Map();
122
- const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
122
+ let inFlight = new AbortController();
123
+ const refreshed = options.adapter.refresh?.bind(options.adapter);
124
+ const reads = {
125
+ bootstrap: (authorized, request) => readUntil(options.adapter.bootstrap(authorized, request), inFlight.signal),
126
+ ...(refreshed === undefined
127
+ ? {}
128
+ : {
129
+ refresh: (authorized, request) => readUntil(refreshed(authorized, request), inFlight.signal),
130
+ }),
131
+ reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
132
+ };
123
133
  let roomsListed = false;
124
134
  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;
135
+ function modelOf(locator) {
136
+ return models.get(locator) ?? NO_MODEL;
132
137
  }
133
- function savedModel(locator, attached) {
134
- return invalidModels.has(modelKey(locator, attached))
138
+ function savedModel(locator) {
139
+ return invalidModels.has(locator)
135
140
  ? { status: 'unavailable', reason: 'revoked' }
136
- : modelOf(locator, attached);
141
+ : modelOf(locator);
142
+ }
143
+ function composed(record) {
144
+ return compositionOf(record, builtIn);
137
145
  }
138
146
  function compositionFor(record) {
139
- return compositionOf({
140
- ...record,
141
- model: savedModel(record.locator, 'collection'),
142
- selectionModel: savedModel(record.locator, 'typed-selection'),
143
- });
147
+ return composed({ ...record, model: savedModel(record.locator) });
144
148
  }
145
149
  let refusal = null;
146
150
  let storageLoss = null;
@@ -150,10 +154,10 @@ export function composeRuntime(options) {
150
154
  let closed = false;
151
155
  let stopping = false;
152
156
  let withdrawStream;
153
- const announced = new Map();
154
157
  let underway = null;
155
158
  let streamed = null;
156
159
  let handed = false;
160
+ let naming = 0;
157
161
  const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
158
162
  const repairing = new Set();
159
163
  const calling = new Set();
@@ -165,14 +169,16 @@ export function composeRuntime(options) {
165
169
  const stale = new Set();
166
170
  let generation = 0;
167
171
  let discovering = null;
168
- let opened = null;
169
- const owed = new Set();
170
- const reloadsDue = new Map();
172
+ const reloadsDue = new Set();
171
173
  const lanes = new Map();
172
- let roomReads = { generation, free: ROOM_READS, waiting: [] };
174
+ const roomLanes = new Map();
175
+ let roomWork = 0;
176
+ const roomReads = { free: ROOM_READS, waiting: [] };
173
177
  const working = new Set();
174
178
  let mutations = Promise.resolve();
179
+ let gathering = null;
175
180
  const roomListeners = new Set();
181
+ const unlistedRooms = new Set();
176
182
  const refusalListeners = new Set();
177
183
  const storageLossListeners = new Set();
178
184
  const statusListeners = new Set();
@@ -192,19 +198,53 @@ export function composeRuntime(options) {
192
198
  const serviceModeListeners = new Set();
193
199
  let member = null;
194
200
  const memberListeners = new Set();
195
- const prepared = new Set();
196
201
  const unpreparedDemand = new Set();
197
- const unindexable = new Set();
202
+ const unpreparable = new Set();
198
203
  let preparing = false;
199
204
  let targets = {};
200
205
  let persistence = options.shell.persistence;
201
- let capacity = null;
202
- const paused = new Set();
203
- let rechecking = null;
206
+ let full = false;
207
+ let probing = null;
208
+ let testing = null;
209
+ const database = {
210
+ async ask(request) {
211
+ try {
212
+ return await options.database.ask(request);
213
+ }
214
+ catch (error) {
215
+ if (refusedForSpace(error))
216
+ filled();
217
+ throw error;
218
+ }
219
+ },
220
+ };
204
221
  const readinessWatches = new Set();
205
222
  let readinessQueued = false;
206
223
  const catching = new Set();
207
224
  const transfers = new FencedMap(isFenced);
225
+ const installs = new Set();
226
+ const arrivals = new Map();
227
+ const streaming = new Set();
228
+ const underived = new Map();
229
+ const unindexed = new Map();
230
+ const startedOver = new Map();
231
+ let arrivalReported = 0;
232
+ let streamsFree = STREAMS_HELD;
233
+ let queuedText = 0;
234
+ const installWaiters = [];
235
+ const stageBudget = {
236
+ bytes: STAGE_FLOOR * 4,
237
+ floor: STAGE_FLOOR,
238
+ ceiling: STAGE_BYTES,
239
+ aim: STAGE_AIM_MS,
240
+ };
241
+ const deriveBudget = {
242
+ bytes: DERIVE_FLOOR * 4,
243
+ floor: DERIVE_FLOOR,
244
+ ceiling: DERIVE_BYTES,
245
+ aim: DERIVE_AIM_MS,
246
+ };
247
+ const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
208
248
  const conversations = createConversationStore({
209
249
  agents: declaredAgents,
210
250
  documents: (declaration, after) => rowsQuery(planOf({
@@ -225,40 +265,27 @@ export function composeRuntime(options) {
225
265
  stream: () => options.adapter.conversations,
226
266
  publish: publishChangeSet,
227
267
  online: () => connectionOf() !== 'unreachable' && refusal === null,
268
+ listed: () => roomsListed,
269
+ full: () => full,
228
270
  closed: () => closed,
229
271
  unanswered: resendLater,
230
272
  expired: (operation) => pastHorizon(operation),
231
273
  });
232
- const searchIndex = createSearchIndex({
233
- async *room(room) {
234
- const local = await openLocal();
235
- for (let after = null;;) {
236
- const key = replicas.get(room)?.record.key;
237
- if (key === undefined)
238
- return;
239
- const page = await local.searched(key, room, after, SEARCH_BATCH, null);
240
- yield page.documents;
241
- if (page.next === null)
242
- return;
243
- after = page.next;
244
- }
245
- },
246
- async documents(room, paths) {
247
- const key = replicas.get(room)?.record.key;
248
- if (key === undefined || paths.length === 0)
249
- return [];
250
- return (await (await openLocal()).searched(key, room, null, paths.length, paths)).documents;
251
- },
252
- });
253
- const roomsToIndex = new Set();
254
- const conversationsToIndex = new Set();
255
- let indexing = false;
256
274
  const journal = createLog();
257
- const keptFiles = openedFiles(options.database, isFenced);
258
- let notifyReady = () => { };
275
+ const keptFiles = openedFiles(database, isFenced);
276
+ let readyFromStore = () => { };
259
277
  const ready = new Promise((resolve) => {
260
- notifyReady = resolve;
278
+ readyFromStore = resolve;
261
279
  });
280
+ let notifyDiscovered = () => { };
281
+ const firstDiscovery = new Promise((resolve) => {
282
+ notifyDiscovered = resolve;
283
+ });
284
+ let roomsNamed = false;
285
+ function notifyReady() {
286
+ notifyDiscovered();
287
+ readyFromStore();
288
+ }
262
289
  function rememberRoomDraft(record) {
263
290
  const room = roomDrafts.get(record.locator) ?? new Map();
264
291
  room.set(record.path, record);
@@ -278,7 +305,7 @@ export function composeRuntime(options) {
278
305
  return undefined;
279
306
  const local = await openLocal();
280
307
  const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
281
- return { ...slice, collection: collectionOf(slice.documents, compositionOf(held.record)) };
308
+ return { ...slice, collection: collectionOf(slice.documents, composed(held.record)) };
282
309
  }
283
310
  function roomDraftOf(record, visible) {
284
311
  return structuredClone({
@@ -334,14 +361,40 @@ export function composeRuntime(options) {
334
361
  const value = presentations.get(app);
335
362
  return value === undefined ? null : structuredClone(value);
336
363
  }
337
- function keepPresentation(app, next) {
338
- return exclusive(async () => {
339
- const local = await openLocal();
340
- const stored = next(presentations.get(app));
341
- await local.present({ app, value: stored });
342
- presentations.set(app, stored);
343
- publishChangeSet(changeSet('', { presentation: app }));
344
- });
364
+ async function writtenRoomDraft(room, path, revision, input, next) {
365
+ const parts = readParts(input);
366
+ if (path === '' || normalize(path) !== path)
367
+ throw runtimeError('room-draft-malformed');
368
+ const local = await openLocal();
369
+ const current = roomDrafts.get(room)?.get(path);
370
+ if ((current?.revision ?? null) !== revision) {
371
+ return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
372
+ }
373
+ const file = parts.file === undefined
374
+ ? undefined
375
+ : await heldFile(parts.file.blob, parts.file.base === undefined
376
+ ? binaryBase(await visibleOf(room, [path]), path)
377
+ : parts.file.base);
378
+ const record = {
379
+ locator: room,
380
+ path,
381
+ revision: next,
382
+ parts: held(current?.parts, parts, file),
383
+ returned: current?.returned ?? null,
384
+ };
385
+ await local.keepRoomDraft(record, parts.file === undefined || file === undefined
386
+ ? []
387
+ : [{ key: file.localId, blob: parts.file.blob }]);
388
+ rememberRoomDraft(record);
389
+ publishChangeSet(changeSet(room, { drafts: [path] }));
390
+ return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
391
+ }
392
+ async function keptPresentation(app, next) {
393
+ const local = await openLocal();
394
+ const stored = next(presentations.get(app));
395
+ await local.present({ app, value: stored });
396
+ presentations.set(app, stored);
397
+ publishChangeSet(changeSet('', { presentation: app }));
345
398
  }
346
399
  function emitPresentation(app) {
347
400
  if (closed)
@@ -356,6 +409,20 @@ export function composeRuntime(options) {
356
409
  (queuesWithoutReplica.size > 0 ||
357
410
  [...replicas.values()].some((replica) => replica.queue.length > 0)));
358
411
  }
412
+ function unsent() {
413
+ const changed = new Map();
414
+ for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
415
+ for (const { batch, state } of entriesOf(room)) {
416
+ if (state === 'local')
417
+ continue;
418
+ for (const { path } of batch.changes)
419
+ changed.set(`${room}\u0000${path}`, { room, path });
420
+ }
421
+ }
422
+ return [...changed]
423
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
424
+ .map(([, change]) => change);
425
+ }
359
426
  function attention() {
360
427
  const attentionRequired = [];
361
428
  for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
@@ -367,16 +434,9 @@ export function composeRuntime(options) {
367
434
  returned: structuredClone(record.returned),
368
435
  });
369
436
  }
370
- for (const [room, refusal] of sourceRefusals)
371
- attentionRequired.push({ room, refusal: structuredClone(refusal) });
372
437
  attentionRequired.sort((left, right) => {
373
438
  if (left.room !== right.room)
374
439
  return left.room < right.room ? -1 : 1;
375
- if ('refusal' in left || 'refusal' in right) {
376
- if ('refusal' in left && 'refusal' in right)
377
- return 0;
378
- return 'refusal' in left ? -1 : 1;
379
- }
380
440
  if (left.path !== right.path)
381
441
  return left.path < right.path ? -1 : 1;
382
442
  return 0;
@@ -388,7 +448,7 @@ export function composeRuntime(options) {
388
448
  if (reset === null || submission.reset < reset)
389
449
  reset = submission.reset;
390
450
  }
391
- return { attentionRequired, reset };
451
+ return { attentionRequired, unsent: unsent(), reset };
392
452
  }
393
453
  function documentsIn(locator) {
394
454
  return replicas.get(locator)?.count ?? 0;
@@ -461,7 +521,7 @@ export function composeRuntime(options) {
461
521
  const governed = governs(reach, room);
462
522
  const attached = [
463
523
  ...(governed.collection ? [modelOf(room.locator)] : []),
464
- ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
524
+ ...(governed.selection ? [builtIn] : []),
465
525
  ];
466
526
  return {
467
527
  locator: room.locator,
@@ -477,11 +537,26 @@ export function composeRuntime(options) {
477
537
  const value = listed();
478
538
  for (const listener of roomListeners)
479
539
  listener(value);
540
+ emitKnownRooms();
541
+ }
542
+ function emitKnownRooms() {
543
+ conversations.learned();
544
+ if (unlistedRooms.size === 0)
545
+ return;
546
+ if (!roomsListed && rooms.size === 0 && refusal === null && !status.providerUnreachable)
547
+ return;
548
+ const value = listed();
549
+ for (const listener of [...unlistedRooms]) {
550
+ unlistedRooms.delete(listener);
551
+ roomListeners.add(listener);
552
+ listener(value);
553
+ }
480
554
  }
481
555
  function emitRefusal() {
482
556
  queueReadiness();
483
557
  for (const listener of refusalListeners)
484
558
  listener(refusal);
559
+ emitKnownRooms();
485
560
  }
486
561
  function emitStorageLoss() {
487
562
  queueReadiness();
@@ -502,6 +577,7 @@ export function composeRuntime(options) {
502
577
  if (closed)
503
578
  return;
504
579
  queueReadiness();
580
+ emitKnownRooms();
505
581
  const next = status.state === 'synced' && !conversations.current()
506
582
  ? { ...status, state: 'syncing' }
507
583
  : status;
@@ -515,9 +591,6 @@ export function composeRuntime(options) {
515
591
  if (localSaves)
516
592
  return Object.freeze({ batch, state: 'local' });
517
593
  const state = pendings.get(pendingKey(locator, batch.id));
518
- if (state?.accepted !== undefined) {
519
- return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
520
- }
521
594
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
522
595
  }
523
596
  function entriesOf(locator) {
@@ -532,23 +605,29 @@ export function composeRuntime(options) {
532
605
  }
533
606
  function applyChangeSet(set) {
534
607
  liveQueries.touched(set);
535
- searchIndex.changed(set);
536
608
  }
537
609
  function unpublished(error) {
538
610
  journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
539
611
  }
612
+ function indexed(locator) {
613
+ return replicas.get(locator)?.indexed === true;
614
+ }
540
615
  async function prepareReplica(locator) {
541
616
  for (;;) {
542
617
  const held = replicas.get(locator);
543
- if (held === undefined || closed || prepared.has(locator))
618
+ if (held === undefined || closed || held.indexed)
544
619
  return;
545
620
  const key = held.record.key;
546
- const done = await exclusive(async () => (await openLocal()).rederive(key, REDERIVE_BATCH));
547
- if (replicas.get(locator)?.record.key !== key)
621
+ const done = await exclusive(async () => {
622
+ const local = await openLocal();
623
+ return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR));
624
+ });
625
+ const now = replicas.get(locator);
626
+ if (now?.record.key !== key)
548
627
  continue;
549
- if (!done)
628
+ if (!done.done)
550
629
  continue;
551
- prepared.add(locator);
630
+ replicas.set(locator, { ...now, indexed: true });
552
631
  publishChangeSet(changeSet(locator, { documents: null }));
553
632
  liveQueries.recover();
554
633
  queueReadiness();
@@ -556,11 +635,11 @@ export function composeRuntime(options) {
556
635
  }
557
636
  }
558
637
  function unprepared() {
559
- return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
638
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
560
639
  }
561
- function unbuilt(locator, error) {
562
- unindexable.add(locator);
563
- journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
640
+ function unpreparedBy(locator, error) {
641
+ unpreparable.add(locator);
642
+ journal.record(`preparation failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
564
643
  queueReadiness();
565
644
  }
566
645
  function prepare(locators = []) {
@@ -576,7 +655,7 @@ export function composeRuntime(options) {
576
655
  if (closed)
577
656
  return;
578
657
  unpreparedDemand.delete(waiting);
579
- await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
658
+ await prepareReplica(waiting).catch((error) => unpreparedBy(waiting, error));
580
659
  }
581
660
  }
582
661
  finally {
@@ -585,7 +664,11 @@ export function composeRuntime(options) {
585
664
  })();
586
665
  }
587
666
  function roomsOfReach(reach) {
588
- return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
667
+ return new Set([
668
+ ...reach.rooms,
669
+ ...reach.documents.map(roomOfKey),
670
+ ...reach.areas.flatMap((area) => (area.types.length === 0 ? [] : [area.room])),
671
+ ]);
589
672
  }
590
673
  function reachedRooms(plan) {
591
674
  const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
@@ -617,14 +700,14 @@ export function composeRuntime(options) {
617
700
  pending.push({ room: locator, awaits: 'acquisition' });
618
701
  continue;
619
702
  }
620
- if (!prepared.has(locator)) {
703
+ if (!replica.indexed) {
621
704
  pending.push({ room: locator, awaits: 'preparation' });
622
705
  continue;
623
706
  }
624
707
  coverage.push({
625
708
  room: locator,
626
709
  generation: replica.record.transfer,
627
- current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
710
+ current: !(catching.has(locator) || transfers.has(locator)),
628
711
  });
629
712
  read.push({ locator, key: replica.record.key });
630
713
  for (const batch of replica.queue)
@@ -650,9 +733,7 @@ export function composeRuntime(options) {
650
733
  return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
651
734
  return { listed: named.every((room) => replicas.has(room)), rooms: named };
652
735
  }
653
- function roomState(locator, search) {
654
- if (sourceRefusals.has(locator))
655
- return 'refused';
736
+ function roomState(locator) {
656
737
  const listedRoom = discovered.get(locator);
657
738
  if (roomsListed &&
658
739
  (listedRoom === undefined ||
@@ -661,27 +742,17 @@ export function composeRuntime(options) {
661
742
  return 'unadmitted';
662
743
  if (!replicas.has(locator))
663
744
  return 'acquiring';
664
- if (!prepared.has(locator))
745
+ if (!indexed(locator))
665
746
  return 'preparing';
666
- if (search && !searchIndex.rooms().has(locator))
667
- return 'indexing';
668
747
  return 'complete';
669
748
  }
670
- function conversationCoverage(reach, search) {
749
+ function conversationCoverage(reach) {
671
750
  const declared = reach === null || reach.conversations;
672
751
  if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
673
752
  return 'undeclared';
674
753
  if (!conversations.known() || !conversations.current())
675
754
  return 'acquiring';
676
- if (!search)
677
- return 'complete';
678
- const unindexed = conversations
679
- .searchable()
680
- .filter((one) => conversations.acquired(one) && !searchIndex.conversations().has(one));
681
- if (unindexed.length === 0)
682
- return 'complete';
683
- index([], unindexed);
684
- return 'indexing';
755
+ return 'complete';
685
756
  }
686
757
  function connectionOf() {
687
758
  if (socketOpen)
@@ -692,58 +763,79 @@ export function composeRuntime(options) {
692
763
  ? 'connected'
693
764
  : 'connecting';
694
765
  }
695
- function refusedIn(locators) {
696
- return locators.flatMap((room) => {
697
- const held = sourceRefusals.get(room);
698
- if (held === undefined)
699
- return [];
700
- const { file, location } = held.fields;
701
- return [
702
- {
703
- room,
704
- file: typeof file === 'string' ? file : '',
705
- rule: held.code,
706
- location: typeof location === 'string' ? location : null,
707
- },
708
- ];
709
- });
766
+ function bytesOf(room, underway) {
767
+ const transfer = transfers.get(room);
768
+ if (transfer !== undefined) {
769
+ const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
770
+ return {
771
+ done: transfer.done.bytes - Math.ceil(owed),
772
+ total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
773
+ };
774
+ }
775
+ const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
776
+ return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
777
+ }
778
+ function installationOf(coverage) {
779
+ const rooms = { done: 0, total: 0 };
780
+ const bytes = { done: 0, total: 0 };
781
+ let installing = 0;
782
+ let downloading = false;
783
+ for (const { room, state } of coverage) {
784
+ if (state === 'unadmitted')
785
+ continue;
786
+ rooms.total += 1;
787
+ if (state === 'complete')
788
+ rooms.done += 1;
789
+ const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
790
+ if (underway)
791
+ installing += 1;
792
+ if (underway && (streaming.has(room) || !arrivals.has(room)))
793
+ downloading = true;
794
+ const now = bytesOf(room, underway);
795
+ const earlier = underway ? startedOver.get(room) : undefined;
796
+ bytes.done += Math.max(now.done, earlier?.done ?? 0);
797
+ bytes.total += Math.max(now.total, earlier?.total ?? 0);
798
+ }
799
+ const step = installing === 0
800
+ ? null
801
+ : downloading
802
+ ? 'downloading'
803
+ : bytes.done < bytes.total
804
+ ? 'writing'
805
+ : 'replaying';
806
+ return { step, rooms, bytes };
710
807
  }
711
808
  function readinessFor(port, reach) {
712
809
  const required = requiredRooms(reach);
713
810
  const search = port.search === 'required';
714
- const coverage = required.rooms.map((room) => ({ room, state: roomState(room, search) }));
715
- const building = (state) => coverage
716
- .filter((room) => room.state === state && !unindexable.has(room.room))
811
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
812
+ const preparing = coverage
813
+ .filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
717
814
  .map(({ room }) => room);
718
- const preparing = building('preparing');
719
815
  if (preparing.length > 0)
720
816
  prepare(preparing);
721
- const indexing = building('indexing');
722
- if (indexing.length > 0)
723
- index(indexing, []);
724
817
  return readinessOf({
725
818
  code: port.code,
726
819
  coverage: {
727
820
  listed: required.listed,
728
821
  rooms: coverage,
729
- conversations: conversationCoverage(reach, search),
822
+ conversations: conversationCoverage(reach),
730
823
  search: search ? 'required' : 'unused',
731
824
  },
732
825
  persistence,
733
826
  connection: connectionOf(),
734
827
  house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
735
- capacity: capacity !== null &&
736
- coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
737
- ? capacity
738
- : port.capacity,
739
- refused: refusedIn(required.rooms),
828
+ space: full ? 'full' : port.space,
829
+ installation: installationOf(coverage),
740
830
  lost: storageLoss !== null,
741
- established: targets[port.target]?.established === true,
831
+ established: targets[port.target]?.established === true ||
832
+ (usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
742
833
  });
743
834
  }
744
835
  function establish(name) {
745
836
  targets = { ...targets, [name]: { established: true } };
746
837
  queueReadiness();
838
+ probeEachMinute();
747
839
  void durably((local) => local.fact('targets', targets)).catch(() => {
748
840
  journal.record(`target record failed ${name}`);
749
841
  });
@@ -781,87 +873,52 @@ export function composeRuntime(options) {
781
873
  readinessWatches.delete(watch);
782
874
  };
783
875
  }
784
- function pageBytes(page) {
785
- let bytes = 0;
786
- for (const { path, content } of page.entries)
787
- bytes += (path.length + (content?.length ?? 0)) * 2;
788
- return bytes;
789
- }
790
- function exceeded(error) {
791
- return (typeof error === 'object' &&
792
- error !== null &&
793
- error.name === 'QuotaExceededError');
794
- }
795
- async function estimated() {
796
- const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
797
- return estimate === null ? null : Math.max(0, estimate.total - estimate.usage);
798
- }
799
- function pause(locator, cause) {
800
- paused.add(locator);
801
- capacity = cause;
802
- journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
876
+ function filled() {
877
+ if (full || closed)
878
+ return;
879
+ full = true;
880
+ journal.record('space full');
803
881
  queueReadiness();
804
- if (rechecking !== null)
882
+ probeEachMinute();
883
+ }
884
+ function installedTarget() {
885
+ return ((usableWhenHeld.length > 0 && reopened.size > 0) ||
886
+ Object.values(targets).some((target) => target.established));
887
+ }
888
+ function probeEachMinute() {
889
+ if (!full || probing !== null || closed || !installedTarget())
805
890
  return;
806
- // A paused preparation measures the browser's space again each minute, so space freed elsewhere resumes it without a retry.
807
- rechecking = setInterval(() => {
808
- void recheck();
891
+ // A device whose storage filled after its install tries a write each minute, so space freed elsewhere ends its read-only mode without a Retry.
892
+ probing = setInterval(() => {
893
+ void probed();
809
894
  }, 60_000);
810
895
  }
811
- async function recheck() {
812
- const held = capacity;
813
- if (held === null || closed)
814
- return;
815
- const available = await estimated().catch(() => null);
816
- if (available === null || available < (held.needed ?? 0))
817
- return;
818
- if (held.cause === 'storage-exceeded' &&
819
- (held.available === null || available <= held.available))
820
- return;
821
- resume();
896
+ function probed() {
897
+ testing ??= database.ask({ kind: 'space.probe' }).then(() => {
898
+ testing = null;
899
+ spaceFreed();
900
+ return true;
901
+ }, () => {
902
+ testing = null;
903
+ return false;
904
+ });
905
+ return testing;
822
906
  }
823
- function resume() {
824
- if (rechecking !== null)
825
- clearInterval(rechecking);
826
- rechecking = null;
827
- if (paused.size === 0 && capacity === null)
907
+ function spaceFreed() {
908
+ if (!full)
828
909
  return;
829
- paused.clear();
830
- capacity = null;
831
- journal.record('capacity resumed');
910
+ full = false;
911
+ if (probing !== null)
912
+ clearInterval(probing);
913
+ probing = null;
914
+ unpreparable.clear();
915
+ journal.record('space freed');
832
916
  queueReadiness();
833
917
  if (!closed && authorization !== undefined)
834
918
  void sync('unstreamed');
835
919
  }
836
- async function reclaimed(local) {
837
- const unreferenced = [...transfers.keys()].filter((locator) => !discovered.has(locator));
838
- for (const locator of unreferenced) {
839
- await local.abandon(locator);
840
- transfers.delete(locator);
841
- }
842
- if (unreferenced.length > 0)
843
- journal.record(`capacity reclaimed ${unreferenced.length}`);
844
- return unreferenced.length > 0;
845
- }
846
- async function admitted(local, locator, needed) {
847
- if (paused.has(locator))
848
- return false;
849
- let available = await estimated().catch(() => null);
850
- if (available === null || available >= needed)
851
- return true;
852
- if (await reclaimed(local))
853
- available = await estimated().catch(() => null);
854
- if (available === null || available >= needed)
855
- return true;
856
- pause(locator, { cause: 'insufficient-space', needed, available });
857
- return false;
858
- }
859
- async function exceededOn(locator, bytes) {
860
- const available = await estimated().catch(() => null);
861
- pause(locator, { cause: 'storage-exceeded', needed: bytes, available });
862
- }
863
920
  function seedRoomHeld(room, version) {
864
- return prepared.has(room) && reachedBy(room, version);
921
+ return indexed(room) && reachedBy(room, version);
865
922
  }
866
923
  function seedHeld(at) {
867
924
  return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
@@ -982,83 +1039,12 @@ export function composeRuntime(options) {
982
1039
  const held = answered.cover;
983
1040
  return { ...answered.rows, coverage: held.coverage };
984
1041
  }
985
- async function everyRow(plan) {
986
- const window = { ...plan, limit: MAXIMUM_WINDOW, after: null, total: false };
987
- const first = await rowsQuery(window);
988
- if ('pending' in first || first.next === undefined)
989
- return first;
990
- const documents = 'documents' in first ? [...first.documents] : [];
991
- const groups = 'groups' in first ? [...first.groups] : [];
992
- for (let next = first.next; next !== undefined;) {
993
- const values = JSON.parse(next);
994
- const page = await rowsQuery({
995
- ...window,
996
- after: { text: next, values: values.length === 0 ? null : values },
997
- });
998
- if ('pending' in page)
999
- return page;
1000
- if ('documents' in page)
1001
- documents.push(...page.documents);
1002
- else
1003
- groups.push(...page.groups);
1004
- if (JSON.stringify(page.coverage) !== JSON.stringify(first.coverage))
1005
- return everyRow(plan);
1006
- next = page.next;
1007
- }
1008
- return 'documents' in first
1009
- ? { documents, coverage: first.coverage }
1010
- : { groups, coverage: first.coverage };
1011
- }
1012
- async function indexConversation(conversation) {
1013
- searchIndex.indexMessages(conversation, [], false);
1014
- let after = null;
1015
- for (;;) {
1016
- const batch = await durably((local) => local.messagesAfter(conversation, after));
1017
- if (closed)
1018
- return;
1019
- searchIndex.indexMessages(conversation, batch.map(({ identity, text }) => ({ conversation, identity, text })), batch.length === 0);
1020
- if (batch.length === 0)
1021
- return;
1022
- after = batch[batch.length - 1].identity;
1023
- }
1024
- }
1025
- function indexNext() {
1026
- const room = [...roomsToIndex].find((locator) => !searchIndex.rooms().has(locator) && !unindexable.has(locator));
1027
- if (room !== undefined) {
1028
- roomsToIndex.delete(room);
1029
- return () => prepared.has(room)
1030
- ? searchIndex.indexRoom(room).catch((error) => unbuilt(room, error))
1031
- : undefined;
1032
- }
1033
- const conversation = [...conversationsToIndex].find((one) => !searchIndex.conversations().has(one));
1034
- if (conversation === undefined)
1035
- return null;
1036
- conversationsToIndex.delete(conversation);
1037
- return () => indexConversation(conversation);
1038
- }
1039
- function index(rooms, named) {
1040
- for (const room of rooms)
1041
- roomsToIndex.add(room);
1042
- for (const conversation of named)
1043
- conversationsToIndex.add(conversation);
1044
- if (indexing)
1045
- return;
1046
- indexing = true;
1047
- void (async () => {
1048
- try {
1049
- for (let unit = indexNext(); unit !== null && !closed; unit = indexNext()) {
1050
- await yielded();
1051
- await unit();
1052
- queueReadiness();
1053
- }
1054
- }
1055
- finally {
1056
- indexing = false;
1057
- }
1058
- })().catch(() => { });
1059
- }
1060
1042
  function publishChangeSet(set) {
1061
1043
  applyChangeSet(set);
1044
+ for (const { locator, path } of set.documents) {
1045
+ if (path !== null)
1046
+ attachedFiles.get(locator)?.delete(path);
1047
+ }
1062
1048
  if (set.rooms.length > 0 ||
1063
1049
  [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
1064
1050
  emitRooms();
@@ -1077,6 +1063,8 @@ export function composeRuntime(options) {
1077
1063
  }
1078
1064
  async function refuse(reason, provider = null) {
1079
1065
  discoveryPaused = true;
1066
+ inFlight.abort();
1067
+ inFlight = new AbortController();
1080
1068
  try {
1081
1069
  if (reason === 'revoked-authorization') {
1082
1070
  await exclusive(async () => {
@@ -1084,27 +1072,24 @@ export function composeRuntime(options) {
1084
1072
  const records = [...replicas].map(([locator, held]) => ({
1085
1073
  locator,
1086
1074
  held,
1087
- record: { ...held.record, model: revoked, selectionModel: revoked },
1075
+ record: { ...held.record, model: revoked },
1088
1076
  }));
1077
+ let answer = null;
1089
1078
  try {
1090
1079
  const local = await openLocal();
1091
- await local.commit({
1080
+ answer = await local.commit({
1092
1081
  rooms: records.map(({ locator, record }) => ({
1093
1082
  locator,
1094
1083
  key: record.key,
1095
1084
  record: storedRecord(record),
1096
- composition: compositionOf(record),
1085
+ composed: composedOf(composed(record)),
1097
1086
  })),
1098
1087
  });
1099
1088
  }
1100
1089
  catch {
1101
1090
  journal.record('model revocation persistence failed');
1102
1091
  }
1103
- const keys = new Set(models.keys());
1104
- for (const locator of rooms.keys()) {
1105
- keys.add(modelKey(locator, 'collection'));
1106
- keys.add(modelKey(locator, 'typed-selection'));
1107
- }
1092
+ const keys = new Set([...models.keys(), ...rooms.keys()]);
1108
1093
  for (const key of keys) {
1109
1094
  invalidModels.add(key);
1110
1095
  models.set(key, revoked);
@@ -1114,8 +1099,8 @@ export function composeRuntime(options) {
1114
1099
  for (const { locator, held, record } of records) {
1115
1100
  if (replicas.get(locator) !== held)
1116
1101
  continue;
1117
- recomposed(locator, held.record, record);
1118
- replicas.set(locator, { ...held, record });
1102
+ const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
1103
+ replicas.set(locator, { ...held, record, indexed });
1119
1104
  }
1120
1105
  for (const locator of [...rooms.keys()]) {
1121
1106
  const room = rooms.get(locator);
@@ -1159,12 +1144,12 @@ export function composeRuntime(options) {
1159
1144
  ? null
1160
1145
  : {
1161
1146
  roots: selection.roots,
1162
- model: modelTypes(modelOf(room.locator, 'typed-selection')),
1147
+ model: modelTypes(builtIn),
1163
1148
  },
1164
1149
  providerOwnedPaths: room.providerOwnedPaths,
1165
1150
  roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
1166
1151
  synchronizationAvailable,
1167
- refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
1152
+ refusal: scopedRefusals.get(room.locator) ?? null,
1168
1153
  });
1169
1154
  }
1170
1155
  async function applyDiscovery(found, started, covered) {
@@ -1248,30 +1233,23 @@ export function composeRuntime(options) {
1248
1233
  queuesWithoutReplica.delete(locator);
1249
1234
  rooms.delete(locator);
1250
1235
  roomDrafts.delete(locator);
1251
- transfers.delete(locator);
1236
+ attachedFiles.delete(locator);
1237
+ untransferred(locator);
1238
+ startedOver.delete(locator);
1252
1239
  catching.delete(locator);
1253
1240
  discovered.delete(locator);
1254
- announced.delete(locator);
1255
1241
  unreadable.delete(locator);
1256
1242
  stale.delete(locator);
1257
1243
  repairable.delete(locator);
1258
- unindexable.delete(locator);
1259
- paused.delete(locator);
1260
- prepared.delete(locator);
1244
+ unpreparable.delete(locator);
1261
1245
  scopedRefusals.delete(locator);
1262
- sourceRefusals.delete(locator);
1263
- declarationRefusals.delete(locator);
1264
- for (const attached of ['collection', 'typed-selection']) {
1265
- const key = modelKey(locator, attached);
1266
- models.delete(key);
1267
- liveModels.delete(key);
1268
- invalidModels.delete(key);
1269
- }
1246
+ models.delete(locator);
1247
+ liveModels.delete(locator);
1248
+ invalidModels.delete(locator);
1270
1249
  journal.record(`room lost ${locator}`);
1271
1250
  }
1272
1251
  if (fresh.length === 0)
1273
1252
  return fresh;
1274
- searchIndex.forget(fresh);
1275
1253
  publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, { documents: null, room: true, queue: true, drafts }))));
1276
1254
  liveQueries.recover();
1277
1255
  moveTo({ ...status, ...attention() });
@@ -1315,7 +1293,14 @@ export function composeRuntime(options) {
1315
1293
  });
1316
1294
  await eraseFenced(fresh);
1317
1295
  }
1318
- function restored(key, persisted) {
1296
+ function retyped(persisted) {
1297
+ if (persisted.status === 'unavailable')
1298
+ return persisted;
1299
+ const { state } = attachedModel({ kind: 'model', snapshot: persisted.snapshot }, NO_MODEL, roomTypes);
1300
+ return state.status === 'unavailable' ? state : { ...persisted, types: state.types };
1301
+ }
1302
+ function restored(key, held) {
1303
+ const persisted = retyped(held);
1319
1304
  const prior = models.get(key);
1320
1305
  if (prior !== undefined &&
1321
1306
  prior.status !== 'unavailable' &&
@@ -1346,8 +1331,9 @@ export function composeRuntime(options) {
1346
1331
  const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1347
1332
  targets = held.targets;
1348
1333
  transfers.clear();
1349
- for (const [locator, record] of held.transfers)
1350
- transfers.set(locator, record);
1334
+ arrivals.clear();
1335
+ streaming.clear();
1336
+ startedOver.clear();
1351
1337
  limits = held.providerLimits;
1352
1338
  capabilities = held.capabilities;
1353
1339
  member = held.member;
@@ -1357,7 +1343,7 @@ export function composeRuntime(options) {
1357
1343
  presentations.clear();
1358
1344
  for (const record of held.presentation)
1359
1345
  presentations.set(record.app, record.value);
1360
- conversations.hydrate(heldConversations, chatDrafts);
1346
+ conversations.hydrate(heldConversations, chatDrafts, held.conversationList);
1361
1347
  pendings.clear();
1362
1348
  for (const queue of queues) {
1363
1349
  for (const { batch, state } of queue.pending) {
@@ -1365,7 +1351,7 @@ export function composeRuntime(options) {
1365
1351
  }
1366
1352
  }
1367
1353
  replicas.clear();
1368
- prepared.clear();
1354
+ reopened.clear();
1369
1355
  queuesWithoutReplica.clear();
1370
1356
  for (const [locator, queue] of queuedBatches) {
1371
1357
  if (!held.rooms.has(locator))
@@ -1374,39 +1360,40 @@ export function composeRuntime(options) {
1374
1360
  records.forEach((stored, index) => {
1375
1361
  if (fences.has(stored.locator))
1376
1362
  return;
1377
- const model = restored(modelKey(stored.locator, 'collection'), stored.model);
1378
- const selectionModel = restored(modelKey(stored.locator, 'typed-selection'), stored.selectionModel);
1379
- const record = { ...stored, model, selectionModel };
1363
+ const record = { ...stored, model: restored(stored.locator, stored.model) };
1364
+ reopened.add(record.locator);
1380
1365
  replicas.set(record.locator, {
1381
1366
  record,
1382
1367
  queue: queuedBatches.get(record.locator) ?? [],
1383
1368
  overlaid: held.overlaid.get(record.key) ?? new Set(),
1384
1369
  count: counts[index] ?? 0,
1385
1370
  confirmed: 0,
1371
+ indexed: !held.unindexed.has(record.key),
1386
1372
  });
1387
- if (!held.unprepared.has(record.key))
1388
- prepared.add(record.locator);
1389
1373
  const existing = rooms.get(record.locator);
1390
1374
  if (existing === undefined)
1391
- list(record, false);
1375
+ list(record, record.roomVersion !== null);
1392
1376
  else {
1393
1377
  rooms.set(record.locator, {
1394
1378
  ...existing,
1395
1379
  roomVersion: record.roomVersion,
1396
1380
  operations: record.operations,
1397
1381
  replica: record.replica,
1398
- model: modelTypes(model),
1382
+ model: modelTypes(record.model),
1399
1383
  typedSelection: record.typedSelection === null
1400
1384
  ? null
1401
- : {
1402
- roots: record.typedSelection.roots,
1403
- model: modelTypes(selectionModel),
1404
- },
1385
+ : { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
1405
1386
  });
1406
1387
  }
1407
1388
  });
1408
- for (const key of held.orphans)
1409
- await opened.release(key);
1389
+ try {
1390
+ for (const key of held.orphans)
1391
+ await opened.release(key);
1392
+ }
1393
+ catch (error) {
1394
+ if (!refusedForSpace(error))
1395
+ throw error;
1396
+ }
1410
1397
  if (held.erasing || held.orphans.length > 0)
1411
1398
  queueMicrotask(sweep);
1412
1399
  return {
@@ -1431,8 +1418,7 @@ export function composeRuntime(options) {
1431
1418
  async function open() {
1432
1419
  if (closed)
1433
1420
  throw new Error('runtime closed');
1434
- const opened = await openRuntimeStore(isFenced, options.database);
1435
- store = opened;
1421
+ const opened = openRuntimeStore(isFenced, database);
1436
1422
  const interrupted = await opened.fences();
1437
1423
  for (const locator of interrupted) {
1438
1424
  fences.set(locator, { at: discoveries, erased: false });
@@ -1478,10 +1464,29 @@ export function composeRuntime(options) {
1478
1464
  return opening;
1479
1465
  }
1480
1466
  function exclusive(work) {
1467
+ gathering = null;
1481
1468
  const next = mutations.then(work, work);
1482
1469
  mutations = next.then(() => undefined, () => undefined);
1483
1470
  return next;
1484
1471
  }
1472
+ function gathered(key, input, merge, work) {
1473
+ const tail = gathering;
1474
+ if (tail !== null && tail.key === key) {
1475
+ const merged = merge(tail.input, input);
1476
+ if (merged !== null) {
1477
+ tail.input = merged;
1478
+ return tail.done;
1479
+ }
1480
+ }
1481
+ const entry = { key, input, done: Promise.resolve() };
1482
+ entry.done = exclusive(() => {
1483
+ if (gathering === entry)
1484
+ gathering = null;
1485
+ return work(entry.input);
1486
+ });
1487
+ gathering = entry;
1488
+ return entry.done;
1489
+ }
1485
1490
  function withdrawable(locator) {
1486
1491
  return (batch) => {
1487
1492
  const state = pendings.get(pendingKey(locator, batch.id));
@@ -1563,26 +1568,14 @@ export function composeRuntime(options) {
1563
1568
  },
1564
1569
  };
1565
1570
  }
1566
- function recomposed(locator, before, after) {
1567
- if (before !== undefined &&
1568
- before.key === after.key &&
1569
- fingerprint(compositionOf(before)) === fingerprint(compositionOf(after))) {
1570
- return;
1571
- }
1572
- if (before !== undefined && before.key === after.key)
1573
- prepared.delete(locator);
1574
- }
1575
- function committedRoom(locator, record, answer) {
1576
- if (answer.prepared[locator] === false)
1577
- prepared.delete(locator);
1578
- else if (answer.prepared[locator] === true)
1579
- prepared.add(locator);
1580
- if (!prepared.has(locator))
1571
+ function committedRoom(locator, answer) {
1572
+ const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
1573
+ if (!indexed)
1581
1574
  prepare([locator]);
1582
- return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0 };
1575
+ return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
1583
1576
  }
1584
1577
  function replayed(record, confirmedLayer, queue) {
1585
- return stepped(replaying(confirmedLayer, queue, record.locator, compositionOf(record)));
1578
+ return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
1586
1579
  }
1587
1580
  async function overlayDiff(record, before, visible, touched) {
1588
1581
  const kept = before !== undefined && before.record.key === record.key
@@ -1602,8 +1595,6 @@ export function composeRuntime(options) {
1602
1595
  function unpairedIn(locator, batch) {
1603
1596
  if (!queueIn(locator).some((held) => held.id === batch.id))
1604
1597
  return false;
1605
- if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
1606
- return false;
1607
1598
  return !leavingIn(batch, locator) || withdrawable(locator)(batch);
1608
1599
  }
1609
1600
  async function together(local, settlements) {
@@ -1645,7 +1636,7 @@ export function composeRuntime(options) {
1645
1636
  const confirmedLayer = overrides === undefined
1646
1637
  ? read
1647
1638
  : await stepped(slicingWith(read, overrides.set, overrides.removed));
1648
- const composition = compositionOf(record);
1639
+ const composition = composed(record);
1649
1640
  const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
1650
1641
  const founded = kept.filter((batch) => !queue.includes(batch));
1651
1642
  const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
@@ -1688,6 +1679,7 @@ export function composeRuntime(options) {
1688
1679
  key: record.key,
1689
1680
  record: storedRecord(record),
1690
1681
  roots: record.typedSelection?.roots ?? [],
1682
+ composed: composedOf(composition),
1691
1683
  ...settling.commit,
1692
1684
  ...(overrides === undefined
1693
1685
  ? {}
@@ -1729,14 +1721,14 @@ export function composeRuntime(options) {
1729
1721
  const confirmedChanged = settling.commit?.published !== undefined ||
1730
1722
  settling.commit?.refreshed === true ||
1731
1723
  overrides !== undefined;
1732
- recomposed(locator, before?.record, record);
1733
- const { count } = committedRoom(locator, record, answer);
1724
+ const { count, indexed } = committedRoom(locator, answer);
1734
1725
  replicas.set(locator, {
1735
1726
  record,
1736
1727
  queue: kept,
1737
1728
  overlaid: touched,
1738
1729
  count,
1739
1730
  confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
1731
+ indexed,
1740
1732
  });
1741
1733
  return mergeChangeSets([
1742
1734
  changeSet(locator, {
@@ -1751,103 +1743,96 @@ export function composeRuntime(options) {
1751
1743
  };
1752
1744
  }
1753
1745
  function adoptModels(locator, record) {
1754
- for (const [attached, model] of [
1755
- ['collection', record.model],
1756
- ['typed-selection', record.selectionModel],
1757
- ]) {
1758
- const key = modelKey(locator, attached);
1759
- models.set(key, model);
1760
- invalidModels.delete(key);
1761
- if (model.status === 'current')
1762
- liveModels.set(key, JSON.stringify(model.snapshot));
1763
- else if (model.status === 'unavailable')
1764
- liveModels.delete(key);
1765
- }
1746
+ const model = record.model;
1747
+ models.set(locator, model);
1748
+ invalidModels.delete(locator);
1749
+ if (model.status === 'current')
1750
+ liveModels.set(locator, JSON.stringify(model.snapshot));
1751
+ else if (model.status === 'unavailable')
1752
+ liveModels.delete(locator);
1766
1753
  }
1767
1754
  function remodel(locator, answers, held) {
1768
- const taken = (answer, previous, attached) => {
1769
- if (answer === undefined)
1770
- return previous;
1771
- const { state, refused } = attachedModel(answer, previous, attached === 'collection' ? roomTypes : NO_ROOM_TYPES);
1772
- if (refused !== null) {
1773
- const where = refused.location === undefined ? '' : ` ${refused.location}`;
1774
- journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
1775
- }
1776
- models.set(modelKey(locator, attached), state);
1777
- return state;
1778
- };
1779
- return {
1780
- collection: taken(answers?.collection, held.collection, 'collection'),
1781
- selection: taken(answers?.selection, held.selection, 'typed-selection'),
1782
- };
1755
+ const answer = answers?.collection;
1756
+ if (answer === undefined)
1757
+ return held;
1758
+ const { state, refused } = attachedModel(answer, held.collection, roomTypes);
1759
+ if (refused !== null) {
1760
+ const where = refused.location === undefined ? '' : ` ${refused.location}`;
1761
+ journal.record(`model invalid ${locator} collection ${refused.identity} ${refused.rule}${where}`);
1762
+ }
1763
+ models.set(locator, state);
1764
+ return { collection: state };
1783
1765
  }
1784
- function publish(local, room, record, landing, live) {
1785
- return exclusive(async () => {
1766
+ function publish(local, room, reached, landing, live) {
1767
+ const landed = exclusive(async () => {
1786
1768
  if (!live()) {
1787
1769
  caughtUpTo(room.locator);
1788
1770
  return true;
1789
1771
  }
1790
1772
  const prior = replicas.get(room.locator);
1791
- if (landing.kind === 'refreshed' &&
1792
- (prior === undefined || prior.record.key !== record.key || prior.confirmed !== landing.base)) {
1793
- await local.unrefresh(record.key);
1773
+ if (!stands(room.locator, landing.from)) {
1774
+ if (landing.kind === 'refreshed') {
1775
+ await local.unrefresh(reached.key);
1776
+ return false;
1777
+ }
1778
+ caughtUpTo(room.locator);
1779
+ await abandoned(local, room.locator);
1794
1780
  return false;
1795
1781
  }
1782
+ const record = landing.kind === 'refreshed' && prior !== undefined
1783
+ ? {
1784
+ ...prior.record,
1785
+ roomVersion: reached.roomVersion,
1786
+ ...(landing.remodelled ? { model: reached.model } : {}),
1787
+ }
1788
+ : reached;
1796
1789
  adoptModels(room.locator, record);
1797
1790
  const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
1798
- const roomVersion = record.roomVersion;
1799
- const accepted = queue.filter((batch) => {
1800
- const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
1801
- return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
1802
- });
1803
1791
  let set;
1804
1792
  try {
1805
- set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
1793
+ set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
1806
1794
  ? {
1807
1795
  room: true,
1808
1796
  view: 'refreshed',
1809
- commit: { refreshed: true, composition: compositionOf(record) },
1797
+ commit: { refreshed: true },
1810
1798
  }
1811
1799
  : {
1812
1800
  room: true,
1813
1801
  view: landing.refreshed ? 'refreshed' : 'confirmed',
1814
1802
  commit: {
1815
1803
  published: { from: prior?.record.key ?? null },
1816
- composition: compositionOf(record),
1817
1804
  ...(landing.refreshed ? { refreshed: true } : {}),
1818
1805
  },
1819
1806
  });
1820
1807
  }
1821
1808
  catch (error) {
1822
- if (!exceeded(error))
1809
+ if (!refusedForSpace(error))
1823
1810
  throw error;
1824
- await exceededOn(room.locator, null);
1825
1811
  caughtUpTo(room.locator);
1826
1812
  return true;
1827
1813
  }
1828
1814
  if (landing.kind === 'bootstrapped') {
1829
- transfers.delete(room.locator);
1815
+ untransferred(room.locator);
1816
+ startedOver.delete(room.locator);
1830
1817
  catching.delete(room.locator);
1831
- sweep();
1818
+ if (prior !== undefined && prior.record.key !== record.key)
1819
+ sweep();
1820
+ name(room.locator);
1832
1821
  }
1833
- for (const batch of accepted)
1834
- pendings.delete(pendingKey(room.locator, batch.id));
1835
1822
  stale.delete(room.locator);
1836
- unindexable.delete(room.locator);
1823
+ unpreparable.delete(room.locator);
1837
1824
  if (unreadable.delete(room.locator))
1838
1825
  scopedRefusals.delete(room.locator);
1839
- sourceRefusals.delete(room.locator);
1840
- declarationRefusals.delete(room.locator);
1841
1826
  list(room, true);
1842
1827
  publishChangeSet(set);
1843
1828
  await unpair(local);
1844
1829
  moveTo({ ...status, ...attention() });
1845
- options.adapter.roomStream?.published(room.locator, roomVersion);
1846
- if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1830
+ if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1847
1831
  submitLater(room.locator);
1848
1832
  }
1849
1833
  return true;
1850
1834
  });
1835
+ return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
1851
1836
  }
1852
1837
  async function savedRoom(locator) {
1853
1838
  const local = await openLocal();
@@ -1872,12 +1857,7 @@ export function composeRuntime(options) {
1872
1857
  return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
1873
1858
  }
1874
1859
  async function saveParts(locator, named, writtenBy) {
1875
- const read = named.map(({ path, parts }) => ({ path, parts: readParts(parts) }));
1876
- if (read.some(({ path }, at) => path === '' ||
1877
- normalize(path) !== path ||
1878
- read.findIndex((one) => one.path === path) !== at)) {
1879
- throw runtimeError('room-draft-malformed');
1880
- }
1860
+ const read = readSavedParts(named);
1881
1861
  const target = await savedRoom(locator);
1882
1862
  const saved = [];
1883
1863
  const files = [];
@@ -1886,7 +1866,7 @@ export function composeRuntime(options) {
1886
1866
  for (const { path, parts } of read) {
1887
1867
  const file = parts.file === undefined
1888
1868
  ? undefined
1889
- : await heldFile(parts.file.blob, binaryBase(visible, path));
1869
+ : await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
1890
1870
  if (parts.file !== undefined && file !== undefined) {
1891
1871
  files.push({ key: file.localId, blob: parts.file.blob });
1892
1872
  }
@@ -1921,13 +1901,14 @@ export function composeRuntime(options) {
1921
1901
  const referring = removed.length === 0
1922
1902
  ? []
1923
1903
  : (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
1924
- const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
1904
+ const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
1925
1905
  const { visible } = await replayed(record, confirmedLayer, replica.queue);
1926
- const composition = compositionOf(record);
1906
+ const composition = composed(record);
1927
1907
  return {
1928
1908
  record,
1929
1909
  confirmed: confirmedLayer,
1930
1910
  visible,
1911
+ folders: new Set(await local.folders(record.key, 'visible', paths)),
1931
1912
  *visibleOver(queue) {
1932
1913
  return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
1933
1914
  },
@@ -1939,13 +1920,7 @@ export function composeRuntime(options) {
1939
1920
  return false;
1940
1921
  const local = await openLocal();
1941
1922
  const roots = record.typedSelection?.roots ?? [];
1942
- if (composition.model.status === 'unavailable' &&
1943
- (await local.typedWithin(record.key, 'visible', roots, false))) {
1944
- return true;
1945
- }
1946
- return (composition.selection !== null &&
1947
- composition.selection.model.status === 'unavailable' &&
1948
- (await local.typedWithin(record.key, 'visible', roots, true)));
1923
+ return local.typedWithin(record.key, 'visible', roots, false);
1949
1924
  }
1950
1925
  function textDiffers(left, right) {
1951
1926
  return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
@@ -1956,18 +1931,6 @@ export function composeRuntime(options) {
1956
1931
  return undefined;
1957
1932
  return entry.binary !== null ? null : entry.content;
1958
1933
  }
1959
- function walkOf(local, key, view) {
1960
- return async (after) => {
1961
- const page = await local.walk(key, view, after, WALK_BYTES);
1962
- return {
1963
- entries: page.entries.map(({ path, content, binary }) => ({
1964
- path,
1965
- content: binary !== null ? null : content,
1966
- })),
1967
- next: page.next,
1968
- };
1969
- };
1970
- }
1971
1934
  function lookupOf(local, key, view) {
1972
1935
  return async (paths) => {
1973
1936
  if (paths.length === 0)
@@ -1984,37 +1947,6 @@ export function composeRuntime(options) {
1984
1947
  const read = parseFrontmatter(entry.content ?? '');
1985
1948
  return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
1986
1949
  }
1987
- function overriddenWalk(walk, overrides) {
1988
- const sorted = [...overrides.keys()].sort();
1989
- const beyond = (path) => {
1990
- if (path === null)
1991
- return 0;
1992
- let low = 0;
1993
- let high = sorted.length;
1994
- while (low < high) {
1995
- const middle = (low + high) >>> 1;
1996
- if (sorted[middle] <= path)
1997
- low = middle + 1;
1998
- else
1999
- high = middle;
2000
- }
2001
- return low;
2002
- };
2003
- return async (after) => {
2004
- const page = await walk(after);
2005
- const end = page.next;
2006
- const added = sorted
2007
- .slice(beyond(after), end === null ? sorted.length : beyond(end))
2008
- .flatMap((path) => {
2009
- const entry = overrides.get(path);
2010
- return entry === null || entry === undefined
2011
- ? []
2012
- : [{ path, content: entry.binary !== null ? null : entry.content }];
2013
- });
2014
- const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
2015
- return { entries, next: end };
2016
- };
2017
- }
2018
1950
  function overriddenLookup(lookup, overrides) {
2019
1951
  return async (paths) => {
2020
1952
  const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
@@ -2032,9 +1964,8 @@ export function composeRuntime(options) {
2032
1964
  }
2033
1965
  function validatorOf(record, confirmedLayer) {
2034
1966
  return async (resulting, touched) => {
2035
- const composition = compositionFor(record);
2036
1967
  const local = await openLocal();
2037
- const changed = [];
1968
+ const changes = [];
2038
1969
  const overrides = new Map();
2039
1970
  const pace = paced();
2040
1971
  for (const path of new Set([...confirmedLayer.held, ...touched])) {
@@ -2045,19 +1976,13 @@ export function composeRuntime(options) {
2045
1976
  continue;
2046
1977
  overrides.set(path, now);
2047
1978
  if (textOf(was) !== textOf(now))
2048
- changed.push({ path, was: textOf(was), now: textOf(now) });
1979
+ changes.push({ path, was: textOf(was), now: textOf(now) });
2049
1980
  }
2050
- const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
2051
- const clean = available(composition) &&
2052
- changed.length <= CLEAN_CHANGES &&
2053
- (await changedCleanly(changed, compositionOf(record), composition, {
2054
- lookup,
2055
- texts: textsOf(local, record.key, 'confirmed'),
2056
- referrers: referrersOf(local, record.key, 'confirmed'),
2057
- }));
2058
- if (clean)
2059
- return { verdict: 'accept' };
2060
- return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
1981
+ return validatedNeighbourhood(changes, compositionFor(record), {
1982
+ lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
1983
+ texts: textsOf(local, record.key, 'confirmed'),
1984
+ referrers: referrersOf(local, record.key, 'confirmed'),
1985
+ });
2061
1986
  };
2062
1987
  }
2063
1988
  function textsOf(local, key, view) {
@@ -2081,6 +2006,7 @@ export function composeRuntime(options) {
2081
2006
  operations: room.operations,
2082
2007
  providerOwnedPaths: room.providerOwnedPaths,
2083
2008
  visible: saving.visible,
2009
+ folders: saving.folders,
2084
2010
  queued: queuedDocuments(replica.queue),
2085
2011
  unmodeled: await unmodeledIn(record),
2086
2012
  validate: validatorOf(record, saving.confirmed),
@@ -2101,6 +2027,7 @@ export function composeRuntime(options) {
2101
2027
  locator,
2102
2028
  key: saving.record.key,
2103
2029
  roots: saving.record.typedSelection?.roots ?? [],
2030
+ composed: composedOf(composed(saving.record)),
2104
2031
  overlay: diff.overlay,
2105
2032
  });
2106
2033
  }
@@ -2132,7 +2059,7 @@ export function composeRuntime(options) {
2132
2059
  }
2133
2060
  const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
2134
2061
  const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
2135
- const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, sourceSaving.visible)));
2062
+ const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
2136
2063
  if (changed.length > 0)
2137
2064
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2138
2065
  const target = replicas.get(destination);
@@ -2244,6 +2171,8 @@ export function composeRuntime(options) {
2244
2171
  submitLater(locator);
2245
2172
  }
2246
2173
  async function committed(locator, target, saved, consumed, writtenBy, files = []) {
2174
+ if (full)
2175
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
2247
2176
  const into = movedInto(locator, saved);
2248
2177
  if (into !== null) {
2249
2178
  return committedMove(locator, into, target, saved, consumed, writtenBy, files);
@@ -2255,7 +2184,7 @@ export function composeRuntime(options) {
2255
2184
  ...saved.map(({ path }) => path),
2256
2185
  ...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
2257
2186
  ]);
2258
- const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, underneath(held, path, saving.visible)));
2187
+ const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
2259
2188
  if (changed.length > 0)
2260
2189
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2261
2190
  const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
@@ -2283,6 +2212,7 @@ export function composeRuntime(options) {
2283
2212
  providerLimits: limits,
2284
2213
  handle: room.handle,
2285
2214
  visible: saving.visible,
2215
+ folders: saving.folders,
2286
2216
  changes,
2287
2217
  bases: parts.flatMap(({ bases }) => bases),
2288
2218
  queued: queuedDocuments(replica.queue),
@@ -2433,6 +2363,9 @@ export function composeRuntime(options) {
2433
2363
  async function readHistory(locator, path, query = {}) {
2434
2364
  if (closed || refusal !== null)
2435
2365
  return UNCERTAIN;
2366
+ await firstDiscovery;
2367
+ if (!roomsNamed)
2368
+ return UNCERTAIN;
2436
2369
  try {
2437
2370
  await ensureAuthorization();
2438
2371
  }
@@ -2522,14 +2455,12 @@ export function composeRuntime(options) {
2522
2455
  return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
2523
2456
  };
2524
2457
  }
2525
- function binaryRoom(request) {
2526
- if (request.kind === 'path')
2527
- return request.locator;
2528
- return options.adapter.referrerScope(request.referrer)?.locator ?? null;
2529
- }
2530
2458
  async function transferBinary(request, transfer, answer) {
2531
2459
  if (closed || refusal !== null)
2532
2460
  return UNCERTAIN;
2461
+ await firstDiscovery;
2462
+ if (!roomsNamed)
2463
+ return UNCERTAIN;
2533
2464
  try {
2534
2465
  await ensureAuthorization();
2535
2466
  }
@@ -2539,16 +2470,13 @@ export function composeRuntime(options) {
2539
2470
  if (closed || authorization === undefined || refusal !== null)
2540
2471
  return UNCERTAIN;
2541
2472
  const since = fencings;
2542
- journal.record(request.kind === 'path'
2543
- ? `transfer binary ${request.locator} ${request.path}`
2544
- : `transfer binary ${request.fileRef}`);
2473
+ journal.record(`transfer binary ${request.locator} ${request.path}`);
2545
2474
  try {
2546
2475
  const result = await transfer(authorization);
2547
2476
  const settled = result;
2548
2477
  journal.record(`outcome binary ${settled.kind}`);
2549
- const locator = binaryRoom(request);
2550
- if (locator !== null && fencedSince(locator, since))
2551
- return failure({ kind: 'room-not-readable', locator });
2478
+ if (fencedSince(request.locator, since))
2479
+ return failure({ kind: 'room-not-readable', locator: request.locator });
2552
2480
  if (settled.kind === 'scoped-refusal') {
2553
2481
  const room = replicas.get(settled.locator)?.record;
2554
2482
  if (room !== undefined)
@@ -2563,23 +2491,27 @@ export function composeRuntime(options) {
2563
2491
  return UNCERTAIN;
2564
2492
  }
2565
2493
  }
2494
+ function fetchBinary(request, name) {
2495
+ return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2496
+ }
2566
2497
  async function readBinary(request, name) {
2567
2498
  if (closed)
2568
2499
  return UNCERTAIN;
2569
2500
  const held = await keptFiles.find(name);
2570
2501
  if (held !== null)
2571
2502
  return held;
2572
- return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2503
+ return fetchBinary(request, name);
2573
2504
  }
2574
2505
  async function binaryPath(locator, path, revision) {
2575
2506
  if (revision !== undefined)
2576
2507
  return { kind: 'path', locator, path, revision };
2577
2508
  const replica = replicas.get(locator);
2578
- if (replica === undefined)
2579
- return null;
2580
2509
  const local = await openLocal();
2581
- const [held] = await local.entries(replica.record.key, 'visible', [path]);
2582
- if (held?.binary === undefined || held.binary === null)
2510
+ const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
2511
+ const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
2512
+ if (attached !== undefined)
2513
+ return { kind: 'path', locator, path, revision: attached.revision };
2514
+ if (replica === undefined || held?.binary === undefined || held.binary === null)
2583
2515
  return null;
2584
2516
  if (held.binary.state === 'pending')
2585
2517
  return { kind: 'pending', localId: held.binary.localId };
@@ -2595,7 +2527,7 @@ export function composeRuntime(options) {
2595
2527
  const holds = (at) => {
2596
2528
  const kept = confirmedLayer.binaries[at];
2597
2529
  return (kept?.state === 'accepted' &&
2598
- kept.fileRef === record.fileRef &&
2530
+ kept.sha256 === record.sha256 &&
2599
2531
  confirmedLayer.revisions[at] === record.revision);
2600
2532
  };
2601
2533
  let confirmedAt = path;
@@ -2609,49 +2541,91 @@ export function composeRuntime(options) {
2609
2541
  locator,
2610
2542
  path: confirmedAt,
2611
2543
  revision: record.revision,
2612
- fileRef: record.fileRef,
2613
2544
  };
2614
2545
  }
2615
- async function readBinaryPath(locator, path, revision) {
2546
+ async function askedBinary(locator, path, revision) {
2616
2547
  await ready;
2617
2548
  const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2618
- const asked = drafted !== undefined && drafted.localId === revision
2549
+ return drafted !== undefined && drafted.localId === revision
2619
2550
  ? { kind: 'pending', localId: revision }
2620
- : await binaryPath(locator, path, revision);
2551
+ : binaryPath(locator, path, revision);
2552
+ }
2553
+ function pendingBinary(locator, path, localId) {
2554
+ const facts = queuedFacts(locator, path, localId);
2555
+ return facts === null
2556
+ ? absentBinary(path)
2557
+ : {
2558
+ kind: 'binary',
2559
+ file: localId,
2560
+ byteLength: facts.byteLength,
2561
+ mediaType: facts.mediaType,
2562
+ };
2563
+ }
2564
+ async function readBinaryPath(locator, path, revision) {
2565
+ const asked = await askedBinary(locator, path, revision);
2621
2566
  if (asked === null)
2622
2567
  return absentBinary(path);
2623
- if (asked.kind === 'pending') {
2624
- const facts = queuedFacts(locator, path, asked.localId);
2625
- return facts === null
2626
- ? absentBinary(path)
2627
- : {
2628
- kind: 'binary',
2629
- file: asked.localId,
2630
- byteLength: facts.byteLength,
2631
- mediaType: facts.mediaType,
2632
- };
2633
- }
2568
+ if (asked.kind === 'pending')
2569
+ return pendingBinary(locator, path, asked.localId);
2634
2570
  return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
2635
2571
  }
2572
+ async function heldMediaType(locator, path) {
2573
+ const replica = replicas.get(locator);
2574
+ const [held] = replica === undefined
2575
+ ? []
2576
+ : await (await openLocal()).entries(replica.record.key, 'visible', [path]);
2577
+ return held === undefined
2578
+ ? attachedFiles.get(locator)?.get(path)?.mediaType
2579
+ : held.binary?.mediaType;
2580
+ }
2581
+ function linkOf(asked, held) {
2582
+ return transferBinary(asked, (authorization) => options.adapter.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
2583
+ }
2636
2584
  async function linkBinaryPath(locator, path, revision) {
2637
2585
  await ready;
2638
2586
  const asked = await binaryPath(locator, path, revision);
2639
2587
  if (asked === null || asked.kind === 'pending')
2640
2588
  return absentBinary(path);
2641
- return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
2589
+ return linkOf(asked, revision === undefined ? await heldMediaType(locator, path) : undefined);
2642
2590
  }
2643
- async function readBinaryReference(reference) {
2591
+ async function showBinaryPath(locator, path, revision) {
2592
+ const asked = await askedBinary(locator, path, revision);
2593
+ if (asked === null)
2594
+ return absentBinary(path);
2595
+ if (asked.kind === 'pending')
2596
+ return pendingBinary(locator, path, asked.localId);
2597
+ if (closed)
2598
+ return UNCERTAIN;
2599
+ const name = { locator, path: asked.path, revision: asked.revision };
2600
+ const kept = await keptFiles.find(name);
2601
+ if (kept !== null)
2602
+ return kept;
2603
+ const mediaType = await heldMediaType(locator, path);
2604
+ if (mediaType?.startsWith('image/') === true)
2605
+ return fetchBinary(asked, name);
2606
+ return linkOf(asked, revision === undefined ? mediaType : undefined);
2607
+ }
2608
+ async function attachBinary(locator, file) {
2644
2609
  await ready;
2645
- const scope = options.adapter.referrerScope(reference.referrer);
2646
- if (scope === null)
2647
- return absentReference(reference.fileRef);
2648
- return readBinary({ kind: 'reference', fileRef: reference.fileRef, referrer: reference.referrer }, { locator: scope.locator, path: scope.path ?? '', revision: reference.fileRef });
2610
+ await firstDiscovery;
2611
+ if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
2612
+ return OFFLINE;
2613
+ const request = await attachedFile(locator, file);
2614
+ return transferBinary({ locator, path: file.name }, async (held) => {
2615
+ const result = await options.adapter.attach(held, request);
2616
+ if (result.kind === 'attached') {
2617
+ const landed = attachedFiles.get(locator) ?? new Map();
2618
+ landed.set(result.path, { revision: result.revision, mediaType: request.mediaType });
2619
+ attachedFiles.set(locator, landed);
2620
+ }
2621
+ return result;
2622
+ }, attachAnswer);
2649
2623
  }
2650
2624
  function readPart(file, part) {
2651
- return options.database.ask({ kind: 'file.part', key: file, part });
2625
+ return database.ask({ kind: 'file.part', key: file, part });
2652
2626
  }
2653
2627
  async function cleanUp() {
2654
- const freed = await options.database.ask({ kind: 'opened.clean' });
2628
+ const freed = await database.ask({ kind: 'opened.clean' });
2655
2629
  return freed + ((await options.shell.cleanUp?.()) ?? 0);
2656
2630
  }
2657
2631
  function declaredAgents() {
@@ -2783,169 +2757,222 @@ export function composeRuntime(options) {
2783
2757
  options.adapter.reached(locator, written, roomVersion) &&
2784
2758
  !options.adapter.reached(locator, roomVersion, written));
2785
2759
  }
2786
- function overriddenTexts(texts, overrides) {
2787
- return async (paths) => {
2788
- const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
2789
- for (const path of paths) {
2790
- const entry = overrides.get(path);
2791
- if (entry === undefined)
2792
- continue;
2793
- if (entry === null)
2794
- known.delete(path);
2795
- else
2796
- known.set(path, entry.binary !== null ? null : entry.content);
2797
- }
2798
- return known;
2799
- };
2800
- }
2801
- async function unreadableRoom(texts, walk, paths) {
2802
- if (paths !== null) {
2803
- const text = {};
2804
- for (const [path, content] of await texts(paths))
2805
- if (content !== null)
2806
- text[path] = content;
2807
- return stepped(unreadableIn(text, Object.keys(text)));
2808
- }
2809
- for (let after = null;;) {
2810
- const page = await walk(after);
2811
- const text = {};
2812
- for (const { path, content } of page.entries)
2813
- if (content !== null)
2814
- text[path] = content;
2815
- const refused = await stepped(unreadableIn(text, Object.keys(text)));
2816
- if (refused !== null)
2817
- return refused;
2818
- if (page.next === null)
2819
- return null;
2820
- after = page.next;
2821
- }
2822
- }
2823
- async function validatedRoom(local, record, view, previous, overrides = null) {
2824
- const composition = compositionOf(record);
2825
- const modeled = record.model.status !== 'unavailable' || record.selectionModel.status !== 'unavailable';
2826
- const lookup = overrides === null
2827
- ? lookupOf(local, record.key, view)
2828
- : overriddenLookup(lookupOf(local, record.key, view), overrides);
2829
- const texts = overrides === null
2830
- ? textsOf(local, record.key, view)
2831
- : overriddenTexts(textsOf(local, record.key, view), overrides);
2832
- const walk = overrides === null
2833
- ? walkOf(local, record.key, view)
2834
- : overriddenWalk(walkOf(local, record.key, view), overrides);
2835
- let changes = null;
2836
- let reshaped = false;
2837
- if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
2838
- const [was, now] = await Promise.all([
2839
- local.entries(record.key, 'confirmed', previous.changed),
2840
- overrides === null
2841
- ? local.entries(record.key, view, previous.changed)
2842
- : Promise.resolve([]),
2843
- ]);
2844
- const before = new Map(was.map((entry) => [entry.path, entry]));
2845
- const after = overrides === null
2846
- ? new Map(now.map((entry) => [entry.path, entry]))
2847
- : overrides;
2848
- changes = previous.changed.flatMap((path) => {
2849
- const one = before.get(path) ?? null;
2850
- const other = after.get(path) ?? null;
2851
- reshaped ||= textDiffers(one, other);
2852
- const held = textOf(one);
2853
- const next = textOf(other);
2854
- return held === next ? [] : [{ path, was: held, now: next }];
2855
- });
2856
- }
2857
- const named = changes === null ? null : changes.map(({ path }) => path);
2858
- const textChanged = changes === null || changes.length > 0 || reshaped;
2859
- const modelsChanged = previous === null || !sameComposition(compositionOf(previous.record), composition);
2860
- if (modeled && (textChanged || modelsChanged)) {
2861
- const clean = previous !== null &&
2862
- changes !== null &&
2863
- (await changedCleanly(changes, compositionOf(previous.record), composition, {
2864
- lookup,
2865
- texts,
2866
- referrers: referrersOf(local, record.key, 'confirmed'),
2867
- }));
2868
- if (clean)
2869
- return null;
2870
- const verdict = await validatedCollection(walk, lookup, composition);
2871
- if (verdict.verdict === 'accept')
2872
- return null;
2873
- if (verdict.rule !== 'model-unavailable')
2874
- return { kind: 'refused', refusal: verdict };
2875
- return unreadableRoom(texts, walk, named);
2876
- }
2877
- if (textChanged)
2878
- return unreadableRoom(texts, walk, named);
2879
- return null;
2880
- }
2881
- function refreshingOf(local, key, liveness) {
2760
+ function refreshingOf(local, key, composition, liveness, replaying = null) {
2761
+ const write = replaying === null ? exclusive : finishingWrite;
2882
2762
  return {
2763
+ turn() {
2764
+ if (replaying !== null)
2765
+ installing(replaying);
2766
+ },
2883
2767
  prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
2884
2768
  async stage(page) {
2885
- const batches = batched(page.entries);
2886
- for (const [index, entries] of batches.entries()) {
2887
- const last = index === batches.length - 1;
2888
- const staged = await exclusive(async () => {
2769
+ for (let written = 0;;) {
2770
+ const end = batchEnd(page.entries, written);
2771
+ const last = end === page.entries.length;
2772
+ const entries = page.entries.slice(written, end);
2773
+ const staged = await write(async () => {
2889
2774
  if (!liveness())
2890
2775
  return false;
2891
- await local.refresh(key, last
2776
+ await timed(stageBudget, () => local.refresh(key, composition, last
2892
2777
  ? { ...page, entries }
2893
- : { entries, removals: [], folders: [], folderRemovals: [] });
2778
+ : { entries, removals: [], folders: [], folderRemovals: [] }));
2894
2779
  return true;
2895
2780
  });
2896
2781
  if (!staged)
2897
2782
  return false;
2783
+ if (last)
2784
+ return true;
2785
+ written = end;
2898
2786
  }
2899
- return true;
2900
2787
  },
2901
2788
  };
2902
2789
  }
2903
- function batched(entries) {
2904
- const batches = [[]];
2790
+ function batchEnd(entries, from) {
2791
+ let end = from;
2905
2792
  let spent = 0;
2906
- for (const entry of entries) {
2907
- const size = (entry.content?.length ?? 0) + ROW_WEIGHT;
2908
- if (spent + size > STAGE_BYTES && batches.at(-1).length > 0) {
2909
- batches.push([]);
2910
- spent = 0;
2911
- }
2912
- batches.at(-1).push(entry);
2793
+ while (end < entries.length) {
2794
+ const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
2795
+ if (spent + size > stageBudget.bytes && end > from)
2796
+ break;
2913
2797
  spent += size;
2798
+ end += 1;
2799
+ }
2800
+ return end;
2801
+ }
2802
+ async function timed(budget, work) {
2803
+ const started = performance.now();
2804
+ const done = await work();
2805
+ const spent = performance.now() - started;
2806
+ if (spent < budget.aim / 2)
2807
+ budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
2808
+ else if (spent > budget.aim * 2) {
2809
+ budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
2810
+ }
2811
+ return done;
2812
+ }
2813
+ function installWrite(work) {
2814
+ return installTurn(installTurns.starting, work);
2815
+ }
2816
+ function finishingWrite(work) {
2817
+ return installTurn(installTurns.finishing, work);
2818
+ }
2819
+ function installTurn(queue, work) {
2820
+ return new Promise((resolve, reject) => {
2821
+ queue.push(() => void exclusive(work)
2822
+ .then(resolve, reject)
2823
+ .finally(() => {
2824
+ installTurns.busy = false;
2825
+ nextInstallTurn();
2826
+ }));
2827
+ nextInstallTurn();
2828
+ });
2829
+ }
2830
+ function nextInstallTurn() {
2831
+ if (installTurns.busy || installTurns.ahead > 0)
2832
+ return;
2833
+ const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
2834
+ if (turn === undefined)
2835
+ return;
2836
+ installTurns.busy = true;
2837
+ turn();
2838
+ }
2839
+ async function ahead(work) {
2840
+ let taken = false;
2841
+ const hold = {
2842
+ take() {
2843
+ if (taken)
2844
+ return;
2845
+ taken = true;
2846
+ installTurns.ahead += 1;
2847
+ },
2848
+ release() {
2849
+ if (!taken)
2850
+ return;
2851
+ taken = false;
2852
+ installTurns.ahead -= 1;
2853
+ resumeInstall();
2854
+ },
2855
+ };
2856
+ hold.take();
2857
+ try {
2858
+ return await work(hold);
2859
+ }
2860
+ finally {
2861
+ hold.release();
2862
+ }
2863
+ }
2864
+ function resumeInstall() {
2865
+ if (installTurns.ahead > 0)
2866
+ return;
2867
+ void yielded().then(nextInstallTurn);
2868
+ }
2869
+ async function heldStream() {
2870
+ while (streamsFree === 0)
2871
+ await new Promise((admit) => installWaiters.push(admit));
2872
+ streamsFree -= 1;
2873
+ return once(() => {
2874
+ streamsFree += 1;
2875
+ });
2876
+ }
2877
+ async function unqueued() {
2878
+ while (queuedText >= QUEUED_TEXT) {
2879
+ await new Promise((admit) => installWaiters.push(admit));
2880
+ }
2881
+ }
2882
+ function once(release) {
2883
+ let released = false;
2884
+ return () => {
2885
+ if (released)
2886
+ return;
2887
+ released = true;
2888
+ release();
2889
+ for (const admit of installWaiters.splice(0))
2890
+ admit();
2891
+ };
2892
+ }
2893
+ function installing(locator) {
2894
+ installs.add(locator);
2895
+ queueReadiness();
2896
+ }
2897
+ function untransferred(locator) {
2898
+ transfers.delete(locator);
2899
+ arrivals.delete(locator);
2900
+ streaming.delete(locator);
2901
+ underived.delete(locator);
2902
+ unindexed.delete(locator);
2903
+ }
2904
+ function setBack(locator) {
2905
+ const now = bytesOf(locator, true);
2906
+ const earlier = startedOver.get(locator);
2907
+ startedOver.set(locator, {
2908
+ done: Math.max(now.done, earlier?.done ?? 0),
2909
+ total: Math.max(now.total, earlier?.total ?? 0),
2910
+ });
2911
+ }
2912
+ function owedIndexes(locator, bytes, text) {
2913
+ if (bytes <= 0 || text <= 0)
2914
+ return;
2915
+ unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
2916
+ }
2917
+ function built(locator, { done, text }) {
2918
+ const owed = unindexed.get(locator) ?? [];
2919
+ let left = text;
2920
+ while (!done && left > 0 && owed.length > 0) {
2921
+ const [first] = owed;
2922
+ if (left >= first.text) {
2923
+ left -= first.text;
2924
+ owed.shift();
2925
+ continue;
2926
+ }
2927
+ owed[0] = {
2928
+ bytes: (first.bytes * (first.text - left)) / first.text,
2929
+ text: first.text - left,
2930
+ };
2931
+ left = 0;
2914
2932
  }
2915
- return batches;
2933
+ if (done || owed.length === 0)
2934
+ unindexed.delete(locator);
2935
+ queueReadiness();
2936
+ }
2937
+ function installed(locator) {
2938
+ if (installs.delete(locator))
2939
+ queueReadiness();
2916
2940
  }
2917
- async function pullRefresh(authorized, held, liveness) {
2941
+ async function pullRefresh(authorized, held, liveness, hold, refresh) {
2918
2942
  if (!liveness())
2919
2943
  return unconfirmed;
2920
- const turn = generation;
2921
2944
  const { record } = held;
2922
- const base = held.confirmed;
2945
+ const from = standingOf(record.locator);
2923
2946
  const local = await openLocal();
2924
2947
  try {
2925
2948
  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));
2949
+ const result = await advancement(async (authorization, request) => {
2950
+ hold.release();
2951
+ try {
2952
+ return await refresh(authorization, request);
2953
+ }
2954
+ finally {
2955
+ hold.take();
2956
+ }
2957
+ }, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, composed(record), liveness));
2927
2958
  if (result.kind !== 'advanced') {
2928
2959
  journal.record(`transfer refresh ${record.locator} ${result.kind}`);
2929
2960
  return result;
2930
2961
  }
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
2962
  journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
2940
- return { kind: 'advanced', record: result.record, base };
2963
+ return {
2964
+ kind: 'advanced',
2965
+ record: result.record,
2966
+ from,
2967
+ remodelled: result.record.model !== record.model,
2968
+ };
2941
2969
  }
2942
2970
  catch (error) {
2971
+ if (refusedForSpace(error))
2972
+ return unconfirmed;
2943
2973
  journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
2944
2974
  throw error;
2945
2975
  }
2946
- finally {
2947
- reads.finished(record.locator, turn);
2948
- }
2949
2976
  }
2950
2977
  const heldContents = new Map();
2951
2978
  function heldContentOf(local, key) {
@@ -2963,63 +2990,115 @@ export function composeRuntime(options) {
2963
2990
  heldContents.set(key, made);
2964
2991
  return made;
2965
2992
  }
2993
+ async function abandoned(local, locator) {
2994
+ await local.abandon(locator);
2995
+ setBack(locator);
2996
+ untransferred(locator);
2997
+ liveQueries.recover();
2998
+ sweep();
2999
+ }
2966
3000
  function staging(local, liveness) {
3001
+ const derived = async (locator, key, composition) => {
3002
+ underived.delete(locator);
3003
+ for (;;) {
3004
+ if (!liveness())
3005
+ return false;
3006
+ const done = await finishingWrite(async () => liveness()
3007
+ ? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
3008
+ : null);
3009
+ if (done === null)
3010
+ return false;
3011
+ built(locator, done);
3012
+ if (done.done)
3013
+ return true;
3014
+ }
3015
+ };
2967
3016
  return {
2968
3017
  held: (locator) => transfers.get(locator) ?? null,
3018
+ async turn(locator, step) {
3019
+ installing(locator);
3020
+ return step === 'downloading' ? heldStream() : () => { };
3021
+ },
3022
+ unqueued,
3023
+ queued(text) {
3024
+ queuedText += text;
3025
+ return once(() => {
3026
+ queuedText -= text;
3027
+ });
3028
+ },
3029
+ arriving(locator) {
3030
+ let ended = false;
3031
+ return {
3032
+ at(bytes) {
3033
+ if (ended)
3034
+ return;
3035
+ arrivals.set(locator, bytes);
3036
+ streaming.add(locator);
3037
+ const now = performance.now();
3038
+ if (now - arrivalReported < ARRIVAL_REPORT_MS)
3039
+ return;
3040
+ arrivalReported = now;
3041
+ queueReadiness();
3042
+ },
3043
+ end() {
3044
+ if (ended)
3045
+ return;
3046
+ ended = true;
3047
+ if (streaming.delete(locator))
3048
+ queueReadiness();
3049
+ },
3050
+ };
3051
+ },
3052
+ streamed: options.adapter.roomStream !== undefined,
2969
3053
  async stage(record, page, composition) {
2970
- const bytes = pageBytes(page);
2971
- const batches = batched(page.entries);
2972
- let key = record.key;
2973
- for (const [index, entries] of batches.entries()) {
2974
- const last = index === batches.length - 1;
2975
- const staged = await exclusive(async () => {
2976
- if (!liveness())
2977
- return null;
2978
- if (index === 0 && !(await admitted(local, record.locator, bytes)))
2979
- return null;
2980
- try {
2981
- return await local.stage(record, composition, { entries, folders: last ? page.folders : [] }, key, last);
2982
- }
2983
- catch (error) {
2984
- if (!exceeded(error))
2985
- throw error;
2986
- await exceededOn(record.locator, bytes);
2987
- return null;
2988
- }
2989
- });
2990
- if (staged === null)
3054
+ const { locator } = record;
3055
+ installing(locator);
3056
+ if (!liveness())
3057
+ return null;
3058
+ const key = await installWrite(async () => {
3059
+ if (!liveness())
2991
3060
  return null;
2992
- key = staged;
2993
- }
2994
- transfers.set(record.locator, { ...record, key });
3061
+ return local.stage(record, composition, page);
3062
+ });
3063
+ if (key === null || !liveness())
3064
+ return null;
3065
+ const arrived = !transfers.has(locator);
3066
+ const prior = transfers.get(locator)?.done.bytes ?? 0;
3067
+ const held = { ...record, key };
3068
+ transfers.set(locator, held);
3069
+ if (arrived)
3070
+ liveQueries.recover();
3071
+ installing(locator);
2995
3072
  liveQueries.recover();
2996
- return key;
3073
+ const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
3074
+ owedIndexes(locator, record.done.bytes - prior, text);
3075
+ const owed = text + (underived.get(locator) ?? 0);
3076
+ underived.set(locator, owed);
3077
+ if (owed < deriveBudget.bytes)
3078
+ return held;
3079
+ return (await derived(locator, key, composition)) ? held : null;
2997
3080
  },
2998
- async refreshing(key) {
3081
+ derive: derived,
3082
+ async refreshing(locator, key, composition) {
2999
3083
  await exclusive(() => local.unrefresh(key));
3000
- return refreshingOf(local, key, liveness);
3084
+ return refreshingOf(local, key, composition, liveness, locator);
3001
3085
  },
3002
3086
  abandon: (locator) => exclusive(async () => {
3003
- if (!liveness())
3004
- return;
3005
- await local.abandon(locator);
3006
- transfers.delete(locator);
3007
- liveQueries.recover();
3008
- sweep();
3087
+ if (liveness())
3088
+ await abandoned(local, locator);
3009
3089
  }),
3010
- setAside: (locator) => exclusive(async () => {
3090
+ setAside: async (locator) => {
3011
3091
  if (!liveness())
3012
3092
  return;
3013
- await local.abandon(locator, true);
3014
- transfers.delete(locator);
3093
+ setBack(locator);
3094
+ untransferred(locator);
3015
3095
  liveQueries.recover();
3016
- }),
3096
+ },
3017
3097
  release: (key) => exclusive(async () => {
3018
3098
  await local.release(key);
3019
3099
  sweep();
3020
3100
  }),
3021
3101
  content: (key) => heldContentOf(local, key),
3022
- validate: (record, view) => validatedRoom(local, record, view, null),
3023
3102
  };
3024
3103
  }
3025
3104
  function reusable(room, local) {
@@ -3031,30 +3110,28 @@ export function composeRuntime(options) {
3031
3110
  async function pullBootstrap(authorized, room, local, liveness) {
3032
3111
  if (!liveness())
3033
3112
  return unconfirmed;
3034
- const turn = generation;
3035
3113
  if (replicas.has(room.locator)) {
3036
3114
  catching.add(room.locator);
3037
3115
  liveQueries.recover();
3038
3116
  }
3039
3117
  try {
3040
- const result = await bootstrapRoom(reads, authorized, room, {
3041
- collection: modelOf(room.locator),
3042
- selection: modelOf(room.locator, 'typed-selection'),
3043
- }, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
3118
+ 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));
3044
3119
  const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
3045
3120
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
3046
- if (result.kind !== 'staged')
3121
+ if (result.kind !== 'staged') {
3047
3122
  caughtUpTo(room.locator);
3123
+ installed(room.locator);
3124
+ }
3048
3125
  return result;
3049
3126
  }
3050
3127
  catch (error) {
3051
- journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
3052
3128
  caughtUpTo(room.locator);
3129
+ installed(room.locator);
3130
+ if (refusedForSpace(error))
3131
+ return unconfirmed;
3132
+ journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
3053
3133
  throw error;
3054
3134
  }
3055
- finally {
3056
- reads.finished(room.locator, turn);
3057
- }
3058
3135
  }
3059
3136
  function caughtUpTo(locator) {
3060
3137
  if (catching.delete(locator))
@@ -3062,72 +3139,82 @@ export function composeRuntime(options) {
3062
3139
  }
3063
3140
  function behind(room) {
3064
3141
  const held = replicas.get(room.locator)?.record.roomVersion;
3065
- return (held !== undefined &&
3066
- [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
3142
+ return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
3067
3143
  }
3068
- async function advance(room, local, authorized, live) {
3069
- const carried = reads.carried(room.locator);
3070
- const withheld = await advanceOnce(room, local, authorized, live);
3071
- if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
3072
- return withheld;
3073
- }
3074
- return advanceOnce(room, local, authorized, live);
3144
+ function standingOf(locator) {
3145
+ const held = replicas.get(locator);
3146
+ return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
3075
3147
  }
3076
- async function advanceOnce(room, local, authorized, live) {
3077
- if (!live() || paused.has(room.locator))
3148
+ function stands(locator, from) {
3149
+ const now = standingOf(locator);
3150
+ return now.key === from.key && now.confirmed === from.confirmed;
3151
+ }
3152
+ async function advance(room, local, authorized, running) {
3153
+ const live = () => running() && !full;
3154
+ if (!live())
3078
3155
  return unconfirmed;
3079
3156
  const held = replicas.get(room.locator);
3080
- if (held !== undefined) {
3081
- const refreshed = await pullRefresh(authorized, held, live);
3082
- if (!live())
3083
- return unconfirmed;
3084
- if (refreshed.kind === 'still-current') {
3085
- stale.delete(room.locator);
3086
- if (sourceRefusals.has(room.locator))
3087
- return null;
3088
- if (unreadable.delete(room.locator)) {
3089
- scopedRefusals.delete(room.locator);
3090
- list(room, true);
3091
- emitRooms();
3092
- }
3093
- return null;
3094
- }
3095
- if (refreshed.kind === 'advanced') {
3096
- const landing = { kind: 'refreshed', base: refreshed.base };
3097
- if (!(await publish(local, room, refreshed.record, landing, live))) {
3098
- const now = replicas.get(room.locator)?.record.roomVersion;
3099
- const reached = now !== undefined &&
3100
- options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3101
- return reached ? null : advance(room, local, authorized, live);
3102
- }
3103
- await retainModel(local, room, 'collection', modelOf(room.locator));
3104
- return null;
3105
- }
3106
- if (refreshed.kind !== 'beyond-horizon')
3157
+ if (held !== undefined && options.adapter.roomStream !== undefined) {
3158
+ stale.delete(room.locator);
3159
+ name(room.locator);
3160
+ return null;
3161
+ }
3162
+ const refresh = reads.refresh;
3163
+ if (held !== undefined && refresh !== undefined) {
3164
+ const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
3165
+ if (refreshed === 'behind')
3166
+ return advance(room, local, authorized, live);
3167
+ if (refreshed !== 'beyond-horizon')
3107
3168
  return refreshed;
3108
- if (opened !== null) {
3109
- owed.add(room.locator);
3110
- liveQueries.recover();
3111
- return unconfirmed;
3112
- }
3113
- if (reloadsDue.get(room.locator) === generation)
3169
+ if (reloadsDue.has(room.locator))
3114
3170
  return unconfirmed;
3115
3171
  }
3172
+ const from = standingOf(room.locator);
3116
3173
  const bootstrapped = await pullBootstrap(authorized, room, local, live);
3174
+ if (bootstrapped.kind !== 'staged')
3175
+ return live() ? bootstrapped : unconfirmed;
3176
+ const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
3177
+ if (await publish(local, room, bootstrapped.record, landing, live)) {
3178
+ return live() ? null : unconfirmed;
3179
+ }
3180
+ return reachedBy(room.locator, bootstrapped.record.roomVersion)
3181
+ ? null
3182
+ : advance(room, local, authorized, live);
3183
+ }
3184
+ async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
3185
+ const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
3117
3186
  if (!live())
3118
3187
  return unconfirmed;
3119
- if (bootstrapped.kind !== 'staged')
3120
- return bootstrapped;
3121
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3122
- return null;
3188
+ if (refreshed.kind === 'still-current') {
3189
+ stale.delete(room.locator);
3190
+ if (unreadable.delete(room.locator)) {
3191
+ scopedRefusals.delete(room.locator);
3192
+ list(room, true);
3193
+ emitRooms();
3194
+ }
3195
+ return null;
3196
+ }
3197
+ if (refreshed.kind === 'advanced') {
3198
+ const landing = {
3199
+ kind: 'refreshed',
3200
+ from: refreshed.from,
3201
+ remodelled: refreshed.remodelled,
3202
+ };
3203
+ if (!(await publish(local, room, refreshed.record, landing, live))) {
3204
+ const now = replicas.get(room.locator)?.record.roomVersion;
3205
+ const reached = now !== undefined &&
3206
+ options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3207
+ return reached ? null : 'behind';
3208
+ }
3209
+ await retainModel(local, room, modelOf(room.locator));
3210
+ return null;
3211
+ }
3212
+ return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
3123
3213
  }
3124
3214
  async function withhold(withheld, room, local, progress) {
3125
3215
  if (withheld.kind === 'unconfirmed')
3126
3216
  return;
3127
- if (withheld.kind === 'refused')
3128
- stale.delete(room.locator);
3129
- else
3130
- stale.add(room.locator);
3217
+ stale.add(room.locator);
3131
3218
  if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
3132
3219
  const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
3133
3220
  repairable.set(room.locator, reset);
@@ -3149,30 +3236,10 @@ export function composeRuntime(options) {
3149
3236
  list(room, false);
3150
3237
  emitRooms();
3151
3238
  }
3152
- else if (withheld.kind === 'refused') {
3153
- refusedSource(room, withheld);
3154
- }
3155
3239
  else if (withheld.kind === 'revoked-authorization') {
3156
3240
  await refuse('revoked-authorization');
3157
3241
  }
3158
3242
  }
3159
- function refusedSource(room, { refusal }) {
3160
- sourceRefusals.set(room.locator, {
3161
- code: refusal.rule,
3162
- retryable: false,
3163
- fields: {
3164
- file: refusal.file,
3165
- ...(refusal.location === undefined ? {} : { location: refusal.location }),
3166
- ...(refusal.referrers === undefined ? {} : { referrers: refusal.referrers }),
3167
- },
3168
- });
3169
- declarationRefusals.delete(room.locator);
3170
- unreadable.add(room.locator);
3171
- list(room, false);
3172
- emitRooms();
3173
- moveTo({ ...status, ...attention() });
3174
- journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
3175
- }
3176
3243
  function leaving(batch, result) {
3177
3244
  if (result.kind === 'expired')
3178
3245
  return { batch, returned: returnedBy('expired') };
@@ -3194,8 +3261,8 @@ export function composeRuntime(options) {
3194
3261
  function settling(acceptance) {
3195
3262
  return acceptance !== null && 'apply' in acceptance;
3196
3263
  }
3197
- function refusalOf(acceptance) {
3198
- return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
3264
+ function failureOf(acceptance) {
3265
+ return settling(acceptance) ? null : acceptance;
3199
3266
  }
3200
3267
  async function acceptance(room, local, submitted, accepted) {
3201
3268
  if (!continuing())
@@ -3218,34 +3285,6 @@ export function composeRuntime(options) {
3218
3285
  ? accepted.roomVersion
3219
3286
  : held,
3220
3287
  };
3221
- const overrides = new Map([
3222
- ...back.set.map((entry) => [entry.path, entry]),
3223
- ...back.removed.map((path) => [path, null]),
3224
- ]);
3225
- const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
3226
- if (refused !== null) {
3227
- const key = pendingKey(room.locator, submitted.id);
3228
- return {
3229
- locator: room.locator,
3230
- left: [],
3231
- commit: {
3232
- rooms: [],
3233
- writes: [
3234
- {
3235
- locator: room.locator,
3236
- accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
3237
- },
3238
- ],
3239
- },
3240
- refused,
3241
- apply() {
3242
- const state = pendings.get(key);
3243
- if (state !== undefined && !fences.has(room.locator))
3244
- pendings.set(key, { ...state, accepted: accepted.roomVersion });
3245
- return changeSet(room.locator, { queue: true });
3246
- },
3247
- };
3248
- }
3249
3288
  changes = back;
3250
3289
  }
3251
3290
  const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
@@ -3255,8 +3294,6 @@ export function composeRuntime(options) {
3255
3294
  const set = settled.apply(answer);
3256
3295
  pendings.delete(pendingKey(room.locator, submitted.id));
3257
3296
  unreadable.delete(room.locator);
3258
- sourceRefusals.delete(room.locator);
3259
- declarationRefusals.delete(room.locator);
3260
3297
  list(room, true);
3261
3298
  return set;
3262
3299
  },
@@ -3274,13 +3311,9 @@ export function composeRuntime(options) {
3274
3311
  const failed = await exclusive(async () => {
3275
3312
  const adopting = await acceptance(room, local, submitted, accepted);
3276
3313
  await adoptedTogether(local, [adopting]);
3277
- return refusalOf(adopting);
3314
+ return failureOf(adopting);
3278
3315
  });
3279
- if (failed === null)
3280
- return true;
3281
- if (failed.kind === 'refused' && continuing())
3282
- refusedSource(room, failed);
3283
- return false;
3316
+ return failed === null;
3284
3317
  }
3285
3318
  async function adoptedMove(room, partner, local, submitted, accepted) {
3286
3319
  const other = discovered.get(partner);
@@ -3294,18 +3327,10 @@ export function composeRuntime(options) {
3294
3327
  ...accepted.destination,
3295
3328
  });
3296
3329
  await adoptedTogether(local, [left, arrived]);
3297
- const refusedArrival = refusalOf(arrived);
3298
- if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
3299
- refusedSource(other, refusedArrival);
3300
- }
3301
- return refusalOf(left);
3330
+ return failureOf(left);
3302
3331
  });
3303
3332
  submitLater(partner);
3304
- if (failed === null)
3305
- return true;
3306
- if (failed.kind === 'refused' && continuing())
3307
- refusedSource(room, failed);
3308
- return false;
3333
+ return failed === null;
3309
3334
  }
3310
3335
  function queueIn(locator) {
3311
3336
  return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
@@ -3428,8 +3453,6 @@ export function composeRuntime(options) {
3428
3453
  if (replica === undefined || head === undefined)
3429
3454
  return 'done';
3430
3455
  const state = pendings.get(pendingKey(room.locator, head.id));
3431
- if (state?.accepted !== undefined)
3432
- return 'done';
3433
3456
  const partner = partnerOf(head, room.locator);
3434
3457
  if (partner !== null && !leavingIn(head, room.locator)) {
3435
3458
  submitLater(partner);
@@ -3543,12 +3566,12 @@ export function composeRuntime(options) {
3543
3566
  }
3544
3567
  if (result.kind === 'expired') {
3545
3568
  await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
3546
- const there = other === undefined ? null : await advance(other, local, authorized, continuing);
3569
+ const there = other === undefined ? null : await advanced(other, local, authorized);
3547
3570
  if (other !== undefined && there !== null) {
3548
3571
  await withhold(there, other, local, progress);
3549
3572
  return 'done';
3550
3573
  }
3551
- const withheld = await advance(room, local, authorized, continuing);
3574
+ const withheld = await advanced(room, local, authorized);
3552
3575
  if (withheld !== null) {
3553
3576
  await withhold(withheld, room, local, progress);
3554
3577
  return 'done';
@@ -3570,8 +3593,8 @@ export function composeRuntime(options) {
3570
3593
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
3571
3594
  const left = leaving(head, result);
3572
3595
  const refreshed = async () => ({
3573
- there: other === undefined ? null : await advance(other, local, authorized, continuing),
3574
- here: await advance(room, local, authorized, continuing),
3596
+ there: other === undefined ? null : await advanced(other, local, authorized),
3597
+ here: await advanced(room, local, authorized),
3575
3598
  });
3576
3599
  const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
3577
3600
  if (judged !== null && (judged.there !== null || judged.here !== null)) {
@@ -3642,6 +3665,7 @@ export function composeRuntime(options) {
3642
3665
  locator: room.locator,
3643
3666
  key: before.record.key,
3644
3667
  roots: before.record.typedSelection?.roots ?? [],
3668
+ composed: composedOf(composed(before.record)),
3645
3669
  overlay: { set: [], cleared: [...before.overlaid] },
3646
3670
  },
3647
3671
  ]),
@@ -3674,39 +3698,37 @@ export function composeRuntime(options) {
3674
3698
  if (signal.aborted)
3675
3699
  return;
3676
3700
  const progress = { complete: true, reachable: true };
3677
- await roomTask(room.locator, async (live) => {
3678
- await Promise.race([
3679
- reload(room, local, progress, () => live() && !signal.aborted),
3680
- abandonedOn(signal),
3681
- ]);
3682
- });
3701
+ await roomTask(room.locator, (live) => reload(room, local, progress, live));
3683
3702
  if (signal.aborted)
3684
3703
  return;
3685
3704
  await rest(progress);
3686
3705
  }
3687
3706
  }
3688
3707
  function awaitingAnswer(locator) {
3708
+ answering.get(locator)?.release();
3689
3709
  let release = () => { };
3690
- const awaited = new Promise((resolve) => {
3710
+ const answered = new Promise((resolve) => {
3691
3711
  release = resolve;
3692
3712
  });
3693
- answering.set(locator, { generation, answered: awaited });
3713
+ const awaited = { answered, release };
3714
+ answering.set(locator, awaited);
3694
3715
  return () => {
3695
- if (answering.get(locator)?.answered === awaited)
3716
+ if (answering.get(locator) === awaited)
3696
3717
  answering.delete(locator);
3697
3718
  release();
3698
3719
  };
3699
3720
  }
3700
- async function retainModel(local, room, attached, next) {
3701
- const key = modelKey(room.locator, attached);
3702
- const previous = modelOf(room.locator, attached);
3703
- const heldBefore = replicas.get(room.locator)?.record;
3704
- if (JSON.stringify(previous) === JSON.stringify(next) &&
3705
- (heldBefore === undefined ||
3706
- (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
3707
- JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
3708
- JSON.stringify(heldBefore.typedSelection) ===
3709
- JSON.stringify(room.typedSelection ?? null))))
3721
+ function declaredAsHeld(room) {
3722
+ const held = replicas.get(room.locator)?.record;
3723
+ return (held === undefined ||
3724
+ (JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
3725
+ JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
3726
+ JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
3727
+ }
3728
+ async function retainModel(local, room, next) {
3729
+ const key = room.locator;
3730
+ const previous = modelOf(room.locator);
3731
+ if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(room))
3710
3732
  return;
3711
3733
  if (next.status === 'unavailable') {
3712
3734
  invalidModels.add(key);
@@ -3718,26 +3740,13 @@ export function composeRuntime(options) {
3718
3740
  const held = replicas.get(room.locator);
3719
3741
  let changed = null;
3720
3742
  if (held !== undefined) {
3721
- const declared = {
3743
+ const record = {
3722
3744
  ...held.record,
3723
3745
  operations: room.operations,
3724
3746
  replica: room.replica,
3725
3747
  typedSelection: room.typedSelection ?? null,
3726
- ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
3748
+ model: next,
3727
3749
  };
3728
- let read = 'staged';
3729
- if (!sameMembership(declared, held.record)) {
3730
- const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), compositionOf(declared));
3731
- read =
3732
- verdict.verdict !== 'refuse'
3733
- ? 'staged'
3734
- : verdict.rule === 'model-unavailable'
3735
- ? 'deferred'
3736
- : { kind: 'refused', refusal: verdict };
3737
- }
3738
- const record = read === 'staged'
3739
- ? declared
3740
- : { ...declared, typedSelection: held.record.typedSelection };
3741
3750
  const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
3742
3751
  const replayedLayer = await replayed(record, confirmedLayer, held.queue);
3743
3752
  const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
@@ -3748,41 +3757,33 @@ export function composeRuntime(options) {
3748
3757
  key: record.key,
3749
3758
  record: storedRecord(record),
3750
3759
  roots: record.typedSelection?.roots ?? [],
3751
- composition: compositionOf(record),
3760
+ composed: composedOf(composed(record)),
3752
3761
  overlay: diff.overlay,
3753
3762
  },
3754
3763
  ],
3755
3764
  });
3756
- recomposed(room.locator, held.record, record);
3757
- const { count } = committedRoom(room.locator, record, answer);
3758
- replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count });
3765
+ const { count, indexed } = committedRoom(room.locator, answer);
3766
+ replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
3759
3767
  changed = changeSet(room.locator, {
3760
3768
  documents: answer.changed[room.locator] ?? [],
3761
3769
  room: true,
3762
3770
  });
3763
- if (read === 'staged' || read === 'deferred') {
3764
- if (declarationRefusals.delete(room.locator)) {
3765
- sourceRefusals.delete(room.locator);
3766
- unreadable.delete(room.locator);
3767
- }
3768
- }
3769
- else {
3770
- refusedSource(room, read);
3771
- declarationRefusals.add(room.locator);
3772
- }
3773
- }
3774
- models.set(key, next);
3775
- if (next.status === 'current') {
3776
- liveModels.set(key, JSON.stringify(next.snapshot));
3777
- invalidModels.delete(key);
3778
3771
  }
3779
- list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
3772
+ heldModel(room, next);
3780
3773
  if (changed === null)
3781
3774
  emitRooms();
3782
3775
  else
3783
3776
  publishChangeSet(changed);
3784
3777
  });
3785
3778
  }
3779
+ function heldModel(room, next) {
3780
+ models.set(room.locator, next);
3781
+ if (next.status === 'current') {
3782
+ liveModels.set(room.locator, JSON.stringify(next.snapshot));
3783
+ invalidModels.delete(room.locator);
3784
+ }
3785
+ list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
3786
+ }
3786
3787
  async function scoped(result, room, local) {
3787
3788
  if (result.scope === 'content' && !result.retryable) {
3788
3789
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
@@ -3796,14 +3797,14 @@ export function composeRuntime(options) {
3796
3797
  });
3797
3798
  let scopedAway = room;
3798
3799
  if (result.scope === 'model')
3799
- await retainModel(local, room, 'collection', {
3800
+ await retainModel(local, room, {
3800
3801
  status: 'unavailable',
3801
3802
  reason: 'not-readable',
3802
3803
  });
3803
3804
  else if (result.scope !== 'operation') {
3804
3805
  unreadable.add(room.locator);
3805
3806
  scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
3806
- await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
3807
+ await retainModel(local, scopedAway, modelOf(room.locator));
3807
3808
  }
3808
3809
  if (!continuing())
3809
3810
  return;
@@ -3811,6 +3812,17 @@ export function composeRuntime(options) {
3811
3812
  emitRooms();
3812
3813
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
3813
3814
  }
3815
+ function name(locator) {
3816
+ const stream = options.adapter.roomStream;
3817
+ const record = replicas.get(locator)?.record;
3818
+ const room = discovered.get(locator);
3819
+ if (stream === undefined || record === undefined || room === undefined)
3820
+ return;
3821
+ if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
3822
+ return;
3823
+ naming += 1;
3824
+ stream.hold([{ locator, roomVersion: record.roomVersion }]);
3825
+ }
3814
3826
  function heldRooms(listed) {
3815
3827
  return listed.flatMap((room) => {
3816
3828
  const record = replicas.get(room.locator)?.record;
@@ -3828,14 +3840,20 @@ export function composeRuntime(options) {
3828
3840
  return outcome?.status === 'fulfilled' && outcome.value;
3829
3841
  });
3830
3842
  if (attempt === 'opened') {
3843
+ naming += 1;
3831
3844
  options.adapter.roomStream?.hold(heldRooms(ready));
3832
3845
  handed = true;
3833
3846
  }
3834
3847
  discovering?.release();
3835
- const loaded = await Promise.allSettled(ready
3836
- .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
3837
- .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
3838
- const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
3848
+ for (const room of ready) {
3849
+ if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
3850
+ if (attempt === 'authorized')
3851
+ name(room.locator);
3852
+ continue;
3853
+ }
3854
+ void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
3855
+ }
3856
+ const failed = prepared.find((room) => room.status === 'rejected');
3839
3857
  if (failed !== undefined)
3840
3858
  throw failed.reason;
3841
3859
  }
@@ -3853,27 +3871,29 @@ export function composeRuntime(options) {
3853
3871
  return false;
3854
3872
  }
3855
3873
  if (prior !== undefined)
3856
- await retainModel(local, room, 'collection', modelOf(room.locator));
3874
+ await retainModel(local, room, modelOf(room.locator));
3857
3875
  if (discoveryPaused || authorization === undefined)
3858
3876
  return false;
3859
3877
  return room.replica.admission === 'admitted';
3860
3878
  }
3861
3879
  async function caughtUp(room, local, progress, live) {
3862
- if (authorization === undefined || !live() || !behind(room))
3880
+ if (options.adapter.roomStream !== undefined || authorization === undefined)
3881
+ return;
3882
+ if (!live() || !behind(room))
3863
3883
  return;
3864
3884
  const withheld = await advance(room, local, authorization, live);
3865
3885
  if (withheld !== null)
3866
3886
  await withhold(withheld, room, local, progress);
3867
3887
  }
3868
3888
  function revoked(locator) {
3869
- return ['collection', 'typed-selection'].some((attached) => {
3870
- const model = modelOf(locator, attached);
3871
- return model.status === 'unavailable' && model.reason === 'revoked';
3872
- });
3889
+ const model = modelOf(locator);
3890
+ return model.status === 'unavailable' && model.reason === 'revoked';
3873
3891
  }
3874
3892
  async function loadRoom(room, local, progress, live) {
3875
3893
  if (!live() || authorization === undefined)
3876
3894
  return;
3895
+ if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
3896
+ return;
3877
3897
  if (replicas.has(room.locator) && revoked(room.locator)) {
3878
3898
  await reload(room, local, progress, live);
3879
3899
  return;
@@ -3891,14 +3911,14 @@ export function composeRuntime(options) {
3891
3911
  reloadsDue.delete(room.locator);
3892
3912
  if (!live() || authorization === undefined)
3893
3913
  return;
3914
+ const from = standingOf(room.locator);
3894
3915
  const bootstrapped = await pullBootstrap(authorization, room, local, live);
3895
- if (!live())
3896
- return;
3897
3916
  if (bootstrapped.kind !== 'staged') {
3898
- await withhold(bootstrapped, room, local, progress);
3917
+ if (live())
3918
+ await withhold(bootstrapped, room, local, progress);
3899
3919
  return;
3900
3920
  }
3901
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3921
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
3902
3922
  await caughtUp(room, local, progress, live);
3903
3923
  }
3904
3924
  finally {
@@ -3920,6 +3940,8 @@ export function composeRuntime(options) {
3920
3940
  catch {
3921
3941
  result = { kind: 'transport-uncertainty' };
3922
3942
  }
3943
+ if (result.kind === 'discovery')
3944
+ roomsNamed = true;
3923
3945
  if (!live()) {
3924
3946
  progress.complete = false;
3925
3947
  notifyReady();
@@ -3927,12 +3949,7 @@ export function composeRuntime(options) {
3927
3949
  }
3928
3950
  journal.record(`connection discover ${result.kind}`);
3929
3951
  if (result.kind === 'transport-uncertainty') {
3930
- for (const room of rooms.values()) {
3931
- await retainModel(local, room, 'collection', cached(modelOf(room.locator)));
3932
- if (room.typedSelection !== null) {
3933
- await retainModel(local, room, 'typed-selection', cached(modelOf(room.locator, 'typed-selection')));
3934
- }
3935
- }
3952
+ await settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
3936
3953
  progress.reachable = false;
3937
3954
  progress.complete = false;
3938
3955
  notifyReady();
@@ -3987,6 +4004,7 @@ export function composeRuntime(options) {
3987
4004
  limits = result.providerLimits;
3988
4005
  await local.fact('providerLimits', limits);
3989
4006
  await local.fact('capabilities', capabilities);
4007
+ conversations.declared();
3990
4008
  }
3991
4009
  const signedIn = result.member ?? null;
3992
4010
  if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
@@ -4009,23 +4027,45 @@ export function composeRuntime(options) {
4009
4027
  }
4010
4028
  }
4011
4029
  async function admittedRead(work) {
4012
- if (roomReads.generation !== generation) {
4013
- roomReads = { generation, free: ROOM_READS, waiting: [] };
4030
+ while (roomReads.free === 0) {
4031
+ await new Promise((admit) => roomReads.waiting.push(admit));
4014
4032
  }
4015
- const reads = roomReads;
4016
- while (reads.free === 0)
4017
- await new Promise((admit) => reads.waiting.push(admit));
4018
- reads.free -= 1;
4033
+ roomReads.free -= 1;
4019
4034
  try {
4020
4035
  await work();
4021
4036
  }
4022
4037
  finally {
4023
- reads.free += 1;
4024
- reads.waiting.shift()?.();
4038
+ roomReads.free += 1;
4039
+ roomReads.waiting.shift()?.();
4025
4040
  }
4026
4041
  }
4042
+ function inRoom(locator, work) {
4043
+ roomWork += 1;
4044
+ const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
4045
+ try {
4046
+ return await work(continuing);
4047
+ }
4048
+ finally {
4049
+ roomWork -= 1;
4050
+ }
4051
+ });
4052
+ const chain = ran.then(() => undefined, () => undefined);
4053
+ roomLanes.set(locator, chain);
4054
+ working.add(chain);
4055
+ void chain.finally(() => {
4056
+ working.delete(chain);
4057
+ if (roomLanes.get(locator) === chain)
4058
+ roomLanes.delete(locator);
4059
+ });
4060
+ return ran;
4061
+ }
4027
4062
  function roomTask(locator, work) {
4028
- return task(roomLane(locator), (live) => admittedRead(() => work(live)));
4063
+ if (stopping)
4064
+ return Promise.resolve();
4065
+ return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
4066
+ }
4067
+ function advanced(room, local, authorized) {
4068
+ return inRoom(room.locator, (live) => advance(room, local, authorized, live));
4029
4069
  }
4030
4070
  function task(lane, work) {
4031
4071
  if (stopping)
@@ -4044,20 +4084,17 @@ export function composeRuntime(options) {
4044
4084
  function sync(attempt) {
4045
4085
  return task(OPENING_LANE, async (live) => {
4046
4086
  if (!live()) {
4047
- notifyReady();
4087
+ readyFromStore();
4048
4088
  return;
4049
4089
  }
4050
4090
  const progress = { complete: true, reachable: true };
4051
4091
  const gate = discover();
4052
- if (attempt === 'opened')
4053
- opened = generation;
4054
- underway = attempt === 'opened' ? { generation, progress } : null;
4092
+ underway = attempt === 'opened' ? progress : null;
4055
4093
  if (attempt === 'opened')
4056
4094
  streamed = progress;
4057
4095
  complete = false;
4058
4096
  recorded = false;
4059
4097
  moveTo(transferring(status));
4060
- handed = false;
4061
4098
  try {
4062
4099
  const local = await openLocal();
4063
4100
  if (!live())
@@ -4085,7 +4122,6 @@ export function composeRuntime(options) {
4085
4122
  if (handed || closed || progress.reachable)
4086
4123
  return;
4087
4124
  underway = null;
4088
- opened = null;
4089
4125
  journal.record('connection reopen');
4090
4126
  await rest(progress, live);
4091
4127
  options.adapter.roomStream?.reopen();
@@ -4122,13 +4158,14 @@ export function composeRuntime(options) {
4122
4158
  });
4123
4159
  }
4124
4160
  function closedSocket(revoked, lost) {
4161
+ notifyDiscovered();
4125
4162
  const opened = socketOpen;
4126
4163
  socketOpen = false;
4164
+ handed = false;
4165
+ naming = 0;
4127
4166
  connectionLost();
4128
- if (lost) {
4129
- reads.lost();
4167
+ if (lost)
4130
4168
  conversations.lost();
4131
- }
4132
4169
  if (revoked) {
4133
4170
  void refuse('revoked-authorization');
4134
4171
  return;
@@ -4143,14 +4180,13 @@ export function composeRuntime(options) {
4143
4180
  });
4144
4181
  }
4145
4182
  function applyOpening(expired) {
4146
- const progress = underway?.generation === generation ? underway.progress : null;
4183
+ const progress = underway;
4184
+ underway = null;
4185
+ naming = Math.max(0, naming - 1);
4147
4186
  const cycle = progress ?? { complete: true, reachable: true };
4148
- if (opened === generation)
4149
- opened = null;
4150
4187
  const reloading = progress === null ? [] : owedReloads(expired);
4151
4188
  const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
4152
4189
  return task(OPENING_LANE, async (live) => {
4153
- underway = null;
4154
4190
  if (!live())
4155
4191
  return;
4156
4192
  if (progress === null) {
@@ -4158,24 +4194,14 @@ export function composeRuntime(options) {
4158
4194
  await rest(cycle, live);
4159
4195
  return;
4160
4196
  }
4161
- await roomsCarried([...expired, ...reloading]);
4162
- if (!live())
4163
- return;
4164
4197
  try {
4165
4198
  const local = await openLocal();
4166
- const reloaded = await replacing;
4199
+ await replacing;
4167
4200
  if (progress.reachable) {
4168
- for (const room of discovered.values()) {
4169
- if (replicas.has(room.locator) &&
4170
- !expired.includes(room.locator) &&
4171
- !reloading.includes(room.locator)) {
4172
- await confirmModels(room, local);
4173
- }
4174
- }
4201
+ await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
4202
+ !expired.includes(room.locator) &&
4203
+ !reloading.includes(room.locator)), 'current', local);
4175
4204
  }
4176
- const failed = reloaded.find((outcome) => outcome.status === 'rejected');
4177
- if (failed !== undefined)
4178
- throw failed.reason;
4179
4205
  }
4180
4206
  catch {
4181
4207
  progress.complete = false;
@@ -4187,11 +4213,8 @@ export function composeRuntime(options) {
4187
4213
  }
4188
4214
  function owedReloads(expired) {
4189
4215
  const reloading = [...discovered.values()]
4190
- .filter((room) => replicas.has(room.locator) &&
4191
- !expired.includes(room.locator) &&
4192
- (owed.has(room.locator) || revoked(room.locator)))
4216
+ .filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
4193
4217
  .map((room) => room.locator);
4194
- owed.clear();
4195
4218
  for (const locator of reloading)
4196
4219
  catching.add(locator);
4197
4220
  liveQueries.recover();
@@ -4200,24 +4223,43 @@ export function composeRuntime(options) {
4200
4223
  async function reloadNow(replaced, progress) {
4201
4224
  const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
4202
4225
  for (const room of reloaded)
4203
- reloadsDue.set(room.locator, generation);
4226
+ reloadsDue.add(room.locator);
4204
4227
  try {
4205
4228
  const local = await openLocal();
4206
- return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
4229
+ for (const room of reloaded) {
4230
+ void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
4231
+ }
4207
4232
  }
4208
4233
  catch {
4209
4234
  for (const room of reloaded)
4210
4235
  reloadsDue.delete(room.locator);
4211
4236
  progress.reachable = false;
4212
- return [];
4213
4237
  }
4214
4238
  }
4215
- async function confirmModels(room, local) {
4216
- for (const attached of ['collection', 'typed-selection']) {
4217
- const model = modelOf(room.locator, attached);
4218
- if (model.status === 'cached') {
4219
- await retainModel(local, room, attached, { ...model, status: 'current' });
4239
+ async function settleModels(held, status, local) {
4240
+ const owed = (room) => {
4241
+ const model = modelOf(room.locator);
4242
+ return model.status !== 'unavailable' && model.status !== status;
4243
+ };
4244
+ const settled = await exclusive(async () => {
4245
+ if (!continuing())
4246
+ return [];
4247
+ const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
4248
+ for (const room of unchanged) {
4249
+ const next = { ...modelOf(room.locator), status };
4250
+ const replica = replicas.get(room.locator);
4251
+ if (replica !== undefined)
4252
+ replicas.set(room.locator, { ...replica, record: { ...replica.record, model: next } });
4253
+ heldModel(room, next);
4220
4254
  }
4255
+ if (unchanged.length > 0)
4256
+ emitRooms();
4257
+ return unchanged;
4258
+ });
4259
+ for (const room of held) {
4260
+ if (settled.includes(room) || !owed(room))
4261
+ continue;
4262
+ await retainModel(local, room, { ...modelOf(room.locator), status });
4221
4263
  }
4222
4264
  }
4223
4265
  function submitLater(locator) {
@@ -4262,14 +4304,10 @@ export function composeRuntime(options) {
4262
4304
  try {
4263
4305
  const local = await openLocal();
4264
4306
  await roomTask(locator, async (live) => {
4265
- const current = () => live() && !signal.aborted;
4266
- if (!current() || authorization === undefined)
4307
+ if (!live() || authorization === undefined)
4267
4308
  return;
4268
- const withheld = await Promise.race([
4269
- advance(room, local, authorization, current),
4270
- abandonedOn(signal),
4271
- ]);
4272
- if (withheld !== null && current())
4309
+ const withheld = await advance(room, local, authorization, live);
4310
+ if (withheld !== null && live())
4273
4311
  await withhold(withheld, room, local, progress);
4274
4312
  });
4275
4313
  }
@@ -4324,14 +4362,11 @@ export function composeRuntime(options) {
4324
4362
  progress.complete = false;
4325
4363
  return;
4326
4364
  }
4327
- reads.applied();
4328
4365
  submitAll();
4329
4366
  for (const locator of stale)
4330
4367
  if (repairable.has(locator))
4331
4368
  repairLater(locator, {});
4332
4369
  await conversations.reconcile();
4333
- if (status.providerUnreachable)
4334
- conversations.reconnected();
4335
4370
  const reading = { complete: true, reachable: true };
4336
4371
  await conversations.catchUp(reading);
4337
4372
  if (progress.reachable && reading.reachable)
@@ -4357,7 +4392,7 @@ export function composeRuntime(options) {
4357
4392
  if (!live())
4358
4393
  return;
4359
4394
  let lastCompleted = status.lastCompleted;
4360
- let synced = complete && stale.size === 0 && !pending();
4395
+ let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
4361
4396
  if (synced && !recorded) {
4362
4397
  const completedAt = new Date().toISOString();
4363
4398
  try {
@@ -4372,74 +4407,79 @@ export function composeRuntime(options) {
4372
4407
  }
4373
4408
  }
4374
4409
  moveTo({
4375
- state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
4410
+ state: pending()
4411
+ ? 'pending'
4412
+ : synced && complete && roomWork === 0 && naming === 0
4413
+ ? 'synced'
4414
+ : 'syncing',
4376
4415
  lastCompleted,
4377
- providerUnreachable: !progress.reachable,
4416
+ providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
4378
4417
  ...attention(),
4379
4418
  });
4380
4419
  }
4381
4420
  function discover() {
4421
+ discovering?.release();
4382
4422
  let release = () => { };
4383
4423
  const done = new Promise((resolve) => {
4384
4424
  release = resolve;
4385
4425
  });
4386
- discovering = { generation, done, release };
4426
+ discovering = { done, release };
4387
4427
  return discovering;
4388
4428
  }
4389
- function applyRoom(locator, roomVersion) {
4390
- announced.set(locator, roomVersion);
4391
- return task(roomLane(locator), async (live) => {
4392
- if (discovering?.generation === generation)
4393
- await discovering.done;
4394
- const saving = answering.get(locator);
4395
- if (saving?.generation === generation)
4396
- await saving.answered;
4397
- if (!live() || authorization === undefined)
4398
- return;
4399
- const room = discovered.get(locator);
4400
- const held = replicas.get(locator);
4401
- if (room === undefined || room.replica.admission !== 'admitted')
4402
- return;
4403
- if (held !== undefined &&
4404
- (held.record.replica.selection !== room.replica.selection ||
4405
- options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
4406
- return;
4407
- }
4408
- const during = underway?.generation === generation ? underway.progress : null;
4409
- const progress = during ?? { complete: true, reachable: true };
4410
- if (during === null)
4411
- moveTo(transferring(status));
4429
+ function applyRoom(change) {
4430
+ const { locator } = change;
4431
+ if (stopping)
4432
+ return Promise.resolve();
4433
+ return inRoom(locator, async (live) => {
4434
+ await discovering?.done;
4435
+ await answering.get(locator)?.answered;
4436
+ const progress = underway ?? { complete: true, reachable: true };
4412
4437
  try {
4413
4438
  const local = await openLocal();
4414
- await admittedRead(async () => {
4415
- if (!live() || authorization === undefined)
4416
- return;
4417
- const withheld = await advance(room, local, authorization, live);
4418
- if (withheld !== null)
4419
- await withhold(withheld, room, local, progress);
4420
- });
4439
+ for (;;) {
4440
+ if (!live())
4441
+ return null;
4442
+ const room = discovered.get(locator);
4443
+ const held = replicas.get(locator);
4444
+ if (room === undefined ||
4445
+ held === undefined ||
4446
+ room.replica.admission !== 'admitted' ||
4447
+ held.record.replica.selection !== room.replica.selection) {
4448
+ return null;
4449
+ }
4450
+ const at = held.record.roomVersion;
4451
+ if (options.adapter.reached(locator, at, change.roomVersion))
4452
+ return progress;
4453
+ if (!options.adapter.reached(locator, at, change.from)) {
4454
+ name(locator);
4455
+ return progress;
4456
+ }
4457
+ if (underway === null)
4458
+ moveTo(transferring(status));
4459
+ const writing = () => live() && !full;
4460
+ const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, () => change.read()));
4461
+ if (refreshed.kind !== 'advanced')
4462
+ return progress;
4463
+ const landing = {
4464
+ kind: 'refreshed',
4465
+ from: refreshed.from,
4466
+ remodelled: refreshed.remodelled,
4467
+ };
4468
+ if (await publish(local, room, refreshed.record, landing, writing)) {
4469
+ stale.delete(locator);
4470
+ await retainModel(local, room, modelOf(locator));
4471
+ return progress;
4472
+ }
4473
+ }
4421
4474
  }
4422
4475
  catch {
4476
+ if (!stale.has(locator))
4477
+ name(locator);
4423
4478
  stale.add(locator);
4479
+ return progress;
4424
4480
  }
4425
- finally {
4426
- if (during === null)
4427
- await rest(progress, live);
4428
- }
4429
- });
4481
+ }).then((progress) => rest(progress), () => undefined);
4430
4482
  }
4431
- function roomsCarried(replacing) {
4432
- const replaced = new Set(replacing.map(roomLane));
4433
- return Promise.all([...lanes]
4434
- .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
4435
- .map(([, held]) => held.chain));
4436
- }
4437
- const receipts = {
4438
- heads: async () => (await openLocal()).receivedHeads(),
4439
- parts: async (key, count) => (await openLocal()).receivedParts(key, count),
4440
- keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
4441
- drop: async (keys) => (await openLocal()).dropReceived(keys),
4442
- };
4443
4483
  function observeStream() {
4444
4484
  const stream = options.adapter.roomStream;
4445
4485
  if (stream === undefined || withdrawStream !== undefined)
@@ -4449,9 +4489,10 @@ export function composeRuntime(options) {
4449
4489
  return;
4450
4490
  if (event.kind === 'opened') {
4451
4491
  socketOpen = true;
4492
+ handed = false;
4493
+ naming = 0;
4452
4494
  connectionLive();
4453
4495
  generation += 1;
4454
- reads.opened();
4455
4496
  conversations.reopened();
4456
4497
  recovery.resume();
4457
4498
  void sync('opened');
@@ -4463,15 +4504,8 @@ export function composeRuntime(options) {
4463
4504
  else if (event.kind === 'authority-changed')
4464
4505
  void rediscover(event.scope);
4465
4506
  else
4466
- void applyRoom(event.locator, event.roomVersion);
4467
- }, receipts, heldRevisions);
4468
- }
4469
- async function heldRevisions(locator, paths) {
4470
- const held = replicas.get(locator);
4471
- if (held === undefined || paths.length === 0)
4472
- return new Map();
4473
- const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
4474
- return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
4507
+ void applyRoom(event);
4508
+ });
4475
4509
  }
4476
4510
  function writesHeld() {
4477
4511
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -4541,8 +4575,9 @@ export function composeRuntime(options) {
4541
4575
  if (closed)
4542
4576
  return;
4543
4577
  closed = true;
4544
- if (rechecking !== null)
4545
- clearInterval(rechecking);
4578
+ notifyDiscovered();
4579
+ if (probing !== null)
4580
+ clearInterval(probing);
4546
4581
  connectionLive();
4547
4582
  lockListeners.clear();
4548
4583
  readinessWatches.clear();
@@ -4553,11 +4588,8 @@ export function composeRuntime(options) {
4553
4588
  withdrawServiceMode?.();
4554
4589
  conversations.close();
4555
4590
  void recovery.retire();
4556
- store?.close();
4557
- void opening?.then((opened) => {
4558
- opened.close();
4559
- }, () => { });
4560
4591
  roomListeners.clear();
4592
+ unlistedRooms.clear();
4561
4593
  refusalListeners.clear();
4562
4594
  storageLossListeners.clear();
4563
4595
  statusListeners.clear();
@@ -4570,6 +4602,7 @@ export function composeRuntime(options) {
4570
4602
  }
4571
4603
  async function stop() {
4572
4604
  stopping = true;
4605
+ inFlight.abort();
4573
4606
  await recovery.retire();
4574
4607
  await Promise.all(working);
4575
4608
  await mutations;
@@ -4583,9 +4616,9 @@ export function composeRuntime(options) {
4583
4616
  observeStream();
4584
4617
  void openLocal().then(() => {
4585
4618
  if (capabilities.length > 0)
4586
- notifyReady();
4619
+ readyFromStore();
4587
4620
  }, () => {
4588
- notifyReady();
4621
+ readyFromStore();
4589
4622
  });
4590
4623
  if (options.adapter.roomStream === undefined)
4591
4624
  void sync('unstreamed');
@@ -4632,9 +4665,8 @@ export function composeRuntime(options) {
4632
4665
  }
4633
4666
  async function searched(input, reach, locked) {
4634
4667
  const asked = searchPlanOf(input);
4635
- const plan = { ...asked, limit: capped(asked.limit) };
4636
4668
  const filter = {
4637
- ...filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)),
4669
+ ...filterOf(asked.documents ?? {}, roomFacts(reach), planReach(reach)),
4638
4670
  locked,
4639
4671
  };
4640
4672
  const unlocking = (filter.rooms ?? [])
@@ -4643,65 +4675,41 @@ export function composeRuntime(options) {
4643
4675
  for (;;) {
4644
4676
  const cover = covered(filter);
4645
4677
  awaitPreparation(cover);
4646
- const pending = [
4647
- ...unlocking,
4648
- ...('pending' in cover ? cover.pending : []),
4649
- ];
4650
- const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
4651
- const held = covered({
4652
- ...filter,
4653
- rooms: reachedRooms(filter).filter((room) => !awaited.has(room)),
4654
- });
4655
- const covering = 'pending' in held ? [] : held.coverage.rooms;
4656
- const unindexed = covering
4657
- .filter(({ room }) => !searchIndex.rooms().has(room))
4658
- .map(({ room }) => room);
4659
- for (const room of unindexed)
4660
- pending.push({ room, awaits: 'index' });
4661
- const listed = conversations.searchable();
4662
- const named = reach !== null && !reach.conversations
4663
- ? []
4664
- : (plan.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
4665
- const waiting = named.filter((one) => !searchIndex.conversations().has(one));
4666
- for (const conversation of waiting)
4667
- pending.push({ conversation, awaits: 'index' });
4668
- if (unindexed.length > 0 || waiting.length > 0)
4669
- index(unindexed, waiting);
4678
+ const pending = [...unlocking, ...('pending' in cover ? cover.pending : [])];
4670
4679
  const complete = pending.length === 0 && !('pending' in cover);
4680
+ const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
4671
4681
  const answering = complete
4672
4682
  ? filter
4673
- : {
4674
- ...filter,
4675
- rooms: covering
4676
- .filter(({ room }) => searchIndex.rooms().has(room))
4677
- .map(({ room }) => room),
4678
- };
4683
+ : { ...filter, rooms: reachedRooms(filter).filter((room) => !awaited.has(room)) };
4679
4684
  const answered = covered(answering);
4680
4685
  if ('pending' in answered)
4681
4686
  continue;
4682
- const searchedConversations = named.filter((one) => searchIndex.conversations().has(one));
4683
- const seen = searchIndex.changes();
4684
- await searchIndex.caughtUp();
4685
- const selected = roomsAlone(answering) ? null : await everyRow(answering);
4686
- if (selected !== null && 'pending' in selected)
4687
- continue;
4688
- if (searchIndex.changes() !== seen ||
4689
- JSON.stringify(covered(answering)) !== JSON.stringify(answered))
4687
+ const listed = conversations.searchable();
4688
+ const named = reach !== null && !reach.conversations
4689
+ ? []
4690
+ : (asked.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
4691
+ const hits = await (await openLocal()).search({
4692
+ text: asked.text,
4693
+ limit: capped(asked.limit),
4694
+ after: asked.after,
4695
+ total: asked.total,
4696
+ documents: {
4697
+ plan: answering,
4698
+ rooms: answered.rooms,
4699
+ beneath: asked.collection
4700
+ ? answered.rooms.map(({ locator, key }) => ({
4701
+ key,
4702
+ roots: replicas.get(locator)?.record.typedSelection?.roots ?? [],
4703
+ }))
4704
+ : [],
4705
+ },
4706
+ conversations: named,
4707
+ });
4708
+ if (JSON.stringify(covered(answering)) !== JSON.stringify(answered))
4690
4709
  continue;
4691
- const hits = searchIndex.search(plan, selected === null ? null : selected.documents, searchedConversations, answering.rooms !== null
4692
- ? new Set(answering.rooms)
4693
- : locked.length === 0
4694
- ? null
4695
- : new Set(reachedRooms(answering)), plan.collection
4696
- ? new Map(answered.coverage.rooms.map(({ room }) => [
4697
- room,
4698
- replicas.get(room)?.record.typedSelection?.roots ?? [],
4699
- ]))
4700
- : null);
4701
4710
  const coverage = {
4702
4711
  rooms: answered.coverage.rooms,
4703
- conversations: searchedConversations,
4704
- index: searchIndex.generation(),
4712
+ conversations: named,
4705
4713
  overlay: answered.coverage.overlay,
4706
4714
  };
4707
4715
  return complete
@@ -4758,12 +4766,13 @@ export function composeRuntime(options) {
4758
4766
  };
4759
4767
  }
4760
4768
  if (topic === 'readiness') {
4761
- return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
4769
+ return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
4762
4770
  }
4763
4771
  if (topic === 'rooms') {
4764
- roomListeners.add(deliver);
4765
- deliver(listed());
4772
+ unlistedRooms.add(deliver);
4773
+ emitKnownRooms();
4766
4774
  return () => {
4775
+ unlistedRooms.delete(deliver);
4767
4776
  roomListeners.delete(deliver);
4768
4777
  };
4769
4778
  }
@@ -4817,7 +4826,17 @@ export function composeRuntime(options) {
4817
4826
  return openLocal().then(() => undefined);
4818
4827
  },
4819
4828
  rooms() {
4820
- return listed();
4829
+ if (closed)
4830
+ return Promise.resolve([]);
4831
+ return new Promise((settle) => {
4832
+ const answer = (value) => {
4833
+ unlistedRooms.delete(answer);
4834
+ roomListeners.delete(answer);
4835
+ settle(value);
4836
+ };
4837
+ unlistedRooms.add(answer);
4838
+ emitKnownRooms();
4839
+ });
4821
4840
  },
4822
4841
  query: ((input) => rowsQuery(planned(input, null))),
4823
4842
  search: (input) => searched(input, null, []),
@@ -4827,7 +4846,7 @@ export function composeRuntime(options) {
4827
4846
  typedSelection: rooms.get(locator)?.typedSelection === undefined ||
4828
4847
  rooms.get(locator)?.typedSelection === null
4829
4848
  ? null
4830
- : modelTypes(modelOf(locator, 'typed-selection')),
4849
+ : modelTypes(builtIn),
4831
4850
  };
4832
4851
  },
4833
4852
  roomDraft: {
@@ -4844,33 +4863,17 @@ export function composeRuntime(options) {
4844
4863
  const record = roomDrafts.get(room)?.get(path);
4845
4864
  return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
4846
4865
  },
4847
- write(room, path, revision, input) {
4848
- return exclusive(async () => {
4849
- const parts = readParts(input);
4850
- if (path === '' || normalize(path) !== path)
4851
- throw runtimeError('room-draft-malformed');
4852
- const local = await openLocal();
4853
- const current = roomDrafts.get(room)?.get(path);
4854
- if ((current?.revision ?? null) !== revision) {
4855
- return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
4856
- }
4857
- const file = parts.file === undefined
4858
- ? undefined
4859
- : await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
4860
- const record = {
4861
- locator: room,
4862
- path,
4863
- revision: roomDraftRevision(),
4864
- parts: held(current?.parts, parts, file),
4865
- returned: current?.returned ?? null,
4866
- };
4867
- await local.keepRoomDraft(record, parts.file === undefined || file === undefined
4868
- ? []
4869
- : [{ key: file.localId, blob: parts.file.blob }]);
4870
- rememberRoomDraft(record);
4871
- publishChangeSet(changeSet(room, { drafts: [path] }));
4872
- return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
4873
- });
4866
+ write(room, path, revision, input, next = roomDraftRevision()) {
4867
+ if (typeof next !== 'string' || next === '')
4868
+ return Promise.reject(runtimeError('room-draft-malformed'));
4869
+ return gathered(`room-draft\u0000${room}\u0000${path}`, { revision, input, next }, (earlier, later) => {
4870
+ if (later.revision !== earlier.next)
4871
+ return null;
4872
+ const parts = gatheredParts(earlier.input, later.input);
4873
+ return parts === null
4874
+ ? null
4875
+ : { revision: earlier.revision, input: parts, next: later.next };
4876
+ }, (written) => writtenRoomDraft(room, path, written.revision, written.input, written.next));
4874
4877
  },
4875
4878
  discard(room, path, revision) {
4876
4879
  return exclusive(async () => {
@@ -4897,13 +4900,13 @@ export function composeRuntime(options) {
4897
4900
  });
4898
4901
  },
4899
4902
  write(app, value) {
4900
- return keepPresentation(app, () => structuredClone(value));
4903
+ return exclusive(() => keptPresentation(app, () => structuredClone(value)));
4901
4904
  },
4902
4905
  edit(app, members) {
4903
- return keepPresentation(app, (held) => Object.fromEntries([
4904
- ...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(members, member)),
4905
- ...Object.entries(structuredClone(members)).filter(([, value]) => value !== null),
4906
- ]));
4906
+ return gathered(`presentation\u0000${app}`, structuredClone(members), (earlier, later) => ({ ...earlier, ...later }), (edited) => keptPresentation(app, (held) => Object.fromEntries([
4907
+ ...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(edited, member)),
4908
+ ...Object.entries(edited).filter(([, value]) => value !== null),
4909
+ ])));
4907
4910
  },
4908
4911
  },
4909
4912
  save(room, named, writtenBy) {
@@ -4922,7 +4925,8 @@ export function composeRuntime(options) {
4922
4925
  part: readPart,
4923
4926
  read: readBinaryPath,
4924
4927
  link: linkBinaryPath,
4925
- readReference: readBinaryReference,
4928
+ show: showBinaryPath,
4929
+ attach: attachBinary,
4926
4930
  },
4927
4931
  async call(name, input) {
4928
4932
  await ready;
@@ -4943,9 +4947,10 @@ export function composeRuntime(options) {
4943
4947
  },
4944
4948
  async retry() {
4945
4949
  journal.record('retry');
4946
- unindexable.clear();
4950
+ unpreparable.clear();
4947
4951
  queueReadiness();
4948
- resume();
4952
+ if (full)
4953
+ await probed();
4949
4954
  },
4950
4955
  observe: observing(watch),
4951
4956
  async reauthorize() {
@@ -4967,7 +4972,6 @@ export function composeRuntime(options) {
4967
4972
  const reachable = {
4968
4973
  md,
4969
4974
  watch,
4970
- referrerScope: (referrer) => options.adapter.referrerScope(referrer),
4971
4975
  plan: planned,
4972
4976
  query: (plan) => rowsQuery(plan),
4973
4977
  watchPlan,
@@ -4977,6 +4981,7 @@ export function composeRuntime(options) {
4977
4981
  lockedRooms,
4978
4982
  watchLocks,
4979
4983
  located,
4984
+ room: (locator) => listed().find((one) => one.locator === locator),
4980
4985
  };
4981
4986
  return {
4982
4987
  md,
@@ -4991,6 +4996,10 @@ export function composeRuntime(options) {
4991
4996
  journal.record(`persistence ${answer}`);
4992
4997
  queueReadiness();
4993
4998
  },
4994
- within: (reach, port) => narrowed(reachable, reach, port),
4999
+ within: (reach, port) => {
5000
+ if (full)
5001
+ void probed();
5002
+ return narrowed(reachable, reach, port);
5003
+ },
4995
5004
  };
4996
5005
  }