@farmslot/protocol 0.73.2 → 0.74.1

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 (194) hide show
  1. package/CHANGELOG.md +22 -8
  2. package/README.md +9 -0
  3. package/dist/client-cache/cache.d.ts +4 -0
  4. package/dist/client-cache/cache.d.ts.map +1 -0
  5. package/dist/client-cache/cache.js +1102 -0
  6. package/dist/client-cache/cache.js.map +1 -0
  7. package/dist/client-cache/collector-admission.d.ts +29 -0
  8. package/dist/client-cache/collector-admission.d.ts.map +1 -0
  9. package/dist/client-cache/collector-admission.js +183 -0
  10. package/dist/client-cache/collector-admission.js.map +1 -0
  11. package/dist/client-cache/command-method.d.ts +5 -0
  12. package/dist/client-cache/command-method.d.ts.map +1 -0
  13. package/dist/client-cache/command-method.js +2 -0
  14. package/dist/client-cache/command-method.js.map +1 -0
  15. package/dist/client-cache/conformance.d.ts +36 -0
  16. package/dist/client-cache/conformance.d.ts.map +1 -0
  17. package/dist/client-cache/conformance.js +578 -0
  18. package/dist/client-cache/conformance.js.map +1 -0
  19. package/dist/client-cache/content.d.ts +29 -0
  20. package/dist/client-cache/content.d.ts.map +1 -0
  21. package/dist/client-cache/content.js +101 -0
  22. package/dist/client-cache/content.js.map +1 -0
  23. package/dist/client-cache/d07-domains.d.ts +17 -0
  24. package/dist/client-cache/d07-domains.d.ts.map +1 -0
  25. package/dist/client-cache/d07-domains.js +111 -0
  26. package/dist/client-cache/d07-domains.js.map +1 -0
  27. package/dist/client-cache/diff-apply.d.ts +29 -0
  28. package/dist/client-cache/diff-apply.d.ts.map +1 -0
  29. package/dist/client-cache/diff-apply.js +206 -0
  30. package/dist/client-cache/diff-apply.js.map +1 -0
  31. package/dist/client-cache/index.d.ts +14 -10
  32. package/dist/client-cache/index.d.ts.map +1 -1
  33. package/dist/client-cache/index.js +14 -1
  34. package/dist/client-cache/index.js.map +1 -1
  35. package/dist/client-cache/keys.d.ts +126 -0
  36. package/dist/client-cache/keys.d.ts.map +1 -0
  37. package/dist/client-cache/keys.js +25 -0
  38. package/dist/client-cache/keys.js.map +1 -0
  39. package/dist/client-cache/native-history.d.ts +64 -0
  40. package/dist/client-cache/native-history.d.ts.map +1 -0
  41. package/dist/client-cache/native-history.js +285 -0
  42. package/dist/client-cache/native-history.js.map +1 -0
  43. package/dist/client-cache/prefetch.d.ts +50 -0
  44. package/dist/client-cache/prefetch.d.ts.map +1 -0
  45. package/dist/client-cache/prefetch.js +507 -0
  46. package/dist/client-cache/prefetch.js.map +1 -0
  47. package/dist/client-cache/registry.d.ts +1365 -0
  48. package/dist/client-cache/registry.d.ts.map +1 -0
  49. package/dist/client-cache/registry.js +1038 -0
  50. package/dist/client-cache/registry.js.map +1 -0
  51. package/dist/client-cache/run-list-versions.d.ts +70 -0
  52. package/dist/client-cache/run-list-versions.d.ts.map +1 -0
  53. package/dist/client-cache/run-list-versions.js +309 -0
  54. package/dist/client-cache/run-list-versions.js.map +1 -0
  55. package/dist/client-cache/store.d.ts +128 -0
  56. package/dist/client-cache/store.d.ts.map +1 -0
  57. package/dist/client-cache/store.js +190 -0
  58. package/dist/client-cache/store.js.map +1 -0
  59. package/dist/client-cache/transport.d.ts +27 -0
  60. package/dist/client-cache/transport.d.ts.map +1 -0
  61. package/dist/client-cache/transport.js +56 -0
  62. package/dist/client-cache/transport.js.map +1 -0
  63. package/dist/client-cache/types.d.ts +116 -0
  64. package/dist/client-cache/types.d.ts.map +1 -0
  65. package/dist/client-cache/types.js +2 -0
  66. package/dist/client-cache/types.js.map +1 -0
  67. package/dist/client-cache/upstream-retention.d.ts +6 -0
  68. package/dist/client-cache/upstream-retention.d.ts.map +1 -0
  69. package/dist/client-cache/upstream-retention.js +28 -0
  70. package/dist/client-cache/upstream-retention.js.map +1 -0
  71. package/dist/client-cache/workspace-retention.d.ts +7 -0
  72. package/dist/client-cache/workspace-retention.d.ts.map +1 -0
  73. package/dist/client-cache/workspace-retention.js +46 -0
  74. package/dist/client-cache/workspace-retention.js.map +1 -0
  75. package/dist/contracts/acceptance-registration.d.ts +0 -6
  76. package/dist/contracts/acceptance-registration.d.ts.map +1 -1
  77. package/dist/contracts/provider-accounts.d.ts +31 -0
  78. package/dist/contracts/provider-accounts.d.ts.map +1 -1
  79. package/dist/contracts/provider-accounts.js +209 -1
  80. package/dist/contracts/provider-accounts.js.map +1 -1
  81. package/dist/contracts/run-bundles.d.ts.map +1 -1
  82. package/dist/contracts/run-bundles.js +2 -0
  83. package/dist/contracts/run-bundles.js.map +1 -1
  84. package/dist/contracts/runs.d.ts +5 -20
  85. package/dist/contracts/runs.d.ts.map +1 -1
  86. package/dist/contracts/runs.js +5 -21
  87. package/dist/contracts/runs.js.map +1 -1
  88. package/dist/contracts/worker-terminal.d.ts +0 -3
  89. package/dist/contracts/worker-terminal.d.ts.map +1 -1
  90. package/dist/contracts/worker-terminal.js.map +1 -1
  91. package/dist/index.d.ts +4 -1
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +4 -1
  94. package/dist/index.js.map +1 -1
  95. package/dist/person-session-client.d.ts +4 -1
  96. package/dist/person-session-client.d.ts.map +1 -1
  97. package/dist/person-session-client.js.map +1 -1
  98. package/dist/rpc/auth.d.ts +17 -0
  99. package/dist/rpc/auth.d.ts.map +1 -1
  100. package/dist/rpc/auth.js.map +1 -1
  101. package/dist/rpc/config.d.ts +2 -0
  102. package/dist/rpc/config.d.ts.map +1 -1
  103. package/dist/rpc/config.js.map +1 -1
  104. package/dist/rpc/fs.d.ts +110 -4
  105. package/dist/rpc/fs.d.ts.map +1 -1
  106. package/dist/rpc/fs.js +7 -1
  107. package/dist/rpc/fs.js.map +1 -1
  108. package/dist/rpc/gateway.d.ts +3 -0
  109. package/dist/rpc/gateway.d.ts.map +1 -1
  110. package/dist/rpc/git.d.ts +42 -0
  111. package/dist/rpc/git.d.ts.map +1 -1
  112. package/dist/rpc/index.d.ts +1 -0
  113. package/dist/rpc/index.d.ts.map +1 -1
  114. package/dist/rpc/index.js +1 -0
  115. package/dist/rpc/index.js.map +1 -1
  116. package/dist/rpc/native-session.d.ts +2 -0
  117. package/dist/rpc/native-session.d.ts.map +1 -1
  118. package/dist/rpc/native-session.js.map +1 -1
  119. package/dist/rpc/nodes.d.ts +2 -0
  120. package/dist/rpc/nodes.d.ts.map +1 -1
  121. package/dist/rpc/nodes.js.map +1 -1
  122. package/dist/rpc/pr-observations.d.ts +22 -0
  123. package/dist/rpc/pr-observations.d.ts.map +1 -0
  124. package/dist/rpc/pr-observations.js +104 -0
  125. package/dist/rpc/pr-observations.js.map +1 -0
  126. package/dist/rpc/pr.d.ts +9 -0
  127. package/dist/rpc/pr.d.ts.map +1 -1
  128. package/dist/rpc/pr.js +1 -1
  129. package/dist/rpc/pr.js.map +1 -1
  130. package/dist/rpc/principal.d.ts +2 -0
  131. package/dist/rpc/principal.d.ts.map +1 -1
  132. package/dist/rpc/registry.d.ts +1 -0
  133. package/dist/rpc/registry.d.ts.map +1 -1
  134. package/dist/rpc/registry.js +2 -0
  135. package/dist/rpc/registry.js.map +1 -1
  136. package/dist/rpc/resource-observers.d.ts +32 -0
  137. package/dist/rpc/resource-observers.d.ts.map +1 -0
  138. package/dist/rpc/resource-observers.js +2 -0
  139. package/dist/rpc/resource-observers.js.map +1 -0
  140. package/dist/rpc/run.d.ts +23 -0
  141. package/dist/rpc/run.d.ts.map +1 -1
  142. package/dist/rpc/run.js.map +1 -1
  143. package/dist/rpc/runner-models.d.ts +15 -0
  144. package/dist/rpc/runner-models.d.ts.map +1 -1
  145. package/dist/rpc/runner-models.js +91 -0
  146. package/dist/rpc/runner-models.js.map +1 -1
  147. package/dist/runs/review-diff-identity.d.ts +15 -0
  148. package/dist/runs/review-diff-identity.d.ts.map +1 -0
  149. package/dist/runs/review-diff-identity.js +47 -0
  150. package/dist/runs/review-diff-identity.js.map +1 -0
  151. package/dist/runs/run-list-filter.d.ts +33 -0
  152. package/dist/runs/run-list-filter.d.ts.map +1 -0
  153. package/dist/runs/run-list-filter.js +116 -0
  154. package/dist/runs/run-list-filter.js.map +1 -0
  155. package/dist/runs/run-list-observation.d.ts +5 -0
  156. package/dist/runs/run-list-observation.d.ts.map +1 -0
  157. package/dist/runs/run-list-observation.js +36 -0
  158. package/dist/runs/run-list-observation.js.map +1 -0
  159. package/dist/runs/run-list-view.d.ts +6 -2
  160. package/dist/runs/run-list-view.d.ts.map +1 -1
  161. package/dist/runs/run-list-view.js +10 -4
  162. package/dist/runs/run-list-view.js.map +1 -1
  163. package/dist/slots/cancelled-run-slot.d.ts +2 -1
  164. package/dist/slots/cancelled-run-slot.d.ts.map +1 -1
  165. package/dist/sources/client.d.ts +10 -1
  166. package/dist/sources/client.d.ts.map +1 -1
  167. package/dist/sources/client.js +23 -4
  168. package/dist/sources/client.js.map +1 -1
  169. package/dist/sources/controller.d.ts +13 -14
  170. package/dist/sources/controller.d.ts.map +1 -1
  171. package/dist/sources/controller.js +87 -82
  172. package/dist/sources/controller.js.map +1 -1
  173. package/dist/sources/status-view.d.ts +2 -0
  174. package/dist/sources/status-view.d.ts.map +1 -1
  175. package/dist/sources/status-view.js +13 -0
  176. package/dist/sources/status-view.js.map +1 -1
  177. package/dist/transport/events.d.ts +1 -4
  178. package/dist/transport/events.d.ts.map +1 -1
  179. package/dist/transport/events.js +1 -0
  180. package/dist/transport/events.js.map +1 -1
  181. package/dist/transport/frames.d.ts +6 -0
  182. package/dist/transport/frames.d.ts.map +1 -1
  183. package/dist/transport/frames.js.map +1 -1
  184. package/dist/transport/observations.d.ts +24 -0
  185. package/dist/transport/observations.d.ts.map +1 -0
  186. package/dist/transport/observations.js +2 -0
  187. package/dist/transport/observations.js.map +1 -0
  188. package/dist/version.d.ts +1 -1
  189. package/dist/version.js +1 -1
  190. package/package.json +1 -1
  191. package/dist/sources/changes.d.ts +0 -37
  192. package/dist/sources/changes.d.ts.map +0 -1
  193. package/dist/sources/changes.js +0 -79
  194. package/dist/sources/changes.js.map +0 -1
@@ -0,0 +1,1102 @@
1
+ import { Methods } from '../rpc/registry.js';
2
+ import { ResourceCollectorAdmission } from './collector-admission.js';
3
+ import { resourceBudgetEvictions, resourceContentWithinLimits, verifyResourceContent, } from './content.js';
4
+ import { RESOURCE_PUBLISH_INTERVAL_MS, resourceKeyId } from './keys.js';
5
+ import { nativeHistoryStorageId, readNativeHistory, storedNativeHistoryMatches, } from './native-history.js';
6
+ import { resourceQuery } from './registry.js';
7
+ import { isRunListCollectionRead, RUN_LIST_COLLECTION_ID, RunListCollection, runListSummaryFlags, } from './run-list-versions.js';
8
+ import { createMemoryResourceStore, normalizeResourceStoreScope, resourceStoreScopeId, sameResourceAuthority, } from './store.js';
9
+ import { isUpstreamStoredQuery, upstreamRetentionEvictions } from './upstream-retention.js';
10
+ import { workspaceRetentionEvictions, workspaceRetentionMembership, } from './workspace-retention.js';
11
+ const deletionReasons = new Set([
12
+ 'sign-out',
13
+ 'principal-change',
14
+ 'credential-refused',
15
+ 'session-ended',
16
+ ]);
17
+ const evictionCodes = new Set([
18
+ 'AUTH_FORBIDDEN',
19
+ 'AUTH_REQUIRED',
20
+ 'NOT_FOUND',
21
+ 'RUN_NOT_FOUND',
22
+ 'RESOURCE_NOT_FOUND',
23
+ ]);
24
+ function queryError(error) {
25
+ const code = error && typeof error === 'object' ? Reflect.get(error, 'code') : undefined;
26
+ return {
27
+ code: typeof code === 'string' ? code : 'READ_FAILED',
28
+ message: error instanceof Error ? error.message : String(error),
29
+ };
30
+ }
31
+ function canPersist(connection) {
32
+ return (connection.authMode === 'none' ||
33
+ (typeof connection.scope.principalId === 'string' &&
34
+ Number.isSafeInteger(connection.scope.principalGeneration) &&
35
+ connection.scope.principalGeneration >= 0));
36
+ }
37
+ function normalizeConnection(connection) {
38
+ const scope = normalizeResourceStoreScope(connection.scope);
39
+ return {
40
+ ...connection,
41
+ scope: connection.authMode === 'none'
42
+ ? { gatewayUrl: scope.gatewayUrl, profileId: scope.profileId }
43
+ : scope,
44
+ };
45
+ }
46
+ const defaultContentBudgetBytes = 500 * 1024 * 1024;
47
+ // Only the identity crosses the store seam, never a caller's other fields.
48
+ const contentKey = ({ kind, id }) => ({ kind, id });
49
+ function profileId(connection) {
50
+ return resourceStoreScopeId({
51
+ gatewayUrl: connection.scope.gatewayUrl,
52
+ profileId: connection.scope.profileId,
53
+ });
54
+ }
55
+ /** Experimental ADR-071 controller. It opens no sockets and never accepts a command. */
56
+ export function createGatewayResourceCache(options) {
57
+ const transport = options.transport;
58
+ let store = options.store ?? createMemoryResourceStore();
59
+ let metadata;
60
+ let connection = normalizeConnection(transport.getConnection());
61
+ let usable = false;
62
+ let disposed = false;
63
+ let version = 0;
64
+ let storeVersion = 0;
65
+ let hydrationOwnerVersion = 0;
66
+ let lastUsed = 0;
67
+ let storageNotice;
68
+ let needsStore = options.store === undefined && options.openStore !== undefined;
69
+ let deletionRequired = false;
70
+ let barrier = Promise.resolve();
71
+ const entries = new Map();
72
+ const revisions = new Map();
73
+ const encoder = new TextEncoder();
74
+ const collectors = new ResourceCollectorAdmission(transport);
75
+ let retiringStore = false;
76
+ let retentionSweptStore;
77
+ let unsubscribeStore = store.onDeleted(storeDeleted);
78
+ // Version lists: one shared run collection answers every run.list view.
79
+ const runList = new RunListCollection({
80
+ request: (params, signal) => transport.request(Methods.RUN_LIST, params, signal),
81
+ persist: (merge) => {
82
+ const writingStore = store;
83
+ const incarnation = storeVersion;
84
+ if (!writingStore.mergeRows || !canPersist(connection))
85
+ return Promise.resolve();
86
+ const current = () => !disposed &&
87
+ writingStore === store &&
88
+ incarnation === storeVersion &&
89
+ !writingStore.deleted;
90
+ return updateStore(writingStore, current, () => writingStore.mergeRows(RUN_LIST_COLLECTION_ID, merge));
91
+ },
92
+ maxBodyBytes: resourceQuery(Methods.RUN_LIST).maxBytes,
93
+ });
94
+ let runListLoad;
95
+ function forgetRunList() {
96
+ runList.clear();
97
+ runListLoad = undefined;
98
+ }
99
+ function updateCollectors() {
100
+ collectors.update({ ...connection, foreground: usable && connection.foreground !== false }, usable
101
+ ? [...entries.values()].filter((entry) => entry.listeners.size).map((entry) => entry.query)
102
+ : []);
103
+ }
104
+ function storeDeleted() {
105
+ if (disposed || retiringStore)
106
+ return;
107
+ usable = false;
108
+ updateCollectors();
109
+ hydrationOwnerVersion++;
110
+ version++;
111
+ forgetRunList();
112
+ for (const entry of entries.values())
113
+ invalidate(entry, true);
114
+ transport.resynchronize();
115
+ }
116
+ function replaceStore(next) {
117
+ unsubscribeStore();
118
+ store = next;
119
+ storeVersion++;
120
+ unsubscribeStore = store.onDeleted(storeDeleted);
121
+ }
122
+ function notify(entry) {
123
+ entry.state = { ...entry.state, storageNotice };
124
+ for (const listener of entry.listeners)
125
+ listener(entry.state);
126
+ }
127
+ function cancelScheduledRead(entry) {
128
+ if (entry.readTimer !== undefined)
129
+ clearTimeout(entry.readTimer);
130
+ entry.readTimer = undefined;
131
+ }
132
+ function invalidate(entry, clear = false) {
133
+ cancelScheduledRead(entry);
134
+ if (clear)
135
+ entry.lastRequestedAt = undefined;
136
+ entry.serial++;
137
+ entry.pending?.controller.abort();
138
+ entry.pending = undefined;
139
+ entry.dirty = true;
140
+ entry.state = clear ? { status: 'loading' } : { ...entry.state, status: 'stale' };
141
+ notify(entry);
142
+ }
143
+ function prune() {
144
+ const inactive = [...entries.values()]
145
+ .filter((entry) => !entry.listeners.size && !entry.pending && entry.hydrated)
146
+ .sort((a, b) => a.lastUsed - b.lastUsed);
147
+ while (inactive.length > (options.maxInactiveEntries ?? 100)) {
148
+ const entry = inactive.shift();
149
+ entry.serial++;
150
+ cancelScheduledRead(entry);
151
+ entries.delete(entry.query.id);
152
+ if (!store.persistent) {
153
+ const memory = store;
154
+ void memory
155
+ .deleteQuery(entry.query.id)
156
+ .catch((error) => recoverStore(error, memory));
157
+ }
158
+ }
159
+ }
160
+ async function recoverStore(error, failed = store) {
161
+ // A retired store's late I/O failure cannot replace the new principal's store.
162
+ if (failed !== store || disposed)
163
+ return;
164
+ replaceStore(createMemoryResourceStore());
165
+ storageNotice = `Cached data will not survive a restart: ${queryError(error).message}`;
166
+ if (metadata)
167
+ await store.setMetadata(metadata);
168
+ try {
169
+ await failed.close();
170
+ }
171
+ catch (closeError) {
172
+ // Data stays in memory; retain both failures in the visible storage notice.
173
+ storageNotice += `; ${queryError(closeError).message}`;
174
+ }
175
+ options.onStorageNotice?.(storageNotice);
176
+ for (const entry of entries.values())
177
+ notify(entry);
178
+ }
179
+ /** Retention, writes and lifecycle changes share one ordered store boundary. */
180
+ function updateStore(target, current, update) {
181
+ const updating = barrier.then(async () => {
182
+ if (!current())
183
+ return;
184
+ try {
185
+ await update();
186
+ }
187
+ catch (error) {
188
+ await recoverStore(error, target);
189
+ }
190
+ });
191
+ barrier = updating;
192
+ return updating;
193
+ }
194
+ async function openFreshStore(next, nextConnection) {
195
+ metadata = next;
196
+ try {
197
+ replaceStore(options.openStore && canPersist(nextConnection)
198
+ ? await options.openStore(next)
199
+ : createMemoryResourceStore());
200
+ const stored = await store.getMetadata();
201
+ if (stored &&
202
+ (resourceStoreScopeId(stored.scope) !== resourceStoreScopeId(next.scope) ||
203
+ !sameResourceAuthority(stored.authority, next.authority))) {
204
+ retiringStore = true;
205
+ try {
206
+ await store.deleteAll();
207
+ }
208
+ finally {
209
+ retiringStore = false;
210
+ }
211
+ await store.close();
212
+ replaceStore(options.openStore && canPersist(nextConnection)
213
+ ? await options.openStore(next)
214
+ : createMemoryResourceStore());
215
+ }
216
+ if (store.persistent) {
217
+ storageNotice = undefined;
218
+ options.onStorageNotice?.(undefined);
219
+ }
220
+ else if (options.openStore && !canPersist(nextConnection)) {
221
+ storageNotice = 'Offline data will not survive a restart with this gateway version.';
222
+ options.onStorageNotice?.(storageNotice);
223
+ }
224
+ }
225
+ catch (error) {
226
+ await recoverStore(error);
227
+ }
228
+ needsStore = false;
229
+ for (const entry of entries.values())
230
+ if (entry.state.status !== 'ready')
231
+ hydrateEntry(entry);
232
+ }
233
+ function schedule(entry) {
234
+ if (entry.queued || disposed)
235
+ return;
236
+ entry.queued = true;
237
+ queueMicrotask(() => {
238
+ entry.queued = false;
239
+ if (entry.listeners.size && usable && !disposed)
240
+ void fetchEntry(entry, true);
241
+ });
242
+ }
243
+ function changedConnection(input, reason) {
244
+ if (disposed)
245
+ return;
246
+ const next = normalizeConnection(input);
247
+ const revoke = reason !== undefined && deletionReasons.has(reason);
248
+ const previousScope = connection.connected
249
+ ? connection.scope
250
+ : (metadata?.scope ?? connection.scope);
251
+ const previousAuthority = connection.authority ?? metadata?.authority;
252
+ const scopeChanged = resourceStoreScopeId(previousScope) !== resourceStoreScopeId(next.scope);
253
+ const authorityChanged = Boolean(previousAuthority &&
254
+ next.authority &&
255
+ !sameResourceAuthority(previousAuthority, next.authority));
256
+ if (usable &&
257
+ next.connected &&
258
+ next.foreground !== false &&
259
+ connection.id === next.id &&
260
+ !scopeChanged &&
261
+ !authorityChanged &&
262
+ !revoke)
263
+ return;
264
+ const ticket = ++version;
265
+ runList.abort();
266
+ // Stored facts belong to an owner, not a socket attempt. Fence a revoked
267
+ // owner's pending I/O synchronously, before queued deletion can run.
268
+ if (revoke || scopeChanged || authorityChanged) {
269
+ hydrationOwnerVersion++;
270
+ forgetRunList();
271
+ }
272
+ connection = next;
273
+ usable = false;
274
+ updateCollectors();
275
+ revisions.clear();
276
+ for (const entry of entries.values()) {
277
+ entry.lastRequestedAt = undefined;
278
+ invalidate(entry, revoke || scopeChanged || authorityChanged);
279
+ }
280
+ barrier = barrier
281
+ .then(async () => {
282
+ // Process every authority transition in order, even if a newer connect is queued.
283
+ // In particular, reconnect must never skip an earlier sign-out deletion.
284
+ if (!metadata) {
285
+ try {
286
+ metadata = await store.getMetadata();
287
+ }
288
+ catch (error) {
289
+ await recoverStore(error);
290
+ }
291
+ }
292
+ const nextMetadata = next.authority
293
+ ? { scope: next.scope, authority: next.authority }
294
+ : undefined;
295
+ const storedScopeChanged = metadata && resourceStoreScopeId(metadata.scope) !== resourceStoreScopeId(next.scope);
296
+ const storedAuthorityChanged = metadata && next.authority && !sameResourceAuthority(metadata.authority, next.authority);
297
+ const previousProfile = metadata
298
+ ? resourceStoreScopeId({
299
+ gatewayUrl: metadata.scope.gatewayUrl,
300
+ profileId: metadata.scope.profileId,
301
+ })
302
+ : undefined;
303
+ const switchingProfile = !revoke && previousProfile !== undefined && previousProfile !== profileId(next);
304
+ if (revoke ||
305
+ deletionRequired ||
306
+ store.deleted ||
307
+ switchingProfile ||
308
+ ((next.connected || next.authority !== undefined) &&
309
+ (storedScopeChanged || storedAuthorityChanged))) {
310
+ storeVersion++;
311
+ forgetRunList();
312
+ for (const entry of entries.values())
313
+ invalidate(entry, true);
314
+ // Switching profiles retains the old profile's store; revoked authority deletes it.
315
+ if (!switchingProfile || revoke || deletionRequired) {
316
+ deletionRequired = true;
317
+ retiringStore = true;
318
+ try {
319
+ await store.deleteAll();
320
+ }
321
+ finally {
322
+ retiringStore = false;
323
+ }
324
+ deletionRequired = false;
325
+ }
326
+ await store.close();
327
+ replaceStore(createMemoryResourceStore());
328
+ metadata = undefined;
329
+ needsStore = true;
330
+ }
331
+ if (next.connected && store.persistent && !canPersist(next)) {
332
+ await store.close();
333
+ replaceStore(createMemoryResourceStore());
334
+ for (const entry of entries.values())
335
+ invalidate(entry, true);
336
+ }
337
+ if (nextMetadata && !revoke) {
338
+ if (needsStore && (next.connected || canPersist(next)))
339
+ await openFreshStore(nextMetadata, next);
340
+ if (next.connected) {
341
+ metadata = nextMetadata;
342
+ try {
343
+ await store.setMetadata(metadata);
344
+ }
345
+ catch (error) {
346
+ await recoverStore(error);
347
+ }
348
+ }
349
+ }
350
+ if (store.persistent && retentionSweptStore !== store) {
351
+ const target = store;
352
+ try {
353
+ for (const id of upstreamRetentionEvictions(await target.listQueries()))
354
+ await target.deleteQuery(id);
355
+ retentionSweptStore = target;
356
+ }
357
+ catch (error) {
358
+ await recoverStore(error, target);
359
+ }
360
+ }
361
+ if (disposed || ticket !== version)
362
+ return;
363
+ usable = next.connected && next.foreground !== false && !revoke;
364
+ updateCollectors();
365
+ if (usable)
366
+ for (const entry of entries.values())
367
+ schedule(entry);
368
+ })
369
+ .catch((error) => {
370
+ // An authority deletion failure must not admit reads or render the old principal's data.
371
+ usable = false;
372
+ updateCollectors();
373
+ for (const entry of entries.values()) {
374
+ entry.state = {
375
+ status: 'error',
376
+ error: { ...queryError(error), code: 'STORE_SCOPE_FAILED' },
377
+ };
378
+ notify(entry);
379
+ }
380
+ });
381
+ }
382
+ function changedResources(payload) {
383
+ if (disposed || !usable)
384
+ return;
385
+ const epoch = connection.capabilities?.resourceChanges?.epoch;
386
+ if (payload.version !== 1 || payload.epoch !== epoch) {
387
+ usable = false;
388
+ updateCollectors();
389
+ version++;
390
+ runList.abort();
391
+ for (const entry of entries.values())
392
+ invalidate(entry);
393
+ transport.resynchronize();
394
+ return;
395
+ }
396
+ for (const change of payload.changes) {
397
+ if (!Number.isSafeInteger(change.revision) || change.revision < 0) {
398
+ usable = false;
399
+ updateCollectors();
400
+ for (const entry of entries.values())
401
+ invalidate(entry);
402
+ transport.resynchronize();
403
+ return;
404
+ }
405
+ const id = resourceKeyId(change.key);
406
+ if (change.revision <= (revisions.get(id) ?? -1))
407
+ continue;
408
+ revisions.set(id, change.revision);
409
+ const affected = [...entries.values()].filter((entry) => entry.query.dependencies.some((key) => resourceKeyId(key) === id));
410
+ const targets = affected.length ? affected : [...entries.values()];
411
+ for (const entry of targets) {
412
+ if (!affected.length)
413
+ entry.unknownGeneration++;
414
+ entry.dirty = true;
415
+ entry.state = { ...entry.state, status: 'stale' };
416
+ notify(entry);
417
+ if (!entry.pending)
418
+ schedule(entry);
419
+ }
420
+ }
421
+ }
422
+ // Listen before any metadata hydration or query read can start.
423
+ const unsubscribeChanges = transport.onResourceChanged(changedResources);
424
+ const unsubscribeConnection = transport.onConnectionChange(changedConnection);
425
+ changedConnection(transport.getConnection());
426
+ function entryFor(query) {
427
+ const existing = entries.get(query.id);
428
+ if (existing) {
429
+ existing.lastUsed = ++lastUsed;
430
+ return existing;
431
+ }
432
+ const entry = {
433
+ query,
434
+ state: { status: 'loading' },
435
+ listeners: new Set(),
436
+ hydration: Promise.resolve(),
437
+ hydrated: false,
438
+ hydrationSerial: 0,
439
+ queued: false,
440
+ serial: 0,
441
+ unknownGeneration: 0,
442
+ lastUsed: ++lastUsed,
443
+ dirty: true,
444
+ };
445
+ entries.set(query.id, entry);
446
+ hydrateEntry(entry);
447
+ return entry;
448
+ }
449
+ function hydrateEntry(entry) {
450
+ const query = entry.query;
451
+ const hydrationSerial = ++entry.hydrationSerial;
452
+ entry.hydrated = false;
453
+ entry.hydration = (async () => {
454
+ await barrier;
455
+ if (disposed || entry.hydrationSerial !== hydrationSerial || entries.get(query.id) !== entry)
456
+ return;
457
+ const incarnation = storeVersion;
458
+ const ownerVersion = hydrationOwnerVersion;
459
+ const readingStore = store;
460
+ try {
461
+ const saved = query.persisted
462
+ ? ((isRunListView(query) ? await storedRunListView(query, readingStore) : undefined) ??
463
+ (await readingStore.getQuery(nativeHistoryStorageId(query))))
464
+ : undefined;
465
+ if (!disposed &&
466
+ !readingStore.deleted &&
467
+ entries.get(query.id) === entry &&
468
+ incarnation === storeVersion &&
469
+ ownerVersion === hydrationOwnerVersion &&
470
+ entry.hydrationSerial === hydrationSerial &&
471
+ entry.state.status !== 'ready' &&
472
+ saved &&
473
+ saved.bytes <= query.maxBytes &&
474
+ (query.method !== Methods.NATIVE_SESSION_READ ||
475
+ storedNativeHistoryMatches(query.params, saved.data))) {
476
+ const sourceObservation = query.sourceObservation?.(saved.data);
477
+ entry.state = {
478
+ status: 'stale',
479
+ data: saved.data,
480
+ observedAt: saved.observedAt,
481
+ ...(sourceObservation ? { sourceObservation, error: sourceObservation.error } : {}),
482
+ };
483
+ notify(entry);
484
+ }
485
+ }
486
+ catch (error) {
487
+ await recoverStore(error, readingStore);
488
+ }
489
+ finally {
490
+ if (entry.hydrationSerial === hydrationSerial) {
491
+ entry.hydrated = true;
492
+ prune();
493
+ }
494
+ }
495
+ })();
496
+ }
497
+ function isRunListView(query) {
498
+ return query.method === Methods.RUN_LIST && !isRunListCollectionRead(query.params);
499
+ }
500
+ /** A cold start's view, derived from the stored run collection when one is stored. */
501
+ async function storedRunListView(query, readingStore) {
502
+ if (!runList.held && readingStore.getRows) {
503
+ if (runListLoad?.store !== readingStore)
504
+ runListLoad = {
505
+ store: readingStore,
506
+ promise: readingStore.getRows(RUN_LIST_COLLECTION_ID).then((stored) => {
507
+ if (store === readingStore && !readingStore.deleted)
508
+ runList.hydrate(stored);
509
+ }),
510
+ };
511
+ await runListLoad.promise;
512
+ }
513
+ const data = runList.view(query.params);
514
+ return data && runList.observedAt !== undefined
515
+ ? { id: query.id, data, observedAt: runList.observedAt, bytes: 0 }
516
+ : undefined;
517
+ }
518
+ /** Summary flags every observed view needs, so one versions read serves them all. */
519
+ function runListFlags() {
520
+ const flags = {};
521
+ for (const entry of entries.values())
522
+ if (entry.listeners.size && isRunListView(entry.query))
523
+ Object.assign(flags, runListSummaryFlags(entry.query.params));
524
+ return flags;
525
+ }
526
+ /** Version lists rules 3-5: a view of the shared collection after its one shared refresh. */
527
+ async function readRunListView(entry, signal) {
528
+ const params = entry.query.params;
529
+ const flags = runListFlags();
530
+ try {
531
+ // A new view of a current collection needs no read: no runs change since its version reply.
532
+ if (!runList.complete || !runList.serves(flags) || !stampCovers(entry, runList.resourceState))
533
+ await runList.refresh(flags);
534
+ }
535
+ catch (error) {
536
+ // A refused collection read evicts the stored collection, like every refused read.
537
+ if (evictionCodes.has(queryError(error).code)) {
538
+ const evictingStore = store;
539
+ forgetRunList();
540
+ void updateStore(evictingStore, () => evictingStore === store && !evictingStore.deleted, () => evictingStore.deleteQuery(RUN_LIST_COLLECTION_ID));
541
+ }
542
+ throw error;
543
+ }
544
+ const data = runList.view(params);
545
+ if (data)
546
+ return { data, resourceState: runList.resourceState, incomplete: !runList.complete };
547
+ // A view whose summaries the version reply cannot answer keeps its own full read.
548
+ return transport.request(Methods.RUN_LIST, params, signal);
549
+ }
550
+ function stampCovers(entry, stamp) {
551
+ if (!stamp ||
552
+ !Array.isArray(stamp.dependencies) ||
553
+ stamp.epoch !== connection.capabilities?.resourceChanges?.epoch)
554
+ return false;
555
+ return entry.query.dependencies.every((key) => {
556
+ const id = resourceKeyId(key);
557
+ const covered = stamp.dependencies.find((dependency) => resourceKeyId(dependency.key) === id)?.revision;
558
+ return (covered !== undefined &&
559
+ Number.isSafeInteger(covered) &&
560
+ covered >= (revisions.get(id) ?? 0));
561
+ });
562
+ }
563
+ async function fetchEntry(entry, observedOnly = false, placement, signal) {
564
+ const prefetch = placement?.origin === 'prefetch' ? placement : undefined;
565
+ await barrier;
566
+ if (signal?.aborted)
567
+ return;
568
+ await entry.hydration;
569
+ if (signal?.aborted ||
570
+ disposed ||
571
+ !usable ||
572
+ entries.get(entry.query.id) !== entry ||
573
+ (observedOnly && !entry.listeners.size))
574
+ return;
575
+ if (entry.query.requiresWorkspaceRead &&
576
+ connection.capabilities?.resourceChanges?.workspaceReads !== 1) {
577
+ entry.state = {
578
+ ...entry.state,
579
+ status: 'unsupported',
580
+ error: {
581
+ code: 'RESOURCE_OBSERVATION_UNSUPPORTED',
582
+ message: 'This gateway cannot observe mutable workspace sources.',
583
+ },
584
+ };
585
+ entry.dirty = false;
586
+ notify(entry);
587
+ return;
588
+ }
589
+ if (entry.query.requiresObservedRead &&
590
+ connection.capabilities?.resourceChanges?.observedReads !== 1) {
591
+ entry.state = {
592
+ ...entry.state,
593
+ status: 'unsupported',
594
+ error: {
595
+ code: 'RESOURCE_OBSERVATION_UNSUPPORTED',
596
+ message: 'This gateway does not verify node observation admission.',
597
+ },
598
+ };
599
+ entry.dirty = false;
600
+ notify(entry);
601
+ return;
602
+ }
603
+ if (entry.pending?.version === version) {
604
+ entry.pending.shared = true;
605
+ // Any caller that is not a prefetch makes the shared read an opened one.
606
+ const shared = entry.pending.prefetch;
607
+ entry.pending.prefetch =
608
+ shared && prefetch
609
+ ? { origin: 'prefetch', cohort: shared.cohort === 'gate' ? 'gate' : prefetch.cohort }
610
+ : undefined;
611
+ return entry.pending.promise;
612
+ }
613
+ const interval = entry.query.dependencies.reduce((maximum, key) => Math.max(maximum, RESOURCE_PUBLISH_INTERVAL_MS[key.family] ?? 0), 0);
614
+ const wait = interval - (performance.now() - (entry.lastRequestedAt ?? -Infinity));
615
+ if (observedOnly && wait > 0) {
616
+ // A slow read's follow-up can finish just before the next producer notice.
617
+ // Coalesce that automatic read too, rather than exceeding the family's bound.
618
+ entry.readTimer ??= setTimeout(() => {
619
+ entry.readTimer = undefined;
620
+ if (entry.dirty || entry.state.status !== 'ready')
621
+ schedule(entry);
622
+ }, wait);
623
+ return;
624
+ }
625
+ cancelScheduledRead(entry);
626
+ const pending = {
627
+ version,
628
+ serial: ++entry.serial,
629
+ controller: new AbortController(),
630
+ promise: Promise.resolve(),
631
+ prefetch,
632
+ };
633
+ entry.pending = pending;
634
+ entry.dirty = false;
635
+ const unknownGeneration = entry.unknownGeneration;
636
+ entry.state = {
637
+ ...entry.state,
638
+ status: entry.state.data === undefined ? 'loading' : 'stale',
639
+ // A cold connected retry can show loading; saved data keeps its last
640
+ // failure visible until a successful read replaces that observation.
641
+ error: entry.state.data === undefined ? undefined : entry.state.error,
642
+ };
643
+ notify(entry);
644
+ pending.promise = (async () => {
645
+ try {
646
+ if (disposed || pending.version !== version || pending.controller.signal.aborted)
647
+ return;
648
+ if (entry.query.collector) {
649
+ await collectors.require(entry.query, !observedOnly);
650
+ if (disposed || pending.version !== version || pending.controller.signal.aborted)
651
+ return;
652
+ }
653
+ entry.lastRequestedAt = performance.now();
654
+ const fromRunList = isRunListView(entry.query) &&
655
+ connection.capabilities?.resourceChanges?.versionLists === true;
656
+ const reply = fromRunList
657
+ ? await readRunListView(entry, pending.controller.signal)
658
+ : entry.query.method === Methods.NATIVE_SESSION_READ
659
+ ? await readNativeHistory(entry.query.params, entry.state.data, (params, signal) => transport.request(Methods.NATIVE_SESSION_READ, params, signal), pending.controller.signal)
660
+ : await transport.request(entry.query.method, entry.query.params, pending.controller.signal);
661
+ if (disposed ||
662
+ store.deleted ||
663
+ pending.version !== version ||
664
+ pending.serial !== entry.serial ||
665
+ pending.controller.signal.aborted)
666
+ return;
667
+ const writingStore = store;
668
+ const writingIncarnation = storeVersion;
669
+ const supported = entry.query.live && connection.capabilities?.resourceChanges?.version === 1;
670
+ const covered = supported &&
671
+ stampCovers(entry, reply.resourceState) &&
672
+ (!entry.query.collector || collectors.has(entry.query)) &&
673
+ entry.unknownGeneration === unknownGeneration &&
674
+ // A view is current only once every listed run is held at its listed version.
675
+ !reply.incomplete;
676
+ const observedAt = Date.now();
677
+ // `null` is an explicit non-upstream answer: its status follows coverage, with no source time.
678
+ const sourceObservation = entry.query.sourceObservation?.(reply.data);
679
+ if (entry.query.sourceObservation && sourceObservation === undefined)
680
+ throw Object.assign(new Error('The gateway supplied no upstream source observation.'), {
681
+ code: 'RESOURCE_SOURCE_STATE_MISSING',
682
+ });
683
+ if (sourceObservation &&
684
+ sourceObservation.observedAt === undefined &&
685
+ entry.state.sourceObservation?.observedAt !== undefined)
686
+ throw Object.assign(new Error(sourceObservation.error?.message ?? 'The gateway has not observed an upstream value.'), {
687
+ code: sourceObservation.error?.code ?? 'UPSTREAM_UNOBSERVED',
688
+ });
689
+ entry.state = {
690
+ status: covered
691
+ ? sourceObservation && sourceObservation.status !== 'ready'
692
+ ? 'stale'
693
+ : 'ready'
694
+ : supported
695
+ ? 'stale'
696
+ : 'unsupported',
697
+ data: reply.data,
698
+ observedAt,
699
+ resourceState: reply.resourceState,
700
+ ...(sourceObservation ? { sourceObservation, error: sourceObservation.error } : {}),
701
+ ...(!reply.resourceState && supported
702
+ ? {
703
+ error: {
704
+ code: 'RESOURCE_STATE_MISSING',
705
+ message: 'The gateway read supplied no resource revision stamp.',
706
+ },
707
+ }
708
+ : {}),
709
+ };
710
+ // Only a real intervening invalidation needs a follow-up; missing metadata is not a retry loop.
711
+ entry.dirty = supported && !covered && entry.dirty;
712
+ entry.lastUsed = ++lastUsed;
713
+ // Version-list views are derived from the run collection, which is stored on its own.
714
+ const persistResult = entry.query.persisted && canPersist(connection) && reply.incomplete === undefined;
715
+ const serialized = persistResult
716
+ ? JSON.stringify(entry.query.persistedData ? entry.query.persistedData(reply.data) : reply.data)
717
+ : undefined;
718
+ const bytes = serialized === undefined ? 0 : encoder.encode(serialized).byteLength;
719
+ const retention = entry.query.retention;
720
+ const saved = serialized !== undefined &&
721
+ bytes <= entry.query.maxBytes &&
722
+ (!retention || bytes <= retention.maxBytes)
723
+ ? {
724
+ id: nativeHistoryStorageId(entry.query),
725
+ data: JSON.parse(serialized),
726
+ observedAt,
727
+ accessedAt: Date.now(),
728
+ bytes,
729
+ }
730
+ : undefined;
731
+ notify(entry);
732
+ // This reply is already accepted. A transport pause may make it stale
733
+ // without changing its owner or value. Authority changes clear data
734
+ // synchronously; a newer accepted reply replaces its data or timestamp.
735
+ const current = () => !disposed &&
736
+ entries.get(entry.query.id) === entry &&
737
+ entry.state.data === reply.data &&
738
+ entry.state.observedAt === observedAt &&
739
+ writingIncarnation === storeVersion &&
740
+ writingStore === store &&
741
+ !writingStore.deleted;
742
+ // A snapshot someone observes is opened, whoever started the read.
743
+ const prefetched = entry.listeners.size ? undefined : pending.prefetch;
744
+ if (persistResult && current()) {
745
+ await updateStore(writingStore, current, async () => {
746
+ // Prefetch never removes a row it could not replace, including one the user opened.
747
+ if (!saved) {
748
+ if (!prefetched)
749
+ await writingStore.deleteQuery(nativeHistoryStorageId(entry.query));
750
+ return;
751
+ }
752
+ let row = saved;
753
+ if (retention?.cohort === 'workspace') {
754
+ const stored = await writingStore.listQueries();
755
+ const previous = stored.find((value) => value.id === saved.id);
756
+ // A prefetch write evicts only what prefetch may evict, even when it refreshes a
757
+ // snapshot the user opened; that row stays opened. Gate outranks run.
758
+ const write = prefetched
759
+ ? {
760
+ ...saved,
761
+ origin: 'prefetch',
762
+ cohort: previous?.cohort === 'gate' ? 'gate' : (prefetched.cohort ?? 'run'),
763
+ }
764
+ : saved;
765
+ if (!previous || previous.origin === 'prefetch')
766
+ row = write;
767
+ const victims = workspaceRetentionEvictions(entry.query, write, stored, options.contentBudgetBytes ?? defaultContentBudgetBytes, await writingStore.listContent());
768
+ if (!current())
769
+ return;
770
+ if (!victims) {
771
+ if (!prefetched)
772
+ await writingStore.deleteQuery(saved.id);
773
+ return;
774
+ }
775
+ for (const id of victims.queries) {
776
+ await writingStore.deleteQuery(id);
777
+ if (!current())
778
+ return;
779
+ }
780
+ for (const key of victims.content) {
781
+ await writingStore.deleteContent(key);
782
+ if (!current())
783
+ return;
784
+ }
785
+ }
786
+ else if (retention) {
787
+ const previous = (await writingStore.listQueries())
788
+ .filter((value) => value.id !== saved.id &&
789
+ // The run collection is bounded by its own body budget, never by its views.
790
+ value.id !== RUN_LIST_COLLECTION_ID &&
791
+ (retention.cohort === 'upstream'
792
+ ? isUpstreamStoredQuery(value.id)
793
+ : value.id.startsWith(`${entry.query.method}:`)))
794
+ .sort((a, b) => a.observedAt - b.observedAt ||
795
+ (entries.get(a.id)?.lastUsed ?? 0) - (entries.get(b.id)?.lastUsed ?? 0));
796
+ if (!current())
797
+ return;
798
+ let count = previous.length + 1;
799
+ let total = previous.reduce((sum, value) => sum + value.bytes, saved.bytes);
800
+ for (const oldest of previous) {
801
+ if (count <= retention.maxEntries && total <= retention.maxBytes)
802
+ break;
803
+ await writingStore.deleteQuery(oldest.id);
804
+ if (!current())
805
+ return;
806
+ count--;
807
+ total -= oldest.bytes;
808
+ }
809
+ }
810
+ if (current())
811
+ await writingStore.putQuery(row);
812
+ });
813
+ }
814
+ }
815
+ catch (error) {
816
+ if (disposed ||
817
+ pending.version !== version ||
818
+ pending.serial !== entry.serial ||
819
+ pending.controller.signal.aborted)
820
+ return;
821
+ const failure = queryError(error);
822
+ // An intervening write still needs one read after failed I/O. Terminal
823
+ // refusals retire that work; an unchanged failed source never retries.
824
+ entry.dirty &&=
825
+ !evictionCodes.has(failure.code) && failure.code !== 'RESOURCE_OBSERVATION_UNSUPPORTED';
826
+ if (evictionCodes.has(failure.code)) {
827
+ const evictingStore = store;
828
+ const evictingIncarnation = storeVersion;
829
+ entry.state = { status: 'error', error: failure };
830
+ // Commit an accepted refusal before a listener queues its profile's close.
831
+ const eviction = updateStore(evictingStore, () => evictingStore === store &&
832
+ evictingIncarnation === storeVersion &&
833
+ !evictingStore.deleted, () => evictingStore.deleteQuery(nativeHistoryStorageId(entry.query)));
834
+ notify(entry);
835
+ // An authorization refusal settles its callers in the reply's own task, so a prefetch
836
+ // stops before a store callback can release another read. The eviction is queued.
837
+ if (failure.code === 'AUTH_FORBIDDEN' || failure.code === 'AUTH_REQUIRED')
838
+ return;
839
+ await eviction;
840
+ if (disposed ||
841
+ pending.version !== version ||
842
+ pending.serial !== entry.serial ||
843
+ pending.controller.signal.aborted)
844
+ return;
845
+ }
846
+ else
847
+ entry.state = {
848
+ ...entry.state,
849
+ status: failure.code === 'RESOURCE_OBSERVATION_UNSUPPORTED'
850
+ ? 'unsupported'
851
+ : entry.state.data === undefined
852
+ ? 'error'
853
+ : 'stale',
854
+ error: failure,
855
+ };
856
+ notify(entry);
857
+ }
858
+ finally {
859
+ if (entry.pending === pending) {
860
+ entry.pending = undefined;
861
+ if (entry.dirty && entry.listeners.size && usable)
862
+ schedule(entry);
863
+ prune();
864
+ }
865
+ }
866
+ })();
867
+ if (signal) {
868
+ // A read only this caller wants stops with it: no response is written after the abort.
869
+ const cancel = () => {
870
+ if (entry.pending === pending && !pending.shared && !entry.listeners.size)
871
+ invalidate(entry);
872
+ };
873
+ signal.addEventListener('abort', cancel, { once: true });
874
+ void pending.promise.finally(() => signal.removeEventListener('abort', cancel));
875
+ }
876
+ return pending.promise;
877
+ }
878
+ /** Content reads follow hydration's owner and incarnation fences; offline reads are allowed. */
879
+ async function readStoredContent(read, open) {
880
+ const ownerVersion = hydrationOwnerVersion;
881
+ await barrier;
882
+ const readingStore = store;
883
+ const incarnation = storeVersion;
884
+ const current = () => !disposed &&
885
+ ownerVersion === hydrationOwnerVersion &&
886
+ incarnation === storeVersion &&
887
+ readingStore === store &&
888
+ !readingStore.deleted;
889
+ if (!current())
890
+ return undefined;
891
+ const value = await read(readingStore).catch(async (error) => {
892
+ await recoverStore(error, readingStore);
893
+ return undefined;
894
+ });
895
+ if (value === undefined || !current())
896
+ return undefined;
897
+ if (open)
898
+ await updateStore(readingStore, current, () => open(readingStore, value));
899
+ return current() ? value : undefined;
900
+ }
901
+ async function readContentBytes(lookup, open) {
902
+ const saved = await readStoredContent(lookup, open
903
+ ? (target, { kind, id }) => target.touchContent({ kind, id }, Date.now(), 'opened')
904
+ : undefined);
905
+ return saved?.data;
906
+ }
907
+ /** Content writes accept only the current connection's owner, from the token's fetch onward. */
908
+ async function writeContent(token, write) {
909
+ if (disposed || !connection.connected)
910
+ return false;
911
+ const ownerVersion = token?.ownerVersion ?? hydrationOwnerVersion;
912
+ const connectionId = token?.connectionId ?? connection.id;
913
+ await barrier;
914
+ const writingStore = store;
915
+ const incarnation = token?.storeVersion ?? storeVersion;
916
+ const current = () => !disposed &&
917
+ connection.connected &&
918
+ connection.id === connectionId &&
919
+ ownerVersion === hydrationOwnerVersion &&
920
+ incarnation === storeVersion &&
921
+ writingStore === store &&
922
+ !writingStore.deleted;
923
+ let written = false;
924
+ await updateStore(writingStore, current, async () => {
925
+ written = (await write(writingStore, current)) && current();
926
+ });
927
+ return written;
928
+ }
929
+ /** Stores verified bytes within the caps and the shared budget. */
930
+ function storeContent(input) {
931
+ const key = contentKey(input.key);
932
+ const { data, origin, cohort } = input;
933
+ if (!resourceContentWithinLimits(key, data) ||
934
+ !verifyResourceContent(key, data, input.untrackedFiles))
935
+ return Promise.resolve(false);
936
+ return writeContent(input.token, async (target, current) => {
937
+ const accessedAt = Date.now();
938
+ const content = await target.listContent();
939
+ if (content.some((row) => row.kind === key.kind && row.id === key.id)) {
940
+ // The same identity holds the same bytes; keep the stronger origin and cohort.
941
+ if (!current())
942
+ return false;
943
+ await target.retainContent(key, accessedAt, { origin, cohort });
944
+ }
945
+ else {
946
+ const victims = resourceBudgetEvictions({ bytes: data.byteLength, origin, cohort }, content, (await target.listQueries()).filter((row) => workspaceRetentionMembership(row.id) !== undefined), options.contentBudgetBytes ?? defaultContentBudgetBytes);
947
+ if (!victims || !current())
948
+ return false;
949
+ for (const id of victims.queries) {
950
+ await target.deleteQuery(id);
951
+ if (!current())
952
+ return false;
953
+ }
954
+ for (const victim of victims.content) {
955
+ await target.deleteContent(victim);
956
+ if (!current())
957
+ return false;
958
+ }
959
+ await target.putContent({
960
+ ...key,
961
+ data,
962
+ bytes: data.byteLength,
963
+ origin,
964
+ cohort,
965
+ accessedAt,
966
+ });
967
+ }
968
+ if (input.alias !== undefined && current())
969
+ await target.putContentAlias(input.alias, key);
970
+ return true;
971
+ });
972
+ }
973
+ /** Promotes held content without its bytes, behind the same fences as a store. */
974
+ function retainContent(key, retain) {
975
+ const held = contentKey(key);
976
+ return writeContent(retain.token, async (target, current) => {
977
+ if (!(await target.hasContent(held)) || !current())
978
+ return false;
979
+ await target.retainContent(held, Date.now(), {
980
+ origin: retain.origin,
981
+ cohort: retain.cohort,
982
+ });
983
+ return true;
984
+ });
985
+ }
986
+ return {
987
+ observe(query, listener) {
988
+ if (disposed)
989
+ throw new Error('Resource cache is disposed');
990
+ const entry = entryFor(query);
991
+ const receive = (state) => listener(state);
992
+ entry.listeners.add(receive);
993
+ updateCollectors();
994
+ if (query.retention?.cohort === 'workspace') {
995
+ const bindingVersion = version;
996
+ const sessionId = connection.id;
997
+ const hydrationVersion = hydrationOwnerVersion;
998
+ const entryQuery = entry.query;
999
+ const accessedAt = Date.now();
1000
+ const sameBinding = () => !disposed &&
1001
+ connection.id === sessionId &&
1002
+ version === bindingVersion &&
1003
+ hydrationOwnerVersion === hydrationVersion &&
1004
+ entries.get(query.id) === entry &&
1005
+ entry.query === entryQuery;
1006
+ const touch = async () => {
1007
+ // Let cold hydration establish the saved authority before selecting the store to touch.
1008
+ await entry.hydration;
1009
+ if (!sameBinding())
1010
+ return;
1011
+ const accessedStore = store;
1012
+ const incarnation = storeVersion;
1013
+ const authority = metadata;
1014
+ const current = () => sameBinding() &&
1015
+ metadata === authority &&
1016
+ store === accessedStore &&
1017
+ storeVersion === incarnation &&
1018
+ !accessedStore.deleted;
1019
+ await updateStore(accessedStore, current, async () => {
1020
+ if (current())
1021
+ await accessedStore.touchQuery?.(nativeHistoryStorageId(query), accessedAt, 'opened');
1022
+ });
1023
+ };
1024
+ void touch().catch((error) => {
1025
+ if (!sameBinding())
1026
+ return;
1027
+ storageNotice = `Cache recency could not be saved: ${queryError(error).message}`;
1028
+ options.onStorageNotice?.(storageNotice);
1029
+ notify(entry);
1030
+ });
1031
+ }
1032
+ receive(entry.state);
1033
+ if (entry.state.status !== 'ready' || entry.dirty)
1034
+ schedule(entry);
1035
+ return () => {
1036
+ entry.listeners.delete(receive);
1037
+ updateCollectors();
1038
+ if (!entry.listeners.size) {
1039
+ cancelScheduledRead(entry);
1040
+ if (entry.pending)
1041
+ invalidate(entry);
1042
+ }
1043
+ entry.lastUsed = ++lastUsed;
1044
+ prune();
1045
+ };
1046
+ },
1047
+ read(query) {
1048
+ if (disposed)
1049
+ throw new Error('Resource cache is disposed');
1050
+ return entryFor(query).state;
1051
+ },
1052
+ async retry(query, placement, signal) {
1053
+ if (disposed)
1054
+ throw new Error('Resource cache is disposed');
1055
+ const entry = entryFor(query);
1056
+ await fetchEntry(entry, false, placement, signal);
1057
+ return entry.state;
1058
+ },
1059
+ async hasContent(key) {
1060
+ return (await readStoredContent((target) => target.hasContent(contentKey(key)))) ?? false;
1061
+ },
1062
+ readContent: (key) => readContentBytes((target) => target.getContent(contentKey(key)), false),
1063
+ runListProjections: () => runList.projections(),
1064
+ openContent: (key) => readContentBytes((target) => target.getContent(contentKey(key)), true),
1065
+ readContentByAlias: (alias) => readContentBytes((target) => target.getContentByAlias(alias), false),
1066
+ openContentByAlias: (alias) => readContentBytes((target) => target.getContentByAlias(alias), true),
1067
+ storeContent,
1068
+ retainContent,
1069
+ contentWriteToken: () => ({
1070
+ connectionId: connection.id,
1071
+ ownerVersion: hydrationOwnerVersion,
1072
+ storeVersion,
1073
+ }),
1074
+ async dispose(reason) {
1075
+ if (disposed)
1076
+ return;
1077
+ const withdrawing = collectors.close();
1078
+ disposed = true;
1079
+ version++;
1080
+ unsubscribeChanges();
1081
+ unsubscribeConnection();
1082
+ unsubscribeStore();
1083
+ for (const entry of entries.values()) {
1084
+ entry.serial++;
1085
+ cancelScheduledRead(entry);
1086
+ entry.pending?.controller.abort();
1087
+ entry.listeners.clear();
1088
+ }
1089
+ entries.clear();
1090
+ await barrier;
1091
+ try {
1092
+ if (reason && deletionReasons.has(reason))
1093
+ await store.deleteAll();
1094
+ await withdrawing;
1095
+ }
1096
+ finally {
1097
+ await store.close();
1098
+ }
1099
+ },
1100
+ };
1101
+ }
1102
+ //# sourceMappingURL=cache.js.map