react-native-nitro-fetch 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/LICENSE +20 -0
  2. package/NitroFetch.podspec +30 -0
  3. package/README.md +134 -0
  4. package/android/CMakeLists.txt +70 -0
  5. package/android/build.gradle +131 -0
  6. package/android/gradle.properties +5 -0
  7. package/android/src/main/AndroidManifest.xml +2 -0
  8. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  9. package/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt +91 -0
  10. package/android/src/main/java/com/margelo/nitro/nitrofetch/FetchCache.kt +48 -0
  11. package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetch.kt +94 -0
  12. package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt +256 -0
  13. package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchPackage.kt +22 -0
  14. package/ios/FetchCache.swift +56 -0
  15. package/ios/NitroAutoPrefetcher.swift +81 -0
  16. package/ios/NitroBootstrap.mm +27 -0
  17. package/ios/NitroFetch.swift +9 -0
  18. package/ios/NitroFetchClient.swift +205 -0
  19. package/lib/module/NitroFetch.nitro.js +7 -0
  20. package/lib/module/NitroFetch.nitro.js.map +1 -0
  21. package/lib/module/NitroInstances.js +6 -0
  22. package/lib/module/NitroInstances.js.map +1 -0
  23. package/lib/module/fetch.js +441 -0
  24. package/lib/module/fetch.js.map +1 -0
  25. package/lib/module/index.js +12 -0
  26. package/lib/module/index.js.map +1 -0
  27. package/lib/module/package.json +1 -0
  28. package/lib/typescript/package.json +1 -0
  29. package/lib/typescript/src/NitroFetch.nitro.d.ts +39 -0
  30. package/lib/typescript/src/NitroFetch.nitro.d.ts.map +1 -0
  31. package/lib/typescript/src/NitroInstances.d.ts +3 -0
  32. package/lib/typescript/src/NitroInstances.d.ts.map +1 -0
  33. package/lib/typescript/src/fetch.d.ts +26 -0
  34. package/lib/typescript/src/fetch.d.ts.map +1 -0
  35. package/lib/typescript/src/index.d.ts +6 -0
  36. package/lib/typescript/src/index.d.ts.map +1 -0
  37. package/nitro.json +21 -0
  38. package/nitrogen/generated/android/c++/JHybridNitroFetchClientSpec.cpp +96 -0
  39. package/nitrogen/generated/android/c++/JHybridNitroFetchClientSpec.hpp +65 -0
  40. package/nitrogen/generated/android/c++/JHybridNitroFetchSpec.cpp +49 -0
  41. package/nitrogen/generated/android/c++/JHybridNitroFetchSpec.hpp +64 -0
  42. package/nitrogen/generated/android/c++/JNitroHeader.hpp +57 -0
  43. package/nitrogen/generated/android/c++/JNitroRequest.hpp +103 -0
  44. package/nitrogen/generated/android/c++/JNitroRequestMethod.hpp +74 -0
  45. package/nitrogen/generated/android/c++/JNitroResponse.hpp +105 -0
  46. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/HybridNitroFetchClientSpec.kt +56 -0
  47. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/HybridNitroFetchSpec.kt +52 -0
  48. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroHeader.kt +32 -0
  49. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroRequest.kt +47 -0
  50. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroRequestMethod.kt +26 -0
  51. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroResponse.kt +50 -0
  52. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/nitrofetchOnLoad.kt +35 -0
  53. package/nitrogen/generated/android/nitrofetch+autolinking.cmake +80 -0
  54. package/nitrogen/generated/android/nitrofetch+autolinking.gradle +27 -0
  55. package/nitrogen/generated/android/nitrofetchOnLoad.cpp +54 -0
  56. package/nitrogen/generated/android/nitrofetchOnLoad.hpp +25 -0
  57. package/nitrogen/generated/ios/NitroFetch+autolinking.rb +60 -0
  58. package/nitrogen/generated/ios/NitroFetch-Swift-Cxx-Bridge.cpp +73 -0
  59. package/nitrogen/generated/ios/NitroFetch-Swift-Cxx-Bridge.hpp +298 -0
  60. package/nitrogen/generated/ios/NitroFetch-Swift-Cxx-Umbrella.hpp +67 -0
  61. package/nitrogen/generated/ios/NitroFetchAutolinking.mm +41 -0
  62. package/nitrogen/generated/ios/NitroFetchAutolinking.swift +40 -0
  63. package/nitrogen/generated/ios/c++/HybridNitroFetchClientSpecSwift.cpp +11 -0
  64. package/nitrogen/generated/ios/c++/HybridNitroFetchClientSpecSwift.hpp +101 -0
  65. package/nitrogen/generated/ios/c++/HybridNitroFetchSpecSwift.cpp +11 -0
  66. package/nitrogen/generated/ios/c++/HybridNitroFetchSpecSwift.hpp +75 -0
  67. package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
  68. package/nitrogen/generated/ios/swift/Func_void_NitroResponse.swift +47 -0
  69. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
  70. package/nitrogen/generated/ios/swift/HybridNitroFetchClientSpec.swift +50 -0
  71. package/nitrogen/generated/ios/swift/HybridNitroFetchClientSpec_cxx.swift +149 -0
  72. package/nitrogen/generated/ios/swift/HybridNitroFetchSpec.swift +49 -0
  73. package/nitrogen/generated/ios/swift/HybridNitroFetchSpec_cxx.swift +126 -0
  74. package/nitrogen/generated/ios/swift/NitroHeader.swift +46 -0
  75. package/nitrogen/generated/ios/swift/NitroRequest.swift +199 -0
  76. package/nitrogen/generated/ios/swift/NitroRequestMethod.swift +60 -0
  77. package/nitrogen/generated/ios/swift/NitroResponse.swift +155 -0
  78. package/nitrogen/generated/shared/c++/HybridNitroFetchClientSpec.cpp +22 -0
  79. package/nitrogen/generated/shared/c++/HybridNitroFetchClientSpec.hpp +68 -0
  80. package/nitrogen/generated/shared/c++/HybridNitroFetchSpec.cpp +21 -0
  81. package/nitrogen/generated/shared/c++/HybridNitroFetchSpec.hpp +64 -0
  82. package/nitrogen/generated/shared/c++/NitroHeader.hpp +71 -0
  83. package/nitrogen/generated/shared/c++/NitroRequest.hpp +101 -0
  84. package/nitrogen/generated/shared/c++/NitroRequestMethod.hpp +96 -0
  85. package/nitrogen/generated/shared/c++/NitroResponse.hpp +102 -0
  86. package/package.json +177 -0
  87. package/src/NitroFetch.nitro.ts +57 -0
  88. package/src/NitroInstances.ts +8 -0
  89. package/src/fetch.ts +426 -0
  90. package/src/index.tsx +17 -0
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Szymon Kapala
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
@@ -0,0 +1,30 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "NitroFetch"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported }
14
+ s.source = { :git => "http://google.com.git", :tag => "#{s.version}" }
15
+
16
+
17
+ s.source_files = [
18
+ "ios/**/*.{swift}",
19
+ "ios/**/*.{m,mm}",
20
+ "cpp/**/*.{hpp,cpp}",
21
+ ]
22
+
23
+ s.dependency 'React-jsi'
24
+ s.dependency 'React-callinvoker'
25
+
26
+ load 'nitrogen/generated/ios/NitroFetch+autolinking.rb'
27
+ add_nitrogen_files(s)
28
+
29
+ install_modules_dependencies(s)
30
+ end
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ <p align="center">
2
+ <img src="./assets/logo.png" alt="Nitro Fetch Logo" width="200" />
3
+ </p>
4
+
5
+ # react-native-nitro-fetch
6
+
7
+ Nitro-powered fetch for React Native. Android uses Chromium Cronet (via `org.chromium.net:cronet-embedded`); iOS currently falls back to the built-in fetch. Includes helpers for background prefetching and off-thread parsing with worklets.
8
+
9
+ ## Installation
10
+
11
+ ```sh
12
+ npm install react-native-nitro-fetch react-native-nitro-modules
13
+ ```
14
+
15
+ - `react-native-nitro-modules` is required as this library relies on Nitro Modules. Rebuild your app after installing.
16
+
17
+ ## Quick Start
18
+
19
+ ```ts
20
+ import { fetch } from 'react-native-nitro-fetch';
21
+
22
+ const res = await fetch('https://httpbin.org/get');
23
+ const json = await res.json();
24
+ ```
25
+
26
+ ## Features
27
+
28
+ - Nitro-backed `fetch`: drop-in replacement for global fetch.
29
+ - Android Cronet: fast HTTP stack via `org.chromium.net:cronet-embedded` (already wired in `android/build.gradle`).
30
+ - Prefetch: start a background request tied to a `prefetchKey` and serve it later.
31
+ - Android auto-prefetch: enqueue requests to MMKV so they warm up on next app start.
32
+ - Worklets helper: run mapping/parsing off the JS thread with `react-native-worklets-core`.
33
+
34
+ ## Why Prefetch
35
+
36
+ - Faster first paint of data: Prefetching lets you move network I/O earlier on the critical path so the screen can render with data sooner.
37
+ - App start wins: With auto‑prefetch + MMKV, we can begin fetching immediately at process start. In our measurements on mid‑range Android devices (e.g., Samsung A16), this starts at least ~220 ms earlier than initiating the same request from JS after React is up.
38
+ - UX hooks: Kick off prefetch on navigation intent (button press) and serve the result when the destination screen mounts.
39
+
40
+ See `docs/prefetch.md` for patterns and examples.
41
+
42
+ ## Why Cronet
43
+
44
+ - Performance: Enables HTTP/2 multiplexing and QUIC/HTTP/3, reducing latency and avoiding head‑of‑line blocking.
45
+ - Efficiency: Advanced connection management, TLS/ALPN, Brotli, and robust on‑disk caching.
46
+ - Battle‑tested: Built on Chromium’s networking stack (the same tech behind Chrome) and widely adopted across the ecosystem, including the Flutter community.
47
+
48
+ ## Philosophy
49
+
50
+ - Nitro Fetch often outperforms built‑in fetch thanks to Cronet’s optimizations, but raw speed is not the primary goal.
51
+ - The main goals are:
52
+ - High‑quality prefetching (including auto‑prefetch on app start)
53
+ - Enabling a multi‑threaded React Native architecture (e.g., off‑thread mapping with worklets)
54
+ - Performance is a nice side‑effect.
55
+
56
+ ## Usage Examples
57
+
58
+ - Basic fetch (drop-in replacement):
59
+
60
+ ```ts
61
+ import { fetch } from 'react-native-nitro-fetch';
62
+ const res = await fetch('https://jsonplaceholder.typicode.com/todos/1');
63
+ console.log(await res.json());
64
+ ```
65
+
66
+ - Prefetch and consume (Android or JS fallback):
67
+
68
+ ```ts
69
+ import { fetch, prefetch } from 'react-native-nitro-fetch';
70
+
71
+ await prefetch('https://httpbin.org/uuid', { headers: { prefetchKey: 'uuid' } });
72
+ const res = await fetch('https://httpbin.org/uuid', { headers: { prefetchKey: 'uuid' } });
73
+ console.log('prefetched header:', res.headers.get('nitroPrefetched'));
74
+ ```
75
+
76
+ - Schedule auto-prefetch on Android (requires `react-native-mmkv` in your app):
77
+
78
+ ```ts
79
+ import { prefetchOnAppStart } from 'react-native-nitro-fetch';
80
+ await prefetchOnAppStart('https://httpbin.org/uuid', { prefetchKey: 'uuid' });
81
+ ```
82
+
83
+ - Off-thread parsing with worklets:
84
+
85
+ ```ts
86
+ import { nitroFetchOnWorklet } from 'react-native-nitro-fetch';
87
+
88
+ const map = (payload: { bodyString?: string }) => {
89
+ 'worklet';
90
+ return JSON.parse(payload.bodyString ?? '{}');
91
+ };
92
+
93
+ const data = await nitroFetchOnWorklet('https://httpbin.org/get', undefined, map, { preferBytes: false });
94
+ ```
95
+
96
+ ## Platform Notes
97
+
98
+ - Android: Uses Cronet Java API; no extra setup needed beyond install and rebuild. Cronet engine is initialized once and enables HTTP/2, QUIC, Brotli, and disk cache.
99
+ - iOS: Uses a native `URLSession` client for requests and prefetch (in‑memory cache). Cronet integration is still planned for future releases.
100
+
101
+ ## Limitations & Alternatives
102
+
103
+ - HTTP streaming: Not supported yet. For streaming responses today, use Expo’s `expo-fetch`. Streaming is on the roadmap.
104
+ - WebSockets: Not supported. For high‑performance sockets and binary streams, consider `react-native-fast-io`.
105
+
106
+ ## Documentation
107
+
108
+ - Getting Started: `docs/getting-started.md`
109
+ - API Reference: `docs/api.md`
110
+ - Android Details: `docs/android.md`
111
+ - iOS Details: `docs/ios.md`
112
+ - Prefetch & Auto-Prefetch: `docs/prefetch.md`
113
+ - Worklets: `docs/worklets.md`
114
+ - Troubleshooting: `docs/troubleshooting.md`
115
+ - Cronet (Android) notes: `docs/cronet-android.md`
116
+ - Cronet (iOS) notes: `docs/cronet-ios.md`
117
+
118
+ ## Work With Margelo
119
+
120
+ Need top‑notch React Native help or custom networking solutions? Reach out to Margelo: hello@margelo.com
121
+
122
+ ## Contributing
123
+
124
+ - Development workflow: `CONTRIBUTING.md#development-workflow`
125
+ - Sending a pull request: `CONTRIBUTING.md#sending-a-pull-request`
126
+ - Code of conduct: `CODE_OF_CONDUCT.md`
127
+
128
+ ## License
129
+
130
+ MIT
131
+
132
+ ---
133
+
134
+ Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
@@ -0,0 +1,70 @@
1
+ project(nitrofetch)
2
+ cmake_minimum_required(VERSION 3.9.0)
3
+
4
+ set(PACKAGE_NAME nitrofetch)
5
+ set(CMAKE_VERBOSE_MAKEFILE ON)
6
+ set(CMAKE_CXX_STANDARD 20)
7
+
8
+ # Define C++ library and add all sources
9
+ add_library(${PACKAGE_NAME} SHARED
10
+ src/main/cpp/cpp-adapter.cpp
11
+ )
12
+
13
+ # Add Nitrogen specs :)
14
+ include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/nitrofetch+autolinking.cmake)
15
+
16
+ # Set up local includes
17
+ include_directories("src/main/cpp" "../cpp")
18
+
19
+ # Allow Gradle to pass a CRONET_ROOT pointing to extracted headers/libs (see build.gradle prepareCronet task)
20
+ if (DEFINED CRONET_ROOT)
21
+ set(CRONET_ROOT_DIR ${CRONET_ROOT})
22
+ else()
23
+ set(CRONET_ROOT_DIR ${CMAKE_SOURCE_DIR}/cronet)
24
+ endif()
25
+
26
+ # Optional: include Cronet headers if present (placed by script under android/cronet/include)
27
+ if (EXISTS ${CRONET_ROOT_DIR}/include)
28
+ message(STATUS "Cronet headers base: ${CRONET_ROOT_DIR}/include")
29
+ include_directories(${CRONET_ROOT_DIR}/include)
30
+ # Some Cronet packages nest headers under include/cronet
31
+ if (EXISTS ${CRONET_ROOT_DIR}/include/cronet)
32
+ include_directories(${CRONET_ROOT_DIR}/include/cronet)
33
+ if (EXISTS ${CRONET_ROOT_DIR}/include/cronet/cronet_c.h)
34
+ message(STATUS "Found cronet_c.h: ${CRONET_ROOT_DIR}/include/cronet/cronet_c.h")
35
+ else()
36
+ message(WARNING "cronet_c.h not found under ${CRONET_ROOT_DIR}/include/cronet")
37
+ endif()
38
+ if (EXISTS ${CRONET_ROOT_DIR}/include/cronet/cronet.idl_c.h)
39
+ message(STATUS "Found cronet.idl_c.h: ${CRONET_ROOT_DIR}/include/cronet/cronet.idl_c.h")
40
+ else()
41
+ message(WARNING "cronet.idl_c.h not found under ${CRONET_ROOT_DIR}/include/cronet")
42
+ endif()
43
+ else()
44
+ message(WARNING "Cronet nested include dir not found: ${CRONET_ROOT_DIR}/include/cronet")
45
+ endif()
46
+ endif()
47
+
48
+ find_library(LOG_LIB log)
49
+
50
+ # Link all libraries together
51
+ target_link_libraries(
52
+ ${PACKAGE_NAME}
53
+ ${LOG_LIB}
54
+ android # <-- Android core
55
+ )
56
+
57
+ ## Kotlin-based implementation only; no custom C++ headers forced.
58
+
59
+ # Optional: link Cronet if library file is present (drop-in via prepare script or Gradle task)
60
+ set(CRONET_LIB_DIR ${CRONET_ROOT_DIR}/libs/${ANDROID_ABI})
61
+ if (EXISTS ${CRONET_LIB_DIR})
62
+ file(GLOB CRONET_LIBS "${CRONET_LIB_DIR}/*cronet*.so")
63
+ if (CRONET_LIBS)
64
+ message(STATUS "Linking Cronet from ${CRONET_LIB_DIR}")
65
+ target_link_libraries(${PACKAGE_NAME} ${CRONET_LIBS})
66
+ target_compile_definitions(${PACKAGE_NAME} PRIVATE NITROFETCH_LINKS_CRONET=1)
67
+ else()
68
+ message(WARNING "Cronet libs not found in ${CRONET_LIB_DIR}")
69
+ endif()
70
+ endif()
@@ -0,0 +1,131 @@
1
+ buildscript {
2
+ ext.getExtOrDefault = {name ->
3
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['NitroFetch_' + name]
4
+ }
5
+
6
+ repositories {
7
+ google()
8
+ mavenCentral()
9
+ }
10
+
11
+ dependencies {
12
+ classpath "com.android.tools.build:gradle:8.7.2"
13
+ // noinspection DifferentKotlinGradleVersion
14
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
15
+ }
16
+ }
17
+
18
+ def reactNativeArchitectures() {
19
+ def value = rootProject.getProperties().get("reactNativeArchitectures")
20
+ return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
21
+ }
22
+
23
+ apply plugin: "com.android.library"
24
+ apply plugin: "kotlin-android"
25
+ apply from: '../nitrogen/generated/android/nitrofetch+autolinking.gradle'
26
+
27
+ // Do not apply React Gradle plugin in library to avoid RN codegen duplicating app classes
28
+
29
+ def getExtOrIntegerDefault(name) {
30
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["NitroFetch_" + name]).toInteger()
31
+ }
32
+
33
+ android {
34
+ namespace "com.margelo.nitro.nitrofetch"
35
+
36
+ compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
37
+
38
+ defaultConfig {
39
+ minSdkVersion getExtOrIntegerDefault("minSdkVersion")
40
+ targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
41
+
42
+ externalNativeBuild {
43
+ cmake {
44
+ cppFlags "-frtti -fexceptions -Wall -fstack-protector-all"
45
+ arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
46
+ abiFilters (*reactNativeArchitectures())
47
+ }
48
+ }
49
+ }
50
+
51
+ externalNativeBuild {
52
+ cmake {
53
+ path "CMakeLists.txt"
54
+ }
55
+ }
56
+
57
+ packagingOptions {
58
+ excludes = [
59
+ "**/libc++_shared.so",
60
+ "**/libfbjni.so",
61
+ "**/libjsi.so",
62
+ "**/libfolly_json.so",
63
+ "**/libfolly_runtime.so",
64
+ "**/libglog.so",
65
+ "**/libhermes.so",
66
+ "**/libhermes-executor-debug.so",
67
+ "**/libhermes_executor.so",
68
+ "**/libreactnative.so",
69
+ "**/libreactnativejni.so",
70
+ "**/libturbomodulejsijni.so",
71
+ "**/libreact_nativemodule_core.so",
72
+ "**/libjscexecutor.so"
73
+ ]
74
+ }
75
+
76
+ buildFeatures {
77
+ buildConfig true
78
+ prefab true
79
+ }
80
+
81
+ buildTypes {
82
+ release {
83
+ minifyEnabled false
84
+ }
85
+ }
86
+
87
+ lintOptions {
88
+ disable "GradleCompatible"
89
+ }
90
+
91
+ compileOptions {
92
+ sourceCompatibility JavaVersion.VERSION_1_8
93
+ targetCompatibility JavaVersion.VERSION_1_8
94
+ }
95
+
96
+ sourceSets {
97
+ main {
98
+ java.srcDirs += [
99
+ "generated/java",
100
+ "generated/jni"
101
+ ]
102
+ }
103
+ }
104
+ }
105
+
106
+ repositories {
107
+ mavenCentral()
108
+ google()
109
+ }
110
+
111
+ def kotlin_version = getExtOrDefault("kotlinVersion")
112
+ // ---------- Cronet (Java API only) ----------
113
+ def cronetVersion = (getExtOrDefault("cronetVersion") ?: "119.6045.31")
114
+
115
+
116
+ dependencies {
117
+ implementation "com.facebook.react:react-android"
118
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
119
+ implementation project(":react-native-nitro-modules")
120
+ // Provide org.chromium.net Java API for CronetEngine in Kotlin
121
+ // Cronet
122
+ api "org.chromium.net:cronet-embedded:${cronetVersion}"
123
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"
124
+ }
125
+
126
+ configurations {
127
+ cronetAar
128
+ }
129
+
130
+
131
+ // No automatic fetching of Cronet headers/libs; library uses Cronet Java API only.
@@ -0,0 +1,5 @@
1
+ NitroFetch_kotlinVersion=2.0.21
2
+ NitroFetch_minSdkVersion=24
3
+ NitroFetch_targetSdkVersion=34
4
+ NitroFetch_compileSdkVersion=35
5
+ NitroFetch_ndkVersion=27.1.12297006
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,6 @@
1
+ #include <jni.h>
2
+ #include "nitrofetchOnLoad.hpp"
3
+
4
+ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
5
+ return margelo::nitro::nitrofetch::initialize(vm);
6
+ }
@@ -0,0 +1,91 @@
1
+ package com.margelo.nitro.nitrofetch
2
+
3
+ import android.app.Application
4
+ import org.json.JSONArray
5
+ import org.json.JSONObject
6
+ import java.util.concurrent.CompletableFuture
7
+
8
+ object AutoPrefetcher {
9
+ @Volatile private var initialized = false
10
+
11
+ fun prefetchOnStart(app: Application) {
12
+ if (initialized) return
13
+ initialized = true
14
+ try {
15
+ val mmkv = getMMKV(app) ?: return
16
+ val raw = invokeMMKVDecodeString(mmkv, KEY_QUEUE) ?: return
17
+ if (raw.isEmpty()) return
18
+ val arr = JSONArray(raw)
19
+ for (i in 0 until arr.length()) {
20
+ val o = arr.optJSONObject(i) ?: continue
21
+ val url = o.optString("url", null) ?: continue
22
+ val prefetchKey = o.optString("prefetchKey", null) ?: continue
23
+ val headersObj = o.optJSONObject("headers") ?: JSONObject()
24
+ val headersList = mutableListOf<Pair<String, String>>()
25
+ headersObj.keys().forEachRemaining { k ->
26
+ headersList.add(k to headersObj.optString(k, ""))
27
+ }
28
+ // Ensure prefetchKey header is present
29
+ headersList.add("prefetchKey" to prefetchKey)
30
+
31
+ val headerObjs = headersList.map { (k, v) -> NitroHeader(k, v) }.toTypedArray()
32
+ val req = NitroRequest(
33
+ url = url,
34
+ method = null,
35
+ headers = headerObjs,
36
+ bodyString = null,
37
+ bodyBytes = null,
38
+ timeoutMs = null,
39
+ followRedirects = null
40
+ )
41
+
42
+ // If already pending or fresh, skip starting a new one
43
+ if (FetchCache.getPending(prefetchKey) != null) {
44
+ continue
45
+ }
46
+ if (FetchCache.getResultIfFresh(prefetchKey, 5_000L) != null) {
47
+ continue
48
+ }
49
+
50
+ val future = CompletableFuture<NitroResponse>()
51
+ FetchCache.setPending(prefetchKey, future)
52
+ NitroFetchClient.fetch(req,
53
+ onSuccess = { res ->
54
+ try {
55
+ FetchCache.complete(prefetchKey, res)
56
+ future.complete(res)
57
+ } catch (t: Throwable) {
58
+ FetchCache.completeExceptionally(prefetchKey, t)
59
+ future.completeExceptionally(t)
60
+ }
61
+ },
62
+ onFail = { err ->
63
+ FetchCache.completeExceptionally(prefetchKey, err)
64
+ future.completeExceptionally(err)
65
+ }
66
+ )
67
+ }
68
+ } catch (_: Throwable) {
69
+ // ignore – prefetch-on-start is best-effort
70
+ }
71
+ }
72
+
73
+ private const val KEY_QUEUE = "nitrofetch_autoprefetch_queue"
74
+
75
+ private fun getMMKV(app: Application): Any? {
76
+ return try {
77
+ val cls = Class.forName("com.tencent.mmkv.MMKV")
78
+ // Initialize if available
79
+ try { cls.getMethod("initialize", Application::class.java).invoke(null, app) } catch (_: Throwable) {}
80
+ // Get default instance
81
+ cls.getMethod("defaultMMKV").invoke(null)
82
+ } catch (_: Throwable) { null }
83
+ }
84
+
85
+ private fun invokeMMKVDecodeString(mmkv: Any, key: String): String? {
86
+ return try {
87
+ val m = mmkv.javaClass.getMethod("decodeString", String::class.java, String::class.java)
88
+ m.invoke(mmkv, key, null) as? String
89
+ } catch (_: Throwable) { null }
90
+ }
91
+ }
@@ -0,0 +1,48 @@
1
+ package com.margelo.nitro.nitrofetch
2
+
3
+ import java.util.concurrent.CompletableFuture
4
+ import java.util.concurrent.ConcurrentHashMap
5
+
6
+ data class CachedEntry(val response: NitroResponse, val timestampMs: Long)
7
+
8
+ object FetchCache {
9
+ private val pending = ConcurrentHashMap<String, CompletableFuture<NitroResponse>>()
10
+ private val results = ConcurrentHashMap<String, CachedEntry>()
11
+
12
+ fun getPending(key: String): CompletableFuture<NitroResponse>? = pending[key]
13
+
14
+ fun setPending(key: String, future: CompletableFuture<NitroResponse>) {
15
+ pending[key] = future
16
+ // Cleanup: remove pending entry when completed
17
+ future.whenComplete { _, _ ->
18
+ pending.remove(key)
19
+ }
20
+ }
21
+
22
+ fun complete(key: String, value: NitroResponse) {
23
+ results[key] = CachedEntry(value, System.currentTimeMillis())
24
+ pending[key]?.complete(value)
25
+ pending.remove(key)
26
+ }
27
+
28
+ fun completeExceptionally(key: String, t: Throwable) {
29
+ pending[key]?.completeExceptionally(t)
30
+ pending.remove(key)
31
+ }
32
+
33
+ fun getResult(key: String): NitroResponse? {
34
+ val entry = results.remove(key) ?: return null
35
+ return entry.response
36
+ }
37
+
38
+ fun getResultIfFresh(key: String, maxAgeMs: Long): NitroResponse? {
39
+ val entry = results.remove(key) ?: return null
40
+ val age = System.currentTimeMillis() - entry.timestampMs
41
+ return if (age <= maxAgeMs) entry.response else null
42
+ }
43
+
44
+ fun clear() {
45
+ pending.clear()
46
+ results.clear()
47
+ }
48
+ }
@@ -0,0 +1,94 @@
1
+ package com.margelo.nitro.nitrofetch
2
+
3
+ import android.app.Application
4
+ import android.util.Log
5
+ import com.facebook.proguard.annotations.DoNotStrip
6
+ import org.chromium.net.CronetEngine
7
+ import org.chromium.net.CronetProvider
8
+ import java.io.File
9
+ import java.util.concurrent.Executor
10
+ import java.util.concurrent.Executors
11
+
12
+ @DoNotStrip
13
+ class NitroFetch : HybridNitroFetchSpec() {
14
+ // Generated base may expect env-less createClient.
15
+ override fun createClient(): NitroFetchClient {
16
+ return NitroFetchClient(getEngine(), ioExecutor)
17
+ }
18
+
19
+ companion object {
20
+ @Volatile private var engineRef: CronetEngine? = null
21
+
22
+ // Simpler & safer for callbacks than a pool (avoid reentrancy races in glue code).
23
+ val ioExecutor: Executor by lazy {
24
+ val cores = Runtime.getRuntime().availableProcessors().coerceAtLeast(2)
25
+ Executors.newFixedThreadPool(cores) { r ->
26
+ Thread(r, "NitroCronet-io").apply {
27
+ isDaemon = true
28
+ priority = Thread.NORM_PRIORITY
29
+ }
30
+ }
31
+ }
32
+
33
+ fun getEngine(): CronetEngine {
34
+ engineRef?.let { return it }
35
+ synchronized(this) {
36
+ engineRef?.let { return it }
37
+
38
+ val app = currentApplication() ?: initialApplication()
39
+ ?: throw IllegalStateException("NitroFetch: Application not available")
40
+
41
+ // Log available providers and prefer the Native one (avoids Play-Services DNS quirks)
42
+ val providers = CronetProvider.getAllProviders(app)
43
+ providers.forEach { Log.i("NitroFetch", "Cronet provider: ${it.name} v=${it.version}") }
44
+ val nativeProvider = providers.firstOrNull { it.name.contains("Native", ignoreCase = true) }
45
+
46
+ val cacheDir = File(app.cacheDir, "nitrofetch_cronet_cache").apply { mkdirs() }
47
+ val builder = (nativeProvider?.createBuilder() ?: CronetEngine.Builder(app))
48
+ .enableHttp2(true)
49
+ .enableQuic(true)
50
+ .enableBrotli(true)
51
+ .setStoragePath(cacheDir.absolutePath)
52
+ .enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK, 50 * 1024 * 1024)
53
+ .setUserAgent("NitroFetch/0.1")
54
+
55
+
56
+ // --- Optional debugging knobs (uncomment temporarily) ---
57
+ // Enable NetLog-like tracing in NetworkService:
58
+ // builder.setExperimentalOptions("""{"NetworkService":{"enable_network_logging":true}}""")
59
+ //
60
+ // Prove DNS issues by mapping a host (TESTING ONLY, remove in prod):
61
+ // builder.setExperimentalOptions("""{"HostResolverRules":{"host_resolver_rules":"MAP httpbin.org 54.167.17.38"}}""")
62
+
63
+ val engine = builder.build()
64
+ Log.i("NitroFetch", "CronetEngine initialized. Provider=${nativeProvider?.name ?: "Default"} Cache=${cacheDir.absolutePath}")
65
+ engineRef = engine
66
+ return engine
67
+ }
68
+ }
69
+
70
+ fun shutdown() {
71
+ synchronized(this) {
72
+ try {
73
+ engineRef?.shutdown()
74
+ } catch (_: Throwable) {
75
+ // ignore – shutdown is best-effort
76
+ } finally {
77
+ engineRef = null
78
+ }
79
+ }
80
+ }
81
+
82
+ private fun currentApplication(): Application? = try {
83
+ val cls = Class.forName("android.app.ActivityThread")
84
+ val m = cls.getMethod("currentApplication")
85
+ m.invoke(null) as? Application
86
+ } catch (_: Throwable) { null }
87
+
88
+ private fun initialApplication(): Application? = try {
89
+ val cls = Class.forName("android.app.AppGlobals")
90
+ val m = cls.getMethod("getInitialApplication")
91
+ m.invoke(null) as? Application
92
+ } catch (_: Throwable) { null }
93
+ }
94
+ }