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.
Files changed (45) hide show
  1. package/android/build.gradle +1 -0
  2. package/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt +8 -27
  3. package/android/src/main/java/com/margelo/nitro/nitrofetch/FetchCache.kt +11 -1
  4. package/android/src/main/java/com/margelo/nitro/nitrofetch/NativeStorage.kt +102 -0
  5. package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt +3 -3
  6. package/ios/NativeStorage.swift +61 -0
  7. package/ios/NitroAutoPrefetcher.swift +5 -41
  8. package/lib/module/NitroFetch.nitro.js +0 -3
  9. package/lib/module/NitroFetch.nitro.js.map +1 -1
  10. package/lib/module/NitroInstances.js +1 -0
  11. package/lib/module/NitroInstances.js.map +1 -1
  12. package/lib/module/fetch.js +63 -81
  13. package/lib/module/fetch.js.map +1 -1
  14. package/lib/typescript/src/NitroFetch.nitro.d.ts +8 -0
  15. package/lib/typescript/src/NitroFetch.nitro.d.ts.map +1 -1
  16. package/lib/typescript/src/NitroInstances.d.ts +2 -1
  17. package/lib/typescript/src/NitroInstances.d.ts.map +1 -1
  18. package/lib/typescript/src/fetch.d.ts.map +1 -1
  19. package/nitro.json +4 -0
  20. package/nitrogen/generated/android/c++/JHybridNativeStorageSpec.cpp +54 -0
  21. package/nitrogen/generated/android/c++/JHybridNativeStorageSpec.hpp +66 -0
  22. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/HybridNativeStorageSpec.kt +60 -0
  23. package/nitrogen/generated/android/nitrofetch+autolinking.cmake +9 -4
  24. package/nitrogen/generated/android/nitrofetchOnLoad.cpp +10 -0
  25. package/nitrogen/generated/ios/NitroFetch-Swift-Cxx-Bridge.cpp +17 -0
  26. package/nitrogen/generated/ios/NitroFetch-Swift-Cxx-Bridge.hpp +35 -0
  27. package/nitrogen/generated/ios/NitroFetch-Swift-Cxx-Umbrella.hpp +5 -0
  28. package/nitrogen/generated/ios/NitroFetchAutolinking.mm +8 -0
  29. package/nitrogen/generated/ios/NitroFetchAutolinking.swift +15 -0
  30. package/nitrogen/generated/ios/c++/HybridNativeStorageSpecSwift.cpp +11 -0
  31. package/nitrogen/generated/ios/c++/HybridNativeStorageSpecSwift.hpp +85 -0
  32. package/nitrogen/generated/ios/swift/HybridNativeStorageSpec.swift +51 -0
  33. package/nitrogen/generated/ios/swift/HybridNativeStorageSpec_cxx.swift +145 -0
  34. package/nitrogen/generated/shared/c++/HybridNativeStorageSpec.cpp +23 -0
  35. package/nitrogen/generated/shared/c++/HybridNativeStorageSpec.hpp +64 -0
  36. package/package.json +8 -24
  37. package/src/NitroFetch.nitro.ts +12 -5
  38. package/src/NitroInstances.ts +6 -2
  39. package/src/fetch.ts +151 -89
  40. package/LICENSE +0 -20
  41. package/README.md +0 -144
  42. package/src/NitroFetch.nitro.js +0 -2
  43. package/src/NitroInstances.js +0 -3
  44. package/src/fetch.js +0 -377
  45. package/src/index.js +0 -8
@@ -121,6 +121,7 @@ dependencies {
121
121
  // Cronet
122
122
  api "org.chromium.net:cronet-embedded:${cronetVersion}"
123
123
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"
124
+
124
125
  }
125
126
 
126
127
  configurations {
@@ -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 mmkv = getMMKV(app) ?: return
16
- val raw = invokeMMKVDecodeString(mmkv, KEY_QUEUE) ?: return
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
- continue
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.getResultIfFresh(key, 5_000L)?.let {
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
- guard let raw = readMMKVString(forKey: queueKey), !raw.isEmpty else { return }
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,7 +1,4 @@
1
1
  "use strict";
2
2
 
3
- // Minimal request/response types to model WHATWG fetch without streaming.
4
-
5
- ;
6
3
  export {};
7
4
  //# sourceMappingURL=NitroFetch.nitro.js.map
@@ -1 +1 @@
1
- {"version":3,"names":[],"sourceRoot":"../../src","sources":["NitroFetch.nitro.ts"],"mappings":";;AAEA;;AAaC;AAAC","ignoreList":[]}
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;AAGzD;AACA,OAAO,MAAMC,UAA0B,GACrCD,YAAY,CAACE,kBAAkB,CAAiB,YAAY,CAAC","ignoreList":[]}
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":[]}
@@ -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
- //normalized?.bodyBytes,
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
- // Fallback lightweight Response-like object (minimal methods)
166
- const headersObj = res.headers.reduce((acc, {
167
- key,
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 () => res.bodyBytes,
181
- text: async () => res.bodyString,
182
- json: async () => JSON.parse(res.bodyString ?? '{}')
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 \"prefetchKey\" header');
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 MMKV so native can prefetch it on app start.
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 MMKV queue
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 = storage.getString(KEY);
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
- storage.set(KEY, JSON.stringify(arr));
266
+ NativeStorageSingleton.setString(KEY, JSON.stringify(arr));
260
267
  } catch (e) {
261
- console.warn('react-native-mmkv not available; prefetchOnAppStart is a no-op', e);
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 in MMKV.
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 = storage.getString(KEY);
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
- if (typeof storage.delete === 'function') {
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
- storage.set(KEY, JSON.stringify(next));
288
+ NativeStorageSingleton.setString(KEY, JSON.stringify(next));
292
289
  }
293
290
  } catch (e) {
294
- console.warn('react-native-mmkv not available; removeFromAutoPrefetch is a no-op', e);
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 in MMKV.
295
+ // Remove all entries from the auto-prefetch queue.
299
296
  export async function removeAllFromAutoprefetch() {
300
- try {
301
- // eslint-disable-next-line @typescript-eslint/no-var-requires
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');