Skip to content

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

Open
msluszniak wants to merge 5 commits into
rne-rewritefrom
@ms/resource-fetcher
Open

[RNE Rewrite] Integrate resource fetcher (react-native-blob-util)#1328
msluszniak wants to merge 5 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 (no separate expo/bare packages). Public download(source | source[], { onProgress, signal }) returns local path(s); local paths pass through untouched. Every create<Task> factory now resolves remote model URLs internally, so create<Task>(models.X) works directly and the use<Task> hooks (which pre-download for progress) hit an instant no-op. HF download-counter + download-event telemetry are bundled and fired once per genuine fetch. Replaces the temporary react-native-fs hook.

Backend is split by platform: Android uses the system DownloadManager (reliable for files >2 GB, continues in the background, OS-managed resume), 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. Imperative: createClassifier(models.classification.EFFICIENTNET_V2_S.XNNPACK_FP32) resolves the URL to a cached path internally.

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

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
…tories

Every create<Task> factory now runs its model/tokenizer path fields through
the fetcher's download() before loading, so remote URLs are transparently
downloaded and cached. Because download() passes local paths through
untouched, the use<Task> hooks (which still pre-download for progress) hit an
instant no-op — no double download, single telemetry event.

- Wrap each loadModel/loadTokenizer arg with download() across all 12 factories
  (single modelPath, plus tokenizerPath and nested vadModel for the multi-file
  tasks). Whisper delegates VAD resolution to the FSMN factory.
- Add a tuple-preserving download() overload so download([a, b]) resolves to
  [string, string] under noUncheckedIndexedAccess.
- Imperative create<Task>(models.X) now works without an explicit download().
…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.
@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
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.
Comment on lines +37 to +38
// Throwaway download to a temp path — inspection shouldn't populate the
// persistent resource cache, so we don't go through `download()`.

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.

Since this will now become part of the standard API we should add an explanation of this throwaway behavior to the jsdoc.

// Set by the native layer when running on a simulator/emulator, so dev traffic
// can be filtered out server-side. Absent (⇒ false) until then.
function isEmulator(): boolean {
return (globalThis as { __rne_isEmulator?: boolean }).__rne_isEmulator === true;

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.

How does this work? Where is the property __rne_isEmulator set?

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.

Let's rename this file to fetcher.ts so that all files have consistent naming.

sources: T,
options?: DownloadOptions
): Promise<{ [K in keyof T]: string }>;
export async function download(

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.

I'm not sure that the pattern of adding download(modelPath) to every task implementation is the best way to achieve that. I think we should separate the downloading from creating the pipeline, so imo the pipelines should stay as they were and the pattern should rather be something like

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

where download would work in the following way: it takes an arbitrary nested object and for each string leaf attribute it checks if it's a valid url and if so downloads it and substitutes the url for the local path. This way we wouldn't need two different mechanisms for downloading in create<Task> and use<Task> (where the path substitution is left as it was) and we wouldn't have to actually write the path substitution by hand in each hook. The only problem I see is that I'm not sure how difficult would it be to implement something like this in a way that download also returns a proper type but is generic at the same time (so it works for all model types out of the box). My gut feeling is that it should be possible as TS type system is quite powerful but I don't have a concrete idea right now.

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