@agentshouse/mdruntime 0.1.0-ca7c26c6bc27e → 0.1.0-ca8707258fdde

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 (253) hide show
  1. package/README.md +25 -12
  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 +200 -54
  6. package/dist/adapter.d.ts.map +1 -1
  7. package/dist/adapter.js +1 -9
  8. package/dist/attached-model.d.ts +7 -10
  9. package/dist/attached-model.d.ts.map +1 -1
  10. package/dist/attached-model.js +40 -28
  11. package/dist/attempt.d.ts +16 -0
  12. package/dist/attempt.d.ts.map +1 -0
  13. package/dist/attempt.js +70 -0
  14. package/dist/binary.d.ts +18 -0
  15. package/dist/binary.d.ts.map +1 -0
  16. package/dist/binary.js +8 -0
  17. package/dist/bootstrap.d.ts +37 -5
  18. package/dist/bootstrap.d.ts.map +1 -1
  19. package/dist/bootstrap.js +336 -51
  20. package/dist/chain.d.ts +12 -0
  21. package/dist/chain.d.ts.map +1 -0
  22. package/dist/chain.js +64 -0
  23. package/dist/change-set.d.ts +9 -14
  24. package/dist/change-set.d.ts.map +1 -1
  25. package/dist/change-set.js +11 -56
  26. package/dist/change.d.ts +46 -0
  27. package/dist/change.d.ts.map +1 -1
  28. package/dist/change.js +34 -0
  29. package/dist/chat-types.d.ts +3 -0
  30. package/dist/chat-types.d.ts.map +1 -0
  31. package/dist/chat-types.js +2 -0
  32. package/dist/client.d.ts +50 -18
  33. package/dist/client.d.ts.map +1 -1
  34. package/dist/client.js +162 -30
  35. package/dist/collection.d.ts +13 -14
  36. package/dist/collection.d.ts.map +1 -1
  37. package/dist/collection.js +52 -56
  38. package/dist/compile.d.ts +16 -0
  39. package/dist/compile.d.ts.map +1 -0
  40. package/dist/compile.js +717 -0
  41. package/dist/connection.d.ts +61 -43
  42. package/dist/connection.d.ts.map +1 -1
  43. package/dist/connection.js +2377 -1476
  44. package/dist/context.d.ts +2 -0
  45. package/dist/context.d.ts.map +1 -0
  46. package/dist/context.js +7 -0
  47. package/dist/conversation-row.d.ts +33 -0
  48. package/dist/conversation-row.d.ts.map +1 -0
  49. package/dist/conversation-row.js +47 -0
  50. package/dist/conversation.d.ts +71 -22
  51. package/dist/conversation.d.ts.map +1 -1
  52. package/dist/conversation.js +996 -273
  53. package/dist/digest.d.ts +7 -0
  54. package/dist/digest.d.ts.map +1 -0
  55. package/dist/digest.js +113 -0
  56. package/dist/durability.d.ts +10 -1
  57. package/dist/durability.d.ts.map +1 -1
  58. package/dist/durability.js +5 -0
  59. package/dist/entry.d.ts +20 -0
  60. package/dist/entry.d.ts.map +1 -0
  61. package/dist/entry.js +161 -0
  62. package/dist/expectation.d.ts +21 -0
  63. package/dist/expectation.d.ts.map +1 -0
  64. package/dist/expectation.js +70 -0
  65. package/dist/failure.d.ts +13 -0
  66. package/dist/failure.d.ts.map +1 -0
  67. package/dist/failure.js +19 -0
  68. package/dist/figures.d.ts +48 -0
  69. package/dist/figures.d.ts.map +1 -0
  70. package/dist/figures.js +197 -0
  71. package/dist/file.d.ts +2 -19
  72. package/dist/file.d.ts.map +1 -1
  73. package/dist/file.js +7 -122
  74. package/dist/host.d.ts +11 -2
  75. package/dist/host.d.ts.map +1 -1
  76. package/dist/host.js +134 -34
  77. package/dist/identity.d.ts +3 -0
  78. package/dist/identity.d.ts.map +1 -1
  79. package/dist/identity.js +13 -1
  80. package/dist/index.d.ts +31 -10
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +13 -0
  83. package/dist/install.d.ts +86 -0
  84. package/dist/install.d.ts.map +1 -0
  85. package/dist/install.js +537 -0
  86. package/dist/job.d.ts +33 -0
  87. package/dist/job.d.ts.map +1 -0
  88. package/dist/job.js +54 -0
  89. package/dist/live.d.ts +23 -0
  90. package/dist/live.d.ts.map +1 -0
  91. package/dist/live.js +165 -0
  92. package/dist/log.d.ts +1 -0
  93. package/dist/log.d.ts.map +1 -1
  94. package/dist/media.d.ts +8 -0
  95. package/dist/media.d.ts.map +1 -0
  96. package/dist/media.js +218 -0
  97. package/dist/memory.d.ts +22 -8
  98. package/dist/memory.d.ts.map +1 -1
  99. package/dist/memory.js +299 -94
  100. package/dist/messages.d.ts +52 -0
  101. package/dist/messages.d.ts.map +1 -0
  102. package/dist/messages.js +168 -0
  103. package/dist/model.d.ts +17 -12
  104. package/dist/model.d.ts.map +1 -1
  105. package/dist/model.js +9 -9
  106. package/dist/name.d.ts +4 -0
  107. package/dist/name.d.ts.map +1 -0
  108. package/dist/name.js +122 -0
  109. package/dist/observation.d.ts +22 -4
  110. package/dist/observation.d.ts.map +1 -1
  111. package/dist/observation.js +120 -14
  112. package/dist/open.d.ts +2 -0
  113. package/dist/open.d.ts.map +1 -0
  114. package/dist/open.js +1 -0
  115. package/dist/opened.d.ts +22 -0
  116. package/dist/opened.d.ts.map +1 -0
  117. package/dist/opened.js +107 -0
  118. package/dist/outcome.d.ts +73 -5
  119. package/dist/outcome.d.ts.map +1 -1
  120. package/dist/outcome.js +84 -1
  121. package/dist/overlay.d.ts +16 -5
  122. package/dist/overlay.d.ts.map +1 -1
  123. package/dist/overlay.js +147 -180
  124. package/dist/parts.d.ts +59 -0
  125. package/dist/parts.d.ts.map +1 -0
  126. package/dist/parts.js +118 -0
  127. package/dist/paths.d.ts +1 -1
  128. package/dist/paths.d.ts.map +1 -1
  129. package/dist/paths.js +4 -16
  130. package/dist/query.d.ts +52 -181
  131. package/dist/query.d.ts.map +1 -1
  132. package/dist/query.js +3 -423
  133. package/dist/questions.d.ts +32 -0
  134. package/dist/questions.d.ts.map +1 -0
  135. package/dist/questions.js +118 -0
  136. package/dist/reach.d.ts +19 -5
  137. package/dist/reach.d.ts.map +1 -1
  138. package/dist/reach.js +309 -92
  139. package/dist/react.d.ts +46 -0
  140. package/dist/react.d.ts.map +1 -0
  141. package/dist/react.js +54 -0
  142. package/dist/read.d.ts +7 -0
  143. package/dist/read.d.ts.map +1 -0
  144. package/dist/read.js +17 -0
  145. package/dist/readiness.d.ts +50 -0
  146. package/dist/readiness.d.ts.map +1 -0
  147. package/dist/readiness.js +27 -0
  148. package/dist/reads.d.ts +35 -0
  149. package/dist/reads.d.ts.map +1 -0
  150. package/dist/reads.js +128 -0
  151. package/dist/recovery.d.ts +26 -0
  152. package/dist/recovery.d.ts.map +1 -0
  153. package/dist/recovery.js +232 -0
  154. package/dist/refresh.d.ts +20 -7
  155. package/dist/refresh.d.ts.map +1 -1
  156. package/dist/refresh.js +155 -59
  157. package/dist/replica.d.ts +58 -34
  158. package/dist/replica.d.ts.map +1 -1
  159. package/dist/replica.js +355 -162
  160. package/dist/room-draft.d.ts +59 -11
  161. package/dist/room-draft.d.ts.map +1 -1
  162. package/dist/room-draft.js +276 -80
  163. package/dist/room-drafts.d.ts +29 -0
  164. package/dist/room-drafts.d.ts.map +1 -0
  165. package/dist/room-drafts.js +216 -0
  166. package/dist/rows.d.ts +32 -34
  167. package/dist/rows.d.ts.map +1 -1
  168. package/dist/rows.js +65 -87
  169. package/dist/save.d.ts +74 -6
  170. package/dist/save.d.ts.map +1 -1
  171. package/dist/save.js +902 -182
  172. package/dist/saves.d.ts +96 -0
  173. package/dist/saves.d.ts.map +1 -0
  174. package/dist/saves.js +999 -0
  175. package/dist/search.d.ts +15 -54
  176. package/dist/search.d.ts.map +1 -1
  177. package/dist/search.js +10 -121
  178. package/dist/seed.d.ts +30 -0
  179. package/dist/seed.d.ts.map +1 -0
  180. package/dist/seed.js +141 -0
  181. package/dist/shared.d.ts +15 -0
  182. package/dist/shared.d.ts.map +1 -0
  183. package/dist/shared.js +421 -0
  184. package/dist/socket/admission.d.ts +7 -0
  185. package/dist/socket/admission.d.ts.map +1 -0
  186. package/dist/socket/admission.js +39 -0
  187. package/dist/socket/answers.d.ts +6 -0
  188. package/dist/socket/answers.d.ts.map +1 -0
  189. package/dist/socket/answers.js +20 -0
  190. package/dist/socket/reads.d.ts +10 -0
  191. package/dist/socket/reads.d.ts.map +1 -0
  192. package/dist/socket/reads.js +31 -0
  193. package/dist/socket.d.ts +43 -0
  194. package/dist/socket.d.ts.map +1 -0
  195. package/dist/socket.js +96 -0
  196. package/dist/source.d.ts +61 -0
  197. package/dist/source.d.ts.map +1 -0
  198. package/dist/source.js +81 -0
  199. package/dist/status.d.ts +7 -4
  200. package/dist/status.d.ts.map +1 -1
  201. package/dist/status.js +3 -15
  202. package/dist/store/fences.d.ts +16 -0
  203. package/dist/store/fences.d.ts.map +1 -0
  204. package/dist/store/fences.js +26 -0
  205. package/dist/store/installs.d.ts +30 -0
  206. package/dist/store/installs.d.ts.map +1 -0
  207. package/dist/store/installs.js +34 -0
  208. package/dist/store/records.d.ts +212 -0
  209. package/dist/store/records.d.ts.map +1 -0
  210. package/dist/store/records.js +408 -0
  211. package/dist/store/room-held.d.ts +8 -0
  212. package/dist/store/room-held.d.ts.map +1 -0
  213. package/dist/store/room-held.js +17 -0
  214. package/dist/store/watchers.d.ts +15 -0
  215. package/dist/store/watchers.d.ts.map +1 -0
  216. package/dist/store/watchers.js +36 -0
  217. package/dist/store/writes.d.ts +24 -0
  218. package/dist/store/writes.d.ts.map +1 -0
  219. package/dist/store/writes.js +125 -0
  220. package/dist/store.d.ts +77 -93
  221. package/dist/store.d.ts.map +1 -1
  222. package/dist/store.js +118 -389
  223. package/dist/stored.d.ts +156 -0
  224. package/dist/stored.d.ts.map +1 -0
  225. package/dist/stored.js +637 -0
  226. package/dist/tables.d.ts +407 -0
  227. package/dist/tables.d.ts.map +1 -0
  228. package/dist/tables.js +724 -0
  229. package/dist/tallies.d.ts +18 -0
  230. package/dist/tallies.d.ts.map +1 -0
  231. package/dist/tallies.js +129 -0
  232. package/dist/transfer.d.ts +0 -5
  233. package/dist/transfer.d.ts.map +1 -1
  234. package/dist/turn.d.ts +9 -0
  235. package/dist/turn.d.ts.map +1 -0
  236. package/dist/turn.js +75 -0
  237. package/dist/validation.d.ts +29 -0
  238. package/dist/validation.d.ts.map +1 -0
  239. package/dist/validation.js +104 -0
  240. package/dist/windows.d.ts +11 -0
  241. package/dist/windows.d.ts.map +1 -0
  242. package/dist/windows.js +16 -0
  243. package/dist/wire.d.ts +185 -45
  244. package/dist/wire.d.ts.map +1 -1
  245. package/dist/wire.js +75 -46
  246. package/package.json +33 -10
  247. package/AGENTS.md +0 -215
  248. package/dist/attachment.d.ts +0 -3
  249. package/dist/attachment.d.ts.map +0 -1
  250. package/dist/attachment.js +0 -8
  251. package/dist/read-side.d.ts +0 -18
  252. package/dist/read-side.d.ts.map +0 -1
  253. package/dist/read-side.js +0 -377
@@ -1,27 +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 { createSearchIndex, searchPlanOf, } from './search.js';
7
- import { ROOM_CONTRACT_VERSION, VERSION_1_CAPABILITIES, } from './adapter.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 { MD_MODEL, parseCollection, validRoot } from './collection.js';
15
+ import { composedOf, validRoot } from './collection.js';
12
16
  import { createLog } from './log.js';
13
- import { refreshRoom } from './refresh.js';
14
- import { buildBatch } from './save.js';
15
- import { applyReturned, compositionOf, landed, readConfirmed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
16
- import { changedParts, changesOf, held, readParts, returnedInto, underneath, } from './room-draft.js';
17
- import { settled, same, saving, STARTING, transferring, } from './status.js';
18
- import { detectStorageLoss, nextGeneration } from './durability.js';
19
- import { roomDraftRevision } from './identity.js';
20
- import { beneath, normalize } from './paths.js';
21
- 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';
22
29
  import { unconfirmed } from './transfer.js';
23
30
  import { governs, holds, narrowed, planReach, } from './reach.js';
24
- const knownCapabilities = new Set(VERSION_1_CAPABILITIES);
31
+ import { binaryFileSurface, overlayDiff, preparedAfter, replayed, saveParts, saveRoomDrafts, settle, sliceAt, } from './saves.js';
32
+ import { submitAll, submitLater, unpair } from './acceptance.js';
25
33
  function within(root, path) {
26
34
  return root === '' || beneath(root, path);
27
35
  }
@@ -35,200 +43,448 @@ function declaredSelection(room) {
35
43
  root !== '' &&
36
44
  selection.roots.every((other, at) => at === index || !within(other, root)));
37
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
+ }
38
60
  export function composeRuntime(options) {
61
+ const localSaves = options.adapter.localSaves === true;
62
+ const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
39
63
  let authorization = options.authorization;
40
- let opening;
41
- let store;
42
- const replicas = new Map();
43
- const pendings = new Map();
44
- const roomDrafts = new Map();
45
- const presentations = new Map();
46
- let limits = [];
47
- let capabilities = [];
48
- function rewriting() {
49
- return capabilities.some((capability) => capability.name === 'move-reference-rewrites')
50
- ? 'provider'
51
- : 'runtime';
52
- }
53
- const rooms = 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();
54
69
  const unreadable = new Set();
55
- const models = new Map();
56
- const liveModels = new Map();
57
- const invalidModels = new Set();
70
+ const builtIn = builtInModel(roomTypes);
71
+ const typedPaths = options.adapter.typedPaths ?? [];
58
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;
59
105
  let roomsListed = false;
60
106
  const scopedRefusals = new Map();
61
- const sourceRefusals = new Map();
62
- const declarationRefusals = new Set();
63
- function modelKey(locator, attached) {
64
- return `${attached} ${locator}`;
65
- }
66
- function modelOf(locator, attached = 'collection') {
67
- return models.get(modelKey(locator, attached)) ?? NO_MODEL;
68
- }
69
- function savedModel(locator, attached) {
70
- return invalidModels.has(modelKey(locator, attached))
71
- ? { status: 'unavailable', reason: 'revoked' }
72
- : 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;
73
111
  }
74
112
  function compositionFor(record) {
75
- return compositionOf({
113
+ return composed({
76
114
  ...record,
77
- model: savedModel(record.locator, 'collection'),
78
- selectionModel: savedModel(record.locator, 'typed-selection'),
115
+ model: modelIn(record.locator, { record, indexed: true, documents: 0 }),
79
116
  });
80
117
  }
81
118
  let refusal = null;
82
119
  let storageLoss = null;
83
- let status = STARTING;
120
+ let status = { state: 'syncing', providerUnreachable: false, resting: false };
84
121
  let shown = STARTING;
85
122
  let discoveryPaused = false;
86
- let resetTimer;
87
123
  let closed = false;
88
124
  let stopping = false;
89
- let withdrawStream;
90
- const announced = new Map();
91
125
  let underway = null;
92
- const submitting = new Map();
93
- const answering = new Map();
126
+ let streamed = null;
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();
94
132
  let complete = false;
95
133
  let recorded = false;
96
134
  const stale = new Set();
97
- let chain = Promise.resolve();
98
- 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();
99
141
  const roomListeners = new Set();
142
+ const unlistedRooms = new Set();
100
143
  const refusalListeners = new Set();
101
144
  const storageLossListeners = new Set();
102
145
  const statusListeners = new Set();
103
146
  const queueSubscriptions = new Set();
104
- const roomDraftListeners = new Set();
105
- const presentationListeners = new Set();
106
- const readSide = createReadSide();
107
- let withdrawConversations;
108
- let currencyQueued = false;
109
- 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();
110
162
  let serviceMode = 'open';
111
163
  let haltedRefusal = false;
112
164
  let openSignals = 0;
113
165
  const serviceModeListeners = new Set();
166
+ const memberListeners = new Set();
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;
114
179
  const conversations = createConversationStore({
115
- cards: declaredCards,
116
- documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
117
180
  call: (name, input) => invokeAction(name, input),
181
+ history: (request) => readConversationHistory(request),
118
182
  durably,
183
+ local: () => openLocal(),
184
+ again: () => {
185
+ void ready.then(scheduleCurrency);
186
+ },
187
+ settled: () => {
188
+ show();
189
+ },
119
190
  stream: () => options.adapter.conversations,
120
191
  publish: publishChangeSet,
192
+ online: providerReached,
193
+ full: () => full,
121
194
  closed: () => closed,
195
+ unanswered: resendLater,
122
196
  });
123
- const searchIndex = createSearchIndex({
124
- documents: () => readSide.documents(),
125
- document: (room, path) => readSide.document(room, path),
126
- messages: () => conversations.messages(),
127
- 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,
128
207
  });
129
- const journal = createLog();
130
- let notifyReady = () => { };
208
+ let readyFromStore = () => { };
131
209
  const ready = new Promise((resolve) => {
132
- notifyReady = resolve;
210
+ readyFromStore = resolve;
133
211
  });
134
- function rememberRoomDraft(record) {
135
- const room = roomDrafts.get(record.locator) ?? new Map();
136
- room.set(record.path, record);
137
- roomDrafts.set(record.locator, room);
138
- }
139
- function forgetRoomDraft(locator, path) {
140
- const room = roomDrafts.get(locator);
141
- if (room === undefined)
142
- return;
143
- room.delete(path);
144
- if (room.size === 0)
145
- roomDrafts.delete(locator);
146
- }
147
- function roomDraftOf(record) {
148
- return structuredClone({
149
- room: record.locator,
150
- path: record.path,
151
- revision: record.revision,
152
- parts: record.parts,
153
- changedUnderneath: underneath(record.parts, record.path, replicas.get(record.locator)?.visible),
154
- returned: record.returned,
155
- });
156
- }
157
- function snapshotRoomDrafts() {
158
- const records = [...roomDrafts.values()].flatMap((room) => [...room.values()]);
159
- records.sort((left, right) => {
160
- if (left.locator !== right.locator)
161
- 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;
162
312
  if (left.path !== right.path)
163
313
  return left.path < right.path ? -1 : 1;
164
314
  return 0;
165
315
  });
166
- return records.map(roomDraftOf);
167
- }
168
- function publishRoomDrafts(waitingOnly) {
169
- if (closed)
170
- return;
171
- const value = snapshotRoomDrafts();
172
- for (const listener of roomDraftListeners) {
173
- if (waitingOnly && listener.ready)
174
- continue;
175
- listener.ready = true;
176
- 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
+ }
177
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
+ };
178
333
  }
179
- function presentationOf(app) {
180
- const value = presentations.get(app);
181
- 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));
182
337
  }
183
- function emitPresentation(app) {
184
- if (closed)
185
- return;
186
- for (const listener of presentationListeners) {
187
- if (listener.app === app && listener.ready)
188
- 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);
189
381
  }
382
+ return listing;
190
383
  }
191
- function pending() {
192
- return [...replicas.values()].some((replica) => replica.queue.length > 0);
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);
394
+ }
395
+ return listing;
193
396
  }
194
- function attention() {
195
- const attentionRequired = [];
196
- for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
197
- if (record.returned === null)
198
- continue;
199
- attentionRequired.push({
200
- room: record.locator,
201
- path: record.path,
202
- returned: structuredClone(record.returned),
203
- });
397
+ async function listingNow() {
398
+ return listingOf(await storedRooms());
399
+ }
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);
204
420
  }
205
- attentionRequired.sort((left, right) => {
206
- if (left.room !== right.room)
207
- return left.room < right.room ? -1 : 1;
208
- if (left.path !== right.path)
209
- return left.path < right.path ? -1 : 1;
210
- return 0;
211
- });
212
- let reset = null;
213
- for (const { submission } of pendings.values()) {
214
- if (submission.state !== 'reset')
215
- continue;
216
- if (reset === null || submission.reset < reset)
217
- 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);
218
453
  }
219
- return { attentionRequired, reset };
220
454
  }
221
- function listed() {
222
- return structuredClone([...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);
223
467
  }
224
- function roomFacts(reach = null) {
225
- const held = [...rooms.values()]
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()]
226
482
  .filter((room) => holds(reach, room.locator))
227
483
  .map((room) => {
228
484
  const governed = governs(reach, room);
229
485
  const attached = [
230
- ...(governed.collection ? [modelOf(room.locator)] : []),
231
- ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
486
+ ...(governed.collection ? [modelIn(room.locator, held.get(room.locator))] : []),
487
+ ...(governed.selection ? [builtIn] : []),
232
488
  ];
233
489
  return {
234
490
  locator: room.locator,
@@ -236,164 +492,665 @@ export function composeRuntime(options) {
236
492
  types: attached.flatMap((model) => model.status === 'unavailable' ? [] : Object.values(model.types)),
237
493
  };
238
494
  });
239
- 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);
501
+ }
502
+ function emitKnownRooms() {
503
+ conversations.learned();
504
+ if (unlistedRooms.size === 0)
505
+ return;
506
+ void publishRooms('known');
240
507
  }
241
- function emitRooms() {
242
- const value = listed();
243
- for (const listener of roomListeners)
244
- listener(value);
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;
245
575
  }
246
576
  function emitRefusal() {
577
+ queueReadiness();
247
578
  for (const listener of refusalListeners)
248
579
  listener(refusal);
580
+ emitKnownRooms();
249
581
  }
250
582
  function emitStorageLoss() {
583
+ queueReadiness();
251
584
  for (const listener of storageLossListeners)
252
585
  listener(storageLoss);
253
586
  }
587
+ function emitMember(member) {
588
+ for (const listener of memberListeners)
589
+ listener(member);
590
+ }
254
591
  function moveTo(next) {
255
592
  if (closed)
256
- return;
593
+ return Promise.resolve();
257
594
  status = next;
258
- show();
595
+ return show();
259
596
  }
597
+ function transferring() {
598
+ return { ...status, state: 'syncing', resting: false };
599
+ }
600
+ let statusOwed = null;
601
+ let statusShown = Promise.resolve();
260
602
  function show() {
261
603
  if (closed)
262
- return;
263
- const next = status.state === 'synced' && !conversations.current()
264
- ? { ...status, state: 'syncing' }
265
- : status;
266
- if (same(shown, next))
267
- return;
268
- shown = next;
269
- for (const listener of statusListeners)
270
- 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;
271
638
  }
272
- function entryOf(batch) {
273
- const state = pendings.get(batch.id);
274
- if (state?.accepted !== undefined) {
275
- return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
276
- }
639
+ function entryOf(states, batch) {
640
+ if (localSaves)
641
+ return Object.freeze({ batch, state: 'local' });
642
+ const state = states.get(batch.id);
277
643
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
278
644
  }
279
- function entriesOf(locator) {
280
- const replica = replicas.get(locator);
281
- if (replica === undefined)
645
+ async function entriesOf(locator) {
646
+ if (isFenced(locator))
282
647
  return [];
283
- return replica.queue.map(entryOf);
648
+ const { queue, states } = await workOf(locator);
649
+ return queue.map((batch) => entryOf(states, batch));
284
650
  }
285
651
  function emitQueue(locator) {
286
- const entries = entriesOf(locator);
287
- for (const subscription of queueSubscriptions) {
288
- 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())
289
662
  subscription.notify(entries);
290
- }
663
+ });
291
664
  }
292
665
  function applyChangeSet(set) {
293
- readSide.apply(set);
294
- searchIndex.changed(set);
666
+ liveQueries.touched(set);
667
+ figures.touched(set);
295
668
  }
296
- function publishChangeSet(set) {
297
- applyChangeSet(set);
298
- if (set.rooms.length > 0)
299
- emitRooms();
300
- for (const locator of set.queues)
301
- emitQueue(locator);
302
- if (set.drafts.length > 0 ||
303
- set.documents.some((change) => roomDrafts.get(change.locator)?.has(change.path) === true)) {
304
- publishRoomDrafts(false);
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();
693
+ return;
305
694
  }
306
- for (const app of set.presentations)
307
- emitPresentation(app);
308
- conversations.changed(set);
309
- const cards = declaredCards();
310
- if (cards !== undefined && set.documents.some((change) => change.locator === cards.locator)) {
311
- scheduleCurrency();
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' });
770
+ continue;
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);
312
784
  }
785
+ if (pending.length > 0)
786
+ return { pending };
787
+ return { coverage: { rooms: coverage, indexes: [], overlay }, rooms: read };
313
788
  }
314
- function stopTimer() {
315
- if (resetTimer === undefined)
789
+ function awaitPreparation(cover) {
790
+ if (!('pending' in cover))
316
791
  return;
317
- clearTimeout(resetTimer);
318
- resetTimer = undefined;
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
+ });
319
872
  }
320
- function armTimer() {
321
- stopTimer();
322
- if (closed || stopping)
873
+ const establishing = new Set();
874
+ function establish(name) {
875
+ if (establishing.has(name))
323
876
  return;
324
- let earliest = null;
325
- for (const { submission } of pendings.values()) {
326
- if (submission.state !== 'reset')
327
- continue;
328
- const at = Date.parse(submission.reset);
329
- if (earliest === null || at < earliest)
330
- earliest = at;
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);
331
896
  }
332
- if (earliest === null)
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);
922
+ }
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)
333
947
  return;
334
- // A Save House held back goes again only once the reset its answer named has passed.
335
- resetTimer = setTimeout(() => {
336
- resetTimer = undefined;
337
- submitAll();
338
- }, Math.max(0, earliest - Date.now()));
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 };
1087
+ }
1088
+ function publishChangeSet(set) {
1089
+ applyChangeSet(set);
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
+ }
1102
+ for (const locator of set.queues)
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));
1108
+ }
1109
+ for (const app of set.presentations)
1110
+ shownNow.push(emitPresentation(drafting, app));
1111
+ conversations.changed(set);
1112
+ return Promise.all(shownNow).then(() => undefined);
339
1113
  }
340
1114
  async function refuse(reason, provider = null) {
341
- stopTimer();
342
1115
  discoveryPaused = true;
1116
+ socket.abortReads();
343
1117
  try {
344
1118
  if (reason === 'revoked-authorization') {
345
1119
  await exclusive(async () => {
346
1120
  const revoked = { status: 'unavailable', reason: 'revoked' };
1121
+ let held = new Map();
347
1122
  try {
348
1123
  const local = await openLocal();
349
- await local.revokeModels([...replicas.values()].map((replica) => ({
350
- record: replica.confirmed.record,
351
- queue: replica.queue,
352
- })));
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
+ });
353
1136
  }
354
1137
  catch {
355
1138
  journal.record('model revocation persistence failed');
356
1139
  }
357
- const keys = new Set(models.keys());
358
- for (const locator of rooms.keys()) {
359
- keys.add(modelKey(locator, 'collection'));
360
- keys.add(modelKey(locator, 'typed-selection'));
361
- }
362
- for (const key of keys) {
363
- invalidModels.add(key);
364
- models.set(key, revoked);
365
- liveModels.delete(key);
1140
+ const listedNow = listedLocators(held);
1141
+ for (const locator of listedNow) {
1142
+ if (!held.has(locator))
1143
+ unheldModels.set(locator, revoked);
366
1144
  }
367
- const changed = [];
368
- for (const [locator, room] of rooms) {
369
- const before = replicas.get(locator) ?? null;
370
- if (before !== null) {
371
- const record = {
372
- ...before.confirmed.record,
373
- model: revoked,
374
- selectionModel: revoked,
375
- };
376
- const read = readConfirmed(record, before.confirmed);
377
- replicas.set(locator, read.kind === 'staged'
378
- ? replicaOf(read.confirmed, before.queue, rewriting(), before)
379
- : { ...before, confirmed: { ...before.confirmed, record } });
380
- }
381
- rooms.set(locator, {
382
- ...room,
383
- model: modelTypes(revoked),
384
- typedSelection: room.typedSelection === null
385
- ? null
386
- : { ...room.typedSelection, model: modelTypes(revoked) },
387
- });
388
- changed.push(changeSet(locator, before, replicas.get(locator) ?? null, { room: true }));
389
- }
390
- publishChangeSet(mergeChangeSets(changed));
1145
+ publishChangeSet(mergeChangeSets(listedNow.map((locator) => changeSet(locator, { documents: null, room: true }))));
1146
+ prepare();
391
1147
  });
392
1148
  }
393
1149
  }
394
1150
  finally {
395
1151
  refusal = {
396
- reason,
1152
+ code: reason,
1153
+ retryable: false,
397
1154
  reauthorizationOffered: reason === 'revoked-authorization',
398
1155
  refusal: provider,
399
1156
  };
@@ -402,425 +1159,219 @@ export function composeRuntime(options) {
402
1159
  }
403
1160
  }
404
1161
  function list(room, synchronizationAvailable) {
405
- const selection = room.typedSelection ?? null;
406
- rooms.set(room.locator, {
407
- name: room.name,
408
- locator: room.locator,
409
- operations: room.operations,
410
- replica: room.replica,
411
- model: modelTypes(modelOf(room.locator)),
412
- typedSelection: selection === null
413
- ? null
414
- : {
415
- roots: selection.roots,
416
- model: modelTypes(modelOf(room.locator, 'typed-selection')),
417
- },
418
- providerOwnedPaths: room.providerOwnedPaths,
419
- roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
420
- attachments: replicas.get(room.locator)?.confirmed.record.attachments ?? [],
421
- synchronizationAvailable,
422
- 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,
423
1175
  });
424
1176
  }
425
- async function applyDiscovery(listed, local) {
426
- const listedLocators = new Set(listed.map((room) => room.locator));
427
- 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)
428
1209
  scopedRefusals.delete(locator);
429
- discovered.clear();
1210
+ if (left === undefined)
1211
+ discovered.clear();
430
1212
  for (const room of listed)
431
1213
  discovered.set(room.locator, room);
432
- 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));
433
1218
  for (const room of listed) {
434
- const held = replicas.get(room.locator)?.confirmed.record;
435
- if (held !== undefined && held.replica.selection !== room.replica.selection) {
436
- 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);
437
1222
  continue;
438
1223
  }
439
1224
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
440
1225
  }
1226
+ confirmLocks(listed);
441
1227
  roomsListed = true;
442
1228
  emitRooms();
443
- }
444
- function unlistedHeld(listed) {
445
- return [...new Set([...rooms.keys(), ...roomDrafts.keys()])].filter((locator) => !listed.has(locator));
446
- }
447
- async function dropUnlisted(listed, local) {
448
- 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)
449
1254
  return;
450
- await exclusive(async () => {
451
- const unlisted = unlistedHeld(listed);
452
- await local.dropRooms(unlisted);
453
- const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
454
- room: true,
455
- queue: true,
456
- drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
457
- }));
458
- for (const locator of unlisted) {
459
- for (const batch of replicas.get(locator)?.queue ?? [])
460
- pendings.delete(batch.id);
461
- replicas.delete(locator);
462
- rooms.delete(locator);
463
- roomDrafts.delete(locator);
464
- unreadable.delete(locator);
465
- stale.delete(locator);
466
- scopedRefusals.delete(locator);
467
- sourceRefusals.delete(locator);
468
- declarationRefusals.delete(locator);
469
- for (const attached of ['collection', 'typed-selection']) {
470
- const key = modelKey(locator, attached);
471
- models.delete(key);
472
- liveModels.delete(key);
473
- 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();
474
1260
  }
475
- journal.record(`room unlisted ${locator}`);
476
1261
  }
477
- publishChangeSet(mergeChangeSets(removed));
478
- moveTo({ ...status, ...attention() });
479
- });
480
- }
481
- function restored(key, persisted) {
482
- const prior = models.get(key);
483
- if (prior !== undefined &&
484
- prior.status !== 'unavailable' &&
485
- (persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
486
- invalidModels.add(key);
487
- }
488
- const model = persisted.status === 'unavailable' ||
489
- liveModels.get(key) === JSON.stringify(persisted.snapshot)
490
- ? persisted
491
- : cached(persisted);
492
- models.set(key, model);
493
- if (model.status === 'unavailable') {
494
- liveModels.delete(key);
495
- invalidModels.delete(key);
496
- }
497
- return model;
498
- }
499
- async function hydrate(opened) {
500
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
501
- opened.all(),
502
- opened.queues(),
503
- opened.roomDrafts(),
504
- opened.presentation(),
505
- opened.conversations(),
506
- opened.chatDrafts(),
507
- opened.lastCompleted(),
508
- opened.providerLimits(),
509
- opened.capabilities(),
510
- ]);
511
- limits = declared;
512
- capabilities = named;
513
- roomDrafts.clear();
514
- for (const record of kept)
515
- rememberRoomDraft(record);
516
- presentations.clear();
517
- for (const record of presented)
518
- presentations.set(record.app, record.value);
519
- conversations.hydrate(held, chatDrafts);
520
- const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
521
- pendings.clear();
522
- for (const queue of queues) {
523
- for (const { batch, state } of queue.pending)
524
- pendings.set(batch.id, state);
525
- }
526
- const previousReplicas = new Map(replicas);
527
- replicas.clear();
528
- for (const record of records) {
529
- const model = restored(modelKey(record.locator, 'collection'), record.model);
530
- const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
531
- const prior = previousReplicas.get(record.locator);
532
- const retyped = { ...record, model, selectionModel };
533
- const read = readConfirmed(retyped, prior?.confirmed);
534
- const confirmed = read.kind === 'staged'
535
- ? read.confirmed
536
- : {
537
- record: retyped,
538
- collection: parseCollection(retyped.documents, compositionOf(retyped)),
539
- };
540
- replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], rewriting(), prior));
541
- const existing = rooms.get(record.locator);
542
- if (existing === undefined)
543
- list(record, false);
544
- else {
545
- rooms.set(record.locator, {
546
- ...existing,
547
- roomVersion: record.roomVersion,
548
- attachments: record.attachments,
549
- operations: record.operations,
550
- replica: record.replica,
551
- model: modelTypes(model),
552
- typedSelection: record.typedSelection === null
553
- ? null
554
- : {
555
- roots: record.typedSelection.roots,
556
- model: modelTypes(selectionModel),
557
- },
558
- });
1262
+ catch {
1263
+ journal.record('erasure failed');
559
1264
  }
560
- if (read.kind === 'refused') {
561
- refusedSource(record, read);
562
- declarationRefusals.add(record.locator);
1265
+ finally {
1266
+ sweeping = false;
563
1267
  }
564
- }
565
- return {
566
- lastCompleted,
567
- locators: new Set([...queuedBatches.keys(), ...replicas.keys()]),
568
- read: mergeChangeSets([...new Set([...previousReplicas.keys(), ...replicas.keys()])].map((locator) => changeSet(locator, previousReplicas.get(locator) ?? null, replicas.get(locator) ?? null))),
569
- };
570
- }
571
- async function readState(opened) {
572
- const { lastCompleted, locators, read } = await hydrate(opened);
573
- applyChangeSet(read);
574
- if (replicas.size > 0)
575
- emitRooms();
576
- for (const locator of locators)
577
- emitQueue(locator);
578
- publishRoomDrafts(false);
579
- return lastCompleted;
580
- }
581
- async function open() {
582
- const opened = await openRuntimeStore();
583
- store = opened;
584
- const kept = options.shell.generation;
585
- const storedGeneration = await opened.generation();
586
- const loss = detectStorageLoss(storedGeneration, kept);
587
- const generation = nextGeneration(storedGeneration, kept);
588
- if (generation !== storedGeneration)
589
- await opened.rememberGeneration(generation);
590
- if (generation !== kept)
591
- await options.shell.keepGeneration(generation);
592
- if (loss !== null) {
593
- storageLoss = loss;
594
- emitStorageLoss();
595
- }
596
- const lastCompleted = await readState(opened);
597
- moveTo({ ...status, lastCompleted, ...attention() });
598
- return opened;
599
- }
600
- function durably(work) {
601
- return exclusive(async () => {
602
- const local = await openLocal();
603
- return work(local);
604
- });
1268
+ })();
605
1269
  }
606
- function openLocal() {
607
- opening ??= open().catch((error) => {
608
- opening = undefined;
609
- throw error;
610
- });
611
- return opening;
612
- }
613
- function exclusive(work) {
614
- const next = mutations.then(work, work);
615
- mutations = next.then(() => undefined, () => undefined);
616
- return next;
617
- }
618
- function withdrawable(batch) {
619
- const state = pendings.get(batch.id);
620
- return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
621
- }
622
- async function settle(local, locator, before, confirmed, queue, settling = {}) {
623
- const withdrawn = withdrawals(confirmed, queue, withdrawable, rewriting());
624
- const kept = queue.filter((batch) => !withdrawn.includes(batch));
625
- const returning = [
626
- ...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
627
- ...(settling.leaving === undefined ? [] : [settling.leaving]),
628
- ];
629
- const written = new Map();
630
- for (const { batch, returned } of returning) {
631
- if (returned === null)
1270
+ function fenceLive(locators) {
1271
+ for (const locator of new Set(locators)) {
1272
+ if (!store.fences.raise(locator, discoveries))
632
1273
  continue;
633
- for (const saved of pendings.get(batch.id)?.drafts ?? []) {
634
- const existing = written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path);
635
- written.set(saved.path, {
636
- locator,
637
- path: saved.path,
638
- revision: roomDraftRevision(),
639
- parts: returnedInto(existing?.parts, saved.parts),
640
- returned,
641
- });
642
- }
1274
+ forget(locator);
1275
+ journal.record(`room lost ${locator}`);
643
1276
  }
644
- await local.write({
645
- next: confirmed.record,
646
- previous: before?.confirmed.record ?? null,
647
- queue: kept,
648
- previousQueue: before?.queue ?? [],
649
- roomDrafts: [...written.values()],
650
- });
651
- for (const batch of before?.queue ?? []) {
652
- if (!kept.includes(batch))
653
- pendings.delete(batch.id);
654
- }
655
- for (const record of written.values())
656
- rememberRoomDraft(record);
657
- for (const batch of withdrawn)
658
- journal.record(`outcome ${locator} ${batch.id} withdrawn`);
659
- replicas.set(locator, replicaOf(confirmed, kept, rewriting(), before));
660
- return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
661
- room: settling.room,
662
- queue: kept.length !== (before?.queue.length ?? 0),
663
- drafts: [...written.keys()],
664
- });
665
1277
  }
666
- function adoptModels(locator, record) {
667
- for (const [attached, model] of [
668
- ['collection', record.model],
669
- ['typed-selection', record.selectionModel],
670
- ]) {
671
- const key = modelKey(locator, attached);
672
- models.set(key, model);
673
- invalidModels.delete(key);
674
- if (model.status === 'current')
675
- liveModels.set(key, JSON.stringify(model.snapshot));
676
- else if (model.status === 'unavailable')
677
- liveModels.delete(key);
678
- }
679
- }
680
- function remodel(locator, answers, held) {
681
- const taken = (answer, previous, attached) => {
682
- if (answer === undefined)
683
- return previous;
684
- const { state, refused } = attachedModel(answer, previous);
685
- if (refused !== null) {
686
- const where = refused.location === undefined ? '' : ` ${refused.location}`;
687
- journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
688
- }
689
- models.set(modelKey(locator, attached), state);
690
- return state;
691
- };
692
- return {
693
- collection: taken(answers?.collection, held.collection, 'collection'),
694
- selection: taken(answers?.selection, held.selection, 'typed-selection'),
695
- };
696
- }
697
- function publish(local, room, confirmed, base) {
698
- return exclusive(async () => {
699
- if (!continuing())
700
- return true;
701
- const prior = replicas.get(room.locator);
702
- if (base !== undefined &&
703
- (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
704
- prior.confirmed.record.revisions !== base.record.revisions)) {
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)
705
1283
  return false;
706
- }
707
- adoptModels(room.locator, confirmed.record);
708
- const queue = prior?.queue ?? [];
709
- const roomVersion = confirmed.record.roomVersion;
710
- const accepted = queue.filter((batch) => {
711
- const accepted = pendings.get(batch.id)?.accepted;
712
- return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
713
- });
714
- const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
715
- for (const batch of accepted)
716
- pendings.delete(batch.id);
717
- stale.delete(room.locator);
718
- if (unreadable.delete(room.locator))
719
- scopedRefusals.delete(room.locator);
720
- sourceRefusals.delete(room.locator);
721
- declarationRefusals.delete(room.locator);
722
- list(room, true);
723
- publishChangeSet(set);
724
- moveTo({ ...status, ...attention() });
1284
+ store.fences.mark(locator, true);
725
1285
  return true;
726
1286
  });
727
- }
728
- async function saveRoomDrafts(locator, named, writtenBy) {
729
- const local = await openLocal();
730
- const replica = replicas.get(locator);
731
- const room = rooms.get(locator);
732
- if (replica === undefined || room === undefined) {
733
- throw new Error(`No Local Replica for ${locator}`);
734
- }
735
- const saved = [];
736
- for (const { path, revision } of named) {
737
- const record = roomDrafts.get(locator)?.get(path);
738
- if (record === undefined || record.revision !== revision) {
739
- return { outcome: 'refused', rule: 'stale', file: path };
740
- }
741
- if (!saved.some((draft) => draft.path === path))
742
- saved.push({ path, parts: record.parts });
743
- }
744
- const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
745
- if (changed.length > 0)
746
- return { outcome: 'refused', rule: 'changed-underneath', parts: changed };
747
- const parts = saved.map(({ path, parts }) => changesOf(path, parts));
748
- const built = buildBatch({
749
- baseRoomVersion: replica.confirmed.record.roomVersion,
750
- composition: compositionFor(replica.confirmed.record),
751
- operations: room.operations,
752
- providerOwnedPaths: room.providerOwnedPaths,
753
- providerLimits: limits,
754
- rewriting: rewriting(),
755
- visible: replica.visible,
756
- changes: parts.flatMap(({ changes }) => changes),
757
- bases: parts.flatMap(({ bases }) => bases),
758
- writtenBy,
759
- });
760
- if (built.kind === 'refused')
761
- return { outcome: 'refused', ...built.refusal };
762
- const batches = [...replica.queue, built.batch];
763
- const consumed = saved.map(({ path }) => path);
1287
+ if (fresh.length === 0)
1288
+ return;
1289
+ let held = [];
764
1290
  try {
765
- await local.write({
766
- next: replica.confirmed.record,
767
- previous: replica.confirmed.record,
768
- queue: batches,
769
- previousQueue: replica.queue,
770
- saved,
771
- consumedRoomDrafts: consumed,
1291
+ await durably(async (local) => {
1292
+ held = await local.roomDrafts();
1293
+ await local.fence(fresh);
772
1294
  });
773
1295
  }
774
- catch {
775
- return { outcome: 'refused', rule: 'durable-write' };
776
- }
777
- pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
778
- recorded = false;
779
- for (const path of consumed)
780
- forgetRoomDraft(locator, path);
781
- replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting(), replica));
782
- publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
783
- queue: true,
784
- drafts: consumed,
785
- }));
786
- moveTo({ ...saving(status), ...attention() });
787
- submitLater(locator);
788
- return { outcome: 'saved', identifier: built.batch.id };
789
- }
790
- async function mark(local, locator, identifier, submission, lost) {
791
- const state = pendings.get(identifier);
792
- if (!continuing() ||
793
- state === undefined ||
794
- (JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
795
- return;
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);
796
1310
  }
797
- await local.submit(locator, identifier, submission, lost);
798
- pendings.set(identifier, { ...state, submission, lost });
799
- const replica = replicas.get(locator);
800
- if (replica !== undefined)
801
- publishChangeSet(changeSet(locator, replica, replica, { queue: true }));
802
- moveTo({ ...status, ...attention() });
1311
+ await durably((local) => unpair(accepting, local));
1312
+ await eraseFenced(fresh);
803
1313
  }
804
- async function keepUpload(local, locator, identifier, upload) {
805
- if (!continuing())
806
- throw new Error('connection refused');
807
- await local.keepUpload(locator, identifier, upload);
808
- pendings.set(identifier, { ...pendings.get(identifier), upload });
809
- if (!continuing())
810
- throw new Error('connection refused');
1314
+ async function open(opened) {
1315
+ if (closed)
1316
+ throw new Error('runtime closed');
1317
+ const interrupted = await opened.fences();
1318
+ const kept = options.shell.generation;
1319
+ const storedGeneration = await opened.generation();
1320
+ const loss = detectStorageLoss(storedGeneration, kept);
1321
+ const generation = nextGeneration(storedGeneration, kept);
1322
+ if (generation !== storedGeneration)
1323
+ await opened.rememberGeneration(generation);
1324
+ if (generation !== kept)
1325
+ await options.shell.keepGeneration(generation);
1326
+ if (loss !== null) {
1327
+ storageLoss = loss;
1328
+ emitStorageLoss();
1329
+ }
1330
+ const held = await opened.opened();
1331
+ heldWhenOpened = usableWhenHeld.length > 0 && (await opened.covering()).rooms.size > 0;
1332
+ try {
1333
+ await opened.demote();
1334
+ await database.ask({ kind: 'read.builds', build });
1335
+ for (const key of held.orphans)
1336
+ await opened.release(key);
1337
+ }
1338
+ catch (error) {
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
+ });
1351
+ }
1352
+ moveTo(status);
1353
+ prepare();
1354
+ emitRooms();
1355
+ void exclusive(() => unpair(accepting, opened)).catch(() => {
1356
+ journal.record('unpair failed');
1357
+ });
1358
+ return opened;
811
1359
  }
812
1360
  async function readHistory(locator, path, query = {}) {
813
1361
  if (closed || refusal !== null)
814
- return { kind: 'transport-uncertainty' };
1362
+ return UNCERTAIN;
1363
+ await firstDiscovery;
1364
+ if (!roomsNamed)
1365
+ return UNCERTAIN;
815
1366
  try {
816
1367
  await ensureAuthorization();
817
1368
  }
818
1369
  catch {
819
- return { kind: 'transport-uncertainty' };
820
- }
821
- if (closed || authorization === undefined || refusal !== null) {
822
- return { kind: 'transport-uncertainty' };
1370
+ return UNCERTAIN;
823
1371
  }
1372
+ if (closed || authorization === undefined || refusal !== null)
1373
+ return UNCERTAIN;
1374
+ const since = store.fences.count();
824
1375
  const transfer = ['transfer', 'history', locator, path];
825
1376
  if (query.revision !== undefined)
826
1377
  transfer.push(query.revision);
@@ -835,29 +1386,64 @@ export function composeRuntime(options) {
835
1386
  ...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
836
1387
  });
837
1388
  journal.record(`outcome history ${result.kind}`);
1389
+ if (store.fences.since(locator, since))
1390
+ return failure({ kind: 'room-not-readable', locator });
838
1391
  if (result.kind === 'scoped-refusal') {
839
- const room = replicas.get(result.locator)?.confirmed.record;
1392
+ const room = await recordAt(result.locator);
840
1393
  if (room !== undefined)
841
1394
  await scoped(result, room, await openLocal());
842
1395
  }
843
1396
  if (result.kind === 'revoked-authorization')
844
1397
  await refuse('revoked-authorization');
845
- return result;
1398
+ return historyAnswer(result);
846
1399
  }
847
1400
  catch {
848
1401
  journal.record('outcome history transport-uncertainty');
849
- return { kind: 'transport-uncertainty' };
850
- }
851
- }
852
- function declaredCards() {
853
- for (const capability of capabilities) {
854
- if (capability.conversationCards !== undefined)
855
- return capability.conversationCards;
856
- }
857
- return undefined;
858
- }
859
- function declaredAction(name) {
860
- for (const capability of capabilities) {
1402
+ return UNCERTAIN;
1403
+ }
1404
+ }
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) {
861
1447
  for (const action of capability.actions ?? []) {
862
1448
  if (action.name === name)
863
1449
  return action;
@@ -865,26 +1451,33 @@ export function composeRuntime(options) {
865
1451
  }
866
1452
  return undefined;
867
1453
  }
868
- 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) {
869
1462
  await ready;
870
1463
  if (closed || refusal !== null)
871
- return { kind: 'transport-uncertainty' };
872
- if (declaredAction(name) === undefined)
873
- return { kind: 'undeclared' };
1464
+ return UNCERTAIN;
1465
+ if ((await declaredAction(name)) === undefined)
1466
+ return undeclared(name);
874
1467
  try {
875
1468
  await ensureAuthorization();
876
1469
  }
877
1470
  catch {
878
- return { kind: 'transport-uncertainty' };
879
- }
880
- if (closed || authorization === undefined || refusal !== null) {
881
- return { kind: 'transport-uncertainty' };
1471
+ return UNCERTAIN;
882
1472
  }
1473
+ if (closed || authorization === undefined || refusal !== null)
1474
+ return UNCERTAIN;
1475
+ await dispatching?.();
883
1476
  journal.record(`transfer action ${name}`);
884
1477
  try {
885
1478
  const result = await options.adapter.action(authorization, { name, input });
886
1479
  if (result.kind === 'scoped-refusal') {
887
- const room = replicas.get(result.locator)?.confirmed.record;
1480
+ const room = await recordAt(result.locator);
888
1481
  if (room !== undefined)
889
1482
  await scoped(result, room, await openLocal());
890
1483
  }
@@ -893,98 +1486,165 @@ export function composeRuntime(options) {
893
1486
  journal.record(`outcome action ${result.kind}`);
894
1487
  if (result.kind === 'rejected')
895
1488
  journal.record(`refusal rejected ${result.code}`);
896
- return result;
1489
+ return calledResult(result);
897
1490
  }
898
1491
  catch {
899
1492
  journal.record('outcome action transport-uncertainty');
900
- return { kind: 'transport-uncertainty' };
1493
+ return UNCERTAIN;
901
1494
  }
902
1495
  }
903
- function continuing() {
904
- return !closed && !discoveryPaused;
905
- }
906
- function reachedBy(locator, roomVersion) {
907
- const held = replicas.get(locator);
908
- return (held !== undefined &&
909
- options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion));
910
- }
911
- function writtenAfter(locator, roomVersion, path) {
912
- const written = replicas.get(locator)?.confirmed.record.written[path];
913
- return (written !== undefined &&
914
- options.adapter.reached(locator, written, roomVersion) &&
915
- !options.adapter.reached(locator, roomVersion, written));
916
- }
917
- async function pullRefresh(authorized, confirmed) {
918
- if (!continuing())
919
- 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');
920
1502
  try {
921
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
922
- const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
923
- journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
924
- 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);
925
1508
  }
926
- catch (error) {
927
- journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
928
- throw error;
1509
+ catch {
1510
+ journal.record('outcome conversation history transport-uncertainty');
1511
+ return UNCERTAIN;
929
1512
  }
930
1513
  }
931
- async function pullBootstrap(authorized, room) {
932
- if (!continuing())
933
- return unconfirmed;
1514
+ function unsettled(result) {
1515
+ return result.kind !== 'action' && result.retryable;
1516
+ }
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);
934
1523
  try {
935
- const result = await bootstrapRoom(options.adapter, authorized, room, {
936
- collection: modelOf(room.locator),
937
- selection: modelOf(room.locator, 'typed-selection'),
938
- }, (models, held) => remodel(room.locator, models, held), continuing);
939
- const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
940
- 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);
941
1537
  return result;
942
1538
  }
943
- catch (error) {
944
- journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
945
- throw error;
1539
+ finally {
1540
+ calling.delete(operation);
946
1541
  }
947
1542
  }
948
- async function advance(room, local, authorized) {
949
- const held = replicas.get(room.locator);
950
- if (held !== undefined) {
951
- const refreshed = await pullRefresh(authorized, held.confirmed);
952
- if (!continuing())
953
- return unconfirmed;
954
- if (refreshed.kind === 'still-current') {
955
- if (sourceRefusals.has(room.locator))
956
- return unconfirmed;
957
- stale.delete(room.locator);
958
- if (unreadable.delete(room.locator)) {
959
- scopedRefusals.delete(room.locator);
960
- list(room, true);
961
- emitRooms();
962
- }
963
- 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);
964
1557
  }
965
- if (refreshed.kind === 'advanced') {
966
- if (!(await publish(local, room, refreshed.confirmed, held.confirmed))) {
967
- const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
968
- const reached = now !== undefined &&
969
- options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
970
- return reached ? null : advance(room, local, authorized);
971
- }
972
- await retainModel(local, room, 'collection', modelOf(room.locator));
973
- return null;
1558
+ finally {
1559
+ calling.delete(held.operation);
974
1560
  }
975
- if (refreshed.kind !== 'beyond-horizon')
976
- return refreshed;
977
1561
  }
978
- const bootstrapped = await pullBootstrap(authorized, room);
979
- 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())
980
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);
981
1592
  if (bootstrapped.kind !== 'staged')
982
- return bootstrapped;
983
- await publish(local, room, bootstrapped.confirmed);
984
- 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);
1601
+ }
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') {
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;
985
1631
  }
986
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
+ }
987
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
+ }
1646
+ else
1647
+ repairable.delete(room.locator);
988
1648
  if (withheld.kind === 'scoped-refusal') {
989
1649
  await scoped(withheld, room, local);
990
1650
  }
@@ -995,319 +1655,156 @@ export function composeRuntime(options) {
995
1655
  journal.record(`refusal ${withheld.kind} ${withheld.code}`);
996
1656
  }
997
1657
  else if (withheld.kind === 'room-not-readable') {
998
- unreadable.add(room.locator);
999
- list(room, false);
1000
- emitRooms();
1001
- }
1002
- else if (withheld.kind === 'refused') {
1003
- refusedSource(room, withheld);
1658
+ roomUnreadable(room);
1004
1659
  }
1005
1660
  else if (withheld.kind === 'revoked-authorization') {
1006
1661
  await refuse('revoked-authorization');
1007
1662
  }
1008
1663
  }
1009
- function refusedSource(room, { refusal }) {
1010
- sourceRefusals.set(room.locator, {
1011
- code: refusal.rule,
1012
- fields: {
1013
- file: refusal.file,
1014
- ...(refusal.location === undefined ? {} : { location: refusal.location }),
1015
- ...(refusal.referrers === undefined ? {} : { referrers: refusal.referrers }),
1016
- },
1017
- });
1018
- declarationRefusals.delete(room.locator);
1664
+ function roomUnreadable(room) {
1019
1665
  unreadable.add(room.locator);
1020
1666
  list(room, false);
1021
1667
  emitRooms();
1022
- journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
1023
- }
1024
- function leaving(batch, result) {
1025
- if (result.kind === 'expired')
1026
- return { batch, returned: { kind: 'expired' } };
1027
- return { batch, returned: { kind: result.kind, code: result.code, fields: result.fields } };
1028
- }
1029
- function logAccepted(locator, submitted, accepted) {
1030
- journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
1031
- (accepted.author === undefined ? '' : ` author ${accepted.author}`) +
1032
- (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1033
- }
1034
- async function adoptAccepted(room, local, submitted, accepted) {
1035
- if (!continuing())
1036
- return unconfirmed;
1037
- const current = replicas.get(room.locator);
1038
- if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1039
- return null;
1040
- }
1041
- const held = current.confirmed.record.roomVersion;
1042
- logAccepted(room.locator, submitted, accepted);
1043
- const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1044
- ? { kind: 'staged', confirmed: current.confirmed }
1045
- : applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
1046
- ? accepted.roomVersion
1047
- : held, accepted.roomVersion);
1048
- if (applied.kind !== 'staged') {
1049
- const state = pendings.get(submitted.id);
1050
- await local.accept(room.locator, submitted.id, accepted.roomVersion);
1051
- if (state !== undefined)
1052
- pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1053
- emitQueue(room.locator);
1054
- return applied;
1055
- }
1056
- const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1057
- pendings.delete(submitted.id);
1058
- unreadable.delete(room.locator);
1059
- sourceRefusals.delete(room.locator);
1060
- declarationRefusals.delete(room.locator);
1061
- list(room, true);
1062
- publishChangeSet(set);
1063
- moveTo({ ...status, ...attention() });
1064
- return null;
1065
- }
1066
- async function adopted(room, local, submitted, accepted) {
1067
- const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
1068
- if (failed === null)
1069
- return true;
1070
- if (failed.kind === 'refused' && continuing())
1071
- refusedSource(room, failed);
1072
- return false;
1073
1668
  }
1074
- async function submitRoom(room, local, authorized, progress) {
1075
- while (continuing() && authorization !== undefined) {
1076
- const replica = replicas.get(room.locator);
1077
- const head = replica?.queue[0];
1078
- if (replica === undefined || head === undefined)
1079
- return;
1080
- if (pendings.get(head.id)?.accepted !== undefined)
1081
- return;
1082
- const state = pendings.get(head.id);
1083
- const { submission, lost: earlier } = state ?? {
1084
- submission: { state: 'waiting' },
1085
- lost: false,
1086
- };
1087
- if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
1088
- progress.complete = false;
1089
- return;
1090
- }
1091
- if (writesHeld()) {
1092
- progress.complete = false;
1093
- journal.record(`submission ${room.locator} ${head.id} held`);
1094
- return;
1095
- }
1096
- const lost = earlier || submission.state === 'submitted';
1097
- let result;
1098
- journal.record(`submission ${room.locator} ${head.id}`);
1099
- await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1100
- if (discoveryPaused)
1101
- return;
1102
- if (writesHeld()) {
1103
- await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
1104
- progress.complete = false;
1105
- journal.record(`submission ${room.locator} ${head.id} held`);
1106
- return;
1107
- }
1108
- const opensBefore = openSignals;
1109
- const answered = awaitingAnswer(room.locator);
1110
- try {
1111
- result = await options.adapter.mutate(authorized, {
1112
- locator: room.locator,
1113
- changeBatch: head,
1114
- ...(state?.upload === undefined ? {} : { upload: state.upload }),
1115
- }, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
1116
- }
1117
- catch {
1118
- answered();
1119
- progress.complete = false;
1120
- if (continuing())
1121
- 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)
1122
1681
  return;
1123
- }
1124
- if (result.kind !== 'accepted' || !continuing())
1125
- 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 () => {
1126
1712
  if (!continuing())
1127
1713
  return;
1128
- if (result.kind === 'transport-uncertainty') {
1129
- progress.complete = false;
1130
- progress.reachable = false;
1131
- return;
1132
- }
1133
- if (result.kind === 'room-not-readable') {
1134
- progress.complete = false;
1135
- unreadable.add(room.locator);
1136
- list(room, false);
1137
- emitRooms();
1138
- return;
1139
- }
1140
- if (result.kind === 'revoked-authorization') {
1141
- await refuse('revoked-authorization');
1142
- return;
1143
- }
1144
- if (result.kind === 'scoped-refusal') {
1145
- await scoped(result, room, local);
1146
- progress.complete = false;
1147
- return;
1148
- }
1149
- if (result.kind === 'write-halted') {
1150
- journal.record(`outcome ${room.locator} ${head.id} write-halted`);
1151
- await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, lost));
1152
- if (serviceMode === 'open' && openSignals !== opensBefore)
1153
- continue;
1154
- haltedRefusal = true;
1155
- progress.complete = false;
1156
- return;
1157
- }
1158
- if (result.kind === 'conflicted' ||
1159
- result.kind === 'rejected' ||
1160
- result.kind === 'expired' ||
1161
- result.kind === 'retryable-refusal') {
1162
- if (result.kind === 'expired') {
1163
- const withheld = await advance(room, local, authorized);
1164
- if (withheld !== null)
1165
- return withhold(withheld, room, local, progress);
1166
- }
1167
- journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
1168
- (result.kind === 'conflicted' && result.roomVersion !== undefined
1169
- ? ` ${result.roomVersion}`
1170
- : ''));
1171
- if (result.kind !== 'expired')
1172
- journal.record(`refusal ${result.kind} ${result.code}`);
1173
- }
1174
- if (result.kind === 'retryable-refusal') {
1175
- const reset = result.reset;
1176
- progress.complete = false;
1177
- await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1178
- armTimer();
1179
- return;
1180
- }
1181
- if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
1182
- const left = leaving(head, result);
1183
- await exclusive(async () => {
1184
- if (!continuing())
1185
- return;
1186
- const current = replicas.get(room.locator);
1187
- if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
1188
- return;
1189
- }
1190
- const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1191
- leaving: result.kind === 'expired' && landed(head, current.confirmed)
1192
- ? { ...left, returned: null }
1193
- : left,
1194
- });
1195
- publishChangeSet(set);
1196
- moveTo({ ...status, ...attention() });
1197
- });
1198
- if (result.kind === 'expired')
1199
- continue;
1200
- const withheld = await advance(room, local, authorized);
1201
- if (withheld !== null)
1202
- return withhold(withheld, room, local, progress);
1203
- continue;
1204
- }
1205
- const kept = await adopted(room, local, head, result).finally(answered);
1206
- if (!kept) {
1207
- progress.complete = false;
1714
+ const current = await recordAt(room.locator);
1715
+ if (current === undefined)
1208
1716
  return;
1209
- }
1210
- }
1211
- }
1212
- function awaitingAnswer(locator) {
1213
- let release = () => { };
1214
- const awaited = new Promise((resolve) => {
1215
- 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();
1216
1729
  });
1217
- answering.set(locator, awaited);
1218
- return () => {
1219
- if (answering.get(locator) === awaited)
1220
- answering.delete(locator);
1221
- release();
1222
- };
1223
1730
  }
1224
- async function retainModel(local, room, attached, next) {
1225
- const key = modelKey(room.locator, attached);
1226
- const previous = modelOf(room.locator, attached);
1227
- const heldBefore = replicas.get(room.locator)?.confirmed.record;
1228
- if (JSON.stringify(previous) === JSON.stringify(next) &&
1229
- (heldBefore === undefined ||
1230
- (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
1231
- JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
1232
- JSON.stringify(heldBefore.typedSelection) ===
1233
- 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))
1234
1739
  return;
1235
- if (next.status === 'unavailable') {
1236
- invalidModels.add(key);
1237
- liveModels.delete(key);
1238
- }
1239
1740
  await exclusive(async () => {
1240
1741
  if (!continuing())
1241
1742
  return;
1242
- const held = replicas.get(room.locator);
1243
- if (held !== undefined) {
1244
- const declared = {
1245
- ...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,
1246
1751
  operations: room.operations,
1247
1752
  replica: room.replica,
1753
+ providerOwnedPaths: room.providerOwnedPaths,
1248
1754
  typedSelection: room.typedSelection ?? null,
1249
- ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1755
+ model: next,
1250
1756
  };
1251
- const reclassified = sameMembership(declared, held.confirmed.record)
1252
- ? readConfirmed(declared, held.confirmed)
1253
- : readReclassified(declared, held.confirmed);
1254
- const record = reclassified.kind === 'staged'
1255
- ? declared
1256
- : {
1257
- ...declared,
1258
- typedSelection: held.confirmed.record.typedSelection,
1259
- };
1260
- const read = reclassified.kind === 'deferred' ? readConfirmed(record, held.confirmed) : reclassified;
1261
- await local.write({
1262
- next: record,
1263
- previous: held.confirmed.record,
1264
- queue: held.queue,
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,
1265
1776
  });
1266
- if (read.kind === 'staged') {
1267
- replicas.set(room.locator, replicaOf(read.confirmed, held.queue, rewriting(), held));
1268
- if (declarationRefusals.delete(room.locator)) {
1269
- sourceRefusals.delete(room.locator);
1270
- unreadable.delete(room.locator);
1271
- }
1272
- }
1273
- else if (read.kind === 'refused') {
1274
- replicas.set(room.locator, { ...held, confirmed: { ...held.confirmed, record } });
1275
- refusedSource(room, read);
1276
- declarationRefusals.add(room.locator);
1277
- }
1278
- }
1279
- models.set(key, next);
1280
- if (next.status === 'current') {
1281
- liveModels.set(key, JSON.stringify(next.snapshot));
1282
- invalidModels.delete(key);
1283
1777
  }
1778
+ else
1779
+ unheldModels.set(room.locator, next);
1284
1780
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
1285
- if (held === undefined)
1781
+ if (changed === null)
1286
1782
  emitRooms();
1287
1783
  else
1288
- publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
1784
+ publishChangeSet(changed);
1289
1785
  });
1290
1786
  }
1291
1787
  async function scoped(result, room, local) {
1292
- scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
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
+ }
1793
+ scopedRefusals.set(room.locator, {
1794
+ code: result.code,
1795
+ fields: result.fields,
1796
+ retryable: result.retryable,
1797
+ });
1293
1798
  let scopedAway = room;
1294
1799
  if (result.scope === 'model')
1295
- await retainModel(local, room, 'collection', {
1800
+ await retainModel(local, room, {
1296
1801
  status: 'unavailable',
1297
1802
  reason: 'not-readable',
1298
1803
  });
1299
1804
  else if (result.scope !== 'operation') {
1300
1805
  unreadable.add(room.locator);
1301
- scopedAway = {
1302
- ...room,
1303
- replica: { ...room.replica, admission: 'unavailable' },
1304
- operations: {
1305
- ...room.operations,
1306
- ...(result.scope === 'content' ? { read: [] } : {}),
1307
- ...(result.scope === 'sync' ? { sync: [] } : {}),
1308
- },
1309
- };
1310
- await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
1806
+ scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
1807
+ await retainModel(local, scopedAway, null);
1311
1808
  }
1312
1809
  if (!continuing())
1313
1810
  return;
@@ -1315,141 +1812,194 @@ export function composeRuntime(options) {
1315
1812
  emitRooms();
1316
1813
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
1317
1814
  }
1318
- 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));
1319
1826
  return listed.flatMap((room) => {
1320
- const record = replicas.get(room.locator)?.confirmed.record;
1321
- 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)
1322
1834
  return [];
1323
- return [
1324
- {
1325
- locator: room.locator,
1326
- roomVersion: record.roomVersion,
1327
- documents: record.documents,
1328
- revisions: record.revisions,
1329
- attachments: record.attachments,
1330
- },
1331
- ];
1835
+ return [{ locator: room.locator, roomVersion: record.roomVersion }];
1332
1836
  });
1333
1837
  }
1334
- async function synchronize(listed, local, progress, attempt) {
1335
- 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;
1336
1843
  const ready = listed.filter((_, index) => {
1337
1844
  const outcome = prepared[index];
1338
1845
  return outcome?.status === 'fulfilled' && outcome.value;
1339
1846
  });
1340
- if (attempt === 'opened')
1341
- options.adapter.roomStream?.hold(heldRooms(ready));
1342
- const loaded = await Promise.allSettled(ready
1343
- .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1344
- .map((room) => loadRoom(room, local, progress)));
1345
- const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1847
+ const held = await heldRooms(ready);
1848
+ if (!live())
1849
+ return;
1850
+ if (attempt === 'opened') {
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));
1863
+ }
1864
+ const failed = prepared.find((room) => room.status === 'rejected');
1346
1865
  if (failed !== undefined)
1347
1866
  throw failed.reason;
1348
1867
  }
1349
- async function prepareRoom(room, local, progress) {
1868
+ async function prepareRoom(room, local, progress, prior) {
1350
1869
  if (closed || authorization === undefined) {
1351
1870
  progress.complete = false;
1352
1871
  return false;
1353
1872
  }
1354
- const prior = replicas.get(room.locator)?.confirmed.record;
1355
1873
  if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
1356
- list(prior, false);
1874
+ listHeld(room.locator);
1357
1875
  emitRooms();
1358
1876
  journal.record(`selection changed ${room.locator}`);
1359
1877
  progress.complete = false;
1360
1878
  return false;
1361
1879
  }
1362
- if (prior !== undefined)
1363
- 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
+ }
1364
1885
  if (discoveryPaused || authorization === undefined)
1365
1886
  return false;
1366
- if (room.replica.admission !== 'admitted') {
1367
- if (replicas.has(room.locator))
1368
- progress.complete = false;
1369
- return false;
1370
- }
1371
- return true;
1887
+ return room.replica.admission === 'admitted';
1372
1888
  }
1373
- async function caughtUp(room, local, progress) {
1374
- const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1375
- if (held === undefined || authorization === undefined)
1889
+ async function caughtUp(room, local, progress, live) {
1890
+ if (options.adapter.roomStream !== undefined || authorization === undefined)
1376
1891
  return;
1377
- const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1378
- if (!behind)
1892
+ if (!live() || !(await behind(installer, room)))
1379
1893
  return;
1380
- const withheld = await advance(room, local, authorization);
1894
+ const withheld = await advance(room, local, authorization, live);
1381
1895
  if (withheld !== null)
1382
1896
  await withhold(withheld, room, local, progress);
1383
1897
  }
1384
- function revoked(locator) {
1385
- return ['collection', 'typed-selection'].some((attached) => {
1386
- const model = modelOf(locator, attached);
1387
- return model.status === 'unavailable' && model.reason === 'revoked';
1388
- });
1898
+ function revoked(model) {
1899
+ return model?.status === 'unavailable' && model.reason === 'revoked';
1389
1900
  }
1390
- async function loadRoom(room, local, progress) {
1391
- if (discoveryPaused || authorization === undefined)
1392
- return;
1393
- if (replicas.has(room.locator) && revoked(room.locator)) {
1394
- await reload(room, local, progress);
1901
+ async function loadRoom(room, local, progress, live, unheld) {
1902
+ if (!live() || authorization === undefined)
1395
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
+ }
1396
1912
  }
1397
- const withheld = await advance(room, local, authorization);
1913
+ const withheld = await advance(room, local, authorization, live, unheld);
1398
1914
  if (withheld !== null) {
1399
1915
  await withhold(withheld, room, local, progress);
1400
1916
  return;
1401
1917
  }
1402
- await caughtUp(room, local, progress);
1918
+ await caughtUp(room, local, progress, live);
1403
1919
  }
1404
- async function reload(room, local, progress) {
1405
- if (discoveryPaused || authorization === undefined)
1406
- return;
1407
- const bootstrapped = await pullBootstrap(authorization, room);
1408
- if (!continuing())
1409
- return;
1410
- if (bootstrapped.kind !== 'staged') {
1411
- await withhold(bootstrapped, room, local, progress);
1412
- return;
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}`;
1413
1963
  }
1414
- await publish(local, room, bootstrapped.confirmed);
1415
- await caughtUp(room, local, progress);
1964
+ return null;
1965
+ }
1966
+ async function signIn(local, signedIn) {
1967
+ if (JSON.stringify(signedIn) === JSON.stringify((await local.facts()).member))
1968
+ return;
1969
+ await local.fact('member', signedIn);
1970
+ emitMember(signedIn);
1416
1971
  }
1417
- async function discoverRooms(local, progress, attempt) {
1972
+ async function discoverRooms(local, progress, attempt, live, found) {
1418
1973
  if (closed || authorization === undefined) {
1419
1974
  notifyReady();
1420
1975
  return;
1421
1976
  }
1422
- moveTo(transferring(status));
1423
- let result;
1424
- try {
1425
- result = await options.adapter.discover(authorization);
1426
- }
1427
- catch {
1428
- result = { kind: 'transport-uncertainty' };
1429
- }
1430
- 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;
1431
1985
  notifyReady();
1432
1986
  return;
1433
1987
  }
1434
1988
  journal.record(`connection discover ${result.kind}`);
1435
1989
  if (result.kind === 'transport-uncertainty') {
1436
- for (const room of rooms.values()) {
1437
- await retainModel(local, room, 'collection', cached(modelOf(room.locator)));
1438
- if (room.typedSelection !== null) {
1439
- await retainModel(local, room, 'typed-selection', cached(modelOf(room.locator, 'typed-selection')));
1440
- }
1441
- }
1990
+ await cachedModels(local);
1442
1991
  progress.reachable = false;
1443
1992
  progress.complete = false;
1444
- if (attempt === 'opened') {
1445
- options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1446
- }
1447
1993
  notifyReady();
1448
1994
  return;
1449
1995
  }
1450
1996
  if (result.kind === 'contract-unavailable') {
1451
1997
  journal.record(`connection contract unavailable ${result.code}`);
1452
- await refuse('missing-required-guarantee', { code: result.code, fields: result.fields });
1998
+ await refuse('missing-required-guarantee', {
1999
+ code: result.code,
2000
+ fields: result.fields,
2001
+ retryable: result.retryable,
2002
+ });
1453
2003
  notifyReady();
1454
2004
  return;
1455
2005
  }
@@ -1458,59 +2008,26 @@ export function composeRuntime(options) {
1458
2008
  notifyReady();
1459
2009
  return;
1460
2010
  }
1461
- if (!result.supportedVersions.includes(ROOM_CONTRACT_VERSION)) {
1462
- await refuse('no-matching-version');
1463
- notifyReady();
1464
- return;
1465
- }
1466
- if (result.mdModel.identity !== MD_MODEL.identity ||
1467
- result.mdModel.version !== MD_MODEL.version) {
1468
- await refuse('md-model-version-mismatch');
2011
+ const invalid = malformed(result.rooms);
2012
+ if (invalid !== null) {
2013
+ journal.record(invalid);
2014
+ await refuse('missing-required-guarantee');
1469
2015
  notifyReady();
1470
2016
  return;
1471
2017
  }
1472
- for (const room of result.rooms) {
1473
- if (room.operations === undefined ||
1474
- !['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
1475
- room.operations[operation].every((scope) => typeof scope === 'string')) ||
1476
- room.replica === undefined ||
1477
- typeof room.replica.selection !== 'string' ||
1478
- room.replica.selection.length === 0 ||
1479
- !['admitted', 'unavailable'].includes(room.replica.admission)) {
1480
- journal.record(`invalid model or replica declaration ${room.locator}`);
1481
- await refuse('missing-required-guarantee');
1482
- notifyReady();
1483
- return;
1484
- }
1485
- if (!declaredSelection(room)) {
1486
- journal.record(`invalid typed selection declaration ${room.locator}`);
1487
- await refuse('missing-required-guarantee');
1488
- notifyReady();
1489
- return;
1490
- }
1491
- }
1492
- const declared = new Set(result.capabilities.map((capability) => capability.name));
1493
- for (const required of options.requiredGuarantees) {
1494
- if (!knownCapabilities.has(required) || !declared.has(required)) {
1495
- await refuse('missing-required-guarantee');
1496
- notifyReady();
1497
- return;
1498
- }
1499
- }
1500
2018
  if (refusal !== null) {
1501
2019
  refusal = null;
1502
2020
  emitRefusal();
1503
2021
  }
1504
- const previousCapabilities = capabilities;
1505
- capabilities = result.capabilities;
1506
- notifyReady();
1507
- if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1508
- JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
1509
- limits = result.providerLimits;
1510
- await local.declare(limits, capabilities);
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);
1511
2027
  }
1512
- await applyDiscovery(result.rooms, local);
1513
- 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);
1514
2031
  }
1515
2032
  async function ensureAuthorization() {
1516
2033
  if (authorization !== undefined)
@@ -1524,247 +2041,538 @@ export function composeRuntime(options) {
1524
2041
  throw error;
1525
2042
  }
1526
2043
  }
1527
- 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) {
1528
2078
  if (stopping)
1529
- return chain;
1530
- chain = chain.then(work, work).catch(() => undefined);
1531
- return chain;
2079
+ return Promise.resolve();
2080
+ return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
1532
2081
  }
1533
- function sync(attempt) {
1534
- return task(async () => {
1535
- if (closed || stopping || discoveryPaused) {
1536
- 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();
1537
2104
  return;
1538
2105
  }
1539
2106
  const progress = { complete: true, reachable: true };
1540
2107
  underway = attempt === 'opened' ? progress : null;
2108
+ if (attempt === 'opened')
2109
+ streamed = progress;
1541
2110
  complete = false;
1542
2111
  recorded = false;
1543
- moveTo(transferring(status));
2112
+ moveTo(transferring());
1544
2113
  try {
1545
2114
  const local = await openLocal();
1546
- if (closed)
2115
+ if (!live())
1547
2116
  return;
1548
2117
  await ensureAuthorization();
1549
- if (closed)
2118
+ if (!live())
1550
2119
  return;
1551
- await discoverRooms(local, progress, attempt);
2120
+ await discoverRooms(local, progress, attempt, live, found);
1552
2121
  }
1553
2122
  catch {
1554
2123
  progress.complete = false;
1555
- if (attempt === 'opened') {
1556
- options.adapter.roomStream?.hold(heldRooms([...discovered.values()]));
1557
- }
2124
+ if (attempt === 'opened' && !socket.isHanded())
2125
+ progress.reachable = false;
1558
2126
  }
1559
2127
  finally {
1560
2128
  notifyReady();
1561
2129
  }
1562
- if (attempt === 'opened')
2130
+ if (!live())
2131
+ return;
2132
+ if (attempt !== 'opened') {
2133
+ await finishOpening(progress, live);
2134
+ return;
2135
+ }
2136
+ if (socket.isHanded() || closed || progress.reachable)
2137
+ return;
2138
+ underway = null;
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())
1563
2194
  return;
1564
- await finishOpening(progress);
2195
+ if (!progress.complete)
2196
+ complete = false;
2197
+ await rest(progress, live);
1565
2198
  });
1566
2199
  }
1567
- function closedSocket(revoked) {
1568
- const opened = socketOpen;
1569
- socketOpen = false;
2200
+ function closedSocket(revoked, lost, opened) {
2201
+ notifyReady();
2202
+ unreadied();
2203
+ connectionLost();
2204
+ if (lost)
2205
+ conversations.lost();
1570
2206
  if (revoked) {
1571
2207
  void refuse('revoked-authorization');
1572
2208
  return;
1573
2209
  }
1574
- if (opened || status.providerUnreachable)
1575
- return;
1576
- moveTo({ ...status, providerUnreachable: true });
2210
+ complete = false;
2211
+ if (streamed !== null)
2212
+ streamed.complete = false;
2213
+ moveTo({
2214
+ ...status,
2215
+ state: status.state === 'synced' ? 'syncing' : status.state,
2216
+ providerUnreachable: status.providerUnreachable || (!opened && lost),
2217
+ });
2218
+ void closing(opened);
1577
2219
  }
1578
- function applyOpening(expired) {
1579
- return task(async () => {
1580
- const progress = underway;
1581
- underway = null;
1582
- 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())
1583
2251
  return;
1584
2252
  if (progress === null) {
1585
- await reloadExpired(expired);
2253
+ await replacing;
2254
+ await rest(cycle, live);
1586
2255
  return;
1587
2256
  }
1588
2257
  try {
1589
2258
  const local = await openLocal();
1590
- const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1591
- const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1592
- const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
2259
+ await replacing;
1593
2260
  if (progress.reachable) {
1594
- for (const room of held)
1595
- if (!reloading.includes(room))
1596
- 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);
1597
2266
  }
1598
- const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1599
- if (failed !== undefined)
1600
- throw failed.reason;
1601
2267
  }
1602
2268
  catch {
1603
2269
  progress.complete = false;
1604
2270
  progress.reachable = false;
1605
2271
  }
1606
- await finishOpening(progress);
2272
+ if (live())
2273
+ await finishOpening(progress, live);
1607
2274
  });
1608
2275
  }
1609
- async function reloadExpired(expired) {
1610
- 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);
1611
2292
  try {
1612
2293
  const local = await openLocal();
1613
- await Promise.allSettled([...discovered.values()]
1614
- .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1615
- .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
+ }
1616
2304
  }
1617
2305
  catch {
2306
+ for (const room of reloaded)
2307
+ reloadsDue.delete(room.locator);
1618
2308
  progress.reachable = false;
1619
2309
  }
1620
- await rest(progress);
1621
2310
  }
1622
- async function confirmModels(room, local) {
1623
- for (const attached of ['collection', 'typed-selection']) {
1624
- const model = modelOf(room.locator, attached);
1625
- if (model.status === 'cached') {
1626
- 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));
1627
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 });
1628
2362
  }
1629
2363
  }
1630
- function submitLater(locator) {
1631
- const running = submitting.get(locator);
1632
- if (running !== undefined) {
1633
- running.again = true;
2364
+ function repairLater(locator, when) {
2365
+ if (!socket.isOpen() || repairing.has(locator))
1634
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
+ });
1635
2386
  }
1636
- const run = { again: false, done: Promise.resolve() };
1637
- submitting.set(locator, run);
1638
- run.done = (async () => {
1639
- try {
1640
- do {
1641
- run.again = false;
1642
- const room = discovered.get(locator);
1643
- if (closed || stopping || discoveryPaused || room === undefined)
1644
- return;
1645
- const progress = { complete: true, reachable: true };
1646
- try {
1647
- await ensureAuthorization();
1648
- if (authorization === undefined)
1649
- return;
1650
- await submitRoom(room, await openLocal(), authorization, progress);
1651
- }
1652
- catch {
1653
- progress.reachable = false;
1654
- }
1655
- armTimer();
1656
- await rest(progress);
1657
- } while (run.again);
1658
- }
1659
- finally {
1660
- submitting.delete(locator);
1661
- }
1662
- })();
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 });
1663
2411
  }
1664
- function submitAll() {
1665
- for (const [locator, replica] of replicas) {
1666
- if (replica.queue.length > 0)
1667
- 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;
1668
2421
  }
2422
+ if (signal.aborted)
2423
+ return 'done';
2424
+ return reading.reachable && reading.complete ? 'done' : 'again';
1669
2425
  }
1670
- async function finishOpening(progress) {
2426
+ async function finishOpening(progress, live) {
1671
2427
  try {
1672
- if (closed || discoveryPaused) {
2428
+ if (!live()) {
1673
2429
  progress.complete = false;
1674
2430
  return;
1675
2431
  }
1676
- submitAll();
2432
+ await submitAll(accepting);
2433
+ for (const locator of stale)
2434
+ if (repairable.has(locator))
2435
+ repairLater(locator, {});
1677
2436
  await conversations.reconcile();
1678
- if (status.providerUnreachable)
1679
- conversations.reconnected();
1680
2437
  const reading = { complete: true, reachable: true };
1681
2438
  await conversations.catchUp(reading);
1682
- 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) {
1683
2444
  progress.reachable = false;
2445
+ resendLater();
2446
+ }
1684
2447
  }
1685
2448
  catch {
1686
2449
  progress.complete = false;
1687
2450
  }
1688
2451
  finally {
1689
- complete = progress.complete;
1690
- await rest(progress);
2452
+ if (live()) {
2453
+ complete = progress.complete;
2454
+ await rest(progress, live);
2455
+ }
1691
2456
  }
1692
2457
  }
1693
- async function rest(progress) {
1694
- if (closed)
2458
+ async function rest(progress, live = () => !closed) {
2459
+ if (!live())
1695
2460
  return;
1696
- let lastCompleted = status.lastCompleted;
1697
- 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
+ }
1698
2469
  if (synced && !recorded) {
1699
2470
  const completedAt = new Date().toISOString();
1700
2471
  try {
1701
- await (await openLocal()).complete(completedAt);
2472
+ await (await openLocal()).fact('lastCompleted', completedAt);
2473
+ if (!live())
2474
+ return;
1702
2475
  recorded = true;
1703
- lastCompleted = completedAt;
1704
2476
  }
1705
2477
  catch {
1706
2478
  synced = false;
1707
2479
  }
1708
2480
  }
1709
2481
  moveTo({
1710
- state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
1711
- lastCompleted,
1712
- providerUnreachable: !progress.reachable,
1713
- ...attention(),
2482
+ state: synced && complete && roomWork === 0 && !socket.naming() ? 'synced' : 'syncing',
2483
+ providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
2484
+ resting: true,
1714
2485
  });
1715
2486
  }
1716
- function applyRoom(locator, roomVersion) {
1717
- announced.set(locator, roomVersion);
1718
- return task(async () => {
1719
- await answering.get(locator);
1720
- if (closed || stopping || discoveryPaused || authorization === undefined)
1721
- return;
1722
- const room = discovered.get(locator);
1723
- const held = replicas.get(locator);
1724
- if (room === undefined || room.replica.admission !== 'admitted')
1725
- return;
1726
- if (held !== undefined &&
1727
- (held.confirmed.record.replica.selection !== room.replica.selection ||
1728
- options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
1729
- return;
1730
- }
1731
- const during = underway;
1732
- const progress = during ?? { complete: true, reachable: true };
1733
- if (during === null)
1734
- 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 };
1735
2494
  try {
1736
2495
  const local = await openLocal();
1737
- const withheld = await advance(room, local, authorization);
1738
- if (withheld !== null)
1739
- 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
+ }
1740
2536
  }
1741
2537
  catch {
2538
+ if (!stale.has(locator))
2539
+ void name(locator);
1742
2540
  stale.add(locator);
2541
+ return progress;
1743
2542
  }
1744
- finally {
1745
- if (during === null)
1746
- await rest(progress);
1747
- }
1748
- });
2543
+ }).then((progress) => rest(progress), () => undefined);
1749
2544
  }
1750
- function observeStream() {
1751
- const stream = options.adapter.roomStream;
1752
- if (stream === undefined || withdrawStream !== undefined)
2545
+ async function installCarried(room, local, change, live, progress) {
2546
+ if (authorization === undefined)
1753
2547
  return;
1754
- withdrawStream = stream.observe((event) => {
1755
- if (closed || stopping)
1756
- return;
1757
- if (event.kind === 'opened') {
1758
- socketOpen = true;
1759
- void sync('opened');
1760
- }
1761
- else if (event.kind === 'closed')
1762
- closedSocket(event.revoked);
1763
- else if (event.kind === 'opening')
1764
- void applyOpening(event.expired);
1765
- else
1766
- void applyRoom(event.locator, event.roomVersion);
1767
- });
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);
1768
2576
  }
1769
2577
  function writesHeld() {
1770
2578
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -1779,160 +2587,244 @@ export function composeRuntime(options) {
1779
2587
  haltedRefusal = false;
1780
2588
  if (mode !== serviceMode) {
1781
2589
  serviceMode = mode;
2590
+ queueReadiness();
1782
2591
  journal.record(`service mode ${mode}`);
1783
2592
  for (const listener of serviceModeListeners)
1784
2593
  listener(mode);
1785
2594
  }
1786
2595
  if (releasing)
1787
- submitAll();
2596
+ submitAll(accepting);
1788
2597
  }
1789
- function observeConversations() {
1790
- const stream = options.adapter.conversations;
1791
- if (stream === undefined || withdrawConversations !== undefined)
1792
- return;
1793
- withdrawConversations = stream.observe((event) => {
1794
- conversations.receive(event);
1795
- if (event.kind !== 'gap' || conversations.current())
1796
- return;
1797
- show();
1798
- scheduleCurrency();
1799
- });
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 });
1800
2608
  }
1801
2609
  function scheduleCurrency() {
1802
- if (currencyQueued || closed || stopping)
2610
+ if (currencyQueued === socket.generation() || closed || stopping)
1803
2611
  return;
1804
- currencyQueued = true;
1805
- void task(runCurrency);
2612
+ currencyQueued = socket.generation();
2613
+ void task(CONVERSATION_LANE, runCurrency);
1806
2614
  }
1807
- async function runCurrency() {
1808
- currencyQueued = false;
1809
- if (closed || stopping || discoveryPaused)
2615
+ async function runCurrency(live) {
2616
+ if (currencyQueued === socket.generation())
2617
+ currencyQueued = null;
2618
+ if (!live())
1810
2619
  return;
1811
2620
  await conversations.reconcile();
1812
2621
  if (conversations.current()) {
1813
2622
  show();
1814
2623
  return;
1815
2624
  }
1816
- const resting = status;
1817
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1818
- moveTo(transferring(resting));
2625
+ const progress = { complete: false, reachable: !status.providerUnreachable };
2626
+ moveTo(transferring());
1819
2627
  try {
1820
2628
  await conversations.catchUp(progress);
1821
2629
  }
1822
2630
  catch {
1823
2631
  progress.reachable = false;
1824
2632
  }
1825
- finally {
1826
- if (!closed) {
1827
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1828
- }
1829
- }
2633
+ await rest(progress, live);
1830
2634
  }
1831
2635
  function close() {
1832
2636
  if (closed)
1833
2637
  return;
1834
2638
  closed = true;
1835
- withdrawStream?.();
1836
- withdrawStream = undefined;
1837
- withdrawConversations?.();
1838
- withdrawConversations = undefined;
2639
+ notifyDiscovered();
2640
+ unreadied();
2641
+ if (probing !== null)
2642
+ clearInterval(probing);
2643
+ connectionLive();
2644
+ lockListeners.clear();
2645
+ readinessWatches.clear();
2646
+ socket.withdraw();
1839
2647
  withdrawServiceMode?.();
1840
2648
  conversations.close();
1841
- stopTimer();
1842
- store?.close();
1843
- void opening?.then((opened) => {
1844
- opened.close();
1845
- }, () => { });
2649
+ void recovery.retire();
1846
2650
  roomListeners.clear();
2651
+ unlistedRooms.clear();
1847
2652
  refusalListeners.clear();
1848
2653
  storageLossListeners.clear();
1849
2654
  statusListeners.clear();
1850
2655
  serviceModeListeners.clear();
1851
- readSide.close();
2656
+ memberListeners.clear();
2657
+ authorityChangeListeners.clear();
2658
+ liveQueries.close();
2659
+ figures.close();
1852
2660
  queueSubscriptions.clear();
1853
- roomDraftListeners.clear();
1854
- presentationListeners.clear();
2661
+ store.close();
1855
2662
  }
1856
2663
  async function stop() {
1857
2664
  stopping = true;
1858
- stopTimer();
1859
- await chain.catch(() => undefined);
1860
- await Promise.all([...submitting.values()].map((run) => run.done));
1861
- await mutations;
2665
+ unreadied();
2666
+ socket.stopReads();
2667
+ await recovery.retire();
2668
+ await Promise.all(working);
2669
+ await store.written();
1862
2670
  close();
1863
2671
  }
1864
2672
  const withdrawServiceMode = options.adapter.serviceMode?.observe(signalled);
1865
- 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
+ });
1866
2682
  options.signal?.addEventListener('abort', close, { once: true });
1867
2683
  if (options.signal?.aborted)
1868
2684
  close();
1869
- observeStream();
1870
- void openLocal().then(() => {
1871
- if (capabilities.length > 0)
1872
- notifyReady();
1873
- }, () => {
1874
- 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();
1875
2716
  });
1876
2717
  if (options.adapter.roomStream === undefined)
1877
- void sync('unstreamed');
1878
- function planned(input, reach) {
1879
- return planOf(input, roomFacts(reach), planReach(reach));
1880
- }
1881
- function watchPlan(plan, deliver) {
1882
- 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
+ };
1883
2747
  }
1884
2748
  async function searched(input, reach) {
1885
- const plan = searchPlanOf(input);
1886
- const selected = plan.documents === undefined && reach === null
1887
- ? null
1888
- : (await readSide.query(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
1889
- return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
1890
- }
1891
- const watch = (topic, input, deliver) => {
1892
- if (topic === 'room-drafts') {
1893
- const listener = { notify: deliver, ready: false };
1894
- roomDraftListeners.add(listener);
1895
- void exclusive(async () => {
1896
- await openLocal();
1897
- if (closed || !roomDraftListeners.has(listener))
1898
- return;
1899
- listener.ready = true;
1900
- }).then(() => {
1901
- if (closed || !listener.ready || !roomDraftListeners.has(listener))
1902
- return;
1903
- listener.notify(snapshotRoomDrafts());
1904
- }, () => { });
1905
- return () => {
1906
- 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)),
1907
2794
  };
1908
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);
1909
2804
  if (topic === 'conversation') {
1910
2805
  return conversations.watch(input[0], input[1], deliver);
1911
2806
  }
1912
- if (topic === 'presentation') {
1913
- const listener = {
1914
- app: input[0],
1915
- notify: deliver,
1916
- ready: false,
1917
- };
1918
- presentationListeners.add(listener);
1919
- void exclusive(async () => {
1920
- await openLocal();
1921
- }).then(() => {
1922
- if (closed || !presentationListeners.has(listener))
1923
- return;
1924
- listener.ready = true;
1925
- listener.notify(presentationOf(listener.app));
1926
- }, () => { });
1927
- return () => {
1928
- presentationListeners.delete(listener);
1929
- };
2807
+ if (topic === 'conversations')
2808
+ return conversations.watchList(deliver);
2809
+ if (topic === 'agents')
2810
+ return conversations.watchAgents(deliver);
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);
1930
2819
  }
1931
2820
  if (topic === 'rooms') {
1932
- roomListeners.add(deliver);
1933
- deliver(listed());
2821
+ unlistedRooms.add(deliver);
2822
+ emitKnownRooms();
1934
2823
  return () => {
2824
+ unlistedRooms.delete(deliver);
1935
2825
  roomListeners.delete(deliver);
2826
+ if (roomListeners.size === 0)
2827
+ roomsShown = null;
1936
2828
  };
1937
2829
  }
1938
2830
  if (topic === 'connection-refusal') {
@@ -1963,153 +2855,162 @@ export function composeRuntime(options) {
1963
2855
  serviceModeListeners.delete(deliver);
1964
2856
  };
1965
2857
  }
2858
+ if (topic === 'member') {
2859
+ memberListeners.add(deliver);
2860
+ void openLocal()
2861
+ .then((local) => local.facts())
2862
+ .then(({ member }) => {
2863
+ if (memberListeners.has(deliver))
2864
+ deliver(member);
2865
+ }, () => { });
2866
+ return () => {
2867
+ memberListeners.delete(deliver);
2868
+ };
2869
+ }
2870
+ if (topic === 'authority-changes') {
2871
+ authorityChangeListeners.add(deliver);
2872
+ deliver(authorityChanges);
2873
+ return () => {
2874
+ authorityChangeListeners.delete(deliver);
2875
+ };
2876
+ }
1966
2877
  if (topic === 'queue') {
1967
2878
  const queue = { locator: input[0], notify: deliver };
1968
2879
  queueSubscriptions.add(queue);
1969
- 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
+ });
1970
2887
  return () => {
1971
2888
  queueSubscriptions.delete(queue);
1972
2889
  };
1973
2890
  }
1974
- return watchPlan(planned(input[0], null), deliver);
2891
+ return watchPlanned(planned(input[0], null), deliver, fail);
1975
2892
  };
1976
2893
  const md = {
1977
2894
  loaded() {
1978
2895
  return openLocal().then(() => undefined);
1979
2896
  },
1980
2897
  rooms() {
1981
- 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
+ });
1982
2909
  },
1983
- query: ((input) => readSide.query(planned(input, null))),
2910
+ query: ((input) => planned(input, null).then(rowsQuery)),
1984
2911
  search: (input) => searched(input, null),
1985
- types(locator) {
2912
+ async types(locator) {
2913
+ const held = (await storedRooms([locator])).get(locator);
2914
+ const room = isFenced(locator) ? undefined : declarationOf(locator, held)?.room;
1986
2915
  return {
1987
- collection: modelTypes(modelOf(locator)),
1988
- typedSelection: rooms.get(locator)?.typedSelection === undefined ||
1989
- rooms.get(locator)?.typedSelection === null
1990
- ? null
1991
- : modelTypes(modelOf(locator, 'typed-selection')),
2916
+ collection: modelTypes(isFenced(locator) ? NO_MODEL : modelIn(locator, held)),
2917
+ typedSelection: (room?.typedSelection ?? null) === null ? null : modelTypes(builtIn),
1992
2918
  };
1993
2919
  },
1994
- roomDraft: {
1995
- list() {
1996
- return exclusive(async () => {
1997
- await openLocal();
1998
- return snapshotRoomDrafts();
1999
- }).then((listed) => {
2000
- publishRoomDrafts(true);
2001
- return listed;
2002
- });
2003
- },
2004
- read(room, path) {
2005
- const record = roomDrafts.get(room)?.get(path);
2006
- return record === undefined ? null : roomDraftOf(record);
2007
- },
2008
- write(room, path, revision, input) {
2009
- return exclusive(async () => {
2010
- const parts = readParts(input);
2011
- if (path === '' || normalize(path) !== path)
2012
- throw new Error('room-draft-malformed');
2013
- const local = await openLocal();
2014
- const current = roomDrafts.get(room)?.get(path);
2015
- if ((current?.revision ?? null) !== revision) {
2016
- return {
2017
- outcome: 'stale',
2018
- current: current === undefined ? null : roomDraftOf(current),
2019
- };
2020
- }
2021
- const record = {
2022
- locator: room,
2023
- path,
2024
- revision: roomDraftRevision(),
2025
- parts: held(current?.parts, parts),
2026
- returned: current?.returned ?? null,
2027
- };
2028
- await local.keepRoomDraft(record);
2029
- rememberRoomDraft(record);
2030
- publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
2031
- return { outcome: 'written', roomDraft: roomDraftOf(record) };
2032
- });
2033
- },
2034
- discard(room, path, revision) {
2035
- return exclusive(async () => {
2036
- const local = await openLocal();
2037
- const current = roomDrafts.get(room)?.get(path);
2038
- if (current === undefined || current.revision !== revision) {
2039
- return {
2040
- outcome: 'stale',
2041
- current: current === undefined ? null : roomDraftOf(current),
2042
- };
2043
- }
2044
- await local.discardRoomDraft(room, path);
2045
- forgetRoomDraft(room, path);
2046
- publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
2047
- moveTo({ ...status, ...attention() });
2048
- return { outcome: 'written', roomDraft: null };
2049
- });
2050
- },
2051
- },
2920
+ roomDraft: roomDraftSurface(drafting),
2052
2921
  conversation: conversations.surface,
2053
2922
  chatDraft: conversations.chatDraft,
2054
- presentation: {
2055
- read(app) {
2056
- return exclusive(async () => {
2057
- await openLocal();
2058
- return presentationOf(app);
2059
- });
2060
- },
2061
- write(app, value) {
2062
- return exclusive(async () => {
2063
- const local = await openLocal();
2064
- const stored = structuredClone(value);
2065
- await local.present({ app, value: stored });
2066
- presentations.set(app, stored);
2067
- publishChangeSet(changeSet('', null, null, { presentation: app }));
2068
- });
2069
- },
2070
- },
2923
+ presentation: presentationSurface(drafting),
2071
2924
  save(room, named, writtenBy) {
2072
- 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));
2073
2929
  },
2074
2930
  history(locator, path, query) {
2075
2931
  return readHistory(locator, path, query);
2076
2932
  },
2077
- call(name, input) {
2078
- return invokeAction(name, input);
2933
+ resolve(room, path, destination) {
2934
+ return resolveReference(room, path, destination);
2935
+ },
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);
2079
2942
  },
2080
- reached(locator, roomVersion, path) {
2943
+ async reached(locator, roomVersion, path) {
2081
2944
  return path === undefined
2082
- ? reachedBy(locator, roomVersion)
2083
- : writtenAfter(locator, roomVersion, path);
2945
+ ? reachedBy(installer, locator, roomVersion)
2946
+ : writtenAfter(installer, locator, roomVersion, path);
2084
2947
  },
2085
2948
  log() {
2086
2949
  return journal.export();
2087
2950
  },
2951
+ cleanUp,
2952
+ unsent: () => database.ask({ kind: 'authored.unsent' }),
2953
+ reset: () => options.shell.reset(),
2954
+ async readAgain() {
2955
+ lastAnswers.again();
2956
+ },
2088
2957
  persistence() {
2089
- 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();
2090
2966
  },
2091
2967
  observe: observing(watch),
2092
2968
  async reauthorize() {
2093
2969
  if (refusal === null || !refusal.reauthorizationOffered)
2094
2970
  return;
2095
2971
  journal.record('connection reauthorize');
2096
- await chain.catch(() => undefined);
2097
- await mutations;
2972
+ await Promise.all([...lanes.values()]
2973
+ .filter((held) => held.generation === socket.generation())
2974
+ .map((held) => held.chain));
2975
+ await store.written();
2098
2976
  authorization = undefined;
2099
2977
  discoveryPaused = false;
2100
2978
  if (options.adapter.roomStream === undefined)
2101
- await sync('unstreamed');
2979
+ await sync('unstreamed', null);
2102
2980
  else
2103
- await task(ensureAuthorization);
2981
+ await task(OPENING_LANE, ensureAuthorization);
2104
2982
  },
2105
2983
  };
2106
2984
  const reachable = {
2107
2985
  md,
2108
2986
  watch,
2109
2987
  plan: planned,
2110
- query: (plan) => readSide.query(plan),
2988
+ query: (plan) => rowsQuery(plan),
2111
2989
  watchPlan,
2112
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
+ },
2113
3015
  };
2114
- return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2115
3016
  }