@agentshouse/mdruntime 0.1.0-c56daf494ecb4 → 0.1.0-c59a5a6bd2f31

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 (290) hide show
  1. package/README.md +19 -16
  2. package/dist/acceptance.d.ts +26 -0
  3. package/dist/acceptance.d.ts.map +1 -0
  4. package/dist/acceptance.js +384 -0
  5. package/dist/adapter.d.ts +152 -57
  6. package/dist/adapter.d.ts.map +1 -1
  7. package/dist/attached-model.d.ts +7 -10
  8. package/dist/attached-model.d.ts.map +1 -1
  9. package/dist/attached-model.js +40 -41
  10. package/dist/binary.d.ts +0 -5
  11. package/dist/binary.d.ts.map +1 -1
  12. package/dist/binary.js +1 -1
  13. package/dist/bootstrap.d.ts +31 -11
  14. package/dist/bootstrap.d.ts.map +1 -1
  15. package/dist/bootstrap.js +323 -93
  16. package/dist/chain.d.ts +12 -0
  17. package/dist/chain.d.ts.map +1 -0
  18. package/dist/chain.js +64 -0
  19. package/dist/change-set.d.ts +9 -14
  20. package/dist/change-set.d.ts.map +1 -1
  21. package/dist/change-set.js +11 -63
  22. package/dist/change.d.ts +32 -1
  23. package/dist/change.d.ts.map +1 -1
  24. package/dist/change.js +10 -1
  25. package/dist/chat-types.d.ts +3 -0
  26. package/dist/chat-types.d.ts.map +1 -0
  27. package/dist/chat-types.js +2 -0
  28. package/dist/client.d.ts +38 -16
  29. package/dist/client.d.ts.map +1 -1
  30. package/dist/client.js +101 -12
  31. package/dist/collection.d.ts +11 -7
  32. package/dist/collection.d.ts.map +1 -1
  33. package/dist/collection.js +38 -79
  34. package/dist/compile.d.ts +16 -0
  35. package/dist/compile.d.ts.map +1 -0
  36. package/dist/compile.js +717 -0
  37. package/dist/connection.d.ts +44 -39
  38. package/dist/connection.d.ts.map +1 -1
  39. package/dist/connection.js +2307 -1712
  40. package/dist/context.d.ts +2 -0
  41. package/dist/context.d.ts.map +1 -0
  42. package/dist/{chat/context.js → context.js} +1 -4
  43. package/dist/conversation-row.d.ts +33 -0
  44. package/dist/conversation-row.d.ts.map +1 -0
  45. package/dist/conversation-row.js +47 -0
  46. package/dist/conversation.d.ts +50 -20
  47. package/dist/conversation.d.ts.map +1 -1
  48. package/dist/conversation.js +897 -303
  49. package/dist/digest.d.ts +7 -0
  50. package/dist/digest.d.ts.map +1 -0
  51. package/dist/digest.js +113 -0
  52. package/dist/durability.d.ts +10 -1
  53. package/dist/durability.d.ts.map +1 -1
  54. package/dist/durability.js +5 -0
  55. package/dist/entry.d.ts +7 -4
  56. package/dist/entry.d.ts.map +1 -1
  57. package/dist/entry.js +33 -28
  58. package/dist/figures.d.ts +48 -0
  59. package/dist/figures.d.ts.map +1 -0
  60. package/dist/figures.js +197 -0
  61. package/dist/file.d.ts +2 -19
  62. package/dist/file.d.ts.map +1 -1
  63. package/dist/file.js +7 -122
  64. package/dist/host.d.ts +11 -2
  65. package/dist/host.d.ts.map +1 -1
  66. package/dist/host.js +127 -17
  67. package/dist/index.d.ts +27 -11
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +10 -0
  70. package/dist/install.d.ts +86 -0
  71. package/dist/install.d.ts.map +1 -0
  72. package/dist/install.js +537 -0
  73. package/dist/job.d.ts +33 -0
  74. package/dist/job.d.ts.map +1 -0
  75. package/dist/job.js +54 -0
  76. package/dist/live.d.ts +23 -0
  77. package/dist/live.d.ts.map +1 -0
  78. package/dist/live.js +165 -0
  79. package/dist/log.d.ts +1 -0
  80. package/dist/log.d.ts.map +1 -1
  81. package/dist/media.d.ts +8 -0
  82. package/dist/media.d.ts.map +1 -0
  83. package/dist/media.js +218 -0
  84. package/dist/memory.d.ts +19 -5
  85. package/dist/memory.d.ts.map +1 -1
  86. package/dist/memory.js +253 -94
  87. package/dist/messages.d.ts +52 -0
  88. package/dist/messages.d.ts.map +1 -0
  89. package/dist/messages.js +168 -0
  90. package/dist/model.d.ts +11 -11
  91. package/dist/model.d.ts.map +1 -1
  92. package/dist/model.js +7 -10
  93. package/dist/name.d.ts +4 -0
  94. package/dist/name.d.ts.map +1 -0
  95. package/dist/name.js +122 -0
  96. package/dist/observation.d.ts +22 -4
  97. package/dist/observation.d.ts.map +1 -1
  98. package/dist/observation.js +120 -14
  99. package/dist/opened.d.ts +22 -0
  100. package/dist/opened.d.ts.map +1 -0
  101. package/dist/opened.js +107 -0
  102. package/dist/outcome.d.ts +50 -7
  103. package/dist/outcome.d.ts.map +1 -1
  104. package/dist/outcome.js +43 -0
  105. package/dist/overlay.d.ts +4 -4
  106. package/dist/overlay.d.ts.map +1 -1
  107. package/dist/overlay.js +111 -182
  108. package/dist/{chat/parts.d.ts → parts.d.ts} +11 -9
  109. package/dist/parts.d.ts.map +1 -0
  110. package/dist/{chat/parts.js → parts.js} +45 -46
  111. package/dist/paths.d.ts +1 -0
  112. package/dist/paths.d.ts.map +1 -1
  113. package/dist/paths.js +4 -0
  114. package/dist/query.d.ts +50 -183
  115. package/dist/query.d.ts.map +1 -1
  116. package/dist/query.js +3 -464
  117. package/dist/{chat/questions.d.ts → questions.d.ts} +3 -2
  118. package/dist/questions.d.ts.map +1 -0
  119. package/dist/{chat/questions.js → questions.js} +8 -1
  120. package/dist/reach.d.ts +19 -7
  121. package/dist/reach.d.ts.map +1 -1
  122. package/dist/reach.js +289 -89
  123. package/dist/react.d.ts +11 -6
  124. package/dist/react.d.ts.map +1 -1
  125. package/dist/react.js +7 -17
  126. package/dist/{chat/read.d.ts → read.d.ts} +2 -0
  127. package/dist/read.d.ts.map +1 -0
  128. package/dist/{chat/read.js → read.js} +4 -0
  129. package/dist/readiness.d.ts +50 -0
  130. package/dist/readiness.d.ts.map +1 -0
  131. package/dist/readiness.js +27 -0
  132. package/dist/reads.d.ts +35 -0
  133. package/dist/reads.d.ts.map +1 -0
  134. package/dist/reads.js +128 -0
  135. package/dist/recovery.d.ts +26 -0
  136. package/dist/recovery.d.ts.map +1 -0
  137. package/dist/recovery.js +232 -0
  138. package/dist/refresh.d.ts +17 -14
  139. package/dist/refresh.d.ts.map +1 -1
  140. package/dist/refresh.js +143 -51
  141. package/dist/replica.d.ts +55 -33
  142. package/dist/replica.d.ts.map +1 -1
  143. package/dist/replica.js +332 -154
  144. package/dist/room-draft.d.ts +37 -6
  145. package/dist/room-draft.d.ts.map +1 -1
  146. package/dist/room-draft.js +161 -74
  147. package/dist/room-drafts.d.ts +29 -0
  148. package/dist/room-drafts.d.ts.map +1 -0
  149. package/dist/room-drafts.js +216 -0
  150. package/dist/rows.d.ts +30 -37
  151. package/dist/rows.d.ts.map +1 -1
  152. package/dist/rows.js +59 -126
  153. package/dist/save.d.ts +53 -9
  154. package/dist/save.d.ts.map +1 -1
  155. package/dist/save.js +542 -172
  156. package/dist/saves.d.ts +96 -0
  157. package/dist/saves.d.ts.map +1 -0
  158. package/dist/saves.js +999 -0
  159. package/dist/search.d.ts +15 -55
  160. package/dist/search.d.ts.map +1 -1
  161. package/dist/search.js +10 -121
  162. package/dist/seed.d.ts +30 -0
  163. package/dist/seed.d.ts.map +1 -0
  164. package/dist/seed.js +141 -0
  165. package/dist/shared.d.ts +15 -0
  166. package/dist/shared.d.ts.map +1 -0
  167. package/dist/shared.js +421 -0
  168. package/dist/socket/admission.d.ts +7 -0
  169. package/dist/socket/admission.d.ts.map +1 -0
  170. package/dist/socket/admission.js +39 -0
  171. package/dist/socket/answers.d.ts +6 -0
  172. package/dist/socket/answers.d.ts.map +1 -0
  173. package/dist/socket/answers.js +20 -0
  174. package/dist/socket/reads.d.ts +10 -0
  175. package/dist/socket/reads.d.ts.map +1 -0
  176. package/dist/socket/reads.js +31 -0
  177. package/dist/socket.d.ts +43 -0
  178. package/dist/socket.d.ts.map +1 -0
  179. package/dist/socket.js +96 -0
  180. package/dist/source.d.ts +61 -0
  181. package/dist/source.d.ts.map +1 -0
  182. package/dist/source.js +81 -0
  183. package/dist/status.d.ts +5 -2
  184. package/dist/status.d.ts.map +1 -1
  185. package/dist/status.js +3 -7
  186. package/dist/store/fences.d.ts +16 -0
  187. package/dist/store/fences.d.ts.map +1 -0
  188. package/dist/store/fences.js +26 -0
  189. package/dist/store/installs.d.ts +30 -0
  190. package/dist/store/installs.d.ts.map +1 -0
  191. package/dist/store/installs.js +34 -0
  192. package/dist/store/records.d.ts +212 -0
  193. package/dist/store/records.d.ts.map +1 -0
  194. package/dist/store/records.js +408 -0
  195. package/dist/store/room-held.d.ts +8 -0
  196. package/dist/store/room-held.d.ts.map +1 -0
  197. package/dist/store/room-held.js +17 -0
  198. package/dist/store/watchers.d.ts +15 -0
  199. package/dist/store/watchers.d.ts.map +1 -0
  200. package/dist/store/watchers.js +36 -0
  201. package/dist/store/writes.d.ts +24 -0
  202. package/dist/store/writes.d.ts.map +1 -0
  203. package/dist/store/writes.js +125 -0
  204. package/dist/store.d.ts +77 -120
  205. package/dist/store.d.ts.map +1 -1
  206. package/dist/store.js +112 -529
  207. package/dist/stored.d.ts +156 -0
  208. package/dist/stored.d.ts.map +1 -0
  209. package/dist/stored.js +637 -0
  210. package/dist/tables.d.ts +407 -0
  211. package/dist/tables.d.ts.map +1 -0
  212. package/dist/tables.js +724 -0
  213. package/dist/tallies.d.ts +18 -0
  214. package/dist/tallies.d.ts.map +1 -0
  215. package/dist/tallies.js +129 -0
  216. package/dist/transfer.d.ts +0 -5
  217. package/dist/transfer.d.ts.map +1 -1
  218. package/dist/turn.d.ts +9 -0
  219. package/dist/turn.d.ts.map +1 -0
  220. package/dist/turn.js +75 -0
  221. package/dist/validation.d.ts +29 -0
  222. package/dist/validation.d.ts.map +1 -0
  223. package/dist/validation.js +104 -0
  224. package/dist/windows.d.ts +11 -0
  225. package/dist/windows.d.ts.map +1 -0
  226. package/dist/windows.js +16 -0
  227. package/dist/wire.d.ts +179 -32
  228. package/dist/wire.d.ts.map +1 -1
  229. package/dist/wire.js +61 -31
  230. package/package.json +18 -19
  231. package/AGENTS.md +0 -302
  232. package/dist/chat/acts.d.ts +0 -34
  233. package/dist/chat/acts.d.ts.map +0 -1
  234. package/dist/chat/acts.js +0 -36
  235. package/dist/chat/context.d.ts +0 -3
  236. package/dist/chat/context.d.ts.map +0 -1
  237. package/dist/chat/input.d.ts +0 -9
  238. package/dist/chat/input.d.ts.map +0 -1
  239. package/dist/chat/input.js +0 -99
  240. package/dist/chat/kit.d.ts +0 -60
  241. package/dist/chat/kit.d.ts.map +0 -1
  242. package/dist/chat/kit.js +0 -123
  243. package/dist/chat/lasting.d.ts +0 -6
  244. package/dist/chat/lasting.d.ts.map +0 -1
  245. package/dist/chat/lasting.js +0 -19
  246. package/dist/chat/list.d.ts +0 -26
  247. package/dist/chat/list.d.ts.map +0 -1
  248. package/dist/chat/list.js +0 -62
  249. package/dist/chat/markdown.d.ts +0 -20
  250. package/dist/chat/markdown.d.ts.map +0 -1
  251. package/dist/chat/markdown.js +0 -188
  252. package/dist/chat/message.d.ts +0 -6
  253. package/dist/chat/message.d.ts.map +0 -1
  254. package/dist/chat/message.js +0 -192
  255. package/dist/chat/messages.d.ts +0 -21
  256. package/dist/chat/messages.d.ts.map +0 -1
  257. package/dist/chat/messages.js +0 -151
  258. package/dist/chat/name.d.ts +0 -6
  259. package/dist/chat/name.d.ts.map +0 -1
  260. package/dist/chat/name.js +0 -37
  261. package/dist/chat/opened.d.ts +0 -11
  262. package/dist/chat/opened.d.ts.map +0 -1
  263. package/dist/chat/opened.js +0 -17
  264. package/dist/chat/overlay.d.ts +0 -31
  265. package/dist/chat/overlay.d.ts.map +0 -1
  266. package/dist/chat/overlay.js +0 -97
  267. package/dist/chat/parts.d.ts.map +0 -1
  268. package/dist/chat/process.d.ts +0 -20
  269. package/dist/chat/process.d.ts.map +0 -1
  270. package/dist/chat/process.js +0 -32
  271. package/dist/chat/provider.d.ts +0 -63
  272. package/dist/chat/provider.d.ts.map +0 -1
  273. package/dist/chat/provider.js +0 -645
  274. package/dist/chat/question.d.ts +0 -9
  275. package/dist/chat/question.d.ts.map +0 -1
  276. package/dist/chat/question.js +0 -80
  277. package/dist/chat/questions.d.ts.map +0 -1
  278. package/dist/chat/read.d.ts.map +0 -1
  279. package/dist/chat/ready.d.ts +0 -33
  280. package/dist/chat/ready.d.ts.map +0 -1
  281. package/dist/chat/ready.js +0 -91
  282. package/dist/chat/style.d.ts +0 -4
  283. package/dist/chat/style.d.ts.map +0 -1
  284. package/dist/chat/style.js +0 -302
  285. package/dist/chat.d.ts +0 -14
  286. package/dist/chat.d.ts.map +0 -1
  287. package/dist/chat.js +0 -8
  288. package/dist/read-side.d.ts +0 -18
  289. package/dist/read-side.d.ts.map +0 -1
  290. package/dist/read-side.js +0 -440
@@ -1,30 +1,35 @@
1
- import { attachedModel } from './attached-model.js';
2
- import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
1
+ import { identityOf, roomTarget, target, } from '@agentshouse/mdmodel';
2
+ import { builtInModel, roomTypesOf } from './attached-model.js';
3
+ import { modelTypes, NO_MODEL } from './model.js';
3
4
  import { observing } from './observation.js';
4
- import { filterOf, identityOf, planOf, } from './query.js';
5
- import { createReadSide } from './read-side.js';
6
- import { markdownPath, roomOfKey } from './rows.js';
7
- import { createSearchIndex, searchPlanOf, } from './search.js';
8
- import { bootstrapRoom } from './bootstrap.js';
5
+ import { planWithin, } from './query.js';
6
+ import { createFigures, ROOM_TOTALS } from './figures.js';
7
+ import { createLiveQueries } from './live.js';
8
+ import { keepEntrySeed, seedApplies, seededResult, watchSeeded, } from './seed.js';
9
+ import { documentKey, roomOfKey } from './rows.js';
10
+ import { searchPlanOf } from './search.js';
11
+ import { capped, MAXIMUM_WINDOW } from './windows.js';
12
+ import { behind, catching, caughtUpTo, forgetInstall, installationOf, publish, pullBootstrap, pullRefresh, reachedBy, replaced, roomInFlight, standingAt, writtenAfter, } from './install.js';
9
13
  import { changeSet, mergeChangeSets } from './change-set.js';
10
14
  import { createConversationStore, } from './conversation.js';
11
- import { queuedFile } from './change.js';
12
- import { validRoot } from './collection.js';
13
- import { runtimeError } from './failure.js';
15
+ import { composedOf, validRoot } from './collection.js';
14
16
  import { createLog } from './log.js';
15
- import { binaryRecord } from './binary.js';
16
- import { absentBinary, binaryAnswer, calledResult, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
17
- import { refreshRoom } from './refresh.js';
18
- import { buildBatch, joined, limitedIn, requeued } from './save.js';
19
- import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
20
- import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
21
- import { same, saving, STARTING, transferring, } from './status.js';
22
- import { detectStorageLoss, nextGeneration } from './durability.js';
23
- import { roomDraftRevision } from './identity.js';
24
- import { beneath, normalize } from './paths.js';
25
- import { openRuntimeStore, } from './store.js';
17
+ import { ABSENT_TARGET, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, OFFLINE, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
18
+ import { readAnswers } from './reads.js';
19
+ import { createRecovery } from './recovery.js';
20
+ import { storedRecord } from './replica.js';
21
+ import { same, STARTING, } from './status.js';
22
+ import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
23
+ import { readinessOf, } from './readiness.js';
24
+ import { yielded } from './turn.js';
25
+ import { beneath } from './paths.js';
26
+ import { createStore, } from './store.js';
27
+ import { emitPresentation, presentationSurface, publishRoomDrafts, roomDraftSurface, watchPresentation, watchRoomDrafts, } from './room-drafts.js';
28
+ import { createSocket } from './socket.js';
26
29
  import { unconfirmed } from './transfer.js';
27
30
  import { governs, holds, narrowed, planReach, } from './reach.js';
31
+ import { binaryFileSurface, overlayDiff, preparedAfter, replayed, saveParts, saveRoomDrafts, settle, sliceAt, } from './saves.js';
32
+ import { submitAll, submitLater, unpair } from './acceptance.js';
28
33
  function within(root, path) {
29
34
  return root === '' || beneath(root, path);
30
35
  }
@@ -38,222 +43,448 @@ function declaredSelection(room) {
38
43
  root !== '' &&
39
44
  selection.roots.every((other, at) => at === index || !within(other, root)));
40
45
  }
46
+ const OPENING_LANE = 'opening';
47
+ const CONVERSATION_LANE = 'conversations';
48
+ const ROOM_READS = 256;
49
+ const ROOM_ATTEMPTS = 16;
50
+ const ERASE_BATCH = 512;
51
+ const QUERY_BYTES = 1 << 20;
52
+ // A Room the Provider answers not ready is asked again after these waits, the last repeating, so a chunk build that catches up within seconds installs at once and a long one is asked every 15 seconds.
53
+ const NOT_READY_MS = [1_000, 2_000, 3_000, 5_000, 8_000, 11_000, 15_000];
54
+ // 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.
55
+ const LOCK_GRACE_MS = 120_000;
56
+ function operationOf(input) {
57
+ const operation = input?.operation_id;
58
+ return typeof operation === 'string' ? operation : null;
59
+ }
41
60
  export function composeRuntime(options) {
42
61
  const localSaves = options.adapter.localSaves === true;
62
+ const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
43
63
  let authorization = options.authorization;
44
- let opening;
45
- let store;
46
- const replicas = new Map();
47
- const queuesWithoutReplica = new Map();
48
- const pendings = new Map();
49
- const roomDrafts = new Map();
50
- const presentations = new Map();
51
- let limits = [];
52
- let capabilities = [];
53
- const rooms = new Map();
54
- const counted = new Map();
64
+ let discoveries = 0;
65
+ const usableWhenHeld = options.usableWhenHeld ?? [];
66
+ const build = options.build ?? '';
67
+ const declared = new Map();
68
+ const unheldModels = new Map();
55
69
  const unreadable = new Set();
56
- const models = new Map();
57
- const liveModels = new Map();
58
- const invalidModels = new Set();
70
+ const builtIn = builtInModel(roomTypes);
71
+ const typedPaths = options.adapter.typedPaths ?? [];
59
72
  const discovered = new Map();
73
+ const journal = createLog();
74
+ const database = {
75
+ async ask(request) {
76
+ try {
77
+ return await options.database.ask(request);
78
+ }
79
+ catch (error) {
80
+ if (refusedForSpace(error))
81
+ filled();
82
+ throw error;
83
+ }
84
+ },
85
+ };
86
+ const socket = createSocket(options.adapter);
87
+ const store = createStore({
88
+ database,
89
+ roomTypes,
90
+ builtIn,
91
+ typedPaths,
92
+ opening: open,
93
+ unpublished,
94
+ });
95
+ const { covering, durably, exclusive, composed, publication } = store;
96
+ const openLocal = store.open;
97
+ const isFenced = store.fences.has;
98
+ const storedRooms = store.rooms;
99
+ const recordAt = store.record;
100
+ const workOf = store.work;
101
+ const replicaOf = store.replica;
102
+ const draftsOf = store.drafts;
103
+ const draftOf = store.draft;
104
+ const reads = socket.reads;
60
105
  let roomsListed = false;
61
106
  const scopedRefusals = new Map();
62
- const sourceRefusals = new Map();
63
- const declarationRefusals = new Set();
64
- function modelKey(locator, attached) {
65
- return `${attached} ${locator}`;
66
- }
67
- function modelOf(locator, attached = 'collection') {
68
- return models.get(modelKey(locator, attached)) ?? NO_MODEL;
69
- }
70
- function savedModel(locator, attached) {
71
- return invalidModels.has(modelKey(locator, attached))
72
- ? { status: 'unavailable', reason: 'revoked' }
73
- : modelOf(locator, attached);
107
+ function modelIn(locator, stored) {
108
+ if (refusal?.code === 'revoked-authorization')
109
+ return { status: 'unavailable', reason: 'revoked' };
110
+ return stored?.record.model ?? unheldModels.get(locator) ?? NO_MODEL;
74
111
  }
75
112
  function compositionFor(record) {
76
- return compositionOf({
113
+ return composed({
77
114
  ...record,
78
- model: savedModel(record.locator, 'collection'),
79
- selectionModel: savedModel(record.locator, 'typed-selection'),
115
+ model: modelIn(record.locator, { record, indexed: true, documents: 0 }),
80
116
  });
81
117
  }
82
118
  let refusal = null;
83
119
  let storageLoss = null;
84
- let status = STARTING;
120
+ let status = { state: 'syncing', providerUnreachable: false, resting: false };
85
121
  let shown = STARTING;
86
122
  let discoveryPaused = false;
87
- let resetTimer;
88
123
  let closed = false;
89
124
  let stopping = false;
90
- let withdrawStream;
91
- const announced = new Map();
92
125
  let underway = null;
93
126
  let streamed = null;
94
- let handed = false;
95
- const submitting = new Map();
96
- const answering = new Map();
127
+ const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
128
+ const repairing = new Set();
129
+ const calling = new Set();
130
+ const repairable = new Map();
131
+ const unready = new Map();
97
132
  let complete = false;
98
133
  let recorded = false;
99
134
  const stale = new Set();
100
- let chain = Promise.resolve();
101
- let mutations = Promise.resolve();
135
+ const reloadsDue = new Set();
136
+ const lanes = new Map();
137
+ const roomLanes = new Map();
138
+ let roomWork = 0;
139
+ const roomReads = { free: ROOM_READS, waiting: [] };
140
+ const working = new Set();
102
141
  const roomListeners = new Set();
142
+ const unlistedRooms = new Set();
103
143
  const refusalListeners = new Set();
104
144
  const storageLossListeners = new Set();
105
145
  const statusListeners = new Set();
106
146
  const queueSubscriptions = new Set();
107
- const roomDraftListeners = new Set();
108
- const presentationListeners = new Set();
109
- const readSide = createReadSide();
110
- let withdrawConversations;
111
- let currencyQueued = false;
112
- let socketOpen = false;
147
+ const liveQueries = createLiveQueries((plan, cover) => answeredPlan(plan, cover), () => covering());
148
+ const figures = createFigures({
149
+ cover: figuresCover,
150
+ rooms: async (rooms, since) => database.ask({
151
+ kind: 'replica.tallies',
152
+ totals: { ...(await planned(ROOM_TOTALS, null)), limit: null },
153
+ rooms,
154
+ since,
155
+ }),
156
+ conversations: (conversations, since) => database.ask({ kind: 'conversation.tallies', conversations, since }),
157
+ });
158
+ let currencyQueued = null;
159
+ const confirmed = new Set();
160
+ let lockGrace = null;
161
+ const lockListeners = new Set();
113
162
  let serviceMode = 'open';
114
163
  let haltedRefusal = false;
115
164
  let openSignals = 0;
116
165
  const serviceModeListeners = new Set();
117
- let member = null;
118
166
  const memberListeners = new Set();
119
- const materialized = new Set();
120
- let everyRoom = false;
121
- const transfers = new Map();
167
+ let authorityChanges = 0;
168
+ const authorityChangeListeners = new Set();
169
+ const unpreparedDemand = new Set();
170
+ const unpreparable = new Set();
171
+ let preparing = false;
172
+ let persistence = options.shell.persistence;
173
+ let heldWhenOpened = false;
174
+ let full = false;
175
+ let probing = null;
176
+ let testing = null;
177
+ const readinessWatches = new Set();
178
+ let readinessQueued = false;
122
179
  const conversations = createConversationStore({
123
- agents: declaredAgents,
124
- documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
125
180
  call: (name, input) => invokeAction(name, input),
181
+ history: (request) => readConversationHistory(request),
126
182
  durably,
183
+ local: () => openLocal(),
184
+ again: () => {
185
+ void ready.then(scheduleCurrency);
186
+ },
187
+ settled: () => {
188
+ show();
189
+ },
127
190
  stream: () => options.adapter.conversations,
128
191
  publish: publishChangeSet,
192
+ online: providerReached,
193
+ full: () => full,
129
194
  closed: () => closed,
195
+ unanswered: resendLater,
130
196
  });
131
- const searchIndex = createSearchIndex({
132
- documents: () => readSide.documents(),
133
- document: (room, path) => readSide.document(room, path),
134
- messages: () => conversations.messages(),
135
- message: (conversation, identity) => conversations.message(conversation, identity),
197
+ const lastAnswers = readAnswers({
198
+ database,
199
+ build,
200
+ unkept: options.adapter.unkeptReads ?? [],
201
+ recovery: recovery.scope(),
202
+ call: (name, input) => md.call(name, input),
203
+ online: providerReached,
204
+ closed: () => closed,
205
+ full: () => full,
206
+ filled,
136
207
  });
137
- const journal = createLog();
138
- let notifyReady = () => { };
208
+ let readyFromStore = () => { };
139
209
  const ready = new Promise((resolve) => {
140
- notifyReady = resolve;
210
+ readyFromStore = resolve;
141
211
  });
142
- function rememberRoomDraft(record) {
143
- const room = roomDrafts.get(record.locator) ?? new Map();
144
- room.set(record.path, record);
145
- roomDrafts.set(record.locator, room);
146
- }
147
- function forgetRoomDraft(locator, path) {
148
- const room = roomDrafts.get(locator);
149
- if (room === undefined)
150
- return;
151
- room.delete(path);
152
- if (room.size === 0)
153
- roomDrafts.delete(locator);
154
- }
155
- function roomDraftOf(record) {
156
- return structuredClone({
157
- room: record.locator,
158
- path: record.path,
159
- revision: record.revision,
160
- parts: record.parts,
161
- changedUnderneath: underneath(record.parts, record.path, replicas.get(record.locator)?.visible),
162
- returned: record.returned,
163
- });
164
- }
165
- function snapshotRoomDrafts() {
166
- const records = [...roomDrafts.values()].flatMap((room) => [...room.values()]);
167
- records.sort((left, right) => {
168
- if (left.locator !== right.locator)
169
- return left.locator < right.locator ? -1 : 1;
212
+ let notifyDiscovered = () => { };
213
+ const firstDiscovery = new Promise((resolve) => {
214
+ notifyDiscovered = resolve;
215
+ });
216
+ let roomsNamed = false;
217
+ const seeding = {
218
+ store,
219
+ reached: reads.reached,
220
+ listed: () => roomsListed,
221
+ declares: (room) => declared.has(room),
222
+ };
223
+ const drafting = {
224
+ store,
225
+ closed: () => closed,
226
+ publishChangeSet,
227
+ statusShown: () => moveTo(status),
228
+ };
229
+ const saver = {
230
+ store,
231
+ socket,
232
+ journal,
233
+ localSaves,
234
+ ready,
235
+ firstDiscovery,
236
+ full: () => full,
237
+ closed: () => closed,
238
+ refused: () => refusal !== null,
239
+ roomsNamed: () => roomsNamed,
240
+ authorized: () => authorization,
241
+ ensureAuthorization,
242
+ connectionNow,
243
+ scoped,
244
+ refuse,
245
+ compositionFor,
246
+ prepare,
247
+ publishChangeSet,
248
+ pendingShown,
249
+ submitLater: (locator) => submitLater(accepting, locator),
250
+ };
251
+ const accepting = {
252
+ ...saver,
253
+ recovery,
254
+ stopping: () => stopping,
255
+ paused: () => discoveryPaused,
256
+ continuing,
257
+ discovered: (locator) => discovered.get(locator),
258
+ readable(room) {
259
+ unreadable.delete(room.locator);
260
+ list(room, true);
261
+ },
262
+ unreadable: roomUnreadable,
263
+ advanced,
264
+ withhold,
265
+ writesHeld,
266
+ openSignals: () => openSignals,
267
+ haltedSince,
268
+ rest,
269
+ statusShown: () => moveTo(status),
270
+ };
271
+ const installer = {
272
+ store,
273
+ socket,
274
+ journal,
275
+ roomTypes,
276
+ builtIn,
277
+ typedPaths,
278
+ unheldModel: (locator) => unheldModels.get(locator),
279
+ recover: recoverReads,
280
+ queueReadiness,
281
+ sweep,
282
+ settle: (local, locator, work, record, queue, settling) => settle(saver, local, locator, work, record, queue, settling),
283
+ readied,
284
+ modelKept: (locator) => {
285
+ unheldModels.delete(locator);
286
+ },
287
+ name,
288
+ usable(room) {
289
+ stale.delete(room.locator);
290
+ unpreparable.delete(room.locator);
291
+ if (unreadable.delete(room.locator))
292
+ scopedRefusals.delete(room.locator);
293
+ list(room, true);
294
+ },
295
+ publishChangeSet,
296
+ unpair: (local) => unpair(accepting, local),
297
+ statusShown: () => moveTo(status),
298
+ submitLater: (locator) => submitLater(accepting, locator),
299
+ };
300
+ function notifyReady() {
301
+ notifyDiscovered();
302
+ readyFromStore();
303
+ }
304
+ async function attended() {
305
+ const held = await (await openLocal()).status();
306
+ const attentionRequired = held.returned.flatMap((record) => record.returned === null || isFenced(record.locator)
307
+ ? []
308
+ : [{ room: record.locator, path: record.path, returned: structuredClone(record.returned) }]);
309
+ attentionRequired.sort((left, right) => {
310
+ if (left.room !== right.room)
311
+ return left.room < right.room ? -1 : 1;
170
312
  if (left.path !== right.path)
171
313
  return left.path < right.path ? -1 : 1;
172
314
  return 0;
173
315
  });
174
- return records.map(roomDraftOf);
175
- }
176
- function publishRoomDrafts(waitingOnly) {
177
- if (closed)
178
- return;
179
- const value = snapshotRoomDrafts();
180
- for (const listener of roomDraftListeners) {
181
- if (waitingOnly && listener.ready)
182
- continue;
183
- listener.ready = true;
184
- listener.notify(value);
316
+ const changed = new Map();
317
+ if (!localSaves) {
318
+ for (const change of held.unsent) {
319
+ if (!isFenced(change.room))
320
+ changed.set(`${change.room}\u0000${change.path}`, change);
321
+ }
185
322
  }
323
+ const unsent = [...changed]
324
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
325
+ .map(([, change]) => change);
326
+ return {
327
+ queued: !localSaves && held.queued.some((locator) => !isFenced(locator)),
328
+ lastCompleted: held.lastCompleted,
329
+ attentionRequired,
330
+ unsent,
331
+ reset: held.resets.reduce((earliest, { locator, reset }) => isFenced(locator) || (earliest !== null && earliest <= reset) ? earliest : reset, null),
332
+ };
186
333
  }
187
- function presentationOf(app) {
188
- const value = presentations.get(app);
189
- return value === undefined ? null : structuredClone(value);
334
+ async function queuedWork() {
335
+ const held = await (await openLocal()).queued();
336
+ return !localSaves && held.some((locator) => !isFenced(locator));
190
337
  }
191
- function emitPresentation(app) {
192
- if (closed)
193
- return;
194
- for (const listener of presentationListeners) {
195
- if (listener.app === app && listener.ready)
196
- listener.notify(presentationOf(app));
338
+ function declarationOf(locator, stored) {
339
+ const held = declared.get(locator);
340
+ const room = stored?.record ?? held?.room ?? discovered.get(locator);
341
+ return room === undefined ? undefined : { room, available: held?.available ?? true };
342
+ }
343
+ function listedLocators(held) {
344
+ return [
345
+ ...declared.keys(),
346
+ ...[...held.keys()].filter((locator) => !declared.has(locator)),
347
+ ].filter((locator) => !isFenced(locator));
348
+ }
349
+ function selectionModel() {
350
+ return refusal?.code === 'revoked-authorization'
351
+ ? { status: 'unavailable', reason: 'revoked' }
352
+ : builtIn;
353
+ }
354
+ function listedRoom(locator, stored) {
355
+ const declaration = declarationOf(locator, stored);
356
+ if (declaration === undefined)
357
+ return undefined;
358
+ const { room, available } = declaration;
359
+ const selection = room.typedSelection ?? null;
360
+ return {
361
+ name: room.name,
362
+ locator,
363
+ handle: room.handle,
364
+ operations: room.operations,
365
+ replica: room.replica,
366
+ model: modelTypes(modelIn(locator, stored)),
367
+ typedSelection: selection === null ? null : { roots: selection.roots, model: modelTypes(selectionModel()) },
368
+ providerOwnedPaths: room.providerOwnedPaths,
369
+ roomVersion: stored?.record.roomVersion ?? null,
370
+ synchronizationAvailable: available,
371
+ refusal: scopedRefusals.get(locator) ?? null,
372
+ documents: stored?.documents ?? 0,
373
+ };
374
+ }
375
+ function listingOf(held) {
376
+ const listing = new Map();
377
+ for (const locator of listedLocators(held)) {
378
+ const room = listedRoom(locator, held.get(locator));
379
+ if (room !== undefined)
380
+ listing.set(locator, room);
381
+ }
382
+ return listing;
383
+ }
384
+ async function patchedListing(changed) {
385
+ if (roomsShown === null)
386
+ return null;
387
+ const held = await storedRooms([...changed]);
388
+ const listing = new Map(roomsShown);
389
+ for (const locator of changed) {
390
+ const room = isFenced(locator) ? undefined : listedRoom(locator, held.get(locator));
391
+ if (room === undefined || !listing.has(locator))
392
+ return null;
393
+ listing.set(locator, room);
197
394
  }
395
+ return listing;
198
396
  }
199
- function pending() {
200
- return (!localSaves &&
201
- (queuesWithoutReplica.size > 0 ||
202
- [...replicas.values()].some((replica) => replica.queue.length > 0)));
397
+ async function listingNow() {
398
+ return listingOf(await storedRooms());
203
399
  }
204
- function attention() {
205
- const attentionRequired = [];
206
- for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
207
- if (record.returned === null)
208
- continue;
209
- attentionRequired.push({
210
- room: record.locator,
211
- path: record.path,
212
- returned: structuredClone(record.returned),
213
- });
400
+ function lockedIn(listing, locator) {
401
+ return listing.get(locator)?.replica.offline === 'locked' && !confirmed.has(locator);
402
+ }
403
+ function shownRooms(listing) {
404
+ return structuredClone([...listing.values()].map(({ documents, ...room }) => lockedIn(listing, room.locator)
405
+ ? {
406
+ ...room,
407
+ model: modelTypes(NO_MODEL),
408
+ typedSelection: null,
409
+ documents: 0,
410
+ locked: true,
411
+ }
412
+ : { ...room, documents, locked: false }));
413
+ }
414
+ async function offlineModes() {
415
+ const modes = new Map();
416
+ for (const [locator, { room }] of declared) {
417
+ const declaration = room ?? discovered.get(locator);
418
+ if (declaration !== undefined)
419
+ modes.set(locator, declaration.replica.offline);
214
420
  }
215
- attentionRequired.sort((left, right) => {
216
- if (left.room !== right.room)
217
- return left.room < right.room ? -1 : 1;
218
- if (left.path !== right.path)
219
- return left.path < right.path ? -1 : 1;
220
- return 0;
221
- });
222
- let reset = null;
223
- for (const { submission } of pendings.values()) {
224
- if (submission.state !== 'reset')
225
- continue;
226
- if (reset === null || submission.reset < reset)
227
- reset = submission.reset;
421
+ for (const [locator, { offline }] of (await covering()).rooms)
422
+ modes.set(locator, offline);
423
+ return modes;
424
+ }
425
+ async function locked(locator) {
426
+ if (isFenced(locator) || confirmed.has(locator))
427
+ return false;
428
+ const held = declared.get(locator)?.room ?? discovered.get(locator);
429
+ const mode = (await (await openLocal()).offline(locator)) ?? held?.replica.offline;
430
+ return mode === 'locked';
431
+ }
432
+ async function lockedRooms() {
433
+ return [...(await offlineModes())]
434
+ .filter(([locator, mode]) => mode === 'locked' && !confirmed.has(locator) && !isFenced(locator))
435
+ .map(([locator]) => locator)
436
+ .sort();
437
+ }
438
+ async function relocked(before) {
439
+ const after = await lockedRooms();
440
+ if (JSON.stringify(after) === JSON.stringify(before))
441
+ return;
442
+ journal.record(`locked ${after.length}`);
443
+ emitRooms();
444
+ for (const notify of [...lockListeners])
445
+ notify();
446
+ }
447
+ function confirmLocks(listed) {
448
+ if (!socket.isOpen())
449
+ return;
450
+ for (const room of listed) {
451
+ if (room.replica.admission === 'admitted')
452
+ confirmed.add(room.locator);
228
453
  }
229
- return { attentionRequired, reset };
230
454
  }
231
- function documentsIn(locator) {
232
- const replica = replicas.get(locator);
233
- if (replica === undefined)
234
- return 0;
235
- const roots = replica.confirmed.record.typedSelection?.roots ?? [];
236
- const paths = [
237
- ...Object.keys(replica.visible.documents).filter(markdownPath),
238
- ...Object.keys(replica.visible.binaries),
239
- ];
240
- return paths.filter((path) => !roots.some((root) => beneath(root, path))).length;
241
- }
242
- function listed() {
243
- return structuredClone([...rooms.values()].map((room) => {
244
- const documents = documentsIn(room.locator);
245
- counted.set(room.locator, documents);
246
- return { ...room, documents };
247
- }));
248
- }
249
- function roomFacts(reach = null) {
250
- const held = [...rooms.values()]
455
+ function connectionLost() {
456
+ if (lockGrace !== null || closed)
457
+ return;
458
+ lockGrace = setTimeout(() => {
459
+ lockGrace = null;
460
+ if (socket.isOpen() || closed)
461
+ return;
462
+ void lockedRooms().then((before) => {
463
+ confirmed.clear();
464
+ return relocked(before);
465
+ }, () => { });
466
+ }, LOCK_GRACE_MS);
467
+ }
468
+ function connectionLive() {
469
+ if (lockGrace !== null)
470
+ clearTimeout(lockGrace);
471
+ lockGrace = null;
472
+ }
473
+ function watchLocks(notify) {
474
+ lockListeners.add(notify);
475
+ return () => {
476
+ lockListeners.delete(notify);
477
+ };
478
+ }
479
+ async function roomFacts(reach = null) {
480
+ const held = await storedRooms();
481
+ const rooms = [...listingOf(held).values()]
251
482
  .filter((room) => holds(reach, room.locator))
252
483
  .map((room) => {
253
484
  const governed = governs(reach, room);
254
485
  const attached = [
255
- ...(governed.collection ? [modelOf(room.locator)] : []),
256
- ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
486
+ ...(governed.collection ? [modelIn(room.locator, held.get(room.locator))] : []),
487
+ ...(governed.selection ? [builtIn] : []),
257
488
  ];
258
489
  return {
259
490
  locator: room.locator,
@@ -261,186 +492,658 @@ export function composeRuntime(options) {
261
492
  types: attached.flatMap((model) => model.status === 'unavailable' ? [] : Object.values(model.types)),
262
493
  };
263
494
  });
264
- return { listed: roomsListed, rooms: held };
495
+ return { listed: roomsListed, rooms };
496
+ }
497
+ function emitRooms(changed) {
498
+ recoverReads();
499
+ queueReadiness();
500
+ return changed === undefined ? publishRooms('all') : publishRooms('changed', changed);
265
501
  }
266
- function emitRooms() {
267
- const value = listed();
268
- for (const listener of roomListeners)
269
- listener(value);
502
+ function emitKnownRooms() {
503
+ conversations.learned();
504
+ if (unlistedRooms.size === 0)
505
+ return;
506
+ void publishRooms('known');
507
+ }
508
+ let roomsOwed = null;
509
+ let roomsShown = null;
510
+ function publishRooms(mode, touched = []) {
511
+ if (closed)
512
+ return Promise.resolve();
513
+ if (roomsOwed !== null) {
514
+ roomsOwed.all ||= mode === 'all';
515
+ for (const locator of touched) {
516
+ (mode === 'changed' ? roomsOwed.changed : roomsOwed.counted).add(locator);
517
+ }
518
+ return roomsOwed.shown;
519
+ }
520
+ const owing = {
521
+ all: mode === 'all',
522
+ changed: new Set(mode === 'changed' ? touched : []),
523
+ counted: new Set(mode === 'changed' ? [] : touched),
524
+ shown: Promise.resolve(),
525
+ };
526
+ roomsOwed = owing;
527
+ owing.shown = publication(async () => {
528
+ const owed = owing;
529
+ if (roomsOwed === owing)
530
+ roomsOwed = null;
531
+ if (closed || (roomListeners.size === 0 && unlistedRooms.size === 0))
532
+ return;
533
+ const patched = owed.all || owed.changed.size === 0 || unlistedRooms.size > 0
534
+ ? null
535
+ : await patchedListing(new Set([...owed.changed, ...owed.counted]));
536
+ if (patched === null && owed.changed.size > 0)
537
+ owed.all = true;
538
+ if (patched === null && !owed.all && owed.counted.size > 0) {
539
+ const held = await covering().catch(() => null);
540
+ if (held === null)
541
+ return;
542
+ owed.all = [...owed.counted].some((locator) => held.rooms.has(locator) &&
543
+ roomsShown?.get(locator)?.documents !== held.rooms.get(locator).documents);
544
+ if (owed.all) {
545
+ recoverReads();
546
+ queueReadiness();
547
+ }
548
+ else if (unlistedRooms.size === 0)
549
+ return;
550
+ }
551
+ const listing = patched ?? (await listingNow().catch(() => null));
552
+ if (listing === null || closed)
553
+ return;
554
+ const value = shownRooms(listing);
555
+ const joining = unlistedRooms.size > 0 &&
556
+ (roomsListed || listing.size > 0 || refusal !== null || status.providerUnreachable);
557
+ if (!owed.all && patched === null && !joining)
558
+ return;
559
+ const moved = owed.all ||
560
+ patched !== null ||
561
+ (roomsShown !== null && JSON.stringify(shownRooms(roomsShown)) !== JSON.stringify(value));
562
+ if (moved)
563
+ for (const listener of roomListeners)
564
+ listener(value);
565
+ if (joining) {
566
+ for (const listener of [...unlistedRooms]) {
567
+ unlistedRooms.delete(listener);
568
+ roomListeners.add(listener);
569
+ listener(value);
570
+ }
571
+ }
572
+ roomsShown = roomListeners.size === 0 ? null : listing;
573
+ });
574
+ return owing.shown;
270
575
  }
271
576
  function emitRefusal() {
577
+ queueReadiness();
272
578
  for (const listener of refusalListeners)
273
579
  listener(refusal);
580
+ emitKnownRooms();
274
581
  }
275
582
  function emitStorageLoss() {
583
+ queueReadiness();
276
584
  for (const listener of storageLossListeners)
277
585
  listener(storageLoss);
278
586
  }
279
- function emitMember() {
587
+ function emitMember(member) {
280
588
  for (const listener of memberListeners)
281
589
  listener(member);
282
590
  }
283
591
  function moveTo(next) {
284
592
  if (closed)
285
- return;
593
+ return Promise.resolve();
286
594
  status = next;
287
- show();
595
+ return show();
596
+ }
597
+ function transferring() {
598
+ return { ...status, state: 'syncing', resting: false };
288
599
  }
600
+ let statusOwed = null;
601
+ let statusShown = Promise.resolve();
289
602
  function show() {
290
603
  if (closed)
291
- return;
292
- const next = status.state === 'synced' && !conversations.current()
293
- ? { ...status, state: 'syncing' }
294
- : status;
295
- if (same(shown, next))
296
- return;
297
- shown = next;
298
- for (const listener of statusListeners)
299
- listener(shown);
604
+ return Promise.resolve();
605
+ queueReadiness();
606
+ emitKnownRooms();
607
+ const moved = { moving: status, current: conversations.current() };
608
+ if (statusOwed !== null) {
609
+ statusOwed.push(moved);
610
+ return statusShown;
611
+ }
612
+ const owed = [moved];
613
+ statusOwed = owed;
614
+ statusShown = publication(async () => {
615
+ if (statusOwed === owed)
616
+ statusOwed = null;
617
+ const held = await attended();
618
+ if (closed)
619
+ return;
620
+ for (const { moving, current } of owed) {
621
+ const state = moving.resting && held.queued ? 'pending' : moving.state;
622
+ const next = {
623
+ state: state === 'synced' && !current ? 'syncing' : state,
624
+ lastCompleted: held.lastCompleted,
625
+ providerUnreachable: moving.providerUnreachable,
626
+ attentionRequired: held.attentionRequired,
627
+ unsent: held.unsent,
628
+ reset: held.reset,
629
+ };
630
+ if (same(shown, next))
631
+ continue;
632
+ shown = next;
633
+ for (const listener of statusListeners)
634
+ listener(shown);
635
+ }
636
+ });
637
+ return statusShown;
300
638
  }
301
- function entryOf(batch) {
639
+ function entryOf(states, batch) {
302
640
  if (localSaves)
303
641
  return Object.freeze({ batch, state: 'local' });
304
- const state = pendings.get(batch.id);
305
- if (state?.accepted !== undefined) {
306
- return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
307
- }
642
+ const state = states.get(batch.id);
308
643
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
309
644
  }
310
- function entriesOf(locator) {
311
- return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
645
+ async function entriesOf(locator) {
646
+ if (isFenced(locator))
647
+ return [];
648
+ const { queue, states } = await workOf(locator);
649
+ return queue.map((batch) => entryOf(states, batch));
312
650
  }
313
651
  function emitQueue(locator) {
314
- const entries = entriesOf(locator);
315
- for (const subscription of queueSubscriptions) {
316
- if (subscription.locator === locator)
652
+ const listening = () => [...queueSubscriptions].filter((subscription) => subscription.locator === locator);
653
+ if (closed || listening().length === 0)
654
+ return Promise.resolve();
655
+ return publication(async () => {
656
+ if (closed || listening().length === 0)
657
+ return;
658
+ const entries = await entriesOf(locator);
659
+ if (closed)
660
+ return;
661
+ for (const subscription of listening())
317
662
  subscription.notify(entries);
318
- }
663
+ });
319
664
  }
320
665
  function applyChangeSet(set) {
321
- const held = everyRoom
322
- ? set
323
- : { ...set, documents: set.documents.filter(({ locator }) => materialized.has(locator)) };
324
- readSide.apply(held);
325
- searchIndex.changed(held);
326
- }
327
- function materialize(locators) {
328
- if (everyRoom)
666
+ liveQueries.touched(set);
667
+ figures.touched(set);
668
+ }
669
+ function recoverReads() {
670
+ liveQueries.recover();
671
+ figures.recover();
672
+ }
673
+ function unpublished(error) {
674
+ journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
675
+ }
676
+ async function prepareReplica(locator) {
677
+ for (;;) {
678
+ const held = (await storedRooms([locator])).get(locator);
679
+ if (held === undefined || closed || held.indexed)
680
+ return;
681
+ const key = held.record.key;
682
+ const done = await exclusive(async () => {
683
+ const local = await openLocal();
684
+ return store.rederive(local, key, composed(held.record));
685
+ });
686
+ if (!done.done)
687
+ continue;
688
+ if ((await storedRooms([locator])).get(locator)?.record.key !== key)
689
+ continue;
690
+ publishChangeSet(changeSet(locator, { documents: null }));
691
+ recoverReads();
692
+ queueReadiness();
329
693
  return;
330
- if (locators === null)
331
- everyRoom = true;
332
- const sets = [];
333
- for (const locator of locators ?? [...replicas.keys()]) {
334
- if (materialized.has(locator))
694
+ }
695
+ }
696
+ async function unprepared() {
697
+ const held = (await covering()).rooms;
698
+ return [...unpreparedDemand, ...held.keys()].find((locator) => !(held.get(locator)?.indexed ?? false) && !unpreparable.has(locator));
699
+ }
700
+ function unpreparedBy(locator, error) {
701
+ unpreparable.add(locator);
702
+ journal.record(`preparation failed ${locator} ${error instanceof Error ? error.name : 'unknown'}`);
703
+ queueReadiness();
704
+ }
705
+ function prepare(locators = []) {
706
+ for (const locator of locators)
707
+ unpreparedDemand.add(locator);
708
+ if (preparing)
709
+ return;
710
+ preparing = true;
711
+ void (async () => {
712
+ try {
713
+ for (let waiting = await unprepared(); waiting !== undefined; waiting = await unprepared()) {
714
+ await yielded();
715
+ if (closed)
716
+ return;
717
+ unpreparedDemand.delete(waiting);
718
+ await prepareReplica(waiting).catch((error) => unpreparedBy(waiting, error));
719
+ }
720
+ }
721
+ catch {
722
+ journal.record('preparation failed');
723
+ }
724
+ finally {
725
+ preparing = false;
726
+ }
727
+ })();
728
+ }
729
+ function roomsOfReach(reach) {
730
+ return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
731
+ }
732
+ function everyRoom(held) {
733
+ return [...new Set([...declared.keys(), ...held.rooms.keys(), ...held.queues.keys()])];
734
+ }
735
+ function reachedRooms(plan, held) {
736
+ const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
737
+ const named = plan.rooms ?? (reached === null ? everyRoom(held) : [...reached]);
738
+ return [...new Set(named)]
739
+ .filter((locator) => (reached === null || reached.has(locator)) && !plan.locked.includes(locator))
740
+ .sort();
741
+ }
742
+ function awaited(locator) {
743
+ return (!roomsListed ||
744
+ (declared.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
745
+ }
746
+ function covered(plan, held) {
747
+ return coveredRooms(reachedRooms(plan, held), plan.rooms === null && plan.reach === null, held);
748
+ }
749
+ async function figuresCover() {
750
+ const held = await covering();
751
+ const locked = await lockedRooms();
752
+ const cover = coveredRooms(everyRoom(held)
753
+ .filter((locator) => !locked.includes(locator))
754
+ .sort(), true, held);
755
+ awaitPreparation(cover);
756
+ return 'pending' in cover ? cover : { rooms: cover.rooms };
757
+ }
758
+ function coveredRooms(locators, whole, held) {
759
+ if (locators.length === 0 && whole && !roomsListed)
760
+ return { pending: [] };
761
+ const pending = [];
762
+ const coverage = [];
763
+ const overlay = [];
764
+ const read = [];
765
+ for (const locator of locators) {
766
+ const replica = held.rooms.get(locator);
767
+ if (replica === undefined) {
768
+ if (awaited(locator))
769
+ pending.push({ room: locator, awaits: 'acquisition' });
335
770
  continue;
336
- materialized.add(locator);
337
- const replica = replicas.get(locator);
338
- if (replica !== undefined)
339
- sets.push(changeSet(locator, null, replica));
771
+ }
772
+ if (!replica.indexed) {
773
+ pending.push({ room: locator, awaits: 'preparation' });
774
+ continue;
775
+ }
776
+ coverage.push({
777
+ room: locator,
778
+ generation: replica.transfer,
779
+ current: !roomInFlight(store, locator),
780
+ });
781
+ read.push({ locator, key: replica.key });
782
+ for (const id of held.queues.get(locator) ?? [])
783
+ overlay.push(id);
340
784
  }
341
- if (sets.length === 0)
785
+ if (pending.length > 0)
786
+ return { pending };
787
+ return { coverage: { rooms: coverage, indexes: [], overlay }, rooms: read };
788
+ }
789
+ function awaitPreparation(cover) {
790
+ if (!('pending' in cover))
342
791
  return;
343
- const set = mergeChangeSets(sets);
344
- readSide.apply(set);
345
- searchIndex.changed(set);
346
- }
347
- function reachedRooms(plan) {
348
- if (plan.rooms !== null)
349
- return plan.rooms;
350
- if (plan.reach === null)
351
- return null;
352
- return [...plan.reach.rooms, ...plan.reach.documents.map(roomOfKey)];
792
+ const waiting = cover.pending.filter(({ awaits }) => awaits === 'preparation');
793
+ if (waiting.length > 0)
794
+ prepare(waiting.map(({ room }) => room));
795
+ }
796
+ function requiredRooms(given, held) {
797
+ const reach = planReach(given);
798
+ const listed = new Set(listedLocators(held));
799
+ if (reach === null)
800
+ return { listed: roomsListed, rooms: [...listed].sort() };
801
+ const named = [...roomsOfReach(reach)].sort();
802
+ if (roomsListed)
803
+ return { listed: true, rooms: named.filter((room) => listed.has(room)) };
804
+ return { listed: named.every((room) => held.has(room)), rooms: named };
805
+ }
806
+ function roomState(locator, held) {
807
+ const listedRoom = discovered.get(locator);
808
+ if (roomsListed &&
809
+ (listedRoom === undefined ||
810
+ listedRoom.replica.admission !== 'admitted' ||
811
+ unreadable.has(locator)))
812
+ return 'unadmitted';
813
+ const stored = held.get(locator);
814
+ if (stored === undefined)
815
+ return 'acquiring';
816
+ if (!stored.indexed)
817
+ return 'preparing';
818
+ return 'complete';
819
+ }
820
+ function conversationCoverage(reach) {
821
+ const declaredConversations = reach === null || reach.conversations;
822
+ if (!declaredConversations || options.adapter.conversations === undefined)
823
+ return 'undeclared';
824
+ if (!conversations.known() || !conversations.current())
825
+ return 'acquiring';
826
+ return 'complete';
827
+ }
828
+ function connectionOf(lastCompleted) {
829
+ if (socket.isOpen())
830
+ return 'connected';
831
+ if (status.providerUnreachable)
832
+ return 'unreachable';
833
+ return options.adapter.roomStream === undefined && lastCompleted !== null
834
+ ? 'connected'
835
+ : 'connecting';
836
+ }
837
+ async function connectionNow() {
838
+ if (socket.isOpen() || status.providerUnreachable || options.adapter.roomStream !== undefined) {
839
+ return connectionOf(null);
840
+ }
841
+ return connectionOf((await (await openLocal()).facts()).lastCompleted);
842
+ }
843
+ async function readinessHeld() {
844
+ const local = await openLocal();
845
+ const [held, facts] = await Promise.all([covering(), local.facts()]);
846
+ return { held: held.rooms, facts };
847
+ }
848
+ function readinessFor(port, reach, { held, facts }) {
849
+ const required = requiredRooms(reach, held);
850
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room, held) }));
851
+ const preparing = coverage
852
+ .filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
853
+ .map(({ room }) => room);
854
+ if (preparing.length > 0)
855
+ prepare(preparing);
856
+ return readinessOf({
857
+ code: port.code,
858
+ coverage: {
859
+ listed: required.listed,
860
+ rooms: coverage,
861
+ conversations: conversationCoverage(reach),
862
+ },
863
+ persistence,
864
+ connection: connectionOf(facts.lastCompleted),
865
+ house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
866
+ space: full ? 'full' : null,
867
+ installation: installationOf(store, coverage, held),
868
+ lost: storageLoss !== null,
869
+ established: facts.targets[port.target]?.established === true ||
870
+ (heldWhenOpened && usableWhenHeld.includes(port.target)),
871
+ });
872
+ }
873
+ const establishing = new Set();
874
+ function establish(name) {
875
+ if (establishing.has(name))
876
+ return;
877
+ establishing.add(name);
878
+ void durably(async (local) => {
879
+ const { targets } = await local.facts();
880
+ await local.fact('targets', { ...targets, [name]: { established: true } });
881
+ })
882
+ .then(() => {
883
+ queueReadiness();
884
+ void probeEachMinute();
885
+ }, () => {
886
+ journal.record(`target record failed ${name}`);
887
+ })
888
+ .finally(() => establishing.delete(name));
889
+ }
890
+ function deliverReadiness(watch, held) {
891
+ if (!readinessWatches.has(watch))
892
+ return;
893
+ const readiness = readinessFor(watch.port, watch.reach, held);
894
+ if (readiness.ready && held.facts.targets[watch.port.target]?.established !== true) {
895
+ establish(watch.port.target);
896
+ }
897
+ const shownReadiness = JSON.stringify(readiness);
898
+ if (shownReadiness === watch.last)
899
+ return;
900
+ watch.last = shownReadiness;
901
+ watch.deliver(structuredClone(readiness));
902
+ }
903
+ let readinessShown = Promise.resolve();
904
+ const readinessOwed = new Set();
905
+ function deliverAll(watches) {
906
+ const queued = readinessOwed.size > 0;
907
+ for (const watch of watches)
908
+ readinessOwed.add(watch);
909
+ if (queued)
910
+ return;
911
+ readinessShown = readinessShown
912
+ .then(async () => {
913
+ const owed = [...readinessOwed];
914
+ readinessOwed.clear();
915
+ const held = await readinessHeld();
916
+ if (closed)
917
+ return;
918
+ for (const watch of owed)
919
+ deliverReadiness(watch, held);
920
+ })
921
+ .catch(unpublished);
353
922
  }
354
- function rowsQuery(plan) {
355
- materialize(reachedRooms(plan));
356
- return readSide.query(plan);
923
+ function queueReadiness() {
924
+ if (readinessQueued || closed || readinessWatches.size === 0)
925
+ return;
926
+ readinessQueued = true;
927
+ queueMicrotask(() => {
928
+ readinessQueued = false;
929
+ if (closed)
930
+ return;
931
+ deliverAll([...readinessWatches]);
932
+ });
933
+ }
934
+ function watchReadiness(port, reach, deliver) {
935
+ const watch = { port, reach, deliver, last: null };
936
+ readinessWatches.add(watch);
937
+ void openLocal().then(() => deliverAll([watch]), () => deliverAll([watch]));
938
+ return () => {
939
+ readinessWatches.delete(watch);
940
+ };
941
+ }
942
+ function providerReached() {
943
+ return (socket.isOpen() || !status.providerUnreachable) && refusal === null;
944
+ }
945
+ function filled() {
946
+ if (full || closed)
947
+ return;
948
+ full = true;
949
+ journal.record('space full');
950
+ queueReadiness();
951
+ void probeEachMinute();
952
+ }
953
+ async function installedTarget() {
954
+ if (heldWhenOpened && usableWhenHeld.length > 0)
955
+ return true;
956
+ const { targets } = await (await openLocal()).facts();
957
+ return Object.values(targets).some((target) => target.established);
958
+ }
959
+ async function probeEachMinute() {
960
+ if (!full || probing !== null || closed)
961
+ return;
962
+ if (!(await installedTarget().catch(() => false)))
963
+ return;
964
+ if (!full || probing !== null || closed)
965
+ return;
966
+ // 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.
967
+ probing = setInterval(() => {
968
+ void probed();
969
+ }, 60_000);
970
+ }
971
+ function probed() {
972
+ testing ??= database.ask({ kind: 'space.probe' }).then(() => {
973
+ testing = null;
974
+ spaceFreed();
975
+ return true;
976
+ }, () => {
977
+ testing = null;
978
+ return false;
979
+ });
980
+ return testing;
981
+ }
982
+ function spaceFreed() {
983
+ if (!full)
984
+ return;
985
+ full = false;
986
+ if (probing !== null)
987
+ clearInterval(probing);
988
+ probing = null;
989
+ unpreparable.clear();
990
+ journal.record('space freed');
991
+ queueReadiness();
992
+ if (closed || authorization === undefined)
993
+ return;
994
+ if (options.adapter.roomStream === undefined)
995
+ void sync('unstreamed', null);
996
+ else
997
+ void resynchronize();
998
+ }
999
+ function sameCover(left, right) {
1000
+ return JSON.stringify(left) === JSON.stringify(right);
1001
+ }
1002
+ async function coveredRows(plan, cover) {
1003
+ for (;;) {
1004
+ const before = await cover();
1005
+ if ('pending' in before)
1006
+ return { pending: before };
1007
+ const local = await openLocal();
1008
+ const keys = before.rooms.map(({ key }) => key);
1009
+ const changes = local.changes(keys);
1010
+ const current = () => local.changes(keys) === changes;
1011
+ const rows = await windowRows(local, plan, before.rooms, current);
1012
+ const after = await cover();
1013
+ if (rows !== null && sameCover(before, after))
1014
+ return { rows, cover: before, current };
1015
+ }
1016
+ }
1017
+ async function windowRows(local, plan, rooms, unchanged) {
1018
+ const first = await local.query(plan, rooms, QUERY_BYTES);
1019
+ if (first.more === undefined || !('documents' in first)) {
1020
+ const { more: _more, ...rows } = first;
1021
+ return rows;
1022
+ }
1023
+ const documents = [...first.documents];
1024
+ for (let more = first.more; more !== undefined;) {
1025
+ if (!unchanged())
1026
+ return null;
1027
+ const page = await local.query({
1028
+ ...plan,
1029
+ after: { text: more, values: JSON.parse(more) },
1030
+ limit: plan.limit === null ? null : plan.limit - documents.length,
1031
+ total: false,
1032
+ }, rooms, QUERY_BYTES);
1033
+ if (!('documents' in page))
1034
+ return null;
1035
+ documents.push(...page.documents);
1036
+ more = page.more;
1037
+ if (more === undefined) {
1038
+ return {
1039
+ documents,
1040
+ ...(page.next === undefined ? {} : { next: page.next }),
1041
+ ...(first.total === undefined ? {} : { total: first.total }),
1042
+ };
1043
+ }
1044
+ }
1045
+ return null;
1046
+ }
1047
+ function keyedOf(rows, coverage) {
1048
+ if ('groups' in rows) {
1049
+ return {
1050
+ result: { ...rows, coverage },
1051
+ keys: rows.groups.map((group) => JSON.stringify(group.key)),
1052
+ };
1053
+ }
1054
+ return {
1055
+ result: { ...rows, coverage },
1056
+ keys: rows.documents.map((document) => documentKey(document.room, document.path)),
1057
+ };
1058
+ }
1059
+ async function answeredPlan(plan, cover) {
1060
+ const answered = await coveredRows(plan, cover);
1061
+ if ('pending' in answered) {
1062
+ const pending = answered.pending;
1063
+ return { result: pending, keys: [], current: () => true };
1064
+ }
1065
+ const held = answered.cover;
1066
+ return { ...keyedOf(answered.rows, held.coverage), current: answered.current };
1067
+ }
1068
+ async function coverNow(plan) {
1069
+ const cover = covered(plan, await covering());
1070
+ awaitPreparation(cover);
1071
+ return cover;
1072
+ }
1073
+ async function rowsQuery(plan) {
1074
+ const held = await covering();
1075
+ const before = covered(plan, held);
1076
+ awaitPreparation(before);
1077
+ const seeded = seedApplies(seeding, plan, held);
1078
+ if (seeded !== null)
1079
+ return seededResult(seeded).result;
1080
+ if ('pending' in before)
1081
+ return before;
1082
+ const answered = await coveredRows(plan, () => coverNow(plan));
1083
+ if ('pending' in answered)
1084
+ return answered.pending;
1085
+ const shownCover = answered.cover;
1086
+ return { ...answered.rows, coverage: shownCover.coverage };
357
1087
  }
358
1088
  function publishChangeSet(set) {
359
1089
  applyChangeSet(set);
360
- if (set.rooms.length > 0 ||
361
- [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
362
- emitRooms();
1090
+ for (const { locator, path } of set.documents) {
1091
+ if (path !== null)
1092
+ store.attached.drop(locator, path);
1093
+ }
1094
+ const shownNow = [];
1095
+ if (set.queues.length > 0 || set.drafts.length > 0)
1096
+ shownNow.push(show());
1097
+ if (set.rooms.length > 0)
1098
+ shownNow.push(emitRooms(set.rooms));
1099
+ else if (set.documents.length > 0) {
1100
+ shownNow.push(publishRooms('counted', [...new Set(set.documents.map(({ locator }) => locator))]));
1101
+ }
363
1102
  for (const locator of set.queues)
364
- emitQueue(locator);
365
- if (set.drafts.length > 0 ||
366
- set.documents.some((change) => roomDrafts.get(change.locator)?.has(change.path) === true)) {
367
- publishRoomDrafts(false);
1103
+ shownNow.push(emitQueue(locator));
1104
+ if (set.drafts.length > 0)
1105
+ shownNow.push(publishRoomDrafts(drafting, false));
1106
+ else if (set.documents.length > 0) {
1107
+ shownNow.push(publishRoomDrafts(drafting, false, set.documents));
368
1108
  }
369
1109
  for (const app of set.presentations)
370
- emitPresentation(app);
1110
+ shownNow.push(emitPresentation(drafting, app));
371
1111
  conversations.changed(set);
372
- }
373
- function stopTimer() {
374
- if (resetTimer === undefined)
375
- return;
376
- clearTimeout(resetTimer);
377
- resetTimer = undefined;
378
- }
379
- function armTimer() {
380
- stopTimer();
381
- if (closed || stopping)
382
- return;
383
- let earliest = null;
384
- for (const { submission } of pendings.values()) {
385
- if (submission.state !== 'reset')
386
- continue;
387
- const at = Date.parse(submission.reset);
388
- if (earliest === null || at < earliest)
389
- earliest = at;
390
- }
391
- if (earliest === null)
392
- return;
393
- // A Save House held back goes again only once the reset its answer named has passed.
394
- resetTimer = setTimeout(() => {
395
- resetTimer = undefined;
396
- submitAll();
397
- }, Math.max(0, earliest - Date.now()));
1112
+ return Promise.all(shownNow).then(() => undefined);
398
1113
  }
399
1114
  async function refuse(reason, provider = null) {
400
- stopTimer();
401
1115
  discoveryPaused = true;
1116
+ socket.abortReads();
402
1117
  try {
403
1118
  if (reason === 'revoked-authorization') {
404
1119
  await exclusive(async () => {
405
1120
  const revoked = { status: 'unavailable', reason: 'revoked' };
1121
+ let held = new Map();
406
1122
  try {
407
1123
  const local = await openLocal();
408
- await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
1124
+ held = await storedRooms();
1125
+ await local.commit({
1126
+ rooms: [...held.values()].map(({ record: kept }) => {
1127
+ const record = { ...kept, model: revoked };
1128
+ return {
1129
+ locator: record.locator,
1130
+ key: record.key,
1131
+ record: storedRecord(record),
1132
+ composed: composedOf(composed(record)),
1133
+ };
1134
+ }),
1135
+ });
409
1136
  }
410
1137
  catch {
411
1138
  journal.record('model revocation persistence failed');
412
1139
  }
413
- const keys = new Set(models.keys());
414
- for (const locator of rooms.keys()) {
415
- keys.add(modelKey(locator, 'collection'));
416
- keys.add(modelKey(locator, 'typed-selection'));
417
- }
418
- for (const key of keys) {
419
- invalidModels.add(key);
420
- models.set(key, revoked);
421
- liveModels.delete(key);
1140
+ const listedNow = listedLocators(held);
1141
+ for (const locator of listedNow) {
1142
+ if (!held.has(locator))
1143
+ unheldModels.set(locator, revoked);
422
1144
  }
423
- const changed = [];
424
- for (const [locator, room] of rooms) {
425
- const before = replicas.get(locator) ?? null;
426
- if (before !== null) {
427
- const record = {
428
- ...before.confirmed.record,
429
- model: revoked,
430
- selectionModel: revoked,
431
- };
432
- replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
433
- }
434
- rooms.set(locator, {
435
- ...room,
436
- model: modelTypes(revoked),
437
- typedSelection: room.typedSelection === null
438
- ? null
439
- : { ...room.typedSelection, model: modelTypes(revoked) },
440
- });
441
- changed.push(changeSet(locator, before, replicas.get(locator) ?? null, { room: true }));
442
- }
443
- publishChangeSet(mergeChangeSets(changed));
1145
+ publishChangeSet(mergeChangeSets(listedNow.map((locator) => changeSet(locator, { documents: null, room: true }))));
1146
+ prepare();
444
1147
  });
445
1148
  }
446
1149
  }
@@ -456,196 +1159,162 @@ export function composeRuntime(options) {
456
1159
  }
457
1160
  }
458
1161
  function list(room, synchronizationAvailable) {
459
- const selection = room.typedSelection ?? null;
460
- rooms.set(room.locator, {
461
- name: room.name,
462
- locator: room.locator,
463
- handle: room.handle,
464
- operations: room.operations,
465
- replica: room.replica,
466
- model: modelTypes(modelOf(room.locator)),
467
- typedSelection: selection === null
468
- ? null
469
- : {
470
- roots: selection.roots,
471
- model: modelTypes(modelOf(room.locator, 'typed-selection')),
472
- },
473
- providerOwnedPaths: room.providerOwnedPaths,
474
- roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
475
- synchronizationAvailable,
476
- refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
1162
+ if (isFenced(room.locator))
1163
+ return;
1164
+ declared.set(room.locator, {
1165
+ room: {
1166
+ name: room.name,
1167
+ locator: room.locator,
1168
+ handle: room.handle,
1169
+ operations: room.operations,
1170
+ replica: room.replica,
1171
+ typedSelection: room.typedSelection ?? null,
1172
+ providerOwnedPaths: room.providerOwnedPaths,
1173
+ },
1174
+ available: synchronizationAvailable,
477
1175
  });
478
1176
  }
479
- async function applyDiscovery(listed, local) {
480
- const listedLocators = new Set(listed.map((room) => room.locator));
481
- for (const locator of listedLocators)
1177
+ function listHeld(locator) {
1178
+ if (isFenced(locator))
1179
+ return;
1180
+ declared.set(locator, { room: null, available: false });
1181
+ }
1182
+ async function applyDiscovery(found, started, left) {
1183
+ const lockedBefore = await lockedRooms();
1184
+ const local = await openLocal();
1185
+ const stillFenced = new Set(await local.fences());
1186
+ const relisting = found
1187
+ .map(({ locator }) => locator)
1188
+ .filter((locator) => {
1189
+ const fence = store.fences.of(locator);
1190
+ return stillFenced.has(locator) && (fence === undefined || fence.at < started);
1191
+ });
1192
+ const relisted = relisting.length === 0 ? new Set() : await durably((held) => held.unfence(relisting));
1193
+ for (const { locator } of found) {
1194
+ const fence = store.fences.of(locator);
1195
+ if (fence === undefined || fence.at >= started)
1196
+ continue;
1197
+ if (relisted.has(locator) || (!stillFenced.has(locator) && !fence.written)) {
1198
+ store.fences.lift(locator);
1199
+ }
1200
+ }
1201
+ const kept = await local.held();
1202
+ await loseRooms(found
1203
+ .filter((room) => room.replica.admission !== 'admitted' &&
1204
+ (kept.held.has(room.locator) || store.installs.of(room.locator).transfer !== null))
1205
+ .map((room) => room.locator));
1206
+ const listed = found.filter(({ locator }) => !isFenced(locator) && (!stillFenced.has(locator) || relisted.has(locator)));
1207
+ const listedLocatorsNow = new Set(listed.map((room) => room.locator));
1208
+ for (const locator of listedLocatorsNow)
482
1209
  scopedRefusals.delete(locator);
483
- discovered.clear();
1210
+ if (left === undefined)
1211
+ discovered.clear();
484
1212
  for (const room of listed)
485
1213
  discovered.set(room.locator, room);
486
- await dropUnlisted(listedLocators, local);
1214
+ const named = new Set(found.map((room) => room.locator));
1215
+ await loseRooms(left ??
1216
+ [...new Set([...declared.keys(), ...kept.held, ...store.installs.transferring()])].filter((locator) => !named.has(locator)));
1217
+ const held = await storedRooms(listed.map((room) => room.locator));
487
1218
  for (const room of listed) {
488
- const held = replicas.get(room.locator)?.confirmed.record;
489
- if (held !== undefined && held.replica.selection !== room.replica.selection) {
490
- list(held, false);
1219
+ const record = held.get(room.locator)?.record;
1220
+ if (record !== undefined && record.replica.selection !== room.replica.selection) {
1221
+ listHeld(room.locator);
491
1222
  continue;
492
1223
  }
493
1224
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
494
1225
  }
1226
+ confirmLocks(listed);
495
1227
  roomsListed = true;
496
1228
  emitRooms();
497
- }
498
- function unlistedHeld(listed) {
499
- return [
500
- ...new Set([
501
- ...rooms.keys(),
502
- ...roomDrafts.keys(),
503
- ...transfers.keys(),
504
- ...queuesWithoutReplica.keys(),
505
- ]),
506
- ].filter((locator) => !listed.has(locator));
507
- }
508
- async function dropUnlisted(listed, local) {
509
- if (unlistedHeld(listed).length === 0)
1229
+ await relocked(lockedBefore);
1230
+ return listed;
1231
+ }
1232
+ function forget(locator) {
1233
+ confirmed.delete(locator);
1234
+ forgetInstall(store, locator);
1235
+ declared.delete(locator);
1236
+ discovered.delete(locator);
1237
+ unreadable.delete(locator);
1238
+ stale.delete(locator);
1239
+ repairable.delete(locator);
1240
+ unpreparable.delete(locator);
1241
+ scopedRefusals.delete(locator);
1242
+ unheldModels.delete(locator);
1243
+ store.forget(locator);
1244
+ }
1245
+ async function eraseFenced(locators) {
1246
+ for (const locator of locators) {
1247
+ while (await durably((local) => local.erase(locator, ERASE_BATCH)))
1248
+ await yielded();
1249
+ }
1250
+ }
1251
+ let sweeping = false;
1252
+ function sweep() {
1253
+ if (sweeping || closed)
510
1254
  return;
511
- await exclusive(async () => {
512
- const unlisted = unlistedHeld(listed);
513
- await local.dropRooms(unlisted);
514
- const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
515
- room: true,
516
- queue: true,
517
- drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
518
- }));
519
- for (const locator of unlisted) {
520
- for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
521
- pendings.delete(batch.id);
522
- replicas.delete(locator);
523
- queuesWithoutReplica.delete(locator);
524
- rooms.delete(locator);
525
- roomDrafts.delete(locator);
526
- transfers.delete(locator);
527
- unreadable.delete(locator);
528
- stale.delete(locator);
529
- scopedRefusals.delete(locator);
530
- sourceRefusals.delete(locator);
531
- declarationRefusals.delete(locator);
532
- for (const attached of ['collection', 'typed-selection']) {
533
- const key = modelKey(locator, attached);
534
- models.delete(key);
535
- liveModels.delete(key);
536
- invalidModels.delete(key);
1255
+ sweeping = true;
1256
+ void (async () => {
1257
+ try {
1258
+ while (!closed && (await durably((local) => local.erase(null, ERASE_BATCH)))) {
1259
+ await yielded();
537
1260
  }
538
- journal.record(`room unlisted ${locator}`);
539
1261
  }
540
- publishChangeSet(mergeChangeSets(removed));
541
- moveTo({ ...status, ...attention() });
542
- });
543
- }
544
- function restored(key, persisted) {
545
- const prior = models.get(key);
546
- if (prior !== undefined &&
547
- prior.status !== 'unavailable' &&
548
- (persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
549
- invalidModels.add(key);
550
- }
551
- const model = persisted.status === 'unavailable' ||
552
- liveModels.get(key) === JSON.stringify(persisted.snapshot)
553
- ? persisted
554
- : cached(persisted);
555
- models.set(key, model);
556
- if (model.status === 'unavailable') {
557
- liveModels.delete(key);
558
- invalidModels.delete(key);
559
- }
560
- return model;
561
- }
562
- async function hydrate(opened) {
563
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
564
- opened.all(),
565
- opened.queues(),
566
- opened.roomDrafts(),
567
- opened.presentation(),
568
- opened.conversations(),
569
- opened.chatDrafts(),
570
- opened.lastCompleted(),
571
- opened.providerLimits(),
572
- opened.capabilities(),
573
- opened.member(),
574
- opened.transfers(),
575
- ]);
576
- transfers.clear();
577
- for (const record of staged)
578
- transfers.set(record.locator, record);
579
- limits = declared;
580
- capabilities = named;
581
- member = known;
582
- roomDrafts.clear();
583
- for (const record of kept)
584
- rememberRoomDraft(record);
585
- presentations.clear();
586
- for (const record of presented)
587
- presentations.set(record.app, record.value);
588
- conversations.hydrate(held, chatDrafts);
589
- const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
590
- pendings.clear();
591
- for (const queue of queues) {
592
- for (const { batch, state } of queue.pending)
593
- pendings.set(batch.id, state);
594
- }
595
- const previousReplicas = new Map(replicas);
596
- replicas.clear();
597
- queuesWithoutReplica.clear();
598
- const replicated = new Set(records.map((record) => record.locator));
599
- for (const [locator, queue] of queuedBatches) {
600
- if (!replicated.has(locator))
601
- queuesWithoutReplica.set(locator, queue);
602
- }
603
- for (const record of records) {
604
- const model = restored(modelKey(record.locator, 'collection'), record.model);
605
- const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
606
- const prior = previousReplicas.get(record.locator);
607
- const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
608
- replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
609
- const existing = rooms.get(record.locator);
610
- if (existing === undefined)
611
- list(record, false);
612
- else {
613
- rooms.set(record.locator, {
614
- ...existing,
615
- roomVersion: record.roomVersion,
616
- operations: record.operations,
617
- replica: record.replica,
618
- model: modelTypes(model),
619
- typedSelection: record.typedSelection === null
620
- ? null
621
- : {
622
- roots: record.typedSelection.roots,
623
- model: modelTypes(selectionModel),
624
- },
625
- });
1262
+ catch {
1263
+ journal.record('erasure failed');
1264
+ }
1265
+ finally {
1266
+ sweeping = false;
626
1267
  }
1268
+ })();
1269
+ }
1270
+ function fenceLive(locators) {
1271
+ for (const locator of new Set(locators)) {
1272
+ if (!store.fences.raise(locator, discoveries))
1273
+ continue;
1274
+ forget(locator);
1275
+ journal.record(`room lost ${locator}`);
627
1276
  }
628
- return {
629
- lastCompleted,
630
- locators: new Set([...queuedBatches.keys(), ...replicas.keys()]),
631
- read: mergeChangeSets([...new Set([...previousReplicas.keys(), ...replicas.keys()])].map((locator) => changeSet(locator, previousReplicas.get(locator) ?? null, replicas.get(locator) ?? null))),
632
- };
633
1277
  }
634
- async function readState(opened) {
635
- const { lastCompleted, locators, read } = await hydrate(opened);
636
- applyChangeSet(read);
637
- if (replicas.size > 0)
638
- emitRooms();
639
- if (member !== null)
640
- emitMember();
641
- for (const locator of locators)
642
- emitQueue(locator);
643
- publishRoomDrafts(false);
644
- return lastCompleted;
1278
+ async function loseRooms(locators) {
1279
+ fenceLive(locators);
1280
+ const fresh = [...new Set(locators)].filter((locator) => {
1281
+ const fence = store.fences.of(locator);
1282
+ if (fence === undefined || fence.written)
1283
+ return false;
1284
+ store.fences.mark(locator, true);
1285
+ return true;
1286
+ });
1287
+ if (fresh.length === 0)
1288
+ return;
1289
+ let held = [];
1290
+ try {
1291
+ await durably(async (local) => {
1292
+ held = await local.roomDrafts();
1293
+ await local.fence(fresh);
1294
+ });
1295
+ }
1296
+ catch (error) {
1297
+ for (const locator of fresh)
1298
+ store.fences.mark(locator, false);
1299
+ throw error;
1300
+ }
1301
+ finally {
1302
+ publishChangeSet(mergeChangeSets(fresh.map((locator) => changeSet(locator, {
1303
+ documents: null,
1304
+ room: true,
1305
+ queue: true,
1306
+ drafts: held.filter((draft) => draft.locator === locator).map(({ path }) => path),
1307
+ }))));
1308
+ recoverReads();
1309
+ moveTo(status);
1310
+ }
1311
+ await durably((local) => unpair(accepting, local));
1312
+ await eraseFenced(fresh);
645
1313
  }
646
- async function open() {
647
- const opened = await openRuntimeStore();
648
- store = opened;
1314
+ async function open(opened) {
1315
+ if (closed)
1316
+ throw new Error('runtime closed');
1317
+ const interrupted = await opened.fences();
649
1318
  const kept = options.shell.generation;
650
1319
  const storedGeneration = await opened.generation();
651
1320
  const loss = detectStorageLoss(storedGeneration, kept);
@@ -658,330 +1327,42 @@ export function composeRuntime(options) {
658
1327
  storageLoss = loss;
659
1328
  emitStorageLoss();
660
1329
  }
661
- const lastCompleted = await readState(opened);
662
- moveTo({ ...status, lastCompleted, ...attention() });
663
- return opened;
664
- }
665
- function durably(work) {
666
- return exclusive(async () => {
667
- const local = await openLocal();
668
- return work(local);
669
- });
670
- }
671
- function openLocal() {
672
- opening ??= open().catch((error) => {
673
- opening = undefined;
674
- throw error;
675
- });
676
- return opening;
677
- }
678
- function exclusive(work) {
679
- const next = mutations.then(work, work);
680
- mutations = next.then(() => undefined, () => undefined);
681
- return next;
682
- }
683
- function withdrawable(batch) {
684
- const state = pendings.get(batch.id);
685
- return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
686
- }
687
- async function settle(local, locator, before, confirmed, queue, settling = {}) {
688
- const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
689
- const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
690
- const kept = queue.filter((batch) => !withdrawn.includes(batch));
691
- const unqueued = withdrawn.map((batch) => ({
692
- batch,
693
- returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
694
- }));
695
- const returning = [
696
- ...[...unqueued].reverse(),
697
- ...(settling.leaving === undefined ? [] : [settling.leaving]),
698
- ];
699
- const written = new Map();
700
- for (const { batch, returned } of returning) {
701
- if (returned === null)
702
- continue;
703
- for (const saved of pendings.get(batch.id)?.drafts ?? []) {
704
- const existing = written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path);
705
- written.set(saved.path, {
706
- locator,
707
- path: saved.path,
708
- revision: roomDraftRevision(),
709
- parts: returnedInto(existing?.parts, saved.parts),
710
- returned,
711
- });
712
- }
713
- }
714
- await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
715
- for (const batch of previousQueue) {
716
- if (!kept.includes(batch))
717
- pendings.delete(batch.id);
718
- }
719
- for (const record of written.values())
720
- rememberRoomDraft(record);
1330
+ const held = await opened.opened();
1331
+ heldWhenOpened = usableWhenHeld.length > 0 && (await opened.covering()).rooms.size > 0;
721
1332
  try {
722
- await local.write({
723
- next: confirmed.record,
724
- previous: before?.confirmed.record ?? null,
725
- ...(settling.stored === undefined ? {} : { stored: settling.stored }),
726
- });
1333
+ await opened.demote();
1334
+ await database.ask({ kind: 'read.builds', build });
1335
+ for (const key of held.orphans)
1336
+ await opened.release(key);
727
1337
  }
728
1338
  catch (error) {
729
- const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
730
- if (held !== null)
731
- replicas.set(locator, held);
732
- else if (kept.length > 0)
733
- queuesWithoutReplica.set(locator, kept);
734
- else
735
- queuesWithoutReplica.delete(locator);
736
- publishChangeSet(changeSet(locator, before ?? null, held, {
737
- queue: kept.length !== previousQueue.length,
738
- drafts: [...written.keys()],
739
- }));
740
- moveTo({ ...status, ...attention() });
741
- throw error;
742
- }
743
- queuesWithoutReplica.delete(locator);
744
- for (const { batch, returned } of unqueued) {
745
- const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
746
- journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
747
- }
748
- replicas.set(locator, replicaOf(confirmed, kept, before));
749
- return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
750
- room: settling.room,
751
- queue: kept.length !== previousQueue.length,
752
- drafts: [...written.keys()],
753
- });
754
- }
755
- function adoptModels(locator, record) {
756
- for (const [attached, model] of [
757
- ['collection', record.model],
758
- ['typed-selection', record.selectionModel],
759
- ]) {
760
- const key = modelKey(locator, attached);
761
- models.set(key, model);
762
- invalidModels.delete(key);
763
- if (model.status === 'current')
764
- liveModels.set(key, JSON.stringify(model.snapshot));
765
- else if (model.status === 'unavailable')
766
- liveModels.delete(key);
767
- }
768
- }
769
- function remodel(locator, answers, held) {
770
- const taken = (answer, previous, attached) => {
771
- if (answer === undefined)
772
- return previous;
773
- const { state, refused } = attachedModel(answer, previous);
774
- if (refused !== null) {
775
- const where = refused.location === undefined ? '' : ` ${refused.location}`;
776
- journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
777
- }
778
- models.set(modelKey(locator, attached), state);
779
- return state;
780
- };
781
- return {
782
- collection: taken(answers?.collection, held.collection, 'collection'),
783
- selection: taken(answers?.selection, held.selection, 'typed-selection'),
784
- };
785
- }
786
- function publish(local, room, confirmed, landing = {}) {
787
- const { base, stored } = landing;
788
- return exclusive(async () => {
789
- if (!continuing())
790
- return true;
791
- const prior = replicas.get(room.locator);
792
- if (base !== undefined &&
793
- (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
794
- prior.confirmed.record.revisions !== base.record.revisions)) {
795
- return false;
796
- }
797
- adoptModels(room.locator, confirmed.record);
798
- const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
799
- const roomVersion = confirmed.record.roomVersion;
800
- const accepted = queue.filter((batch) => {
801
- const accepted = pendings.get(batch.id)?.accepted;
802
- return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
803
- });
804
- const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
805
- if (stored !== undefined)
806
- transfers.delete(room.locator);
807
- for (const batch of accepted)
808
- pendings.delete(batch.id);
809
- stale.delete(room.locator);
810
- if (unreadable.delete(room.locator))
811
- scopedRefusals.delete(room.locator);
812
- sourceRefusals.delete(room.locator);
813
- declarationRefusals.delete(room.locator);
814
- list(room, true);
815
- publishChangeSet(set);
816
- moveTo({ ...status, ...attention() });
817
- return true;
818
- });
819
- }
820
- async function saveRoomDrafts(locator, named, writtenBy) {
821
- const local = await openLocal();
822
- const replica = replicas.get(locator);
823
- const room = rooms.get(locator);
824
- if (replica === undefined || room === undefined) {
825
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
826
- }
827
- const saved = [];
828
- for (const { path, revision } of named) {
829
- const record = roomDrafts.get(locator)?.get(path);
830
- if (record === undefined || record.revision !== revision) {
831
- return { outcome: 'refused', code: 'stale', retryable: false, file: path };
832
- }
833
- if (!saved.some((draft) => draft.path === path))
834
- saved.push({ path, parts: record.parts });
835
- }
836
- const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
837
- if (changed.length > 0)
838
- return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
839
- const parts = saved.map(({ path, parts }) => changesOf(path, parts));
840
- const changes = parts.flatMap(({ changes }) => changes);
841
- const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
842
- if (requeue.kind === 'refused')
843
- return { outcome: 'refused', ...requeue.refusal };
844
- const foreign = [...requeue.withdrawn.keys()].some((origin) => {
845
- const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
846
- return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
847
- });
848
- if (foreign) {
849
- return {
850
- outcome: 'refused',
851
- code: 'unknown-revision',
852
- retryable: false,
853
- file: [...requeue.queued].at(-1),
854
- };
855
- }
856
- const withdrawing = requeue.withdrawn.size > 0;
857
- const input = {
858
- local: localSaves,
859
- baseRoomVersion: replica.confirmed.record.roomVersion,
860
- composition: compositionFor(replica.confirmed.record),
861
- operations: room.operations,
862
- providerOwnedPaths: room.providerOwnedPaths,
863
- providerLimits: limits,
864
- handle: room.handle,
865
- visible: replica.visible,
866
- changes,
867
- bases: parts.flatMap(({ bases }) => bases),
868
- writtenBy,
869
- };
870
- const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
871
- if (checked?.kind === 'refused')
872
- return { outcome: 'refused', ...checked.refusal };
873
- const owners = [...requeue.owners].flatMap((origin) => {
874
- const batch = requeue.withdrawn.get(origin);
875
- return batch === undefined || batch === null ? [] : [{ origin, batch }];
876
- });
877
- if (owners.length > 1) {
878
- return {
879
- outcome: 'refused',
880
- code: 'unknown-revision',
881
- retryable: false,
882
- file: [...requeue.queued].at(-1),
883
- };
884
- }
885
- const built = !withdrawing
886
- ? buildBatch(input)
887
- : requeue.changes.length === 0
888
- ? null
889
- : buildBatch({
890
- ...input,
891
- visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
892
- changes: requeue.changes,
893
- });
894
- if (built?.kind === 'refused')
895
- return { outcome: 'refused', ...built.refusal };
896
- const [owner] = owners;
897
- const join = built === null || owner === undefined
898
- ? null
899
- : joined(requeue.queue, owner.batch, built.batch);
900
- if (join?.kind === 'refused')
901
- return { outcome: 'refused', ...join.refusal };
902
- const withdrawn = new Map(requeue.withdrawn);
903
- if (join !== null && owner !== undefined)
904
- withdrawn.set(owner.origin, join.batch);
905
- const drafted = new Map();
906
- for (const [origin, replaced] of withdrawn) {
907
- if (replaced === null)
908
- continue;
909
- const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
910
- drafted.set(replaced.id, replaced === join?.batch
911
- ? [
912
- ...carried.map((held) => joinedDraft(held, saved)),
913
- ...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
914
- ]
915
- : carried);
916
- }
917
- if (built !== null && join === null)
918
- drafted.set(built.batch.id, saved);
919
- const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
920
- for (const replaced of withdrawn.values()) {
921
- if (replaced === null || !batches.includes(replaced))
922
- continue;
923
- const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
924
- if (limited !== null)
925
- return { outcome: 'refused', ...limited };
926
- }
927
- const consumed = saved.map(({ path }) => path);
928
- try {
929
- await local.keepAuthored({
930
- locator,
931
- queue: batches,
932
- saved: drafted,
933
- consumedRoomDrafts: consumed,
934
- });
935
- }
936
- catch {
937
- return { outcome: 'refused', code: 'durable-write', retryable: true };
938
- }
939
- for (const origin of withdrawn.keys())
940
- pendings.delete(origin);
941
- for (const [id, drafts] of drafted) {
942
- pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
943
- }
944
- if (!localSaves)
945
- recorded = false;
946
- for (const path of consumed)
947
- forgetRoomDraft(locator, path);
948
- replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
949
- publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
950
- queue: true,
951
- drafts: consumed,
952
- }));
953
- moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
954
- submitLater(locator);
955
- const identifier = join?.batch.id ??
956
- built?.batch.id ??
957
- [...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
958
- return { outcome: 'saved', identifier };
959
- }
960
- async function mark(local, locator, identifier, submission, lost) {
961
- const state = pendings.get(identifier);
962
- if (!continuing() ||
963
- state === undefined ||
964
- (JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
965
- return;
1339
+ if (!refusedForSpace(error))
1340
+ throw error;
1341
+ }
1342
+ if (held.erasing || held.orphans.length > 0)
1343
+ queueMicrotask(sweep);
1344
+ await conversations.opened(opened);
1345
+ if (interrupted.length > 0) {
1346
+ queueMicrotask(() => {
1347
+ void eraseFenced(interrupted).catch(() => {
1348
+ journal.record('erasure failed');
1349
+ });
1350
+ });
966
1351
  }
967
- await local.submit(locator, identifier, submission, lost);
968
- pendings.set(identifier, { ...state, submission, lost });
969
- const replica = replicas.get(locator);
970
- if (replica !== undefined)
971
- publishChangeSet(changeSet(locator, replica, replica, { queue: true }));
972
- moveTo({ ...status, ...attention() });
973
- }
974
- async function keepUpload(local, locator, identifier, upload) {
975
- if (!continuing())
976
- throw new Error('connection refused');
977
- await local.keepUpload(locator, identifier, upload);
978
- pendings.set(identifier, { ...pendings.get(identifier), upload });
979
- if (!continuing())
980
- throw new Error('connection refused');
1352
+ moveTo(status);
1353
+ prepare();
1354
+ emitRooms();
1355
+ void exclusive(() => unpair(accepting, opened)).catch(() => {
1356
+ journal.record('unpair failed');
1357
+ });
1358
+ return opened;
981
1359
  }
982
1360
  async function readHistory(locator, path, query = {}) {
983
1361
  if (closed || refusal !== null)
984
1362
  return UNCERTAIN;
1363
+ await firstDiscovery;
1364
+ if (!roomsNamed)
1365
+ return UNCERTAIN;
985
1366
  try {
986
1367
  await ensureAuthorization();
987
1368
  }
@@ -990,6 +1371,7 @@ export function composeRuntime(options) {
990
1371
  }
991
1372
  if (closed || authorization === undefined || refusal !== null)
992
1373
  return UNCERTAIN;
1374
+ const since = store.fences.count();
993
1375
  const transfer = ['transfer', 'history', locator, path];
994
1376
  if (query.revision !== undefined)
995
1377
  transfer.push(query.revision);
@@ -1004,8 +1386,10 @@ export function composeRuntime(options) {
1004
1386
  ...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
1005
1387
  });
1006
1388
  journal.record(`outcome history ${result.kind}`);
1389
+ if (store.fences.since(locator, since))
1390
+ return failure({ kind: 'room-not-readable', locator });
1007
1391
  if (result.kind === 'scoped-refusal') {
1008
- const room = replicas.get(result.locator)?.confirmed.record;
1392
+ const room = await recordAt(result.locator);
1009
1393
  if (room !== undefined)
1010
1394
  await scoped(result, room, await openLocal());
1011
1395
  }
@@ -1018,99 +1402,48 @@ export function composeRuntime(options) {
1018
1402
  return UNCERTAIN;
1019
1403
  }
1020
1404
  }
1021
- function queuedBlob(locator, path, localId) {
1022
- for (const batch of replicas.get(locator)?.queue ?? []) {
1023
- const change = batch.changes[queuedFile(batch, path, localId)];
1024
- if (change?.kind === 'binary')
1025
- return change.blob;
1026
- }
1027
- return null;
1028
- }
1029
- async function readBinary(request) {
1030
- if (closed || refusal !== null)
1031
- return UNCERTAIN;
1032
- try {
1033
- await ensureAuthorization();
1034
- }
1035
- catch {
1036
- return UNCERTAIN;
1037
- }
1038
- if (closed || authorization === undefined || refusal !== null)
1039
- return UNCERTAIN;
1040
- journal.record(request.kind === 'path'
1041
- ? `transfer binary ${request.locator} ${request.path}`
1042
- : `transfer binary ${request.fileRef}`);
1043
- try {
1044
- const result = await options.adapter.binary(authorization, request);
1045
- journal.record(`outcome binary ${result.kind}`);
1046
- if (result.kind === 'scoped-refusal') {
1047
- const room = replicas.get(result.locator)?.confirmed.record;
1048
- if (room !== undefined)
1049
- await scoped(result, room, await openLocal());
1050
- }
1051
- if (result.kind === 'revoked-authorization')
1052
- await refuse('revoked-authorization');
1053
- return binaryAnswer(result);
1054
- }
1055
- catch {
1056
- journal.record('outcome binary transport-uncertainty');
1057
- return UNCERTAIN;
1058
- }
1059
- }
1060
- async function readBinaryPath(locator, path, revision) {
1061
- await ready;
1062
- if (revision !== undefined)
1063
- return readBinary({ kind: 'path', locator, path, revision });
1064
- const visible = replicas.get(locator)?.visible;
1065
- const held = visible?.binaries[path];
1066
- if (visible === undefined || held === undefined)
1067
- return absentBinary(path);
1068
- if (held.state === 'pending') {
1069
- const blob = queuedBlob(locator, path, held.localId);
1070
- return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1071
- }
1072
- const record = binaryRecord(path, held, visible.revisions[path]);
1073
- if (record.state !== 'accepted')
1074
- return absentBinary(path);
1075
- const replica = replicas.get(locator);
1076
- const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
1077
- const holds = (at) => {
1078
- const kept = replica?.confirmed.record.binaries[at];
1079
- return (kept?.state === 'accepted' &&
1080
- kept.fileRef === record.fileRef &&
1081
- replica?.confirmed.record.revisions[at] === record.revision);
1082
- };
1083
- let confirmed = path;
1084
- for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
1085
- const change = queued[index];
1086
- if (change.kind === 'move' && change.to === confirmed)
1087
- confirmed = change.path;
1088
- }
1089
- return readBinary({
1090
- kind: 'path',
1091
- locator,
1092
- path: confirmed,
1093
- revision: record.revision,
1094
- fileRef: record.fileRef,
1095
- });
1096
- }
1097
- async function readBinaryReference(reference) {
1098
- await ready;
1099
- return readBinary({
1100
- kind: 'reference',
1101
- fileRef: reference.fileRef,
1102
- referrer: reference.referrer,
1103
- });
1104
- }
1105
- function declaredAgents() {
1106
- for (const capability of capabilities) {
1107
- if (capability.conversationAgents !== undefined)
1108
- return capability.conversationAgents;
1109
- }
1110
- return undefined;
1111
- }
1112
- function declaredAction(name) {
1113
- for (const capability of capabilities) {
1405
+ async function located(room, path, destination) {
1406
+ const crossing = roomTarget(destination);
1407
+ const listing = await listingNow();
1408
+ if (crossing === null) {
1409
+ const named = target(path, destination);
1410
+ return named === null || !listing.has(room) ? null : { room, path: named };
1411
+ }
1412
+ const held = [...listing.values()].find((listed) => listed.handle === crossing.handle);
1413
+ return held === undefined ? null : { room: held.locator, path: crossing.path };
1414
+ }
1415
+ async function resolveReference(room, path, destination) {
1416
+ if (roomTarget(destination) === null && target(path, destination) === null) {
1417
+ return ABSENT_TARGET;
1418
+ }
1419
+ const at = await located(room, path, destination);
1420
+ if (at === null)
1421
+ return UNREADABLE;
1422
+ if (await locked(at.room))
1423
+ return LOCKED_UNREADABLE;
1424
+ const held = await recordAt(at.room);
1425
+ if (held !== undefined &&
1426
+ (await (await openLocal()).entries(held.key, 'visible', [at.path])).length > 0) {
1427
+ return { kind: 'document', ...at };
1428
+ }
1429
+ const history = await readHistory(at.room, at.path);
1430
+ if (history.kind === 'no-history' || history.kind === 'revision')
1431
+ return ABSENT_TARGET;
1432
+ if (history.kind !== 'history')
1433
+ return history;
1434
+ const [last] = history.revisions;
1435
+ if (last === undefined)
1436
+ return ABSENT_TARGET;
1437
+ return history.deleted === true
1438
+ ? { kind: 'deleted', ...at, revision: last }
1439
+ : { kind: 'document', ...at };
1440
+ }
1441
+ async function cleanUp() {
1442
+ const freed = await database.ask({ kind: 'opened.clean' });
1443
+ return freed + ((await options.shell.cleanUp?.()) ?? 0);
1444
+ }
1445
+ async function declaredAction(name) {
1446
+ for (const capability of (await (await openLocal()).facts()).capabilities) {
1114
1447
  for (const action of capability.actions ?? []) {
1115
1448
  if (action.name === name)
1116
1449
  return action;
@@ -1118,11 +1451,18 @@ export function composeRuntime(options) {
1118
1451
  }
1119
1452
  return undefined;
1120
1453
  }
1121
- async function invokeAction(name, input) {
1454
+ async function calledRooms(name, input) {
1455
+ const member = (await declaredAction(name))?.room;
1456
+ if (member === undefined || input === null || typeof input !== 'object')
1457
+ return [];
1458
+ const named = input[member];
1459
+ return (Array.isArray(named) ? named : [named]).filter((room) => typeof room === 'string');
1460
+ }
1461
+ async function invokeAction(name, input, dispatching) {
1122
1462
  await ready;
1123
1463
  if (closed || refusal !== null)
1124
1464
  return UNCERTAIN;
1125
- if (declaredAction(name) === undefined)
1465
+ if ((await declaredAction(name)) === undefined)
1126
1466
  return undeclared(name);
1127
1467
  try {
1128
1468
  await ensureAuthorization();
@@ -1132,11 +1472,12 @@ export function composeRuntime(options) {
1132
1472
  }
1133
1473
  if (closed || authorization === undefined || refusal !== null)
1134
1474
  return UNCERTAIN;
1475
+ await dispatching?.();
1135
1476
  journal.record(`transfer action ${name}`);
1136
1477
  try {
1137
1478
  const result = await options.adapter.action(authorization, { name, input });
1138
1479
  if (result.kind === 'scoped-refusal') {
1139
- const room = replicas.get(result.locator)?.confirmed.record;
1480
+ const room = await recordAt(result.locator);
1140
1481
  if (room !== undefined)
1141
1482
  await scoped(result, room, await openLocal());
1142
1483
  }
@@ -1152,113 +1493,158 @@ export function composeRuntime(options) {
1152
1493
  return UNCERTAIN;
1153
1494
  }
1154
1495
  }
1155
- function continuing() {
1156
- return !closed && !discoveryPaused;
1157
- }
1158
- function reachedBy(locator, roomVersion) {
1159
- const held = replicas.get(locator);
1160
- return (held !== undefined &&
1161
- options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion));
1162
- }
1163
- function writtenAfter(locator, roomVersion, path) {
1164
- const written = replicas.get(locator)?.confirmed.record.written[path];
1165
- return (written !== undefined &&
1166
- options.adapter.reached(locator, written, roomVersion) &&
1167
- !options.adapter.reached(locator, roomVersion, written));
1168
- }
1169
- async function pullRefresh(authorized, confirmed) {
1170
- if (!continuing())
1171
- return unconfirmed;
1496
+ async function readConversationHistory(request) {
1497
+ await ready;
1498
+ const stream = options.adapter.conversations;
1499
+ if (closed || refusal !== null || stream?.history === undefined)
1500
+ return UNCERTAIN;
1501
+ journal.record('transfer conversation history');
1172
1502
  try {
1173
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
1174
- const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
1175
- journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
1176
- return result;
1503
+ const result = await stream.history(request);
1504
+ if (result.kind === 'revoked-authorization')
1505
+ await refuse('revoked-authorization');
1506
+ journal.record(`outcome conversation history ${result.kind}`);
1507
+ return calledResult(result);
1177
1508
  }
1178
- catch (error) {
1179
- journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
1180
- throw error;
1509
+ catch {
1510
+ journal.record('outcome conversation history transport-uncertainty');
1511
+ return UNCERTAIN;
1181
1512
  }
1182
1513
  }
1183
- function staging(local) {
1184
- return {
1185
- held: (locator) => transfers.get(locator) ?? null,
1186
- staged: (record) => local.staged(record),
1187
- stage: (record, page) => exclusive(async () => {
1188
- if (!continuing())
1189
- return false;
1190
- await local.stage(record, page);
1191
- transfers.set(record.locator, record);
1192
- return true;
1193
- }),
1194
- abandon: (locator) => exclusive(async () => {
1195
- if (!continuing())
1196
- return;
1197
- await local.abandon(locator);
1198
- transfers.delete(locator);
1199
- }),
1200
- };
1514
+ function unsettled(result) {
1515
+ return result.kind !== 'action' && result.retryable;
1201
1516
  }
1202
- async function pullBootstrap(authorized, room, local) {
1203
- if (!continuing())
1204
- return unconfirmed;
1517
+ async function recoveredAction(name, input) {
1518
+ const operation = operationOf(input);
1519
+ if (operation === null)
1520
+ return invokeAction(name, input);
1521
+ const kept = async () => (await openLocal()).keepCall({ operation, name, input });
1522
+ calling.add(operation);
1205
1523
  try {
1206
- const result = await bootstrapRoom(options.adapter, authorized, room, {
1207
- collection: modelOf(room.locator),
1208
- selection: modelOf(room.locator, 'typed-selection'),
1209
- }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
1210
- const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
1211
- journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1524
+ let result = await invokeAction(name, input, kept);
1525
+ if (unsettled(result)) {
1526
+ await recovery.attempt(`action ${operation}`, async () => {
1527
+ if (closed || refusal !== null)
1528
+ return 'done';
1529
+ if (!socket.isOpen())
1530
+ return 'again';
1531
+ result = await invokeAction(name, input, kept);
1532
+ return unsettled(result) ? 'again' : 'done';
1533
+ }, { again: true });
1534
+ }
1535
+ if (!unsettled(result))
1536
+ await (await openLocal()).dropCall(operation);
1212
1537
  return result;
1213
1538
  }
1214
- catch (error) {
1215
- journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
1216
- throw error;
1539
+ finally {
1540
+ calling.delete(operation);
1217
1541
  }
1218
1542
  }
1219
- async function advance(room, local, authorized) {
1220
- const held = replicas.get(room.locator);
1221
- if (held !== undefined) {
1222
- const refreshed = await pullRefresh(authorized, held.confirmed);
1223
- if (!continuing())
1224
- return unconfirmed;
1225
- if (refreshed.kind === 'still-current') {
1226
- stale.delete(room.locator);
1227
- if (sourceRefusals.has(room.locator))
1228
- return null;
1229
- if (unreadable.delete(room.locator)) {
1230
- scopedRefusals.delete(room.locator);
1231
- list(room, true);
1232
- emitRooms();
1233
- }
1234
- return null;
1543
+ async function resendCalls(signal) {
1544
+ if (!socket.isOpen() || closed || stopping)
1545
+ return 'done';
1546
+ const local = await openLocal();
1547
+ for (const held of await local.calls()) {
1548
+ if (calling.has(held.operation))
1549
+ continue;
1550
+ if (closed || signal.aborted)
1551
+ return 'done';
1552
+ calling.add(held.operation);
1553
+ try {
1554
+ if (unsettled(await invokeAction(held.name, held.input)))
1555
+ return 'again';
1556
+ await local.dropCall(held.operation);
1235
1557
  }
1236
- if (refreshed.kind === 'advanced') {
1237
- if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
1238
- const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
1239
- const reached = now !== undefined &&
1240
- options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
1241
- return reached ? null : advance(room, local, authorized);
1242
- }
1243
- await retainModel(local, room, 'collection', modelOf(room.locator));
1244
- return null;
1558
+ finally {
1559
+ calling.delete(held.operation);
1245
1560
  }
1246
- if (refreshed.kind !== 'beyond-horizon')
1247
- return refreshed;
1248
1561
  }
1249
- const bootstrapped = await pullBootstrap(authorized, room, local);
1250
- if (!continuing())
1562
+ return 'done';
1563
+ }
1564
+ function continuing() {
1565
+ return !closed && !discoveryPaused;
1566
+ }
1567
+ async function advance(room, local, authorized, running, unheld) {
1568
+ const live = () => running() && !full;
1569
+ if (!live())
1251
1570
  return unconfirmed;
1571
+ const held = unheld === undefined ? await recordAt(room.locator) : undefined;
1572
+ if (options.adapter.roomStream !== undefined &&
1573
+ (held !== undefined || !store.installs.of(room.locator).beyondHorizon)) {
1574
+ stale.delete(room.locator);
1575
+ await name(room.locator);
1576
+ return null;
1577
+ }
1578
+ const refresh = reads.refresh;
1579
+ if (held !== undefined && refresh !== undefined) {
1580
+ const refreshed = await store.ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
1581
+ if (refreshed === 'behind')
1582
+ return advance(room, local, authorized, live);
1583
+ if (refreshed !== 'beyond-horizon')
1584
+ return refreshed;
1585
+ if (reloadsDue.has(room.locator))
1586
+ return unconfirmed;
1587
+ replaced(store, [room.locator]);
1588
+ }
1589
+ const prior = held === undefined ? undefined : await recordAt(room.locator);
1590
+ const from = standingAt(store, room.locator, prior);
1591
+ const bootstrapped = await pullBootstrap(installer, authorized, room, local, live, prior, reads, unheld);
1252
1592
  if (bootstrapped.kind !== 'staged')
1253
- return bootstrapped;
1254
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1255
- return null;
1593
+ return live() ? bootstrapped : unconfirmed;
1594
+ const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
1595
+ if (await publish(installer, local, room, bootstrapped.record, landing, live)) {
1596
+ return live() ? null : unconfirmed;
1597
+ }
1598
+ return (await reachedBy(installer, room.locator, bootstrapped.record.roomVersion))
1599
+ ? null
1600
+ : advance(room, local, authorized, live);
1256
1601
  }
1257
- async function withhold(withheld, room, local, progress) {
1258
- if (withheld.kind === 'refused')
1602
+ async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
1603
+ const refreshed = await pullRefresh(installer, authorized, held, live, hold, refresh);
1604
+ if (!live())
1605
+ return unconfirmed;
1606
+ if (refreshed.kind === 'still-current') {
1259
1607
  stale.delete(room.locator);
1608
+ if (unreadable.delete(room.locator)) {
1609
+ scopedRefusals.delete(room.locator);
1610
+ list(room, true);
1611
+ emitRooms();
1612
+ }
1613
+ return null;
1614
+ }
1615
+ if (refreshed.kind === 'advanced') {
1616
+ const landing = {
1617
+ kind: 'refreshed',
1618
+ from: refreshed.from,
1619
+ remodelled: refreshed.remodelled,
1620
+ };
1621
+ if (!(await publish(installer, local, room, refreshed.record, landing, live))) {
1622
+ const now = (await recordAt(room.locator))?.roomVersion;
1623
+ const reached = now !== undefined &&
1624
+ options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
1625
+ return reached ? null : 'behind';
1626
+ }
1627
+ await retainModel(local, room, null);
1628
+ return null;
1629
+ }
1630
+ return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
1631
+ }
1632
+ async function withhold(withheld, room, local, progress) {
1633
+ if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
1634
+ return;
1635
+ if (withheld.kind === 'not-ready') {
1636
+ progress.complete = false;
1637
+ askAgain(room.locator);
1638
+ return;
1639
+ }
1640
+ stale.add(room.locator);
1641
+ if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
1642
+ const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
1643
+ repairable.set(room.locator, reset);
1644
+ repairLater(room.locator, reset === undefined ? { again: true } : { at: Date.parse(reset) });
1645
+ }
1260
1646
  else
1261
- stale.add(room.locator);
1647
+ repairable.delete(room.locator);
1262
1648
  if (withheld.kind === 'scoped-refusal') {
1263
1649
  await scoped(withheld, room, local);
1264
1650
  }
@@ -1269,308 +1655,141 @@ export function composeRuntime(options) {
1269
1655
  journal.record(`refusal ${withheld.kind} ${withheld.code}`);
1270
1656
  }
1271
1657
  else if (withheld.kind === 'room-not-readable') {
1272
- unreadable.add(room.locator);
1273
- list(room, false);
1274
- emitRooms();
1275
- }
1276
- else if (withheld.kind === 'refused') {
1277
- refusedSource(room, withheld);
1658
+ roomUnreadable(room);
1278
1659
  }
1279
1660
  else if (withheld.kind === 'revoked-authorization') {
1280
1661
  await refuse('revoked-authorization');
1281
1662
  }
1282
1663
  }
1283
- function refusedSource(room, { refusal }) {
1284
- sourceRefusals.set(room.locator, {
1285
- code: refusal.rule,
1286
- retryable: false,
1287
- fields: {
1288
- file: refusal.file,
1289
- ...(refusal.location === undefined ? {} : { location: refusal.location }),
1290
- ...(refusal.referrers === undefined ? {} : { referrers: refusal.referrers }),
1291
- },
1292
- });
1293
- declarationRefusals.delete(room.locator);
1664
+ function roomUnreadable(room) {
1294
1665
  unreadable.add(room.locator);
1295
1666
  list(room, false);
1296
1667
  emitRooms();
1297
- journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
1298
- }
1299
- function leaving(batch, result) {
1300
- if (result.kind === 'expired')
1301
- return { batch, returned: returnedBy('expired') };
1302
- return {
1303
- batch,
1304
- returned: {
1305
- kind: result.kind,
1306
- code: result.code,
1307
- fields: result.fields,
1308
- retryable: result.retryable,
1309
- },
1310
- };
1311
- }
1312
- function logAccepted(locator, submitted, accepted) {
1313
- journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
1314
- (accepted.author === undefined ? '' : ` author ${accepted.author}`) +
1315
- (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1316
- }
1317
- async function adoptAccepted(room, local, submitted, accepted) {
1318
- if (!continuing())
1319
- return unconfirmed;
1320
- const current = replicas.get(room.locator);
1321
- if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1322
- return null;
1323
- }
1324
- const held = current.confirmed.record.roomVersion;
1325
- logAccepted(room.locator, submitted, accepted);
1326
- const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1327
- ? { kind: 'staged', confirmed: current.confirmed }
1328
- : applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
1329
- ? accepted.roomVersion
1330
- : held, accepted.roomVersion);
1331
- if (applied.kind !== 'staged') {
1332
- const state = pendings.get(submitted.id);
1333
- await local.accept(room.locator, submitted.id, accepted.roomVersion);
1334
- if (state !== undefined)
1335
- pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1336
- emitQueue(room.locator);
1337
- return applied;
1338
- }
1339
- const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1340
- pendings.delete(submitted.id);
1341
- unreadable.delete(room.locator);
1342
- sourceRefusals.delete(room.locator);
1343
- declarationRefusals.delete(room.locator);
1344
- list(room, true);
1345
- publishChangeSet(set);
1346
- moveTo({ ...status, ...attention() });
1347
- return null;
1348
- }
1349
- async function adopted(room, local, submitted, accepted) {
1350
- const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
1351
- if (failed === null)
1352
- return true;
1353
- if (failed.kind === 'refused' && continuing())
1354
- refusedSource(room, failed);
1355
- return false;
1356
1668
  }
1357
- async function submitRoom(room, local, authorized, progress) {
1358
- while (continuing() && authorization !== undefined) {
1359
- const replica = replicas.get(room.locator);
1360
- const head = replica?.queue[0];
1361
- if (replica === undefined || head === undefined)
1362
- return;
1363
- if (pendings.get(head.id)?.accepted !== undefined)
1364
- return;
1365
- const state = pendings.get(head.id);
1366
- const { submission, lost: earlier } = state ?? {
1367
- submission: { state: 'waiting' },
1368
- lost: false,
1369
- };
1370
- if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
1371
- progress.complete = false;
1372
- return;
1373
- }
1374
- if (writesHeld()) {
1375
- progress.complete = false;
1376
- journal.record(`submission ${room.locator} ${head.id} held`);
1377
- return;
1378
- }
1379
- const lost = earlier || submission.state === 'submitted';
1380
- let result;
1381
- journal.record(`submission ${room.locator} ${head.id}`);
1382
- await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1383
- if (discoveryPaused)
1384
- return;
1385
- if (writesHeld()) {
1386
- await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
1387
- progress.complete = false;
1388
- journal.record(`submission ${room.locator} ${head.id} held`);
1389
- return;
1390
- }
1391
- const opensBefore = openSignals;
1392
- const answered = awaitingAnswer(room.locator);
1393
- try {
1394
- result = await options.adapter.mutate(authorized, {
1395
- locator: room.locator,
1396
- changeBatch: head,
1397
- ...(state?.upload === undefined ? {} : { upload: state.upload }),
1398
- }, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
1399
- }
1400
- catch {
1401
- answered();
1402
- progress.complete = false;
1403
- if (continuing())
1404
- progress.reachable = false;
1669
+ function askAgain(locator) {
1670
+ const held = unready.get(locator) ?? { tries: 0, timer: null };
1671
+ unready.set(locator, held);
1672
+ if (held.timer !== null)
1673
+ return;
1674
+ const wait = NOT_READY_MS[Math.min(held.tries, NOT_READY_MS.length - 1)];
1675
+ held.tries += 1;
1676
+ held.timer = setTimeout(() => {
1677
+ held.timer = null;
1678
+ const room = discovered.get(locator);
1679
+ const unstreamed = options.adapter.roomStream === undefined;
1680
+ if (closed || stopping || (!unstreamed && !socket.isOpen()) || room === undefined)
1405
1681
  return;
1406
- }
1407
- if (result.kind !== 'accepted' || !continuing())
1408
- answered();
1682
+ const progress = { complete: true, reachable: true };
1683
+ void openLocal()
1684
+ .then((local) => roomTask(locator, (live) => loadRoom(room, local, progress, live)))
1685
+ .then(() => rest(progress), () => undefined);
1686
+ }, wait);
1687
+ }
1688
+ function readied(locator) {
1689
+ const held = unready.get(locator);
1690
+ if (held === undefined)
1691
+ return;
1692
+ if (held.timer !== null)
1693
+ clearTimeout(held.timer);
1694
+ unready.delete(locator);
1695
+ }
1696
+ function unreadied() {
1697
+ for (const locator of [...unready.keys()])
1698
+ readied(locator);
1699
+ }
1700
+ function declaredAsHeld(held, room) {
1701
+ return (held === undefined ||
1702
+ (JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
1703
+ JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
1704
+ JSON.stringify(held.providerOwnedPaths) === JSON.stringify(room.providerOwnedPaths) &&
1705
+ JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
1706
+ }
1707
+ async function keepNaming(local, room) {
1708
+ const held = await recordAt(room.locator);
1709
+ if (held === undefined || (held.name === room.name && held.handle === room.handle))
1710
+ return;
1711
+ await exclusive(async () => {
1409
1712
  if (!continuing())
1410
1713
  return;
1411
- if (result.kind === 'transport-uncertainty') {
1412
- progress.complete = false;
1413
- progress.reachable = false;
1414
- return;
1415
- }
1416
- if (result.kind === 'room-not-readable') {
1417
- progress.complete = false;
1418
- unreadable.add(room.locator);
1419
- list(room, false);
1420
- emitRooms();
1421
- return;
1422
- }
1423
- if (result.kind === 'revoked-authorization') {
1424
- await refuse('revoked-authorization');
1425
- return;
1426
- }
1427
- if (result.kind === 'scoped-refusal') {
1428
- await scoped(result, room, local);
1429
- progress.complete = false;
1430
- return;
1431
- }
1432
- if (result.kind === 'write-halted') {
1433
- journal.record(`outcome ${room.locator} ${head.id} write-halted`);
1434
- await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, lost));
1435
- if (serviceMode === 'open' && openSignals !== opensBefore)
1436
- continue;
1437
- haltedRefusal = true;
1438
- progress.complete = false;
1439
- return;
1440
- }
1441
- if (result.kind === 'conflicted' ||
1442
- result.kind === 'rejected' ||
1443
- result.kind === 'expired' ||
1444
- result.kind === 'retryable-refusal') {
1445
- if (result.kind === 'expired') {
1446
- const withheld = await advance(room, local, authorized);
1447
- if (withheld !== null)
1448
- return withhold(withheld, room, local, progress);
1449
- }
1450
- journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
1451
- (result.kind === 'conflicted' && result.roomVersion !== undefined
1452
- ? ` ${result.roomVersion}`
1453
- : ''));
1454
- if (result.kind !== 'expired')
1455
- journal.record(`refusal ${result.kind} ${result.code}`);
1456
- }
1457
- if (result.kind === 'retryable-refusal') {
1458
- const reset = result.reset;
1459
- progress.complete = false;
1460
- await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1461
- armTimer();
1462
- return;
1463
- }
1464
- if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
1465
- const left = leaving(head, result);
1466
- await exclusive(async () => {
1467
- if (!continuing())
1468
- return;
1469
- const current = replicas.get(room.locator);
1470
- if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
1471
- return;
1472
- }
1473
- const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1474
- leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1475
- (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1476
- ? { ...left, returned: null }
1477
- : left,
1478
- });
1479
- publishChangeSet(set);
1480
- moveTo({ ...status, ...attention() });
1481
- });
1482
- if (result.kind === 'expired')
1483
- continue;
1484
- const withheld = await advance(room, local, authorized);
1485
- if (withheld !== null)
1486
- return withhold(withheld, room, local, progress);
1487
- continue;
1488
- }
1489
- const kept = await adopted(room, local, head, result).finally(answered);
1490
- if (!kept) {
1491
- progress.complete = false;
1714
+ const current = await recordAt(room.locator);
1715
+ if (current === undefined)
1492
1716
  return;
1493
- }
1494
- }
1495
- }
1496
- function awaitingAnswer(locator) {
1497
- let release = () => { };
1498
- const awaited = new Promise((resolve) => {
1499
- release = resolve;
1717
+ const record = { ...current, name: room.name, handle: room.handle };
1718
+ await local.commit({
1719
+ rooms: [
1720
+ {
1721
+ locator: room.locator,
1722
+ key: record.key,
1723
+ record: storedRecord(record),
1724
+ composed: composedOf(composed(record)),
1725
+ },
1726
+ ],
1727
+ });
1728
+ emitRooms();
1500
1729
  });
1501
- answering.set(locator, awaited);
1502
- return () => {
1503
- if (answering.get(locator) === awaited)
1504
- answering.delete(locator);
1505
- release();
1506
- };
1507
1730
  }
1508
- async function retainModel(local, room, attached, next) {
1509
- const key = modelKey(room.locator, attached);
1510
- const previous = modelOf(room.locator, attached);
1511
- const heldBefore = replicas.get(room.locator)?.confirmed.record;
1512
- if (JSON.stringify(previous) === JSON.stringify(next) &&
1513
- (heldBefore === undefined ||
1514
- (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
1515
- JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
1516
- JSON.stringify(heldBefore.typedSelection) ===
1517
- JSON.stringify(room.typedSelection ?? null))))
1731
+ async function modelNow(locator) {
1732
+ return (await recordAt(locator))?.model ?? unheldModels.get(locator) ?? NO_MODEL;
1733
+ }
1734
+ async function retainModel(local, room, given) {
1735
+ const held = await recordAt(room.locator);
1736
+ const previous = held?.model ?? unheldModels.get(room.locator) ?? NO_MODEL;
1737
+ const next = given ?? previous;
1738
+ if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(held, room))
1518
1739
  return;
1519
- if (next.status === 'unavailable') {
1520
- invalidModels.add(key);
1521
- liveModels.delete(key);
1522
- }
1523
1740
  await exclusive(async () => {
1524
1741
  if (!continuing())
1525
1742
  return;
1526
- const held = replicas.get(room.locator);
1527
- if (held !== undefined) {
1528
- const declared = {
1529
- ...held.confirmed.record,
1743
+ const work = await workOf(room.locator);
1744
+ const current = work.replica;
1745
+ let changed = null;
1746
+ if (current !== undefined) {
1747
+ const record = {
1748
+ ...current.record,
1749
+ name: room.name,
1750
+ handle: room.handle,
1530
1751
  operations: room.operations,
1531
1752
  replica: room.replica,
1753
+ providerOwnedPaths: room.providerOwnedPaths,
1532
1754
  typedSelection: room.typedSelection ?? null,
1533
- ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1755
+ model: next,
1534
1756
  };
1535
- const reclassified = sameMembership(declared, held.confirmed.record)
1536
- ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
1537
- : readReclassified(declared, held.confirmed);
1538
- const record = reclassified.kind === 'staged'
1539
- ? declared
1540
- : {
1541
- ...declared,
1542
- typedSelection: held.confirmed.record.typedSelection,
1543
- };
1544
- const read = reclassified.kind === 'deferred'
1545
- ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
1546
- : reclassified;
1547
- await local.write({ next: record, previous: held.confirmed.record });
1548
- if (read.kind === 'staged') {
1549
- replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1550
- if (declarationRefusals.delete(room.locator)) {
1551
- sourceRefusals.delete(room.locator);
1552
- unreadable.delete(room.locator);
1553
- }
1554
- }
1555
- else if (read.kind === 'refused') {
1556
- replicas.set(room.locator, { ...held, confirmed: { ...held.confirmed, record } });
1557
- refusedSource(room, read);
1558
- declarationRefusals.add(room.locator);
1559
- }
1560
- }
1561
- models.set(key, next);
1562
- if (next.status === 'current') {
1563
- liveModels.set(key, JSON.stringify(next.snapshot));
1564
- invalidModels.delete(key);
1757
+ const confirmedLayer = await sliceAt(store, record, 'confirmed', current.queue, current.overlaid);
1758
+ const replayedLayer = await replayed(store, record, confirmedLayer, current.queue);
1759
+ const diff = await overlayDiff(record, current, replayedLayer.visible, replayedLayer.touched);
1760
+ const answer = await local.commit({
1761
+ rooms: [
1762
+ {
1763
+ locator: room.locator,
1764
+ key: record.key,
1765
+ record: storedRecord(record),
1766
+ roots: record.typedSelection?.roots ?? [],
1767
+ composed: composedOf(composed(record)),
1768
+ overlay: diff.overlay,
1769
+ },
1770
+ ],
1771
+ });
1772
+ preparedAfter(saver, room.locator, answer);
1773
+ changed = changeSet(room.locator, {
1774
+ documents: answer.changed[room.locator] ?? [],
1775
+ room: true,
1776
+ });
1565
1777
  }
1778
+ else
1779
+ unheldModels.set(room.locator, next);
1566
1780
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
1567
- if (held === undefined)
1781
+ if (changed === null)
1568
1782
  emitRooms();
1569
1783
  else
1570
- publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
1784
+ publishChangeSet(changed);
1571
1785
  });
1572
1786
  }
1573
1787
  async function scoped(result, room, local) {
1788
+ if (result.scope === 'content' && !result.retryable) {
1789
+ journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
1790
+ await loseRooms([room.locator]);
1791
+ return;
1792
+ }
1574
1793
  scopedRefusals.set(room.locator, {
1575
1794
  code: result.code,
1576
1795
  fields: result.fields,
@@ -1578,22 +1797,14 @@ export function composeRuntime(options) {
1578
1797
  });
1579
1798
  let scopedAway = room;
1580
1799
  if (result.scope === 'model')
1581
- await retainModel(local, room, 'collection', {
1800
+ await retainModel(local, room, {
1582
1801
  status: 'unavailable',
1583
1802
  reason: 'not-readable',
1584
1803
  });
1585
1804
  else if (result.scope !== 'operation') {
1586
1805
  unreadable.add(room.locator);
1587
- scopedAway = {
1588
- ...room,
1589
- replica: { ...room.replica, admission: 'unavailable' },
1590
- operations: {
1591
- ...room.operations,
1592
- ...(result.scope === 'content' ? { read: [] } : {}),
1593
- ...(result.scope === 'sync' ? { sync: [] } : {}),
1594
- },
1595
- };
1596
- await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
1806
+ scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
1807
+ await retainModel(local, scopedAway, null);
1597
1808
  }
1598
1809
  if (!continuing())
1599
1810
  return;
@@ -1601,131 +1812,182 @@ export function composeRuntime(options) {
1601
1812
  emitRooms();
1602
1813
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
1603
1814
  }
1604
- function heldRooms(listed) {
1815
+ async function name(locator) {
1816
+ const room = discovered.get(locator);
1817
+ if (!socket.streamed || room === undefined || !socket.isOpen() || !socket.isHanded())
1818
+ return;
1819
+ const [held] = await heldRooms([room]);
1820
+ if (held === undefined || !socket.isOpen() || !socket.isHanded())
1821
+ return;
1822
+ socket.name(held);
1823
+ }
1824
+ async function heldRooms(listed) {
1825
+ const stored = await storedRooms(listed.map((room) => room.locator));
1605
1826
  return listed.flatMap((room) => {
1606
- const record = replicas.get(room.locator)?.confirmed.record;
1607
- if (record === undefined || record.replica.selection !== room.replica.selection)
1827
+ const record = stored.get(room.locator)?.record;
1828
+ if (record === undefined) {
1829
+ return room.replica.admission === 'admitted'
1830
+ ? [{ locator: room.locator, roomVersion: null }]
1831
+ : [];
1832
+ }
1833
+ if (record.replica.selection !== room.replica.selection)
1608
1834
  return [];
1609
- return [
1610
- {
1611
- locator: room.locator,
1612
- roomVersion: record.roomVersion,
1613
- documents: record.documents,
1614
- revisions: record.revisions,
1615
- },
1616
- ];
1835
+ return [{ locator: room.locator, roomVersion: record.roomVersion }];
1617
1836
  });
1618
1837
  }
1619
- async function synchronize(listed, local, progress, attempt) {
1620
- const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1838
+ async function synchronize(listed, local, progress, attempt, live) {
1839
+ const heldNow = await storedRooms(listed.map(({ locator }) => locator));
1840
+ const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress, heldNow.get(room.locator)?.record)));
1841
+ if (!live())
1842
+ return;
1621
1843
  const ready = listed.filter((_, index) => {
1622
1844
  const outcome = prepared[index];
1623
1845
  return outcome?.status === 'fulfilled' && outcome.value;
1624
1846
  });
1847
+ const held = await heldRooms(ready);
1848
+ if (!live())
1849
+ return;
1625
1850
  if (attempt === 'opened') {
1626
- options.adapter.roomStream?.hold(heldRooms(ready));
1627
- handed = true;
1851
+ socket.hand(held);
1852
+ }
1853
+ const kept = attempt === 'unstreamed'
1854
+ ? new Set()
1855
+ : new Set((await storedRooms(ready.map(({ locator }) => locator))).keys());
1856
+ for (const room of ready) {
1857
+ if (attempt === 'opened' || kept.has(room.locator)) {
1858
+ if (attempt === 'authorized')
1859
+ await name(room.locator);
1860
+ continue;
1861
+ }
1862
+ void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
1628
1863
  }
1629
- const loaded = await Promise.allSettled(ready
1630
- .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1631
- .map((room) => loadRoom(room, local, progress)));
1632
- const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1864
+ const failed = prepared.find((room) => room.status === 'rejected');
1633
1865
  if (failed !== undefined)
1634
1866
  throw failed.reason;
1635
1867
  }
1636
- async function prepareRoom(room, local, progress) {
1868
+ async function prepareRoom(room, local, progress, prior) {
1637
1869
  if (closed || authorization === undefined) {
1638
1870
  progress.complete = false;
1639
1871
  return false;
1640
1872
  }
1641
- const prior = replicas.get(room.locator)?.confirmed.record;
1642
1873
  if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
1643
- list(prior, false);
1874
+ listHeld(room.locator);
1644
1875
  emitRooms();
1645
1876
  journal.record(`selection changed ${room.locator}`);
1646
1877
  progress.complete = false;
1647
1878
  return false;
1648
1879
  }
1649
- if (prior !== undefined)
1650
- await retainModel(local, room, 'collection', modelOf(room.locator));
1880
+ if (prior !== undefined && !declaredAsHeld(prior, room))
1881
+ await retainModel(local, room, null);
1882
+ if (prior !== undefined && (prior.name !== room.name || prior.handle !== room.handle)) {
1883
+ await keepNaming(local, room);
1884
+ }
1651
1885
  if (discoveryPaused || authorization === undefined)
1652
1886
  return false;
1653
- if (room.replica.admission !== 'admitted') {
1654
- if (replicas.has(room.locator))
1655
- progress.complete = false;
1656
- return false;
1657
- }
1658
- return true;
1887
+ return room.replica.admission === 'admitted';
1659
1888
  }
1660
- async function caughtUp(room, local, progress) {
1661
- const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1662
- if (held === undefined || authorization === undefined)
1889
+ async function caughtUp(room, local, progress, live) {
1890
+ if (options.adapter.roomStream !== undefined || authorization === undefined)
1663
1891
  return;
1664
- const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1665
- if (!behind)
1892
+ if (!live() || !(await behind(installer, room)))
1666
1893
  return;
1667
- const withheld = await advance(room, local, authorization);
1894
+ const withheld = await advance(room, local, authorization, live);
1668
1895
  if (withheld !== null)
1669
1896
  await withhold(withheld, room, local, progress);
1670
1897
  }
1671
- function revoked(locator) {
1672
- return ['collection', 'typed-selection'].some((attached) => {
1673
- const model = modelOf(locator, attached);
1674
- return model.status === 'unavailable' && model.reason === 'revoked';
1675
- });
1898
+ function revoked(model) {
1899
+ return model?.status === 'unavailable' && model.reason === 'revoked';
1676
1900
  }
1677
- async function loadRoom(room, local, progress) {
1678
- if (discoveryPaused || authorization === undefined)
1679
- return;
1680
- if (replicas.has(room.locator) && revoked(room.locator)) {
1681
- await reload(room, local, progress);
1901
+ async function loadRoom(room, local, progress, live, unheld) {
1902
+ if (!live() || authorization === undefined)
1682
1903
  return;
1904
+ const prior = unheld === undefined ? await recordAt(room.locator) : undefined;
1905
+ if (prior !== undefined) {
1906
+ if (!(await prepareRoom(room, local, progress, prior)))
1907
+ return;
1908
+ if (revoked(prior.model)) {
1909
+ await reload(room, local, progress, live);
1910
+ return;
1911
+ }
1683
1912
  }
1684
- const withheld = await advance(room, local, authorization);
1913
+ const withheld = await advance(room, local, authorization, live, unheld);
1685
1914
  if (withheld !== null) {
1686
1915
  await withhold(withheld, room, local, progress);
1687
1916
  return;
1688
1917
  }
1689
- await caughtUp(room, local, progress);
1918
+ await caughtUp(room, local, progress, live);
1919
+ }
1920
+ async function reload(room, local, progress, live) {
1921
+ try {
1922
+ if (live())
1923
+ reloadsDue.delete(room.locator);
1924
+ if (!live() || authorization === undefined)
1925
+ return;
1926
+ const prior = await recordAt(room.locator);
1927
+ const from = standingAt(store, room.locator, prior);
1928
+ const bootstrapped = await pullBootstrap(installer, authorization, room, local, live, prior);
1929
+ if (bootstrapped.kind !== 'staged') {
1930
+ if (live())
1931
+ await withhold(bootstrapped, room, local, progress);
1932
+ return;
1933
+ }
1934
+ await publish(installer, local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
1935
+ await caughtUp(room, local, progress, live);
1936
+ }
1937
+ finally {
1938
+ caughtUpTo(installer, room.locator);
1939
+ }
1940
+ }
1941
+ async function asked(authorized) {
1942
+ try {
1943
+ return (await options.adapter.discover?.(authorized)) ?? { kind: 'transport-uncertainty' };
1944
+ }
1945
+ catch {
1946
+ return { kind: 'transport-uncertainty' };
1947
+ }
1948
+ }
1949
+ function malformed(listed) {
1950
+ for (const room of listed) {
1951
+ if (room.operations === undefined ||
1952
+ !['write', 'erase'].every((operation) => Array.isArray(room.operations[operation]) &&
1953
+ room.operations[operation].every((scope) => typeof scope === 'string')) ||
1954
+ room.replica === undefined ||
1955
+ typeof room.replica.selection !== 'string' ||
1956
+ room.replica.selection.length === 0 ||
1957
+ !['admitted', 'unavailable'].includes(room.replica.admission) ||
1958
+ !['locked', 'open'].includes(room.replica.offline)) {
1959
+ return `invalid model or replica declaration ${room.locator}`;
1960
+ }
1961
+ if (!declaredSelection(room))
1962
+ return `invalid typed selection declaration ${room.locator}`;
1963
+ }
1964
+ return null;
1690
1965
  }
1691
- async function reload(room, local, progress) {
1692
- if (discoveryPaused || authorization === undefined)
1693
- return;
1694
- const bootstrapped = await pullBootstrap(authorization, room, local);
1695
- if (!continuing())
1966
+ async function signIn(local, signedIn) {
1967
+ if (JSON.stringify(signedIn) === JSON.stringify((await local.facts()).member))
1696
1968
  return;
1697
- if (bootstrapped.kind !== 'staged') {
1698
- await withhold(bootstrapped, room, local, progress);
1699
- return;
1700
- }
1701
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1702
- await caughtUp(room, local, progress);
1969
+ await local.fact('member', signedIn);
1970
+ emitMember(signedIn);
1703
1971
  }
1704
- async function discoverRooms(local, progress, attempt) {
1972
+ async function discoverRooms(local, progress, attempt, live, found) {
1705
1973
  if (closed || authorization === undefined) {
1706
1974
  notifyReady();
1707
1975
  return;
1708
1976
  }
1709
- moveTo(transferring(status));
1710
- let result;
1711
- try {
1712
- result = await options.adapter.discover(authorization);
1713
- }
1714
- catch {
1715
- result = { kind: 'transport-uncertainty' };
1716
- }
1717
- if (closed) {
1977
+ moveTo(transferring());
1978
+ discoveries += 1;
1979
+ const started = discoveries;
1980
+ const result = found ?? (await asked(authorization));
1981
+ if (result.kind === 'discovery')
1982
+ roomsNamed = true;
1983
+ if (!live()) {
1984
+ progress.complete = false;
1718
1985
  notifyReady();
1719
1986
  return;
1720
1987
  }
1721
1988
  journal.record(`connection discover ${result.kind}`);
1722
1989
  if (result.kind === 'transport-uncertainty') {
1723
- for (const room of rooms.values()) {
1724
- await retainModel(local, room, 'collection', cached(modelOf(room.locator)));
1725
- if (room.typedSelection !== null) {
1726
- await retainModel(local, room, 'typed-selection', cached(modelOf(room.locator, 'typed-selection')));
1727
- }
1728
- }
1990
+ await cachedModels(local);
1729
1991
  progress.reachable = false;
1730
1992
  progress.complete = false;
1731
1993
  notifyReady();
@@ -1746,46 +2008,26 @@ export function composeRuntime(options) {
1746
2008
  notifyReady();
1747
2009
  return;
1748
2010
  }
1749
- for (const room of result.rooms) {
1750
- if (room.operations === undefined ||
1751
- !['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
1752
- room.operations[operation].every((scope) => typeof scope === 'string')) ||
1753
- room.replica === undefined ||
1754
- typeof room.replica.selection !== 'string' ||
1755
- room.replica.selection.length === 0 ||
1756
- !['admitted', 'unavailable'].includes(room.replica.admission)) {
1757
- journal.record(`invalid model or replica declaration ${room.locator}`);
1758
- await refuse('missing-required-guarantee');
1759
- notifyReady();
1760
- return;
1761
- }
1762
- if (!declaredSelection(room)) {
1763
- journal.record(`invalid typed selection declaration ${room.locator}`);
1764
- await refuse('missing-required-guarantee');
1765
- notifyReady();
1766
- return;
1767
- }
2011
+ const invalid = malformed(result.rooms);
2012
+ if (invalid !== null) {
2013
+ journal.record(invalid);
2014
+ await refuse('missing-required-guarantee');
2015
+ notifyReady();
2016
+ return;
1768
2017
  }
1769
2018
  if (refusal !== null) {
1770
2019
  refusal = null;
1771
2020
  emitRefusal();
1772
2021
  }
1773
- const previousCapabilities = capabilities;
1774
- capabilities = result.capabilities;
1775
- notifyReady();
1776
- if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1777
- JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
1778
- limits = result.providerLimits;
1779
- await local.declare(limits, capabilities);
1780
- }
1781
- const signedIn = result.member ?? null;
1782
- if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
1783
- await local.keepMember(signedIn);
1784
- member = signedIn;
1785
- emitMember();
2022
+ const held = await local.facts();
2023
+ if (JSON.stringify(result.providerLimits) !== JSON.stringify(held.providerLimits) ||
2024
+ JSON.stringify(result.capabilities) !== JSON.stringify(held.capabilities)) {
2025
+ await local.fact('providerLimits', result.providerLimits);
2026
+ await local.fact('capabilities', result.capabilities);
1786
2027
  }
1787
- await applyDiscovery(result.rooms, local);
1788
- await synchronize(result.rooms, local, progress, attempt);
2028
+ notifyReady();
2029
+ await signIn(local, result.member ?? null);
2030
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
1789
2031
  }
1790
2032
  async function ensureAuthorization() {
1791
2033
  if (authorization !== undefined)
@@ -1799,16 +2041,66 @@ export function composeRuntime(options) {
1799
2041
  throw error;
1800
2042
  }
1801
2043
  }
1802
- function task(work) {
2044
+ async function admittedRead(work) {
2045
+ while (roomReads.free === 0) {
2046
+ await new Promise((admit) => roomReads.waiting.push(admit));
2047
+ }
2048
+ roomReads.free -= 1;
2049
+ try {
2050
+ await work();
2051
+ }
2052
+ finally {
2053
+ roomReads.free += 1;
2054
+ roomReads.waiting.shift()?.();
2055
+ }
2056
+ }
2057
+ function inRoom(locator, work) {
2058
+ roomWork += 1;
2059
+ const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
2060
+ try {
2061
+ return await work(continuing);
2062
+ }
2063
+ finally {
2064
+ roomWork -= 1;
2065
+ }
2066
+ });
2067
+ const chain = ran.then(() => undefined, () => undefined);
2068
+ roomLanes.set(locator, chain);
2069
+ working.add(chain);
2070
+ void chain.finally(() => {
2071
+ working.delete(chain);
2072
+ if (roomLanes.get(locator) === chain)
2073
+ roomLanes.delete(locator);
2074
+ });
2075
+ return ran;
2076
+ }
2077
+ function roomTask(locator, work) {
1803
2078
  if (stopping)
1804
- return chain;
1805
- chain = chain.then(work, work).catch(() => undefined);
1806
- return chain;
2079
+ return Promise.resolve();
2080
+ return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
1807
2081
  }
1808
- function sync(attempt) {
1809
- return task(async () => {
1810
- if (closed || stopping || discoveryPaused) {
1811
- notifyReady();
2082
+ function advanced(room, local, authorized) {
2083
+ return inRoom(room.locator, (live) => advance(room, local, authorized, live));
2084
+ }
2085
+ function task(lane, work) {
2086
+ if (stopping)
2087
+ return Promise.resolve();
2088
+ const held = lanes.get(lane);
2089
+ const opened = socket.generation();
2090
+ const current = held?.generation === opened ? held : { generation: opened, chain: Promise.resolve() };
2091
+ const turn = current.generation;
2092
+ const live = () => turn === socket.generation() && continuing();
2093
+ const run = current.chain.then(() => work(live)).catch(() => undefined);
2094
+ current.chain = run;
2095
+ lanes.set(lane, current);
2096
+ working.add(run);
2097
+ void run.finally(() => working.delete(run));
2098
+ return run;
2099
+ }
2100
+ function sync(attempt, found) {
2101
+ return task(OPENING_LANE, async (live) => {
2102
+ if (!live()) {
2103
+ readyFromStore();
1812
2104
  return;
1813
2105
  }
1814
2106
  const progress = { complete: true, reachable: true };
@@ -1817,40 +2109,100 @@ export function composeRuntime(options) {
1817
2109
  streamed = progress;
1818
2110
  complete = false;
1819
2111
  recorded = false;
1820
- moveTo(transferring(status));
1821
- handed = false;
2112
+ moveTo(transferring());
1822
2113
  try {
1823
2114
  const local = await openLocal();
1824
- if (closed)
2115
+ if (!live())
1825
2116
  return;
1826
2117
  await ensureAuthorization();
1827
- if (closed)
2118
+ if (!live())
1828
2119
  return;
1829
- await discoverRooms(local, progress, attempt);
2120
+ await discoverRooms(local, progress, attempt, live, found);
1830
2121
  }
1831
2122
  catch {
1832
2123
  progress.complete = false;
1833
- if (attempt === 'opened' && !handed)
2124
+ if (attempt === 'opened' && !socket.isHanded())
1834
2125
  progress.reachable = false;
1835
2126
  }
1836
2127
  finally {
1837
2128
  notifyReady();
1838
2129
  }
2130
+ if (!live())
2131
+ return;
1839
2132
  if (attempt !== 'opened') {
1840
- await finishOpening(progress);
2133
+ await finishOpening(progress, live);
1841
2134
  return;
1842
2135
  }
1843
- if (handed || closed || progress.reachable)
2136
+ if (socket.isHanded() || closed || progress.reachable)
1844
2137
  return;
1845
2138
  underway = null;
1846
- journal.record('connection reopen');
1847
- await rest(progress);
1848
- options.adapter.roomStream?.reopen();
2139
+ await rest(progress, live);
2140
+ });
2141
+ }
2142
+ function resynchronize() {
2143
+ return task(OPENING_LANE, async (live) => {
2144
+ if (!live() || !socket.isAnnounced())
2145
+ return;
2146
+ const progress = { complete: true, reachable: true };
2147
+ moveTo(transferring());
2148
+ try {
2149
+ const local = await openLocal();
2150
+ if (!live())
2151
+ return;
2152
+ await synchronize([...discovered.values()], local, progress, 'unstreamed', live);
2153
+ }
2154
+ catch {
2155
+ progress.complete = false;
2156
+ }
2157
+ if (live())
2158
+ await finishOpening(progress, live);
2159
+ });
2160
+ }
2161
+ function countAuthorityChange() {
2162
+ authorityChanges += 1;
2163
+ for (const listener of authorityChangeListeners)
2164
+ listener(authorityChanges);
2165
+ lastAnswers.again();
2166
+ }
2167
+ function changeAuthority(change) {
2168
+ return task(OPENING_LANE, async (live) => {
2169
+ if (!live() || !socket.isAnnounced())
2170
+ return;
2171
+ countAuthorityChange();
2172
+ const progress = { complete: true, reachable: true };
2173
+ moveTo(transferring());
2174
+ try {
2175
+ const local = await openLocal();
2176
+ if (!live())
2177
+ return;
2178
+ const invalid = malformed(change.rooms);
2179
+ if (invalid !== null) {
2180
+ journal.record(invalid);
2181
+ await refuse('missing-required-guarantee');
2182
+ return;
2183
+ }
2184
+ if (change.member !== undefined)
2185
+ await signIn(local, change.member);
2186
+ discoveries += 1;
2187
+ const listed = await applyDiscovery(change.rooms, discoveries, change.left);
2188
+ await synchronize(listed, local, progress, 'authorized', live);
2189
+ }
2190
+ catch {
2191
+ progress.complete = false;
2192
+ }
2193
+ if (!live())
2194
+ return;
2195
+ if (!progress.complete)
2196
+ complete = false;
2197
+ await rest(progress, live);
1849
2198
  });
1850
2199
  }
1851
- function closedSocket(revoked) {
1852
- const opened = socketOpen;
1853
- socketOpen = false;
2200
+ function closedSocket(revoked, lost, opened) {
2201
+ notifyReady();
2202
+ unreadied();
2203
+ connectionLost();
2204
+ if (lost)
2205
+ conversations.lost();
1854
2206
  if (revoked) {
1855
2207
  void refuse('revoked-authorization');
1856
2208
  return;
@@ -1861,201 +2213,366 @@ export function composeRuntime(options) {
1861
2213
  moveTo({
1862
2214
  ...status,
1863
2215
  state: status.state === 'synced' ? 'syncing' : status.state,
1864
- providerUnreachable: status.providerUnreachable || !opened,
2216
+ providerUnreachable: status.providerUnreachable || (!opened && lost),
1865
2217
  });
2218
+ void closing(opened);
1866
2219
  }
1867
- function applyOpening(expired) {
1868
- return task(async () => {
1869
- const progress = underway;
1870
- underway = null;
1871
- if (closed || stopping)
2220
+ function closing(opened) {
2221
+ return task(OPENING_LANE, async (live) => {
2222
+ if (!live())
2223
+ return;
2224
+ const local = await openLocal();
2225
+ if (!opened && !socket.isOpen())
2226
+ await cachedModels(local);
2227
+ await rest(null, live);
2228
+ });
2229
+ }
2230
+ async function cachedModels(local) {
2231
+ const held = await storedRooms();
2232
+ await settleModels(listedLocators(held).flatMap((locator) => {
2233
+ const room = discovered.get(locator) ?? declarationOf(locator, held.get(locator))?.room;
2234
+ return room === undefined ? [] : [room];
2235
+ }), 'cached', local);
2236
+ }
2237
+ function caughtUpRooms(expired) {
2238
+ const progress = underway;
2239
+ underway = null;
2240
+ socket.named();
2241
+ const cycle = progress ?? { complete: true, reachable: true };
2242
+ replaced(store, expired);
2243
+ const installing = store.installs.underway();
2244
+ const held = progress === null ? storedRooms(expired) : storedRooms();
2245
+ const reloading = progress === null
2246
+ ? Promise.resolve([])
2247
+ : held.then((stored) => owedReloads(expired, stored));
2248
+ const replacing = Promise.all([held, reloading]).then(([stored, owed]) => reloadNow((room) => expired.includes(room.locator) || owed.includes(room.locator), cycle, stored, installing));
2249
+ return task(OPENING_LANE, async (live) => {
2250
+ if (!live())
1872
2251
  return;
1873
2252
  if (progress === null) {
1874
- await reloadExpired(expired);
2253
+ await replacing;
2254
+ await rest(cycle, live);
1875
2255
  return;
1876
2256
  }
1877
2257
  try {
1878
2258
  const local = await openLocal();
1879
- const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1880
- const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1881
- const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
2259
+ await replacing;
1882
2260
  if (progress.reachable) {
1883
- for (const room of held)
1884
- if (!reloading.includes(room))
1885
- await confirmModels(room, local);
2261
+ const stored = await held;
2262
+ const owed = await reloading;
2263
+ await settleModels([...discovered.values()].filter((room) => stored.has(room.locator) &&
2264
+ !expired.includes(room.locator) &&
2265
+ !owed.includes(room.locator)), 'current', local);
1886
2266
  }
1887
- const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1888
- if (failed !== undefined)
1889
- throw failed.reason;
1890
2267
  }
1891
2268
  catch {
1892
2269
  progress.complete = false;
1893
2270
  progress.reachable = false;
1894
2271
  }
1895
- await finishOpening(progress);
2272
+ if (live())
2273
+ await finishOpening(progress, live);
1896
2274
  });
1897
2275
  }
1898
- async function reloadExpired(expired) {
1899
- const progress = { complete: true, reachable: true };
2276
+ function owedReloads(expired, held) {
2277
+ const reloading = [...discovered.values()]
2278
+ .filter((room) => !expired.includes(room.locator) && revoked(held.get(room.locator)?.record.model))
2279
+ .map((room) => room.locator);
2280
+ for (const locator of reloading)
2281
+ catching(store, locator);
2282
+ recoverReads();
2283
+ return reloading;
2284
+ }
2285
+ async function reloadNow(replaced, progress, held, installing) {
2286
+ const reloaded = [...discovered.values()].filter((room) => held.has(room.locator) && !installing.has(room.locator) && replaced(room));
2287
+ const acquired = [...discovered.values()].filter((room) => (!held.has(room.locator) || installing.has(room.locator)) &&
2288
+ room.replica.admission === 'admitted' &&
2289
+ replaced(room));
2290
+ for (const room of reloaded)
2291
+ reloadsDue.add(room.locator);
1900
2292
  try {
1901
2293
  const local = await openLocal();
1902
- await Promise.allSettled([...discovered.values()]
1903
- .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1904
- .map((room) => reload(room, local, progress)));
2294
+ const kept = acquired.length === 0 ? new Set() : (await local.held()).staged;
2295
+ for (const room of reloaded) {
2296
+ void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
2297
+ }
2298
+ for (const room of acquired) {
2299
+ const unheld = installing.has(room.locator)
2300
+ ? undefined
2301
+ : { staged: kept.has(room.locator) };
2302
+ void roomTask(room.locator, (live) => loadRoom(room, local, progress, live, unheld)).then(() => rest(progress));
2303
+ }
1905
2304
  }
1906
2305
  catch {
2306
+ for (const room of reloaded)
2307
+ reloadsDue.delete(room.locator);
1907
2308
  progress.reachable = false;
1908
2309
  }
1909
- await rest(progress);
1910
2310
  }
1911
- async function confirmModels(room, local) {
1912
- for (const attached of ['collection', 'typed-selection']) {
1913
- const model = modelOf(room.locator, attached);
1914
- if (model.status === 'cached') {
1915
- await retainModel(local, room, attached, { ...model, status: 'current' });
2311
+ async function settleModels(held, status, local) {
2312
+ const owed = (model) => model.status !== 'unavailable' && model.status !== status;
2313
+ const declaredAnew = await exclusive(async () => {
2314
+ if (!continuing())
2315
+ return [];
2316
+ const stored = await storedRooms(held.map(({ locator }) => locator));
2317
+ const unchanged = held.filter((room) => {
2318
+ const record = stored.get(room.locator)?.record;
2319
+ return (owed(record?.model ?? unheldModels.get(room.locator) ?? NO_MODEL) &&
2320
+ declaredAsHeld(record, room));
2321
+ });
2322
+ const records = unchanged.flatMap((room) => {
2323
+ const record = stored.get(room.locator)?.record;
2324
+ return record === undefined
2325
+ ? []
2326
+ : [{ ...record, model: { ...record.model, status } }];
2327
+ });
2328
+ if (records.length > 0) {
2329
+ try {
2330
+ await local.commit({
2331
+ rooms: records.map((record) => ({
2332
+ locator: record.locator,
2333
+ key: record.key,
2334
+ record: storedRecord(record),
2335
+ composed: composedOf(composed(record)),
2336
+ })),
2337
+ });
2338
+ }
2339
+ catch {
2340
+ journal.record(`model ${status} persistence failed`);
2341
+ }
2342
+ }
2343
+ for (const room of unchanged) {
2344
+ const unheld = unheldModels.get(room.locator);
2345
+ if (!stored.has(room.locator) && unheld !== undefined) {
2346
+ unheldModels.set(room.locator, { ...unheld, status });
2347
+ }
2348
+ list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
1916
2349
  }
2350
+ if (unchanged.length > 0)
2351
+ emitRooms();
2352
+ return held.filter((room) => {
2353
+ const record = stored.get(room.locator)?.record;
2354
+ return (!unchanged.includes(room) &&
2355
+ owed(record?.model ?? unheldModels.get(room.locator) ?? NO_MODEL));
2356
+ });
2357
+ });
2358
+ for (const room of declaredAnew) {
2359
+ const model = await modelNow(room.locator);
2360
+ if (owed(model))
2361
+ await retainModel(local, room, { ...model, status });
1917
2362
  }
1918
2363
  }
1919
- function submitLater(locator) {
1920
- if (localSaves)
1921
- return;
1922
- const running = submitting.get(locator);
1923
- if (running !== undefined) {
1924
- running.again = true;
2364
+ function repairLater(locator, when) {
2365
+ if (!socket.isOpen() || repairing.has(locator))
1925
2366
  return;
2367
+ void recovery.attempt(`repair ${locator}`, (signal) => repairPass(locator, signal), when);
2368
+ }
2369
+ async function repairPass(locator, signal) {
2370
+ const room = discovered.get(locator);
2371
+ if (!socket.isOpen() || closed || stopping || discoveryPaused || !stale.has(locator))
2372
+ return 'done';
2373
+ if (room === undefined || room.replica.admission !== 'admitted')
2374
+ return 'done';
2375
+ const progress = { complete: true, reachable: true };
2376
+ repairing.add(locator);
2377
+ try {
2378
+ const local = await openLocal();
2379
+ await roomTask(locator, async (live) => {
2380
+ if (!live() || authorization === undefined)
2381
+ return;
2382
+ const withheld = await advance(room, local, authorization, live);
2383
+ if (withheld !== null && live())
2384
+ await withhold(withheld, room, local, progress);
2385
+ });
1926
2386
  }
1927
- const run = { again: false, done: Promise.resolve() };
1928
- submitting.set(locator, run);
1929
- run.done = (async () => {
1930
- try {
1931
- do {
1932
- run.again = false;
1933
- const room = discovered.get(locator);
1934
- if (closed || stopping || discoveryPaused || room === undefined)
1935
- return;
1936
- const progress = { complete: true, reachable: true };
1937
- try {
1938
- await ensureAuthorization();
1939
- if (authorization === undefined)
1940
- return;
1941
- await submitRoom(room, await openLocal(), authorization, progress);
1942
- }
1943
- catch {
1944
- progress.reachable = false;
1945
- }
1946
- armTimer();
1947
- await rest(progress);
1948
- } while (run.again);
1949
- }
1950
- finally {
1951
- submitting.delete(locator);
1952
- }
1953
- })();
2387
+ catch {
2388
+ stale.add(locator);
2389
+ progress.reachable = false;
2390
+ }
2391
+ finally {
2392
+ repairing.delete(locator);
2393
+ }
2394
+ if (signal.aborted)
2395
+ return 'done';
2396
+ await rest(progress);
2397
+ if (!stale.has(locator)) {
2398
+ if ((await (await openLocal()).order(locator)).length > 0)
2399
+ submitLater(accepting, locator);
2400
+ return 'done';
2401
+ }
2402
+ if (!repairable.has(locator))
2403
+ return 'done';
2404
+ const reset = repairable.get(locator);
2405
+ return reset === undefined ? 'again' : { at: Date.parse(reset) };
2406
+ }
2407
+ function resendLater() {
2408
+ if (!socket.isOpen())
2409
+ return;
2410
+ void recovery.attempt('resend', resendPass, { again: true });
1954
2411
  }
1955
- function submitAll() {
1956
- for (const [locator, replica] of replicas) {
1957
- if (replica.queue.length > 0)
1958
- submitLater(locator);
2412
+ async function resendPass(signal) {
2413
+ if (!socket.isOpen() || closed || stopping || discoveryPaused)
2414
+ return 'done';
2415
+ const reading = { complete: true, reachable: true };
2416
+ try {
2417
+ await conversations.resend(reading);
2418
+ }
2419
+ catch {
2420
+ reading.reachable = false;
1959
2421
  }
2422
+ if (signal.aborted)
2423
+ return 'done';
2424
+ return reading.reachable && reading.complete ? 'done' : 'again';
1960
2425
  }
1961
- async function finishOpening(progress) {
2426
+ async function finishOpening(progress, live) {
1962
2427
  try {
1963
- if (closed || discoveryPaused) {
2428
+ if (!live()) {
1964
2429
  progress.complete = false;
1965
2430
  return;
1966
2431
  }
1967
- submitAll();
2432
+ await submitAll(accepting);
2433
+ for (const locator of stale)
2434
+ if (repairable.has(locator))
2435
+ repairLater(locator, {});
1968
2436
  await conversations.reconcile();
1969
- if (status.providerUnreachable)
1970
- conversations.reconnected();
1971
2437
  const reading = { complete: true, reachable: true };
1972
2438
  await conversations.catchUp(reading);
1973
- if (!reading.reachable)
2439
+ if (progress.reachable && reading.reachable)
2440
+ await conversations.resend(reading);
2441
+ if (progress.reachable)
2442
+ void recovery.attempt('calls', resendCalls);
2443
+ if (!reading.reachable) {
1974
2444
  progress.reachable = false;
2445
+ resendLater();
2446
+ }
1975
2447
  }
1976
2448
  catch {
1977
2449
  progress.complete = false;
1978
2450
  }
1979
2451
  finally {
1980
- complete = progress.complete;
1981
- await rest(progress);
2452
+ if (live()) {
2453
+ complete = progress.complete;
2454
+ await rest(progress, live);
2455
+ }
1982
2456
  }
1983
2457
  }
1984
- async function rest(progress) {
1985
- if (closed)
2458
+ async function rest(progress, live = () => !closed) {
2459
+ if (!live())
1986
2460
  return;
1987
- let lastCompleted = status.lastCompleted;
1988
- let synced = complete && stale.size === 0 && !pending();
2461
+ const settled = () => complete && roomWork === 0 && !socket.naming() && stale.size === 0;
2462
+ let synced = settled();
2463
+ if (synced && !recorded) {
2464
+ const queued = await queuedWork().catch(() => true);
2465
+ if (!live())
2466
+ return;
2467
+ synced = !queued && settled();
2468
+ }
1989
2469
  if (synced && !recorded) {
1990
2470
  const completedAt = new Date().toISOString();
1991
2471
  try {
1992
- await (await openLocal()).complete(completedAt);
2472
+ await (await openLocal()).fact('lastCompleted', completedAt);
2473
+ if (!live())
2474
+ return;
1993
2475
  recorded = true;
1994
- lastCompleted = completedAt;
1995
2476
  }
1996
2477
  catch {
1997
2478
  synced = false;
1998
2479
  }
1999
2480
  }
2000
2481
  moveTo({
2001
- state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
2002
- lastCompleted,
2003
- providerUnreachable: !progress.reachable,
2004
- ...attention(),
2482
+ state: synced && complete && roomWork === 0 && !socket.naming() ? 'synced' : 'syncing',
2483
+ providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
2484
+ resting: true,
2005
2485
  });
2006
2486
  }
2007
- function applyRoom(locator, roomVersion) {
2008
- announced.set(locator, roomVersion);
2009
- return task(async () => {
2010
- await answering.get(locator);
2011
- if (closed || stopping || discoveryPaused || authorization === undefined)
2012
- return;
2013
- const room = discovered.get(locator);
2014
- const held = replicas.get(locator);
2015
- if (room === undefined || room.replica.admission !== 'admitted')
2016
- return;
2017
- if (held !== undefined &&
2018
- (held.confirmed.record.replica.selection !== room.replica.selection ||
2019
- options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
2020
- return;
2021
- }
2022
- const during = underway;
2023
- const progress = during ?? { complete: true, reachable: true };
2024
- if (during === null)
2025
- moveTo(transferring(status));
2487
+ function applyRoom(change) {
2488
+ const { locator } = change;
2489
+ if (stopping)
2490
+ return Promise.resolve();
2491
+ return inRoom(locator, async (live) => {
2492
+ await socket.answered(locator);
2493
+ const progress = underway ?? { complete: true, reachable: true };
2026
2494
  try {
2027
2495
  const local = await openLocal();
2028
- const withheld = await advance(room, local, authorization);
2029
- if (withheld !== null)
2030
- await withhold(withheld, room, local, progress);
2496
+ for (;;) {
2497
+ if (!live())
2498
+ return null;
2499
+ const room = discovered.get(locator);
2500
+ if (room === undefined || room.replica.admission !== 'admitted')
2501
+ return null;
2502
+ const held = await recordAt(locator);
2503
+ if (held === undefined) {
2504
+ if (change.from !== null)
2505
+ return null;
2506
+ await installCarried(room, local, change, live, progress);
2507
+ return progress;
2508
+ }
2509
+ if (held.replica.selection !== room.replica.selection)
2510
+ return null;
2511
+ const at = held.roomVersion;
2512
+ if (options.adapter.reached(locator, at, change.roomVersion))
2513
+ return progress;
2514
+ if (change.from === null || !options.adapter.reached(locator, at, change.from)) {
2515
+ await name(locator);
2516
+ return progress;
2517
+ }
2518
+ if (underway === null)
2519
+ moveTo(transferring());
2520
+ const writing = () => live() && !full;
2521
+ const refreshed = await store.ahead((hold) => pullRefresh(installer, authorization ?? {}, held, writing, hold, () => change.read()));
2522
+ if (refreshed.kind !== 'advanced')
2523
+ return progress;
2524
+ const landing = {
2525
+ kind: 'refreshed',
2526
+ from: refreshed.from,
2527
+ remodelled: refreshed.remodelled,
2528
+ };
2529
+ if (await publish(installer, local, room, refreshed.record, landing, writing)) {
2530
+ stale.delete(locator);
2531
+ if (!declaredAsHeld(held, room))
2532
+ await retainModel(local, room, null);
2533
+ return progress;
2534
+ }
2535
+ }
2031
2536
  }
2032
2537
  catch {
2538
+ if (!stale.has(locator))
2539
+ void name(locator);
2033
2540
  stale.add(locator);
2541
+ return progress;
2034
2542
  }
2035
- finally {
2036
- if (during === null)
2037
- await rest(progress);
2038
- }
2039
- });
2543
+ }).then((progress) => rest(progress), () => undefined);
2040
2544
  }
2041
- function observeStream() {
2042
- const stream = options.adapter.roomStream;
2043
- if (stream === undefined || withdrawStream !== undefined)
2545
+ async function installCarried(room, local, change, live, progress) {
2546
+ if (authorization === undefined)
2044
2547
  return;
2045
- withdrawStream = stream.observe((event) => {
2046
- if (closed || stopping)
2047
- return;
2048
- if (event.kind === 'opened') {
2049
- socketOpen = true;
2050
- void sync('opened');
2051
- }
2052
- else if (event.kind === 'closed')
2053
- closedSocket(event.revoked);
2054
- else if (event.kind === 'opening')
2055
- void applyOpening(event.expired);
2056
- else
2057
- void applyRoom(event.locator, event.roomVersion);
2058
- });
2548
+ if (underway === null)
2549
+ moveTo(transferring());
2550
+ const prior = await recordAt(room.locator);
2551
+ const from = standingAt(store, room.locator, prior);
2552
+ const page = await change.read();
2553
+ const carried = {
2554
+ bootstrap: () => Promise.resolve({
2555
+ kind: 'bootstrap',
2556
+ roomVersion: page.roomVersion,
2557
+ ...(page.models === undefined ? {} : { models: page.models }),
2558
+ documents: page.documents,
2559
+ facts: page.facts,
2560
+ revisions: page.revisions,
2561
+ ...(page.fileIds === undefined ? {} : { fileIds: page.fileIds }),
2562
+ ...(page.created === undefined ? {} : { created: page.created }),
2563
+ changes: page.changes,
2564
+ binaryFiles: page.binaryFiles,
2565
+ ...(page.folders === undefined ? {} : { folders: page.folders }),
2566
+ }),
2567
+ reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
2568
+ };
2569
+ const bootstrapped = await pullBootstrap(installer, authorization, room, local, live, prior, carried);
2570
+ if (bootstrapped.kind !== 'staged') {
2571
+ if (live())
2572
+ await withhold(bootstrapped, room, local, progress);
2573
+ return;
2574
+ }
2575
+ await publish(installer, local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
2059
2576
  }
2060
2577
  function writesHeld() {
2061
2578
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -2070,37 +2587,35 @@ export function composeRuntime(options) {
2070
2587
  haltedRefusal = false;
2071
2588
  if (mode !== serviceMode) {
2072
2589
  serviceMode = mode;
2590
+ queueReadiness();
2073
2591
  journal.record(`service mode ${mode}`);
2074
2592
  for (const listener of serviceModeListeners)
2075
2593
  listener(mode);
2076
2594
  }
2077
2595
  if (releasing)
2078
- submitAll();
2596
+ submitAll(accepting);
2079
2597
  }
2080
- function observeConversations() {
2081
- const stream = options.adapter.conversations;
2082
- if (stream === undefined || withdrawConversations !== undefined)
2083
- return;
2084
- withdrawConversations = stream.observe((event) => {
2085
- if (conversations.receive(event)) {
2086
- void ready.then(scheduleCurrency);
2087
- return;
2088
- }
2089
- if (event.kind !== 'gap' || conversations.current())
2090
- return;
2091
- show();
2092
- scheduleCurrency();
2093
- });
2598
+ function haltedSince(opens) {
2599
+ if (serviceMode === 'open' && openSignals !== opens)
2600
+ return false;
2601
+ haltedRefusal = true;
2602
+ return true;
2603
+ }
2604
+ function pendingShown() {
2605
+ if (!localSaves)
2606
+ recorded = false;
2607
+ return moveTo(localSaves ? status : { ...status, state: 'pending', resting: false });
2094
2608
  }
2095
2609
  function scheduleCurrency() {
2096
- if (currencyQueued || closed || stopping)
2610
+ if (currencyQueued === socket.generation() || closed || stopping)
2097
2611
  return;
2098
- currencyQueued = true;
2099
- void task(runCurrency);
2612
+ currencyQueued = socket.generation();
2613
+ void task(CONVERSATION_LANE, runCurrency);
2100
2614
  }
2101
- async function runCurrency() {
2102
- currencyQueued = false;
2103
- if (closed || stopping || discoveryPaused)
2615
+ async function runCurrency(live) {
2616
+ if (currencyQueued === socket.generation())
2617
+ currencyQueued = null;
2618
+ if (!live())
2104
2619
  return;
2105
2620
  await conversations.reconcile();
2106
2621
  if (conversations.current()) {
@@ -2108,97 +2623,184 @@ export function composeRuntime(options) {
2108
2623
  return;
2109
2624
  }
2110
2625
  const progress = { complete: false, reachable: !status.providerUnreachable };
2111
- moveTo(transferring(status));
2626
+ moveTo(transferring());
2112
2627
  try {
2113
2628
  await conversations.catchUp(progress);
2114
2629
  }
2115
2630
  catch {
2116
2631
  progress.reachable = false;
2117
2632
  }
2118
- await rest(progress);
2633
+ await rest(progress, live);
2119
2634
  }
2120
2635
  function close() {
2121
2636
  if (closed)
2122
2637
  return;
2123
2638
  closed = true;
2124
- withdrawStream?.();
2125
- withdrawStream = undefined;
2126
- withdrawConversations?.();
2127
- withdrawConversations = undefined;
2639
+ notifyDiscovered();
2640
+ unreadied();
2641
+ if (probing !== null)
2642
+ clearInterval(probing);
2643
+ connectionLive();
2644
+ lockListeners.clear();
2645
+ readinessWatches.clear();
2646
+ socket.withdraw();
2128
2647
  withdrawServiceMode?.();
2129
2648
  conversations.close();
2130
- stopTimer();
2131
- store?.close();
2132
- void opening?.then((opened) => {
2133
- opened.close();
2134
- }, () => { });
2649
+ void recovery.retire();
2135
2650
  roomListeners.clear();
2651
+ unlistedRooms.clear();
2136
2652
  refusalListeners.clear();
2137
2653
  storageLossListeners.clear();
2138
2654
  statusListeners.clear();
2139
2655
  serviceModeListeners.clear();
2140
2656
  memberListeners.clear();
2141
- readSide.close();
2657
+ authorityChangeListeners.clear();
2658
+ liveQueries.close();
2659
+ figures.close();
2142
2660
  queueSubscriptions.clear();
2143
- roomDraftListeners.clear();
2144
- presentationListeners.clear();
2661
+ store.close();
2145
2662
  }
2146
2663
  async function stop() {
2147
2664
  stopping = true;
2148
- stopTimer();
2149
- await chain.catch(() => undefined);
2150
- await Promise.all([...submitting.values()].map((run) => run.done));
2151
- await mutations;
2665
+ unreadied();
2666
+ socket.stopReads();
2667
+ await recovery.retire();
2668
+ await Promise.all(working);
2669
+ await store.written();
2152
2670
  close();
2153
2671
  }
2154
2672
  const withdrawServiceMode = options.adapter.serviceMode?.observe(signalled);
2155
- observeConversations();
2673
+ socket.observeConversations((event) => {
2674
+ if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
2675
+ queueReadiness();
2676
+ conversations.receive(event);
2677
+ if (event.kind !== 'gap' || conversations.current())
2678
+ return;
2679
+ show();
2680
+ scheduleCurrency();
2681
+ });
2156
2682
  options.signal?.addEventListener('abort', close, { once: true });
2157
2683
  if (options.signal?.aborted)
2158
2684
  close();
2159
- observeStream();
2160
- void openLocal().then(() => {
2161
- if (capabilities.length > 0)
2162
- notifyReady();
2163
- }, () => {
2164
- notifyReady();
2685
+ socket.observe(() => !closed && !stopping, (event) => {
2686
+ if (event.kind === 'opened') {
2687
+ connectionLive();
2688
+ conversations.reopened();
2689
+ lastAnswers.reopened();
2690
+ recovery.resume();
2691
+ }
2692
+ else if (event.kind === 'discovered') {
2693
+ if (event.missed)
2694
+ countAuthorityChange();
2695
+ void sync('opened', event.discovery);
2696
+ }
2697
+ else if (event.kind === 'closed')
2698
+ closedSocket(event.revoked, event.lost, event.opened);
2699
+ else if (event.kind === 'caught-up')
2700
+ void caughtUpRooms(event.expired);
2701
+ else if (event.kind === 'authority-changed') {
2702
+ if (socket.isAnnounced() && socket.isOpen())
2703
+ fenceLive(event.left);
2704
+ void changeAuthority(event);
2705
+ }
2706
+ else
2707
+ void applyRoom(event);
2708
+ });
2709
+ void openLocal()
2710
+ .then(async (local) => {
2711
+ if ((await local.facts()).capabilities.length > 0)
2712
+ readyFromStore();
2713
+ })
2714
+ .catch(() => {
2715
+ readyFromStore();
2165
2716
  });
2166
2717
  if (options.adapter.roomStream === undefined)
2167
- void sync('unstreamed');
2168
- function planned(input, reach) {
2169
- return planOf(input, roomFacts(reach), planReach(reach));
2170
- }
2171
- function watchPlan(plan, deliver) {
2172
- materialize(reachedRooms(plan));
2173
- return readSide.watch(plan, identityOf(plan), deliver);
2718
+ void sync('unstreamed', null);
2719
+ async function planned(input, reach) {
2720
+ const plan = planWithin(input, await roomFacts(reach), planReach(reach));
2721
+ const named = plan.paths !== null &&
2722
+ plan.rooms !== null &&
2723
+ plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
2724
+ return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
2725
+ }
2726
+ function watchPlan(plan, deliver, fail) {
2727
+ return watchSeeded(seeding, plan, deliver, (behind, answered) => liveQueries.watch(plan, identityOf(plan), (held) => {
2728
+ const cover = covered(plan, held);
2729
+ awaitPreparation(cover);
2730
+ return behind(cover, held);
2731
+ }, answered, fail));
2732
+ }
2733
+ function watchPlanned(planning, deliver, fail) {
2734
+ let stop = null;
2735
+ let stopped = false;
2736
+ planning.then((plan) => {
2737
+ if (!stopped)
2738
+ stop = watchPlan(plan, deliver, fail);
2739
+ }, (error) => {
2740
+ if (!stopped)
2741
+ fail(error);
2742
+ });
2743
+ return () => {
2744
+ stopped = true;
2745
+ stop?.();
2746
+ };
2174
2747
  }
2175
2748
  async function searched(input, reach) {
2176
- const plan = searchPlanOf(input);
2177
- const selected = plan.documents === undefined && reach === null
2178
- ? null
2179
- : (await rowsQuery(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
2180
- if (selected === null)
2181
- materialize(null);
2182
- return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
2183
- }
2184
- const watch = (topic, input, deliver) => {
2185
- if (topic === 'room-drafts') {
2186
- const listener = { notify: deliver, ready: false };
2187
- roomDraftListeners.add(listener);
2188
- void exclusive(async () => {
2189
- await openLocal();
2190
- if (closed || !roomDraftListeners.has(listener))
2191
- return;
2192
- listener.ready = true;
2193
- }).then(() => {
2194
- if (closed || !listener.ready || !roomDraftListeners.has(listener))
2195
- return;
2196
- listener.notify(snapshotRoomDrafts());
2197
- }, () => { });
2198
- return () => {
2199
- roomDraftListeners.delete(listener);
2749
+ const asked = searchPlanOf(input);
2750
+ await ready;
2751
+ const search = options.adapter.search;
2752
+ if (search === undefined) {
2753
+ return {
2754
+ kind: 'undeclared',
2755
+ code: 'action-undeclared',
2756
+ fields: { name: 'search' },
2757
+ retryable: false,
2758
+ };
2759
+ }
2760
+ if (closed || refusal !== null || (await connectionNow()) !== 'connected')
2761
+ return OFFLINE;
2762
+ const reached = reach === null ? null : new Set(reach.rooms);
2763
+ const named = asked.rooms === null
2764
+ ? reached === null
2765
+ ? null
2766
+ : [...reached]
2767
+ : asked.rooms.filter((room) => reached === null || reached.has(room));
2768
+ const held = named === null ? null : await storedRooms(named);
2769
+ const rooms = named === null ? null : named.filter((room) => held.has(room)).sort();
2770
+ if (rooms !== null && rooms.length === 0)
2771
+ return { kind: 'found', documents: [] };
2772
+ try {
2773
+ await ensureAuthorization();
2774
+ }
2775
+ catch {
2776
+ return UNCERTAIN;
2777
+ }
2778
+ if (closed || authorization === undefined || refusal !== null)
2779
+ return UNCERTAIN;
2780
+ journal.record('transfer search');
2781
+ try {
2782
+ const result = await search(authorization, { text: asked.text, rooms });
2783
+ if (result.kind === 'revoked-authorization')
2784
+ await refuse('revoked-authorization');
2785
+ journal.record(`outcome search ${result.kind}`);
2786
+ if (result.kind === 'rejected')
2787
+ return result;
2788
+ if (result.kind !== 'found')
2789
+ return failure(result);
2790
+ const kept = rooms === null ? null : new Set(rooms);
2791
+ return {
2792
+ kind: 'found',
2793
+ documents: result.documents.filter((hit) => kept === null || kept.has(hit.room)),
2200
2794
  };
2201
2795
  }
2796
+ catch {
2797
+ journal.record('outcome search transport-uncertainty');
2798
+ return UNCERTAIN;
2799
+ }
2800
+ }
2801
+ const watch = (topic, input, deliver, fail) => {
2802
+ if (topic === 'room-drafts')
2803
+ return watchRoomDrafts(drafting, deliver);
2202
2804
  if (topic === 'conversation') {
2203
2805
  return conversations.watch(input[0], input[1], deliver);
2204
2806
  }
@@ -2206,30 +2808,23 @@ export function composeRuntime(options) {
2206
2808
  return conversations.watchList(deliver);
2207
2809
  if (topic === 'agents')
2208
2810
  return conversations.watchAgents(deliver);
2209
- if (topic === 'presentation') {
2210
- const listener = {
2211
- app: input[0],
2212
- notify: deliver,
2213
- ready: false,
2214
- };
2215
- presentationListeners.add(listener);
2216
- void exclusive(async () => {
2217
- await openLocal();
2218
- }).then(() => {
2219
- if (closed || !presentationListeners.has(listener))
2220
- return;
2221
- listener.ready = true;
2222
- listener.notify(presentationOf(listener.app));
2223
- }, () => { });
2224
- return () => {
2225
- presentationListeners.delete(listener);
2226
- };
2811
+ if (topic === 'figures')
2812
+ return figures.watch(deliver, fail);
2813
+ if (topic === 'read')
2814
+ return lastAnswers.watch(input[0], input[1], deliver);
2815
+ if (topic === 'presentation')
2816
+ return watchPresentation(drafting, input[0], deliver);
2817
+ if (topic === 'readiness') {
2818
+ return watchReadiness({ target: '', code: 'stored' }, null, deliver);
2227
2819
  }
2228
2820
  if (topic === 'rooms') {
2229
- roomListeners.add(deliver);
2230
- deliver(listed());
2821
+ unlistedRooms.add(deliver);
2822
+ emitKnownRooms();
2231
2823
  return () => {
2824
+ unlistedRooms.delete(deliver);
2232
2825
  roomListeners.delete(deliver);
2826
+ if (roomListeners.size === 0)
2827
+ roomsShown = null;
2233
2828
  };
2234
2829
  }
2235
2830
  if (topic === 'connection-refusal') {
@@ -2262,160 +2857,160 @@ export function composeRuntime(options) {
2262
2857
  }
2263
2858
  if (topic === 'member') {
2264
2859
  memberListeners.add(deliver);
2265
- deliver(member);
2860
+ void openLocal()
2861
+ .then((local) => local.facts())
2862
+ .then(({ member }) => {
2863
+ if (memberListeners.has(deliver))
2864
+ deliver(member);
2865
+ }, () => { });
2266
2866
  return () => {
2267
2867
  memberListeners.delete(deliver);
2268
2868
  };
2269
2869
  }
2870
+ if (topic === 'authority-changes') {
2871
+ authorityChangeListeners.add(deliver);
2872
+ deliver(authorityChanges);
2873
+ return () => {
2874
+ authorityChangeListeners.delete(deliver);
2875
+ };
2876
+ }
2270
2877
  if (topic === 'queue') {
2271
2878
  const queue = { locator: input[0], notify: deliver };
2272
2879
  queueSubscriptions.add(queue);
2273
- queue.notify(entriesOf(queue.locator));
2880
+ void publication(async () => {
2881
+ if (!queueSubscriptions.has(queue))
2882
+ return;
2883
+ const entries = await entriesOf(queue.locator);
2884
+ if (queueSubscriptions.has(queue))
2885
+ queue.notify(entries);
2886
+ });
2274
2887
  return () => {
2275
2888
  queueSubscriptions.delete(queue);
2276
2889
  };
2277
2890
  }
2278
- return watchPlan(planned(input[0], null), deliver);
2891
+ return watchPlanned(planned(input[0], null), deliver, fail);
2279
2892
  };
2280
2893
  const md = {
2281
2894
  loaded() {
2282
2895
  return openLocal().then(() => undefined);
2283
2896
  },
2284
2897
  rooms() {
2285
- return listed();
2898
+ if (closed)
2899
+ return Promise.resolve([]);
2900
+ return new Promise((settle) => {
2901
+ const answer = (value) => {
2902
+ unlistedRooms.delete(answer);
2903
+ roomListeners.delete(answer);
2904
+ settle(value);
2905
+ };
2906
+ unlistedRooms.add(answer);
2907
+ emitKnownRooms();
2908
+ });
2286
2909
  },
2287
- query: ((input) => rowsQuery(planned(input, null))),
2910
+ query: ((input) => planned(input, null).then(rowsQuery)),
2288
2911
  search: (input) => searched(input, null),
2289
- types(locator) {
2912
+ async types(locator) {
2913
+ const held = (await storedRooms([locator])).get(locator);
2914
+ const room = isFenced(locator) ? undefined : declarationOf(locator, held)?.room;
2290
2915
  return {
2291
- collection: modelTypes(modelOf(locator)),
2292
- typedSelection: rooms.get(locator)?.typedSelection === undefined ||
2293
- rooms.get(locator)?.typedSelection === null
2294
- ? null
2295
- : modelTypes(modelOf(locator, 'typed-selection')),
2916
+ collection: modelTypes(isFenced(locator) ? NO_MODEL : modelIn(locator, held)),
2917
+ typedSelection: (room?.typedSelection ?? null) === null ? null : modelTypes(builtIn),
2296
2918
  };
2297
2919
  },
2298
- roomDraft: {
2299
- list() {
2300
- return exclusive(async () => {
2301
- await openLocal();
2302
- return snapshotRoomDrafts();
2303
- }).then((listed) => {
2304
- publishRoomDrafts(true);
2305
- return listed;
2306
- });
2307
- },
2308
- read(room, path) {
2309
- const record = roomDrafts.get(room)?.get(path);
2310
- return record === undefined ? null : roomDraftOf(record);
2311
- },
2312
- write(room, path, revision, input) {
2313
- return exclusive(async () => {
2314
- const parts = readParts(input);
2315
- if (path === '' || normalize(path) !== path)
2316
- throw runtimeError('room-draft-malformed');
2317
- const local = await openLocal();
2318
- const current = roomDrafts.get(room)?.get(path);
2319
- if ((current?.revision ?? null) !== revision) {
2320
- return staleWrite(current === undefined ? null : roomDraftOf(current));
2321
- }
2322
- const file = parts.file === undefined
2323
- ? undefined
2324
- : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
2325
- const record = {
2326
- locator: room,
2327
- path,
2328
- revision: roomDraftRevision(),
2329
- parts: held(current?.parts, parts, file),
2330
- returned: current?.returned ?? null,
2331
- };
2332
- await local.keepRoomDraft(record);
2333
- rememberRoomDraft(record);
2334
- publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
2335
- return { outcome: 'written', roomDraft: roomDraftOf(record) };
2336
- });
2337
- },
2338
- discard(room, path, revision) {
2339
- return exclusive(async () => {
2340
- const local = await openLocal();
2341
- const current = roomDrafts.get(room)?.get(path);
2342
- if (current === undefined || current.revision !== revision) {
2343
- return staleWrite(current === undefined ? null : roomDraftOf(current));
2344
- }
2345
- await local.discardRoomDraft(room, path);
2346
- forgetRoomDraft(room, path);
2347
- publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
2348
- moveTo({ ...status, ...attention() });
2349
- return { outcome: 'written', roomDraft: null };
2350
- });
2351
- },
2352
- },
2920
+ roomDraft: roomDraftSurface(drafting),
2353
2921
  conversation: conversations.surface,
2354
2922
  chatDraft: conversations.chatDraft,
2355
- presentation: {
2356
- read(app) {
2357
- return exclusive(async () => {
2358
- await openLocal();
2359
- return presentationOf(app);
2360
- });
2361
- },
2362
- write(app, value) {
2363
- return exclusive(async () => {
2364
- const local = await openLocal();
2365
- const stored = structuredClone(value);
2366
- await local.present({ app, value: stored });
2367
- presentations.set(app, stored);
2368
- publishChangeSet(changeSet('', null, null, { presentation: app }));
2369
- });
2370
- },
2371
- },
2923
+ presentation: presentationSurface(drafting),
2372
2924
  save(room, named, writtenBy) {
2373
- return exclusive(() => saveRoomDrafts(room, named, writtenBy));
2925
+ return exclusive(() => saveRoomDrafts(saver, room, named, writtenBy));
2926
+ },
2927
+ saveParts(room, parts, writtenBy) {
2928
+ return exclusive(() => saveParts(saver, room, parts, writtenBy));
2374
2929
  },
2375
2930
  history(locator, path, query) {
2376
2931
  return readHistory(locator, path, query);
2377
2932
  },
2378
- binaryFile: {
2379
- read: readBinaryPath,
2380
- readReference: readBinaryReference,
2933
+ resolve(room, path, destination) {
2934
+ return resolveReference(room, path, destination);
2381
2935
  },
2382
- call(name, input) {
2383
- return invokeAction(name, input);
2936
+ binaryFile: binaryFileSurface(saver),
2937
+ async call(name, input) {
2938
+ await ready;
2939
+ const lockedNow = await lockedRooms();
2940
+ const room = (await calledRooms(name, input)).find((one) => lockedNow.includes(one));
2941
+ return room === undefined ? recoveredAction(name, input) : lockedRoom(room);
2384
2942
  },
2385
- reached(locator, roomVersion, path) {
2943
+ async reached(locator, roomVersion, path) {
2386
2944
  return path === undefined
2387
- ? reachedBy(locator, roomVersion)
2388
- : writtenAfter(locator, roomVersion, path);
2945
+ ? reachedBy(installer, locator, roomVersion)
2946
+ : writtenAfter(installer, locator, roomVersion, path);
2389
2947
  },
2390
2948
  log() {
2391
2949
  return journal.export();
2392
2950
  },
2951
+ cleanUp,
2952
+ unsent: () => database.ask({ kind: 'authored.unsent' }),
2953
+ reset: () => options.shell.reset(),
2954
+ async readAgain() {
2955
+ lastAnswers.again();
2956
+ },
2393
2957
  persistence() {
2394
- return options.shell.persistentStorage;
2958
+ return persistence === 'granted';
2959
+ },
2960
+ async retry() {
2961
+ journal.record('retry');
2962
+ unpreparable.clear();
2963
+ queueReadiness();
2964
+ if (full)
2965
+ await probed();
2395
2966
  },
2396
2967
  observe: observing(watch),
2397
2968
  async reauthorize() {
2398
2969
  if (refusal === null || !refusal.reauthorizationOffered)
2399
2970
  return;
2400
2971
  journal.record('connection reauthorize');
2401
- await chain.catch(() => undefined);
2402
- await mutations;
2972
+ await Promise.all([...lanes.values()]
2973
+ .filter((held) => held.generation === socket.generation())
2974
+ .map((held) => held.chain));
2975
+ await store.written();
2403
2976
  authorization = undefined;
2404
2977
  discoveryPaused = false;
2405
2978
  if (options.adapter.roomStream === undefined)
2406
- await sync('unstreamed');
2979
+ await sync('unstreamed', null);
2407
2980
  else
2408
- await task(ensureAuthorization);
2981
+ await task(OPENING_LANE, ensureAuthorization);
2409
2982
  },
2410
2983
  };
2411
2984
  const reachable = {
2412
2985
  md,
2413
2986
  watch,
2414
- referrerScope: (referrer) => options.adapter.referrerScope(referrer),
2415
2987
  plan: planned,
2416
2988
  query: (plan) => rowsQuery(plan),
2417
2989
  watchPlan,
2418
2990
  search: searched,
2991
+ watchReadiness,
2992
+ locked,
2993
+ lockedRooms,
2994
+ watchLocks,
2995
+ located,
2996
+ room: async (locator) => shownRooms(await listingNow()).find((one) => one.locator === locator),
2997
+ };
2998
+ return {
2999
+ md,
3000
+ seed: (entry) => keepEntrySeed(store, entry),
3001
+ stop,
3002
+ watch,
3003
+ persisted(answer) {
3004
+ if (answer === persistence)
3005
+ return;
3006
+ persistence = answer;
3007
+ journal.record(`persistence ${answer}`);
3008
+ queueReadiness();
3009
+ },
3010
+ within: (reach, port) => {
3011
+ if (full)
3012
+ void probed();
3013
+ return narrowed(reachable, reach, port);
3014
+ },
2419
3015
  };
2420
- return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2421
3016
  }