react-native-nitro-fetch 0.1.2 → 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 -0
- package/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt +8 -27
- 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 +3 -3
- 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 +63 -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 +151 -89
- package/LICENSE +0 -20
- package/README.md +0 -144
- package/src/NitroFetch.nitro.js +0 -2
- package/src/NitroInstances.js +0 -3
- package/src/fetch.js +0 -377
- package/src/index.js +0 -8
package/android/build.gradle
CHANGED
|
@@ -1,19 +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
8
|
|
|
9
|
+
|
|
8
10
|
object AutoPrefetcher {
|
|
9
11
|
@Volatile private var initialized = false
|
|
12
|
+
private const val KEY_QUEUE = "nitrofetch_autoprefetch_queue"
|
|
13
|
+
private const val PREFS_NAME = "nitro_fetch_storage"
|
|
10
14
|
|
|
11
15
|
fun prefetchOnStart(app: Application) {
|
|
12
16
|
if (initialized) return
|
|
13
17
|
initialized = true
|
|
14
18
|
try {
|
|
15
|
-
val
|
|
16
|
-
val raw =
|
|
19
|
+
val prefs = app.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
20
|
+
val raw = prefs.getString(KEY_QUEUE, null) ?: ""
|
|
17
21
|
if (raw.isEmpty()) return
|
|
18
22
|
val arr = JSONArray(raw)
|
|
19
23
|
for (i in 0 until arr.length()) {
|
|
@@ -40,12 +44,8 @@ object AutoPrefetcher {
|
|
|
40
44
|
)
|
|
41
45
|
|
|
42
46
|
// If already pending or fresh, skip starting a new one
|
|
43
|
-
if (FetchCache.getPending(prefetchKey) != null)
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
if (FetchCache.getResultIfFresh(prefetchKey, 5_000L) != null) {
|
|
47
|
-
continue
|
|
48
|
-
}
|
|
47
|
+
if (FetchCache.getPending(prefetchKey) != null) continue
|
|
48
|
+
if (FetchCache.hasFreshResult(prefetchKey, 5_000L)) continue
|
|
49
49
|
|
|
50
50
|
val future = CompletableFuture<NitroResponse>()
|
|
51
51
|
FetchCache.setPending(prefetchKey, future)
|
|
@@ -69,23 +69,4 @@ object AutoPrefetcher {
|
|
|
69
69
|
// ignore – prefetch-on-start is best-effort
|
|
70
70
|
}
|
|
71
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
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
|
+
|
|
@@ -129,7 +129,7 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E
|
|
|
129
129
|
val bodyStr = req.bodyString
|
|
130
130
|
if ((bodyBytes != null) || !bodyStr.isNullOrEmpty()) {
|
|
131
131
|
val body: ByteArray = when {
|
|
132
|
-
bodyBytes != null -> bodyBytes.getBuffer(true).toByteArray()
|
|
132
|
+
bodyBytes != null -> ByteArray(1);//bodyBytes.getBuffer(true).toByteArray()
|
|
133
133
|
!bodyStr.isNullOrEmpty() -> bodyStr!!.toByteArray(Charsets.UTF_8)
|
|
134
134
|
else -> ByteArray(0)
|
|
135
135
|
}
|
|
@@ -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,43 +248,34 @@ 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 {
|
|
256
260
|
arr = [];
|
|
257
261
|
}
|
|
262
|
+
if (arr.some(e => e && e.prefetchKey === prefetchKey)) {
|
|
263
|
+
arr = arr.filter(e => e && e.prefetchKey !== prefetchKey);
|
|
264
|
+
}
|
|
258
265
|
arr.push(entry);
|
|
259
|
-
|
|
266
|
+
NativeStorageSingleton.setString(KEY, JSON.stringify(arr));
|
|
260
267
|
} catch (e) {
|
|
261
|
-
console.warn('
|
|
268
|
+
console.warn('Failed to persist prefetch queue', e);
|
|
262
269
|
}
|
|
263
270
|
}
|
|
264
271
|
|
|
265
|
-
// Remove one entry (by prefetchKey) from the auto-prefetch queue
|
|
272
|
+
// Remove one entry (by prefetchKey) from the auto-prefetch queue.
|
|
266
273
|
export async function removeFromAutoPrefetch(prefetchKey) {
|
|
267
|
-
// No-op on iOS
|
|
268
274
|
try {
|
|
269
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
270
|
-
const {
|
|
271
|
-
MMKV
|
|
272
|
-
} = require('react-native-mmkv');
|
|
273
|
-
const storage = new MMKV();
|
|
274
275
|
const KEY = 'nitrofetch_autoprefetch_queue';
|
|
275
276
|
let arr = [];
|
|
276
277
|
try {
|
|
277
|
-
const raw =
|
|
278
|
+
const raw = NativeStorageSingleton.getString('nitrofetch_autoprefetch_queue');
|
|
278
279
|
if (raw) arr = JSON.parse(raw);
|
|
279
280
|
if (!Array.isArray(arr)) arr = [];
|
|
280
281
|
} catch {
|
|
@@ -282,36 +283,19 @@ export async function removeFromAutoPrefetch(prefetchKey) {
|
|
|
282
283
|
}
|
|
283
284
|
const next = arr.filter(e => e && e.prefetchKey !== prefetchKey);
|
|
284
285
|
if (next.length === 0) {
|
|
285
|
-
|
|
286
|
-
storage.delete(KEY);
|
|
287
|
-
} else {
|
|
288
|
-
storage.set(KEY, JSON.stringify([]));
|
|
289
|
-
}
|
|
286
|
+
NativeStorageSingleton.removeString(KEY);
|
|
290
287
|
} else if (next.length !== arr.length) {
|
|
291
|
-
|
|
288
|
+
NativeStorageSingleton.setString(KEY, JSON.stringify(next));
|
|
292
289
|
}
|
|
293
290
|
} catch (e) {
|
|
294
|
-
console.warn('
|
|
291
|
+
console.warn('Failed to remove from prefetch queue', e);
|
|
295
292
|
}
|
|
296
293
|
}
|
|
297
294
|
|
|
298
|
-
// Remove all entries from the auto-prefetch queue
|
|
295
|
+
// Remove all entries from the auto-prefetch queue.
|
|
299
296
|
export async function removeAllFromAutoprefetch() {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const {
|
|
303
|
-
MMKV
|
|
304
|
-
} = require('react-native-mmkv');
|
|
305
|
-
const storage = new MMKV();
|
|
306
|
-
const KEY = 'nitrofetch_autoprefetch_queue';
|
|
307
|
-
if (typeof storage.delete === 'function') {
|
|
308
|
-
storage.delete(KEY);
|
|
309
|
-
} else {
|
|
310
|
-
storage.set(KEY, JSON.stringify([]));
|
|
311
|
-
}
|
|
312
|
-
} catch (e) {
|
|
313
|
-
console.warn('react-native-mmkv not available; removeAllFromAutoprefetch is a no-op', e);
|
|
314
|
-
}
|
|
297
|
+
const KEY = 'nitrofetch_autoprefetch_queue';
|
|
298
|
+
NativeStorageSingleton.setString(KEY, JSON.stringify([]));
|
|
315
299
|
}
|
|
316
300
|
|
|
317
301
|
// Optional off-thread processing using react-native-worklets-core
|
|
@@ -321,7 +305,6 @@ let WorkletsRef;
|
|
|
321
305
|
function ensureWorkletRuntime(name = 'nitro-fetch') {
|
|
322
306
|
console.log('ensuring worklet runtime');
|
|
323
307
|
try {
|
|
324
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
325
308
|
const {
|
|
326
309
|
Worklets
|
|
327
310
|
} = require('react-native-worklets-core');
|
|
@@ -336,7 +319,6 @@ function ensureWorkletRuntime(name = 'nitro-fetch') {
|
|
|
336
319
|
function getWorklets() {
|
|
337
320
|
try {
|
|
338
321
|
if (WorkletsRef) return WorkletsRef;
|
|
339
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
340
322
|
const {
|
|
341
323
|
Worklets
|
|
342
324
|
} = require('react-native-worklets-core');
|