@agentshouse/mdruntime 0.1.0-c52f87bb83a70 → 0.1.0-c55c6f69061ee

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