Skip to content

Commit dad7bd6

Browse files
jlobue10claude
andcommitted
Expose the cache's persisted state per URL to the renderer
CacheManager records the outcome of every download in a `-info` sidecar next to the cached file, but the renderer could only reach that state by asking for the content itself, which starts a download for anything not yet cached. A new read-only `getCacheInfos(urls)` IPC returns the persisted CacheInfo for a batch of URLs — CACHED with its checksum, the persisted ERROR, or NOT_CACHED for a URL never requested — without ever fetching. A URL the cache cannot key at all is reported as an ERROR entry for that URL instead of failing the whole batch. This lets the renderer classify NFTs it has not rendered from what earlier visits and sessions already learned about their files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YKEGodEgVdEuvUM8dza52q
1 parent 265258c commit dad7bd6

5 files changed

Lines changed: 84 additions & 0 deletions

File tree

packages/gui/src/@types/CacheService.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type CacheInfo from './CacheInfo';
2+
13
type CacheRequestOptions = {
24
maxSize?: number;
35
timeout?: number;
@@ -22,6 +24,8 @@ type CacheService = {
2224
getChecksum: (url: string, options?: CacheRequestOptions) => Promise<string>;
2325
getURI: (url: string, options?: CacheRequestOptions) => Promise<string>;
2426
invalidate: (url: string) => Promise<void>;
27+
// Read-only lookup of the persisted cache state of each url — never downloads
28+
getCacheInfos: (urls: string[]) => Promise<CacheInfo[]>;
2529

2630
// Event subscriptions
2731
subscribeToDirectoryChange: (callback: (newDirectory: string) => void) => () => void;

packages/gui/src/electron/CacheManager.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,3 +219,57 @@ describe('CacheManager eviction', () => {
219219
expect(mockDownloadFile).toHaveBeenCalledTimes(1);
220220
});
221221
});
222+
223+
describe('CacheManager getCacheInfos', () => {
224+
let cacheDirectory: string;
225+
226+
beforeEach(async () => {
227+
mockDownloadFile.mockReset();
228+
cacheDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'chia-cache-manager-'));
229+
});
230+
231+
afterEach(async () => {
232+
await fs.rm(cacheDirectory, { recursive: true, force: true });
233+
});
234+
235+
it('reports persisted outcomes per url without downloading anything', async () => {
236+
const payload = Buffer.from('cached payload');
237+
mockDownloadFile.mockImplementation(async (url, localPath) => {
238+
if (url === 'https://example.com/broken.png') {
239+
throw new Error('getaddrinfo ENOTFOUND example.com');
240+
}
241+
await fs.writeFile(localPath, payload);
242+
return {
243+
'content-type': 'image/png',
244+
};
245+
});
246+
247+
const cacheManager = new CacheManager({
248+
cacheDirectory,
249+
maxCacheSize: 1024,
250+
});
251+
await cacheManager.init();
252+
253+
await expect(cacheManager.getContent('https://example.com/ok.png')).resolves.toEqual(payload);
254+
await expect(cacheManager.getContent('https://example.com/broken.png')).rejects.toThrow('ENOTFOUND');
255+
mockDownloadFile.mockClear();
256+
257+
const infos = await cacheManager.getCacheInfos([
258+
'https://example.com/ok.png',
259+
'https://example.com/broken.png',
260+
'https://example.com/never-requested.png',
261+
'not a url',
262+
]);
263+
264+
expect(infos.map((info) => [info.url, info.state])).toEqual([
265+
['https://example.com/ok.png', 'CACHED'],
266+
['https://example.com/broken.png', 'ERROR'],
267+
['https://example.com/never-requested.png', 'NOT_CACHED'],
268+
['not a url', 'ERROR'],
269+
]);
270+
expect(infos[0]).toMatchObject({ checksum: expect.any(String) });
271+
expect(infos[1]).toMatchObject({ error: 'getaddrinfo ENOTFOUND example.com' });
272+
expect(infos[3]).toMatchObject({ error: 'Invalid URL: not a url' });
273+
expect(mockDownloadFile).not.toHaveBeenCalled();
274+
});
275+
});

packages/gui/src/electron/CacheManager.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ export default class CacheManager extends EventEmitter {
235235
this.getURI(url, options),
236236
);
237237
ipcMainHandle(CacheAPI.INVALIDATE, (url: string) => this.invalidate(url));
238+
ipcMainHandle(CacheAPI.GET_CACHE_INFOS, (urls: string[]) => this.getCacheInfos(urls));
238239

239240
ipcMainHandle(CacheAPI.GET_CACHE_DIRECTORY, () => this.cacheDirectory);
240241
ipcMainHandle(CacheAPI.GET_MAX_CACHE_SIZE, () => this.maxCacheSize);
@@ -642,6 +643,29 @@ export default class CacheManager extends EventEmitter {
642643
throw new Error('Unknown cache state');
643644
}
644645

646+
// Reports what the cache already knows about each url without fetching
647+
// anything: a download that never happened stays NOT_CACHED, and a url the
648+
// cache cannot key at all is reported as an error instead of failing the
649+
// whole batch. This lets the renderer classify NFTs that are not on screen
650+
// (and so never verify their files) from outcomes persisted by earlier
651+
// visits and sessions.
652+
async getCacheInfos(urls: string[]): Promise<CacheInfo[]> {
653+
return Promise.all(
654+
urls.map(async (url) => {
655+
try {
656+
return await this.getCacheInfoByURL(url);
657+
} catch (error) {
658+
return {
659+
url,
660+
state: CacheState.ERROR,
661+
error: (error as Error).message,
662+
timestamp: Date.now(),
663+
};
664+
}
665+
}),
666+
);
667+
}
668+
645669
async clearCache() {
646670
// cancel all ongoing requests
647671
for (const ongoingRequest of this.ongoingRequests.values()) {

packages/gui/src/electron/constants/CacheAPI.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ enum CacheAPI {
1818
GET_HEADERS = `${API.CACHE}:getHeaders`,
1919
GET_CHECKSUM = `${API.CACHE}:getChecksum`,
2020
GET_URI = `${API.CACHE}:getUri`,
21+
GET_CACHE_INFOS = `${API.CACHE}:getCacheInfos`,
2122
INVALIDATE = `${API.CACHE}:invalidate`,
2223

2324
// Event subscriptions

packages/gui/src/electron/preload.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ contextBridge.exposeInMainWorld(API.CACHE, {
132132
getURI: (url: string, options?: { maxSize?: number; timeout?: number }) =>
133133
invokeWithCustomErrors(CacheAPI.GET_URI, url, options),
134134
invalidate: (url: string) => invokeWithCustomErrors(CacheAPI.INVALIDATE, url),
135+
getCacheInfos: (urls: string[]) => invokeWithCustomErrors(CacheAPI.GET_CACHE_INFOS, urls),
135136
subscribeToDirectoryChange: (callback: (...args: unknown[]) => void) =>
136137
onIpcEvent(CacheAPI.ON_CACHE_DIRECTORY_CHANGED, callback),
137138
subscribeToMaxSizeChange: (callback: (...args: unknown[]) => void) =>

0 commit comments

Comments
 (0)