Skip to content

Commit 3d8a0de

Browse files
authored
feat(utils): expose resource cache path utility (#1592)
## Description Expose `getResourceCachePath(source)` from the fetcher through `utils.ts`, allowing apps to locate cached resources without duplicating cache naming logic. The helper performs no downloads or existence checks and preserves repository aliases and platform-specific paths. Adds regression tests for cache identity, query parameters, local paths, and agreement with the downloader. ### Introduces a breaking change? - [ ] Yes - [x] No ### Type of change - [ ] Bug fix (change which fixes an issue) - [x] New feature (change which adds functionality) - [ ] Documentation update (improves or adds clarity to existing documentation) - [ ] Other (chores, tests, code style improvements etc.) ### Tested on Tested in local dev branch of the gallery app. - [x] iOS - [x] Android ### Testing instructions - [ ] Tests pass ### Screenshots <!-- Add screenshots here, if applicable --> ### Related issues <!-- Link related issues here using #issue-number --> ### Checklist - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have updated the documentation accordingly - [x] My changes generate no new warnings ### Additional notes <!-- Include any additional information, assumptions, or context that reviewers might need to understand this PR. -->
1 parent d76ceaf commit 3d8a0de

4 files changed

Lines changed: 74 additions & 3 deletions

File tree

‎packages/react-native-executorch/__tests__/api/__snapshots__/apiSurface.test.ts.snap‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ exports[`public API surface matches the recorded export list 1`] = `
164164
"defaultWorkletRuntime",
165165
"download",
166166
"getRegisteredBackends",
167+
"getResourceCachePath",
167168
"inspectModel",
168169
"isRnExecuTorchError",
169170
"llm",
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Platform } from 'react-native';
2+
import RNBlobUtil from 'react-native-blob-util';
3+
import { getResourceCachePath } from '../../src/utils';
4+
import { download } from '../../src/fetcher/fetcher';
5+
import { setTelemetryEnabled } from '../../src/fetcher/telemetry';
6+
import { RENAMED_HF_REPOS } from '../../src/fetcher/renamedRepos';
7+
import { fakeFs, fakeNet } from '../support/blobUtilMock';
8+
9+
const org = 'https://huggingface.co/software-mansion/';
10+
11+
describe('getResourceCachePath', () => {
12+
it.each(Object.entries(RENAMED_HF_REPOS))('preserves cache identity for %s', (name, alias) => {
13+
const path = '/resolve/v1.0.0/model.pte';
14+
expect(getResourceCachePath(org + name + path)).toBe(getResourceCachePath(org + alias + path));
15+
});
16+
17+
it('ignores query parameters and keeps different resource URLs distinct', () => {
18+
const source = 'https://example.com/models/one/model.pte';
19+
expect(getResourceCachePath(source + '?download=true')).toBe(getResourceCachePath(source));
20+
expect(getResourceCachePath(source)).not.toBe(
21+
getResourceCachePath(source.replace('/one/', '/two/'))
22+
);
23+
});
24+
25+
it('uses the platform persistent directory', () => {
26+
const directory =
27+
Platform.OS === 'android'
28+
? RNBlobUtil.fs.dirs.SDCardDir || RNBlobUtil.fs.dirs.DocumentDir
29+
: RNBlobUtil.fs.dirs.DocumentDir;
30+
expect(getResourceCachePath('https://example.com/model.pte')).toMatch(
31+
directory + '/react-native-executorch/'
32+
);
33+
});
34+
35+
it.each(['file:///local/model.pte', '/local/model.pte'])(
36+
'passes through %s unchanged, matching the downloader without filesystem or network work',
37+
async (source) => {
38+
expect(getResourceCachePath(source)).toBe(source);
39+
expect(await download(source)).toBe(getResourceCachePath(source));
40+
expect(fakeFs.paths()).toHaveLength(0);
41+
expect(fakeNet.requests()).toHaveLength(0);
42+
}
43+
);
44+
45+
it('returns the same path as the downloader', async () => {
46+
const source = 'https://example.com/model.pte';
47+
fakeNet.serve(source, { body: 'model-bytes' });
48+
setTelemetryEnabled(false);
49+
try {
50+
const path = await download(source);
51+
expect(path).toBe(getResourceCachePath(source));
52+
expect(fakeFs.readText(path)).toBe('model-bytes');
53+
} finally {
54+
setTelemetryEnabled(true);
55+
}
56+
});
57+
});

‎packages/react-native-executorch/src/fetcher/fetcher.ts‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,18 @@ const djb2 = (s: string): number => {
5757

5858
const isRemote = (source: string): boolean => /^https?:\/\//.test(source);
5959

60-
function cachePathFor(url: string): string {
60+
/**
61+
* Returns the persistent resource path used by {@link download} without downloading
62+
* or checking whether it exists. Repository aliases and query parameters use the
63+
* same cache identity as downloads. Local paths are returned unchanged, including
64+
* any `file://` prefix, matching {@link download}.
65+
* @category Utils / Functions
66+
* @param source The remote resource URL or local file path.
67+
* @returns The persistent cache path or the supplied local path.
68+
*/
69+
export function getResourceCachePath(source: string): string {
70+
if (!isRemote(source)) return source;
71+
const url = source;
6172
const urlWithoutQuery = url.split('?')[0]!;
6273
const basename = urlWithoutQuery.split('/').pop() || 'model';
6374
// Hash guards against basename collisions across different repos/versions.
@@ -224,7 +235,7 @@ const inFlight = new Map<string, InFlightDownload>();
224235
async function downloadUrl(url: string, cb: DownloadUrlCallbacks): Promise<string> {
225236
if (cb.signal?.aborted) throw abortError();
226237

227-
const dest = cachePathFor(url);
238+
const dest = getResourceCachePath(url);
228239

229240
if (cb.forceDownload) {
230241
// Drop the cached copy so the checks below fall through to a real fetch.
@@ -800,7 +811,7 @@ export async function download<T>(source: T, options: DownloadOptions = {}): Pro
800811
const measured = await Promise.all(
801812
urls.map(async (url) => {
802813
if (!options.forceDownload) {
803-
const cached = await fileSize(cachePathFor(url));
814+
const cached = await fileSize(getResourceCachePath(url));
804815
if (cached > 0) return { size: cached, pending: undefined, cached: true };
805816
}
806817
return { size: 0, pending: remoteSize(url, options.signal), cached: false };

‎packages/react-native-executorch/src/utils.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { loadModel } from './core/model';
88
import type { ModelSpec, ConcreteDim } from './core/schema';
99
import RNBlobUtil from 'react-native-blob-util';
1010

11+
export { getResourceCachePath } from './fetcher/fetcher';
12+
1113
/**
1214
* Options accepted by {@link useResourceDownload} and by every `use<Task>` hook
1315
* built on top of it.

0 commit comments

Comments
 (0)