react-native-nitro-fetch 0.1.3 → 0.1.4
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.
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt +7 -28
- package/android/src/main/java/com/margelo/nitro/nitrofetch/FetchCache.kt +11 -1
- package/android/src/main/java/com/margelo/nitro/nitrofetch/NativeStorage.kt +102 -0
- package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt +2 -2
- package/ios/NativeStorage.swift +61 -0
- package/ios/NitroAutoPrefetcher.swift +5 -41
- package/lib/module/NitroFetch.nitro.js +0 -3
- package/lib/module/NitroFetch.nitro.js.map +1 -1
- package/lib/module/NitroInstances.js +1 -0
- package/lib/module/NitroInstances.js.map +1 -1
- package/lib/module/fetch.js +60 -81
- package/lib/module/fetch.js.map +1 -1
- package/lib/typescript/src/NitroFetch.nitro.d.ts +8 -0
- package/lib/typescript/src/NitroFetch.nitro.d.ts.map +1 -1
- package/lib/typescript/src/NitroInstances.d.ts +2 -1
- package/lib/typescript/src/NitroInstances.d.ts.map +1 -1
- package/lib/typescript/src/fetch.d.ts.map +1 -1
- package/nitro.json +4 -0
- package/nitrogen/generated/android/c++/JHybridNativeStorageSpec.cpp +54 -0
- package/nitrogen/generated/android/c++/JHybridNativeStorageSpec.hpp +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/HybridNativeStorageSpec.kt +60 -0
- package/nitrogen/generated/android/nitrofetch+autolinking.cmake +9 -4
- package/nitrogen/generated/android/nitrofetchOnLoad.cpp +10 -0
- package/nitrogen/generated/ios/NitroFetch-Swift-Cxx-Bridge.cpp +17 -0
- package/nitrogen/generated/ios/NitroFetch-Swift-Cxx-Bridge.hpp +35 -0
- package/nitrogen/generated/ios/NitroFetch-Swift-Cxx-Umbrella.hpp +5 -0
- package/nitrogen/generated/ios/NitroFetchAutolinking.mm +8 -0
- package/nitrogen/generated/ios/NitroFetchAutolinking.swift +15 -0
- package/nitrogen/generated/ios/c++/HybridNativeStorageSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNativeStorageSpecSwift.hpp +85 -0
- package/nitrogen/generated/ios/swift/HybridNativeStorageSpec.swift +51 -0
- package/nitrogen/generated/ios/swift/HybridNativeStorageSpec_cxx.swift +145 -0
- package/nitrogen/generated/shared/c++/HybridNativeStorageSpec.cpp +23 -0
- package/nitrogen/generated/shared/c++/HybridNativeStorageSpec.hpp +64 -0
- package/package.json +8 -24
- package/src/NitroFetch.nitro.ts +12 -5
- package/src/NitroInstances.ts +6 -2
- package/src/fetch.ts +150 -91
- package/LICENSE +0 -20
- package/README.md +0 -151
package/android/build.gradle
CHANGED
|
@@ -117,11 +117,11 @@ dependencies {
|
|
|
117
117
|
implementation "com.facebook.react:react-android"
|
|
118
118
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
119
119
|
implementation project(":react-native-nitro-modules")
|
|
120
|
-
implementation("com.tencent:mmkv:1.3.14")// or latest
|
|
121
120
|
// Provide org.chromium.net Java API for CronetEngine in Kotlin
|
|
122
121
|
// Cronet
|
|
123
122
|
api "org.chromium.net:cronet-embedded:${cronetVersion}"
|
|
124
123
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"
|
|
124
|
+
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
configurations {
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
package com.margelo.nitro.nitrofetch
|
|
2
2
|
|
|
3
3
|
import android.app.Application
|
|
4
|
+
import android.content.Context
|
|
4
5
|
import org.json.JSONArray
|
|
5
6
|
import org.json.JSONObject
|
|
6
7
|
import java.util.concurrent.CompletableFuture
|
|
7
|
-
import com.tencent.mmkv.MMKV;
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
object AutoPrefetcher {
|
|
11
11
|
@Volatile private var initialized = false
|
|
12
|
+
private const val KEY_QUEUE = "nitrofetch_autoprefetch_queue"
|
|
13
|
+
private const val PREFS_NAME = "nitro_fetch_storage"
|
|
12
14
|
|
|
13
15
|
fun prefetchOnStart(app: Application) {
|
|
14
16
|
if (initialized) return
|
|
15
17
|
initialized = true
|
|
16
18
|
try {
|
|
17
|
-
val
|
|
18
|
-
val raw =
|
|
19
|
+
val prefs = app.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
20
|
+
val raw = prefs.getString(KEY_QUEUE, null) ?: ""
|
|
19
21
|
if (raw.isEmpty()) return
|
|
20
22
|
val arr = JSONArray(raw)
|
|
21
23
|
for (i in 0 until arr.length()) {
|
|
@@ -42,12 +44,8 @@ object AutoPrefetcher {
|
|
|
42
44
|
)
|
|
43
45
|
|
|
44
46
|
// If already pending or fresh, skip starting a new one
|
|
45
|
-
if (FetchCache.getPending(prefetchKey) != null)
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
if (FetchCache.getResultIfFresh(prefetchKey, 5_000L) != null) {
|
|
49
|
-
continue
|
|
50
|
-
}
|
|
47
|
+
if (FetchCache.getPending(prefetchKey) != null) continue
|
|
48
|
+
if (FetchCache.hasFreshResult(prefetchKey, 5_000L)) continue
|
|
51
49
|
|
|
52
50
|
val future = CompletableFuture<NitroResponse>()
|
|
53
51
|
FetchCache.setPending(prefetchKey, future)
|
|
@@ -71,23 +69,4 @@ object AutoPrefetcher {
|
|
|
71
69
|
// ignore – prefetch-on-start is best-effort
|
|
72
70
|
}
|
|
73
71
|
}
|
|
74
|
-
|
|
75
|
-
private const val KEY_QUEUE = "nitrofetch_autoprefetch_queue"
|
|
76
|
-
|
|
77
|
-
private fun getMMKV(app: Application): Any? {
|
|
78
|
-
return try {
|
|
79
|
-
MMKV.initialize(app);
|
|
80
|
-
|
|
81
|
-
return MMKV.defaultMMKV()
|
|
82
|
-
} catch (_: Throwable) {
|
|
83
|
-
null
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
private fun invokeMMKVDecodeString(mmkv: Any, key: String): String? {
|
|
88
|
-
return try {
|
|
89
|
-
val m = mmkv.javaClass.getMethod("decodeString", String::class.java, String::class.java)
|
|
90
|
-
m.invoke(mmkv, key, null) as? String
|
|
91
|
-
} catch (_: Throwable) { null }
|
|
92
|
-
}
|
|
93
72
|
}
|
|
@@ -41,8 +41,18 @@ object FetchCache {
|
|
|
41
41
|
return if (age <= maxAgeMs) entry.response else null
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Check if a fresh result exists WITHOUT consuming it.
|
|
46
|
+
* Used to check if we should skip starting a new prefetch.
|
|
47
|
+
*/
|
|
48
|
+
fun hasFreshResult(key: String, maxAgeMs: Long): Boolean {
|
|
49
|
+
val entry = results[key] ?: return false
|
|
50
|
+
val age = System.currentTimeMillis() - entry.timestampMs
|
|
51
|
+
return age <= maxAgeMs
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
fun clear() {
|
|
45
55
|
pending.clear()
|
|
46
56
|
results.clear()
|
|
47
57
|
}
|
|
48
|
-
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
package com.margelo.nitro.nitrofetch
|
|
2
|
+
|
|
3
|
+
import android.app.Application
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.SharedPreferences
|
|
6
|
+
import android.util.Log
|
|
7
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
8
|
+
import com.margelo.nitro.NitroModules
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@DoNotStrip
|
|
13
|
+
class NativeStorage : HybridNativeStorageSpec() {
|
|
14
|
+
|
|
15
|
+
companion object {
|
|
16
|
+
private const val TAG = "HybridNativeStorage"
|
|
17
|
+
private const val PREFS_NAME = "nitro_fetch_storage"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
private val sharedPreferences: SharedPreferences by lazy {
|
|
21
|
+
val context = NitroModules.applicationContext ?: throw Error("Cannot get Android Context - No Context available!")
|
|
22
|
+
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Retrieves a string value for the given key.
|
|
30
|
+
*
|
|
31
|
+
* @param key The key to look up in storage
|
|
32
|
+
* @return The stored string value, or empty string if key doesn't exist
|
|
33
|
+
* @throws IllegalStateException if SharedPreferences is not available
|
|
34
|
+
*/
|
|
35
|
+
override fun getString(key: String): String {
|
|
36
|
+
return try {
|
|
37
|
+
val value = sharedPreferences.getString(key, null)
|
|
38
|
+
if (value != null) {
|
|
39
|
+
Log.d(TAG, "Retrieved value for key: $key")
|
|
40
|
+
value
|
|
41
|
+
} else {
|
|
42
|
+
Log.d(TAG, "Key not found: $key, returning empty string")
|
|
43
|
+
""
|
|
44
|
+
}
|
|
45
|
+
} catch (t: Throwable) {
|
|
46
|
+
Log.e(TAG, "Error getting string for key: $key", t)
|
|
47
|
+
throw RuntimeException("Failed to get string for key: $key", t)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Stores a string value with the given key.
|
|
53
|
+
*
|
|
54
|
+
* @param key The key to store the value under
|
|
55
|
+
* @param value The string value to store
|
|
56
|
+
* @throws IllegalStateException if SharedPreferences is not available
|
|
57
|
+
* @throws RuntimeException if the write operation fails
|
|
58
|
+
*/
|
|
59
|
+
override fun setString(key: String, value: String) {
|
|
60
|
+
try {
|
|
61
|
+
val editor = sharedPreferences.edit()
|
|
62
|
+
editor.putString(key, value)
|
|
63
|
+
val success = editor.commit() // commit() is synchronous and returns boolean
|
|
64
|
+
if (success) {
|
|
65
|
+
Log.d(TAG, "Successfully stored value for key: $key")
|
|
66
|
+
} else {
|
|
67
|
+
Log.e(TAG, "Failed to commit value for key: $key")
|
|
68
|
+
throw RuntimeException("Failed to store value for key: $key")
|
|
69
|
+
}
|
|
70
|
+
} catch (t: Throwable) {
|
|
71
|
+
Log.e(TAG, "Error setting string for key: $key", t)
|
|
72
|
+
throw RuntimeException("Failed to set string for key: $key", t)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Deletes the value associated with the given key.
|
|
78
|
+
* If the key doesn't exist, this is a no-op.
|
|
79
|
+
*
|
|
80
|
+
* @param key The key to delete from storage
|
|
81
|
+
* @throws IllegalStateException if SharedPreferences is not available
|
|
82
|
+
* @throws RuntimeException if the delete operation fails
|
|
83
|
+
*/
|
|
84
|
+
override fun removeString(key: String) {
|
|
85
|
+
try {
|
|
86
|
+
val editor = sharedPreferences.edit()
|
|
87
|
+
editor.remove(key)
|
|
88
|
+
val success = editor.commit() // commit() is synchronous and returns boolean
|
|
89
|
+
if (success) {
|
|
90
|
+
Log.d(TAG, "Successfully deleted key: $key")
|
|
91
|
+
} else {
|
|
92
|
+
Log.e(TAG, "Failed to commit deletion for key: $key")
|
|
93
|
+
throw RuntimeException("Failed to delete key: $key")
|
|
94
|
+
}
|
|
95
|
+
} catch (t: Throwable) {
|
|
96
|
+
Log.e(TAG, "Error deleting key: $key", t)
|
|
97
|
+
throw RuntimeException("Failed to delete key: $key", t)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
@@ -220,8 +220,8 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E
|
|
|
220
220
|
promise.reject(IllegalArgumentException("prefetch: missing 'prefetchKey' header"))
|
|
221
221
|
return promise
|
|
222
222
|
}
|
|
223
|
-
// If already have a fresh result, resolve immediately
|
|
224
|
-
FetchCache.
|
|
223
|
+
// If already have a fresh result, resolve immediately (NON-DESTRUCTIVE CHECK)
|
|
224
|
+
if (FetchCache.hasFreshResult(key, 5_000L)) {
|
|
225
225
|
promise.resolve(Unit)
|
|
226
226
|
return promise
|
|
227
227
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//
|
|
2
|
+
// NativeStorage.swift
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Ritesh Shukla on 08/11/25.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
final class NativeStorage: HybridNativeStorageSpec {
|
|
12
|
+
|
|
13
|
+
private static let suiteName = "nitro_fetch_storage"
|
|
14
|
+
|
|
15
|
+
private let userDefaults: UserDefaults
|
|
16
|
+
|
|
17
|
+
public override init() {
|
|
18
|
+
// Use a named suite for better isolation, fallback to standard if creation fails
|
|
19
|
+
if let suite = UserDefaults(suiteName: NativeStorage.suiteName) {
|
|
20
|
+
self.userDefaults = suite
|
|
21
|
+
} else {
|
|
22
|
+
self.userDefaults = UserDefaults.standard
|
|
23
|
+
}
|
|
24
|
+
super.init()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Retrieves a string value for the given key.
|
|
28
|
+
///
|
|
29
|
+
/// - Parameter key: The key to look up in storage
|
|
30
|
+
/// - Returns: The stored string value, or empty string if key doesn't exist
|
|
31
|
+
/// - Throws: RuntimeError if the operation fails
|
|
32
|
+
func getString(key: String) throws -> String {
|
|
33
|
+
guard let value = userDefaults.string(forKey: key) else {
|
|
34
|
+
return ""
|
|
35
|
+
}
|
|
36
|
+
return value
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Stores a string value with the given key.
|
|
40
|
+
///
|
|
41
|
+
/// - Parameters:
|
|
42
|
+
/// - key: The key to store the value under
|
|
43
|
+
/// - value: The string value to store
|
|
44
|
+
/// - Throws: RuntimeError if the write operation fails
|
|
45
|
+
func setString(key: String, value: String) throws {
|
|
46
|
+
userDefaults.set(value, forKey: key)
|
|
47
|
+
// Synchronize to ensure immediate persistence
|
|
48
|
+
userDefaults.synchronize()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Deletes the value associated with the given key.
|
|
52
|
+
/// If the key doesn't exist, this is a no-op.
|
|
53
|
+
///
|
|
54
|
+
/// - Parameter key: The key to delete from storage
|
|
55
|
+
/// - Throws: RuntimeError if the delete operation fails
|
|
56
|
+
func removeString(key: String) throws {
|
|
57
|
+
userDefaults.removeObject(forKey: key)
|
|
58
|
+
// Synchronize to ensure immediate persistence
|
|
59
|
+
userDefaults.synchronize()
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -4,12 +4,16 @@ import Foundation
|
|
|
4
4
|
public final class NitroAutoPrefetcher: NSObject {
|
|
5
5
|
private static var initialized = false
|
|
6
6
|
private static let queueKey = "nitrofetch_autoprefetch_queue"
|
|
7
|
+
private static let suiteName = "nitro_fetch_storage"
|
|
7
8
|
|
|
8
9
|
@objc
|
|
9
10
|
public static func prefetchOnStart() {
|
|
10
11
|
if initialized { return }
|
|
11
12
|
initialized = true
|
|
12
|
-
|
|
13
|
+
|
|
14
|
+
// Read from UserDefaults
|
|
15
|
+
let userDefaults = UserDefaults(suiteName: suiteName) ?? UserDefaults.standard
|
|
16
|
+
guard let raw = userDefaults.string(forKey: queueKey), !raw.isEmpty else { return }
|
|
13
17
|
guard let data = raw.data(using: .utf8) else { return }
|
|
14
18
|
guard let arr = try? JSONSerialization.jsonObject(with: data, options: []) as? [Any] else { return }
|
|
15
19
|
|
|
@@ -32,46 +36,6 @@ public final class NitroAutoPrefetcher: NSObject {
|
|
|
32
36
|
}
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
|
-
|
|
36
|
-
// MARK: - MMKV dynamic access (optional)
|
|
37
|
-
|
|
38
|
-
private static func readMMKVString(forKey key: String) -> String? {
|
|
39
|
-
guard let mmkvClass = NSClassFromString("MMKV") as? NSObject.Type else { return nil }
|
|
40
|
-
// Try to initialize if needed (ignore failures)
|
|
41
|
-
let initSelectors = [
|
|
42
|
-
NSSelectorFromString("initializeMMKV:"),
|
|
43
|
-
NSSelectorFromString("initialize:")
|
|
44
|
-
]
|
|
45
|
-
for sel in initSelectors where mmkvClass.responds(to: sel) {
|
|
46
|
-
_ = mmkvClass.perform(sel, with: nil)
|
|
47
|
-
break
|
|
48
|
-
}
|
|
49
|
-
guard let mmkvObjUnretained = mmkvClass.perform(NSSelectorFromString("defaultMMKV"))?.takeUnretainedValue() else { return nil }
|
|
50
|
-
let mmkv = mmkvObjUnretained as AnyObject
|
|
51
|
-
// Try common selectors
|
|
52
|
-
let candidates = [
|
|
53
|
-
NSSelectorFromString("decodeStringForKey:"),
|
|
54
|
-
NSSelectorFromString("stringForKey:"),
|
|
55
|
-
NSSelectorFromString("getStringForKey:"),
|
|
56
|
-
]
|
|
57
|
-
for sel in candidates where mmkv.responds(to: sel) {
|
|
58
|
-
if let val = mmkv.perform(sel, with: key)?.takeUnretainedValue() as? String {
|
|
59
|
-
return val
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
// Some APIs have (forKey: defaultValue:) signatures
|
|
63
|
-
let twoArgCandidates = [
|
|
64
|
-
NSSelectorFromString("decodeStringForKey:defaultValue:"),
|
|
65
|
-
NSSelectorFromString("stringForKey:defaultValue:"),
|
|
66
|
-
NSSelectorFromString("getStringForKey:defaultValue:"),
|
|
67
|
-
]
|
|
68
|
-
for sel in twoArgCandidates where mmkv.responds(to: sel) {
|
|
69
|
-
// NSInvocation is cumbersome in Swift; best-effort fallthrough without it
|
|
70
|
-
// Prefer single-arg variants above.
|
|
71
|
-
break
|
|
72
|
-
}
|
|
73
|
-
return nil
|
|
74
|
-
}
|
|
75
39
|
}
|
|
76
40
|
|
|
77
41
|
// Expose a C-ABI symbol the ObjC++ file can call
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":[],"sourceRoot":"../../src","sources":["NitroFetch.nitro.ts"],"mappings":"
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../src","sources":["NitroFetch.nitro.ts"],"mappings":"","ignoreList":[]}
|
|
@@ -3,4 +3,5 @@
|
|
|
3
3
|
import { NitroModules } from 'react-native-nitro-modules';
|
|
4
4
|
// Create singletons once per JS runtime
|
|
5
5
|
export const NitroFetch = NitroModules.createHybridObject('NitroFetch');
|
|
6
|
+
export const NativeStorage = NitroModules.createHybridObject('NativeStorage');
|
|
6
7
|
//# sourceMappingURL=NitroInstances.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["NitroModules","NitroFetch","createHybridObject"],"sourceRoot":"../../src","sources":["NitroInstances.ts"],"mappings":";;AAAA,SAASA,YAAY,QAAQ,4BAA4B;
|
|
1
|
+
{"version":3,"names":["NitroModules","NitroFetch","createHybridObject","NativeStorage"],"sourceRoot":"../../src","sources":["NitroInstances.ts"],"mappings":";;AAAA,SAASA,YAAY,QAAQ,4BAA4B;AAMzD;AACA,OAAO,MAAMC,UAA0B,GACrCD,YAAY,CAACE,kBAAkB,CAAiB,YAAY,CAAC;AAE/D,OAAO,MAAMC,aAAgC,GAC3CH,YAAY,CAACE,kBAAkB,CAAoB,eAAe,CAAC","ignoreList":[]}
|
package/lib/module/fetch.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import { NitroFetch as NitroFetchSingleton } from "./NitroInstances.js";
|
|
4
|
+
import { NativeStorage as NativeStorageSingleton } from "./NitroInstances.js";
|
|
4
5
|
|
|
5
6
|
// No base64: pass strings/ArrayBuffers directly
|
|
6
7
|
|
|
@@ -55,26 +56,14 @@ function normalizeBody(body) {
|
|
|
55
56
|
if (ArrayBuffer.isView(body)) {
|
|
56
57
|
const view = body;
|
|
57
58
|
// Pass a copy/slice of the underlying bytes without base64
|
|
58
|
-
//@ts-ignore
|
|
59
59
|
return {
|
|
60
|
+
//@ts-ignore
|
|
60
61
|
bodyBytes: view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength)
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
64
|
// TODO: Blob/FormData support can be added later
|
|
64
65
|
throw new Error('Unsupported body type for nitro fetch');
|
|
65
66
|
}
|
|
66
|
-
|
|
67
|
-
// @ts-ignore
|
|
68
|
-
function pairsToHeaders(pairs) {
|
|
69
|
-
'worklet';
|
|
70
|
-
|
|
71
|
-
const h = new Headers();
|
|
72
|
-
for (const {
|
|
73
|
-
key,
|
|
74
|
-
value
|
|
75
|
-
} of pairs) h.append(key, value);
|
|
76
|
-
return h;
|
|
77
|
-
}
|
|
78
67
|
const NitroFetchHybrid = NitroFetchSingleton;
|
|
79
68
|
let client;
|
|
80
69
|
function ensureClient() {
|
|
@@ -114,8 +103,8 @@ function buildNitroRequest(input, init) {
|
|
|
114
103
|
method: method?.toUpperCase() ?? 'GET',
|
|
115
104
|
headers,
|
|
116
105
|
bodyString: normalized?.bodyString,
|
|
117
|
-
bodyBytes
|
|
118
|
-
|
|
106
|
+
// Only include bodyBytes when provided to avoid signaling upload data unintentionally
|
|
107
|
+
bodyBytes: undefined,
|
|
119
108
|
followRedirects: true
|
|
120
109
|
};
|
|
121
110
|
}
|
|
@@ -151,36 +140,58 @@ async function nitroFetchRaw(input, init) {
|
|
|
151
140
|
const res = await client.request(req);
|
|
152
141
|
return res;
|
|
153
142
|
}
|
|
143
|
+
|
|
144
|
+
// Simple Headers-like class that supports get() method
|
|
145
|
+
class NitroHeaders {
|
|
146
|
+
constructor(headers) {
|
|
147
|
+
this._headers = new Map();
|
|
148
|
+
for (const {
|
|
149
|
+
key,
|
|
150
|
+
value
|
|
151
|
+
} of headers) {
|
|
152
|
+
// Headers are case-insensitive, normalize to lowercase
|
|
153
|
+
this._headers.set(key.toLowerCase(), value);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
get(name) {
|
|
157
|
+
return this._headers.get(name.toLowerCase()) ?? null;
|
|
158
|
+
}
|
|
159
|
+
has(name) {
|
|
160
|
+
return this._headers.has(name.toLowerCase());
|
|
161
|
+
}
|
|
162
|
+
forEach(callback) {
|
|
163
|
+
this._headers.forEach(callback);
|
|
164
|
+
}
|
|
165
|
+
entries() {
|
|
166
|
+
return this._headers.entries();
|
|
167
|
+
}
|
|
168
|
+
keys() {
|
|
169
|
+
return this._headers.keys();
|
|
170
|
+
}
|
|
171
|
+
values() {
|
|
172
|
+
return this._headers.values();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
154
175
|
export async function nitroFetch(input, init) {
|
|
155
176
|
'worklet';
|
|
156
177
|
|
|
157
|
-
// If native implementation is not present yet, fallback to global fetch
|
|
158
|
-
const hasNative = typeof NitroFetchHybrid?.createClient === 'function';
|
|
159
|
-
if (!hasNative) {
|
|
160
|
-
// @ts-ignore: global fetch exists in RN
|
|
161
|
-
return fetch(input, init);
|
|
162
|
-
}
|
|
163
178
|
const res = await nitroFetchRaw(input, init);
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
value
|
|
169
|
-
}) => {
|
|
170
|
-
acc[key] = value;
|
|
171
|
-
return acc;
|
|
172
|
-
}, {});
|
|
173
|
-
const light = {
|
|
179
|
+
const headersObj = new NitroHeaders(res.headers);
|
|
180
|
+
const bodyBytes = res.bodyBytes;
|
|
181
|
+
const bodyString = res.bodyString;
|
|
182
|
+
const makeLight = () => ({
|
|
174
183
|
url: res.url,
|
|
175
184
|
ok: res.ok,
|
|
176
185
|
status: res.status,
|
|
177
186
|
statusText: res.statusText,
|
|
178
187
|
redirected: res.redirected,
|
|
179
188
|
headers: headersObj,
|
|
180
|
-
arrayBuffer: async () =>
|
|
181
|
-
text: async () =>
|
|
182
|
-
json: async () => JSON.parse(
|
|
183
|
-
|
|
189
|
+
arrayBuffer: async () => bodyBytes,
|
|
190
|
+
text: async () => bodyString,
|
|
191
|
+
json: async () => JSON.parse(bodyString ?? '{}'),
|
|
192
|
+
clone: () => makeLight()
|
|
193
|
+
});
|
|
194
|
+
const light = makeLight();
|
|
184
195
|
return light;
|
|
185
196
|
}
|
|
186
197
|
|
|
@@ -203,7 +214,7 @@ export async function prefetch(input, init) {
|
|
|
203
214
|
}
|
|
204
215
|
const finalHasKey = req.headers?.some(h => h.key.toLowerCase() === 'prefetchkey');
|
|
205
216
|
if (!finalHasKey) {
|
|
206
|
-
throw new Error('prefetch requires a
|
|
217
|
+
throw new Error('prefetch requires a "prefetchKey" header');
|
|
207
218
|
}
|
|
208
219
|
|
|
209
220
|
// Ensure client and call native prefetch
|
|
@@ -212,8 +223,7 @@ export async function prefetch(input, init) {
|
|
|
212
223
|
await client.prefetch(req);
|
|
213
224
|
}
|
|
214
225
|
|
|
215
|
-
// Persist a request to
|
|
216
|
-
// Stores an array of entries under the same key Android reads: "nitrofetch_autoprefetch_queue".
|
|
226
|
+
// Persist a request to storage so native can prefetch it on app start.
|
|
217
227
|
export async function prefetchOnAppStart(input, init) {
|
|
218
228
|
// Resolve request and prefetchKey
|
|
219
229
|
const req = buildNitroRequest(input, init);
|
|
@@ -238,18 +248,12 @@ export async function prefetchOnAppStart(input, init) {
|
|
|
238
248
|
headers: headersObj
|
|
239
249
|
};
|
|
240
250
|
|
|
241
|
-
// Write or append to
|
|
251
|
+
// Write or append to storage queue
|
|
242
252
|
try {
|
|
243
|
-
// Dynamically require to keep it optional for consumers
|
|
244
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
245
|
-
const {
|
|
246
|
-
MMKV
|
|
247
|
-
} = require('react-native-mmkv');
|
|
248
|
-
const storage = new MMKV(); // default instance matches Android's defaultMMKV
|
|
249
253
|
const KEY = 'nitrofetch_autoprefetch_queue';
|
|
250
254
|
let arr = [];
|
|
251
255
|
try {
|
|
252
|
-
const raw =
|
|
256
|
+
const raw = NativeStorageSingleton.getString('nitrofetch_autoprefetch_queue');
|
|
253
257
|
if (raw) arr = JSON.parse(raw);
|
|
254
258
|
if (!Array.isArray(arr)) arr = [];
|
|
255
259
|
} catch {
|
|
@@ -259,25 +263,19 @@ export async function prefetchOnAppStart(input, init) {
|
|
|
259
263
|
arr = arr.filter(e => e && e.prefetchKey !== prefetchKey);
|
|
260
264
|
}
|
|
261
265
|
arr.push(entry);
|
|
262
|
-
|
|
266
|
+
NativeStorageSingleton.setString(KEY, JSON.stringify(arr));
|
|
263
267
|
} catch (e) {
|
|
264
|
-
console.warn('
|
|
268
|
+
console.warn('Failed to persist prefetch queue', e);
|
|
265
269
|
}
|
|
266
270
|
}
|
|
267
271
|
|
|
268
|
-
// Remove one entry (by prefetchKey) from the auto-prefetch queue
|
|
272
|
+
// Remove one entry (by prefetchKey) from the auto-prefetch queue.
|
|
269
273
|
export async function removeFromAutoPrefetch(prefetchKey) {
|
|
270
|
-
// No-op on iOS
|
|
271
274
|
try {
|
|
272
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
273
|
-
const {
|
|
274
|
-
MMKV
|
|
275
|
-
} = require('react-native-mmkv');
|
|
276
|
-
const storage = new MMKV();
|
|
277
275
|
const KEY = 'nitrofetch_autoprefetch_queue';
|
|
278
276
|
let arr = [];
|
|
279
277
|
try {
|
|
280
|
-
const raw =
|
|
278
|
+
const raw = NativeStorageSingleton.getString('nitrofetch_autoprefetch_queue');
|
|
281
279
|
if (raw) arr = JSON.parse(raw);
|
|
282
280
|
if (!Array.isArray(arr)) arr = [];
|
|
283
281
|
} catch {
|
|
@@ -285,36 +283,19 @@ export async function removeFromAutoPrefetch(prefetchKey) {
|
|
|
285
283
|
}
|
|
286
284
|
const next = arr.filter(e => e && e.prefetchKey !== prefetchKey);
|
|
287
285
|
if (next.length === 0) {
|
|
288
|
-
|
|
289
|
-
storage.delete(KEY);
|
|
290
|
-
} else {
|
|
291
|
-
storage.set(KEY, JSON.stringify([]));
|
|
292
|
-
}
|
|
286
|
+
NativeStorageSingleton.removeString(KEY);
|
|
293
287
|
} else if (next.length !== arr.length) {
|
|
294
|
-
|
|
288
|
+
NativeStorageSingleton.setString(KEY, JSON.stringify(next));
|
|
295
289
|
}
|
|
296
290
|
} catch (e) {
|
|
297
|
-
console.warn('
|
|
291
|
+
console.warn('Failed to remove from prefetch queue', e);
|
|
298
292
|
}
|
|
299
293
|
}
|
|
300
294
|
|
|
301
|
-
// Remove all entries from the auto-prefetch queue
|
|
295
|
+
// Remove all entries from the auto-prefetch queue.
|
|
302
296
|
export async function removeAllFromAutoprefetch() {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const {
|
|
306
|
-
MMKV
|
|
307
|
-
} = require('react-native-mmkv');
|
|
308
|
-
const storage = new MMKV();
|
|
309
|
-
const KEY = 'nitrofetch_autoprefetch_queue';
|
|
310
|
-
if (typeof storage.delete === 'function') {
|
|
311
|
-
storage.delete(KEY);
|
|
312
|
-
} else {
|
|
313
|
-
storage.set(KEY, JSON.stringify([]));
|
|
314
|
-
}
|
|
315
|
-
} catch (e) {
|
|
316
|
-
console.warn('react-native-mmkv not available; removeAllFromAutoprefetch is a no-op', e);
|
|
317
|
-
}
|
|
297
|
+
const KEY = 'nitrofetch_autoprefetch_queue';
|
|
298
|
+
NativeStorageSingleton.setString(KEY, JSON.stringify([]));
|
|
318
299
|
}
|
|
319
300
|
|
|
320
301
|
// Optional off-thread processing using react-native-worklets-core
|
|
@@ -324,7 +305,6 @@ let WorkletsRef;
|
|
|
324
305
|
function ensureWorkletRuntime(name = 'nitro-fetch') {
|
|
325
306
|
console.log('ensuring worklet runtime');
|
|
326
307
|
try {
|
|
327
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
328
308
|
const {
|
|
329
309
|
Worklets
|
|
330
310
|
} = require('react-native-worklets-core');
|
|
@@ -339,7 +319,6 @@ function ensureWorkletRuntime(name = 'nitro-fetch') {
|
|
|
339
319
|
function getWorklets() {
|
|
340
320
|
try {
|
|
341
321
|
if (WorkletsRef) return WorkletsRef;
|
|
342
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
343
322
|
const {
|
|
344
323
|
Worklets
|
|
345
324
|
} = require('react-native-worklets-core');
|