[RNE Rewrite] Integrate resource fetcher (react-native-blob-util) - #1328
Conversation
Replace the temporary react-native-fs download hook with a single
blob-util-backed fetcher living in react-native-executorch (no separate
expo/bare packages).
- src/fetcher: imperative `download(source | source[], { onProgress, signal })`
returning local path(s); persistent DocumentDir cache keyed by URL hash.
- HTTP-Range auto-resume of interrupted downloads via a .partial file, with a
safe fallback to a fresh full download if partial assembly fails.
- Byte-weighted unified progress across multiple files; AbortSignal cancel that
preserves bytes for later resume.
- Bundled download telemetry (HF download counter + anonymous download event),
fired once per genuine, non-cached fetch.
- Rewire useResourceDownload + inspectModel onto the new fetcher; swap the
react-native-fs dependency for react-native-blob-util in the lib and example
apps.
Closes #1253
…large downloads On-device testing revealed react-native-blob-util's in-process streaming download is broken on modern Android (a 0.24.10 regression, upstream #475): it aborts after 8 KB with "Download interrupted", so every model download failed. curl and the system DownloadManager fetch the same URLs fine. Split the fetcher backend by platform: - Android: route through blob-util's system DownloadManager. It reliably handles files >2 GB (the whole point — LLM .pte files exceed the OkHttp 2 GB in-process limit), continues in the background / across app kill, and resumes transient network drops itself. Downloads stage in the app-private external files dir so DownloadManager can write there and the move into the cache stays on one volume (no multi-GB cross-filesystem copy). - iOS: keep the NSURLSession streaming path with .partial/HTTP-Range resume (unaffected by the Android regression, no 2 GB limit). Verified on a physical Android device: byte-weighted progress, cache-hit short-circuit, and create<Task>(models.X) URL resolution all pass end to end.
Analytics stay enabled by default; setTelemetryEnabled(false) opts out of the anonymous download-event POST to Software Mansion. The Hugging Face download counter is unaffected and always fires.
…e in a config
Downloading is now fully separated from pipeline creation: `create<Task>`
factories stay untouched and `download()` becomes a single generic entry point.
`download(source)` accepts any nested structure of plain objects and arrays,
downloads every string leaf that is an http(s) URL, and returns the value with
those URLs replaced by local paths. Non-URL leaves (local paths, labels,
thresholds) pass through untouched, so the result is structurally identical to
the input — `download<T>(source: T): Promise<T>` types correctly for every task
config without per-task overloads:
const model = await download(models.classification.EFFICIENTNET_V2_S.XNNPACK_FP32);
const { classify, dispose } = await createClassifier(model);
URLs are deduplicated so a file referenced twice is fetched once, and branches
with nothing to substitute keep their original reference.
`useResourceDownload` takes the whole config too and returns the resolved one,
so the hooks no longer hand-write path substitution. Whisper resolves its
model, tokenizer and nested VAD model in one pass (three hook calls before) and
now reports progress weighted across all three rather than the model alone.
Also renames `fetcher/ResourceFetcher.ts` to `fetcher/fetcher.ts` for
consistency with the surrounding file naming.
inspectModel deliberately bypasses `download()` so inspection doesn't populate the persistent resource cache. That was only an inline comment; now that the utility is part of the public API, document the consequence in its JSDoc — inspecting a remote model re-downloads it on every call and leaves nothing behind.
Download analytics read `globalThis.__rne_isEmulator`, a global the old architecture set from its native installer. The rewrite's installer never set it, so the flag was silently always false and emulator traffic was indistinguishable from real devices. Install the value on the `__rnexecutorch_jsi__` module object instead of adding a second bare global, and read it from there. Detection is ported from the old installer: Android reads `ro.build.fingerprint` / `ro.hardware` (goldfish and ranchu are the QEMU emulator kernels), Apple platforms use TARGET_OS_SIMULATOR.
6c9c66e to
cc2b050
Compare
barhanc
left a comment
There was a problem hiding this comment.
We should also delete the resource fetcher packages. It can be in a follow-up PR though as not to clutter this one.
…leanups Concurrent `download()` calls for the same URL each missed the cache check, double-counted the fetch in telemetry, and wrote the same temporary file — on Android the second call's opening `unlink` deleted the first one's partially downloaded data. Downloads are now shared through an in-flight registry: one request per URL, progress fanned out to every joined caller, and the underlying request cancelled only once the last caller has aborted. Review cleanups alongside it: - Add a `forceDownload` option to re-fetch an already cached resource. - Return the full resolved resource from the `use<Task>` hooks instead of just `localPath`, so callers can manage every downloaded file. - Read `rnexecutorchJsi.isEmulator` directly instead of wrapping it. - Widen Android emulator detection: Cuttlefish (`cutf`/`vsoc` hardware) and `sdk_gphone` / `google_sdk` / `Emulator` product models were all missed. - Report swallowed telemetry failures via `console.warn` under `__DEV__` instead of silently discarding them. - Rename `downloadOne` to `downloadUrl` and the platform backends to `downloadUrlViaAndroidDownloadManager` / `downloadUrlViaIosStream`. - Import telemetry as a namespace, drop the single-use `DownloadProgressCallback` type, and build `collectRemoteSources`' accumulator in an inner closure rather than a default parameter.
What do you mean by deleting fetcher packages? We cannot delete them as such, because we need them for versions <= 0.9.x. I guess the first moment when we could remove them will be release of version 2.0.0. In the current implementation we don't ship any resource fetchers. |
I meant deleting the directories in the repo, not the npm packages. Do we need to keep the directories in repo? |
Oh, I forgot we still have them. Sure, we need to delete them, they are not needed anymore. |
…an AbortError class Cancellation now throws an `AbortError` subclass instead of a plain `Error` with a patched `name`, so internal call sites match on `instanceof` rather than a string. It stays internal — not re-exported from `fetcher/index.ts` — and still sets `name = 'AbortError'`, so consumers keep the standard `AbortSignal` contract and are unaffected by however the wider RNE error hierarchy lands.
…t URL set Keying `useResourceDownload` on the set of remote URLs meant an opts-only config change never reached the pipeline: `create<Task>` bakes `modelOpts` in at construction time, so a new `labels` array or threshold was silently ignored while the hook kept returning `labels` read fresh from the raw config. The two could disagree. `JSON.stringify(config)` keys on the value instead, so any genuine change rebuilds the pipeline while an inline config object still doesn't re-run the effect every render. Re-resolving is cheap for files that are already cached. Because the hook now passes the whole config to `download()` and uses its return value directly, `collectRemoteSources` and `substituteRemoteSources` go back to being internal, and `useModel` derives its own dependency from the config reference instead of taking a `deps` array.
…ctually has
`locale.split('-').pop()` returns the language subtag when the locale carries no
region, and the two-character check waved it through. Bare locales were filed
under whichever country shares their language code: 'de' became DE, 'uk' became
UK (drawn as the United Kingdom on most maps), 'sv' became SV (El Salvador) and
'ar' became AR (Argentina). Those look like ordinary rows, so the error is
invisible downstream.
Walk the subtags after the language instead and accept only a real BCP 47
region, falling back to UNKNOWN. This also fixes locales carrying a Unicode
extension: 'de-DE-u-ca-gregory' previously resolved to UNKNOWN because the last
subtag was 'gregory', and now correctly resolves to DE.
The endpoint payload is unchanged, since releases up to 0.9.x keep sending the
current shape regardless.
Three review follow-ups: - `useTextToImage` was the one hook not returning `resource`. - `useModel` keys on `JSON.stringify(config)` rather than the config reference. It is exported, so a caller passing an inline config directly (rather than the stable `resource` from `useResourceDownload`) would otherwise rebuild the model on every render. - `forceDownload` is reachable from the hooks. `useResourceDownload` now takes the same `options` object the `use<Task>` hooks already accept, so the shared `ResourceOptions` type carries both `preventLoad` and `forceDownload` and the task hooks forward `options` straight through.
## Description Add supertonic TTS pipeline and example app screen to test it. ### 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 - [x] iOS - [x] Android ### Testing instructions - [ ] Run the Speech example app on both iOS and Android and test the newly added TTS functionality. ### Screenshots <!-- Add screenshots here, if applicable --> ### Related issues Part of #1250 ### Checklist - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have updated the documentation accordingly - [x] My changes generate no new warnings ### Additional notes Should wait for #1328 so that the hook implementation can be refactored to a more concise and elegant one.
Description
Adds resource fetching to the rewrite as a single
react-native-blob-util-backed fetcher insidereact-native-executorch. Replaces the temporaryreact-native-fshook.download(source, { onProgress, signal })is a single generic entry point: it takes any nested structure of plain objects and arrays, downloads every string leaf that is anhttp(s)URL, and returns the value with those URLs replaced by local paths. Everything else passes through untouched, so the result is structurally identical to the input.download<T>(source: T): Promise<T>types correctly for every task config without per-task overloads:Backend is split by platform: Android uses the system DownloadManager (reliable for files >2 GB, background), iOS streams via NSURLSession with
.partial/HTTP-Range resume.Introduces a breaking change?
Type of change
Tested on
Testing instructions
computer-visionexample app.createClassifier(await download(models.classification.EFFICIENTNET_V2_S.XNNPACK_FP32)).Screenshots
Related issues
Closes #1253
Checklist
Additional notes
react-native-fspeer dependency forreact-native-blob-util.isEmulatoris now installed on the__rnexecutorch_jsi__module by the native layer.