Skip to content

[RNE Rewrite] Integrate resource fetcher (react-native-blob-util) - #1328

Merged
msluszniak merged 12 commits into
rne-rewritefrom
@ms/resource-fetcher
Aug 6, 2026
Merged

[RNE Rewrite] Integrate resource fetcher (react-native-blob-util)#1328
msluszniak merged 12 commits into
rne-rewritefrom
@ms/resource-fetcher

Conversation

@msluszniak

@msluszniak msluszniak commented Jul 24, 2026

Copy link
Copy Markdown
Member

Description

Adds resource fetching to the rewrite as a single react-native-blob-util-backed fetcher inside react-native-executorch. Replaces the temporary react-native-fs hook.

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 an http(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:

const model = await download(models.classification.EFFICIENTNET_V2_S.XNNPACK_FP32);
const { classify, dispose } = await createClassifier(model);

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?

  • Yes
  • No

Type of change

  • Bug fix (change which fixes an issue)
  • New feature (change which adds functionality)
  • Documentation update (improves or adds clarity to existing documentation)
  • Other (chores, tests, code style improvements etc.)

Tested on

  • iOS
  • Android

Testing instructions

  1. Run the computer-vision example app.
  2. Open Classification, pick an XNNPACK model — it downloads with progress, then loads and runs.
  3. Reopen the screen — the model is served from cache instantly (no re-download).
  4. Open Speech-to-Text — model, tokenizer and the nested VAD model resolve in one pass, with progress weighted across all three.
  5. Imperative: createClassifier(await download(models.classification.EFFICIENTNET_V2_S.XNNPACK_FP32)).

Screenshots

Related issues

Closes #1253

Checklist

  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have updated the documentation accordingly
  • My changes generate no new warnings

Additional notes

  • Swaps the react-native-fs peer dependency for react-native-blob-util.
  • Android must use DownloadManager.
  • isEmulator is now installed on the __rnexecutorch_jsi__ module by the native layer.
  • File management (list/delete downloaded models) is intentionally out of scope; the fetcher only downloads and returns paths.

@msluszniak
msluszniak marked this pull request as draft July 24, 2026 16:12
@msluszniak msluszniak self-assigned this Jul 25, 2026
@msluszniak msluszniak added refactoring feature PRs that implement a new feature labels Jul 25, 2026
@msluszniak msluszniak linked an issue Jul 25, 2026 that may be closed by this pull request
@msluszniak
msluszniak marked this pull request as ready for review July 25, 2026 09:58
Comment thread packages/react-native-executorch/src/utils.ts
Comment thread packages/react-native-executorch/src/fetcher/telemetry.ts Outdated
Comment thread packages/react-native-executorch/src/fetcher/fetcher.ts
Comment thread packages/react-native-executorch/src/fetcher/ResourceFetcher.ts Outdated
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.
@msluszniak
msluszniak force-pushed the @ms/resource-fetcher branch from 6c9c66e to cc2b050 Compare August 4, 2026 07:29

@barhanc barhanc left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also delete the resource fetcher packages. It can be in a follow-up PR though as not to clutter this one.

Comment thread packages/react-native-executorch/src/fetcher/telemetry.ts Outdated
Comment thread packages/react-native-executorch/src/fetcher/telemetry.ts
Comment thread packages/react-native-executorch/src/fetcher/telemetry.ts Outdated
Comment thread packages/react-native-executorch/src/hooks/useClassifier.ts Outdated
Comment thread packages/react-native-executorch/src/fetcher/fetcher.ts Outdated
Comment thread packages/react-native-executorch/src/fetcher/fetcher.ts
Comment thread packages/react-native-executorch/src/fetcher/fetcher.ts
Comment thread packages/react-native-executorch/src/fetcher/fetcher.ts Outdated
Comment thread packages/react-native-executorch/cpp/core/utils.cpp Outdated
Comment thread packages/react-native-executorch/src/hooks/useResourceDownload.ts Outdated
…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.
@msluszniak

Copy link
Copy Markdown
Member Author

We should also delete the resource fetcher packages. It can be in a follow-up PR though as not to clutter this one.

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.

@barhanc

barhanc commented Aug 6, 2026

Copy link
Copy Markdown
Member

We should also delete the resource fetcher packages. It can be in a follow-up PR though as not to clutter this one.

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?

@msluszniak

Copy link
Copy Markdown
Member Author

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.
@msluszniak
msluszniak requested review from barhanc August 6, 2026 11:45
Comment thread packages/react-native-executorch/src/hooks/useTextToImage.ts
Comment thread packages/react-native-executorch/src/hooks/useModel.ts Outdated
Comment thread packages/react-native-executorch/src/hooks/useResourceDownload.ts
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.
@msluszniak
msluszniak merged commit ad46ff0 into rne-rewrite Aug 6, 2026
3 checks passed
@msluszniak
msluszniak deleted the @ms/resource-fetcher branch August 6, 2026 14:31
barhanc added a commit that referenced this pull request Aug 7, 2026
## 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature PRs that implement a new feature refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[RNE Rewrite] Integrate ResourceFetcher with refactor

2 participants