@agentshouse/mdruntime 0.1.0-c2e8a6f99866d → 0.1.0-c2ef9303a058d

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 (220) hide show
  1. package/README.md +11 -9
  2. package/dist/adapter.d.ts +95 -67
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/attached-model.d.ts +5 -11
  5. package/dist/attached-model.d.ts.map +1 -1
  6. package/dist/attached-model.js +36 -47
  7. package/dist/binary.d.ts +0 -5
  8. package/dist/binary.d.ts.map +1 -1
  9. package/dist/binary.js +1 -1
  10. package/dist/bootstrap.d.ts +17 -9
  11. package/dist/bootstrap.d.ts.map +1 -1
  12. package/dist/bootstrap.js +143 -94
  13. package/dist/chain.d.ts +2 -1
  14. package/dist/chain.d.ts.map +1 -1
  15. package/dist/change-set.d.ts +1 -7
  16. package/dist/change-set.d.ts.map +1 -1
  17. package/dist/change-set.js +1 -4
  18. package/dist/client.d.ts +15 -12
  19. package/dist/client.d.ts.map +1 -1
  20. package/dist/client.js +56 -8
  21. package/dist/collection.d.ts +7 -4
  22. package/dist/collection.d.ts.map +1 -1
  23. package/dist/collection.js +24 -19
  24. package/dist/compile.d.ts.map +1 -1
  25. package/dist/compile.js +117 -243
  26. package/dist/connection.d.ts +14 -12
  27. package/dist/connection.d.ts.map +1 -1
  28. package/dist/connection.js +1466 -1431
  29. package/dist/context.d.ts +2 -0
  30. package/dist/context.d.ts.map +1 -0
  31. package/dist/{chat/context.js → context.js} +1 -4
  32. package/dist/conversation.d.ts +18 -15
  33. package/dist/conversation.d.ts.map +1 -1
  34. package/dist/conversation.js +273 -185
  35. package/dist/durability.d.ts +6 -4
  36. package/dist/durability.d.ts.map +1 -1
  37. package/dist/durability.js +5 -0
  38. package/dist/entry.d.ts +3 -2
  39. package/dist/entry.d.ts.map +1 -1
  40. package/dist/entry.js +17 -8
  41. package/dist/file.d.ts +2 -11
  42. package/dist/file.d.ts.map +1 -1
  43. package/dist/file.js +6 -4
  44. package/dist/host.d.ts +3 -7
  45. package/dist/host.d.ts.map +1 -1
  46. package/dist/host.js +55 -38
  47. package/dist/identity.d.ts +0 -2
  48. package/dist/identity.d.ts.map +1 -1
  49. package/dist/identity.js +0 -6
  50. package/dist/index.d.ts +15 -12
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +6 -3
  53. package/dist/job.d.ts +14 -0
  54. package/dist/job.d.ts.map +1 -0
  55. package/dist/job.js +28 -0
  56. package/dist/live.d.ts +2 -1
  57. package/dist/live.d.ts.map +1 -1
  58. package/dist/live.js +9 -5
  59. package/dist/media.d.ts +8 -0
  60. package/dist/media.d.ts.map +1 -0
  61. package/dist/media.js +218 -0
  62. package/dist/memory.d.ts +12 -6
  63. package/dist/memory.d.ts.map +1 -1
  64. package/dist/memory.js +209 -101
  65. package/dist/messages.d.ts +28 -0
  66. package/dist/messages.d.ts.map +1 -0
  67. package/dist/messages.js +89 -0
  68. package/dist/model.d.ts +8 -8
  69. package/dist/model.d.ts.map +1 -1
  70. package/dist/model.js +7 -13
  71. package/dist/observation.d.ts +3 -2
  72. package/dist/observation.d.ts.map +1 -1
  73. package/dist/observation.js +8 -3
  74. package/dist/outcome.d.ts +13 -6
  75. package/dist/outcome.d.ts.map +1 -1
  76. package/dist/outcome.js +16 -5
  77. package/dist/overlay.js +1 -1
  78. package/dist/paths.d.ts +1 -0
  79. package/dist/paths.d.ts.map +1 -1
  80. package/dist/paths.js +4 -0
  81. package/dist/query.d.ts +10 -206
  82. package/dist/query.d.ts.map +1 -1
  83. package/dist/query.js +3 -494
  84. package/dist/reach.d.ts +14 -9
  85. package/dist/reach.d.ts.map +1 -1
  86. package/dist/reach.js +84 -72
  87. package/dist/react.d.ts +3 -1
  88. package/dist/react.d.ts.map +1 -1
  89. package/dist/readiness.d.ts +14 -17
  90. package/dist/readiness.d.ts.map +1 -1
  91. package/dist/readiness.js +2 -8
  92. package/dist/recovery.d.ts +1 -0
  93. package/dist/recovery.d.ts.map +1 -1
  94. package/dist/recovery.js +26 -0
  95. package/dist/refresh.d.ts +10 -4
  96. package/dist/refresh.d.ts.map +1 -1
  97. package/dist/refresh.js +88 -30
  98. package/dist/replica.d.ts +11 -8
  99. package/dist/replica.d.ts.map +1 -1
  100. package/dist/replica.js +30 -15
  101. package/dist/room-draft.d.ts +22 -5
  102. package/dist/room-draft.d.ts.map +1 -1
  103. package/dist/room-draft.js +131 -51
  104. package/dist/rows.d.ts +18 -16
  105. package/dist/rows.d.ts.map +1 -1
  106. package/dist/rows.js +45 -153
  107. package/dist/save.d.ts +2 -0
  108. package/dist/save.d.ts.map +1 -1
  109. package/dist/save.js +13 -7
  110. package/dist/search.d.ts +12 -95
  111. package/dist/search.d.ts.map +1 -1
  112. package/dist/search.js +10 -349
  113. package/dist/shared.d.ts +0 -1
  114. package/dist/shared.d.ts.map +1 -1
  115. package/dist/shared.js +0 -10
  116. package/dist/status.d.ts +4 -3
  117. package/dist/status.d.ts.map +1 -1
  118. package/dist/status.js +3 -1
  119. package/dist/store.d.ts +25 -62
  120. package/dist/store.d.ts.map +1 -1
  121. package/dist/store.js +43 -282
  122. package/dist/stored.d.ts +12 -26
  123. package/dist/stored.d.ts.map +1 -1
  124. package/dist/stored.js +189 -226
  125. package/dist/tables.d.ts +55 -48
  126. package/dist/tables.d.ts.map +1 -1
  127. package/dist/tables.js +107 -57
  128. package/dist/transfer.d.ts +0 -5
  129. package/dist/transfer.d.ts.map +1 -1
  130. package/dist/validation.d.ts +3 -4
  131. package/dist/validation.d.ts.map +1 -1
  132. package/dist/validation.js +39 -97
  133. package/dist/windows.d.ts +1 -12
  134. package/dist/windows.d.ts.map +1 -1
  135. package/dist/windows.js +2 -75
  136. package/dist/wire.d.ts +14 -9
  137. package/dist/wire.d.ts.map +1 -1
  138. package/dist/wire.js +5 -3
  139. package/package.json +14 -18
  140. package/AGENTS.md +0 -312
  141. package/dist/chat/acts.d.ts +0 -33
  142. package/dist/chat/acts.d.ts.map +0 -1
  143. package/dist/chat/acts.js +0 -36
  144. package/dist/chat/context.d.ts +0 -3
  145. package/dist/chat/context.d.ts.map +0 -1
  146. package/dist/chat/earlier.d.ts +0 -10
  147. package/dist/chat/earlier.d.ts.map +0 -1
  148. package/dist/chat/earlier.js +0 -54
  149. package/dist/chat/house.d.ts +0 -25
  150. package/dist/chat/house.d.ts.map +0 -1
  151. package/dist/chat/house.js +0 -38
  152. package/dist/chat/input.d.ts +0 -11
  153. package/dist/chat/input.d.ts.map +0 -1
  154. package/dist/chat/input.js +0 -92
  155. package/dist/chat/kit.d.ts +0 -61
  156. package/dist/chat/kit.d.ts.map +0 -1
  157. package/dist/chat/kit.js +0 -123
  158. package/dist/chat/lasting.d.ts +0 -6
  159. package/dist/chat/lasting.d.ts.map +0 -1
  160. package/dist/chat/lasting.js +0 -19
  161. package/dist/chat/list.d.ts +0 -28
  162. package/dist/chat/list.d.ts.map +0 -1
  163. package/dist/chat/list.js +0 -103
  164. package/dist/chat/markdown.d.ts +0 -21
  165. package/dist/chat/markdown.d.ts.map +0 -1
  166. package/dist/chat/markdown.js +0 -246
  167. package/dist/chat/message.d.ts +0 -7
  168. package/dist/chat/message.d.ts.map +0 -1
  169. package/dist/chat/message.js +0 -244
  170. package/dist/chat/messages.d.ts +0 -30
  171. package/dist/chat/messages.d.ts.map +0 -1
  172. package/dist/chat/messages.js +0 -198
  173. package/dist/chat/name.d.ts +0 -7
  174. package/dist/chat/name.d.ts.map +0 -1
  175. package/dist/chat/name.js +0 -37
  176. package/dist/chat/opened.d.ts +0 -12
  177. package/dist/chat/opened.d.ts.map +0 -1
  178. package/dist/chat/opened.js +0 -19
  179. package/dist/chat/overlay.d.ts +0 -32
  180. package/dist/chat/overlay.d.ts.map +0 -1
  181. package/dist/chat/overlay.js +0 -103
  182. package/dist/chat/parts.d.ts +0 -58
  183. package/dist/chat/parts.d.ts.map +0 -1
  184. package/dist/chat/parts.js +0 -120
  185. package/dist/chat/pieces.d.ts +0 -62
  186. package/dist/chat/pieces.d.ts.map +0 -1
  187. package/dist/chat/pieces.js +0 -1110
  188. package/dist/chat/process.d.ts +0 -20
  189. package/dist/chat/process.d.ts.map +0 -1
  190. package/dist/chat/process.js +0 -32
  191. package/dist/chat/provider.d.ts +0 -70
  192. package/dist/chat/provider.d.ts.map +0 -1
  193. package/dist/chat/provider.js +0 -635
  194. package/dist/chat/question.d.ts +0 -9
  195. package/dist/chat/question.d.ts.map +0 -1
  196. package/dist/chat/question.js +0 -69
  197. package/dist/chat/questions.d.ts +0 -31
  198. package/dist/chat/questions.d.ts.map +0 -1
  199. package/dist/chat/questions.js +0 -111
  200. package/dist/chat/read.d.ts +0 -5
  201. package/dist/chat/read.d.ts.map +0 -1
  202. package/dist/chat/read.js +0 -13
  203. package/dist/chat/ready.d.ts +0 -37
  204. package/dist/chat/ready.d.ts.map +0 -1
  205. package/dist/chat/ready.js +0 -93
  206. package/dist/chat/room-file.d.ts +0 -8
  207. package/dist/chat/room-file.d.ts.map +0 -1
  208. package/dist/chat/room-file.js +0 -72
  209. package/dist/chat/steady.d.ts +0 -3
  210. package/dist/chat/steady.d.ts.map +0 -1
  211. package/dist/chat/steady.js +0 -26
  212. package/dist/chat/style.d.ts +0 -4
  213. package/dist/chat/style.d.ts.map +0 -1
  214. package/dist/chat/style.js +0 -306
  215. package/dist/chat.d.ts +0 -15
  216. package/dist/chat.d.ts.map +0 -1
  217. package/dist/chat.js +0 -8
  218. package/dist/reads.d.ts +0 -11
  219. package/dist/reads.d.ts.map +0 -1
  220. 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 { identityOf, parseFrontmatter, plain, queryKey, roomTarget, target, } from '@agentshouse/mdmodel';
2
+ import { attachedModel, builtInModel, roomTypesOf, schemaTypes } from './attached-model.js';
3
3
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
4
4
  import { observing, whole } from './observation.js';
5
- import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
5
+ import { planWithin, } from './query.js';
6
6
  import { createLiveQueries } from './live.js';
7
7
  import { seededResult } from './seed.js';
8
8
  import { documentKey, roomOfKey } from './rows.js';
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
- import { identityTime, roomDraftRevision } from './identity.js';
35
- import { beneath, normalize } from './paths.js';
33
+ import { roomDraftRevision } from './identity.js';
34
+ import { ancestors, beneath, normalize } from './paths.js';
36
35
  import { openRuntimeStore, } from './store.js';
37
- import { changedCleanly, sameComposition, validatedCollection, } from './validation.js';
36
+ import { validatedNeighbourhood } from './validation.js';
38
37
  import { unconfirmed } from './transfer.js';
39
38
  import { governs, holds, narrowed, planReach, } from './reach.js';
40
- function roomsAlone(plan) {
41
- return (plan.reach === null &&
42
- plan.root.types === null &&
43
- plan.root.where === null &&
44
- plan.root.pathPrefix === null &&
45
- plan.paths === null &&
46
- plan.hop === null &&
47
- plan.backlinks === null);
48
- }
49
39
  function within(root, path) {
50
40
  return root === '' || beneath(root, path);
51
41
  }
@@ -63,39 +53,46 @@ const OPENING_LANE = 'opening';
63
53
  const CONVERSATION_LANE = 'conversations';
64
54
  const ROOM_READS = 256;
65
55
  const ROOM_ATTEMPTS = 16;
66
- const SEARCH_BATCH = 256;
67
56
  const ERASE_BATCH = 512;
68
- const REDERIVE_BATCH = 256;
69
- const WALK_BYTES = 1 << 20;
57
+ const DERIVE_BYTES = 1 << 23;
58
+ const DERIVE_FLOOR = 1 << 18;
59
+ // An index build aims at this much database work, so a Save, a query or the next part's progress waits behind at most one short build.
60
+ const DERIVE_AIM_MS = 250;
61
+ // A build past its floor of text stops after a second and a half of work, its full-text writes included, so a Room whose text costs ten times the usual holds a Save, a query or the next part's progress behind no longer build.
62
+ const DERIVE_LONGEST_MS = 1_500;
70
63
  const QUERY_BYTES = 1 << 20;
71
64
  const STAGE_BYTES = 1 << 20;
72
- const CLEAN_CHANGES = 2_000;
65
+ const STAGE_FLOOR = 1 << 16;
66
+ // A batch aims at this much database work, so a Save or a query waits behind at most one short write.
67
+ const STAGE_AIM_MS = 50;
68
+ const STREAMS_HELD = 4;
69
+ const NOTHING_HELD = { bytes: 0 };
70
+ const QUEUED_TEXT = 1 << 23;
71
+ // Bytes still arriving are reported at most this often, so readiness is not rebuilt for every chunk the network hands over while a slow link still moves the figure.
72
+ const ARRIVAL_REPORT_MS = 250;
73
73
  // A Room that locks offline stays readable this long after the connection drops, so a laptop waking or changing network keeps working while a disconnected device locks soon after.
74
74
  const LOCK_GRACE_MS = 120_000;
75
75
  function operationOf(input) {
76
76
  const operation = input?.operation_id;
77
77
  return typeof operation === 'string' ? operation : null;
78
78
  }
79
- function abandonedOn(signal) {
80
- return new Promise((resolve) => {
81
- if (signal.aborted)
82
- resolve(null);
83
- else
84
- signal.addEventListener('abort', () => resolve(null), { once: true });
79
+ function readUntil(read, signal) {
80
+ if (signal.aborted)
81
+ return Promise.resolve({ kind: 'transport-uncertainty' });
82
+ return new Promise((resolve, reject) => {
83
+ const stop = () => resolve({ kind: 'transport-uncertainty' });
84
+ signal.addEventListener('abort', stop, { once: true });
85
+ read.then(resolve, reject).finally(() => signal.removeEventListener('abort', stop));
85
86
  });
86
87
  }
87
- function roomLane(locator) {
88
- return `room ${locator}`;
89
- }
90
88
  function pendingKey(locator, id) {
91
89
  return `${locator}\u0000${id}`;
92
90
  }
93
91
  export function composeRuntime(options) {
94
92
  const localSaves = options.adapter.localSaves === true;
95
- const roomTypes = roomTypesOf(options.adapter.roomTypes);
93
+ const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
96
94
  let authorization = options.authorization;
97
95
  let opening;
98
- let store;
99
96
  const fences = new Map();
100
97
  const isFenced = (locator) => fences.has(locator);
101
98
  let fencings = 0;
@@ -103,44 +100,53 @@ export function composeRuntime(options) {
103
100
  const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
104
101
  let discoveries = 0;
105
102
  const replicas = new FencedMap(isFenced);
103
+ const reopened = new Set();
104
+ const usableWhenHeld = options.usableWhenHeld ?? [];
106
105
  const queuesWithoutReplica = new FencedMap(isFenced);
107
106
  const pendings = new Map();
107
+ const landedAhead = new Map();
108
108
  const seeds = new Map();
109
109
  const seedWatchers = new Map();
110
110
  const roomDrafts = new FencedMap(isFenced);
111
+ const attachedFiles = new FencedMap(isFenced);
111
112
  const presentations = new Map();
112
113
  let limits = [];
113
114
  let capabilities = [];
114
115
  const rooms = new FencedMap(isFenced);
115
116
  const counted = new Map();
116
117
  const unreadable = new Set();
117
- const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
118
- const models = new FencedMap(isModelFenced);
119
- const liveModels = new FencedMap(isModelFenced);
118
+ const builtIn = builtInModel(roomTypes);
119
+ const typedPaths = options.adapter.typedPaths ?? [];
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, typedPaths);
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,29 +156,31 @@ 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();
160
166
  const repairable = new Map();
161
- let outcomeRetention = null;
162
167
  const answering = new Map();
163
168
  let complete = false;
164
169
  let recorded = false;
165
170
  const stale = new Set();
166
171
  let generation = 0;
167
- let discovering = null;
168
- let opened = null;
169
- const owed = new Set();
170
- const reloadsDue = new Map();
172
+ const reloadsDue = new Set();
173
+ const highest = new Map();
174
+ const beyondHorizon = new Set();
171
175
  const lanes = new Map();
172
- let roomReads = { generation, free: ROOM_READS, waiting: [] };
176
+ const roomLanes = new Map();
177
+ let roomWork = 0;
178
+ const roomReads = { free: ROOM_READS, waiting: [] };
173
179
  const working = new Set();
174
180
  let mutations = Promise.resolve();
181
+ let gathering = null;
175
182
  const roomListeners = new Set();
183
+ const unlistedRooms = new Set();
176
184
  const refusalListeners = new Set();
177
185
  const storageLossListeners = new Set();
178
186
  const statusListeners = new Set();
@@ -183,6 +191,8 @@ export function composeRuntime(options) {
183
191
  let withdrawConversations;
184
192
  let currencyQueued = null;
185
193
  let socketOpen = false;
194
+ let announced = false;
195
+ let missedChange = false;
186
196
  const confirmed = new Set();
187
197
  let lockGrace = null;
188
198
  const lockListeners = new Set();
@@ -192,73 +202,81 @@ export function composeRuntime(options) {
192
202
  const serviceModeListeners = new Set();
193
203
  let member = null;
194
204
  const memberListeners = new Set();
195
- const prepared = new Set();
205
+ let authorityChanges = 0;
206
+ const authorityChangeListeners = new Set();
196
207
  const unpreparedDemand = new Set();
197
- const unindexable = new Set();
208
+ const unpreparable = new Set();
198
209
  let preparing = false;
199
210
  let targets = {};
200
211
  let persistence = options.shell.persistence;
201
- let capacity = null;
202
- const paused = new Set();
203
- let rechecking = null;
212
+ let full = false;
213
+ let probing = null;
214
+ let testing = null;
215
+ const database = {
216
+ async ask(request) {
217
+ try {
218
+ return await options.database.ask(request);
219
+ }
220
+ catch (error) {
221
+ if (refusedForSpace(error))
222
+ filled();
223
+ throw error;
224
+ }
225
+ },
226
+ };
204
227
  const readinessWatches = new Set();
205
228
  let readinessQueued = false;
206
229
  const catching = new Set();
207
230
  const transfers = new FencedMap(isFenced);
231
+ const installs = new Set();
232
+ const arrivals = new Map();
233
+ const streaming = new Set();
234
+ const underived = new Map();
235
+ const unindexed = new Map();
236
+ const startedOver = new Map();
237
+ let arrivalReported = 0;
238
+ let streamsFree = STREAMS_HELD;
239
+ let queuedText = 0;
240
+ const installWaiters = [];
241
+ const stageBudget = {
242
+ bytes: STAGE_FLOOR * 4,
243
+ floor: STAGE_FLOOR,
244
+ ceiling: STAGE_BYTES,
245
+ aim: STAGE_AIM_MS,
246
+ };
247
+ const deriveBudget = {
248
+ bytes: DERIVE_FLOOR * 4,
249
+ floor: DERIVE_FLOOR,
250
+ ceiling: DERIVE_BYTES,
251
+ aim: DERIVE_AIM_MS,
252
+ };
253
+ const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
208
254
  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
255
  call: (name, input) => invokeAction(name, input),
256
+ history: (request) => readConversationHistory(request),
224
257
  durably,
225
258
  stream: () => options.adapter.conversations,
226
259
  publish: publishChangeSet,
227
260
  online: () => connectionOf() !== 'unreachable' && refusal === null,
261
+ full: () => full,
228
262
  closed: () => closed,
229
263
  unanswered: resendLater,
230
- expired: (operation) => pastHorizon(operation),
231
- });
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
264
  });
253
- const roomsToIndex = new Set();
254
- const conversationsToIndex = new Set();
255
- let indexing = false;
256
265
  const journal = createLog();
257
- const keptFiles = openedFiles(options.database, isFenced);
258
- let notifyReady = () => { };
266
+ const keptFiles = openedFiles(database, isFenced);
267
+ let readyFromStore = () => { };
259
268
  const ready = new Promise((resolve) => {
260
- notifyReady = resolve;
269
+ readyFromStore = resolve;
261
270
  });
271
+ let notifyDiscovered = () => { };
272
+ const firstDiscovery = new Promise((resolve) => {
273
+ notifyDiscovered = resolve;
274
+ });
275
+ let roomsNamed = false;
276
+ function notifyReady() {
277
+ notifyDiscovered();
278
+ readyFromStore();
279
+ }
262
280
  function rememberRoomDraft(record) {
263
281
  const room = roomDrafts.get(record.locator) ?? new Map();
264
282
  room.set(record.path, record);
@@ -278,7 +296,7 @@ export function composeRuntime(options) {
278
296
  return undefined;
279
297
  const local = await openLocal();
280
298
  const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
281
- return { ...slice, collection: collectionOf(slice.documents, compositionOf(held.record)) };
299
+ return { ...slice, collection: collectionOf(slice.documents, composed(held.record)) };
282
300
  }
283
301
  function roomDraftOf(record, visible) {
284
302
  return structuredClone({
@@ -334,14 +352,40 @@ export function composeRuntime(options) {
334
352
  const value = presentations.get(app);
335
353
  return value === undefined ? null : structuredClone(value);
336
354
  }
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
- });
355
+ async function writtenRoomDraft(room, path, revision, input, next) {
356
+ const parts = readParts(input, path);
357
+ if (path === '' || normalize(path) !== path)
358
+ throw runtimeError('room-draft-malformed', `room-draft-malformed at ${path}: a path inside the Room, such as notes/a.md`);
359
+ const local = await openLocal();
360
+ const current = roomDrafts.get(room)?.get(path);
361
+ if ((current?.revision ?? null) !== revision) {
362
+ return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
363
+ }
364
+ const file = parts.file === undefined
365
+ ? undefined
366
+ : await heldFile(parts.file.blob, parts.file.base === undefined
367
+ ? binaryBase(await visibleOf(room, [path]), path)
368
+ : parts.file.base);
369
+ const record = {
370
+ locator: room,
371
+ path,
372
+ revision: next,
373
+ parts: held(current?.parts, parts, file),
374
+ returned: current?.returned ?? null,
375
+ };
376
+ await local.keepRoomDraft(record, parts.file === undefined || file === undefined
377
+ ? []
378
+ : [{ key: file.localId, blob: parts.file.blob }]);
379
+ rememberRoomDraft(record);
380
+ publishChangeSet(changeSet(room, { drafts: [path] }));
381
+ return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
382
+ }
383
+ async function keptPresentation(app, next) {
384
+ const local = await openLocal();
385
+ const stored = next(presentations.get(app));
386
+ await local.present({ app, value: stored });
387
+ presentations.set(app, stored);
388
+ publishChangeSet(changeSet('', { presentation: app }));
345
389
  }
346
390
  function emitPresentation(app) {
347
391
  if (closed)
@@ -356,6 +400,20 @@ export function composeRuntime(options) {
356
400
  (queuesWithoutReplica.size > 0 ||
357
401
  [...replicas.values()].some((replica) => replica.queue.length > 0)));
358
402
  }
403
+ function unsent() {
404
+ const changed = new Map();
405
+ for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
406
+ for (const { batch, state } of entriesOf(room)) {
407
+ if (state === 'local')
408
+ continue;
409
+ for (const { path } of batch.changes)
410
+ changed.set(`${room}\u0000${path}`, { room, path });
411
+ }
412
+ }
413
+ return [...changed]
414
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
415
+ .map(([, change]) => change);
416
+ }
359
417
  function attention() {
360
418
  const attentionRequired = [];
361
419
  for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
@@ -367,16 +425,9 @@ export function composeRuntime(options) {
367
425
  returned: structuredClone(record.returned),
368
426
  });
369
427
  }
370
- for (const [room, refusal] of sourceRefusals)
371
- attentionRequired.push({ room, refusal: structuredClone(refusal) });
372
428
  attentionRequired.sort((left, right) => {
373
429
  if (left.room !== right.room)
374
430
  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
431
  if (left.path !== right.path)
381
432
  return left.path < right.path ? -1 : 1;
382
433
  return 0;
@@ -388,7 +439,7 @@ export function composeRuntime(options) {
388
439
  if (reset === null || submission.reset < reset)
389
440
  reset = submission.reset;
390
441
  }
391
- return { attentionRequired, reset };
442
+ return { attentionRequired, unsent: unsent(), reset };
392
443
  }
393
444
  function documentsIn(locator) {
394
445
  return replicas.get(locator)?.count ?? 0;
@@ -461,7 +512,7 @@ export function composeRuntime(options) {
461
512
  const governed = governs(reach, room);
462
513
  const attached = [
463
514
  ...(governed.collection ? [modelOf(room.locator)] : []),
464
- ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
515
+ ...(governed.selection ? [builtIn] : []),
465
516
  ];
466
517
  return {
467
518
  locator: room.locator,
@@ -477,11 +528,26 @@ export function composeRuntime(options) {
477
528
  const value = listed();
478
529
  for (const listener of roomListeners)
479
530
  listener(value);
531
+ emitKnownRooms();
532
+ }
533
+ function emitKnownRooms() {
534
+ conversations.learned();
535
+ if (unlistedRooms.size === 0)
536
+ return;
537
+ if (!roomsListed && rooms.size === 0 && refusal === null && !status.providerUnreachable)
538
+ return;
539
+ const value = listed();
540
+ for (const listener of [...unlistedRooms]) {
541
+ unlistedRooms.delete(listener);
542
+ roomListeners.add(listener);
543
+ listener(value);
544
+ }
480
545
  }
481
546
  function emitRefusal() {
482
547
  queueReadiness();
483
548
  for (const listener of refusalListeners)
484
549
  listener(refusal);
550
+ emitKnownRooms();
485
551
  }
486
552
  function emitStorageLoss() {
487
553
  queueReadiness();
@@ -502,6 +568,7 @@ export function composeRuntime(options) {
502
568
  if (closed)
503
569
  return;
504
570
  queueReadiness();
571
+ emitKnownRooms();
505
572
  const next = status.state === 'synced' && !conversations.current()
506
573
  ? { ...status, state: 'syncing' }
507
574
  : status;
@@ -515,9 +582,6 @@ export function composeRuntime(options) {
515
582
  if (localSaves)
516
583
  return Object.freeze({ batch, state: 'local' });
517
584
  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
585
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
522
586
  }
523
587
  function entriesOf(locator) {
@@ -532,23 +596,29 @@ export function composeRuntime(options) {
532
596
  }
533
597
  function applyChangeSet(set) {
534
598
  liveQueries.touched(set);
535
- searchIndex.changed(set);
536
599
  }
537
600
  function unpublished(error) {
538
601
  journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
539
602
  }
603
+ function indexed(locator) {
604
+ return replicas.get(locator)?.indexed === true;
605
+ }
540
606
  async function prepareReplica(locator) {
541
607
  for (;;) {
542
608
  const held = replicas.get(locator);
543
- if (held === undefined || closed || prepared.has(locator))
609
+ if (held === undefined || closed || held.indexed)
544
610
  return;
545
611
  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)
612
+ const done = await exclusive(async () => {
613
+ const local = await openLocal();
614
+ return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR));
615
+ });
616
+ const now = replicas.get(locator);
617
+ if (now?.record.key !== key)
548
618
  continue;
549
- if (!done)
619
+ if (!done.done)
550
620
  continue;
551
- prepared.add(locator);
621
+ replicas.set(locator, { ...now, indexed: true });
552
622
  publishChangeSet(changeSet(locator, { documents: null }));
553
623
  liveQueries.recover();
554
624
  queueReadiness();
@@ -556,11 +626,11 @@ export function composeRuntime(options) {
556
626
  }
557
627
  }
558
628
  function unprepared() {
559
- return [...unpreparedDemand, ...replicas.keys()].find((locator) => !prepared.has(locator) && !unindexable.has(locator));
629
+ return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
560
630
  }
561
- function unbuilt(locator, error) {
562
- unindexable.add(locator);
563
- journal.record(`index failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
631
+ function unpreparedBy(locator, error) {
632
+ unpreparable.add(locator);
633
+ journal.record(`preparation failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
564
634
  queueReadiness();
565
635
  }
566
636
  function prepare(locators = []) {
@@ -576,7 +646,7 @@ export function composeRuntime(options) {
576
646
  if (closed)
577
647
  return;
578
648
  unpreparedDemand.delete(waiting);
579
- await prepareReplica(waiting).catch((error) => unbuilt(waiting, error));
649
+ await prepareReplica(waiting).catch((error) => unpreparedBy(waiting, error));
580
650
  }
581
651
  }
582
652
  finally {
@@ -617,14 +687,14 @@ export function composeRuntime(options) {
617
687
  pending.push({ room: locator, awaits: 'acquisition' });
618
688
  continue;
619
689
  }
620
- if (!prepared.has(locator)) {
690
+ if (!replica.indexed) {
621
691
  pending.push({ room: locator, awaits: 'preparation' });
622
692
  continue;
623
693
  }
624
694
  coverage.push({
625
695
  room: locator,
626
696
  generation: replica.record.transfer,
627
- current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
697
+ current: !(catching.has(locator) || transfers.has(locator)),
628
698
  });
629
699
  read.push({ locator, key: replica.record.key });
630
700
  for (const batch of replica.queue)
@@ -650,9 +720,7 @@ export function composeRuntime(options) {
650
720
  return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
651
721
  return { listed: named.every((room) => replicas.has(room)), rooms: named };
652
722
  }
653
- function roomState(locator, search) {
654
- if (sourceRefusals.has(locator))
655
- return 'refused';
723
+ function roomState(locator) {
656
724
  const listedRoom = discovered.get(locator);
657
725
  if (roomsListed &&
658
726
  (listedRoom === undefined ||
@@ -661,27 +729,17 @@ export function composeRuntime(options) {
661
729
  return 'unadmitted';
662
730
  if (!replicas.has(locator))
663
731
  return 'acquiring';
664
- if (!prepared.has(locator))
732
+ if (!indexed(locator))
665
733
  return 'preparing';
666
- if (search && !searchIndex.rooms().has(locator))
667
- return 'indexing';
668
734
  return 'complete';
669
735
  }
670
- function conversationCoverage(reach, search) {
736
+ function conversationCoverage(reach) {
671
737
  const declared = reach === null || reach.conversations;
672
- if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
738
+ if (!declared || options.adapter.conversations === undefined)
673
739
  return 'undeclared';
674
740
  if (!conversations.known() || !conversations.current())
675
741
  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';
742
+ return 'complete';
685
743
  }
686
744
  function connectionOf() {
687
745
  if (socketOpen)
@@ -692,58 +750,77 @@ export function composeRuntime(options) {
692
750
  ? 'connected'
693
751
  : 'connecting';
694
752
  }
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
- });
753
+ function bytesOf(room, underway) {
754
+ const transfer = transfers.get(room);
755
+ if (transfer !== undefined) {
756
+ const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
757
+ return {
758
+ done: transfer.done.bytes - Math.ceil(owed),
759
+ total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
760
+ };
761
+ }
762
+ const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
763
+ return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
764
+ }
765
+ function installationOf(coverage) {
766
+ const rooms = { done: 0, total: 0 };
767
+ const bytes = { done: 0, total: 0 };
768
+ let installing = 0;
769
+ let downloading = false;
770
+ for (const { room, state } of coverage) {
771
+ if (state === 'unadmitted')
772
+ continue;
773
+ rooms.total += 1;
774
+ if (state === 'complete')
775
+ rooms.done += 1;
776
+ const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
777
+ if (underway)
778
+ installing += 1;
779
+ if (underway && (streaming.has(room) || !arrivals.has(room)))
780
+ downloading = true;
781
+ const now = bytesOf(room, underway);
782
+ const earlier = underway ? startedOver.get(room) : undefined;
783
+ bytes.done += Math.max(now.done, earlier?.done ?? 0);
784
+ bytes.total += Math.max(now.total, earlier?.total ?? 0);
785
+ }
786
+ const step = installing === 0
787
+ ? null
788
+ : downloading
789
+ ? 'downloading'
790
+ : bytes.done < bytes.total
791
+ ? 'writing'
792
+ : 'replaying';
793
+ return { step, rooms, bytes };
710
794
  }
711
795
  function readinessFor(port, reach) {
712
796
  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))
797
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
798
+ const preparing = coverage
799
+ .filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
717
800
  .map(({ room }) => room);
718
- const preparing = building('preparing');
719
801
  if (preparing.length > 0)
720
802
  prepare(preparing);
721
- const indexing = building('indexing');
722
- if (indexing.length > 0)
723
- index(indexing, []);
724
803
  return readinessOf({
725
804
  code: port.code,
726
805
  coverage: {
727
806
  listed: required.listed,
728
807
  rooms: coverage,
729
- conversations: conversationCoverage(reach, search),
730
- search: search ? 'required' : 'unused',
808
+ conversations: conversationCoverage(reach),
731
809
  },
732
810
  persistence,
733
811
  connection: connectionOf(),
734
812
  house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
735
- capacity: capacity !== null &&
736
- coverage.some(({ room, state }) => paused.has(room) || !SETTLED_ROOMS.has(state))
737
- ? capacity
738
- : port.capacity,
739
- refused: refusedIn(required.rooms),
813
+ space: full ? 'full' : null,
814
+ installation: installationOf(coverage),
740
815
  lost: storageLoss !== null,
741
- established: targets[port.target]?.established === true,
816
+ established: targets[port.target]?.established === true ||
817
+ (usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
742
818
  });
743
819
  }
744
820
  function establish(name) {
745
821
  targets = { ...targets, [name]: { established: true } };
746
822
  queueReadiness();
823
+ probeEachMinute();
747
824
  void durably((local) => local.fact('targets', targets)).catch(() => {
748
825
  journal.record(`target record failed ${name}`);
749
826
  });
@@ -781,87 +858,56 @@ export function composeRuntime(options) {
781
858
  readinessWatches.delete(watch);
782
859
  };
783
860
  }
784
- function pageBytes(page) {
785
- let bytes = 0;
786
- for (const { path, content } of page.entries)
787
- bytes += (path.length + (content?.length ?? 0)) * 2;
788
- return bytes;
789
- }
790
- function exceeded(error) {
791
- return (typeof error === 'object' &&
792
- error !== null &&
793
- error.name === 'QuotaExceededError');
794
- }
795
- async function estimated() {
796
- const estimate = options.shell.estimate === undefined ? null : await options.shell.estimate();
797
- return estimate === null ? null : Math.max(0, estimate.total - estimate.usage);
798
- }
799
- function pause(locator, cause) {
800
- paused.add(locator);
801
- capacity = cause;
802
- journal.record(`capacity ${cause.cause} ${locator} ${cause.needed} ${cause.available ?? '-'}`);
803
- queueReadiness();
804
- if (rechecking !== null)
861
+ function filled() {
862
+ if (full || closed)
805
863
  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);
864
+ full = true;
865
+ journal.record('space full');
866
+ queueReadiness();
867
+ probeEachMinute();
810
868
  }
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();
869
+ function installedTarget() {
870
+ return ((usableWhenHeld.length > 0 && reopened.size > 0) ||
871
+ Object.values(targets).some((target) => target.established));
822
872
  }
823
- function resume() {
824
- if (rechecking !== null)
825
- clearInterval(rechecking);
826
- rechecking = null;
827
- if (paused.size === 0 && capacity === null)
873
+ function probeEachMinute() {
874
+ if (!full || probing !== null || closed || !installedTarget())
828
875
  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;
876
+ // A device whose storage filled after its install tries a write each minute, so space freed elsewhere ends its read-only mode without a Retry.
877
+ probing = setInterval(() => {
878
+ void probed();
879
+ }, 60_000);
845
880
  }
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)
881
+ function probed() {
882
+ testing ??= database.ask({ kind: 'space.probe' }).then(() => {
883
+ testing = null;
884
+ spaceFreed();
855
885
  return true;
856
- pause(locator, { cause: 'insufficient-space', needed, available });
857
- return false;
886
+ }, () => {
887
+ testing = null;
888
+ return false;
889
+ });
890
+ return testing;
858
891
  }
859
- async function exceededOn(locator, bytes) {
860
- const available = await estimated().catch(() => null);
861
- pause(locator, { cause: 'storage-exceeded', needed: bytes, available });
892
+ function spaceFreed() {
893
+ if (!full)
894
+ return;
895
+ full = false;
896
+ if (probing !== null)
897
+ clearInterval(probing);
898
+ probing = null;
899
+ unpreparable.clear();
900
+ journal.record('space freed');
901
+ queueReadiness();
902
+ if (closed || authorization === undefined)
903
+ return;
904
+ if (options.adapter.roomStream === undefined)
905
+ void sync('unstreamed', null);
906
+ else
907
+ void resynchronize();
862
908
  }
863
909
  function seedRoomHeld(room, version) {
864
- return prepared.has(room) && reachedBy(room, version);
910
+ return indexed(room) && reachedBy(room, version);
865
911
  }
866
912
  function seedHeld(at) {
867
913
  return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
@@ -982,83 +1028,12 @@ export function composeRuntime(options) {
982
1028
  const held = answered.cover;
983
1029
  return { ...answered.rows, coverage: held.coverage };
984
1030
  }
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
1031
  function publishChangeSet(set) {
1061
1032
  applyChangeSet(set);
1033
+ for (const { locator, path } of set.documents) {
1034
+ if (path !== null)
1035
+ attachedFiles.get(locator)?.delete(path);
1036
+ }
1062
1037
  if (set.rooms.length > 0 ||
1063
1038
  [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
1064
1039
  emitRooms();
@@ -1077,6 +1052,8 @@ export function composeRuntime(options) {
1077
1052
  }
1078
1053
  async function refuse(reason, provider = null) {
1079
1054
  discoveryPaused = true;
1055
+ inFlight.abort();
1056
+ inFlight = new AbortController();
1080
1057
  try {
1081
1058
  if (reason === 'revoked-authorization') {
1082
1059
  await exclusive(async () => {
@@ -1084,27 +1061,24 @@ export function composeRuntime(options) {
1084
1061
  const records = [...replicas].map(([locator, held]) => ({
1085
1062
  locator,
1086
1063
  held,
1087
- record: { ...held.record, model: revoked, selectionModel: revoked },
1064
+ record: { ...held.record, model: revoked },
1088
1065
  }));
1066
+ let answer = null;
1089
1067
  try {
1090
1068
  const local = await openLocal();
1091
- await local.commit({
1069
+ answer = await local.commit({
1092
1070
  rooms: records.map(({ locator, record }) => ({
1093
1071
  locator,
1094
1072
  key: record.key,
1095
1073
  record: storedRecord(record),
1096
- composition: compositionOf(record),
1074
+ composed: composedOf(composed(record)),
1097
1075
  })),
1098
1076
  });
1099
1077
  }
1100
1078
  catch {
1101
1079
  journal.record('model revocation persistence failed');
1102
1080
  }
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
- }
1081
+ const keys = new Set([...models.keys(), ...rooms.keys()]);
1108
1082
  for (const key of keys) {
1109
1083
  invalidModels.add(key);
1110
1084
  models.set(key, revoked);
@@ -1114,8 +1088,8 @@ export function composeRuntime(options) {
1114
1088
  for (const { locator, held, record } of records) {
1115
1089
  if (replicas.get(locator) !== held)
1116
1090
  continue;
1117
- recomposed(locator, held.record, record);
1118
- replicas.set(locator, { ...held, record });
1091
+ const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
1092
+ replicas.set(locator, { ...held, record, indexed });
1119
1093
  }
1120
1094
  for (const locator of [...rooms.keys()]) {
1121
1095
  const room = rooms.get(locator);
@@ -1159,15 +1133,15 @@ export function composeRuntime(options) {
1159
1133
  ? null
1160
1134
  : {
1161
1135
  roots: selection.roots,
1162
- model: modelTypes(modelOf(room.locator, 'typed-selection')),
1136
+ model: modelTypes(builtIn),
1163
1137
  },
1164
1138
  providerOwnedPaths: room.providerOwnedPaths,
1165
1139
  roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
1166
1140
  synchronizationAvailable,
1167
- refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
1141
+ refusal: scopedRefusals.get(room.locator) ?? null,
1168
1142
  });
1169
1143
  }
1170
- async function applyDiscovery(found, started, covered) {
1144
+ async function applyDiscovery(found, started, left) {
1171
1145
  const lockedBefore = lockedRooms();
1172
1146
  for (const room of found) {
1173
1147
  const fence = fences.get(room.locator);
@@ -1181,18 +1155,11 @@ export function composeRuntime(options) {
1181
1155
  const listedLocators = new Set(listed.map((room) => room.locator));
1182
1156
  for (const locator of listedLocators)
1183
1157
  scopedRefusals.delete(locator);
1184
- if (covered === undefined)
1158
+ if (left === undefined)
1185
1159
  discovered.clear();
1186
- else
1187
- for (const locator of covered)
1188
- discovered.delete(locator);
1189
1160
  for (const room of listed)
1190
1161
  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);
1162
+ await loseRooms(left ?? unlistedHeld(new Set(found.map((room) => room.locator))));
1196
1163
  for (const room of listed) {
1197
1164
  const held = replicas.get(room.locator)?.record;
1198
1165
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -1223,15 +1190,6 @@ export function composeRuntime(options) {
1223
1190
  ]),
1224
1191
  ].filter((locator) => !listed.has(locator));
1225
1192
  }
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
1193
  function fence(locators) {
1236
1194
  const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1237
1195
  const removed = fresh.map((locator) => ({
@@ -1242,36 +1200,33 @@ export function composeRuntime(options) {
1242
1200
  fences.set(locator, { at: discoveries, erased: false });
1243
1201
  lastFenced.set(locator, ++fencings);
1244
1202
  confirmed.delete(locator);
1245
- for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1203
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []) {
1246
1204
  pendings.delete(pendingKey(locator, batch.id));
1205
+ landedAhead.delete(pendingKey(locator, batch.id));
1206
+ }
1247
1207
  replicas.delete(locator);
1208
+ highest.delete(locator);
1209
+ beyondHorizon.delete(locator);
1248
1210
  queuesWithoutReplica.delete(locator);
1249
1211
  rooms.delete(locator);
1250
1212
  roomDrafts.delete(locator);
1251
- transfers.delete(locator);
1213
+ attachedFiles.delete(locator);
1214
+ untransferred(locator);
1215
+ startedOver.delete(locator);
1252
1216
  catching.delete(locator);
1253
1217
  discovered.delete(locator);
1254
- announced.delete(locator);
1255
1218
  unreadable.delete(locator);
1256
1219
  stale.delete(locator);
1257
1220
  repairable.delete(locator);
1258
- unindexable.delete(locator);
1259
- paused.delete(locator);
1260
- prepared.delete(locator);
1221
+ unpreparable.delete(locator);
1261
1222
  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
- }
1223
+ models.delete(locator);
1224
+ liveModels.delete(locator);
1225
+ invalidModels.delete(locator);
1270
1226
  journal.record(`room lost ${locator}`);
1271
1227
  }
1272
1228
  if (fresh.length === 0)
1273
1229
  return fresh;
1274
- searchIndex.forget(fresh);
1275
1230
  publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, { documents: null, room: true, queue: true, drafts }))));
1276
1231
  liveQueries.recover();
1277
1232
  moveTo({ ...status, ...attention() });
@@ -1315,7 +1270,13 @@ export function composeRuntime(options) {
1315
1270
  });
1316
1271
  await eraseFenced(fresh);
1317
1272
  }
1318
- function restored(key, persisted) {
1273
+ function retyped(persisted) {
1274
+ if (persisted.status === 'unavailable')
1275
+ return persisted;
1276
+ return { ...persisted, types: schemaTypes(persisted.snapshot.schema, roomTypes) };
1277
+ }
1278
+ function restored(key, held) {
1279
+ const persisted = retyped(held);
1319
1280
  const prior = models.get(key);
1320
1281
  if (prior !== undefined &&
1321
1282
  prior.status !== 'unavailable' &&
@@ -1346,8 +1307,9 @@ export function composeRuntime(options) {
1346
1307
  const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1347
1308
  targets = held.targets;
1348
1309
  transfers.clear();
1349
- for (const [locator, record] of held.transfers)
1350
- transfers.set(locator, record);
1310
+ arrivals.clear();
1311
+ streaming.clear();
1312
+ startedOver.clear();
1351
1313
  limits = held.providerLimits;
1352
1314
  capabilities = held.capabilities;
1353
1315
  member = held.member;
@@ -1357,15 +1319,16 @@ export function composeRuntime(options) {
1357
1319
  presentations.clear();
1358
1320
  for (const record of held.presentation)
1359
1321
  presentations.set(record.app, record.value);
1360
- conversations.hydrate(heldConversations, chatDrafts);
1322
+ conversations.hydrate(heldConversations, chatDrafts, held.conversationList, held.conversationCursor);
1361
1323
  pendings.clear();
1324
+ landedAhead.clear();
1362
1325
  for (const queue of queues) {
1363
1326
  for (const { batch, state } of queue.pending) {
1364
1327
  pendings.set(pendingKey(queue.locator, batch.id), state);
1365
1328
  }
1366
1329
  }
1367
1330
  replicas.clear();
1368
- prepared.clear();
1331
+ reopened.clear();
1369
1332
  queuesWithoutReplica.clear();
1370
1333
  for (const [locator, queue] of queuedBatches) {
1371
1334
  if (!held.rooms.has(locator))
@@ -1374,39 +1337,40 @@ export function composeRuntime(options) {
1374
1337
  records.forEach((stored, index) => {
1375
1338
  if (fences.has(stored.locator))
1376
1339
  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 };
1340
+ const record = { ...stored, model: restored(stored.locator, stored.model) };
1341
+ reopened.add(record.locator);
1380
1342
  replicas.set(record.locator, {
1381
1343
  record,
1382
1344
  queue: queuedBatches.get(record.locator) ?? [],
1383
1345
  overlaid: held.overlaid.get(record.key) ?? new Set(),
1384
1346
  count: counts[index] ?? 0,
1385
1347
  confirmed: 0,
1348
+ indexed: !held.unindexed.has(record.key),
1386
1349
  });
1387
- if (!held.unprepared.has(record.key))
1388
- prepared.add(record.locator);
1389
1350
  const existing = rooms.get(record.locator);
1390
1351
  if (existing === undefined)
1391
- list(record, false);
1352
+ list(record, record.roomVersion !== null);
1392
1353
  else {
1393
1354
  rooms.set(record.locator, {
1394
1355
  ...existing,
1395
1356
  roomVersion: record.roomVersion,
1396
1357
  operations: record.operations,
1397
1358
  replica: record.replica,
1398
- model: modelTypes(model),
1359
+ model: modelTypes(record.model),
1399
1360
  typedSelection: record.typedSelection === null
1400
1361
  ? null
1401
- : {
1402
- roots: record.typedSelection.roots,
1403
- model: modelTypes(selectionModel),
1404
- },
1362
+ : { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
1405
1363
  });
1406
1364
  }
1407
1365
  });
1408
- for (const key of held.orphans)
1409
- await opened.release(key);
1366
+ try {
1367
+ for (const key of held.orphans)
1368
+ await opened.release(key);
1369
+ }
1370
+ catch (error) {
1371
+ if (!refusedForSpace(error))
1372
+ throw error;
1373
+ }
1410
1374
  if (held.erasing || held.orphans.length > 0)
1411
1375
  queueMicrotask(sweep);
1412
1376
  return {
@@ -1431,8 +1395,7 @@ export function composeRuntime(options) {
1431
1395
  async function open() {
1432
1396
  if (closed)
1433
1397
  throw new Error('runtime closed');
1434
- const opened = await openRuntimeStore(isFenced, options.database);
1435
- store = opened;
1398
+ const opened = openRuntimeStore(isFenced, database);
1436
1399
  const interrupted = await opened.fences();
1437
1400
  for (const locator of interrupted) {
1438
1401
  fences.set(locator, { at: discoveries, erased: false });
@@ -1478,24 +1441,41 @@ export function composeRuntime(options) {
1478
1441
  return opening;
1479
1442
  }
1480
1443
  function exclusive(work) {
1444
+ gathering = null;
1481
1445
  const next = mutations.then(work, work);
1482
1446
  mutations = next.then(() => undefined, () => undefined);
1483
1447
  return next;
1484
1448
  }
1449
+ function gathered(key, input, merge, work) {
1450
+ const tail = gathering;
1451
+ if (tail !== null && tail.key === key) {
1452
+ const merged = merge(tail.input, input);
1453
+ if (merged !== null) {
1454
+ tail.input = merged;
1455
+ return tail.done;
1456
+ }
1457
+ }
1458
+ const entry = { key, input, done: Promise.resolve() };
1459
+ entry.done = exclusive(() => {
1460
+ if (gathering === entry)
1461
+ gathering = null;
1462
+ return work(entry.input);
1463
+ });
1464
+ gathering = entry;
1465
+ return entry.done;
1466
+ }
1485
1467
  function withdrawable(locator) {
1486
1468
  return (batch) => {
1487
1469
  const state = pendings.get(pendingKey(locator, batch.id));
1488
1470
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
1489
1471
  };
1490
1472
  }
1491
- const REWRITE_LINKS = ['field', 'body'];
1492
- const CHECKED_LINKS = ['field', 'section'];
1493
1473
  async function sliceAt(record, view, queue, extra = [], moved = []) {
1494
1474
  const local = await openLocal();
1495
1475
  const needed = new Set([...touchedBy(queue), ...extra]);
1496
1476
  const movedPaths = [...new Set([...movedBy(queue, record.locator), ...moved])];
1497
1477
  if (movedPaths.length > 0) {
1498
- for (const { path } of await local.referrers(record.key, view, movedPaths, REWRITE_LINKS)) {
1478
+ for (const { path } of await local.referrers(record.key, view, movedPaths)) {
1499
1479
  needed.add(path);
1500
1480
  }
1501
1481
  }
@@ -1563,26 +1543,14 @@ export function composeRuntime(options) {
1563
1543
  },
1564
1544
  };
1565
1545
  }
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))
1546
+ function committedRoom(locator, answer) {
1547
+ const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
1548
+ if (!indexed)
1581
1549
  prepare([locator]);
1582
- return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0 };
1550
+ return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
1583
1551
  }
1584
1552
  function replayed(record, confirmedLayer, queue) {
1585
- return stepped(replaying(confirmedLayer, queue, record.locator, compositionOf(record)));
1553
+ return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
1586
1554
  }
1587
1555
  async function overlayDiff(record, before, visible, touched) {
1588
1556
  const kept = before !== undefined && before.record.key === record.key
@@ -1602,8 +1570,6 @@ export function composeRuntime(options) {
1602
1570
  function unpairedIn(locator, batch) {
1603
1571
  if (!queueIn(locator).some((held) => held.id === batch.id))
1604
1572
  return false;
1605
- if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
1606
- return false;
1607
1573
  return !leavingIn(batch, locator) || withdrawable(locator)(batch);
1608
1574
  }
1609
1575
  async function together(local, settlements) {
@@ -1645,11 +1611,17 @@ export function composeRuntime(options) {
1645
1611
  const confirmedLayer = overrides === undefined
1646
1612
  ? read
1647
1613
  : 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));
1614
+ const composition = composed(record);
1615
+ const shown = queue.filter((batch) => {
1616
+ const held = landedAhead.get(pendingKey(locator, batch.id));
1617
+ return (held !== undefined &&
1618
+ Object.entries(held).some(([path, revision]) => confirmedLayer.revisions[path] !== revision));
1619
+ });
1620
+ const unshown = queue.filter((batch) => !shown.includes(batch));
1621
+ const { withdrawn, kept } = await stepped(standing(confirmedLayer, unshown, withdrawable(locator), localSaves, locator, composition));
1622
+ const founded = kept.filter((batch) => !unshown.includes(batch));
1651
1623
  const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
1652
- const unqueued = [];
1624
+ const unqueued = shown.map((batch) => ({ batch, landed: true }));
1653
1625
  for (const batch of withdrawn) {
1654
1626
  unqueued.push({ batch, landed: await landedWhole(locator, batch, confirmedLayer) });
1655
1627
  }
@@ -1688,6 +1660,7 @@ export function composeRuntime(options) {
1688
1660
  key: record.key,
1689
1661
  record: storedRecord(record),
1690
1662
  roots: record.typedSelection?.roots ?? [],
1663
+ composed: composedOf(composition),
1691
1664
  ...settling.commit,
1692
1665
  ...(overrides === undefined
1693
1666
  ? {}
@@ -1713,8 +1686,10 @@ export function composeRuntime(options) {
1713
1686
  commit,
1714
1687
  apply: (answer) => {
1715
1688
  for (const batch of previousQueue) {
1716
- if (!staying.has(batch.id))
1717
- pendings.delete(pendingKey(locator, batch.id));
1689
+ if (staying.has(batch.id))
1690
+ continue;
1691
+ pendings.delete(pendingKey(locator, batch.id));
1692
+ landedAhead.delete(pendingKey(locator, batch.id));
1718
1693
  }
1719
1694
  for (const draft of written.values())
1720
1695
  rememberRoomDraft(draft);
@@ -1729,14 +1704,14 @@ export function composeRuntime(options) {
1729
1704
  const confirmedChanged = settling.commit?.published !== undefined ||
1730
1705
  settling.commit?.refreshed === true ||
1731
1706
  overrides !== undefined;
1732
- recomposed(locator, before?.record, record);
1733
- const { count } = committedRoom(locator, record, answer);
1707
+ const { count, indexed } = committedRoom(locator, answer);
1734
1708
  replicas.set(locator, {
1735
1709
  record,
1736
1710
  queue: kept,
1737
1711
  overlaid: touched,
1738
1712
  count,
1739
1713
  confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
1714
+ indexed,
1740
1715
  });
1741
1716
  return mergeChangeSets([
1742
1717
  changeSet(locator, {
@@ -1751,110 +1726,101 @@ export function composeRuntime(options) {
1751
1726
  };
1752
1727
  }
1753
1728
  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
- }
1729
+ const model = record.model;
1730
+ models.set(locator, model);
1731
+ invalidModels.delete(locator);
1732
+ if (model.status === 'current')
1733
+ liveModels.set(locator, JSON.stringify(model.snapshot));
1734
+ else if (model.status === 'unavailable')
1735
+ liveModels.delete(locator);
1766
1736
  }
1767
1737
  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
- };
1738
+ const answer = answers?.collection;
1739
+ if (answer === undefined)
1740
+ return held;
1741
+ const state = attachedModel(answer, roomTypes);
1742
+ models.set(locator, state);
1743
+ return { collection: state };
1783
1744
  }
1784
- function publish(local, room, record, landing, live) {
1785
- return exclusive(async () => {
1745
+ function publish(local, room, reached, landing, live) {
1746
+ const landed = exclusive(async () => {
1786
1747
  if (!live()) {
1787
1748
  caughtUpTo(room.locator);
1788
1749
  return true;
1789
1750
  }
1790
1751
  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);
1752
+ if (!stands(room.locator, landing.from)) {
1753
+ if (landing.kind === 'refreshed') {
1754
+ await local.unrefresh(reached.key);
1755
+ return false;
1756
+ }
1757
+ caughtUpTo(room.locator);
1758
+ await abandoned(local, room.locator);
1794
1759
  return false;
1795
1760
  }
1761
+ const record = landing.kind === 'refreshed' && prior !== undefined
1762
+ ? {
1763
+ ...prior.record,
1764
+ roomVersion: reached.roomVersion,
1765
+ ...(landing.remodelled ? { model: reached.model } : {}),
1766
+ }
1767
+ : reached;
1796
1768
  adoptModels(room.locator, record);
1797
1769
  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
1770
  let set;
1804
1771
  try {
1805
- set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
1772
+ set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
1806
1773
  ? {
1807
1774
  room: true,
1808
1775
  view: 'refreshed',
1809
- commit: { refreshed: true, composition: compositionOf(record) },
1776
+ commit: { refreshed: true },
1810
1777
  }
1811
1778
  : {
1812
1779
  room: true,
1813
1780
  view: landing.refreshed ? 'refreshed' : 'confirmed',
1814
1781
  commit: {
1815
1782
  published: { from: prior?.record.key ?? null },
1816
- composition: compositionOf(record),
1817
1783
  ...(landing.refreshed ? { refreshed: true } : {}),
1818
1784
  },
1819
1785
  });
1820
1786
  }
1821
1787
  catch (error) {
1822
- if (!exceeded(error))
1788
+ if (!refusedForSpace(error))
1823
1789
  throw error;
1824
- await exceededOn(room.locator, null);
1825
1790
  caughtUpTo(room.locator);
1826
1791
  return true;
1827
1792
  }
1828
1793
  if (landing.kind === 'bootstrapped') {
1829
- transfers.delete(room.locator);
1794
+ highest.delete(room.locator);
1795
+ beyondHorizon.delete(room.locator);
1796
+ untransferred(room.locator);
1797
+ startedOver.delete(room.locator);
1830
1798
  catching.delete(room.locator);
1831
- sweep();
1799
+ if (prior !== undefined && prior.record.key !== record.key)
1800
+ sweep();
1801
+ name(room.locator);
1832
1802
  }
1833
- for (const batch of accepted)
1834
- pendings.delete(pendingKey(room.locator, batch.id));
1835
1803
  stale.delete(room.locator);
1836
- unindexable.delete(room.locator);
1804
+ unpreparable.delete(room.locator);
1837
1805
  if (unreadable.delete(room.locator))
1838
1806
  scopedRefusals.delete(room.locator);
1839
- sourceRefusals.delete(room.locator);
1840
- declarationRefusals.delete(room.locator);
1841
1807
  list(room, true);
1842
1808
  publishChangeSet(set);
1843
1809
  await unpair(local);
1844
1810
  moveTo({ ...status, ...attention() });
1845
- options.adapter.roomStream?.published(room.locator, roomVersion);
1846
- if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1811
+ if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1847
1812
  submitLater(room.locator);
1848
1813
  }
1849
1814
  return true;
1850
1815
  });
1816
+ return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
1851
1817
  }
1852
1818
  async function savedRoom(locator) {
1853
1819
  const local = await openLocal();
1854
1820
  const replica = replicas.get(locator);
1855
1821
  const room = rooms.get(locator);
1856
1822
  if (replica === undefined || room === undefined) {
1857
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1823
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
1858
1824
  }
1859
1825
  return { local, replica, room };
1860
1826
  }
@@ -1872,12 +1838,7 @@ export function composeRuntime(options) {
1872
1838
  return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
1873
1839
  }
1874
1840
  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
- }
1841
+ const read = readSavedParts(named);
1881
1842
  const target = await savedRoom(locator);
1882
1843
  const saved = [];
1883
1844
  const files = [];
@@ -1886,7 +1847,7 @@ export function composeRuntime(options) {
1886
1847
  for (const { path, parts } of read) {
1887
1848
  const file = parts.file === undefined
1888
1849
  ? undefined
1889
- : await heldFile(parts.file.blob, binaryBase(visible, path));
1850
+ : await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
1890
1851
  if (parts.file !== undefined && file !== undefined) {
1891
1852
  files.push({ key: file.localId, blob: parts.file.blob });
1892
1853
  }
@@ -1920,14 +1881,15 @@ export function composeRuntime(options) {
1920
1881
  const removed = changes.flatMap((change) => (change.kind === 'remove' ? [change.path] : []));
1921
1882
  const referring = removed.length === 0
1922
1883
  ? []
1923
- : (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
1924
- const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
1884
+ : (await local.referrers(record.key, 'confirmed', removed)).map(({ path }) => path);
1885
+ const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
1925
1886
  const { visible } = await replayed(record, confirmedLayer, replica.queue);
1926
- const composition = compositionOf(record);
1887
+ const composition = composed(record);
1927
1888
  return {
1928
1889
  record,
1929
1890
  confirmed: confirmedLayer,
1930
1891
  visible,
1892
+ folders: new Set(await local.folders(record.key, 'visible', paths)),
1931
1893
  *visibleOver(queue) {
1932
1894
  return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
1933
1895
  },
@@ -1939,13 +1901,7 @@ export function composeRuntime(options) {
1939
1901
  return false;
1940
1902
  const local = await openLocal();
1941
1903
  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)));
1904
+ return local.typedWithin(record.key, 'visible', roots, false);
1949
1905
  }
1950
1906
  function textDiffers(left, right) {
1951
1907
  return ((left?.content ?? undefined) !== (right?.content ?? undefined) ||
@@ -1956,18 +1912,6 @@ export function composeRuntime(options) {
1956
1912
  return undefined;
1957
1913
  return entry.binary !== null ? null : entry.content;
1958
1914
  }
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
1915
  function lookupOf(local, key, view) {
1972
1916
  return async (paths) => {
1973
1917
  if (paths.length === 0)
@@ -1984,37 +1928,6 @@ export function composeRuntime(options) {
1984
1928
  const read = parseFrontmatter(entry.content ?? '');
1985
1929
  return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
1986
1930
  }
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
1931
  function overriddenLookup(lookup, overrides) {
2019
1932
  return async (paths) => {
2020
1933
  const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
@@ -2032,9 +1945,8 @@ export function composeRuntime(options) {
2032
1945
  }
2033
1946
  function validatorOf(record, confirmedLayer) {
2034
1947
  return async (resulting, touched) => {
2035
- const composition = compositionFor(record);
2036
1948
  const local = await openLocal();
2037
- const changed = [];
1949
+ const changes = [];
2038
1950
  const overrides = new Map();
2039
1951
  const pace = paced();
2040
1952
  for (const path of new Set([...confirmedLayer.held, ...touched])) {
@@ -2045,19 +1957,13 @@ export function composeRuntime(options) {
2045
1957
  continue;
2046
1958
  overrides.set(path, now);
2047
1959
  if (textOf(was) !== textOf(now))
2048
- changed.push({ path, was: textOf(was), now: textOf(now) });
1960
+ changes.push({ path, was: textOf(was), now: textOf(now) });
2049
1961
  }
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);
1962
+ return validatedNeighbourhood(changes, compositionFor(record), {
1963
+ lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
1964
+ texts: textsOf(local, record.key, 'confirmed'),
1965
+ referrers: referrersOf(local, record.key, 'confirmed'),
1966
+ });
2061
1967
  };
2062
1968
  }
2063
1969
  function textsOf(local, key, view) {
@@ -2068,7 +1974,7 @@ export function composeRuntime(options) {
2068
1974
  }
2069
1975
  function referrersOf(local, key, view) {
2070
1976
  return async (targets) => [
2071
- ...new Set((await local.referrers(key, view, targets, CHECKED_LINKS)).map(({ path }) => path)),
1977
+ ...new Set((await local.referrers(key, view, targets)).map(({ path }) => path)),
2072
1978
  ];
2073
1979
  }
2074
1980
  async function sideOf(locator, replica, room, saving) {
@@ -2081,6 +1987,7 @@ export function composeRuntime(options) {
2081
1987
  operations: room.operations,
2082
1988
  providerOwnedPaths: room.providerOwnedPaths,
2083
1989
  visible: saving.visible,
1990
+ folders: saving.folders,
2084
1991
  queued: queuedDocuments(replica.queue),
2085
1992
  unmodeled: await unmodeledIn(record),
2086
1993
  validate: validatorOf(record, saving.confirmed),
@@ -2101,6 +2008,7 @@ export function composeRuntime(options) {
2101
2008
  locator,
2102
2009
  key: saving.record.key,
2103
2010
  roots: saving.record.typedSelection?.roots ?? [],
2011
+ composed: composedOf(composed(saving.record)),
2104
2012
  overlay: diff.overlay,
2105
2013
  });
2106
2014
  }
@@ -2132,13 +2040,13 @@ export function composeRuntime(options) {
2132
2040
  }
2133
2041
  const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
2134
2042
  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)));
2043
+ const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
2136
2044
  if (changed.length > 0)
2137
2045
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2138
2046
  const target = replicas.get(destination);
2139
2047
  const listedRoom = rooms.get(destination);
2140
2048
  if (target === undefined || listedRoom === undefined) {
2141
- throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
2049
+ throw runtimeError('unknown-room', `Unknown Room ${destination}`);
2142
2050
  }
2143
2051
  const arriving = await savingIn(target, [], moves.map(({ to }) => to));
2144
2052
  const built = await buildMove({
@@ -2201,10 +2109,10 @@ export function composeRuntime(options) {
2201
2109
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2202
2110
  const { answer, touched } = committedBoth;
2203
2111
  if (answer.fenced !== null) {
2204
- throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2112
+ throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2205
2113
  }
2206
2114
  if (fences.has(locator) || fences.has(destination)) {
2207
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2115
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2208
2116
  }
2209
2117
  pendings.set(pendingKey(locator, id), {
2210
2118
  drafts: saved,
@@ -2244,6 +2152,8 @@ export function composeRuntime(options) {
2244
2152
  submitLater(locator);
2245
2153
  }
2246
2154
  async function committed(locator, target, saved, consumed, writtenBy, files = []) {
2155
+ if (full)
2156
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
2247
2157
  const into = movedInto(locator, saved);
2248
2158
  if (into !== null) {
2249
2159
  return committedMove(locator, into, target, saved, consumed, writtenBy, files);
@@ -2255,7 +2165,7 @@ export function composeRuntime(options) {
2255
2165
  ...saved.map(({ path }) => path),
2256
2166
  ...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
2257
2167
  ]);
2258
- const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, underneath(held, path, saving.visible)));
2168
+ const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
2259
2169
  if (changed.length > 0)
2260
2170
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2261
2171
  const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
@@ -2283,6 +2193,7 @@ export function composeRuntime(options) {
2283
2193
  providerLimits: limits,
2284
2194
  handle: room.handle,
2285
2195
  visible: saving.visible,
2196
+ folders: saving.folders,
2286
2197
  changes,
2287
2198
  bases: parts.flatMap(({ bases }) => bases),
2288
2199
  queued: queuedDocuments(replica.queue),
@@ -2372,10 +2283,10 @@ export function composeRuntime(options) {
2372
2283
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2373
2284
  const { answer, touched } = done;
2374
2285
  if (answer.fenced !== null) {
2375
- throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2286
+ throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2376
2287
  }
2377
2288
  if (fences.has(locator))
2378
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2289
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2379
2290
  for (const origin of withdrawn.keys())
2380
2291
  pendings.delete(pendingKey(locator, origin));
2381
2292
  for (const [id, drafts] of drafted) {
@@ -2424,7 +2335,7 @@ export function composeRuntime(options) {
2424
2335
  throw new Error('connection refused');
2425
2336
  await local.keepUpload(locator, identifier, upload);
2426
2337
  if (fences.has(locator))
2427
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2338
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2428
2339
  const key = pendingKey(locator, identifier);
2429
2340
  pendings.set(key, { ...pendings.get(key), upload });
2430
2341
  if (!continuing())
@@ -2433,6 +2344,9 @@ export function composeRuntime(options) {
2433
2344
  async function readHistory(locator, path, query = {}) {
2434
2345
  if (closed || refusal !== null)
2435
2346
  return UNCERTAIN;
2347
+ await firstDiscovery;
2348
+ if (!roomsNamed)
2349
+ return UNCERTAIN;
2436
2350
  try {
2437
2351
  await ensureAuthorization();
2438
2352
  }
@@ -2522,14 +2436,12 @@ export function composeRuntime(options) {
2522
2436
  return (await openLocal()).file(localId, change?.kind === 'binary' ? change.mediaType : '');
2523
2437
  };
2524
2438
  }
2525
- function binaryRoom(request) {
2526
- if (request.kind === 'path')
2527
- return request.locator;
2528
- return options.adapter.referrerScope(request.referrer)?.locator ?? null;
2529
- }
2530
2439
  async function transferBinary(request, transfer, answer) {
2531
2440
  if (closed || refusal !== null)
2532
2441
  return UNCERTAIN;
2442
+ await firstDiscovery;
2443
+ if (!roomsNamed)
2444
+ return UNCERTAIN;
2533
2445
  try {
2534
2446
  await ensureAuthorization();
2535
2447
  }
@@ -2539,16 +2451,13 @@ export function composeRuntime(options) {
2539
2451
  if (closed || authorization === undefined || refusal !== null)
2540
2452
  return UNCERTAIN;
2541
2453
  const since = fencings;
2542
- journal.record(request.kind === 'path'
2543
- ? `transfer binary ${request.locator} ${request.path}`
2544
- : `transfer binary ${request.fileRef}`);
2454
+ journal.record(`transfer binary ${request.locator} ${request.path}`);
2545
2455
  try {
2546
2456
  const result = await transfer(authorization);
2547
2457
  const settled = result;
2548
2458
  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 });
2459
+ if (fencedSince(request.locator, since))
2460
+ return failure({ kind: 'room-not-readable', locator: request.locator });
2552
2461
  if (settled.kind === 'scoped-refusal') {
2553
2462
  const room = replicas.get(settled.locator)?.record;
2554
2463
  if (room !== undefined)
@@ -2563,23 +2472,27 @@ export function composeRuntime(options) {
2563
2472
  return UNCERTAIN;
2564
2473
  }
2565
2474
  }
2475
+ function fetchBinary(request, name) {
2476
+ return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2477
+ }
2566
2478
  async function readBinary(request, name) {
2567
2479
  if (closed)
2568
2480
  return UNCERTAIN;
2569
2481
  const held = await keptFiles.find(name);
2570
2482
  if (held !== null)
2571
2483
  return held;
2572
- return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2484
+ return fetchBinary(request, name);
2573
2485
  }
2574
2486
  async function binaryPath(locator, path, revision) {
2575
2487
  if (revision !== undefined)
2576
2488
  return { kind: 'path', locator, path, revision };
2577
2489
  const replica = replicas.get(locator);
2578
- if (replica === undefined)
2579
- return null;
2580
2490
  const local = await openLocal();
2581
- const [held] = await local.entries(replica.record.key, 'visible', [path]);
2582
- if (held?.binary === undefined || held.binary === null)
2491
+ const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
2492
+ const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
2493
+ if (attached !== undefined)
2494
+ return { kind: 'path', locator, path, revision: attached.revision };
2495
+ if (replica === undefined || held?.binary === undefined || held.binary === null)
2583
2496
  return null;
2584
2497
  if (held.binary.state === 'pending')
2585
2498
  return { kind: 'pending', localId: held.binary.localId };
@@ -2595,7 +2508,7 @@ export function composeRuntime(options) {
2595
2508
  const holds = (at) => {
2596
2509
  const kept = confirmedLayer.binaries[at];
2597
2510
  return (kept?.state === 'accepted' &&
2598
- kept.fileRef === record.fileRef &&
2511
+ kept.sha256 === record.sha256 &&
2599
2512
  confirmedLayer.revisions[at] === record.revision);
2600
2513
  };
2601
2514
  let confirmedAt = path;
@@ -2609,58 +2522,93 @@ export function composeRuntime(options) {
2609
2522
  locator,
2610
2523
  path: confirmedAt,
2611
2524
  revision: record.revision,
2612
- fileRef: record.fileRef,
2613
2525
  };
2614
2526
  }
2615
- async function readBinaryPath(locator, path, revision) {
2527
+ async function askedBinary(locator, path, revision) {
2616
2528
  await ready;
2617
2529
  const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2618
- const asked = drafted !== undefined && drafted.localId === revision
2530
+ return drafted !== undefined && drafted.localId === revision
2619
2531
  ? { kind: 'pending', localId: revision }
2620
- : await binaryPath(locator, path, revision);
2532
+ : binaryPath(locator, path, revision);
2533
+ }
2534
+ function pendingBinary(locator, path, localId) {
2535
+ const facts = queuedFacts(locator, path, localId);
2536
+ return facts === null
2537
+ ? absentBinary(path)
2538
+ : {
2539
+ kind: 'binary',
2540
+ file: localId,
2541
+ byteLength: facts.byteLength,
2542
+ mediaType: facts.mediaType,
2543
+ };
2544
+ }
2545
+ async function readBinaryPath(locator, path, revision) {
2546
+ const asked = await askedBinary(locator, path, revision);
2621
2547
  if (asked === null)
2622
2548
  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
- }
2549
+ if (asked.kind === 'pending')
2550
+ return pendingBinary(locator, path, asked.localId);
2634
2551
  return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
2635
2552
  }
2553
+ async function heldMediaType(locator, path) {
2554
+ const replica = replicas.get(locator);
2555
+ const [held] = replica === undefined
2556
+ ? []
2557
+ : await (await openLocal()).entries(replica.record.key, 'visible', [path]);
2558
+ return held === undefined
2559
+ ? attachedFiles.get(locator)?.get(path)?.mediaType
2560
+ : held.binary?.mediaType;
2561
+ }
2562
+ function linkOf(asked, held) {
2563
+ return transferBinary(asked, (authorization) => options.adapter.link(authorization, asked), (result) => binaryLinkAnswer(result, held));
2564
+ }
2636
2565
  async function linkBinaryPath(locator, path, revision) {
2637
2566
  await ready;
2638
2567
  const asked = await binaryPath(locator, path, revision);
2639
2568
  if (asked === null || asked.kind === 'pending')
2640
2569
  return absentBinary(path);
2641
- return transferBinary(asked, (held) => options.adapter.link(held, asked), binaryLinkAnswer);
2570
+ return linkOf(asked, revision === undefined ? await heldMediaType(locator, path) : undefined);
2642
2571
  }
2643
- async function readBinaryReference(reference) {
2572
+ async function showBinaryPath(locator, path, revision) {
2573
+ const asked = await askedBinary(locator, path, revision);
2574
+ if (asked === null)
2575
+ return absentBinary(path);
2576
+ if (asked.kind === 'pending')
2577
+ return pendingBinary(locator, path, asked.localId);
2578
+ if (closed)
2579
+ return UNCERTAIN;
2580
+ const name = { locator, path: asked.path, revision: asked.revision };
2581
+ const kept = await keptFiles.find(name);
2582
+ if (kept !== null)
2583
+ return kept;
2584
+ const mediaType = await heldMediaType(locator, path);
2585
+ if (mediaType?.startsWith('image/') === true)
2586
+ return fetchBinary(asked, name);
2587
+ return linkOf(asked, revision === undefined ? mediaType : undefined);
2588
+ }
2589
+ async function attachBinary(locator, file) {
2644
2590
  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 });
2591
+ await firstDiscovery;
2592
+ if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
2593
+ return OFFLINE;
2594
+ const request = await attachedFile(locator, file);
2595
+ return transferBinary({ locator, path: file.name }, async (held) => {
2596
+ const result = await options.adapter.attach(held, request);
2597
+ if (result.kind === 'attached') {
2598
+ const landed = attachedFiles.get(locator) ?? new Map();
2599
+ landed.set(result.path, { revision: result.revision, mediaType: request.mediaType });
2600
+ attachedFiles.set(locator, landed);
2601
+ }
2602
+ return result;
2603
+ }, attachAnswer);
2649
2604
  }
2650
2605
  function readPart(file, part) {
2651
- return options.database.ask({ kind: 'file.part', key: file, part });
2606
+ return database.ask({ kind: 'file.part', key: file, part });
2652
2607
  }
2653
2608
  async function cleanUp() {
2654
- const freed = await options.database.ask({ kind: 'opened.clean' });
2609
+ const freed = await database.ask({ kind: 'opened.clean' });
2655
2610
  return freed + ((await options.shell.cleanUp?.()) ?? 0);
2656
2611
  }
2657
- function declaredAgents() {
2658
- for (const capability of capabilities) {
2659
- if (capability.conversationAgents !== undefined)
2660
- return capability.conversationAgents;
2661
- }
2662
- return undefined;
2663
- }
2664
2612
  function declaredAction(name) {
2665
2613
  for (const capability of capabilities) {
2666
2614
  for (const action of capability.actions ?? []) {
@@ -2712,6 +2660,24 @@ export function composeRuntime(options) {
2712
2660
  return UNCERTAIN;
2713
2661
  }
2714
2662
  }
2663
+ async function readConversationHistory(request) {
2664
+ await ready;
2665
+ const stream = options.adapter.conversations;
2666
+ if (closed || refusal !== null || stream?.history === undefined)
2667
+ return UNCERTAIN;
2668
+ journal.record('transfer conversation history');
2669
+ try {
2670
+ const result = await stream.history(request);
2671
+ if (result.kind === 'revoked-authorization')
2672
+ await refuse('revoked-authorization');
2673
+ journal.record(`outcome conversation history ${result.kind}`);
2674
+ return calledResult(result);
2675
+ }
2676
+ catch {
2677
+ journal.record('outcome conversation history transport-uncertainty');
2678
+ return UNCERTAIN;
2679
+ }
2680
+ }
2715
2681
  function unsettled(result) {
2716
2682
  return result.kind !== 'action' && result.retryable;
2717
2683
  }
@@ -2750,10 +2716,6 @@ export function composeRuntime(options) {
2750
2716
  continue;
2751
2717
  if (closed || signal.aborted)
2752
2718
  return 'done';
2753
- if (pastHorizon(held.operation)) {
2754
- await local.dropCall(held.operation);
2755
- continue;
2756
- }
2757
2719
  calling.add(held.operation);
2758
2720
  try {
2759
2721
  if (unsettled(await invokeAction(held.name, held.input)))
@@ -2773,6 +2735,33 @@ export function composeRuntime(options) {
2773
2735
  const held = replicas.get(locator);
2774
2736
  return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
2775
2737
  }
2738
+ function shownAt(locator) {
2739
+ const held = replicas.get(locator)?.record.roomVersion;
2740
+ const raised = highest.get(locator);
2741
+ if (held === undefined || raised === undefined)
2742
+ return held ?? raised;
2743
+ return options.adapter.reached(locator, held, raised) ? held : raised;
2744
+ }
2745
+ function showing(locator, roomVersion) {
2746
+ const at = shownAt(locator);
2747
+ if (at === undefined || !options.adapter.reached(locator, at, roomVersion)) {
2748
+ highest.set(locator, roomVersion);
2749
+ }
2750
+ }
2751
+ function older(locator, roomVersion) {
2752
+ if (beyondHorizon.has(locator))
2753
+ return false;
2754
+ const at = shownAt(locator);
2755
+ return (at !== undefined &&
2756
+ options.adapter.reached(locator, at, roomVersion) &&
2757
+ !options.adapter.reached(locator, roomVersion, at));
2758
+ }
2759
+ function replaced(locators) {
2760
+ for (const locator of locators) {
2761
+ highest.delete(locator);
2762
+ beyondHorizon.add(locator);
2763
+ }
2764
+ }
2776
2765
  async function writtenAfter(locator, roomVersion, path) {
2777
2766
  const held = replicas.get(locator);
2778
2767
  if (held === undefined)
@@ -2783,169 +2772,233 @@ export function composeRuntime(options) {
2783
2772
  options.adapter.reached(locator, written, roomVersion) &&
2784
2773
  !options.adapter.reached(locator, roomVersion, written));
2785
2774
  }
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;
2775
+ async function writtenLater(local, locator, key, paths, roomVersion) {
2776
+ if (paths.length === 0 || !older(locator, roomVersion))
2777
+ return new Set();
2778
+ const entries = await local.entries(key, 'confirmed', paths);
2779
+ return new Set(entries.flatMap(({ path, written }) => written !== null &&
2780
+ options.adapter.reached(locator, written, roomVersion) &&
2781
+ !options.adapter.reached(locator, roomVersion, written)
2782
+ ? [path]
2783
+ : []));
2880
2784
  }
2881
- function refreshingOf(local, key, liveness) {
2785
+ function refreshingOf(local, locator, key, composition, liveness, replaying = false) {
2786
+ const write = replaying ? finishingWrite : exclusive;
2882
2787
  return {
2788
+ turn() {
2789
+ if (replaying)
2790
+ installing(locator);
2791
+ },
2883
2792
  prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
2793
+ later: (paths, roomVersion) => writtenLater(local, locator, key, paths, roomVersion),
2884
2794
  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 () => {
2795
+ for (let written = 0;;) {
2796
+ const end = batchEnd(page.entries, written);
2797
+ const last = end === page.entries.length;
2798
+ const entries = page.entries.slice(written, end);
2799
+ const staged = await write(async () => {
2889
2800
  if (!liveness())
2890
2801
  return false;
2891
- await local.refresh(key, last
2802
+ await timed(stageBudget, () => local.refresh(key, composition, last
2892
2803
  ? { ...page, entries }
2893
- : { entries, removals: [], folders: [], folderRemovals: [] });
2804
+ : { entries, removals: [], folders: [], folderRemovals: [] }));
2894
2805
  return true;
2895
2806
  });
2896
2807
  if (!staged)
2897
2808
  return false;
2809
+ if (last)
2810
+ return true;
2811
+ written = end;
2898
2812
  }
2899
- return true;
2900
2813
  },
2901
2814
  };
2902
2815
  }
2903
- function batched(entries) {
2904
- const batches = [[]];
2816
+ function batchEnd(entries, from) {
2817
+ let end = from;
2905
2818
  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);
2819
+ while (end < entries.length) {
2820
+ const size = (entries[end].content?.length ?? 0) + ROW_WEIGHT;
2821
+ if (spent + size > stageBudget.bytes && end > from)
2822
+ break;
2913
2823
  spent += size;
2914
- }
2915
- return batches;
2824
+ end += 1;
2825
+ }
2826
+ return end;
2827
+ }
2828
+ async function timed(budget, work) {
2829
+ const started = performance.now();
2830
+ const done = await work();
2831
+ const spent = performance.now() - started;
2832
+ if (spent < budget.aim / 2)
2833
+ budget.bytes = Math.min(budget.ceiling, Math.round(budget.bytes * 1.5));
2834
+ else if (spent > budget.aim * 2) {
2835
+ budget.bytes = Math.max(budget.floor, Math.round(budget.bytes / 1.5));
2836
+ }
2837
+ return done;
2838
+ }
2839
+ function installWrite(work) {
2840
+ return installTurn(installTurns.starting, work);
2841
+ }
2842
+ function finishingWrite(work) {
2843
+ return installTurn(installTurns.finishing, work);
2844
+ }
2845
+ function installTurn(queue, work) {
2846
+ return new Promise((resolve, reject) => {
2847
+ queue.push(() => void exclusive(work)
2848
+ .then(resolve, reject)
2849
+ .finally(() => {
2850
+ installTurns.busy = false;
2851
+ nextInstallTurn();
2852
+ }));
2853
+ nextInstallTurn();
2854
+ });
2916
2855
  }
2917
- async function pullRefresh(authorized, held, liveness) {
2918
- if (!liveness())
2919
- return unconfirmed;
2920
- const turn = generation;
2921
- const { record } = held;
2922
- const base = held.confirmed;
2923
- const local = await openLocal();
2856
+ function nextInstallTurn() {
2857
+ if (installTurns.busy || installTurns.ahead > 0)
2858
+ return;
2859
+ const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
2860
+ if (turn === undefined)
2861
+ return;
2862
+ installTurns.busy = true;
2863
+ turn();
2864
+ }
2865
+ async function ahead(work) {
2866
+ let taken = false;
2867
+ const hold = {
2868
+ take() {
2869
+ if (taken)
2870
+ return;
2871
+ taken = true;
2872
+ installTurns.ahead += 1;
2873
+ },
2874
+ release() {
2875
+ if (!taken)
2876
+ return;
2877
+ taken = false;
2878
+ installTurns.ahead -= 1;
2879
+ resumeInstall();
2880
+ },
2881
+ };
2882
+ hold.take();
2924
2883
  try {
2925
- await exclusive(() => local.unrefresh(record.key));
2926
- const result = await advancement(reads, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, liveness));
2927
- if (result.kind !== 'advanced') {
2928
- journal.record(`transfer refresh ${record.locator} ${result.kind}`);
2929
- return result;
2930
- }
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;
2884
+ return await work(hold);
2885
+ }
2886
+ finally {
2887
+ hold.release();
2888
+ }
2889
+ }
2890
+ function resumeInstall() {
2891
+ if (installTurns.ahead > 0)
2892
+ return;
2893
+ void yielded().then(nextInstallTurn);
2894
+ }
2895
+ async function heldStream() {
2896
+ while (streamsFree === 0)
2897
+ await new Promise((admit) => installWaiters.push(admit));
2898
+ streamsFree -= 1;
2899
+ return once(() => {
2900
+ streamsFree += 1;
2901
+ });
2902
+ }
2903
+ async function unqueued() {
2904
+ while (queuedText >= QUEUED_TEXT) {
2905
+ await new Promise((admit) => installWaiters.push(admit));
2906
+ }
2907
+ }
2908
+ function once(release) {
2909
+ let released = false;
2910
+ return () => {
2911
+ if (released)
2912
+ return;
2913
+ released = true;
2914
+ release();
2915
+ for (const admit of installWaiters.splice(0))
2916
+ admit();
2917
+ };
2918
+ }
2919
+ function installing(locator) {
2920
+ installs.add(locator);
2921
+ queueReadiness();
2922
+ }
2923
+ function untransferred(locator) {
2924
+ transfers.delete(locator);
2925
+ arrivals.delete(locator);
2926
+ streaming.delete(locator);
2927
+ underived.delete(locator);
2928
+ unindexed.delete(locator);
2929
+ }
2930
+ function setBack(locator) {
2931
+ const now = bytesOf(locator, true);
2932
+ const earlier = startedOver.get(locator);
2933
+ startedOver.set(locator, {
2934
+ done: Math.max(now.done, earlier?.done ?? 0),
2935
+ total: Math.max(now.total, earlier?.total ?? 0),
2936
+ });
2937
+ }
2938
+ function owedIndexes(locator, bytes, text) {
2939
+ if (bytes <= 0 || text <= 0)
2940
+ return;
2941
+ unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
2942
+ }
2943
+ function built(locator, { done, text }) {
2944
+ const owed = unindexed.get(locator) ?? [];
2945
+ let left = text;
2946
+ while (!done && left > 0 && owed.length > 0) {
2947
+ const [first] = owed;
2948
+ if (left >= first.text) {
2949
+ left -= first.text;
2950
+ owed.shift();
2951
+ continue;
2952
+ }
2953
+ owed[0] = {
2954
+ bytes: (first.bytes * (first.text - left)) / first.text,
2955
+ text: first.text - left,
2956
+ };
2957
+ left = 0;
2958
+ }
2959
+ if (done || owed.length === 0)
2960
+ unindexed.delete(locator);
2961
+ queueReadiness();
2962
+ }
2963
+ function installed(locator) {
2964
+ if (installs.delete(locator))
2965
+ queueReadiness();
2966
+ }
2967
+ async function pullRefresh(authorized, held, liveness, hold, refresh) {
2968
+ if (!liveness())
2969
+ return unconfirmed;
2970
+ const { record } = held;
2971
+ const from = standingOf(record.locator);
2972
+ const local = await openLocal();
2973
+ try {
2974
+ await exclusive(() => local.unrefresh(record.key));
2975
+ const result = await advancement(async (authorization, request) => {
2976
+ hold.release();
2977
+ try {
2978
+ return await refresh(authorization, request);
2979
+ }
2980
+ finally {
2981
+ hold.take();
2982
+ }
2983
+ }, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.locator, record.key, composed(record), liveness));
2984
+ if (result.kind !== 'advanced') {
2985
+ journal.record(`transfer refresh ${record.locator} ${result.kind}`);
2986
+ return result;
2938
2987
  }
2939
2988
  journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
2940
- return { kind: 'advanced', record: result.record, base };
2989
+ return {
2990
+ kind: 'advanced',
2991
+ record: result.record,
2992
+ from,
2993
+ remodelled: result.record.model !== record.model,
2994
+ };
2941
2995
  }
2942
2996
  catch (error) {
2997
+ if (refusedForSpace(error))
2998
+ return unconfirmed;
2943
2999
  journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
2944
3000
  throw error;
2945
3001
  }
2946
- finally {
2947
- reads.finished(record.locator, turn);
2948
- }
2949
3002
  }
2950
3003
  const heldContents = new Map();
2951
3004
  function heldContentOf(local, key) {
@@ -2963,63 +3016,115 @@ export function composeRuntime(options) {
2963
3016
  heldContents.set(key, made);
2964
3017
  return made;
2965
3018
  }
3019
+ async function abandoned(local, locator) {
3020
+ await local.abandon(locator);
3021
+ setBack(locator);
3022
+ untransferred(locator);
3023
+ liveQueries.recover();
3024
+ sweep();
3025
+ }
2966
3026
  function staging(local, liveness) {
3027
+ const derived = async (locator, key, composition) => {
3028
+ underived.delete(locator);
3029
+ for (;;) {
3030
+ if (!liveness())
3031
+ return false;
3032
+ const done = await finishingWrite(async () => liveness()
3033
+ ? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
3034
+ : null);
3035
+ if (done === null)
3036
+ return false;
3037
+ built(locator, done);
3038
+ if (done.done)
3039
+ return true;
3040
+ }
3041
+ };
2967
3042
  return {
2968
3043
  held: (locator) => transfers.get(locator) ?? null,
3044
+ async turn(locator, step) {
3045
+ installing(locator);
3046
+ return step === 'downloading' ? heldStream() : () => { };
3047
+ },
3048
+ unqueued,
3049
+ queued(text) {
3050
+ queuedText += text;
3051
+ return once(() => {
3052
+ queuedText -= text;
3053
+ });
3054
+ },
3055
+ arriving(locator) {
3056
+ let ended = false;
3057
+ return {
3058
+ at(bytes) {
3059
+ if (ended)
3060
+ return;
3061
+ arrivals.set(locator, bytes);
3062
+ streaming.add(locator);
3063
+ const now = performance.now();
3064
+ if (now - arrivalReported < ARRIVAL_REPORT_MS)
3065
+ return;
3066
+ arrivalReported = now;
3067
+ queueReadiness();
3068
+ },
3069
+ end() {
3070
+ if (ended)
3071
+ return;
3072
+ ended = true;
3073
+ if (streaming.delete(locator))
3074
+ queueReadiness();
3075
+ },
3076
+ };
3077
+ },
3078
+ streamed: options.adapter.roomStream !== undefined,
2969
3079
  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)
3080
+ const { locator } = record;
3081
+ installing(locator);
3082
+ if (!liveness())
3083
+ return null;
3084
+ const key = await installWrite(async () => {
3085
+ if (!liveness())
2991
3086
  return null;
2992
- key = staged;
2993
- }
2994
- transfers.set(record.locator, { ...record, key });
3087
+ return local.stage(record, composition, page);
3088
+ });
3089
+ if (key === null || !liveness())
3090
+ return null;
3091
+ const arrived = !transfers.has(locator);
3092
+ const prior = transfers.get(locator)?.done.bytes ?? 0;
3093
+ const held = { ...record, key };
3094
+ transfers.set(locator, held);
3095
+ if (arrived)
3096
+ liveQueries.recover();
3097
+ installing(locator);
2995
3098
  liveQueries.recover();
2996
- return key;
3099
+ const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
3100
+ owedIndexes(locator, record.done.bytes - prior, text);
3101
+ const owed = text + (underived.get(locator) ?? 0);
3102
+ underived.set(locator, owed);
3103
+ if (owed < deriveBudget.bytes)
3104
+ return held;
3105
+ return (await derived(locator, key, composition)) ? held : null;
2997
3106
  },
2998
- async refreshing(key) {
3107
+ derive: derived,
3108
+ async refreshing(locator, key, composition) {
2999
3109
  await exclusive(() => local.unrefresh(key));
3000
- return refreshingOf(local, key, liveness);
3110
+ return refreshingOf(local, locator, key, composition, liveness, true);
3001
3111
  },
3002
3112
  abandon: (locator) => exclusive(async () => {
3003
- if (!liveness())
3004
- return;
3005
- await local.abandon(locator);
3006
- transfers.delete(locator);
3007
- liveQueries.recover();
3008
- sweep();
3113
+ if (liveness())
3114
+ await abandoned(local, locator);
3009
3115
  }),
3010
- setAside: (locator) => exclusive(async () => {
3116
+ setAside: async (locator) => {
3011
3117
  if (!liveness())
3012
3118
  return;
3013
- await local.abandon(locator, true);
3014
- transfers.delete(locator);
3119
+ setBack(locator);
3120
+ untransferred(locator);
3015
3121
  liveQueries.recover();
3016
- }),
3122
+ },
3017
3123
  release: (key) => exclusive(async () => {
3018
3124
  await local.release(key);
3019
3125
  sweep();
3020
3126
  }),
3021
3127
  content: (key) => heldContentOf(local, key),
3022
- validate: (record, view) => validatedRoom(local, record, view, null),
3023
3128
  };
3024
3129
  }
3025
3130
  function reusable(room, local) {
@@ -3031,30 +3136,35 @@ export function composeRuntime(options) {
3031
3136
  async function pullBootstrap(authorized, room, local, liveness) {
3032
3137
  if (!liveness())
3033
3138
  return unconfirmed;
3034
- const turn = generation;
3035
3139
  if (replicas.has(room.locator)) {
3036
3140
  catching.add(room.locator);
3037
3141
  liveQueries.recover();
3038
3142
  }
3039
3143
  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));
3144
+ const result = await bootstrapRoom(reads, authorized, room, { collection: modelOf(room.locator) }, builtIn, typedPaths, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
3044
3145
  const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
3045
3146
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
3046
- if (result.kind !== 'staged')
3147
+ if (result.kind === 'staged' && older(room.locator, result.record.roomVersion)) {
3047
3148
  caughtUpTo(room.locator);
3149
+ installed(room.locator);
3150
+ await abandoned(local, room.locator);
3151
+ journal.record(`transfer bootstrap ${room.locator} older than shown`);
3152
+ return { kind: 'transport-uncertainty' };
3153
+ }
3154
+ if (result.kind !== 'staged') {
3155
+ caughtUpTo(room.locator);
3156
+ installed(room.locator);
3157
+ }
3048
3158
  return result;
3049
3159
  }
3050
3160
  catch (error) {
3051
- journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
3052
3161
  caughtUpTo(room.locator);
3162
+ installed(room.locator);
3163
+ if (refusedForSpace(error))
3164
+ return unconfirmed;
3165
+ journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
3053
3166
  throw error;
3054
3167
  }
3055
- finally {
3056
- reads.finished(room.locator, turn);
3057
- }
3058
3168
  }
3059
3169
  function caughtUpTo(locator) {
3060
3170
  if (catching.delete(locator))
@@ -3062,72 +3172,83 @@ export function composeRuntime(options) {
3062
3172
  }
3063
3173
  function behind(room) {
3064
3174
  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)));
3175
+ return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
3067
3176
  }
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);
3177
+ function standingOf(locator) {
3178
+ const held = replicas.get(locator);
3179
+ return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
3180
+ }
3181
+ function stands(locator, from) {
3182
+ const now = standingOf(locator);
3183
+ return now.key === from.key && now.confirmed === from.confirmed;
3075
3184
  }
3076
- async function advanceOnce(room, local, authorized, live) {
3077
- if (!live() || paused.has(room.locator))
3185
+ async function advance(room, local, authorized, running) {
3186
+ const live = () => running() && !full;
3187
+ if (!live())
3078
3188
  return unconfirmed;
3079
3189
  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')
3190
+ if (held !== undefined && options.adapter.roomStream !== undefined) {
3191
+ stale.delete(room.locator);
3192
+ name(room.locator);
3193
+ return null;
3194
+ }
3195
+ const refresh = reads.refresh;
3196
+ if (held !== undefined && refresh !== undefined) {
3197
+ const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
3198
+ if (refreshed === 'behind')
3199
+ return advance(room, local, authorized, live);
3200
+ if (refreshed !== 'beyond-horizon')
3107
3201
  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)
3202
+ if (reloadsDue.has(room.locator))
3114
3203
  return unconfirmed;
3204
+ replaced([room.locator]);
3115
3205
  }
3206
+ const from = standingOf(room.locator);
3116
3207
  const bootstrapped = await pullBootstrap(authorized, room, local, live);
3208
+ if (bootstrapped.kind !== 'staged')
3209
+ return live() ? bootstrapped : unconfirmed;
3210
+ const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
3211
+ if (await publish(local, room, bootstrapped.record, landing, live)) {
3212
+ return live() ? null : unconfirmed;
3213
+ }
3214
+ return reachedBy(room.locator, bootstrapped.record.roomVersion)
3215
+ ? null
3216
+ : advance(room, local, authorized, live);
3217
+ }
3218
+ async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
3219
+ const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
3117
3220
  if (!live())
3118
3221
  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;
3222
+ if (refreshed.kind === 'still-current') {
3223
+ stale.delete(room.locator);
3224
+ if (unreadable.delete(room.locator)) {
3225
+ scopedRefusals.delete(room.locator);
3226
+ list(room, true);
3227
+ emitRooms();
3228
+ }
3229
+ return null;
3230
+ }
3231
+ if (refreshed.kind === 'advanced') {
3232
+ const landing = {
3233
+ kind: 'refreshed',
3234
+ from: refreshed.from,
3235
+ remodelled: refreshed.remodelled,
3236
+ };
3237
+ if (!(await publish(local, room, refreshed.record, landing, live))) {
3238
+ const now = replicas.get(room.locator)?.record.roomVersion;
3239
+ const reached = now !== undefined &&
3240
+ options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3241
+ return reached ? null : 'behind';
3242
+ }
3243
+ await retainModel(local, room, modelOf(room.locator));
3244
+ return null;
3245
+ }
3246
+ return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
3123
3247
  }
3124
3248
  async function withhold(withheld, room, local, progress) {
3125
- if (withheld.kind === 'unconfirmed')
3249
+ if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
3126
3250
  return;
3127
- if (withheld.kind === 'refused')
3128
- stale.delete(room.locator);
3129
- else
3130
- stale.add(room.locator);
3251
+ stale.add(room.locator);
3131
3252
  if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
3132
3253
  const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
3133
3254
  repairable.set(room.locator, reset);
@@ -3149,33 +3270,11 @@ export function composeRuntime(options) {
3149
3270
  list(room, false);
3150
3271
  emitRooms();
3151
3272
  }
3152
- else if (withheld.kind === 'refused') {
3153
- refusedSource(room, withheld);
3154
- }
3155
3273
  else if (withheld.kind === 'revoked-authorization') {
3156
3274
  await refuse('revoked-authorization');
3157
3275
  }
3158
3276
  }
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
3277
  function leaving(batch, result) {
3177
- if (result.kind === 'expired')
3178
- return { batch, returned: returnedBy('expired') };
3179
3278
  return {
3180
3279
  batch,
3181
3280
  returned: {
@@ -3194,8 +3293,8 @@ export function composeRuntime(options) {
3194
3293
  function settling(acceptance) {
3195
3294
  return acceptance !== null && 'apply' in acceptance;
3196
3295
  }
3197
- function refusalOf(acceptance) {
3198
- return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
3296
+ function failureOf(acceptance) {
3297
+ return settling(acceptance) ? null : acceptance;
3199
3298
  }
3200
3299
  async function acceptance(room, local, submitted, accepted) {
3201
3300
  if (!continuing())
@@ -3208,44 +3307,17 @@ export function composeRuntime(options) {
3208
3307
  logAccepted(room.locator, submitted, accepted);
3209
3308
  let record = current.record;
3210
3309
  let changes;
3211
- if (!options.adapter.reached(room.locator, held, accepted.roomVersion)) {
3310
+ if (!options.adapter.reached(room.locator, held, accepted.roomVersion) &&
3311
+ !older(room.locator, accepted.roomVersion)) {
3212
3312
  const paths = [...returnedPaths(submitted, accepted)];
3213
3313
  const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
3214
- const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
3314
+ const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion, composed(record)));
3215
3315
  record = {
3216
3316
  ...record,
3217
3317
  roomVersion: options.adapter.follows(room.locator, held, accepted.roomVersion)
3218
3318
  ? accepted.roomVersion
3219
3319
  : held,
3220
3320
  };
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
3321
  changes = back;
3250
3322
  }
3251
3323
  const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
@@ -3253,10 +3325,9 @@ export function composeRuntime(options) {
3253
3325
  ...settled,
3254
3326
  apply(answer) {
3255
3327
  const set = settled.apply(answer);
3328
+ showing(room.locator, accepted.roomVersion);
3256
3329
  pendings.delete(pendingKey(room.locator, submitted.id));
3257
3330
  unreadable.delete(room.locator);
3258
- sourceRefusals.delete(room.locator);
3259
- declarationRefusals.delete(room.locator);
3260
3331
  list(room, true);
3261
3332
  return set;
3262
3333
  },
@@ -3274,13 +3345,9 @@ export function composeRuntime(options) {
3274
3345
  const failed = await exclusive(async () => {
3275
3346
  const adopting = await acceptance(room, local, submitted, accepted);
3276
3347
  await adoptedTogether(local, [adopting]);
3277
- return refusalOf(adopting);
3348
+ return failureOf(adopting);
3278
3349
  });
3279
- if (failed === null)
3280
- return true;
3281
- if (failed.kind === 'refused' && continuing())
3282
- refusedSource(room, failed);
3283
- return false;
3350
+ return failed === null;
3284
3351
  }
3285
3352
  async function adoptedMove(room, partner, local, submitted, accepted) {
3286
3353
  const other = discovered.get(partner);
@@ -3294,18 +3361,10 @@ export function composeRuntime(options) {
3294
3361
  ...accepted.destination,
3295
3362
  });
3296
3363
  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);
3364
+ return failureOf(left);
3302
3365
  });
3303
3366
  submitLater(partner);
3304
- if (failed === null)
3305
- return true;
3306
- if (failed.kind === 'refused' && continuing())
3307
- refusedSource(room, failed);
3308
- return false;
3367
+ return failed === null;
3309
3368
  }
3310
3369
  function queueIn(locator) {
3311
3370
  return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
@@ -3416,20 +3475,13 @@ export function composeRuntime(options) {
3416
3475
  return;
3417
3476
  moveTo({ ...status, ...attention() });
3418
3477
  }
3419
- async function discardHeld(head, room, local, signal) {
3420
- const partner = partnerOf(head, room.locator);
3421
- const other = partner === null ? undefined : discovered.get(partner);
3422
- await discardPending(other === undefined ? [room] : [room, other], local, signal);
3423
- }
3424
3478
  async function submitRoom(room, local, authorized, progress, signal) {
3425
3479
  while (continuing() && authorization !== undefined && !signal.aborted) {
3426
3480
  const replica = replicas.get(room.locator);
3427
- const head = replica?.queue[0];
3481
+ const head = replica?.queue.find((batch) => !landedAhead.has(pendingKey(room.locator, batch.id)));
3428
3482
  if (replica === undefined || head === undefined)
3429
3483
  return 'done';
3430
3484
  const state = pendings.get(pendingKey(room.locator, head.id));
3431
- if (state?.accepted !== undefined)
3432
- return 'done';
3433
3485
  const partner = partnerOf(head, room.locator);
3434
3486
  if (partner !== null && !leavingIn(head, room.locator)) {
3435
3487
  submitLater(partner);
@@ -3453,10 +3505,6 @@ export function composeRuntime(options) {
3453
3505
  return 'done';
3454
3506
  }
3455
3507
  const lost = earlier || submission.state === 'submitted';
3456
- if (lost && pastHorizon(head.id)) {
3457
- await discardHeld(head, room, local, signal);
3458
- return 'done';
3459
- }
3460
3508
  let result;
3461
3509
  journal.record(`submission ${room.locator} ${head.id}`);
3462
3510
  await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
@@ -3534,32 +3582,12 @@ export function composeRuntime(options) {
3534
3582
  const other = partner === null ? undefined : discovered.get(partner);
3535
3583
  if (result.kind === 'conflicted' ||
3536
3584
  result.kind === 'rejected' ||
3537
- result.kind === 'expired' ||
3538
3585
  result.kind === 'retryable-refusal') {
3539
- if (result.kind === 'expired' && lost && result.restored !== true) {
3540
- journal.record(`outcome ${room.locator} ${head.id} expired`);
3541
- await discardHeld(head, room, local, signal);
3542
- return 'done';
3543
- }
3544
- if (result.kind === 'expired') {
3545
- await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
3546
- const there = other === undefined ? null : await advance(other, local, authorized, continuing);
3547
- if (other !== undefined && there !== null) {
3548
- await withhold(there, other, local, progress);
3549
- return 'done';
3550
- }
3551
- const withheld = await advance(room, local, authorized, continuing);
3552
- if (withheld !== null) {
3553
- await withhold(withheld, room, local, progress);
3554
- return 'done';
3555
- }
3556
- }
3557
3586
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
3558
3587
  (result.kind === 'conflicted' && result.roomVersion !== undefined
3559
3588
  ? ` ${result.roomVersion}`
3560
3589
  : ''));
3561
- if (result.kind !== 'expired')
3562
- journal.record(`refusal ${result.kind} ${result.code}`);
3590
+ journal.record(`refusal ${result.kind} ${result.code}`);
3563
3591
  }
3564
3592
  if (result.kind === 'retryable-refusal') {
3565
3593
  const reset = result.reset;
@@ -3567,11 +3595,11 @@ export function composeRuntime(options) {
3567
3595
  await exclusive(() => mark(local, room.locator, head, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
3568
3596
  return reset === undefined ? 'again' : { at: Date.parse(reset) };
3569
3597
  }
3570
- if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
3598
+ if (result.kind === 'conflicted' || result.kind === 'rejected') {
3571
3599
  const left = leaving(head, result);
3572
3600
  const refreshed = async () => ({
3573
- there: other === undefined ? null : await advance(other, local, authorized, continuing),
3574
- here: await advance(room, local, authorized, continuing),
3601
+ there: other === undefined ? null : await advanced(other, local, authorized),
3602
+ here: await advanced(room, local, authorized),
3575
3603
  });
3576
3604
  const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
3577
3605
  if (judged !== null && (judged.there !== null || judged.here !== null)) {
@@ -3591,16 +3619,19 @@ export function composeRuntime(options) {
3591
3619
  return;
3592
3620
  }
3593
3621
  const confirmedLayer = await sliceAt(current.record, 'confirmed', [head]);
3594
- const held = (result.kind === 'expired' &&
3595
- (await landedWhole(room.locator, head, confirmedLayer))) ||
3596
- (result.kind === 'conflicted' &&
3597
- (await landedWhole(room.locator, head, confirmedLayer, result.affected)));
3622
+ const held = result.kind === 'conflicted' &&
3623
+ (await landedWhole(room.locator, head, confirmedLayer, result.affected));
3624
+ if (held &&
3625
+ result.kind === 'conflicted' &&
3626
+ partner === null &&
3627
+ !landed(head, confirmedLayer)) {
3628
+ landedAhead.set(pendingKey(room.locator, head.id), Object.fromEntries([...touchedBy([head])].map((path) => [path, confirmedLayer.revisions[path]])));
3629
+ return;
3630
+ }
3598
3631
  publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
3599
3632
  await unpair(local);
3600
3633
  moveTo({ ...status, ...attention() });
3601
3634
  });
3602
- if (result.kind === 'expired')
3603
- continue;
3604
3635
  const { there, here } = judged ?? (await refreshed());
3605
3636
  if (other !== undefined && there !== null)
3606
3637
  await withhold(there, other, local, progress);
@@ -3620,93 +3651,55 @@ export function composeRuntime(options) {
3620
3651
  }
3621
3652
  return 'done';
3622
3653
  }
3623
- function pastHorizon(identity) {
3624
- return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
3625
- }
3626
- async function discardPending(rooms, local, signal) {
3627
- const discarded = await exclusive(async () => {
3628
- const held = rooms
3629
- .filter(({ locator }) => !fences.has(locator))
3630
- .map((room) => ({
3631
- room,
3632
- before: replicas.get(room.locator),
3633
- queue: queueIn(room.locator),
3634
- }));
3635
- if (held.length === 0)
3636
- return [];
3637
- const answer = await local.commit({
3638
- rooms: held.flatMap(({ room, before }) => before === undefined
3639
- ? []
3640
- : [
3641
- {
3642
- locator: room.locator,
3643
- key: before.record.key,
3644
- roots: before.record.typedSelection?.roots ?? [],
3645
- overlay: { set: [], cleared: [...before.overlaid] },
3646
- },
3647
- ]),
3648
- discards: held.map(({ room }) => room.locator),
3649
- });
3650
- for (const { room, before, queue } of held) {
3651
- const { locator } = room;
3652
- lastFenced.set(locator, ++fencings);
3653
- for (const batch of queue)
3654
- pendings.delete(pendingKey(locator, batch.id));
3655
- queuesWithoutReplica.delete(locator);
3656
- if (before !== undefined) {
3657
- replicas.set(locator, {
3658
- ...before,
3659
- queue: [],
3660
- overlaid: new Set(),
3661
- count: answer.counts[locator] ?? before.count,
3662
- });
3663
- }
3664
- journal.record(`room discarded ${locator}`);
3665
- }
3666
- publishChangeSet(mergeChangeSets(held.map(({ room }) => changeSet(room.locator, {
3667
- documents: answer.changed[room.locator] ?? [],
3668
- queue: true,
3669
- }))));
3670
- moveTo({ ...status, ...attention() });
3671
- return held.map(({ room }) => room);
3672
- });
3673
- for (const room of discarded) {
3674
- if (signal.aborted)
3675
- return;
3676
- 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
- });
3683
- if (signal.aborted)
3684
- return;
3685
- await rest(progress);
3686
- }
3687
- }
3688
3654
  function awaitingAnswer(locator) {
3655
+ answering.get(locator)?.release();
3689
3656
  let release = () => { };
3690
- const awaited = new Promise((resolve) => {
3657
+ const answered = new Promise((resolve) => {
3691
3658
  release = resolve;
3692
3659
  });
3693
- answering.set(locator, { generation, answered: awaited });
3660
+ const awaited = { answered, release };
3661
+ answering.set(locator, awaited);
3694
3662
  return () => {
3695
- if (answering.get(locator)?.answered === awaited)
3663
+ if (answering.get(locator) === awaited)
3696
3664
  answering.delete(locator);
3697
3665
  release();
3698
3666
  };
3699
3667
  }
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))))
3668
+ function declaredAsHeld(room) {
3669
+ const held = replicas.get(room.locator)?.record;
3670
+ return (held === undefined ||
3671
+ (JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
3672
+ JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
3673
+ JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
3674
+ }
3675
+ async function keepNaming(local, room) {
3676
+ const held = replicas.get(room.locator)?.record;
3677
+ if (held === undefined || (held.name === room.name && held.handle === room.handle))
3678
+ return;
3679
+ await exclusive(async () => {
3680
+ if (!continuing())
3681
+ return;
3682
+ const replica = replicas.get(room.locator);
3683
+ if (replica === undefined)
3684
+ return;
3685
+ const record = { ...replica.record, name: room.name, handle: room.handle };
3686
+ await local.commit({
3687
+ rooms: [
3688
+ {
3689
+ locator: room.locator,
3690
+ key: record.key,
3691
+ record: storedRecord(record),
3692
+ composed: composedOf(composed(record)),
3693
+ },
3694
+ ],
3695
+ });
3696
+ replicas.set(room.locator, { ...replica, record });
3697
+ });
3698
+ }
3699
+ async function retainModel(local, room, next) {
3700
+ const key = room.locator;
3701
+ const previous = modelOf(room.locator);
3702
+ if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(room))
3710
3703
  return;
3711
3704
  if (next.status === 'unavailable') {
3712
3705
  invalidModels.add(key);
@@ -3718,26 +3711,15 @@ export function composeRuntime(options) {
3718
3711
  const held = replicas.get(room.locator);
3719
3712
  let changed = null;
3720
3713
  if (held !== undefined) {
3721
- const declared = {
3714
+ const record = {
3722
3715
  ...held.record,
3716
+ name: room.name,
3717
+ handle: room.handle,
3723
3718
  operations: room.operations,
3724
3719
  replica: room.replica,
3725
3720
  typedSelection: room.typedSelection ?? null,
3726
- ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
3721
+ model: next,
3727
3722
  };
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
3723
  const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
3742
3724
  const replayedLayer = await replayed(record, confirmedLayer, held.queue);
3743
3725
  const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
@@ -3748,41 +3730,33 @@ export function composeRuntime(options) {
3748
3730
  key: record.key,
3749
3731
  record: storedRecord(record),
3750
3732
  roots: record.typedSelection?.roots ?? [],
3751
- composition: compositionOf(record),
3733
+ composed: composedOf(composed(record)),
3752
3734
  overlay: diff.overlay,
3753
3735
  },
3754
3736
  ],
3755
3737
  });
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 });
3738
+ const { count, indexed } = committedRoom(room.locator, answer);
3739
+ replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
3759
3740
  changed = changeSet(room.locator, {
3760
3741
  documents: answer.changed[room.locator] ?? [],
3761
3742
  room: true,
3762
3743
  });
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
3744
  }
3779
- list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
3745
+ heldModel(room, next);
3780
3746
  if (changed === null)
3781
3747
  emitRooms();
3782
3748
  else
3783
3749
  publishChangeSet(changed);
3784
3750
  });
3785
3751
  }
3752
+ function heldModel(room, next) {
3753
+ models.set(room.locator, next);
3754
+ if (next.status === 'current') {
3755
+ liveModels.set(room.locator, JSON.stringify(next.snapshot));
3756
+ invalidModels.delete(room.locator);
3757
+ }
3758
+ list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
3759
+ }
3786
3760
  async function scoped(result, room, local) {
3787
3761
  if (result.scope === 'content' && !result.retryable) {
3788
3762
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
@@ -3796,14 +3770,14 @@ export function composeRuntime(options) {
3796
3770
  });
3797
3771
  let scopedAway = room;
3798
3772
  if (result.scope === 'model')
3799
- await retainModel(local, room, 'collection', {
3773
+ await retainModel(local, room, {
3800
3774
  status: 'unavailable',
3801
3775
  reason: 'not-readable',
3802
3776
  });
3803
3777
  else if (result.scope !== 'operation') {
3804
3778
  unreadable.add(room.locator);
3805
3779
  scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
3806
- await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
3780
+ await retainModel(local, scopedAway, modelOf(room.locator));
3807
3781
  }
3808
3782
  if (!continuing())
3809
3783
  return;
@@ -3811,6 +3785,17 @@ export function composeRuntime(options) {
3811
3785
  emitRooms();
3812
3786
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
3813
3787
  }
3788
+ function name(locator) {
3789
+ const stream = options.adapter.roomStream;
3790
+ const record = replicas.get(locator)?.record;
3791
+ const room = discovered.get(locator);
3792
+ if (stream === undefined || record === undefined || room === undefined)
3793
+ return;
3794
+ if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
3795
+ return;
3796
+ naming += 1;
3797
+ stream.hold([{ locator, roomVersion: record.roomVersion }]);
3798
+ }
3814
3799
  function heldRooms(listed) {
3815
3800
  return listed.flatMap((room) => {
3816
3801
  const record = replicas.get(room.locator)?.record;
@@ -3828,14 +3813,19 @@ export function composeRuntime(options) {
3828
3813
  return outcome?.status === 'fulfilled' && outcome.value;
3829
3814
  });
3830
3815
  if (attempt === 'opened') {
3816
+ naming += 1;
3831
3817
  options.adapter.roomStream?.hold(heldRooms(ready));
3832
3818
  handed = true;
3833
3819
  }
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');
3820
+ for (const room of ready) {
3821
+ if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
3822
+ if (attempt === 'authorized')
3823
+ name(room.locator);
3824
+ continue;
3825
+ }
3826
+ void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
3827
+ }
3828
+ const failed = prepared.find((room) => room.status === 'rejected');
3839
3829
  if (failed !== undefined)
3840
3830
  throw failed.reason;
3841
3831
  }
@@ -3853,27 +3843,31 @@ export function composeRuntime(options) {
3853
3843
  return false;
3854
3844
  }
3855
3845
  if (prior !== undefined)
3856
- await retainModel(local, room, 'collection', modelOf(room.locator));
3846
+ await retainModel(local, room, modelOf(room.locator));
3847
+ if (prior !== undefined)
3848
+ await keepNaming(local, room);
3857
3849
  if (discoveryPaused || authorization === undefined)
3858
3850
  return false;
3859
3851
  return room.replica.admission === 'admitted';
3860
3852
  }
3861
3853
  async function caughtUp(room, local, progress, live) {
3862
- if (authorization === undefined || !live() || !behind(room))
3854
+ if (options.adapter.roomStream !== undefined || authorization === undefined)
3855
+ return;
3856
+ if (!live() || !behind(room))
3863
3857
  return;
3864
3858
  const withheld = await advance(room, local, authorization, live);
3865
3859
  if (withheld !== null)
3866
3860
  await withhold(withheld, room, local, progress);
3867
3861
  }
3868
3862
  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
- });
3863
+ const model = modelOf(locator);
3864
+ return model.status === 'unavailable' && model.reason === 'revoked';
3873
3865
  }
3874
3866
  async function loadRoom(room, local, progress, live) {
3875
3867
  if (!live() || authorization === undefined)
3876
3868
  return;
3869
+ if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
3870
+ return;
3877
3871
  if (replicas.has(room.locator) && revoked(room.locator)) {
3878
3872
  await reload(room, local, progress, live);
3879
3873
  return;
@@ -3891,21 +3885,53 @@ export function composeRuntime(options) {
3891
3885
  reloadsDue.delete(room.locator);
3892
3886
  if (!live() || authorization === undefined)
3893
3887
  return;
3888
+ const from = standingOf(room.locator);
3894
3889
  const bootstrapped = await pullBootstrap(authorization, room, local, live);
3895
- if (!live())
3896
- return;
3897
3890
  if (bootstrapped.kind !== 'staged') {
3898
- await withhold(bootstrapped, room, local, progress);
3891
+ if (live())
3892
+ await withhold(bootstrapped, room, local, progress);
3899
3893
  return;
3900
3894
  }
3901
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3895
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
3902
3896
  await caughtUp(room, local, progress, live);
3903
3897
  }
3904
3898
  finally {
3905
3899
  caughtUpTo(room.locator);
3906
3900
  }
3907
3901
  }
3908
- async function discoverRooms(local, progress, attempt, live, scope) {
3902
+ async function asked(authorized) {
3903
+ try {
3904
+ return (await options.adapter.discover?.(authorized)) ?? { kind: 'transport-uncertainty' };
3905
+ }
3906
+ catch {
3907
+ return { kind: 'transport-uncertainty' };
3908
+ }
3909
+ }
3910
+ function malformed(listed) {
3911
+ for (const room of listed) {
3912
+ if (room.operations === undefined ||
3913
+ !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
3914
+ room.operations[operation].every((scope) => typeof scope === 'string')) ||
3915
+ room.replica === undefined ||
3916
+ typeof room.replica.selection !== 'string' ||
3917
+ room.replica.selection.length === 0 ||
3918
+ !['admitted', 'unavailable'].includes(room.replica.admission) ||
3919
+ !['locked', 'open'].includes(room.replica.offline)) {
3920
+ return `invalid model or replica declaration ${room.locator}`;
3921
+ }
3922
+ if (!declaredSelection(room))
3923
+ return `invalid typed selection declaration ${room.locator}`;
3924
+ }
3925
+ return null;
3926
+ }
3927
+ async function signIn(local, signedIn) {
3928
+ if (JSON.stringify(signedIn) === JSON.stringify(member))
3929
+ return;
3930
+ await local.fact('member', signedIn);
3931
+ member = signedIn;
3932
+ emitMember();
3933
+ }
3934
+ async function discoverRooms(local, progress, attempt, live, found) {
3909
3935
  if (closed || authorization === undefined) {
3910
3936
  notifyReady();
3911
3937
  return;
@@ -3913,13 +3939,9 @@ export function composeRuntime(options) {
3913
3939
  moveTo(transferring(status));
3914
3940
  discoveries += 1;
3915
3941
  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
- }
3942
+ const result = found ?? (await asked(authorization));
3943
+ if (result.kind === 'discovery')
3944
+ roomsNamed = true;
3923
3945
  if (!live()) {
3924
3946
  progress.complete = false;
3925
3947
  notifyReady();
@@ -3927,12 +3949,7 @@ export function composeRuntime(options) {
3927
3949
  }
3928
3950
  journal.record(`connection discover ${result.kind}`);
3929
3951
  if (result.kind === 'transport-uncertainty') {
3930
- for (const room of rooms.values()) {
3931
- await retainModel(local, room, 'collection', cached(modelOf(room.locator)));
3932
- if (room.typedSelection !== null) {
3933
- await retainModel(local, room, 'typed-selection', cached(modelOf(room.locator, 'typed-selection')));
3934
- }
3935
- }
3952
+ await cachedModels(local);
3936
3953
  progress.reachable = false;
3937
3954
  progress.complete = false;
3938
3955
  notifyReady();
@@ -3953,26 +3970,12 @@ export function composeRuntime(options) {
3953
3970
  notifyReady();
3954
3971
  return;
3955
3972
  }
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
- }
3973
+ const invalid = malformed(result.rooms);
3974
+ if (invalid !== null) {
3975
+ journal.record(invalid);
3976
+ await refuse('missing-required-guarantee');
3977
+ notifyReady();
3978
+ return;
3976
3979
  }
3977
3980
  if (refusal !== null) {
3978
3981
  refusal = null;
@@ -3980,7 +3983,6 @@ export function composeRuntime(options) {
3980
3983
  }
3981
3984
  const previousCapabilities = capabilities;
3982
3985
  capabilities = result.capabilities;
3983
- outcomeRetention = result.retention.outcome;
3984
3986
  notifyReady();
3985
3987
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
3986
3988
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
@@ -3988,13 +3990,8 @@ export function composeRuntime(options) {
3988
3990
  await local.fact('providerLimits', limits);
3989
3991
  await local.fact('capabilities', capabilities);
3990
3992
  }
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);
3993
+ await signIn(local, result.member ?? null);
3994
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
3998
3995
  }
3999
3996
  async function ensureAuthorization() {
4000
3997
  if (authorization !== undefined)
@@ -4009,23 +4006,45 @@ export function composeRuntime(options) {
4009
4006
  }
4010
4007
  }
4011
4008
  async function admittedRead(work) {
4012
- if (roomReads.generation !== generation) {
4013
- roomReads = { generation, free: ROOM_READS, waiting: [] };
4009
+ while (roomReads.free === 0) {
4010
+ await new Promise((admit) => roomReads.waiting.push(admit));
4014
4011
  }
4015
- const reads = roomReads;
4016
- while (reads.free === 0)
4017
- await new Promise((admit) => reads.waiting.push(admit));
4018
- reads.free -= 1;
4012
+ roomReads.free -= 1;
4019
4013
  try {
4020
4014
  await work();
4021
4015
  }
4022
4016
  finally {
4023
- reads.free += 1;
4024
- reads.waiting.shift()?.();
4017
+ roomReads.free += 1;
4018
+ roomReads.waiting.shift()?.();
4025
4019
  }
4026
4020
  }
4021
+ function inRoom(locator, work) {
4022
+ roomWork += 1;
4023
+ const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
4024
+ try {
4025
+ return await work(continuing);
4026
+ }
4027
+ finally {
4028
+ roomWork -= 1;
4029
+ }
4030
+ });
4031
+ const chain = ran.then(() => undefined, () => undefined);
4032
+ roomLanes.set(locator, chain);
4033
+ working.add(chain);
4034
+ void chain.finally(() => {
4035
+ working.delete(chain);
4036
+ if (roomLanes.get(locator) === chain)
4037
+ roomLanes.delete(locator);
4038
+ });
4039
+ return ran;
4040
+ }
4027
4041
  function roomTask(locator, work) {
4028
- return task(roomLane(locator), (live) => admittedRead(() => work(live)));
4042
+ if (stopping)
4043
+ return Promise.resolve();
4044
+ return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
4045
+ }
4046
+ function advanced(room, local, authorized) {
4047
+ return inRoom(room.locator, (live) => advance(room, local, authorized, live));
4029
4048
  }
4030
4049
  function task(lane, work) {
4031
4050
  if (stopping)
@@ -4041,23 +4060,19 @@ export function composeRuntime(options) {
4041
4060
  void run.finally(() => working.delete(run));
4042
4061
  return run;
4043
4062
  }
4044
- function sync(attempt) {
4063
+ function sync(attempt, found) {
4045
4064
  return task(OPENING_LANE, async (live) => {
4046
4065
  if (!live()) {
4047
- notifyReady();
4066
+ readyFromStore();
4048
4067
  return;
4049
4068
  }
4050
4069
  const progress = { complete: true, reachable: true };
4051
- const gate = discover();
4052
- if (attempt === 'opened')
4053
- opened = generation;
4054
- underway = attempt === 'opened' ? { generation, progress } : null;
4070
+ underway = attempt === 'opened' ? progress : null;
4055
4071
  if (attempt === 'opened')
4056
4072
  streamed = progress;
4057
4073
  complete = false;
4058
4074
  recorded = false;
4059
4075
  moveTo(transferring(status));
4060
- handed = false;
4061
4076
  try {
4062
4077
  const local = await openLocal();
4063
4078
  if (!live())
@@ -4065,7 +4080,7 @@ export function composeRuntime(options) {
4065
4080
  await ensureAuthorization();
4066
4081
  if (!live())
4067
4082
  return;
4068
- await discoverRooms(local, progress, attempt, live);
4083
+ await discoverRooms(local, progress, attempt, live, found);
4069
4084
  }
4070
4085
  catch {
4071
4086
  progress.complete = false;
@@ -4073,7 +4088,6 @@ export function composeRuntime(options) {
4073
4088
  progress.reachable = false;
4074
4089
  }
4075
4090
  finally {
4076
- gate.release();
4077
4091
  notifyReady();
4078
4092
  }
4079
4093
  if (!live())
@@ -4085,35 +4099,59 @@ export function composeRuntime(options) {
4085
4099
  if (handed || closed || progress.reachable)
4086
4100
  return;
4087
4101
  underway = null;
4088
- opened = null;
4089
- journal.record('connection reopen');
4090
4102
  await rest(progress, live);
4091
- options.adapter.roomStream?.reopen();
4092
4103
  });
4093
4104
  }
4094
- function rediscover(scope) {
4105
+ function resynchronize() {
4095
4106
  return task(OPENING_LANE, async (live) => {
4096
- if (!live() || !socketOpen)
4107
+ if (!live() || !announced)
4097
4108
  return;
4098
4109
  const progress = { complete: true, reachable: true };
4099
- const gate = discover();
4100
4110
  moveTo(transferring(status));
4101
4111
  try {
4102
4112
  const local = await openLocal();
4103
4113
  if (!live())
4104
4114
  return;
4105
- await ensureAuthorization();
4115
+ await synchronize([...discovered.values()], local, progress, 'unstreamed', live);
4116
+ }
4117
+ catch {
4118
+ progress.complete = false;
4119
+ }
4120
+ if (live())
4121
+ await finishOpening(progress, live);
4122
+ });
4123
+ }
4124
+ function countAuthorityChange() {
4125
+ authorityChanges += 1;
4126
+ for (const listener of authorityChangeListeners)
4127
+ listener(authorityChanges);
4128
+ }
4129
+ function changeAuthority(change) {
4130
+ return task(OPENING_LANE, async (live) => {
4131
+ if (!live() || !announced)
4132
+ return;
4133
+ countAuthorityChange();
4134
+ const progress = { complete: true, reachable: true };
4135
+ moveTo(transferring(status));
4136
+ try {
4137
+ const local = await openLocal();
4106
4138
  if (!live())
4107
4139
  return;
4108
- await discoverRooms(local, progress, 'authorized', live, scope);
4140
+ const invalid = malformed(change.rooms);
4141
+ if (invalid !== null) {
4142
+ journal.record(invalid);
4143
+ await refuse('missing-required-guarantee');
4144
+ return;
4145
+ }
4146
+ if (change.member !== undefined)
4147
+ await signIn(local, change.member);
4148
+ discoveries += 1;
4149
+ const listed = await applyDiscovery(change.rooms, discoveries, change.left);
4150
+ await synchronize(listed, local, progress, 'authorized', live);
4109
4151
  }
4110
4152
  catch {
4111
4153
  progress.complete = false;
4112
4154
  }
4113
- finally {
4114
- gate.release();
4115
- notifyReady();
4116
- }
4117
4155
  if (!live())
4118
4156
  return;
4119
4157
  if (!progress.complete)
@@ -4121,14 +4159,17 @@ export function composeRuntime(options) {
4121
4159
  await rest(progress, live);
4122
4160
  });
4123
4161
  }
4124
- function closedSocket(revoked, lost) {
4125
- const opened = socketOpen;
4162
+ function closedSocket(revoked, lost, authorityChanged) {
4163
+ notifyReady();
4164
+ const opened = announced;
4126
4165
  socketOpen = false;
4166
+ announced = false;
4167
+ missedChange ||= authorityChanged;
4168
+ handed = false;
4169
+ naming = 0;
4127
4170
  connectionLost();
4128
- if (lost) {
4129
- reads.lost();
4171
+ if (lost)
4130
4172
  conversations.lost();
4131
- }
4132
4173
  if (revoked) {
4133
4174
  void refuse('revoked-authorization');
4134
4175
  return;
@@ -4139,18 +4180,32 @@ export function composeRuntime(options) {
4139
4180
  moveTo({
4140
4181
  ...status,
4141
4182
  state: status.state === 'synced' ? 'syncing' : status.state,
4142
- providerUnreachable: status.providerUnreachable || !opened,
4183
+ providerUnreachable: status.providerUnreachable || (!opened && lost),
4184
+ });
4185
+ void closing(opened);
4186
+ }
4187
+ function closing(opened) {
4188
+ return task(OPENING_LANE, async (live) => {
4189
+ if (!live())
4190
+ return;
4191
+ const local = await openLocal();
4192
+ if (!opened && !socketOpen)
4193
+ await cachedModels(local);
4194
+ await rest(null, live);
4143
4195
  });
4144
4196
  }
4145
- function applyOpening(expired) {
4146
- const progress = underway?.generation === generation ? underway.progress : null;
4197
+ function cachedModels(local) {
4198
+ return settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
4199
+ }
4200
+ function caughtUpRooms(expired) {
4201
+ const progress = underway;
4202
+ underway = null;
4203
+ naming = Math.max(0, naming - 1);
4147
4204
  const cycle = progress ?? { complete: true, reachable: true };
4148
- if (opened === generation)
4149
- opened = null;
4205
+ replaced(expired);
4150
4206
  const reloading = progress === null ? [] : owedReloads(expired);
4151
4207
  const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
4152
4208
  return task(OPENING_LANE, async (live) => {
4153
- underway = null;
4154
4209
  if (!live())
4155
4210
  return;
4156
4211
  if (progress === null) {
@@ -4158,24 +4213,14 @@ export function composeRuntime(options) {
4158
4213
  await rest(cycle, live);
4159
4214
  return;
4160
4215
  }
4161
- await roomsCarried([...expired, ...reloading]);
4162
- if (!live())
4163
- return;
4164
4216
  try {
4165
4217
  const local = await openLocal();
4166
- const reloaded = await replacing;
4218
+ await replacing;
4167
4219
  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
- }
4220
+ await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
4221
+ !expired.includes(room.locator) &&
4222
+ !reloading.includes(room.locator)), 'current', local);
4175
4223
  }
4176
- const failed = reloaded.find((outcome) => outcome.status === 'rejected');
4177
- if (failed !== undefined)
4178
- throw failed.reason;
4179
4224
  }
4180
4225
  catch {
4181
4226
  progress.complete = false;
@@ -4187,11 +4232,8 @@ export function composeRuntime(options) {
4187
4232
  }
4188
4233
  function owedReloads(expired) {
4189
4234
  const reloading = [...discovered.values()]
4190
- .filter((room) => replicas.has(room.locator) &&
4191
- !expired.includes(room.locator) &&
4192
- (owed.has(room.locator) || revoked(room.locator)))
4235
+ .filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
4193
4236
  .map((room) => room.locator);
4194
- owed.clear();
4195
4237
  for (const locator of reloading)
4196
4238
  catching.add(locator);
4197
4239
  liveQueries.recover();
@@ -4200,24 +4242,43 @@ export function composeRuntime(options) {
4200
4242
  async function reloadNow(replaced, progress) {
4201
4243
  const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
4202
4244
  for (const room of reloaded)
4203
- reloadsDue.set(room.locator, generation);
4245
+ reloadsDue.add(room.locator);
4204
4246
  try {
4205
4247
  const local = await openLocal();
4206
- return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
4248
+ for (const room of reloaded) {
4249
+ void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
4250
+ }
4207
4251
  }
4208
4252
  catch {
4209
4253
  for (const room of reloaded)
4210
4254
  reloadsDue.delete(room.locator);
4211
4255
  progress.reachable = false;
4212
- return [];
4213
4256
  }
4214
4257
  }
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' });
4258
+ async function settleModels(held, status, local) {
4259
+ const owed = (room) => {
4260
+ const model = modelOf(room.locator);
4261
+ return model.status !== 'unavailable' && model.status !== status;
4262
+ };
4263
+ const settled = await exclusive(async () => {
4264
+ if (!continuing())
4265
+ return [];
4266
+ const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
4267
+ for (const room of unchanged) {
4268
+ const next = { ...modelOf(room.locator), status };
4269
+ const replica = replicas.get(room.locator);
4270
+ if (replica !== undefined)
4271
+ replicas.set(room.locator, { ...replica, record: { ...replica.record, model: next } });
4272
+ heldModel(room, next);
4220
4273
  }
4274
+ if (unchanged.length > 0)
4275
+ emitRooms();
4276
+ return unchanged;
4277
+ });
4278
+ for (const room of held) {
4279
+ if (settled.includes(room) || !owed(room))
4280
+ continue;
4281
+ await retainModel(local, room, { ...modelOf(room.locator), status });
4221
4282
  }
4222
4283
  }
4223
4284
  function submitLater(locator) {
@@ -4262,14 +4323,10 @@ export function composeRuntime(options) {
4262
4323
  try {
4263
4324
  const local = await openLocal();
4264
4325
  await roomTask(locator, async (live) => {
4265
- const current = () => live() && !signal.aborted;
4266
- if (!current() || authorization === undefined)
4326
+ if (!live() || authorization === undefined)
4267
4327
  return;
4268
- const withheld = await Promise.race([
4269
- advance(room, local, authorization, current),
4270
- abandonedOn(signal),
4271
- ]);
4272
- if (withheld !== null && current())
4328
+ const withheld = await advance(room, local, authorization, live);
4329
+ if (withheld !== null && live())
4273
4330
  await withhold(withheld, room, local, progress);
4274
4331
  });
4275
4332
  }
@@ -4324,14 +4381,11 @@ export function composeRuntime(options) {
4324
4381
  progress.complete = false;
4325
4382
  return;
4326
4383
  }
4327
- reads.applied();
4328
4384
  submitAll();
4329
4385
  for (const locator of stale)
4330
4386
  if (repairable.has(locator))
4331
4387
  repairLater(locator, {});
4332
4388
  await conversations.reconcile();
4333
- if (status.providerUnreachable)
4334
- conversations.reconnected();
4335
4389
  const reading = { complete: true, reachable: true };
4336
4390
  await conversations.catchUp(reading);
4337
4391
  if (progress.reachable && reading.reachable)
@@ -4357,7 +4411,7 @@ export function composeRuntime(options) {
4357
4411
  if (!live())
4358
4412
  return;
4359
4413
  let lastCompleted = status.lastCompleted;
4360
- let synced = complete && stale.size === 0 && !pending();
4414
+ let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
4361
4415
  if (synced && !recorded) {
4362
4416
  const completedAt = new Date().toISOString();
4363
4417
  try {
@@ -4372,74 +4426,69 @@ export function composeRuntime(options) {
4372
4426
  }
4373
4427
  }
4374
4428
  moveTo({
4375
- state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
4429
+ state: pending()
4430
+ ? 'pending'
4431
+ : synced && complete && roomWork === 0 && naming === 0
4432
+ ? 'synced'
4433
+ : 'syncing',
4376
4434
  lastCompleted,
4377
- providerUnreachable: !progress.reachable,
4435
+ providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
4378
4436
  ...attention(),
4379
4437
  });
4380
4438
  }
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));
4439
+ function applyRoom(change) {
4440
+ const { locator } = change;
4441
+ if (stopping)
4442
+ return Promise.resolve();
4443
+ return inRoom(locator, async (live) => {
4444
+ await answering.get(locator)?.answered;
4445
+ const progress = underway ?? { complete: true, reachable: true };
4412
4446
  try {
4413
4447
  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
- });
4448
+ for (;;) {
4449
+ if (!live())
4450
+ return null;
4451
+ const room = discovered.get(locator);
4452
+ const held = replicas.get(locator);
4453
+ if (room === undefined ||
4454
+ held === undefined ||
4455
+ room.replica.admission !== 'admitted' ||
4456
+ held.record.replica.selection !== room.replica.selection) {
4457
+ return null;
4458
+ }
4459
+ const at = held.record.roomVersion;
4460
+ if (options.adapter.reached(locator, at, change.roomVersion))
4461
+ return progress;
4462
+ if (!options.adapter.reached(locator, at, change.from)) {
4463
+ name(locator);
4464
+ return progress;
4465
+ }
4466
+ if (underway === null)
4467
+ moveTo(transferring(status));
4468
+ const writing = () => live() && !full;
4469
+ const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, () => change.read()));
4470
+ if (refreshed.kind !== 'advanced')
4471
+ return progress;
4472
+ const landing = {
4473
+ kind: 'refreshed',
4474
+ from: refreshed.from,
4475
+ remodelled: refreshed.remodelled,
4476
+ };
4477
+ if (await publish(local, room, refreshed.record, landing, writing)) {
4478
+ stale.delete(locator);
4479
+ await retainModel(local, room, modelOf(locator));
4480
+ return progress;
4481
+ }
4482
+ }
4421
4483
  }
4422
4484
  catch {
4485
+ if (!stale.has(locator))
4486
+ name(locator);
4423
4487
  stale.add(locator);
4488
+ return progress;
4424
4489
  }
4425
- finally {
4426
- if (during === null)
4427
- await rest(progress, live);
4428
- }
4429
- });
4490
+ }).then((progress) => rest(progress), () => undefined);
4430
4491
  }
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
4492
  function observeStream() {
4444
4493
  const stream = options.adapter.roomStream;
4445
4494
  if (stream === undefined || withdrawStream !== undefined)
@@ -4449,29 +4498,33 @@ export function composeRuntime(options) {
4449
4498
  return;
4450
4499
  if (event.kind === 'opened') {
4451
4500
  socketOpen = true;
4501
+ announced = false;
4502
+ handed = false;
4503
+ naming = 0;
4452
4504
  connectionLive();
4453
4505
  generation += 1;
4454
- reads.opened();
4455
4506
  conversations.reopened();
4456
4507
  recovery.resume();
4457
- void sync('opened');
4508
+ }
4509
+ else if (event.kind === 'discovered') {
4510
+ if (!socketOpen)
4511
+ return;
4512
+ announced = true;
4513
+ if (missedChange) {
4514
+ missedChange = false;
4515
+ countAuthorityChange();
4516
+ }
4517
+ void sync('opened', event.discovery);
4458
4518
  }
4459
4519
  else if (event.kind === 'closed')
4460
- closedSocket(event.revoked, event.lost);
4461
- else if (event.kind === 'opening')
4462
- void applyOpening(event.expired);
4520
+ closedSocket(event.revoked, event.lost, event.authorityChanged);
4521
+ else if (event.kind === 'caught-up')
4522
+ void caughtUpRooms(event.expired);
4463
4523
  else if (event.kind === 'authority-changed')
4464
- void rediscover(event.scope);
4524
+ void changeAuthority(event);
4465
4525
  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]]));
4526
+ void applyRoom(event);
4527
+ });
4475
4528
  }
4476
4529
  function writesHeld() {
4477
4530
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -4541,8 +4594,9 @@ export function composeRuntime(options) {
4541
4594
  if (closed)
4542
4595
  return;
4543
4596
  closed = true;
4544
- if (rechecking !== null)
4545
- clearInterval(rechecking);
4597
+ notifyDiscovered();
4598
+ if (probing !== null)
4599
+ clearInterval(probing);
4546
4600
  connectionLive();
4547
4601
  lockListeners.clear();
4548
4602
  readinessWatches.clear();
@@ -4553,16 +4607,14 @@ export function composeRuntime(options) {
4553
4607
  withdrawServiceMode?.();
4554
4608
  conversations.close();
4555
4609
  void recovery.retire();
4556
- store?.close();
4557
- void opening?.then((opened) => {
4558
- opened.close();
4559
- }, () => { });
4560
4610
  roomListeners.clear();
4611
+ unlistedRooms.clear();
4561
4612
  refusalListeners.clear();
4562
4613
  storageLossListeners.clear();
4563
4614
  statusListeners.clear();
4564
4615
  serviceModeListeners.clear();
4565
4616
  memberListeners.clear();
4617
+ authorityChangeListeners.clear();
4566
4618
  liveQueries.close();
4567
4619
  queueSubscriptions.clear();
4568
4620
  roomDraftListeners.clear();
@@ -4570,6 +4622,7 @@ export function composeRuntime(options) {
4570
4622
  }
4571
4623
  async function stop() {
4572
4624
  stopping = true;
4625
+ inFlight.abort();
4573
4626
  await recovery.retire();
4574
4627
  await Promise.all(working);
4575
4628
  await mutations;
@@ -4583,20 +4636,20 @@ export function composeRuntime(options) {
4583
4636
  observeStream();
4584
4637
  void openLocal().then(() => {
4585
4638
  if (capabilities.length > 0)
4586
- notifyReady();
4639
+ readyFromStore();
4587
4640
  }, () => {
4588
- notifyReady();
4641
+ readyFromStore();
4589
4642
  });
4590
4643
  if (options.adapter.roomStream === undefined)
4591
- void sync('unstreamed');
4644
+ void sync('unstreamed', null);
4592
4645
  function planned(input, reach) {
4593
- const plan = planOf(input, roomFacts(reach), planReach(reach));
4646
+ const plan = planWithin(input, roomFacts(reach), planReach(reach));
4594
4647
  const named = plan.paths !== null &&
4595
4648
  plan.rooms !== null &&
4596
4649
  plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
4597
4650
  return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
4598
4651
  }
4599
- function watchPlan(plan, deliver) {
4652
+ function watchPlan(plan, deliver, fail) {
4600
4653
  let seeded = false;
4601
4654
  let pending = true;
4602
4655
  const shown = () => {
@@ -4622,7 +4675,7 @@ export function composeRuntime(options) {
4622
4675
  }
4623
4676
  seeded = false;
4624
4677
  deliver(message);
4625
- });
4678
+ }, fail);
4626
4679
  return () => {
4627
4680
  watchers.delete(shown);
4628
4681
  if (watchers.size === 0)
@@ -4630,86 +4683,59 @@ export function composeRuntime(options) {
4630
4683
  stop();
4631
4684
  };
4632
4685
  }
4633
- async function searched(input, reach, locked) {
4686
+ async function searched(input, reach) {
4634
4687
  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,
4688
+ await ready;
4689
+ const search = options.adapter.search;
4690
+ if (search === undefined) {
4691
+ return {
4692
+ kind: 'undeclared',
4693
+ code: 'action-undeclared',
4694
+ fields: { name: 'search' },
4695
+ retryable: false,
4696
+ };
4697
+ }
4698
+ if (closed || refusal !== null || connectionOf() !== 'connected')
4699
+ return OFFLINE;
4700
+ const reached = reach === null ? null : new Set(reach.rooms);
4701
+ const named = asked.rooms === null
4702
+ ? reached === null
4703
+ ? null
4704
+ : [...reached]
4705
+ : asked.rooms.filter((room) => reached === null || reached.has(room));
4706
+ const rooms = named === null ? null : named.filter((room) => replicas.has(room)).sort();
4707
+ if (rooms !== null && rooms.length === 0)
4708
+ return { kind: 'found', documents: [] };
4709
+ try {
4710
+ await ensureAuthorization();
4711
+ }
4712
+ catch {
4713
+ return UNCERTAIN;
4714
+ }
4715
+ if (closed || authorization === undefined || refusal !== null)
4716
+ return UNCERTAIN;
4717
+ journal.record('transfer search');
4718
+ try {
4719
+ const result = await search(authorization, { text: asked.text, rooms });
4720
+ if (result.kind === 'revoked-authorization')
4721
+ await refuse('revoked-authorization');
4722
+ journal.record(`outcome search ${result.kind}`);
4723
+ if (result.kind === 'rejected')
4724
+ return result;
4725
+ if (result.kind !== 'found')
4726
+ return failure(result);
4727
+ const kept = rooms === null ? null : new Set(rooms);
4728
+ return {
4729
+ kind: 'found',
4730
+ documents: result.documents.filter((hit) => kept === null || kept.has(hit.room)),
4706
4731
  };
4707
- return complete
4708
- ? { ...hits, coverage }
4709
- : { pending, documents: hits.documents, messages: hits.messages, coverage };
4732
+ }
4733
+ catch {
4734
+ journal.record('outcome search transport-uncertainty');
4735
+ return UNCERTAIN;
4710
4736
  }
4711
4737
  }
4712
- const watch = (topic, input, deliver) => {
4738
+ const watch = (topic, input, deliver, fail) => {
4713
4739
  if (topic === 'room-drafts') {
4714
4740
  const listener = { notify: deliver, ready: false };
4715
4741
  roomDraftListeners.add(listener);
@@ -4758,12 +4784,13 @@ export function composeRuntime(options) {
4758
4784
  };
4759
4785
  }
4760
4786
  if (topic === 'readiness') {
4761
- return watchReadiness({ target: '', code: 'stored', capacity: null, search: 'unused' }, null, deliver);
4787
+ return watchReadiness({ target: '', code: 'stored' }, null, deliver);
4762
4788
  }
4763
4789
  if (topic === 'rooms') {
4764
- roomListeners.add(deliver);
4765
- deliver(listed());
4790
+ unlistedRooms.add(deliver);
4791
+ emitKnownRooms();
4766
4792
  return () => {
4793
+ unlistedRooms.delete(deliver);
4767
4794
  roomListeners.delete(deliver);
4768
4795
  };
4769
4796
  }
@@ -4802,6 +4829,13 @@ export function composeRuntime(options) {
4802
4829
  memberListeners.delete(deliver);
4803
4830
  };
4804
4831
  }
4832
+ if (topic === 'authority-changes') {
4833
+ authorityChangeListeners.add(deliver);
4834
+ deliver(authorityChanges);
4835
+ return () => {
4836
+ authorityChangeListeners.delete(deliver);
4837
+ };
4838
+ }
4805
4839
  if (topic === 'queue') {
4806
4840
  const queue = { locator: input[0], notify: deliver };
4807
4841
  queueSubscriptions.add(queue);
@@ -4810,24 +4844,34 @@ export function composeRuntime(options) {
4810
4844
  queueSubscriptions.delete(queue);
4811
4845
  };
4812
4846
  }
4813
- return watchPlan(planned(input[0], null), deliver);
4847
+ return watchPlan(planned(input[0], null), deliver, fail);
4814
4848
  };
4815
4849
  const md = {
4816
4850
  loaded() {
4817
4851
  return openLocal().then(() => undefined);
4818
4852
  },
4819
4853
  rooms() {
4820
- return listed();
4854
+ if (closed)
4855
+ return Promise.resolve([]);
4856
+ return new Promise((settle) => {
4857
+ const answer = (value) => {
4858
+ unlistedRooms.delete(answer);
4859
+ roomListeners.delete(answer);
4860
+ settle(value);
4861
+ };
4862
+ unlistedRooms.add(answer);
4863
+ emitKnownRooms();
4864
+ });
4821
4865
  },
4822
4866
  query: ((input) => rowsQuery(planned(input, null))),
4823
- search: (input) => searched(input, null, []),
4867
+ search: (input) => searched(input, null),
4824
4868
  types(locator) {
4825
4869
  return {
4826
4870
  collection: modelTypes(modelOf(locator)),
4827
4871
  typedSelection: rooms.get(locator)?.typedSelection === undefined ||
4828
4872
  rooms.get(locator)?.typedSelection === null
4829
4873
  ? null
4830
- : modelTypes(modelOf(locator, 'typed-selection')),
4874
+ : modelTypes(builtIn),
4831
4875
  };
4832
4876
  },
4833
4877
  roomDraft: {
@@ -4844,33 +4888,17 @@ export function composeRuntime(options) {
4844
4888
  const record = roomDrafts.get(room)?.get(path);
4845
4889
  return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
4846
4890
  },
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
- });
4891
+ write(room, path, revision, input, next = roomDraftRevision()) {
4892
+ if (typeof next !== 'string' || next === '')
4893
+ return Promise.reject(runtimeError('room-draft-malformed'));
4894
+ return gathered(`room-draft\u0000${room}\u0000${path}`, { revision, input, next }, (earlier, later) => {
4895
+ if (later.revision !== earlier.next)
4896
+ return null;
4897
+ const parts = gatheredParts(earlier.input, later.input);
4898
+ return parts === null
4899
+ ? null
4900
+ : { revision: earlier.revision, input: parts, next: later.next };
4901
+ }, (written) => writtenRoomDraft(room, path, written.revision, written.input, written.next));
4874
4902
  },
4875
4903
  discard(room, path, revision) {
4876
4904
  return exclusive(async () => {
@@ -4897,13 +4925,13 @@ export function composeRuntime(options) {
4897
4925
  });
4898
4926
  },
4899
4927
  write(app, value) {
4900
- return keepPresentation(app, () => structuredClone(value));
4928
+ return exclusive(() => keptPresentation(app, () => structuredClone(value)));
4901
4929
  },
4902
4930
  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
- ]));
4931
+ return gathered(`presentation\u0000${app}`, structuredClone(members), (earlier, later) => ({ ...earlier, ...later }), (edited) => keptPresentation(app, (held) => Object.fromEntries([
4932
+ ...Object.entries(plain(held) ? held : {}).filter(([member]) => !Object.hasOwn(edited, member)),
4933
+ ...Object.entries(edited).filter(([, value]) => value !== null),
4934
+ ])));
4907
4935
  },
4908
4936
  },
4909
4937
  save(room, named, writtenBy) {
@@ -4922,7 +4950,8 @@ export function composeRuntime(options) {
4922
4950
  part: readPart,
4923
4951
  read: readBinaryPath,
4924
4952
  link: linkBinaryPath,
4925
- readReference: readBinaryReference,
4953
+ show: showBinaryPath,
4954
+ attach: attachBinary,
4926
4955
  },
4927
4956
  async call(name, input) {
4928
4957
  await ready;
@@ -4938,14 +4967,17 @@ export function composeRuntime(options) {
4938
4967
  return journal.export();
4939
4968
  },
4940
4969
  cleanUp,
4970
+ unsent: () => database.ask({ kind: 'authored.unsent' }),
4971
+ reset: () => options.shell.reset(),
4941
4972
  persistence() {
4942
4973
  return persistence === 'granted';
4943
4974
  },
4944
4975
  async retry() {
4945
4976
  journal.record('retry');
4946
- unindexable.clear();
4977
+ unpreparable.clear();
4947
4978
  queueReadiness();
4948
- resume();
4979
+ if (full)
4980
+ await probed();
4949
4981
  },
4950
4982
  observe: observing(watch),
4951
4983
  async reauthorize() {
@@ -4959,7 +4991,7 @@ export function composeRuntime(options) {
4959
4991
  authorization = undefined;
4960
4992
  discoveryPaused = false;
4961
4993
  if (options.adapter.roomStream === undefined)
4962
- await sync('unstreamed');
4994
+ await sync('unstreamed', null);
4963
4995
  else
4964
4996
  await task(OPENING_LANE, ensureAuthorization);
4965
4997
  },
@@ -4967,7 +4999,6 @@ export function composeRuntime(options) {
4967
4999
  const reachable = {
4968
5000
  md,
4969
5001
  watch,
4970
- referrerScope: (referrer) => options.adapter.referrerScope(referrer),
4971
5002
  plan: planned,
4972
5003
  query: (plan) => rowsQuery(plan),
4973
5004
  watchPlan,
@@ -4977,12 +5008,12 @@ export function composeRuntime(options) {
4977
5008
  lockedRooms,
4978
5009
  watchLocks,
4979
5010
  located,
5011
+ room: (locator) => listed().find((one) => one.locator === locator),
4980
5012
  };
4981
5013
  return {
4982
5014
  md,
4983
5015
  seed,
4984
5016
  stop,
4985
- lose: loseRooms,
4986
5017
  watch,
4987
5018
  persisted(answer) {
4988
5019
  if (answer === persistence)
@@ -4991,6 +5022,10 @@ export function composeRuntime(options) {
4991
5022
  journal.record(`persistence ${answer}`);
4992
5023
  queueReadiness();
4993
5024
  },
4994
- within: (reach, port) => narrowed(reachable, reach, port),
5025
+ within: (reach, port) => {
5026
+ if (full)
5027
+ void probed();
5028
+ return narrowed(reachable, reach, port);
5029
+ },
4995
5030
  };
4996
5031
  }