@agentshouse/mdruntime 0.1.0-ce531b51ff088 → 0.1.0-ce6bc9239a3ee

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