Skip to content

Commit 00fa5bb

Browse files
committed
refactor: add bare workflow, throw instead of returning null
1 parent ac7e089 commit 00fa5bb

File tree

12 files changed

+568
-907
lines changed

12 files changed

+568
-907
lines changed

packages/bare-resource-fetcher/src/ResourceFetcher.ts

Lines changed: 100 additions & 544 deletions
Large diffs are not rendered by default.

packages/bare-resource-fetcher/src/ResourceFetcherUtils.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,13 @@ import {
66
HTTP_CODE,
77
DownloadStatus,
88
SourceType,
9-
ResourceSourceExtended,
109
RnExecutorchError,
1110
RnExecutorchErrorCode,
1211
} from 'react-native-executorch';
1312
import { Image } from 'react-native';
1413
import * as RNFS from '@dr.pogodin/react-native-fs';
1514

1615
export { HTTP_CODE, DownloadStatus, SourceType };
17-
export type { ResourceSourceExtended };
1816

1917
/**
2018
* Utility functions for fetching and managing resources.
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import {
2+
createDownloadTask,
3+
completeHandler,
4+
DownloadTask,
5+
BeginHandlerParams,
6+
ProgressHandlerParams,
7+
} from '@kesha-antonov/react-native-background-downloader';
8+
import * as RNFS from '@dr.pogodin/react-native-fs';
9+
import { Image, Platform } from 'react-native';
10+
import {
11+
ResourceSource,
12+
RnExecutorchErrorCode,
13+
RnExecutorchError,
14+
} from 'react-native-executorch';
15+
import { RNEDirectory } from './constants/directories';
16+
import { ResourceFetcherUtils, DownloadStatus } from './ResourceFetcherUtils';
17+
18+
export interface ActiveDownload {
19+
status: DownloadStatus;
20+
uri: string;
21+
fileUri: string;
22+
cacheFileUri: string;
23+
// settle and reject are the resolve/reject of the Promise returned by handleRemote.
24+
// They are stored here so that cancel() and resume() in the fetcher class can
25+
// unblock the fetch() loop from outside the download flow.
26+
settle: (path: string) => void;
27+
reject: (error: unknown) => void;
28+
// iOS only: background downloader task, used for pause/resume/cancel
29+
task?: DownloadTask;
30+
// Android only: RNFS job ID, used for cancel via RNFS.stopDownload
31+
jobId?: number;
32+
}
33+
34+
export async function handleObject(source: object): Promise<string> {
35+
const jsonString = JSON.stringify(source);
36+
const digest = ResourceFetcherUtils.hashObject(jsonString);
37+
const path = `${RNEDirectory}${digest}.json`;
38+
39+
if (await ResourceFetcherUtils.checkFileExists(path)) {
40+
return ResourceFetcherUtils.removeFilePrefix(path);
41+
}
42+
43+
await ResourceFetcherUtils.createDirectoryIfNoExists();
44+
await RNFS.writeFile(path, jsonString, 'utf8');
45+
return ResourceFetcherUtils.removeFilePrefix(path);
46+
}
47+
48+
export function handleLocalFile(source: string): string {
49+
return ResourceFetcherUtils.removeFilePrefix(source);
50+
}
51+
52+
export async function handleAsset(
53+
source: number,
54+
progressCallback: (progress: number) => void,
55+
downloads: Map<ResourceSource, ActiveDownload>
56+
): Promise<string> {
57+
const assetSource = Image.resolveAssetSource(source);
58+
const uri = assetSource.uri;
59+
60+
if (uri.startsWith('http')) {
61+
// Dev mode: asset served from Metro dev server.
62+
// uri is the resolved HTTP URL; source is the original require() number the
63+
// user holds, so it must be used as the downloads map key for pause/cancel to work.
64+
return handleRemote(uri, source, progressCallback, downloads);
65+
}
66+
67+
// Release mode: asset bundled locally, copy to RNEDirectory
68+
const filename = ResourceFetcherUtils.getFilenameFromUri(uri);
69+
const fileUri = `${RNEDirectory}${filename}`;
70+
71+
if (await ResourceFetcherUtils.checkFileExists(fileUri)) {
72+
return ResourceFetcherUtils.removeFilePrefix(fileUri);
73+
}
74+
75+
await ResourceFetcherUtils.createDirectoryIfNoExists();
76+
if (uri.startsWith('file')) {
77+
await RNFS.copyFile(uri, fileUri);
78+
}
79+
return ResourceFetcherUtils.removeFilePrefix(fileUri);
80+
}
81+
82+
// uri and source are separate parameters because for asset sources (dev mode),
83+
// source is the require() number the user holds (used as the downloads map key),
84+
// while uri is the resolved HTTP URL needed for the actual download.
85+
// For plain remote strings they are the same value.
86+
export async function handleRemote(
87+
uri: string,
88+
source: ResourceSource,
89+
progressCallback: (progress: number) => void,
90+
downloads: Map<ResourceSource, ActiveDownload>
91+
): Promise<string> {
92+
if (downloads.has(source)) {
93+
throw new RnExecutorchError(
94+
RnExecutorchErrorCode.ResourceFetcherDownloadInProgress,
95+
'Already downloading this file'
96+
);
97+
}
98+
99+
const filename = ResourceFetcherUtils.getFilenameFromUri(uri);
100+
const fileUri = `${RNEDirectory}${filename}`;
101+
const cacheFileUri = `${RNFS.CachesDirectoryPath}/${filename}`;
102+
103+
if (await ResourceFetcherUtils.checkFileExists(fileUri)) {
104+
return ResourceFetcherUtils.removeFilePrefix(fileUri);
105+
}
106+
107+
await ResourceFetcherUtils.createDirectoryIfNoExists();
108+
109+
// We need a Promise whose resolution can be triggered from outside this function —
110+
// by cancel() or resume() in the fetcher class. A plain async function can't do that,
111+
// so we create the Promise manually and store settle/reject in the downloads map.
112+
let settle: (path: string) => void = () => {};
113+
let reject: (error: unknown) => void = () => {};
114+
const promise = new Promise<string>((res, rej) => {
115+
settle = res;
116+
reject = rej;
117+
});
118+
119+
if (Platform.OS === 'android') {
120+
const rnfsDownload = RNFS.downloadFile({
121+
fromUrl: uri,
122+
toFile: cacheFileUri,
123+
progress: (res: { bytesWritten: number; contentLength: number }) => {
124+
if (res.contentLength > 0) {
125+
progressCallback(res.bytesWritten / res.contentLength);
126+
}
127+
},
128+
progressInterval: 500,
129+
});
130+
131+
downloads.set(source, {
132+
status: DownloadStatus.ONGOING,
133+
uri,
134+
fileUri,
135+
cacheFileUri,
136+
settle,
137+
reject,
138+
jobId: rnfsDownload.jobId,
139+
});
140+
141+
rnfsDownload.promise
142+
.then(async (result: { statusCode: number }) => {
143+
if (!downloads.has(source)) return; // canceled externally via cancel()
144+
145+
if (result.statusCode < 200 || result.statusCode >= 300) {
146+
downloads.delete(source);
147+
reject(
148+
new RnExecutorchError(
149+
RnExecutorchErrorCode.ResourceFetcherDownloadFailed,
150+
`Failed to fetch resource from '${uri}', status: ${result.statusCode}`
151+
)
152+
);
153+
return;
154+
}
155+
156+
try {
157+
await RNFS.moveFile(cacheFileUri, fileUri);
158+
} catch (error) {
159+
downloads.delete(source);
160+
reject(error);
161+
return;
162+
}
163+
164+
downloads.delete(source);
165+
ResourceFetcherUtils.triggerHuggingFaceDownloadCounter(uri);
166+
settle(ResourceFetcherUtils.removeFilePrefix(fileUri));
167+
})
168+
.catch((error: unknown) => {
169+
if (!downloads.has(source)) return; // canceled externally
170+
downloads.delete(source);
171+
reject(
172+
new RnExecutorchError(
173+
RnExecutorchErrorCode.ResourceFetcherDownloadFailed,
174+
`Failed to fetch resource from '${uri}', context: ${error}`
175+
)
176+
);
177+
});
178+
} else {
179+
const task = createDownloadTask({
180+
id: filename,
181+
url: uri,
182+
destination: cacheFileUri,
183+
})
184+
.begin((_: BeginHandlerParams) => progressCallback(0))
185+
.progress((progress: ProgressHandlerParams) => {
186+
progressCallback(progress.bytesDownloaded / progress.bytesTotal);
187+
})
188+
.done(async () => {
189+
const dl = downloads.get(source);
190+
// If paused or canceled, settle/reject will be called externally — do nothing here.
191+
if (!dl || dl.status === DownloadStatus.PAUSED) return;
192+
193+
try {
194+
await RNFS.moveFile(cacheFileUri, fileUri);
195+
// Required by the background downloader library to signal iOS that the
196+
// background download session is complete.
197+
const fn = fileUri.split('/').pop();
198+
if (fn) await completeHandler(fn);
199+
} catch (error) {
200+
downloads.delete(source);
201+
reject(error);
202+
return;
203+
}
204+
205+
downloads.delete(source);
206+
ResourceFetcherUtils.triggerHuggingFaceDownloadCounter(uri);
207+
settle(ResourceFetcherUtils.removeFilePrefix(fileUri));
208+
})
209+
.error((error: any) => {
210+
if (!downloads.has(source)) return; // canceled externally
211+
downloads.delete(source);
212+
reject(
213+
new RnExecutorchError(
214+
RnExecutorchErrorCode.ResourceFetcherDownloadFailed,
215+
`Failed to fetch resource from '${uri}', context: ${error}`
216+
)
217+
);
218+
});
219+
220+
task.start();
221+
222+
downloads.set(source, {
223+
status: DownloadStatus.ONGOING,
224+
uri,
225+
fileUri,
226+
cacheFileUri,
227+
settle,
228+
reject,
229+
task,
230+
});
231+
}
232+
233+
return promise;
234+
}

packages/expo-resource-fetcher/src/ResourceFetcher.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,6 @@ import {
5454
HTTP_CODE,
5555
DownloadStatus,
5656
SourceType,
57-
ResourceSourceExtended,
58-
DownloadResource,
5957
} from './ResourceFetcherUtils';
6058

6159
interface ExpoResourceFetcherInterface extends ResourceFetcherAdapter {

packages/expo-resource-fetcher/src/ResourceFetcherUtils.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
HTTP_CODE,
77
DownloadStatus,
88
SourceType,
9-
ResourceSourceExtended,
109
RnExecutorchError,
1110
RnExecutorchErrorCode,
1211
} from 'react-native-executorch';
@@ -15,19 +14,9 @@ import { Asset } from 'expo-asset';
1514
/**
1615
* @internal
1716
*/
18-
import {
19-
getInfoAsync,
20-
makeDirectoryAsync,
21-
type DownloadResumable,
22-
} from 'expo-file-system/legacy';
23-
24-
export { HTTP_CODE, DownloadStatus, SourceType, ResourceSourceExtended };
17+
import { getInfoAsync, makeDirectoryAsync } from 'expo-file-system/legacy';
2518

26-
export interface DownloadResource {
27-
downloadResumable: DownloadResumable;
28-
status: DownloadStatus;
29-
extendedInfo: ResourceSourceExtended;
30-
}
19+
export { HTTP_CODE, DownloadStatus, SourceType };
3120

3221
/**
3322
* Utility functions for fetching and managing resources.

0 commit comments

Comments
 (0)