@farmslot/protocol 0.74.10 → 0.76.0
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.
- package/CHANGELOG.md +42 -0
- package/dist/client-cache/artifact-response.d.ts +15 -0
- package/dist/client-cache/artifact-response.d.ts.map +1 -0
- package/dist/client-cache/artifact-response.js +103 -0
- package/dist/client-cache/artifact-response.js.map +1 -0
- package/dist/client-cache/cache.d.ts +6 -1
- package/dist/client-cache/cache.d.ts.map +1 -1
- package/dist/client-cache/cache.js +231 -40
- package/dist/client-cache/cache.js.map +1 -1
- package/dist/client-cache/command-method.d.ts +17 -4
- package/dist/client-cache/command-method.d.ts.map +1 -1
- package/dist/client-cache/command-method.js +222 -1
- package/dist/client-cache/command-method.js.map +1 -1
- package/dist/client-cache/conformance.d.ts +2 -5
- package/dist/client-cache/conformance.d.ts.map +1 -1
- package/dist/client-cache/conformance.js +4 -0
- package/dist/client-cache/conformance.js.map +1 -1
- package/dist/client-cache/content.d.ts +11 -0
- package/dist/client-cache/content.d.ts.map +1 -1
- package/dist/client-cache/content.js +43 -0
- package/dist/client-cache/content.js.map +1 -1
- package/dist/client-cache/index.d.ts +5 -2
- package/dist/client-cache/index.d.ts.map +1 -1
- package/dist/client-cache/index.js +5 -2
- package/dist/client-cache/index.js.map +1 -1
- package/dist/client-cache/network-cost.d.ts +4 -0
- package/dist/client-cache/network-cost.d.ts.map +1 -0
- package/dist/client-cache/network-cost.js +24 -0
- package/dist/client-cache/network-cost.js.map +1 -0
- package/dist/client-cache/prefetch.d.ts +17 -0
- package/dist/client-cache/prefetch.d.ts.map +1 -1
- package/dist/client-cache/prefetch.js +110 -47
- package/dist/client-cache/prefetch.js.map +1 -1
- package/dist/client-cache/read-stored.d.ts +7 -0
- package/dist/client-cache/read-stored.d.ts.map +1 -0
- package/dist/client-cache/read-stored.js +18 -0
- package/dist/client-cache/read-stored.js.map +1 -0
- package/dist/client-cache/registry.d.ts +309 -16
- package/dist/client-cache/registry.d.ts.map +1 -1
- package/dist/client-cache/registry.js +422 -27
- package/dist/client-cache/registry.js.map +1 -1
- package/dist/client-cache/run-details.d.ts +29 -0
- package/dist/client-cache/run-details.d.ts.map +1 -0
- package/dist/client-cache/run-details.js +145 -0
- package/dist/client-cache/run-details.js.map +1 -0
- package/dist/client-cache/run-list-versions.d.ts +2 -0
- package/dist/client-cache/run-list-versions.d.ts.map +1 -1
- package/dist/client-cache/run-list-versions.js +9 -0
- package/dist/client-cache/run-list-versions.js.map +1 -1
- package/dist/client-cache/store.d.ts +8 -0
- package/dist/client-cache/store.d.ts.map +1 -1
- package/dist/client-cache/store.js +25 -0
- package/dist/client-cache/store.js.map +1 -1
- package/dist/client-cache/transport.d.ts +8 -1
- package/dist/client-cache/transport.d.ts.map +1 -1
- package/dist/client-cache/transport.js +8 -0
- package/dist/client-cache/transport.js.map +1 -1
- package/dist/client-cache/types.d.ts +11 -1
- package/dist/client-cache/types.d.ts.map +1 -1
- package/dist/contracts/agents.d.ts +2 -0
- package/dist/contracts/agents.d.ts.map +1 -1
- package/dist/contracts/agents.js.map +1 -1
- package/dist/contracts/config.d.ts +10 -0
- package/dist/contracts/config.d.ts.map +1 -1
- package/dist/contracts/config.js +7 -0
- package/dist/contracts/config.js.map +1 -1
- package/dist/contracts/dispatch.d.ts +18 -0
- package/dist/contracts/dispatch.d.ts.map +1 -1
- package/dist/contracts/run-lifecycle-view.d.ts +9 -0
- package/dist/contracts/run-lifecycle-view.d.ts.map +1 -1
- package/dist/contracts/run-lifecycle-view.js +8 -0
- package/dist/contracts/run-lifecycle-view.js.map +1 -1
- package/dist/contracts/runs.d.ts +78 -7
- package/dist/contracts/runs.d.ts.map +1 -1
- package/dist/contracts/runs.js +33 -2
- package/dist/contracts/runs.js.map +1 -1
- package/dist/contracts/slot-recovery.d.ts +11 -0
- package/dist/contracts/slot-recovery.d.ts.map +1 -1
- package/dist/contracts/slot-recovery.js +11 -1
- package/dist/contracts/slot-recovery.js.map +1 -1
- package/dist/contracts/slots.d.ts +17 -0
- package/dist/contracts/slots.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/recipe/recording.d.ts +1 -1
- package/dist/recipe/recording.d.ts.map +1 -1
- package/dist/recipe/recording.js +2 -2
- package/dist/recipe/recording.js.map +1 -1
- package/dist/rpc/dispatch.d.ts +8 -0
- package/dist/rpc/dispatch.d.ts.map +1 -1
- package/dist/rpc/dispatch.js +8 -0
- package/dist/rpc/dispatch.js.map +1 -1
- package/dist/rpc/fleet.d.ts +3 -1
- package/dist/rpc/fleet.d.ts.map +1 -1
- package/dist/rpc/fleet.js.map +1 -1
- package/dist/rpc/native-session.d.ts +29 -0
- package/dist/rpc/native-session.d.ts.map +1 -1
- package/dist/rpc/native-session.js +9 -0
- package/dist/rpc/native-session.js.map +1 -1
- package/dist/rpc/registry.d.ts +1 -0
- package/dist/rpc/registry.d.ts.map +1 -1
- package/dist/rpc/registry.js +1 -0
- package/dist/rpc/registry.js.map +1 -1
- package/dist/rpc/run.d.ts +4 -0
- package/dist/rpc/run.d.ts.map +1 -1
- package/dist/rpc/run.js.map +1 -1
- package/dist/rpc/runner-models.d.ts +14 -3
- package/dist/rpc/runner-models.d.ts.map +1 -1
- package/dist/rpc/runner-models.js +37 -13
- package/dist/rpc/runner-models.js.map +1 -1
- package/dist/rpc/slot.d.ts +3 -2
- package/dist/rpc/slot.d.ts.map +1 -1
- package/dist/rpc/slot.js.map +1 -1
- package/dist/rpc/tmux.d.ts +8 -0
- package/dist/rpc/tmux.d.ts.map +1 -1
- package/dist/runs/list-trim.d.ts +13 -0
- package/dist/runs/list-trim.d.ts.map +1 -0
- package/dist/runs/list-trim.js +71 -0
- package/dist/runs/list-trim.js.map +1 -0
- package/dist/runs/list-version.d.ts +4 -0
- package/dist/runs/list-version.d.ts.map +1 -0
- package/dist/runs/list-version.js +9 -0
- package/dist/runs/list-version.js.map +1 -0
- package/dist/runs/review-chain.d.ts.map +1 -1
- package/dist/runs/review-chain.js +2 -1
- package/dist/runs/review-chain.js.map +1 -1
- package/dist/runs/run-family-groups.d.ts.map +1 -1
- package/dist/runs/run-family-groups.js +5 -4
- package/dist/runs/run-family-groups.js.map +1 -1
- package/dist/runs/run-list-view.d.ts.map +1 -1
- package/dist/runs/run-list-view.js +2 -1
- package/dist/runs/run-list-view.js.map +1 -1
- package/dist/slots/tracking-branch.d.ts +7 -0
- package/dist/slots/tracking-branch.d.ts.map +1 -1
- package/dist/slots/tracking-branch.js.map +1 -1
- package/dist/surfaces/command-center.js +1 -1
- package/dist/surfaces/command-center.js.map +1 -1
- package/dist/surfaces/view-links.d.ts +1 -0
- package/dist/surfaces/view-links.d.ts.map +1 -1
- package/dist/surfaces/view-links.js +23 -8
- package/dist/surfaces/view-links.js.map +1 -1
- package/dist/text-order.d.ts +7 -0
- package/dist/text-order.d.ts.map +1 -0
- package/dist/text-order.js +7 -0
- package/dist/text-order.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Methods } from '../rpc/registry.js';
|
|
2
|
+
import { runListVersion } from '../runs/list-version.js';
|
|
2
3
|
import { ResourceCollectorAdmission } from './collector-admission.js';
|
|
3
|
-
import { resourceBudgetEvictions, resourceContentWithinLimits, verifyResourceContent, } from './content.js';
|
|
4
|
+
import { resourceBudgetEvictions, resourceContentWithinLimits, verifyResourceContent, verifyResourceContentInSlices, } from './content.js';
|
|
4
5
|
import { isKnownResourceFamily, RESOURCE_PUBLISH_INTERVAL_MS, RESOURCE_REREAD_INTERVAL_MS, RESOURCE_RETRY_MAX_WAIT_MS, resourceKeyId, } from './keys.js';
|
|
5
6
|
import { nativeHistoryStorageId, readNativeHistory, storedNativeHistoryMatches, } from './native-history.js';
|
|
6
7
|
import { resourceQuery } from './registry.js';
|
|
8
|
+
import { createRunDetailSync, runDetailTarget } from './run-details.js';
|
|
7
9
|
import { isRunListCollectionRead, RUN_LIST_COLLECTION_ID, RunListCollection, runListSummaryFlags, } from './run-list-versions.js';
|
|
8
|
-
import { createMemoryResourceStore, normalizeResourceStoreScope, resourceStoreScopeId, sameResourceAuthority, } from './store.js';
|
|
10
|
+
import { createMemoryResourceStore, normalizeResourceStoreScope, readStoredQueryBatch, resourceStoreScopeId, sameResourceAuthority, } from './store.js';
|
|
9
11
|
import { isUpstreamStoredQuery, upstreamRetentionEvictions } from './upstream-retention.js';
|
|
10
12
|
import { workspaceRetentionEvictions, workspaceRetentionMembership, } from './workspace-retention.js';
|
|
11
13
|
const deletionReasons = new Set([
|
|
@@ -21,6 +23,13 @@ const evictionCodes = new Set([
|
|
|
21
23
|
'RUN_NOT_FOUND',
|
|
22
24
|
'RESOURCE_NOT_FOUND',
|
|
23
25
|
]);
|
|
26
|
+
/**
|
|
27
|
+
* The gateway refused the read: the resource is gone or not this principal's. The
|
|
28
|
+
* cache evicts saved data on these; a network failure or timeout is not one.
|
|
29
|
+
*/
|
|
30
|
+
export function readRefused(error) {
|
|
31
|
+
return Boolean(error && evictionCodes.has(error.code));
|
|
32
|
+
}
|
|
24
33
|
function queryError(error) {
|
|
25
34
|
const code = error && typeof error === 'object' ? Reflect.get(error, 'code') : undefined;
|
|
26
35
|
return {
|
|
@@ -44,6 +53,7 @@ function normalizeConnection(connection) {
|
|
|
44
53
|
};
|
|
45
54
|
}
|
|
46
55
|
const defaultContentBudgetBytes = 500 * 1024 * 1024;
|
|
56
|
+
const startupQueryBytes = 8 * 1024 * 1024;
|
|
47
57
|
// Only the identity crosses the store seam, never a caller's other fields.
|
|
48
58
|
const contentKey = ({ kind, id }) => ({ kind, id });
|
|
49
59
|
function profileId(connection) {
|
|
@@ -56,6 +66,9 @@ export function createGatewayResourceCache(options) {
|
|
|
56
66
|
const transport = options.transport;
|
|
57
67
|
let store = options.store ?? createMemoryResourceStore();
|
|
58
68
|
let metadata;
|
|
69
|
+
let startupStore;
|
|
70
|
+
let startupOwnerVersion = -1;
|
|
71
|
+
const startupQueries = new Map();
|
|
59
72
|
let connection = normalizeConnection(transport.getConnection());
|
|
60
73
|
let usable = false;
|
|
61
74
|
let disposed = false;
|
|
@@ -91,9 +104,33 @@ export function createGatewayResourceCache(options) {
|
|
|
91
104
|
maxBodyBytes: resourceQuery(Methods.RUN_LIST).maxBytes,
|
|
92
105
|
});
|
|
93
106
|
let runListLoad;
|
|
107
|
+
const fallbackDetailTargets = new Map();
|
|
108
|
+
const detailSync = createRunDetailSync({
|
|
109
|
+
connection: () => connection,
|
|
110
|
+
list: async () => (await readStoredContent((target) => target.listQueries())) ?? [],
|
|
111
|
+
stored: (runId) => readStoredContent((target) => target.getQuery(resourceQuery(Methods.RUN_GET, { runId }).id)),
|
|
112
|
+
touch: async (runId) => {
|
|
113
|
+
const target = store;
|
|
114
|
+
const ownerVersion = hydrationOwnerVersion;
|
|
115
|
+
await updateStore(target, () => target === store && !target.deleted && ownerVersion === hydrationOwnerVersion, () => target.touchQuery?.(resourceQuery(Methods.RUN_GET, { runId }).id, Date.now()) ??
|
|
116
|
+
Promise.resolve());
|
|
117
|
+
},
|
|
118
|
+
fetch: (runId, maxStoredBytes, signal) => retryQuery(resourceQuery(Methods.RUN_GET, { runId }), { origin: 'prefetch', cohort: 'run', maxStoredBytes }, signal),
|
|
119
|
+
onError: (error) => options.onStorageNotice?.(`Background run details could not sync: ${queryError(error).message}`),
|
|
120
|
+
});
|
|
121
|
+
function refreshDetailTargets() {
|
|
122
|
+
const versioned = runList.detailTargets();
|
|
123
|
+
detailSync.setTargets(store.persistent && usable
|
|
124
|
+
? runList.held
|
|
125
|
+
? versioned
|
|
126
|
+
: [...fallbackDetailTargets.values()]
|
|
127
|
+
: []);
|
|
128
|
+
}
|
|
94
129
|
function forgetRunList() {
|
|
95
130
|
runList.clear();
|
|
96
131
|
runListLoad = undefined;
|
|
132
|
+
fallbackDetailTargets.clear();
|
|
133
|
+
detailSync.reset();
|
|
97
134
|
}
|
|
98
135
|
function updateCollectors() {
|
|
99
136
|
collectors.update({ ...connection, foreground: usable && connection.foreground !== false }, usable
|
|
@@ -116,12 +153,29 @@ export function createGatewayResourceCache(options) {
|
|
|
116
153
|
unsubscribeStore();
|
|
117
154
|
store = next;
|
|
118
155
|
storeVersion++;
|
|
156
|
+
startupStore = undefined;
|
|
157
|
+
startupQueries.clear();
|
|
158
|
+
detailSync.reset();
|
|
119
159
|
unsubscribeStore = store.onDeleted(storeDeleted);
|
|
120
160
|
}
|
|
161
|
+
/** Observers whose failure was already logged, so a repeating one is reported once. */
|
|
162
|
+
const failedObservers = new WeakSet();
|
|
163
|
+
/** One observer's failure is its own: the other observers and the cache's reads go on. */
|
|
164
|
+
function deliver(entry, listener) {
|
|
165
|
+
try {
|
|
166
|
+
listener(entry.state);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
if (failedObservers.has(listener))
|
|
170
|
+
return;
|
|
171
|
+
failedObservers.add(listener);
|
|
172
|
+
console.error(`Resource cache observer of ${entry.query.id} failed:`, error);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
121
175
|
function notify(entry) {
|
|
122
176
|
entry.state = { ...entry.state, storageNotice };
|
|
123
177
|
for (const listener of entry.listeners)
|
|
124
|
-
|
|
178
|
+
deliver(entry, listener);
|
|
125
179
|
}
|
|
126
180
|
function cancelScheduledRead(entry) {
|
|
127
181
|
if (entry.readTimer !== undefined)
|
|
@@ -178,13 +232,24 @@ export function createGatewayResourceCache(options) {
|
|
|
178
232
|
for (const entry of entries.values())
|
|
179
233
|
notify(entry);
|
|
180
234
|
}
|
|
235
|
+
function withStoreWriteLock(target, write) {
|
|
236
|
+
return target.withWriteLock ? target.withWriteLock(write) : write();
|
|
237
|
+
}
|
|
181
238
|
/** Retention, writes and lifecycle changes share one ordered store boundary. */
|
|
182
|
-
function updateStore(target, current, update) {
|
|
239
|
+
function updateStore(target, current, update, lockWrites = true) {
|
|
183
240
|
const updating = barrier.then(async () => {
|
|
184
241
|
if (!current())
|
|
185
242
|
return;
|
|
186
243
|
try {
|
|
187
|
-
|
|
244
|
+
const write = async () => {
|
|
245
|
+
// A queued cross-tab write must recheck the owner after acquiring its lock.
|
|
246
|
+
if (current())
|
|
247
|
+
await update();
|
|
248
|
+
};
|
|
249
|
+
if (lockWrites)
|
|
250
|
+
await withStoreWriteLock(target, write);
|
|
251
|
+
else
|
|
252
|
+
await write();
|
|
188
253
|
}
|
|
189
254
|
catch (error) {
|
|
190
255
|
await recoverStore(error, target);
|
|
@@ -232,6 +297,46 @@ export function createGatewayResourceCache(options) {
|
|
|
232
297
|
if (entry.state.status !== 'ready')
|
|
233
298
|
hydrateEntry(entry);
|
|
234
299
|
}
|
|
300
|
+
async function warmStoredQueries() {
|
|
301
|
+
if (!store.persistent ||
|
|
302
|
+
(startupStore === store && startupOwnerVersion === hydrationOwnerVersion))
|
|
303
|
+
return;
|
|
304
|
+
const readingStore = store;
|
|
305
|
+
const incarnation = storeVersion;
|
|
306
|
+
const ownerVersion = hydrationOwnerVersion;
|
|
307
|
+
const current = () => !disposed &&
|
|
308
|
+
store === readingStore &&
|
|
309
|
+
!readingStore.deleted &&
|
|
310
|
+
incarnation === storeVersion &&
|
|
311
|
+
ownerVersion === hydrationOwnerVersion;
|
|
312
|
+
try {
|
|
313
|
+
const batch = await readStoredQueryBatch(readingStore, startupQueryBytes);
|
|
314
|
+
if (!current())
|
|
315
|
+
return;
|
|
316
|
+
const collection = batch.find((saved) => saved.id === RUN_LIST_COLLECTION_ID);
|
|
317
|
+
const rows = collection && readingStore.getRows
|
|
318
|
+
? await readingStore.getRows(RUN_LIST_COLLECTION_ID)
|
|
319
|
+
: undefined;
|
|
320
|
+
if (!current())
|
|
321
|
+
return;
|
|
322
|
+
startupQueries.clear();
|
|
323
|
+
let remaining = startupQueryBytes;
|
|
324
|
+
for (const saved of batch) {
|
|
325
|
+
if (saved.bytes > remaining)
|
|
326
|
+
continue;
|
|
327
|
+
remaining -= saved.bytes;
|
|
328
|
+
if (saved.id !== RUN_LIST_COLLECTION_ID)
|
|
329
|
+
startupQueries.set(saved.id, saved);
|
|
330
|
+
}
|
|
331
|
+
if (rows)
|
|
332
|
+
runList.hydrate(rows);
|
|
333
|
+
startupStore = readingStore;
|
|
334
|
+
startupOwnerVersion = ownerVersion;
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
await recoverStore(error, readingStore);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
235
340
|
function schedule(entry) {
|
|
236
341
|
if (entry.queued || disposed)
|
|
237
342
|
return;
|
|
@@ -261,8 +366,13 @@ export function createGatewayResourceCache(options) {
|
|
|
261
366
|
connection.id === next.id &&
|
|
262
367
|
!scopeChanged &&
|
|
263
368
|
!authorityChanged &&
|
|
264
|
-
!revoke)
|
|
369
|
+
!revoke) {
|
|
370
|
+
if (connection.metered !== next.metered) {
|
|
371
|
+
connection = { ...connection, metered: next.metered };
|
|
372
|
+
refreshDetailTargets();
|
|
373
|
+
}
|
|
265
374
|
return;
|
|
375
|
+
}
|
|
266
376
|
const ticket = ++version;
|
|
267
377
|
runList.abort();
|
|
268
378
|
// Stored facts belong to an owner, not a socket attempt. Fence a revoked
|
|
@@ -273,6 +383,7 @@ export function createGatewayResourceCache(options) {
|
|
|
273
383
|
}
|
|
274
384
|
connection = next;
|
|
275
385
|
usable = false;
|
|
386
|
+
refreshDetailTargets();
|
|
276
387
|
updateCollectors();
|
|
277
388
|
revisions.clear();
|
|
278
389
|
for (const entry of entries.values()) {
|
|
@@ -353,17 +464,24 @@ export function createGatewayResourceCache(options) {
|
|
|
353
464
|
if (store.persistent && retentionSweptStore !== store) {
|
|
354
465
|
const target = store;
|
|
355
466
|
try {
|
|
356
|
-
|
|
357
|
-
await target.
|
|
358
|
-
|
|
467
|
+
await withStoreWriteLock(target, async () => {
|
|
468
|
+
for (const id of upstreamRetentionEvictions(await target.listQueries()))
|
|
469
|
+
await target.deleteQuery(id);
|
|
470
|
+
retentionSweptStore = target;
|
|
471
|
+
});
|
|
359
472
|
}
|
|
360
473
|
catch (error) {
|
|
361
474
|
await recoverStore(error, target);
|
|
362
475
|
}
|
|
363
476
|
}
|
|
477
|
+
if (disposed || ticket !== version)
|
|
478
|
+
return;
|
|
479
|
+
if (!revoke && canPersist(next))
|
|
480
|
+
await warmStoredQueries();
|
|
364
481
|
if (disposed || ticket !== version)
|
|
365
482
|
return;
|
|
366
483
|
usable = next.connected && next.foreground !== false && !revoke;
|
|
484
|
+
refreshDetailTargets();
|
|
367
485
|
updateCollectors();
|
|
368
486
|
if (usable)
|
|
369
487
|
for (const entry of entries.values())
|
|
@@ -409,7 +527,7 @@ export function createGatewayResourceCache(options) {
|
|
|
409
527
|
if (change.revision <= (revisions.get(id) ?? -1))
|
|
410
528
|
continue;
|
|
411
529
|
revisions.set(id, change.revision);
|
|
412
|
-
const affected = [...entries.values()].filter((entry) => entry.
|
|
530
|
+
const affected = [...entries.values()].filter((entry) => entry.dependencyIds.has(id));
|
|
413
531
|
// Coverage counts declared dependencies only, so a known key that no entry declares (another
|
|
414
532
|
// slot, an unopened workspace) changes nothing held. Only a family this client cannot name might.
|
|
415
533
|
if (!affected.length && isKnownResourceFamily(change.key?.family))
|
|
@@ -441,6 +559,7 @@ export function createGatewayResourceCache(options) {
|
|
|
441
559
|
}
|
|
442
560
|
const entry = {
|
|
443
561
|
query,
|
|
562
|
+
dependencyIds: new Set(query.dependencies.map(resourceKeyId)),
|
|
444
563
|
state: { status: 'loading' },
|
|
445
564
|
listeners: new Set(),
|
|
446
565
|
hydration: Promise.resolve(),
|
|
@@ -454,9 +573,40 @@ export function createGatewayResourceCache(options) {
|
|
|
454
573
|
failures: 0,
|
|
455
574
|
};
|
|
456
575
|
entries.set(query.id, entry);
|
|
457
|
-
|
|
576
|
+
const saved = isRunListView(query) ? heldRunListView(query) : takeStartupQuery(query);
|
|
577
|
+
if (applyStoredQuery(entry, saved)) {
|
|
578
|
+
entry.hydrated = true;
|
|
579
|
+
queueMicrotask(prune);
|
|
580
|
+
}
|
|
581
|
+
else
|
|
582
|
+
hydrateEntry(entry);
|
|
458
583
|
return entry;
|
|
459
584
|
}
|
|
585
|
+
function takeStartupQuery(query) {
|
|
586
|
+
if (startupStore !== store || startupOwnerVersion !== hydrationOwnerVersion)
|
|
587
|
+
return undefined;
|
|
588
|
+
const id = nativeHistoryStorageId(query);
|
|
589
|
+
const saved = startupQueries.get(id);
|
|
590
|
+
startupQueries.delete(id);
|
|
591
|
+
return saved;
|
|
592
|
+
}
|
|
593
|
+
function applyStoredQuery(entry, saved) {
|
|
594
|
+
const query = entry.query;
|
|
595
|
+
if (!saved ||
|
|
596
|
+
!query.persisted ||
|
|
597
|
+
saved.bytes > query.maxBytes ||
|
|
598
|
+
(query.method === Methods.NATIVE_SESSION_READ &&
|
|
599
|
+
!storedNativeHistoryMatches(query.params, saved.data)))
|
|
600
|
+
return false;
|
|
601
|
+
const sourceObservation = query.sourceObservation?.(saved.data);
|
|
602
|
+
entry.state = {
|
|
603
|
+
status: 'stale',
|
|
604
|
+
data: saved.data,
|
|
605
|
+
observedAt: saved.observedAt,
|
|
606
|
+
...(sourceObservation ? { sourceObservation, error: sourceObservation.error } : {}),
|
|
607
|
+
};
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
460
610
|
function hydrateEntry(entry) {
|
|
461
611
|
const query = entry.query;
|
|
462
612
|
const hydrationSerial = ++entry.hydrationSerial;
|
|
@@ -470,7 +620,8 @@ export function createGatewayResourceCache(options) {
|
|
|
470
620
|
const readingStore = store;
|
|
471
621
|
try {
|
|
472
622
|
const saved = query.persisted
|
|
473
|
-
? ((
|
|
623
|
+
? (takeStartupQuery(query) ??
|
|
624
|
+
(isRunListView(query) ? await storedRunListView(query, readingStore) : undefined) ??
|
|
474
625
|
(await readingStore.getQuery(nativeHistoryStorageId(query))))
|
|
475
626
|
: undefined;
|
|
476
627
|
if (!disposed &&
|
|
@@ -480,17 +631,7 @@ export function createGatewayResourceCache(options) {
|
|
|
480
631
|
ownerVersion === hydrationOwnerVersion &&
|
|
481
632
|
entry.hydrationSerial === hydrationSerial &&
|
|
482
633
|
entry.state.status !== 'ready' &&
|
|
483
|
-
saved
|
|
484
|
-
saved.bytes <= query.maxBytes &&
|
|
485
|
-
(query.method !== Methods.NATIVE_SESSION_READ ||
|
|
486
|
-
storedNativeHistoryMatches(query.params, saved.data))) {
|
|
487
|
-
const sourceObservation = query.sourceObservation?.(saved.data);
|
|
488
|
-
entry.state = {
|
|
489
|
-
status: 'stale',
|
|
490
|
-
data: saved.data,
|
|
491
|
-
observedAt: saved.observedAt,
|
|
492
|
-
...(sourceObservation ? { sourceObservation, error: sourceObservation.error } : {}),
|
|
493
|
-
};
|
|
634
|
+
applyStoredQuery(entry, saved)) {
|
|
494
635
|
notify(entry);
|
|
495
636
|
}
|
|
496
637
|
}
|
|
@@ -521,6 +662,9 @@ export function createGatewayResourceCache(options) {
|
|
|
521
662
|
};
|
|
522
663
|
await runListLoad.promise;
|
|
523
664
|
}
|
|
665
|
+
return heldRunListView(query);
|
|
666
|
+
}
|
|
667
|
+
function heldRunListView(query) {
|
|
524
668
|
const data = runList.view(query.params);
|
|
525
669
|
return data && runList.observedAt !== undefined
|
|
526
670
|
? { id: query.id, data, observedAt: runList.observedAt, bytes: 0 }
|
|
@@ -548,7 +692,9 @@ export function createGatewayResourceCache(options) {
|
|
|
548
692
|
if (evictionCodes.has(queryError(error).code)) {
|
|
549
693
|
const evictingStore = store;
|
|
550
694
|
forgetRunList();
|
|
551
|
-
void updateStore(evictingStore, () => evictingStore === store && !evictingStore.deleted, () => evictingStore.deleteQuery(RUN_LIST_COLLECTION_ID)
|
|
695
|
+
void updateStore(evictingStore, () => evictingStore === store && !evictingStore.deleted, () => evictingStore.deleteQuery(RUN_LIST_COLLECTION_ID),
|
|
696
|
+
// Queue one-statement refusal evictions before their callers settle.
|
|
697
|
+
false);
|
|
552
698
|
}
|
|
553
699
|
throw error;
|
|
554
700
|
}
|
|
@@ -623,7 +769,11 @@ export function createGatewayResourceCache(options) {
|
|
|
623
769
|
const shared = entry.pending.prefetch;
|
|
624
770
|
entry.pending.prefetch =
|
|
625
771
|
shared && prefetch
|
|
626
|
-
? {
|
|
772
|
+
? {
|
|
773
|
+
origin: 'prefetch',
|
|
774
|
+
cohort: shared.cohort === 'gate' ? 'gate' : prefetch.cohort,
|
|
775
|
+
maxStoredBytes: Math.min(shared.maxStoredBytes ?? Infinity, prefetch.maxStoredBytes ?? Infinity),
|
|
776
|
+
}
|
|
627
777
|
: undefined;
|
|
628
778
|
return entry.pending.promise;
|
|
629
779
|
}
|
|
@@ -744,6 +894,7 @@ export function createGatewayResourceCache(options) {
|
|
|
744
894
|
const retention = entry.query.retention;
|
|
745
895
|
const saved = serialized !== undefined &&
|
|
746
896
|
bytes <= entry.query.maxBytes &&
|
|
897
|
+
(entry.listeners.size > 0 || bytes <= (pending.prefetch?.maxStoredBytes ?? Infinity)) &&
|
|
747
898
|
(!retention || bytes <= retention.maxBytes)
|
|
748
899
|
? {
|
|
749
900
|
id: nativeHistoryStorageId(entry.query),
|
|
@@ -816,7 +967,9 @@ export function createGatewayResourceCache(options) {
|
|
|
816
967
|
(retention.cohort === 'upstream'
|
|
817
968
|
? isUpstreamStoredQuery(value.id)
|
|
818
969
|
: value.id.startsWith(`${entry.query.method}:`)))
|
|
819
|
-
.sort((a, b) =>
|
|
970
|
+
.sort((a, b) => (entry.query.method === Methods.RUN_GET
|
|
971
|
+
? (a.accessedAt ?? a.observedAt) - (b.accessedAt ?? b.observedAt)
|
|
972
|
+
: a.observedAt - b.observedAt) ||
|
|
820
973
|
(entries.get(a.id)?.lastUsed ?? 0) - (entries.get(b.id)?.lastUsed ?? 0));
|
|
821
974
|
if (!current())
|
|
822
975
|
return;
|
|
@@ -836,6 +989,14 @@ export function createGatewayResourceCache(options) {
|
|
|
836
989
|
await writingStore.putQuery(row);
|
|
837
990
|
});
|
|
838
991
|
}
|
|
992
|
+
if (store.persistent && entry.query.method === Methods.RUN_LIST) {
|
|
993
|
+
const listed = reply.data;
|
|
994
|
+
if (Array.isArray(listed.runs)) {
|
|
995
|
+
for (const run of listed.runs)
|
|
996
|
+
fallbackDetailTargets.set(run.id, runDetailTarget(run, runListVersion(run)));
|
|
997
|
+
}
|
|
998
|
+
refreshDetailTargets();
|
|
999
|
+
}
|
|
839
1000
|
}
|
|
840
1001
|
catch (error) {
|
|
841
1002
|
if (disposed ||
|
|
@@ -866,7 +1027,9 @@ export function createGatewayResourceCache(options) {
|
|
|
866
1027
|
// Commit an accepted refusal before a listener queues its profile's close.
|
|
867
1028
|
const eviction = updateStore(evictingStore, () => evictingStore === store &&
|
|
868
1029
|
evictingIncarnation === storeVersion &&
|
|
869
|
-
!evictingStore.deleted, () => evictingStore.deleteQuery(nativeHistoryStorageId(entry.query))
|
|
1030
|
+
!evictingStore.deleted, () => evictingStore.deleteQuery(nativeHistoryStorageId(entry.query)),
|
|
1031
|
+
// Refusals must not wait for another tab's admission lock before enqueueing deletion.
|
|
1032
|
+
false);
|
|
870
1033
|
notify(entry);
|
|
871
1034
|
// An authorization refusal settles its callers in the reply's own task, so a prefetch
|
|
872
1035
|
// stops before a store callback can release another read. The eviction is queued.
|
|
@@ -962,14 +1125,25 @@ export function createGatewayResourceCache(options) {
|
|
|
962
1125
|
});
|
|
963
1126
|
return written;
|
|
964
1127
|
}
|
|
1128
|
+
function contentWriteToken() {
|
|
1129
|
+
return { connectionId: connection.id, ownerVersion: hydrationOwnerVersion, storeVersion };
|
|
1130
|
+
}
|
|
965
1131
|
/** Stores verified bytes within the caps and the shared budget. */
|
|
966
|
-
function storeContent(input) {
|
|
1132
|
+
async function storeContent(input) {
|
|
967
1133
|
const key = contentKey(input.key);
|
|
968
1134
|
const { data, origin, cohort } = input;
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1135
|
+
// Prefetch verifies between frames, so the write still answers to the owner it was asked under.
|
|
1136
|
+
const token = input.token ?? contentWriteToken();
|
|
1137
|
+
if (!resourceContentWithinLimits(key, data))
|
|
1138
|
+
return false;
|
|
1139
|
+
// Prefetch is background work; content a person opened is on their critical path and
|
|
1140
|
+
// verifies at once, as before.
|
|
1141
|
+
const verified = origin === 'prefetch'
|
|
1142
|
+
? await verifyResourceContentInSlices(key, data, input.untrackedFiles)
|
|
1143
|
+
: verifyResourceContent(key, data, input.untrackedFiles);
|
|
1144
|
+
if (!verified)
|
|
1145
|
+
return false;
|
|
1146
|
+
return writeContent(token, async (target, current) => {
|
|
973
1147
|
const accessedAt = Date.now();
|
|
974
1148
|
const content = await target.listContent();
|
|
975
1149
|
if (content.some((row) => row.kind === key.kind && row.id === key.id)) {
|
|
@@ -1019,6 +1193,13 @@ export function createGatewayResourceCache(options) {
|
|
|
1019
1193
|
return true;
|
|
1020
1194
|
});
|
|
1021
1195
|
}
|
|
1196
|
+
async function retryQuery(query, placement, signal) {
|
|
1197
|
+
if (disposed)
|
|
1198
|
+
throw new Error('Resource cache is disposed');
|
|
1199
|
+
const entry = entryFor(query);
|
|
1200
|
+
await fetchEntry(entry, false, placement, signal);
|
|
1201
|
+
return entry.state;
|
|
1202
|
+
}
|
|
1022
1203
|
return {
|
|
1023
1204
|
observedKey(match) {
|
|
1024
1205
|
for (const entry of entries.values())
|
|
@@ -1071,7 +1252,7 @@ export function createGatewayResourceCache(options) {
|
|
|
1071
1252
|
notify(entry);
|
|
1072
1253
|
});
|
|
1073
1254
|
}
|
|
1074
|
-
|
|
1255
|
+
deliver(entry, receive);
|
|
1075
1256
|
if (entry.state.status !== 'ready' || entry.dirty)
|
|
1076
1257
|
schedule(entry);
|
|
1077
1258
|
return () => {
|
|
@@ -1091,13 +1272,21 @@ export function createGatewayResourceCache(options) {
|
|
|
1091
1272
|
throw new Error('Resource cache is disposed');
|
|
1092
1273
|
return entryFor(query).state;
|
|
1093
1274
|
},
|
|
1094
|
-
async
|
|
1275
|
+
async readStored(query, refresh = true) {
|
|
1095
1276
|
if (disposed)
|
|
1096
1277
|
throw new Error('Resource cache is disposed');
|
|
1278
|
+
const ownerVersion = hydrationOwnerVersion;
|
|
1279
|
+
await barrier;
|
|
1097
1280
|
const entry = entryFor(query);
|
|
1098
|
-
await
|
|
1281
|
+
await entry.hydration;
|
|
1282
|
+
if (disposed || ownerVersion !== hydrationOwnerVersion)
|
|
1283
|
+
throw new Error('The offline store owner changed');
|
|
1284
|
+
// fetchEntry records network refusals in the query state and notifies its readers.
|
|
1285
|
+
if (refresh && entry.state.data !== undefined)
|
|
1286
|
+
void fetchEntry(entry);
|
|
1099
1287
|
return entry.state;
|
|
1100
1288
|
},
|
|
1289
|
+
retry: retryQuery,
|
|
1101
1290
|
async retryAfterPending(query) {
|
|
1102
1291
|
if (disposed)
|
|
1103
1292
|
throw new Error('Resource cache is disposed');
|
|
@@ -1112,20 +1301,22 @@ export function createGatewayResourceCache(options) {
|
|
|
1112
1301
|
readContent: (key) => readContentBytes((target) => target.getContent(contentKey(key)), false),
|
|
1113
1302
|
runListProjections: () => runList.projections(),
|
|
1114
1303
|
openContent: (key) => readContentBytes((target) => target.getContent(contentKey(key)), true),
|
|
1304
|
+
async forgetContent(key) {
|
|
1305
|
+
const target = store;
|
|
1306
|
+
const ownerVersion = hydrationOwnerVersion;
|
|
1307
|
+
await updateStore(target, () => target === store && !target.deleted && ownerVersion === hydrationOwnerVersion, () => target.deleteContent(contentKey(key)));
|
|
1308
|
+
},
|
|
1115
1309
|
readContentByAlias: (alias) => readContentBytes((target) => target.getContentByAlias(alias), false),
|
|
1116
1310
|
openContentByAlias: (alias) => readContentBytes((target) => target.getContentByAlias(alias), true),
|
|
1117
1311
|
storeContent,
|
|
1118
1312
|
retainContent,
|
|
1119
|
-
contentWriteToken
|
|
1120
|
-
connectionId: connection.id,
|
|
1121
|
-
ownerVersion: hydrationOwnerVersion,
|
|
1122
|
-
storeVersion,
|
|
1123
|
-
}),
|
|
1313
|
+
contentWriteToken,
|
|
1124
1314
|
async dispose(reason) {
|
|
1125
1315
|
if (disposed)
|
|
1126
1316
|
return;
|
|
1127
1317
|
const withdrawing = collectors.close();
|
|
1128
1318
|
disposed = true;
|
|
1319
|
+
detailSync.reset();
|
|
1129
1320
|
version++;
|
|
1130
1321
|
unsubscribeChanges();
|
|
1131
1322
|
unsubscribeConnection();
|