@agentshouse/mdruntime 0.1.0-c03e7b79830dd → 0.1.0-c069136dcb4e8

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 +47 -48
  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 -1174
  28. package/dist/conversation.d.ts +6 -9
  29. package/dist/conversation.d.ts.map +1 -1
  30. package/dist/conversation.js +191 -121
  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 +5 -3
  58. package/dist/memory.d.ts.map +1 -1
  59. package/dist/memory.js +96 -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 -244
  164. package/dist/chat/messages.d.ts +0 -30
  165. package/dist/chat/messages.d.ts.map +0 -1
  166. package/dist/chat/messages.js +0 -198
  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 -306
  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
- import { escapePointer, parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
2
- import { attachedModel, NO_ROOM_TYPES, roomTypesOf } from './attached-model.js';
1
+ import { parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
2
+ import { attachedModel, builtInModel, roomTypesOf } from './attached-model.js';
3
3
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
4
4
  import { observing, whole } from './observation.js';
5
5
  import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
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}`;
135
+ function modelOf(locator) {
136
+ return models.get(locator) ?? NO_MODEL;
129
137
  }
130
- function modelOf(locator, attached = 'collection') {
131
- return models.get(modelKey(locator, attached)) ?? NO_MODEL;
132
- }
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,73 +198,79 @@ 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
- agents: declaredAgents,
210
- documents: (declaration, after) => rowsQuery(planOf({
211
- rooms: [declaration.locator],
212
- types: [declaration.type],
213
- select: {
214
- fields: [
215
- declaration.identity,
216
- declaration.name,
217
- ...(declaration.retired === undefined ? [] : [declaration.retired]),
218
- ].map((field) => `/${escapePointer(field)}`),
219
- },
220
- limit: MAXIMUM_WINDOW,
221
- ...(after === undefined ? {} : { after }),
222
- }, roomFacts())),
223
249
  call: (name, input) => invokeAction(name, input),
224
250
  durably,
225
251
  stream: () => options.adapter.conversations,
226
252
  publish: publishChangeSet,
227
253
  online: () => connectionOf() !== 'unreachable' && refusal === null,
254
+ full: () => full,
228
255
  closed: () => closed,
229
256
  unanswered: resendLater,
230
257
  expired: (operation) => pastHorizon(operation),
231
258
  });
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
259
  const journal = createLog();
257
- const keptFiles = openedFiles(options.database, isFenced);
258
- let notifyReady = () => { };
260
+ const keptFiles = openedFiles(database, isFenced);
261
+ let readyFromStore = () => { };
259
262
  const ready = new Promise((resolve) => {
260
- notifyReady = resolve;
263
+ readyFromStore = resolve;
261
264
  });
265
+ let notifyDiscovered = () => { };
266
+ const firstDiscovery = new Promise((resolve) => {
267
+ notifyDiscovered = resolve;
268
+ });
269
+ let roomsNamed = false;
270
+ function notifyReady() {
271
+ notifyDiscovered();
272
+ readyFromStore();
273
+ }
262
274
  function rememberRoomDraft(record) {
263
275
  const room = roomDrafts.get(record.locator) ?? new Map();
264
276
  room.set(record.path, record);
@@ -278,7 +290,7 @@ export function composeRuntime(options) {
278
290
  return undefined;
279
291
  const local = await openLocal();
280
292
  const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
281
- return { ...slice, collection: collectionOf(slice.documents, compositionOf(held.record)) };
293
+ return { ...slice, collection: collectionOf(slice.documents, composed(held.record)) };
282
294
  }
283
295
  function roomDraftOf(record, visible) {
284
296
  return structuredClone({
@@ -334,14 +346,40 @@ export function composeRuntime(options) {
334
346
  const value = presentations.get(app);
335
347
  return value === undefined ? null : structuredClone(value);
336
348
  }
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
- });
349
+ async function writtenRoomDraft(room, path, revision, input, next) {
350
+ const parts = readParts(input);
351
+ if (path === '' || normalize(path) !== path)
352
+ throw runtimeError('room-draft-malformed');
353
+ const local = await openLocal();
354
+ const current = roomDrafts.get(room)?.get(path);
355
+ if ((current?.revision ?? null) !== revision) {
356
+ return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
357
+ }
358
+ const file = parts.file === undefined
359
+ ? undefined
360
+ : await heldFile(parts.file.blob, parts.file.base === undefined
361
+ ? binaryBase(await visibleOf(room, [path]), path)
362
+ : parts.file.base);
363
+ const record = {
364
+ locator: room,
365
+ path,
366
+ revision: next,
367
+ parts: held(current?.parts, parts, file),
368
+ returned: current?.returned ?? null,
369
+ };
370
+ await local.keepRoomDraft(record, parts.file === undefined || file === undefined
371
+ ? []
372
+ : [{ key: file.localId, blob: parts.file.blob }]);
373
+ rememberRoomDraft(record);
374
+ publishChangeSet(changeSet(room, { drafts: [path] }));
375
+ return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
376
+ }
377
+ async function keptPresentation(app, next) {
378
+ const local = await openLocal();
379
+ const stored = next(presentations.get(app));
380
+ await local.present({ app, value: stored });
381
+ presentations.set(app, stored);
382
+ publishChangeSet(changeSet('', { presentation: app }));
345
383
  }
346
384
  function emitPresentation(app) {
347
385
  if (closed)
@@ -356,6 +394,20 @@ export function composeRuntime(options) {
356
394
  (queuesWithoutReplica.size > 0 ||
357
395
  [...replicas.values()].some((replica) => replica.queue.length > 0)));
358
396
  }
397
+ function unsent() {
398
+ const changed = new Map();
399
+ for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
400
+ for (const { batch, state } of entriesOf(room)) {
401
+ if (state === 'local')
402
+ continue;
403
+ for (const { path } of batch.changes)
404
+ changed.set(`${room}\u0000${path}`, { room, path });
405
+ }
406
+ }
407
+ return [...changed]
408
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
409
+ .map(([, change]) => change);
410
+ }
359
411
  function attention() {
360
412
  const attentionRequired = [];
361
413
  for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
@@ -367,16 +419,9 @@ export function composeRuntime(options) {
367
419
  returned: structuredClone(record.returned),
368
420
  });
369
421
  }
370
- for (const [room, refusal] of sourceRefusals)
371
- attentionRequired.push({ room, refusal: structuredClone(refusal) });
372
422
  attentionRequired.sort((left, right) => {
373
423
  if (left.room !== right.room)
374
424
  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
425
  if (left.path !== right.path)
381
426
  return left.path < right.path ? -1 : 1;
382
427
  return 0;
@@ -388,7 +433,7 @@ export function composeRuntime(options) {
388
433
  if (reset === null || submission.reset < reset)
389
434
  reset = submission.reset;
390
435
  }
391
- return { attentionRequired, reset };
436
+ return { attentionRequired, unsent: unsent(), reset };
392
437
  }
393
438
  function documentsIn(locator) {
394
439
  return replicas.get(locator)?.count ?? 0;
@@ -461,7 +506,7 @@ export function composeRuntime(options) {
461
506
  const governed = governs(reach, room);
462
507
  const attached = [
463
508
  ...(governed.collection ? [modelOf(room.locator)] : []),
464
- ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
509
+ ...(governed.selection ? [builtIn] : []),
465
510
  ];
466
511
  return {
467
512
  locator: room.locator,
@@ -477,11 +522,26 @@ export function composeRuntime(options) {
477
522
  const value = listed();
478
523
  for (const listener of roomListeners)
479
524
  listener(value);
525
+ emitKnownRooms();
526
+ }
527
+ function emitKnownRooms() {
528
+ conversations.learned();
529
+ if (unlistedRooms.size === 0)
530
+ return;
531
+ if (!roomsListed && rooms.size === 0 && refusal === null && !status.providerUnreachable)
532
+ return;
533
+ const value = listed();
534
+ for (const listener of [...unlistedRooms]) {
535
+ unlistedRooms.delete(listener);
536
+ roomListeners.add(listener);
537
+ listener(value);
538
+ }
480
539
  }
481
540
  function emitRefusal() {
482
541
  queueReadiness();
483
542
  for (const listener of refusalListeners)
484
543
  listener(refusal);
544
+ emitKnownRooms();
485
545
  }
486
546
  function emitStorageLoss() {
487
547
  queueReadiness();
@@ -502,6 +562,7 @@ export function composeRuntime(options) {
502
562
  if (closed)
503
563
  return;
504
564
  queueReadiness();
565
+ emitKnownRooms();
505
566
  const next = status.state === 'synced' && !conversations.current()
506
567
  ? { ...status, state: 'syncing' }
507
568
  : status;
@@ -515,9 +576,6 @@ export function composeRuntime(options) {
515
576
  if (localSaves)
516
577
  return Object.freeze({ batch, state: 'local' });
517
578
  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
579
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
522
580
  }
523
581
  function entriesOf(locator) {
@@ -532,23 +590,29 @@ export function composeRuntime(options) {
532
590
  }
533
591
  function applyChangeSet(set) {
534
592
  liveQueries.touched(set);
535
- searchIndex.changed(set);
536
593
  }
537
594
  function unpublished(error) {
538
595
  journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
539
596
  }
597
+ function indexed(locator) {
598
+ return replicas.get(locator)?.indexed === true;
599
+ }
540
600
  async function prepareReplica(locator) {
541
601
  for (;;) {
542
602
  const held = replicas.get(locator);
543
- if (held === undefined || closed || prepared.has(locator))
603
+ if (held === undefined || closed || held.indexed)
544
604
  return;
545
605
  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)
606
+ const done = await exclusive(async () => {
607
+ const local = await openLocal();
608
+ return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR));
609
+ });
610
+ const now = replicas.get(locator);
611
+ if (now?.record.key !== key)
548
612
  continue;
549
- if (!done)
613
+ if (!done.done)
550
614
  continue;
551
- prepared.add(locator);
615
+ replicas.set(locator, { ...now, indexed: true });
552
616
  publishChangeSet(changeSet(locator, { documents: null }));
553
617
  liveQueries.recover();
554
618
  queueReadiness();
@@ -556,11 +620,11 @@ export function composeRuntime(options) {
556
620
  }
557
621
  }
558
622
  function unprepared() {
559
- return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
623
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
560
624
  }
561
- function unbuilt(locator, error) {
562
- unindexable.add(locator);
563
- journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
625
+ function unpreparedBy(locator, error) {
626
+ unpreparable.add(locator);
627
+ journal.record(`preparation failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
564
628
  queueReadiness();
565
629
  }
566
630
  function prepare(locators = []) {
@@ -576,7 +640,7 @@ export function composeRuntime(options) {
576
640
  if (closed)
577
641
  return;
578
642
  unpreparedDemand.delete(waiting);
579
- await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
643
+ await prepareReplica(waiting).catch((error) => unpreparedBy(waiting, error));
580
644
  }
581
645
  }
582
646
  finally {
@@ -585,7 +649,11 @@ export function composeRuntime(options) {
585
649
  })();
586
650
  }
587
651
  function roomsOfReach(reach) {
588
- return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
652
+ return new Set([
653
+ ...reach.rooms,
654
+ ...reach.documents.map(roomOfKey),
655
+ ...reach.areas.flatMap((area) => (area.types.length === 0 ? [] : [area.room])),
656
+ ]);
589
657
  }
590
658
  function reachedRooms(plan) {
591
659
  const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
@@ -617,14 +685,14 @@ export function composeRuntime(options) {
617
685
  pending.push({ room: locator, awaits: 'acquisition' });
618
686
  continue;
619
687
  }
620
- if (!prepared.has(locator)) {
688
+ if (!replica.indexed) {
621
689
  pending.push({ room: locator, awaits: 'preparation' });
622
690
  continue;
623
691
  }
624
692
  coverage.push({
625
693
  room: locator,
626
694
  generation: replica.record.transfer,
627
- current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
695
+ current: !(catching.has(locator) || transfers.has(locator)),
628
696
  });
629
697
  read.push({ locator, key: replica.record.key });
630
698
  for (const batch of replica.queue)
@@ -650,9 +718,7 @@ export function composeRuntime(options) {
650
718
  return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
651
719
  return { listed: named.every((room) => replicas.has(room)), rooms: named };
652
720
  }
653
- function roomState(locator, search) {
654
- if (sourceRefusals.has(locator))
655
- return 'refused';
721
+ function roomState(locator) {
656
722
  const listedRoom = discovered.get(locator);
657
723
  if (roomsListed &&
658
724
  (listedRoom === undefined ||
@@ -661,27 +727,17 @@ export function composeRuntime(options) {
661
727
  return 'unadmitted';
662
728
  if (!replicas.has(locator))
663
729
  return 'acquiring';
664
- if (!prepared.has(locator))
730
+ if (!indexed(locator))
665
731
  return 'preparing';
666
- if (search && !searchIndex.rooms().has(locator))
667
- return 'indexing';
668
732
  return 'complete';
669
733
  }
670
- function conversationCoverage(reach, search) {
734
+ function conversationCoverage(reach) {
671
735
  const declared = reach === null || reach.conversations;
672
- if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
736
+ if (!declared || options.adapter.conversations === undefined)
673
737
  return 'undeclared';
674
738
  if (!conversations.known() || !conversations.current())
675
739
  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';
740
+ return 'complete';
685
741
  }
686
742
  function connectionOf() {
687
743
  if (socketOpen)
@@ -692,58 +748,79 @@ export function composeRuntime(options) {
692
748
  ? 'connected'
693
749
  : 'connecting';
694
750
  }
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
- });
751
+ function bytesOf(room, underway) {
752
+ const transfer = transfers.get(room);
753
+ if (transfer !== undefined) {
754
+ const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
755
+ return {
756
+ done: transfer.done.bytes - Math.ceil(owed),
757
+ total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
758
+ };
759
+ }
760
+ const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
761
+ return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
762
+ }
763
+ function installationOf(coverage) {
764
+ const rooms = { done: 0, total: 0 };
765
+ const bytes = { done: 0, total: 0 };
766
+ let installing = 0;
767
+ let downloading = false;
768
+ for (const { room, state } of coverage) {
769
+ if (state === 'unadmitted')
770
+ continue;
771
+ rooms.total += 1;
772
+ if (state === 'complete')
773
+ rooms.done += 1;
774
+ const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
775
+ if (underway)
776
+ installing += 1;
777
+ if (underway && (streaming.has(room) || !arrivals.has(room)))
778
+ downloading = true;
779
+ const now = bytesOf(room, underway);
780
+ const earlier = underway ? startedOver.get(room) : undefined;
781
+ bytes.done += Math.max(now.done, earlier?.done ?? 0);
782
+ bytes.total += Math.max(now.total, earlier?.total ?? 0);
783
+ }
784
+ const step = installing === 0
785
+ ? null
786
+ : downloading
787
+ ? 'downloading'
788
+ : bytes.done < bytes.total
789
+ ? 'writing'
790
+ : 'replaying';
791
+ return { step, rooms, bytes };
710
792
  }
711
793
  function readinessFor(port, reach) {
712
794
  const required = requiredRooms(reach);
713
795
  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))
796
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
797
+ const preparing = coverage
798
+ .filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
717
799
  .map(({ room }) => room);
718
- const preparing = building('preparing');
719
800
  if (preparing.length > 0)
720
801
  prepare(preparing);
721
- const indexing = building('indexing');
722
- if (indexing.length > 0)
723
- index(indexing, []);
724
802
  return readinessOf({
725
803
  code: port.code,
726
804
  coverage: {
727
805
  listed: required.listed,
728
806
  rooms: coverage,
729
- conversations: conversationCoverage(reach, search),
807
+ conversations: conversationCoverage(reach),
730
808
  search: search ? 'required' : 'unused',
731
809
  },
732
810
  persistence,
733
811
  connection: connectionOf(),
734
812
  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),
813
+ space: full ? 'full' : port.space,
814
+ installation: installationOf(coverage),
740
815
  lost: storageLoss !== null,
741
- established: targets[port.target]?.established === true,
816
+ established: targets[port.target]?.established === true ||
817
+ (usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
742
818
  });
743
819
  }
744
820
  function establish(name) {
745
821
  targets = { ...targets, [name]: { established: true } };
746
822
  queueReadiness();
823
+ probeEachMinute();
747
824
  void durably((local) => local.fact('targets', targets)).catch(() => {
748
825
  journal.record(`target record failed ${name}`);
749
826
  });
@@ -781,87 +858,52 @@ export function composeRuntime(options) {
781
858
  readinessWatches.delete(watch);
782
859
  };
783
860
  }
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 ?? '-'}`);
861
+ function filled() {
862
+ if (full || closed)
863
+ return;
864
+ full = true;
865
+ journal.record('space full');
803
866
  queueReadiness();
804
- if (rechecking !== null)
867
+ probeEachMinute();
868
+ }
869
+ function installedTarget() {
870
+ return ((usableWhenHeld.length > 0 && reopened.size > 0) ||
871
+ Object.values(targets).some((target) => target.established));
872
+ }
873
+ function probeEachMinute() {
874
+ if (!full || probing !== null || closed || !installedTarget())
805
875
  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();
876
+ // 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.
877
+ probing = setInterval(() => {
878
+ void probed();
809
879
  }, 60_000);
810
880
  }
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();
881
+ function probed() {
882
+ testing ??= database.ask({ kind: 'space.probe' }).then(() => {
883
+ testing = null;
884
+ spaceFreed();
885
+ return true;
886
+ }, () => {
887
+ testing = null;
888
+ return false;
889
+ });
890
+ return testing;
822
891
  }
823
- function resume() {
824
- if (rechecking !== null)
825
- clearInterval(rechecking);
826
- rechecking = null;
827
- if (paused.size === 0 && capacity === null)
892
+ function spaceFreed() {
893
+ if (!full)
828
894
  return;
829
- paused.clear();
830
- capacity = null;
831
- journal.record('capacity resumed');
895
+ full = false;
896
+ if (probing !== null)
897
+ clearInterval(probing);
898
+ probing = null;
899
+ unpreparable.clear();
900
+ journal.record('space freed');
832
901
  queueReadiness();
833
902
  if (!closed && authorization !== undefined)
834
903
  void sync('unstreamed');
835
904
  }
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
905
  function seedRoomHeld(room, version) {
864
- return prepared.has(room) && reachedBy(room, version);
906
+ return indexed(room) && reachedBy(room, version);
865
907
  }
866
908
  function seedHeld(at) {
867
909
  return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
@@ -982,83 +1024,12 @@ export function composeRuntime(options) {
982
1024
  const held = answered.cover;
983
1025
  return { ...answered.rows, coverage: held.coverage };
984
1026
  }
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
1027
  function publishChangeSet(set) {
1061
1028
  applyChangeSet(set);
1029
+ for (const { locator, path } of set.documents) {
1030
+ if (path !== null)
1031
+ attachedFiles.get(locator)?.delete(path);
1032
+ }
1062
1033
  if (set.rooms.length > 0 ||
1063
1034
  [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
1064
1035
  emitRooms();
@@ -1077,6 +1048,8 @@ export function composeRuntime(options) {
1077
1048
  }
1078
1049
  async function refuse(reason, provider = null) {
1079
1050
  discoveryPaused = true;
1051
+ inFlight.abort();
1052
+ inFlight = new AbortController();
1080
1053
  try {
1081
1054
  if (reason === 'revoked-authorization') {
1082
1055
  await exclusive(async () => {
@@ -1084,27 +1057,24 @@ export function composeRuntime(options) {
1084
1057
  const records = [...replicas].map(([locator, held]) => ({
1085
1058
  locator,
1086
1059
  held,
1087
- record: { ...held.record, model: revoked, selectionModel: revoked },
1060
+ record: { ...held.record, model: revoked },
1088
1061
  }));
1062
+ let answer = null;
1089
1063
  try {
1090
1064
  const local = await openLocal();
1091
- await local.commit({
1065
+ answer = await local.commit({
1092
1066
  rooms: records.map(({ locator, record }) => ({
1093
1067
  locator,
1094
1068
  key: record.key,
1095
1069
  record: storedRecord(record),
1096
- composition: compositionOf(record),
1070
+ composed: composedOf(composed(record)),
1097
1071
  })),
1098
1072
  });
1099
1073
  }
1100
1074
  catch {
1101
1075
  journal.record('model revocation persistence failed');
1102
1076
  }
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
- }
1077
+ const keys = new Set([...models.keys(), ...rooms.keys()]);
1108
1078
  for (const key of keys) {
1109
1079
  invalidModels.add(key);
1110
1080
  models.set(key, revoked);
@@ -1114,8 +1084,8 @@ export function composeRuntime(options) {
1114
1084
  for (const { locator, held, record } of records) {
1115
1085
  if (replicas.get(locator) !== held)
1116
1086
  continue;
1117
- recomposed(locator, held.record, record);
1118
- replicas.set(locator, { ...held, record });
1087
+ const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
1088
+ replicas.set(locator, { ...held, record, indexed });
1119
1089
  }
1120
1090
  for (const locator of [...rooms.keys()]) {
1121
1091
  const room = rooms.get(locator);
@@ -1159,12 +1129,12 @@ export function composeRuntime(options) {
1159
1129
  ? null
1160
1130
  : {
1161
1131
  roots: selection.roots,
1162
- model: modelTypes(modelOf(room.locator, 'typed-selection')),
1132
+ model: modelTypes(builtIn),
1163
1133
  },
1164
1134
  providerOwnedPaths: room.providerOwnedPaths,
1165
1135
  roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
1166
1136
  synchronizationAvailable,
1167
- refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
1137
+ refusal: scopedRefusals.get(room.locator) ?? null,
1168
1138
  });
1169
1139
  }
1170
1140
  async function applyDiscovery(found, started, covered) {
@@ -1248,30 +1218,23 @@ export function composeRuntime(options) {
1248
1218
  queuesWithoutReplica.delete(locator);
1249
1219
  rooms.delete(locator);
1250
1220
  roomDrafts.delete(locator);
1251
- transfers.delete(locator);
1221
+ attachedFiles.delete(locator);
1222
+ untransferred(locator);
1223
+ startedOver.delete(locator);
1252
1224
  catching.delete(locator);
1253
1225
  discovered.delete(locator);
1254
- announced.delete(locator);
1255
1226
  unreadable.delete(locator);
1256
1227
  stale.delete(locator);
1257
1228
  repairable.delete(locator);
1258
- unindexable.delete(locator);
1259
- paused.delete(locator);
1260
- prepared.delete(locator);
1229
+ unpreparable.delete(locator);
1261
1230
  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
- }
1231
+ models.delete(locator);
1232
+ liveModels.delete(locator);
1233
+ invalidModels.delete(locator);
1270
1234
  journal.record(`room lost ${locator}`);
1271
1235
  }
1272
1236
  if (fresh.length === 0)
1273
1237
  return fresh;
1274
- searchIndex.forget(fresh);
1275
1238
  publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, { documents: null, room: true, queue: true, drafts }))));
1276
1239
  liveQueries.recover();
1277
1240
  moveTo({ ...status, ...attention() });
@@ -1315,7 +1278,14 @@ export function composeRuntime(options) {
1315
1278
  });
1316
1279
  await eraseFenced(fresh);
1317
1280
  }
1318
- function restored(key, persisted) {
1281
+ function retyped(persisted) {
1282
+ if (persisted.status === 'unavailable')
1283
+ return persisted;
1284
+ const { state } = attachedModel({ kind: 'model', snapshot: persisted.snapshot }, NO_MODEL, roomTypes);
1285
+ return state.status === 'unavailable' ? state : { ...persisted, types: state.types };
1286
+ }
1287
+ function restored(key, held) {
1288
+ const persisted = retyped(held);
1319
1289
  const prior = models.get(key);
1320
1290
  if (prior !== undefined &&
1321
1291
  prior.status !== 'unavailable' &&
@@ -1346,8 +1316,9 @@ export function composeRuntime(options) {
1346
1316
  const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1347
1317
  targets = held.targets;
1348
1318
  transfers.clear();
1349
- for (const [locator, record] of held.transfers)
1350
- transfers.set(locator, record);
1319
+ arrivals.clear();
1320
+ streaming.clear();
1321
+ startedOver.clear();
1351
1322
  limits = held.providerLimits;
1352
1323
  capabilities = held.capabilities;
1353
1324
  member = held.member;
@@ -1357,7 +1328,7 @@ export function composeRuntime(options) {
1357
1328
  presentations.clear();
1358
1329
  for (const record of held.presentation)
1359
1330
  presentations.set(record.app, record.value);
1360
- conversations.hydrate(heldConversations, chatDrafts);
1331
+ conversations.hydrate(heldConversations, chatDrafts, held.conversationList);
1361
1332
  pendings.clear();
1362
1333
  for (const queue of queues) {
1363
1334
  for (const { batch, state } of queue.pending) {
@@ -1365,7 +1336,7 @@ export function composeRuntime(options) {
1365
1336
  }
1366
1337
  }
1367
1338
  replicas.clear();
1368
- prepared.clear();
1339
+ reopened.clear();
1369
1340
  queuesWithoutReplica.clear();
1370
1341
  for (const [locator, queue] of queuedBatches) {
1371
1342
  if (!held.rooms.has(locator))
@@ -1374,39 +1345,40 @@ export function composeRuntime(options) {
1374
1345
  records.forEach((stored, index) => {
1375
1346
  if (fences.has(stored.locator))
1376
1347
  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 };
1348
+ const record = { ...stored, model: restored(stored.locator, stored.model) };
1349
+ reopened.add(record.locator);
1380
1350
  replicas.set(record.locator, {
1381
1351
  record,
1382
1352
  queue: queuedBatches.get(record.locator) ?? [],
1383
1353
  overlaid: held.overlaid.get(record.key) ?? new Set(),
1384
1354
  count: counts[index] ?? 0,
1385
1355
  confirmed: 0,
1356
+ indexed: !held.unindexed.has(record.key),
1386
1357
  });
1387
- if (!held.unprepared.has(record.key))
1388
- prepared.add(record.locator);
1389
1358
  const existing = rooms.get(record.locator);
1390
1359
  if (existing === undefined)
1391
- list(record, false);
1360
+ list(record, record.roomVersion !== null);
1392
1361
  else {
1393
1362
  rooms.set(record.locator, {
1394
1363
  ...existing,
1395
1364
  roomVersion: record.roomVersion,
1396
1365
  operations: record.operations,
1397
1366
  replica: record.replica,
1398
- model: modelTypes(model),
1367
+ model: modelTypes(record.model),
1399
1368
  typedSelection: record.typedSelection === null
1400
1369
  ? null
1401
- : {
1402
- roots: record.typedSelection.roots,
1403
- model: modelTypes(selectionModel),
1404
- },
1370
+ : { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
1405
1371
  });
1406
1372
  }
1407
1373
  });
1408
- for (const key of held.orphans)
1409
- await opened.release(key);
1374
+ try {
1375
+ for (const key of held.orphans)
1376
+ await opened.release(key);
1377
+ }
1378
+ catch (error) {
1379
+ if (!refusedForSpace(error))
1380
+ throw error;
1381
+ }
1410
1382
  if (held.erasing || held.orphans.length > 0)
1411
1383
  queueMicrotask(sweep);
1412
1384
  return {
@@ -1431,8 +1403,7 @@ export function composeRuntime(options) {
1431
1403
  async function open() {
1432
1404
  if (closed)
1433
1405
  throw new Error('runtime closed');
1434
- const opened = await openRuntimeStore(isFenced, options.database);
1435
- store = opened;
1406
+ const opened = openRuntimeStore(isFenced, database);
1436
1407
  const interrupted = await opened.fences();
1437
1408
  for (const locator of interrupted) {
1438
1409
  fences.set(locator, { at: discoveries, erased: false });
@@ -1478,10 +1449,29 @@ export function composeRuntime(options) {
1478
1449
  return opening;
1479
1450
  }
1480
1451
  function exclusive(work) {
1452
+ gathering = null;
1481
1453
  const next = mutations.then(work, work);
1482
1454
  mutations = next.then(() => undefined, () => undefined);
1483
1455
  return next;
1484
1456
  }
1457
+ function gathered(key, input, merge, work) {
1458
+ const tail = gathering;
1459
+ if (tail !== null && tail.key === key) {
1460
+ const merged = merge(tail.input, input);
1461
+ if (merged !== null) {
1462
+ tail.input = merged;
1463
+ return tail.done;
1464
+ }
1465
+ }
1466
+ const entry = { key, input, done: Promise.resolve() };
1467
+ entry.done = exclusive(() => {
1468
+ if (gathering === entry)
1469
+ gathering = null;
1470
+ return work(entry.input);
1471
+ });
1472
+ gathering = entry;
1473
+ return entry.done;
1474
+ }
1485
1475
  function withdrawable(locator) {
1486
1476
  return (batch) => {
1487
1477
  const state = pendings.get(pendingKey(locator, batch.id));
@@ -1563,26 +1553,14 @@ export function composeRuntime(options) {
1563
1553
  },
1564
1554
  };
1565
1555
  }
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))
1556
+ function committedRoom(locator, answer) {
1557
+ const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
1558
+ if (!indexed)
1581
1559
  prepare([locator]);
1582
- return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0 };
1560
+ return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
1583
1561
  }
1584
1562
  function replayed(record, confirmedLayer, queue) {
1585
- return stepped(replaying(confirmedLayer, queue, record.locator, compositionOf(record)));
1563
+ return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
1586
1564
  }
1587
1565
  async function overlayDiff(record, before, visible, touched) {
1588
1566
  const kept = before !== undefined && before.record.key === record.key
@@ -1602,8 +1580,6 @@ export function composeRuntime(options) {
1602
1580
  function unpairedIn(locator, batch) {
1603
1581
  if (!queueIn(locator).some((held) => held.id === batch.id))
1604
1582
  return false;
1605
- if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
1606
- return false;
1607
1583
  return !leavingIn(batch, locator) || withdrawable(locator)(batch);
1608
1584
  }
1609
1585
  async function together(local, settlements) {
@@ -1645,7 +1621,7 @@ export function composeRuntime(options) {
1645
1621
  const confirmedLayer = overrides === undefined
1646
1622
  ? read
1647
1623
  : await stepped(slicingWith(read, overrides.set, overrides.removed));
1648
- const composition = compositionOf(record);
1624
+ const composition = composed(record);
1649
1625
  const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
1650
1626
  const founded = kept.filter((batch) => !queue.includes(batch));
1651
1627
  const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
@@ -1688,6 +1664,7 @@ export function composeRuntime(options) {
1688
1664
  key: record.key,
1689
1665
  record: storedRecord(record),
1690
1666
  roots: record.typedSelection?.roots ?? [],
1667
+ composed: composedOf(composition),
1691
1668
  ...settling.commit,
1692
1669
  ...(overrides === undefined
1693
1670
  ? {}
@@ -1729,14 +1706,14 @@ export function composeRuntime(options) {
1729
1706
  const confirmedChanged = settling.commit?.published !== undefined ||
1730
1707
  settling.commit?.refreshed === true ||
1731
1708
  overrides !== undefined;
1732
- recomposed(locator, before?.record, record);
1733
- const { count } = committedRoom(locator, record, answer);
1709
+ const { count, indexed } = committedRoom(locator, answer);
1734
1710
  replicas.set(locator, {
1735
1711
  record,
1736
1712
  queue: kept,
1737
1713
  overlaid: touched,
1738
1714
  count,
1739
1715
  confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
1716
+ indexed,
1740
1717
  });
1741
1718
  return mergeChangeSets([
1742
1719
  changeSet(locator, {
@@ -1751,103 +1728,96 @@ export function composeRuntime(options) {
1751
1728
  };
1752
1729
  }
1753
1730
  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
- }
1731
+ const model = record.model;
1732
+ models.set(locator, model);
1733
+ invalidModels.delete(locator);
1734
+ if (model.status === 'current')
1735
+ liveModels.set(locator, JSON.stringify(model.snapshot));
1736
+ else if (model.status === 'unavailable')
1737
+ liveModels.delete(locator);
1766
1738
  }
1767
1739
  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
- };
1740
+ const answer = answers?.collection;
1741
+ if (answer === undefined)
1742
+ return held;
1743
+ const { state, refused } = attachedModel(answer, held.collection, roomTypes);
1744
+ if (refused !== null) {
1745
+ const where = refused.location === undefined ? '' : ` ${refused.location}`;
1746
+ journal.record(`model invalid ${locator} collection ${refused.identity} ${refused.rule}${where}`);
1747
+ }
1748
+ models.set(locator, state);
1749
+ return { collection: state };
1783
1750
  }
1784
- function publish(local, room, record, landing, live) {
1785
- return exclusive(async () => {
1751
+ function publish(local, room, reached, landing, live) {
1752
+ const landed = exclusive(async () => {
1786
1753
  if (!live()) {
1787
1754
  caughtUpTo(room.locator);
1788
1755
  return true;
1789
1756
  }
1790
1757
  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);
1758
+ if (!stands(room.locator, landing.from)) {
1759
+ if (landing.kind === 'refreshed') {
1760
+ await local.unrefresh(reached.key);
1761
+ return false;
1762
+ }
1763
+ caughtUpTo(room.locator);
1764
+ await abandoned(local, room.locator);
1794
1765
  return false;
1795
1766
  }
1767
+ const record = landing.kind === 'refreshed' && prior !== undefined
1768
+ ? {
1769
+ ...prior.record,
1770
+ roomVersion: reached.roomVersion,
1771
+ ...(landing.remodelled ? { model: reached.model } : {}),
1772
+ }
1773
+ : reached;
1796
1774
  adoptModels(room.locator, record);
1797
1775
  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
1776
  let set;
1804
1777
  try {
1805
- set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
1778
+ set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
1806
1779
  ? {
1807
1780
  room: true,
1808
1781
  view: 'refreshed',
1809
- commit: { refreshed: true, composition: compositionOf(record) },
1782
+ commit: { refreshed: true },
1810
1783
  }
1811
1784
  : {
1812
1785
  room: true,
1813
1786
  view: landing.refreshed ? 'refreshed' : 'confirmed',
1814
1787
  commit: {
1815
1788
  published: { from: prior?.record.key ?? null },
1816
- composition: compositionOf(record),
1817
1789
  ...(landing.refreshed ? { refreshed: true } : {}),
1818
1790
  },
1819
1791
  });
1820
1792
  }
1821
1793
  catch (error) {
1822
- if (!exceeded(error))
1794
+ if (!refusedForSpace(error))
1823
1795
  throw error;
1824
- await exceededOn(room.locator, null);
1825
1796
  caughtUpTo(room.locator);
1826
1797
  return true;
1827
1798
  }
1828
1799
  if (landing.kind === 'bootstrapped') {
1829
- transfers.delete(room.locator);
1800
+ untransferred(room.locator);
1801
+ startedOver.delete(room.locator);
1830
1802
  catching.delete(room.locator);
1831
- sweep();
1803
+ if (prior !== undefined && prior.record.key !== record.key)
1804
+ sweep();
1805
+ name(room.locator);
1832
1806
  }
1833
- for (const batch of accepted)
1834
- pendings.delete(pendingKey(room.locator, batch.id));
1835
1807
  stale.delete(room.locator);
1836
- unindexable.delete(room.locator);
1808
+ unpreparable.delete(room.locator);
1837
1809
  if (unreadable.delete(room.locator))
1838
1810
  scopedRefusals.delete(room.locator);
1839
- sourceRefusals.delete(room.locator);
1840
- declarationRefusals.delete(room.locator);
1841
1811
  list(room, true);
1842
1812
  publishChangeSet(set);
1843
1813
  await unpair(local);
1844
1814
  moveTo({ ...status, ...attention() });
1845
- options.adapter.roomStream?.published(room.locator, roomVersion);
1846
- if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1815
+ if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1847
1816
  submitLater(room.locator);
1848
1817
  }
1849
1818
  return true;
1850
1819
  });
1820
+ return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
1851
1821
  }
1852
1822
  async function savedRoom(locator) {
1853
1823
  const local = await openLocal();
@@ -1872,12 +1842,7 @@ export function composeRuntime(options) {
1872
1842
  return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
1873
1843
  }
1874
1844
  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
- }
1845
+ const read = readSavedParts(named);
1881
1846
  const target = await savedRoom(locator);
1882
1847
  const saved = [];
1883
1848
  const files = [];
@@ -1886,7 +1851,7 @@ export function composeRuntime(options) {
1886
1851
  for (const { path, parts } of read) {
1887
1852
  const file = parts.file === undefined
1888
1853
  ? undefined
1889
- : await heldFile(parts.file.blob, binaryBase(visible, path));
1854
+ : await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
1890
1855
  if (parts.file !== undefined && file !== undefined) {
1891
1856
  files.push({ key: file.localId, blob: parts.file.blob });
1892
1857
  }
@@ -1921,13 +1886,14 @@ export function composeRuntime(options) {
1921
1886
  const referring = removed.length === 0
1922
1887
  ? []
1923
1888
  : (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
1924
- const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
1889
+ const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
1925
1890
  const { visible } = await replayed(record, confirmedLayer, replica.queue);
1926
- const composition = compositionOf(record);
1891
+ const composition = composed(record);
1927
1892
  return {
1928
1893
  record,
1929
1894
  confirmed: confirmedLayer,
1930
1895
  visible,
1896
+ folders: new Set(await local.folders(record.key, 'visible', paths)),
1931
1897
  *visibleOver(queue) {
1932
1898
  return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
1933
1899
  },
@@ -1939,13 +1905,7 @@ export function composeRuntime(options) {
1939
1905
  return false;
1940
1906
  const local = await openLocal();
1941
1907
  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)));
1908
+ return local.typedWithin(record.key, 'visible', roots, false);
1949
1909
  }
1950
1910
  function textDiffers(left, right) {
1951
1911
  return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
@@ -1956,18 +1916,6 @@ export function composeRuntime(options) {
1956
1916
  return undefined;
1957
1917
  return entry.binary !== null ? null : entry.content;
1958
1918
  }
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
1919
  function lookupOf(local, key, view) {
1972
1920
  return async (paths) => {
1973
1921
  if (paths.length === 0)
@@ -1984,37 +1932,6 @@ export function composeRuntime(options) {
1984
1932
  const read = parseFrontmatter(entry.content ?? '');
1985
1933
  return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
1986
1934
  }
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
1935
  function overriddenLookup(lookup, overrides) {
2019
1936
  return async (paths) => {
2020
1937
  const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
@@ -2032,9 +1949,8 @@ export function composeRuntime(options) {
2032
1949
  }
2033
1950
  function validatorOf(record, confirmedLayer) {
2034
1951
  return async (resulting, touched) => {
2035
- const composition = compositionFor(record);
2036
1952
  const local = await openLocal();
2037
- const changed = [];
1953
+ const changes = [];
2038
1954
  const overrides = new Map();
2039
1955
  const pace = paced();
2040
1956
  for (const path of new Set([...confirmedLayer.held, ...touched])) {
@@ -2045,19 +1961,13 @@ export function composeRuntime(options) {
2045
1961
  continue;
2046
1962
  overrides.set(path, now);
2047
1963
  if (textOf(was) !== textOf(now))
2048
- changed.push({ path, was: textOf(was), now: textOf(now) });
1964
+ changes.push({ path, was: textOf(was), now: textOf(now) });
2049
1965
  }
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);
1966
+ return validatedNeighbourhood(changes, compositionFor(record), {
1967
+ lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
1968
+ texts: textsOf(local, record.key, 'confirmed'),
1969
+ referrers: referrersOf(local, record.key, 'confirmed'),
1970
+ });
2061
1971
  };
2062
1972
  }
2063
1973
  function textsOf(local, key, view) {
@@ -2081,6 +1991,7 @@ export function composeRuntime(options) {
2081
1991
  operations: room.operations,
2082
1992
  providerOwnedPaths: room.providerOwnedPaths,
2083
1993
  visible: saving.visible,
1994
+ folders: saving.folders,
2084
1995
  queued: queuedDocuments(replica.queue),
2085
1996
  unmodeled: await unmodeledIn(record),
2086
1997
  validate: validatorOf(record, saving.confirmed),
@@ -2101,6 +2012,7 @@ export function composeRuntime(options) {
2101
2012
  locator,
2102
2013
  key: saving.record.key,
2103
2014
  roots: saving.record.typedSelection?.roots ?? [],
2015
+ composed: composedOf(composed(saving.record)),
2104
2016
  overlay: diff.overlay,
2105
2017
  });
2106
2018
  }
@@ -2132,7 +2044,7 @@ export function composeRuntime(options) {
2132
2044
  }
2133
2045
  const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
2134
2046
  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)));
2047
+ const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
2136
2048
  if (changed.length > 0)
2137
2049
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2138
2050
  const target = replicas.get(destination);
@@ -2244,6 +2156,8 @@ export function composeRuntime(options) {
2244
2156
  submitLater(locator);
2245
2157
  }
2246
2158
  async function committed(locator, target, saved, consumed, writtenBy, files = []) {
2159
+ if (full)
2160
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
2247
2161
  const into = movedInto(locator, saved);
2248
2162
  if (into !== null) {
2249
2163
  return committedMove(locator, into, target, saved, consumed, writtenBy, files);
@@ -2255,7 +2169,7 @@ export function composeRuntime(options) {
2255
2169
  ...saved.map(({ path }) => path),
2256
2170
  ...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
2257
2171
  ]);
2258
- const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, underneath(held, path, saving.visible)));
2172
+ const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
2259
2173
  if (changed.length > 0)
2260
2174
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2261
2175
  const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
@@ -2283,6 +2197,7 @@ export function composeRuntime(options) {
2283
2197
  providerLimits: limits,
2284
2198
  handle: room.handle,
2285
2199
  visible: saving.visible,
2200
+ folders: saving.folders,
2286
2201
  changes,
2287
2202
  bases: parts.flatMap(({ bases }) => bases),
2288
2203
  queued: queuedDocuments(replica.queue),
@@ -2433,6 +2348,9 @@ export function composeRuntime(options) {
2433
2348
  async function readHistory(locator, path, query = {}) {
2434
2349
  if (closed || refusal !== null)
2435
2350
  return UNCERTAIN;
2351
+ await firstDiscovery;
2352
+ if (!roomsNamed)
2353
+ return UNCERTAIN;
2436
2354
  try {
2437
2355
  await ensureAuthorization();
2438
2356
  }
@@ -2522,14 +2440,12 @@ export function composeRuntime(options) {
2522
2440
  return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
2523
2441
  };
2524
2442
  }
2525
- function binaryRoom(request) {
2526
- if (request.kind === 'path')
2527
- return request.locator;
2528
- return options.adapter.referrerScope(request.referrer)?.locator ?? null;
2529
- }
2530
2443
  async function transferBinary(request, transfer, answer) {
2531
2444
  if (closed || refusal !== null)
2532
2445
  return UNCERTAIN;
2446
+ await firstDiscovery;
2447
+ if (!roomsNamed)
2448
+ return UNCERTAIN;
2533
2449
  try {
2534
2450
  await ensureAuthorization();
2535
2451
  }
@@ -2539,16 +2455,13 @@ export function composeRuntime(options) {
2539
2455
  if (closed || authorization === undefined || refusal !== null)
2540
2456
  return UNCERTAIN;
2541
2457
  const since = fencings;
2542
- journal.record(request.kind === 'path'
2543
- ? `transfer binary ${request.locator} ${request.path}`
2544
- : `transfer binary ${request.fileRef}`);
2458
+ journal.record(`transfer binary ${request.locator} ${request.path}`);
2545
2459
  try {
2546
2460
  const result = await transfer(authorization);
2547
2461
  const settled = result;
2548
2462
  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 });
2463
+ if (fencedSince(request.locator, since))
2464
+ return failure({ kind: 'room-not-readable', locator: request.locator });
2552
2465
  if (settled.kind === 'scoped-refusal') {
2553
2466
  const room = replicas.get(settled.locator)?.record;
2554
2467
  if (room !== undefined)
@@ -2563,23 +2476,27 @@ export function composeRuntime(options) {
2563
2476
  return UNCERTAIN;
2564
2477
  }
2565
2478
  }
2479
+ function fetchBinary(request, name) {
2480
+ return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2481
+ }
2566
2482
  async function readBinary(request, name) {
2567
2483
  if (closed)
2568
2484
  return UNCERTAIN;
2569
2485
  const held = await keptFiles.find(name);
2570
2486
  if (held !== null)
2571
2487
  return held;
2572
- return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2488
+ return fetchBinary(request, name);
2573
2489
  }
2574
2490
  async function binaryPath(locator, path, revision) {
2575
2491
  if (revision !== undefined)
2576
2492
  return { kind: 'path', locator, path, revision };
2577
2493
  const replica = replicas.get(locator);
2578
- if (replica === undefined)
2579
- return null;
2580
2494
  const local = await openLocal();
2581
- const [held] = await local.entries(replica.record.key, 'visible', [path]);
2582
- if (held?.binary === undefined || held.binary === null)
2495
+ const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
2496
+ const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
2497
+ if (attached !== undefined)
2498
+ return { kind: 'path', locator, path, revision: attached.revision };
2499
+ if (replica === undefined || held?.binary === undefined || held.binary === null)
2583
2500
  return null;
2584
2501
  if (held.binary.state === 'pending')
2585
2502
  return { kind: 'pending', localId: held.binary.localId };
@@ -2595,7 +2512,7 @@ export function composeRuntime(options) {
2595
2512
  const holds = (at) => {
2596
2513
  const kept = confirmedLayer.binaries[at];
2597
2514
  return (kept?.state === 'accepted' &&
2598
- kept.fileRef === record.fileRef &&
2515
+ kept.sha256 === record.sha256 &&
2599
2516
  confirmedLayer.revisions[at] === record.revision);
2600
2517
  };
2601
2518
  let confirmedAt = path;
@@ -2609,58 +2526,93 @@ export function composeRuntime(options) {
2609
2526
  locator,
2610
2527
  path: confirmedAt,
2611
2528
  revision: record.revision,
2612
- fileRef: record.fileRef,
2613
2529
  };
2614
2530
  }
2615
- async function readBinaryPath(locator, path, revision) {
2531
+ async function askedBinary(locator, path, revision) {
2616
2532
  await ready;
2617
2533
  const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2618
- const asked = drafted !== undefined && drafted.localId === revision
2534
+ return drafted !== undefined && drafted.localId === revision
2619
2535
  ? { kind: 'pending', localId: revision }
2620
- : await binaryPath(locator, path, revision);
2536
+ : binaryPath(locator, path, revision);
2537
+ }
2538
+ function pendingBinary(locator, path, localId) {
2539
+ const facts = queuedFacts(locator, path, localId);
2540
+ return facts === null
2541
+ ? absentBinary(path)
2542
+ : {
2543
+ kind: 'binary',
2544
+ file: localId,
2545
+ byteLength: facts.byteLength,
2546
+ mediaType: facts.mediaType,
2547
+ };
2548
+ }
2549
+ async function readBinaryPath(locator, path, revision) {
2550
+ const asked = await askedBinary(locator, path, revision);
2621
2551
  if (asked === null)
2622
2552
  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
- }
2553
+ if (asked.kind === 'pending')
2554
+ return pendingBinary(locator, path, asked.localId);
2634
2555
  return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
2635
2556
  }
2557
+ async function heldMediaType(locator, path) {
2558
+ const replica = replicas.get(locator);
2559
+ const [held] = replica === undefined
2560
+ ? []
2561
+ : await (await openLocal()).entries(replica.record.key, 'visible', [path]);
2562
+ return held === undefined
2563
+ ? attachedFiles.get(locator)?.get(path)?.mediaType
2564
+ : held.binary?.mediaType;
2565
+ }
2566
+ function linkOf(asked, held) {
2567
+ return transferBinary(asked, (authorization) => options.adapter.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
2568
+ }
2636
2569
  async function linkBinaryPath(locator, path, revision) {
2637
2570
  await ready;
2638
2571
  const asked = await binaryPath(locator, path, revision);
2639
2572
  if (asked === null || asked.kind === 'pending')
2640
2573
  return absentBinary(path);
2641
- return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
2574
+ return linkOf(asked, revision === undefined ? await heldMediaType(locator, path) : undefined);
2642
2575
  }
2643
- async function readBinaryReference(reference) {
2576
+ async function showBinaryPath(locator, path, revision) {
2577
+ const asked = await askedBinary(locator, path, revision);
2578
+ if (asked === null)
2579
+ return absentBinary(path);
2580
+ if (asked.kind === 'pending')
2581
+ return pendingBinary(locator, path, asked.localId);
2582
+ if (closed)
2583
+ return UNCERTAIN;
2584
+ const name = { locator, path: asked.path, revision: asked.revision };
2585
+ const kept = await keptFiles.find(name);
2586
+ if (kept !== null)
2587
+ return kept;
2588
+ const mediaType = await heldMediaType(locator, path);
2589
+ if (mediaType?.startsWith('image/') === true)
2590
+ return fetchBinary(asked, name);
2591
+ return linkOf(asked, revision === undefined ? mediaType : undefined);
2592
+ }
2593
+ async function attachBinary(locator, file) {
2644
2594
  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 });
2595
+ await firstDiscovery;
2596
+ if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
2597
+ return OFFLINE;
2598
+ const request = await attachedFile(locator, file);
2599
+ return transferBinary({ locator, path: file.name }, async (held) => {
2600
+ const result = await options.adapter.attach(held, request);
2601
+ if (result.kind === 'attached') {
2602
+ const landed = attachedFiles.get(locator) ?? new Map();
2603
+ landed.set(result.path, { revision: result.revision, mediaType: request.mediaType });
2604
+ attachedFiles.set(locator, landed);
2605
+ }
2606
+ return result;
2607
+ }, attachAnswer);
2649
2608
  }
2650
2609
  function readPart(file, part) {
2651
- return options.database.ask({ kind: 'file.part', key: file, part });
2610
+ return database.ask({ kind: 'file.part', key: file, part });
2652
2611
  }
2653
2612
  async function cleanUp() {
2654
- const freed = await options.database.ask({ kind: 'opened.clean' });
2613
+ const freed = await database.ask({ kind: 'opened.clean' });
2655
2614
  return freed + ((await options.shell.cleanUp?.()) ?? 0);
2656
2615
  }
2657
- function declaredAgents() {
2658
- for (const capability of capabilities) {
2659
- if (capability.conversationAgents !== undefined)
2660
- return capability.conversationAgents;
2661
- }
2662
- return undefined;
2663
- }
2664
2616
  function declaredAction(name) {
2665
2617
  for (const capability of capabilities) {
2666
2618
  for (const action of capability.actions ?? []) {
@@ -2783,169 +2735,222 @@ export function composeRuntime(options) {
2783
2735
  options.adapter.reached(locator, written, roomVersion) &&
2784
2736
  !options.adapter.reached(locator, roomVersion, written));
2785
2737
  }
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) {
2738
+ function refreshingOf(local, key, composition, liveness, replaying = null) {
2739
+ const write = replaying === null ? exclusive : finishingWrite;
2882
2740
  return {
2741
+ turn() {
2742
+ if (replaying !== null)
2743
+ installing(replaying);
2744
+ },
2883
2745
  prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
2884
2746
  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 () => {
2747
+ for (let written = 0;;) {
2748
+ const end = batchEnd(page.entries, written);
2749
+ const last = end === page.entries.length;
2750
+ const entries = page.entries.slice(written, end);
2751
+ const staged = await write(async () => {
2889
2752
  if (!liveness())
2890
2753
  return false;
2891
- await local.refresh(key, last
2754
+ await timed(stageBudget, () => local.refresh(key, composition, last
2892
2755
  ? { ...page, entries }
2893
- : { entries, removals: [], folders: [], folderRemovals: [] });
2756
+ : { entries, removals: [], folders: [], folderRemovals: [] }));
2894
2757
  return true;
2895
2758
  });
2896
2759
  if (!staged)
2897
2760
  return false;
2761
+ if (last)
2762
+ return true;
2763
+ written = end;
2898
2764
  }
2899
- return true;
2900
2765
  },
2901
2766
  };
2902
2767
  }
2903
- function batched(entries) {
2904
- const batches = [[]];
2768
+ function batchEnd(entries, from) {
2769
+ let end = from;
2905
2770
  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);
2771
+ while (end < entries.length) {
2772
+ const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
2773
+ if (spent + size > stageBudget.bytes && end > from)
2774
+ break;
2913
2775
  spent += size;
2776
+ end += 1;
2777
+ }
2778
+ return end;
2779
+ }
2780
+ async function timed(budget, work) {
2781
+ const started = performance.now();
2782
+ const done = await work();
2783
+ const spent = performance.now() - started;
2784
+ if (spent < budget.aim / 2)
2785
+ budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
2786
+ else if (spent > budget.aim * 2) {
2787
+ budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
2788
+ }
2789
+ return done;
2790
+ }
2791
+ function installWrite(work) {
2792
+ return installTurn(installTurns.starting, work);
2793
+ }
2794
+ function finishingWrite(work) {
2795
+ return installTurn(installTurns.finishing, work);
2796
+ }
2797
+ function installTurn(queue, work) {
2798
+ return new Promise((resolve, reject) => {
2799
+ queue.push(() => void exclusive(work)
2800
+ .then(resolve, reject)
2801
+ .finally(() => {
2802
+ installTurns.busy = false;
2803
+ nextInstallTurn();
2804
+ }));
2805
+ nextInstallTurn();
2806
+ });
2807
+ }
2808
+ function nextInstallTurn() {
2809
+ if (installTurns.busy || installTurns.ahead > 0)
2810
+ return;
2811
+ const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
2812
+ if (turn === undefined)
2813
+ return;
2814
+ installTurns.busy = true;
2815
+ turn();
2816
+ }
2817
+ async function ahead(work) {
2818
+ let taken = false;
2819
+ const hold = {
2820
+ take() {
2821
+ if (taken)
2822
+ return;
2823
+ taken = true;
2824
+ installTurns.ahead += 1;
2825
+ },
2826
+ release() {
2827
+ if (!taken)
2828
+ return;
2829
+ taken = false;
2830
+ installTurns.ahead -= 1;
2831
+ resumeInstall();
2832
+ },
2833
+ };
2834
+ hold.take();
2835
+ try {
2836
+ return await work(hold);
2837
+ }
2838
+ finally {
2839
+ hold.release();
2914
2840
  }
2915
- return batches;
2916
2841
  }
2917
- async function pullRefresh(authorized, held, liveness) {
2842
+ function resumeInstall() {
2843
+ if (installTurns.ahead > 0)
2844
+ return;
2845
+ void yielded().then(nextInstallTurn);
2846
+ }
2847
+ async function heldStream() {
2848
+ while (streamsFree === 0)
2849
+ await new Promise((admit) => installWaiters.push(admit));
2850
+ streamsFree -= 1;
2851
+ return once(() => {
2852
+ streamsFree += 1;
2853
+ });
2854
+ }
2855
+ async function unqueued() {
2856
+ while (queuedText >= QUEUED_TEXT) {
2857
+ await new Promise((admit) => installWaiters.push(admit));
2858
+ }
2859
+ }
2860
+ function once(release) {
2861
+ let released = false;
2862
+ return () => {
2863
+ if (released)
2864
+ return;
2865
+ released = true;
2866
+ release();
2867
+ for (const admit of installWaiters.splice(0))
2868
+ admit();
2869
+ };
2870
+ }
2871
+ function installing(locator) {
2872
+ installs.add(locator);
2873
+ queueReadiness();
2874
+ }
2875
+ function untransferred(locator) {
2876
+ transfers.delete(locator);
2877
+ arrivals.delete(locator);
2878
+ streaming.delete(locator);
2879
+ underived.delete(locator);
2880
+ unindexed.delete(locator);
2881
+ }
2882
+ function setBack(locator) {
2883
+ const now = bytesOf(locator, true);
2884
+ const earlier = startedOver.get(locator);
2885
+ startedOver.set(locator, {
2886
+ done: Math.max(now.done, earlier?.done ?? 0),
2887
+ total: Math.max(now.total, earlier?.total ?? 0),
2888
+ });
2889
+ }
2890
+ function owedIndexes(locator, bytes, text) {
2891
+ if (bytes <= 0 || text <= 0)
2892
+ return;
2893
+ unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
2894
+ }
2895
+ function built(locator, { done, text }) {
2896
+ const owed = unindexed.get(locator) ?? [];
2897
+ let left = text;
2898
+ while (!done && left > 0 && owed.length > 0) {
2899
+ const [first] = owed;
2900
+ if (left >= first.text) {
2901
+ left -= first.text;
2902
+ owed.shift();
2903
+ continue;
2904
+ }
2905
+ owed[0] = {
2906
+ bytes: (first.bytes * (first.text - left)) / first.text,
2907
+ text: first.text - left,
2908
+ };
2909
+ left = 0;
2910
+ }
2911
+ if (done || owed.length === 0)
2912
+ unindexed.delete(locator);
2913
+ queueReadiness();
2914
+ }
2915
+ function installed(locator) {
2916
+ if (installs.delete(locator))
2917
+ queueReadiness();
2918
+ }
2919
+ async function pullRefresh(authorized, held, liveness, hold, refresh) {
2918
2920
  if (!liveness())
2919
2921
  return unconfirmed;
2920
- const turn = generation;
2921
2922
  const { record } = held;
2922
- const base = held.confirmed;
2923
+ const from = standingOf(record.locator);
2923
2924
  const local = await openLocal();
2924
2925
  try {
2925
2926
  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));
2927
+ const result = await advancement(async (authorization, request) => {
2928
+ hold.release();
2929
+ try {
2930
+ return await refresh(authorization, request);
2931
+ }
2932
+ finally {
2933
+ hold.take();
2934
+ }
2935
+ }, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, composed(record), liveness));
2927
2936
  if (result.kind !== 'advanced') {
2928
2937
  journal.record(`transfer refresh ${record.locator} ${result.kind}`);
2929
2938
  return result;
2930
2939
  }
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
2940
  journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
2940
- return { kind: 'advanced', record: result.record, base };
2941
+ return {
2942
+ kind: 'advanced',
2943
+ record: result.record,
2944
+ from,
2945
+ remodelled: result.record.model !== record.model,
2946
+ };
2941
2947
  }
2942
2948
  catch (error) {
2949
+ if (refusedForSpace(error))
2950
+ return unconfirmed;
2943
2951
  journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
2944
2952
  throw error;
2945
2953
  }
2946
- finally {
2947
- reads.finished(record.locator, turn);
2948
- }
2949
2954
  }
2950
2955
  const heldContents = new Map();
2951
2956
  function heldContentOf(local, key) {
@@ -2963,63 +2968,115 @@ export function composeRuntime(options) {
2963
2968
  heldContents.set(key, made);
2964
2969
  return made;
2965
2970
  }
2971
+ async function abandoned(local, locator) {
2972
+ await local.abandon(locator);
2973
+ setBack(locator);
2974
+ untransferred(locator);
2975
+ liveQueries.recover();
2976
+ sweep();
2977
+ }
2966
2978
  function staging(local, liveness) {
2979
+ const derived = async (locator, key, composition) => {
2980
+ underived.delete(locator);
2981
+ for (;;) {
2982
+ if (!liveness())
2983
+ return false;
2984
+ const done = await finishingWrite(async () => liveness()
2985
+ ? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
2986
+ : null);
2987
+ if (done === null)
2988
+ return false;
2989
+ built(locator, done);
2990
+ if (done.done)
2991
+ return true;
2992
+ }
2993
+ };
2967
2994
  return {
2968
2995
  held: (locator) => transfers.get(locator) ?? null,
2996
+ async turn(locator, step) {
2997
+ installing(locator);
2998
+ return step === 'downloading' ? heldStream() : () => { };
2999
+ },
3000
+ unqueued,
3001
+ queued(text) {
3002
+ queuedText += text;
3003
+ return once(() => {
3004
+ queuedText -= text;
3005
+ });
3006
+ },
3007
+ arriving(locator) {
3008
+ let ended = false;
3009
+ return {
3010
+ at(bytes) {
3011
+ if (ended)
3012
+ return;
3013
+ arrivals.set(locator, bytes);
3014
+ streaming.add(locator);
3015
+ const now = performance.now();
3016
+ if (now - arrivalReported < ARRIVAL_REPORT_MS)
3017
+ return;
3018
+ arrivalReported = now;
3019
+ queueReadiness();
3020
+ },
3021
+ end() {
3022
+ if (ended)
3023
+ return;
3024
+ ended = true;
3025
+ if (streaming.delete(locator))
3026
+ queueReadiness();
3027
+ },
3028
+ };
3029
+ },
3030
+ streamed: options.adapter.roomStream !== undefined,
2969
3031
  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)
3032
+ const { locator } = record;
3033
+ installing(locator);
3034
+ if (!liveness())
3035
+ return null;
3036
+ const key = await installWrite(async () => {
3037
+ if (!liveness())
2991
3038
  return null;
2992
- key = staged;
2993
- }
2994
- transfers.set(record.locator, { ...record, key });
3039
+ return local.stage(record, composition, page);
3040
+ });
3041
+ if (key === null || !liveness())
3042
+ return null;
3043
+ const arrived = !transfers.has(locator);
3044
+ const prior = transfers.get(locator)?.done.bytes ?? 0;
3045
+ const held = { ...record, key };
3046
+ transfers.set(locator, held);
3047
+ if (arrived)
3048
+ liveQueries.recover();
3049
+ installing(locator);
2995
3050
  liveQueries.recover();
2996
- return key;
3051
+ const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
3052
+ owedIndexes(locator, record.done.bytes - prior, text);
3053
+ const owed = text + (underived.get(locator) ?? 0);
3054
+ underived.set(locator, owed);
3055
+ if (owed < deriveBudget.bytes)
3056
+ return held;
3057
+ return (await derived(locator, key, composition)) ? held : null;
2997
3058
  },
2998
- async refreshing(key) {
3059
+ derive: derived,
3060
+ async refreshing(locator, key, composition) {
2999
3061
  await exclusive(() => local.unrefresh(key));
3000
- return refreshingOf(local, key, liveness);
3062
+ return refreshingOf(local, key, composition, liveness, locator);
3001
3063
  },
3002
3064
  abandon: (locator) => exclusive(async () => {
3003
- if (!liveness())
3004
- return;
3005
- await local.abandon(locator);
3006
- transfers.delete(locator);
3007
- liveQueries.recover();
3008
- sweep();
3065
+ if (liveness())
3066
+ await abandoned(local, locator);
3009
3067
  }),
3010
- setAside: (locator) => exclusive(async () => {
3068
+ setAside: async (locator) => {
3011
3069
  if (!liveness())
3012
3070
  return;
3013
- await local.abandon(locator, true);
3014
- transfers.delete(locator);
3071
+ setBack(locator);
3072
+ untransferred(locator);
3015
3073
  liveQueries.recover();
3016
- }),
3074
+ },
3017
3075
  release: (key) => exclusive(async () => {
3018
3076
  await local.release(key);
3019
3077
  sweep();
3020
3078
  }),
3021
3079
  content: (key) => heldContentOf(local, key),
3022
- validate: (record, view) => validatedRoom(local, record, view, null),
3023
3080
  };
3024
3081
  }
3025
3082
  function reusable(room, local) {
@@ -3031,30 +3088,28 @@ export function composeRuntime(options) {
3031
3088
  async function pullBootstrap(authorized, room, local, liveness) {
3032
3089
  if (!liveness())
3033
3090
  return unconfirmed;
3034
- const turn = generation;
3035
3091
  if (replicas.has(room.locator)) {
3036
3092
  catching.add(room.locator);
3037
3093
  liveQueries.recover();
3038
3094
  }
3039
3095
  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));
3096
+ 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
3097
  const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
3045
3098
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
3046
- if (result.kind !== 'staged')
3099
+ if (result.kind !== 'staged') {
3047
3100
  caughtUpTo(room.locator);
3101
+ installed(room.locator);
3102
+ }
3048
3103
  return result;
3049
3104
  }
3050
3105
  catch (error) {
3051
- journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
3052
3106
  caughtUpTo(room.locator);
3107
+ installed(room.locator);
3108
+ if (refusedForSpace(error))
3109
+ return unconfirmed;
3110
+ journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
3053
3111
  throw error;
3054
3112
  }
3055
- finally {
3056
- reads.finished(room.locator, turn);
3057
- }
3058
3113
  }
3059
3114
  function caughtUpTo(locator) {
3060
3115
  if (catching.delete(locator))
@@ -3062,72 +3117,82 @@ export function composeRuntime(options) {
3062
3117
  }
3063
3118
  function behind(room) {
3064
3119
  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)));
3120
+ return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
3067
3121
  }
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);
3122
+ function standingOf(locator) {
3123
+ const held = replicas.get(locator);
3124
+ return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
3125
+ }
3126
+ function stands(locator, from) {
3127
+ const now = standingOf(locator);
3128
+ return now.key === from.key && now.confirmed === from.confirmed;
3075
3129
  }
3076
- async function advanceOnce(room, local, authorized, live) {
3077
- if (!live() || paused.has(room.locator))
3130
+ async function advance(room, local, authorized, running) {
3131
+ const live = () => running() && !full;
3132
+ if (!live())
3078
3133
  return unconfirmed;
3079
3134
  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')
3135
+ if (held !== undefined && options.adapter.roomStream !== undefined) {
3136
+ stale.delete(room.locator);
3137
+ name(room.locator);
3138
+ return null;
3139
+ }
3140
+ const refresh = reads.refresh;
3141
+ if (held !== undefined && refresh !== undefined) {
3142
+ const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
3143
+ if (refreshed === 'behind')
3144
+ return advance(room, local, authorized, live);
3145
+ if (refreshed !== 'beyond-horizon')
3107
3146
  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)
3147
+ if (reloadsDue.has(room.locator))
3114
3148
  return unconfirmed;
3115
3149
  }
3150
+ const from = standingOf(room.locator);
3116
3151
  const bootstrapped = await pullBootstrap(authorized, room, local, live);
3152
+ if (bootstrapped.kind !== 'staged')
3153
+ return live() ? bootstrapped : unconfirmed;
3154
+ const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
3155
+ if (await publish(local, room, bootstrapped.record, landing, live)) {
3156
+ return live() ? null : unconfirmed;
3157
+ }
3158
+ return reachedBy(room.locator, bootstrapped.record.roomVersion)
3159
+ ? null
3160
+ : advance(room, local, authorized, live);
3161
+ }
3162
+ async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
3163
+ const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
3117
3164
  if (!live())
3118
3165
  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;
3166
+ if (refreshed.kind === 'still-current') {
3167
+ stale.delete(room.locator);
3168
+ if (unreadable.delete(room.locator)) {
3169
+ scopedRefusals.delete(room.locator);
3170
+ list(room, true);
3171
+ emitRooms();
3172
+ }
3173
+ return null;
3174
+ }
3175
+ if (refreshed.kind === 'advanced') {
3176
+ const landing = {
3177
+ kind: 'refreshed',
3178
+ from: refreshed.from,
3179
+ remodelled: refreshed.remodelled,
3180
+ };
3181
+ if (!(await publish(local, room, refreshed.record, landing, live))) {
3182
+ const now = replicas.get(room.locator)?.record.roomVersion;
3183
+ const reached = now !== undefined &&
3184
+ options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3185
+ return reached ? null : 'behind';
3186
+ }
3187
+ await retainModel(local, room, modelOf(room.locator));
3188
+ return null;
3189
+ }
3190
+ return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
3123
3191
  }
3124
3192
  async function withhold(withheld, room, local, progress) {
3125
3193
  if (withheld.kind === 'unconfirmed')
3126
3194
  return;
3127
- if (withheld.kind === 'refused')
3128
- stale.delete(room.locator);
3129
- else
3130
- stale.add(room.locator);
3195
+ stale.add(room.locator);
3131
3196
  if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
3132
3197
  const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
3133
3198
  repairable.set(room.locator, reset);
@@ -3149,30 +3214,10 @@ export function composeRuntime(options) {
3149
3214
  list(room, false);
3150
3215
  emitRooms();
3151
3216
  }
3152
- else if (withheld.kind === 'refused') {
3153
- refusedSource(room, withheld);
3154
- }
3155
3217
  else if (withheld.kind === 'revoked-authorization') {
3156
3218
  await refuse('revoked-authorization');
3157
3219
  }
3158
3220
  }
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
3221
  function leaving(batch, result) {
3177
3222
  if (result.kind === 'expired')
3178
3223
  return { batch, returned: returnedBy('expired') };
@@ -3194,8 +3239,8 @@ export function composeRuntime(options) {
3194
3239
  function settling(acceptance) {
3195
3240
  return acceptance !== null && 'apply' in acceptance;
3196
3241
  }
3197
- function refusalOf(acceptance) {
3198
- return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
3242
+ function failureOf(acceptance) {
3243
+ return settling(acceptance) ? null : acceptance;
3199
3244
  }
3200
3245
  async function acceptance(room, local, submitted, accepted) {
3201
3246
  if (!continuing())
@@ -3218,34 +3263,6 @@ export function composeRuntime(options) {
3218
3263
  ? accepted.roomVersion
3219
3264
  : held,
3220
3265
  };
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
3266
  changes = back;
3250
3267
  }
3251
3268
  const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
@@ -3255,8 +3272,6 @@ export function composeRuntime(options) {
3255
3272
  const set = settled.apply(answer);
3256
3273
  pendings.delete(pendingKey(room.locator, submitted.id));
3257
3274
  unreadable.delete(room.locator);
3258
- sourceRefusals.delete(room.locator);
3259
- declarationRefusals.delete(room.locator);
3260
3275
  list(room, true);
3261
3276
  return set;
3262
3277
  },
@@ -3274,13 +3289,9 @@ export function composeRuntime(options) {
3274
3289
  const failed = await exclusive(async () => {
3275
3290
  const adopting = await acceptance(room, local, submitted, accepted);
3276
3291
  await adoptedTogether(local, [adopting]);
3277
- return refusalOf(adopting);
3292
+ return failureOf(adopting);
3278
3293
  });
3279
- if (failed === null)
3280
- return true;
3281
- if (failed.kind === 'refused' && continuing())
3282
- refusedSource(room, failed);
3283
- return false;
3294
+ return failed === null;
3284
3295
  }
3285
3296
  async function adoptedMove(room, partner, local, submitted, accepted) {
3286
3297
  const other = discovered.get(partner);
@@ -3294,18 +3305,10 @@ export function composeRuntime(options) {
3294
3305
  ...accepted.destination,
3295
3306
  });
3296
3307
  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);
3308
+ return failureOf(left);
3302
3309
  });
3303
3310
  submitLater(partner);
3304
- if (failed === null)
3305
- return true;
3306
- if (failed.kind === 'refused' && continuing())
3307
- refusedSource(room, failed);
3308
- return false;
3311
+ return failed === null;
3309
3312
  }
3310
3313
  function queueIn(locator) {
3311
3314
  return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
@@ -3428,8 +3431,6 @@ export function composeRuntime(options) {
3428
3431
  if (replica === undefined || head === undefined)
3429
3432
  return 'done';
3430
3433
  const state = pendings.get(pendingKey(room.locator, head.id));
3431
- if (state?.accepted !== undefined)
3432
- return 'done';
3433
3434
  const partner = partnerOf(head, room.locator);
3434
3435
  if (partner !== null && !leavingIn(head, room.locator)) {
3435
3436
  submitLater(partner);
@@ -3543,12 +3544,12 @@ export function composeRuntime(options) {
3543
3544
  }
3544
3545
  if (result.kind === 'expired') {
3545
3546
  await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
3546
- const there = other === undefined ? null : await advance(other, local, authorized, continuing);
3547
+ const there = other === undefined ? null : await advanced(other, local, authorized);
3547
3548
  if (other !== undefined && there !== null) {
3548
3549
  await withhold(there, other, local, progress);
3549
3550
  return 'done';
3550
3551
  }
3551
- const withheld = await advance(room, local, authorized, continuing);
3552
+ const withheld = await advanced(room, local, authorized);
3552
3553
  if (withheld !== null) {
3553
3554
  await withhold(withheld, room, local, progress);
3554
3555
  return 'done';
@@ -3570,8 +3571,8 @@ export function composeRuntime(options) {
3570
3571
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
3571
3572
  const left = leaving(head, result);
3572
3573
  const refreshed = async () => ({
3573
- there: other === undefined ? null : await advance(other, local, authorized, continuing),
3574
- here: await advance(room, local, authorized, continuing),
3574
+ there: other === undefined ? null : await advanced(other, local, authorized),
3575
+ here: await advanced(room, local, authorized),
3575
3576
  });
3576
3577
  const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
3577
3578
  if (judged !== null && (judged.there !== null || judged.here !== null)) {
@@ -3642,6 +3643,7 @@ export function composeRuntime(options) {
3642
3643
  locator: room.locator,
3643
3644
  key: before.record.key,
3644
3645
  roots: before.record.typedSelection?.roots ?? [],
3646
+ composed: composedOf(composed(before.record)),
3645
3647
  overlay: { set: [], cleared: [...before.overlaid] },
3646
3648
  },
3647
3649
  ]),
@@ -3674,39 +3676,37 @@ export function composeRuntime(options) {
3674
3676
  if (signal.aborted)
3675
3677
  return;
3676
3678
  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
- });
3679
+ await roomTask(room.locator, (live) => reload(room, local, progress, live));
3683
3680
  if (signal.aborted)
3684
3681
  return;
3685
3682
  await rest(progress);
3686
3683
  }
3687
3684
  }
3688
3685
  function awaitingAnswer(locator) {
3686
+ answering.get(locator)?.release();
3689
3687
  let release = () => { };
3690
- const awaited = new Promise((resolve) => {
3688
+ const answered = new Promise((resolve) => {
3691
3689
  release = resolve;
3692
3690
  });
3693
- answering.set(locator, { generation, answered: awaited });
3691
+ const awaited = { answered, release };
3692
+ answering.set(locator, awaited);
3694
3693
  return () => {
3695
- if (answering.get(locator)?.answered === awaited)
3694
+ if (answering.get(locator) === awaited)
3696
3695
  answering.delete(locator);
3697
3696
  release();
3698
3697
  };
3699
3698
  }
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))))
3699
+ function declaredAsHeld(room) {
3700
+ const held = replicas.get(room.locator)?.record;
3701
+ return (held === undefined ||
3702
+ (JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
3703
+ JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
3704
+ JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
3705
+ }
3706
+ async function retainModel(local, room, next) {
3707
+ const key = room.locator;
3708
+ const previous = modelOf(room.locator);
3709
+ if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(room))
3710
3710
  return;
3711
3711
  if (next.status === 'unavailable') {
3712
3712
  invalidModels.add(key);
@@ -3718,26 +3718,13 @@ export function composeRuntime(options) {
3718
3718
  const held = replicas.get(room.locator);
3719
3719
  let changed = null;
3720
3720
  if (held !== undefined) {
3721
- const declared = {
3721
+ const record = {
3722
3722
  ...held.record,
3723
3723
  operations: room.operations,
3724
3724
  replica: room.replica,
3725
3725
  typedSelection: room.typedSelection ?? null,
3726
- ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
3726
+ model: next,
3727
3727
  };
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
3728
  const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
3742
3729
  const replayedLayer = await replayed(record, confirmedLayer, held.queue);
3743
3730
  const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
@@ -3748,41 +3735,33 @@ export function composeRuntime(options) {
3748
3735
  key: record.key,
3749
3736
  record: storedRecord(record),
3750
3737
  roots: record.typedSelection?.roots ?? [],
3751
- composition: compositionOf(record),
3738
+ composed: composedOf(composed(record)),
3752
3739
  overlay: diff.overlay,
3753
3740
  },
3754
3741
  ],
3755
3742
  });
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 });
3743
+ const { count, indexed } = committedRoom(room.locator, answer);
3744
+ replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
3759
3745
  changed = changeSet(room.locator, {
3760
3746
  documents: answer.changed[room.locator] ?? [],
3761
3747
  room: true,
3762
3748
  });
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
3749
  }
3774
- models.set(key, next);
3775
- if (next.status === 'current') {
3776
- liveModels.set(key, JSON.stringify(next.snapshot));
3777
- invalidModels.delete(key);
3778
- }
3779
- list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
3750
+ heldModel(room, next);
3780
3751
  if (changed === null)
3781
3752
  emitRooms();
3782
3753
  else
3783
3754
  publishChangeSet(changed);
3784
3755
  });
3785
3756
  }
3757
+ function heldModel(room, next) {
3758
+ models.set(room.locator, next);
3759
+ if (next.status === 'current') {
3760
+ liveModels.set(room.locator, JSON.stringify(next.snapshot));
3761
+ invalidModels.delete(room.locator);
3762
+ }
3763
+ list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
3764
+ }
3786
3765
  async function scoped(result, room, local) {
3787
3766
  if (result.scope === 'content' && !result.retryable) {
3788
3767
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
@@ -3796,14 +3775,14 @@ export function composeRuntime(options) {
3796
3775
  });
3797
3776
  let scopedAway = room;
3798
3777
  if (result.scope === 'model')
3799
- await retainModel(local, room, 'collection', {
3778
+ await retainModel(local, room, {
3800
3779
  status: 'unavailable',
3801
3780
  reason: 'not-readable',
3802
3781
  });
3803
3782
  else if (result.scope !== 'operation') {
3804
3783
  unreadable.add(room.locator);
3805
3784
  scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
3806
- await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
3785
+ await retainModel(local, scopedAway, modelOf(room.locator));
3807
3786
  }
3808
3787
  if (!continuing())
3809
3788
  return;
@@ -3811,6 +3790,17 @@ export function composeRuntime(options) {
3811
3790
  emitRooms();
3812
3791
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
3813
3792
  }
3793
+ function name(locator) {
3794
+ const stream = options.adapter.roomStream;
3795
+ const record = replicas.get(locator)?.record;
3796
+ const room = discovered.get(locator);
3797
+ if (stream === undefined || record === undefined || room === undefined)
3798
+ return;
3799
+ if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
3800
+ return;
3801
+ naming += 1;
3802
+ stream.hold([{ locator, roomVersion: record.roomVersion }]);
3803
+ }
3814
3804
  function heldRooms(listed) {
3815
3805
  return listed.flatMap((room) => {
3816
3806
  const record = replicas.get(room.locator)?.record;
@@ -3828,14 +3818,20 @@ export function composeRuntime(options) {
3828
3818
  return outcome?.status === 'fulfilled' && outcome.value;
3829
3819
  });
3830
3820
  if (attempt === 'opened') {
3821
+ naming += 1;
3831
3822
  options.adapter.roomStream?.hold(heldRooms(ready));
3832
3823
  handed = true;
3833
3824
  }
3834
3825
  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');
3826
+ for (const room of ready) {
3827
+ if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
3828
+ if (attempt === 'authorized')
3829
+ name(room.locator);
3830
+ continue;
3831
+ }
3832
+ void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
3833
+ }
3834
+ const failed = prepared.find((room) => room.status === 'rejected');
3839
3835
  if (failed !== undefined)
3840
3836
  throw failed.reason;
3841
3837
  }
@@ -3853,27 +3849,29 @@ export function composeRuntime(options) {
3853
3849
  return false;
3854
3850
  }
3855
3851
  if (prior !== undefined)
3856
- await retainModel(local, room, 'collection', modelOf(room.locator));
3852
+ await retainModel(local, room, modelOf(room.locator));
3857
3853
  if (discoveryPaused || authorization === undefined)
3858
3854
  return false;
3859
3855
  return room.replica.admission === 'admitted';
3860
3856
  }
3861
3857
  async function caughtUp(room, local, progress, live) {
3862
- if (authorization === undefined || !live() || !behind(room))
3858
+ if (options.adapter.roomStream !== undefined || authorization === undefined)
3859
+ return;
3860
+ if (!live() || !behind(room))
3863
3861
  return;
3864
3862
  const withheld = await advance(room, local, authorization, live);
3865
3863
  if (withheld !== null)
3866
3864
  await withhold(withheld, room, local, progress);
3867
3865
  }
3868
3866
  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
- });
3867
+ const model = modelOf(locator);
3868
+ return model.status === 'unavailable' && model.reason === 'revoked';
3873
3869
  }
3874
3870
  async function loadRoom(room, local, progress, live) {
3875
3871
  if (!live() || authorization === undefined)
3876
3872
  return;
3873
+ if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
3874
+ return;
3877
3875
  if (replicas.has(room.locator) && revoked(room.locator)) {
3878
3876
  await reload(room, local, progress, live);
3879
3877
  return;
@@ -3891,14 +3889,14 @@ export function composeRuntime(options) {
3891
3889
  reloadsDue.delete(room.locator);
3892
3890
  if (!live() || authorization === undefined)
3893
3891
  return;
3892
+ const from = standingOf(room.locator);
3894
3893
  const bootstrapped = await pullBootstrap(authorization, room, local, live);
3895
- if (!live())
3896
- return;
3897
3894
  if (bootstrapped.kind !== 'staged') {
3898
- await withhold(bootstrapped, room, local, progress);
3895
+ if (live())
3896
+ await withhold(bootstrapped, room, local, progress);
3899
3897
  return;
3900
3898
  }
3901
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3899
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
3902
3900
  await caughtUp(room, local, progress, live);
3903
3901
  }
3904
3902
  finally {
@@ -3920,6 +3918,8 @@ export function composeRuntime(options) {
3920
3918
  catch {
3921
3919
  result = { kind: 'transport-uncertainty' };
3922
3920
  }
3921
+ if (result.kind === 'discovery')
3922
+ roomsNamed = true;
3923
3923
  if (!live()) {
3924
3924
  progress.complete = false;
3925
3925
  notifyReady();
@@ -3927,12 +3927,7 @@ export function composeRuntime(options) {
3927
3927
  }
3928
3928
  journal.record(`connection discover ${result.kind}`);
3929
3929
  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
- }
3930
+ await settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
3936
3931
  progress.reachable = false;
3937
3932
  progress.complete = false;
3938
3933
  notifyReady();
@@ -4009,23 +4004,45 @@ export function composeRuntime(options) {
4009
4004
  }
4010
4005
  }
4011
4006
  async function admittedRead(work) {
4012
- if (roomReads.generation !== generation) {
4013
- roomReads = { generation, free: ROOM_READS, waiting: [] };
4007
+ while (roomReads.free === 0) {
4008
+ await new Promise((admit) => roomReads.waiting.push(admit));
4014
4009
  }
4015
- const reads = roomReads;
4016
- while (reads.free === 0)
4017
- await new Promise((admit) => reads.waiting.push(admit));
4018
- reads.free -= 1;
4010
+ roomReads.free -= 1;
4019
4011
  try {
4020
4012
  await work();
4021
4013
  }
4022
4014
  finally {
4023
- reads.free += 1;
4024
- reads.waiting.shift()?.();
4015
+ roomReads.free += 1;
4016
+ roomReads.waiting.shift()?.();
4025
4017
  }
4026
4018
  }
4019
+ function inRoom(locator, work) {
4020
+ roomWork += 1;
4021
+ const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
4022
+ try {
4023
+ return await work(continuing);
4024
+ }
4025
+ finally {
4026
+ roomWork -= 1;
4027
+ }
4028
+ });
4029
+ const chain = ran.then(() => undefined, () => undefined);
4030
+ roomLanes.set(locator, chain);
4031
+ working.add(chain);
4032
+ void chain.finally(() => {
4033
+ working.delete(chain);
4034
+ if (roomLanes.get(locator) === chain)
4035
+ roomLanes.delete(locator);
4036
+ });
4037
+ return ran;
4038
+ }
4027
4039
  function roomTask(locator, work) {
4028
- return task(roomLane(locator), (live) => admittedRead(() => work(live)));
4040
+ if (stopping)
4041
+ return Promise.resolve();
4042
+ return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
4043
+ }
4044
+ function advanced(room, local, authorized) {
4045
+ return inRoom(room.locator, (live) => advance(room, local, authorized, live));
4029
4046
  }
4030
4047
  function task(lane, work) {
4031
4048
  if (stopping)
@@ -4044,20 +4061,17 @@ export function composeRuntime(options) {
4044
4061
  function sync(attempt) {
4045
4062
  return task(OPENING_LANE, async (live) => {
4046
4063
  if (!live()) {
4047
- notifyReady();
4064
+ readyFromStore();
4048
4065
  return;
4049
4066
  }
4050
4067
  const progress = { complete: true, reachable: true };
4051
4068
  const gate = discover();
4052
- if (attempt === 'opened')
4053
- opened = generation;
4054
- underway = attempt === 'opened' ? { generation, progress } : null;
4069
+ underway = attempt === 'opened' ? progress : null;
4055
4070
  if (attempt === 'opened')
4056
4071
  streamed = progress;
4057
4072
  complete = false;
4058
4073
  recorded = false;
4059
4074
  moveTo(transferring(status));
4060
- handed = false;
4061
4075
  try {
4062
4076
  const local = await openLocal();
4063
4077
  if (!live())
@@ -4085,7 +4099,6 @@ export function composeRuntime(options) {
4085
4099
  if (handed || closed || progress.reachable)
4086
4100
  return;
4087
4101
  underway = null;
4088
- opened = null;
4089
4102
  journal.record('connection reopen');
4090
4103
  await rest(progress, live);
4091
4104
  options.adapter.roomStream?.reopen();
@@ -4122,13 +4135,14 @@ export function composeRuntime(options) {
4122
4135
  });
4123
4136
  }
4124
4137
  function closedSocket(revoked, lost) {
4138
+ notifyDiscovered();
4125
4139
  const opened = socketOpen;
4126
4140
  socketOpen = false;
4141
+ handed = false;
4142
+ naming = 0;
4127
4143
  connectionLost();
4128
- if (lost) {
4129
- reads.lost();
4144
+ if (lost)
4130
4145
  conversations.lost();
4131
- }
4132
4146
  if (revoked) {
4133
4147
  void refuse('revoked-authorization');
4134
4148
  return;
@@ -4143,14 +4157,13 @@ export function composeRuntime(options) {
4143
4157
  });
4144
4158
  }
4145
4159
  function applyOpening(expired) {
4146
- const progress = underway?.generation === generation ? underway.progress : null;
4160
+ const progress = underway;
4161
+ underway = null;
4162
+ naming = Math.max(0, naming - 1);
4147
4163
  const cycle = progress ?? { complete: true, reachable: true };
4148
- if (opened === generation)
4149
- opened = null;
4150
4164
  const reloading = progress === null ? [] : owedReloads(expired);
4151
4165
  const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
4152
4166
  return task(OPENING_LANE, async (live) => {
4153
- underway = null;
4154
4167
  if (!live())
4155
4168
  return;
4156
4169
  if (progress === null) {
@@ -4158,24 +4171,14 @@ export function composeRuntime(options) {
4158
4171
  await rest(cycle, live);
4159
4172
  return;
4160
4173
  }
4161
- await roomsCarried([...expired, ...reloading]);
4162
- if (!live())
4163
- return;
4164
4174
  try {
4165
4175
  const local = await openLocal();
4166
- const reloaded = await replacing;
4176
+ await replacing;
4167
4177
  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
- }
4178
+ await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
4179
+ !expired.includes(room.locator) &&
4180
+ !reloading.includes(room.locator)), 'current', local);
4175
4181
  }
4176
- const failed = reloaded.find((outcome) => outcome.status === 'rejected');
4177
- if (failed !== undefined)
4178
- throw failed.reason;
4179
4182
  }
4180
4183
  catch {
4181
4184
  progress.complete = false;
@@ -4187,11 +4190,8 @@ export function composeRuntime(options) {
4187
4190
  }
4188
4191
  function owedReloads(expired) {
4189
4192
  const reloading = [...discovered.values()]
4190
- .filter((room) => replicas.has(room.locator) &&
4191
- !expired.includes(room.locator) &&
4192
- (owed.has(room.locator) || revoked(room.locator)))
4193
+ .filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
4193
4194
  .map((room) => room.locator);
4194
- owed.clear();
4195
4195
  for (const locator of reloading)
4196
4196
  catching.add(locator);
4197
4197
  liveQueries.recover();
@@ -4200,24 +4200,43 @@ export function composeRuntime(options) {
4200
4200
  async function reloadNow(replaced, progress) {
4201
4201
  const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
4202
4202
  for (const room of reloaded)
4203
- reloadsDue.set(room.locator, generation);
4203
+ reloadsDue.add(room.locator);
4204
4204
  try {
4205
4205
  const local = await openLocal();
4206
- return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
4206
+ for (const room of reloaded) {
4207
+ void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
4208
+ }
4207
4209
  }
4208
4210
  catch {
4209
4211
  for (const room of reloaded)
4210
4212
  reloadsDue.delete(room.locator);
4211
4213
  progress.reachable = false;
4212
- return [];
4213
4214
  }
4214
4215
  }
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' });
4216
+ async function settleModels(held, status, local) {
4217
+ const owed = (room) => {
4218
+ const model = modelOf(room.locator);
4219
+ return model.status !== 'unavailable' && model.status !== status;
4220
+ };
4221
+ const settled = await exclusive(async () => {
4222
+ if (!continuing())
4223
+ return [];
4224
+ const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
4225
+ for (const room of unchanged) {
4226
+ const next = { ...modelOf(room.locator), status };
4227
+ const replica = replicas.get(room.locator);
4228
+ if (replica !== undefined)
4229
+ replicas.set(room.locator, { ...replica, record: { ...replica.record, model: next } });
4230
+ heldModel(room, next);
4220
4231
  }
4232
+ if (unchanged.length > 0)
4233
+ emitRooms();
4234
+ return unchanged;
4235
+ });
4236
+ for (const room of held) {
4237
+ if (settled.includes(room) || !owed(room))
4238
+ continue;
4239
+ await retainModel(local, room, { ...modelOf(room.locator), status });
4221
4240
  }
4222
4241
  }
4223
4242
  function submitLater(locator) {
@@ -4262,14 +4281,10 @@ export function composeRuntime(options) {
4262
4281
  try {
4263
4282
  const local = await openLocal();
4264
4283
  await roomTask(locator, async (live) => {
4265
- const current = () => live() && !signal.aborted;
4266
- if (!current() || authorization === undefined)
4284
+ if (!live() || authorization === undefined)
4267
4285
  return;
4268
- const withheld = await Promise.race([
4269
- advance(room, local, authorization, current),
4270
- abandonedOn(signal),
4271
- ]);
4272
- if (withheld !== null && current())
4286
+ const withheld = await advance(room, local, authorization, live);
4287
+ if (withheld !== null && live())
4273
4288
  await withhold(withheld, room, local, progress);
4274
4289
  });
4275
4290
  }
@@ -4324,14 +4339,11 @@ export function composeRuntime(options) {
4324
4339
  progress.complete = false;
4325
4340
  return;
4326
4341
  }
4327
- reads.applied();
4328
4342
  submitAll();
4329
4343
  for (const locator of stale)
4330
4344
  if (repairable.has(locator))
4331
4345
  repairLater(locator, {});
4332
4346
  await conversations.reconcile();
4333
- if (status.providerUnreachable)
4334
- conversations.reconnected();
4335
4347
  const reading = { complete: true, reachable: true };
4336
4348
  await conversations.catchUp(reading);
4337
4349
  if (progress.reachable && reading.reachable)
@@ -4357,7 +4369,7 @@ export function composeRuntime(options) {
4357
4369
  if (!live())
4358
4370
  return;
4359
4371
  let lastCompleted = status.lastCompleted;
4360
- let synced = complete && stale.size === 0 && !pending();
4372
+ let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
4361
4373
  if (synced && !recorded) {
4362
4374
  const completedAt = new Date().toISOString();
4363
4375
  try {
@@ -4372,74 +4384,79 @@ export function composeRuntime(options) {
4372
4384
  }
4373
4385
  }
4374
4386
  moveTo({
4375
- state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
4387
+ state: pending()
4388
+ ? 'pending'
4389
+ : synced && complete && roomWork === 0 && naming === 0
4390
+ ? 'synced'
4391
+ : 'syncing',
4376
4392
  lastCompleted,
4377
- providerUnreachable: !progress.reachable,
4393
+ providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
4378
4394
  ...attention(),
4379
4395
  });
4380
4396
  }
4381
4397
  function discover() {
4398
+ discovering?.release();
4382
4399
  let release = () => { };
4383
4400
  const done = new Promise((resolve) => {
4384
4401
  release = resolve;
4385
4402
  });
4386
- discovering = { generation, done, release };
4403
+ discovering = { done, release };
4387
4404
  return discovering;
4388
4405
  }
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));
4406
+ function applyRoom(change) {
4407
+ const { locator } = change;
4408
+ if (stopping)
4409
+ return Promise.resolve();
4410
+ return inRoom(locator, async (live) => {
4411
+ await discovering?.done;
4412
+ await answering.get(locator)?.answered;
4413
+ const progress = underway ?? { complete: true, reachable: true };
4412
4414
  try {
4413
4415
  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
- });
4416
+ for (;;) {
4417
+ if (!live())
4418
+ return null;
4419
+ const room = discovered.get(locator);
4420
+ const held = replicas.get(locator);
4421
+ if (room === undefined ||
4422
+ held === undefined ||
4423
+ room.replica.admission !== 'admitted' ||
4424
+ held.record.replica.selection !== room.replica.selection) {
4425
+ return null;
4426
+ }
4427
+ const at = held.record.roomVersion;
4428
+ if (options.adapter.reached(locator, at, change.roomVersion))
4429
+ return progress;
4430
+ if (!options.adapter.reached(locator, at, change.from)) {
4431
+ name(locator);
4432
+ return progress;
4433
+ }
4434
+ if (underway === null)
4435
+ moveTo(transferring(status));
4436
+ const writing = () => live() && !full;
4437
+ const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, () => change.read()));
4438
+ if (refreshed.kind !== 'advanced')
4439
+ return progress;
4440
+ const landing = {
4441
+ kind: 'refreshed',
4442
+ from: refreshed.from,
4443
+ remodelled: refreshed.remodelled,
4444
+ };
4445
+ if (await publish(local, room, refreshed.record, landing, writing)) {
4446
+ stale.delete(locator);
4447
+ await retainModel(local, room, modelOf(locator));
4448
+ return progress;
4449
+ }
4450
+ }
4421
4451
  }
4422
4452
  catch {
4453
+ if (!stale.has(locator))
4454
+ name(locator);
4423
4455
  stale.add(locator);
4456
+ return progress;
4424
4457
  }
4425
- finally {
4426
- if (during === null)
4427
- await rest(progress, live);
4428
- }
4429
- });
4458
+ }).then((progress) => rest(progress), () => undefined);
4430
4459
  }
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
4460
  function observeStream() {
4444
4461
  const stream = options.adapter.roomStream;
4445
4462
  if (stream === undefined || withdrawStream !== undefined)
@@ -4449,9 +4466,10 @@ export function composeRuntime(options) {
4449
4466
  return;
4450
4467
  if (event.kind === 'opened') {
4451
4468
  socketOpen = true;
4469
+ handed = false;
4470
+ naming = 0;
4452
4471
  connectionLive();
4453
4472
  generation += 1;
4454
- reads.opened();
4455
4473
  conversations.reopened();
4456
4474
  recovery.resume();
4457
4475
  void sync('opened');
@@ -4463,15 +4481,8 @@ export function composeRuntime(options) {
4463
4481
  else if (event.kind === 'authority-changed')
4464
4482
  void rediscover(event.scope);
4465
4483
  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]]));
4484
+ void applyRoom(event);
4485
+ });
4475
4486
  }
4476
4487
  function writesHeld() {
4477
4488
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -4541,8 +4552,9 @@ export function composeRuntime(options) {
4541
4552
  if (closed)
4542
4553
  return;
4543
4554
  closed = true;
4544
- if (rechecking !== null)
4545
- clearInterval(rechecking);
4555
+ notifyDiscovered();
4556
+ if (probing !== null)
4557
+ clearInterval(probing);
4546
4558
  connectionLive();
4547
4559
  lockListeners.clear();
4548
4560
  readinessWatches.clear();
@@ -4553,11 +4565,8 @@ export function composeRuntime(options) {
4553
4565
  withdrawServiceMode?.();
4554
4566
  conversations.close();
4555
4567
  void recovery.retire();
4556
- store?.close();
4557
- void opening?.then((opened) => {
4558
- opened.close();
4559
- }, () => { });
4560
4568
  roomListeners.clear();
4569
+ unlistedRooms.clear();
4561
4570
  refusalListeners.clear();
4562
4571
  storageLossListeners.clear();
4563
4572
  statusListeners.clear();
@@ -4570,6 +4579,7 @@ export function composeRuntime(options) {
4570
4579
  }
4571
4580
  async function stop() {
4572
4581
  stopping = true;
4582
+ inFlight.abort();
4573
4583
  await recovery.retire();
4574
4584
  await Promise.all(working);
4575
4585
  await mutations;
@@ -4583,9 +4593,9 @@ export function composeRuntime(options) {
4583
4593
  observeStream();
4584
4594
  void openLocal().then(() => {
4585
4595
  if (capabilities.length > 0)
4586
- notifyReady();
4596
+ readyFromStore();
4587
4597
  }, () => {
4588
- notifyReady();
4598
+ readyFromStore();
4589
4599
  });
4590
4600
  if (options.adapter.roomStream === undefined)
4591
4601
  void sync('unstreamed');
@@ -4632,9 +4642,8 @@ export function composeRuntime(options) {
4632
4642
  }
4633
4643
  async function searched(input, reach, locked) {
4634
4644
  const asked = searchPlanOf(input);
4635
- const plan = { ...asked, limit: capped(asked.limit) };
4636
4645
  const filter = {
4637
- ...filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)),
4646
+ ...filterOf(asked.documents ?? {}, roomFacts(reach), planReach(reach)),
4638
4647
  locked,
4639
4648
  };
4640
4649
  const unlocking = (filter.rooms ?? [])
@@ -4643,65 +4652,41 @@ export function composeRuntime(options) {
4643
4652
  for (;;) {
4644
4653
  const cover = covered(filter);
4645
4654
  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);
4655
+ const pending = [...unlocking, ...('pending' in cover ? cover.pending : [])];
4670
4656
  const complete = pending.length === 0 && !('pending' in cover);
4657
+ const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
4671
4658
  const answering = complete
4672
4659
  ? filter
4673
- : {
4674
- ...filter,
4675
- rooms: covering
4676
- .filter(({ room }) => searchIndex.rooms().has(room))
4677
- .map(({ room }) => room),
4678
- };
4660
+ : { ...filter, rooms: reachedRooms(filter).filter((room) => !awaited.has(room)) };
4679
4661
  const answered = covered(answering);
4680
4662
  if ('pending' in answered)
4681
4663
  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))
4664
+ const listed = conversations.searchable();
4665
+ const named = reach !== null && !reach.conversations
4666
+ ? []
4667
+ : (asked.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
4668
+ const hits = await (await openLocal()).search({
4669
+ text: asked.text,
4670
+ limit: capped(asked.limit),
4671
+ after: asked.after,
4672
+ total: asked.total,
4673
+ documents: {
4674
+ plan: answering,
4675
+ rooms: answered.rooms,
4676
+ beneath: asked.collection
4677
+ ? answered.rooms.map(({ locator, key }) => ({
4678
+ key,
4679
+ roots: replicas.get(locator)?.record.typedSelection?.roots ?? [],
4680
+ }))
4681
+ : [],
4682
+ },
4683
+ conversations: named,
4684
+ });
4685
+ if (JSON.stringify(covered(answering)) !== JSON.stringify(answered))
4690
4686
  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
4687
  const coverage = {
4702
4688
  rooms: answered.coverage.rooms,
4703
- conversations: searchedConversations,
4704
- index: searchIndex.generation(),
4689
+ conversations: named,
4705
4690
  overlay: answered.coverage.overlay,
4706
4691
  };
4707
4692
  return complete
@@ -4758,12 +4743,13 @@ export function composeRuntime(options) {
4758
4743
  };
4759
4744
  }
4760
4745
  if (topic === 'readiness') {
4761
- return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
4746
+ return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
4762
4747
  }
4763
4748
  if (topic === 'rooms') {
4764
- roomListeners.add(deliver);
4765
- deliver(listed());
4749
+ unlistedRooms.add(deliver);
4750
+ emitKnownRooms();
4766
4751
  return () => {
4752
+ unlistedRooms.delete(deliver);
4767
4753
  roomListeners.delete(deliver);
4768
4754
  };
4769
4755
  }
@@ -4817,7 +4803,17 @@ export function composeRuntime(options) {
4817
4803
  return openLocal().then(() => undefined);
4818
4804
  },
4819
4805
  rooms() {
4820
- return listed();
4806
+ if (closed)
4807
+ return Promise.resolve([]);
4808
+ return new Promise((settle) => {
4809
+ const answer = (value) => {
4810
+ unlistedRooms.delete(answer);
4811
+ roomListeners.delete(answer);
4812
+ settle(value);
4813
+ };
4814
+ unlistedRooms.add(answer);
4815
+ emitKnownRooms();
4816
+ });
4821
4817
  },
4822
4818
  query: ((input) => rowsQuery(planned(input, null))),
4823
4819
  search: (input) => searched(input, null, []),
@@ -4827,7 +4823,7 @@ export function composeRuntime(options) {
4827
4823
  typedSelection: rooms.get(locator)?.typedSelection === undefined ||
4828
4824
  rooms.get(locator)?.typedSelection === null
4829
4825
  ? null
4830
- : modelTypes(modelOf(locator, 'typed-selection')),
4826
+ : modelTypes(builtIn),
4831
4827
  };
4832
4828
  },
4833
4829
  roomDraft: {
@@ -4844,33 +4840,17 @@ export function composeRuntime(options) {
4844
4840
  const record = roomDrafts.get(room)?.get(path);
4845
4841
  return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
4846
4842
  },
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
- });
4843
+ write(room, path, revision, input, next = roomDraftRevision()) {
4844
+ if (typeof next !== 'string' || next === '')
4845
+ return Promise.reject(runtimeError('room-draft-malformed'));
4846
+ return gathered(`room-draft\u0000${room}\u0000${path}`, { revision, input, next }, (earlier, later) => {
4847
+ if (later.revision !== earlier.next)
4848
+ return null;
4849
+ const parts = gatheredParts(earlier.input, later.input);
4850
+ return parts === null
4851
+ ? null
4852
+ : { revision: earlier.revision, input: parts, next: later.next };
4853
+ }, (written) => writtenRoomDraft(room, path, written.revision, written.input, written.next));
4874
4854
  },
4875
4855
  discard(room, path, revision) {
4876
4856
  return exclusive(async () => {
@@ -4897,13 +4877,13 @@ export function composeRuntime(options) {
4897
4877
  });
4898
4878
  },
4899
4879
  write(app, value) {
4900
- return keepPresentation(app, () => structuredClone(value));
4880
+ return exclusive(() => keptPresentation(app, () => structuredClone(value)));
4901
4881
  },
4902
4882
  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
- ]));
4883
+ return gathered(`presentation\u0000${app}`, structuredClone(members), (earlier, later) => ({ ...earlier, ...later }), (edited) => keptPresentation(app, (held) => Object.fromEntries([
4884
+ ...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(edited, member)),
4885
+ ...Object.entries(edited).filter(([, value]) => value !== null),
4886
+ ])));
4907
4887
  },
4908
4888
  },
4909
4889
  save(room, named, writtenBy) {
@@ -4922,7 +4902,8 @@ export function composeRuntime(options) {
4922
4902
  part: readPart,
4923
4903
  read: readBinaryPath,
4924
4904
  link: linkBinaryPath,
4925
- readReference: readBinaryReference,
4905
+ show: showBinaryPath,
4906
+ attach: attachBinary,
4926
4907
  },
4927
4908
  async call(name, input) {
4928
4909
  await ready;
@@ -4943,9 +4924,10 @@ export function composeRuntime(options) {
4943
4924
  },
4944
4925
  async retry() {
4945
4926
  journal.record('retry');
4946
- unindexable.clear();
4927
+ unpreparable.clear();
4947
4928
  queueReadiness();
4948
- resume();
4929
+ if (full)
4930
+ await probed();
4949
4931
  },
4950
4932
  observe: observing(watch),
4951
4933
  async reauthorize() {
@@ -4967,7 +4949,6 @@ export function composeRuntime(options) {
4967
4949
  const reachable = {
4968
4950
  md,
4969
4951
  watch,
4970
- referrerScope: (referrer) => options.adapter.referrerScope(referrer),
4971
4952
  plan: planned,
4972
4953
  query: (plan) => rowsQuery(plan),
4973
4954
  watchPlan,
@@ -4977,6 +4958,7 @@ export function composeRuntime(options) {
4977
4958
  lockedRooms,
4978
4959
  watchLocks,
4979
4960
  located,
4961
+ room: (locator) => listed().find((one) => one.locator === locator),
4980
4962
  };
4981
4963
  return {
4982
4964
  md,
@@ -4991,6 +4973,10 @@ export function composeRuntime(options) {
4991
4973
  journal.record(`persistence ${answer}`);
4992
4974
  queueReadiness();
4993
4975
  },
4994
- within: (reach, port) => narrowed(reachable, reach, port),
4976
+ within: (reach, port) => {
4977
+ if (full)
4978
+ void probed();
4979
+ return narrowed(reachable, reach, port);
4980
+ },
4995
4981
  };
4996
4982
  }