@pigeonmal/react-native-nitro-fetch 0.1.6

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 (104) hide show
  1. package/NitroFetch.podspec +30 -0
  2. package/android/CMakeLists.txt +70 -0
  3. package/android/build.gradle +130 -0
  4. package/android/gradle.properties +5 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  7. package/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt +72 -0
  8. package/android/src/main/java/com/margelo/nitro/nitrofetch/FetchCache.kt +58 -0
  9. package/android/src/main/java/com/margelo/nitro/nitrofetch/NativeStorage.kt +102 -0
  10. package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetch.kt +94 -0
  11. package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt +331 -0
  12. package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchPackage.kt +22 -0
  13. package/ios/FetchCache.swift +56 -0
  14. package/ios/NativeStorage.swift +61 -0
  15. package/ios/NitroAutoPrefetcher.swift +45 -0
  16. package/ios/NitroBootstrap.mm +27 -0
  17. package/ios/NitroFetch.swift +9 -0
  18. package/ios/NitroFetchClient.swift +230 -0
  19. package/lib/module/NitroFetch.nitro.js +4 -0
  20. package/lib/module/NitroFetch.nitro.js.map +1 -0
  21. package/lib/module/NitroInstances.js +8 -0
  22. package/lib/module/NitroInstances.js.map +1 -0
  23. package/lib/module/fetch.js +522 -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/module/type.js +2 -0
  29. package/lib/module/type.js.map +1 -0
  30. package/lib/typescript/package.json +1 -0
  31. package/lib/typescript/src/NitroFetch.nitro.d.ts +48 -0
  32. package/lib/typescript/src/NitroFetch.nitro.d.ts.map +1 -0
  33. package/lib/typescript/src/NitroInstances.d.ts +5 -0
  34. package/lib/typescript/src/NitroInstances.d.ts.map +1 -0
  35. package/lib/typescript/src/fetch.d.ts +28 -0
  36. package/lib/typescript/src/fetch.d.ts.map +1 -0
  37. package/lib/typescript/src/index.d.ts +6 -0
  38. package/lib/typescript/src/index.d.ts.map +1 -0
  39. package/lib/typescript/src/type.d.ts +4 -0
  40. package/lib/typescript/src/type.d.ts.map +1 -0
  41. package/nitro.json +25 -0
  42. package/nitrogen/generated/android/c++/JHybridNativeStorageSpec.cpp +54 -0
  43. package/nitrogen/generated/android/c++/JHybridNativeStorageSpec.hpp +66 -0
  44. package/nitrogen/generated/android/c++/JHybridNitroFetchClientSpec.cpp +96 -0
  45. package/nitrogen/generated/android/c++/JHybridNitroFetchClientSpec.hpp +66 -0
  46. package/nitrogen/generated/android/c++/JHybridNitroFetchSpec.cpp +49 -0
  47. package/nitrogen/generated/android/c++/JHybridNitroFetchSpec.hpp +64 -0
  48. package/nitrogen/generated/android/c++/JNitroHeader.hpp +57 -0
  49. package/nitrogen/generated/android/c++/JNitroRequest.hpp +100 -0
  50. package/nitrogen/generated/android/c++/JNitroRequestMethod.hpp +74 -0
  51. package/nitrogen/generated/android/c++/JNitroResponse.hpp +102 -0
  52. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/HybridNativeStorageSpec.kt +60 -0
  53. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/HybridNitroFetchClientSpec.kt +60 -0
  54. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/HybridNitroFetchSpec.kt +52 -0
  55. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroHeader.kt +32 -0
  56. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroRequest.kt +47 -0
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroRequestMethod.kt +26 -0
  58. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroResponse.kt +50 -0
  59. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/nitrofetchOnLoad.kt +35 -0
  60. package/nitrogen/generated/android/nitrofetch+autolinking.cmake +85 -0
  61. package/nitrogen/generated/android/nitrofetch+autolinking.gradle +27 -0
  62. package/nitrogen/generated/android/nitrofetchOnLoad.cpp +64 -0
  63. package/nitrogen/generated/android/nitrofetchOnLoad.hpp +25 -0
  64. package/nitrogen/generated/ios/NitroFetch+autolinking.rb +60 -0
  65. package/nitrogen/generated/ios/NitroFetch-Swift-Cxx-Bridge.cpp +90 -0
  66. package/nitrogen/generated/ios/NitroFetch-Swift-Cxx-Bridge.hpp +321 -0
  67. package/nitrogen/generated/ios/NitroFetch-Swift-Cxx-Umbrella.hpp +69 -0
  68. package/nitrogen/generated/ios/NitroFetchAutolinking.mm +49 -0
  69. package/nitrogen/generated/ios/NitroFetchAutolinking.swift +55 -0
  70. package/nitrogen/generated/ios/c++/HybridNativeStorageSpecSwift.cpp +11 -0
  71. package/nitrogen/generated/ios/c++/HybridNativeStorageSpecSwift.hpp +85 -0
  72. package/nitrogen/generated/ios/c++/HybridNitroFetchClientSpecSwift.cpp +11 -0
  73. package/nitrogen/generated/ios/c++/HybridNitroFetchClientSpecSwift.hpp +103 -0
  74. package/nitrogen/generated/ios/c++/HybridNitroFetchSpecSwift.cpp +11 -0
  75. package/nitrogen/generated/ios/c++/HybridNitroFetchSpecSwift.hpp +75 -0
  76. package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
  77. package/nitrogen/generated/ios/swift/Func_void_NitroResponse.swift +47 -0
  78. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
  79. package/nitrogen/generated/ios/swift/HybridNativeStorageSpec.swift +51 -0
  80. package/nitrogen/generated/ios/swift/HybridNativeStorageSpec_cxx.swift +145 -0
  81. package/nitrogen/generated/ios/swift/HybridNitroFetchClientSpec.swift +51 -0
  82. package/nitrogen/generated/ios/swift/HybridNitroFetchClientSpec_cxx.swift +161 -0
  83. package/nitrogen/generated/ios/swift/HybridNitroFetchSpec.swift +49 -0
  84. package/nitrogen/generated/ios/swift/HybridNitroFetchSpec_cxx.swift +126 -0
  85. package/nitrogen/generated/ios/swift/NitroHeader.swift +46 -0
  86. package/nitrogen/generated/ios/swift/NitroRequest.swift +206 -0
  87. package/nitrogen/generated/ios/swift/NitroRequestMethod.swift +60 -0
  88. package/nitrogen/generated/ios/swift/NitroResponse.swift +162 -0
  89. package/nitrogen/generated/shared/c++/HybridNativeStorageSpec.cpp +23 -0
  90. package/nitrogen/generated/shared/c++/HybridNativeStorageSpec.hpp +64 -0
  91. package/nitrogen/generated/shared/c++/HybridNitroFetchClientSpec.cpp +23 -0
  92. package/nitrogen/generated/shared/c++/HybridNitroFetchClientSpec.hpp +69 -0
  93. package/nitrogen/generated/shared/c++/HybridNitroFetchSpec.cpp +21 -0
  94. package/nitrogen/generated/shared/c++/HybridNitroFetchSpec.hpp +64 -0
  95. package/nitrogen/generated/shared/c++/NitroHeader.hpp +71 -0
  96. package/nitrogen/generated/shared/c++/NitroRequest.hpp +98 -0
  97. package/nitrogen/generated/shared/c++/NitroRequestMethod.hpp +96 -0
  98. package/nitrogen/generated/shared/c++/NitroResponse.hpp +99 -0
  99. package/package.json +162 -0
  100. package/src/NitroFetch.nitro.ts +67 -0
  101. package/src/NitroInstances.ts +14 -0
  102. package/src/fetch.ts +603 -0
  103. package/src/index.tsx +17 -0
  104. package/src/type.ts +3 -0
@@ -0,0 +1,331 @@
1
+ package com.margelo.nitro.nitrofetch
2
+
3
+ import android.util.Log
4
+ import com.facebook.proguard.annotations.DoNotStrip
5
+ import com.margelo.nitro.core.ArrayBuffer
6
+ import com.margelo.nitro.core.Promise
7
+ import org.chromium.net.CronetEngine
8
+ import org.chromium.net.CronetException
9
+ import org.chromium.net.UrlRequest
10
+ import org.chromium.net.UrlResponseInfo
11
+ import java.io.InterruptedIOException
12
+ import java.nio.ByteBuffer
13
+ import java.util.concurrent.Executor
14
+ import java.util.concurrent.TimeUnit
15
+ import java.util.concurrent.atomic.AtomicBoolean
16
+ import okio.AsyncTimeout
17
+
18
+ fun ByteBuffer.toByteArray(): ByteArray {
19
+ // duplicate to avoid modifying the original buffer's position
20
+ val dup = this.duplicate()
21
+ dup.clear() // sets position=0, limit=capacity
22
+ val arr = ByteArray(dup.remaining())
23
+ dup.get(arr)
24
+ return arr
25
+ }
26
+
27
+ @DoNotStrip
28
+ class NitroFetchClient(private val engine: CronetEngine, private val executor: Executor) : HybridNitroFetchClientSpec() {
29
+
30
+ private fun findPrefetchKey(req: NitroRequest): String? {
31
+ val h = req.headers ?: return null
32
+ for (pair in h) {
33
+ if (pair.key.equals("prefetchKey", ignoreCase = true)) return pair.value
34
+ }
35
+ return null
36
+ }
37
+
38
+ companion object {
39
+ @JvmStatic
40
+ fun fetch(
41
+ req: NitroRequest,
42
+ onSuccess: (NitroResponse) -> Unit,
43
+ onFail: (Throwable) -> Unit
44
+ ) {
45
+ try {
46
+ val engine = NitroFetch.getEngine()
47
+ val executor = NitroFetch.ioExecutor
48
+ startCronet(engine, executor, req, onSuccess, onFail)
49
+ } catch (t: Throwable) {
50
+ onFail(t)
51
+ }
52
+ }
53
+
54
+ private fun startCronet(
55
+ engine: CronetEngine,
56
+ executor: Executor,
57
+ req: NitroRequest,
58
+ onSuccess: (NitroResponse) -> Unit,
59
+ onFail: (Throwable) -> Unit
60
+ ) {
61
+ val url = req.url
62
+ val completed = AtomicBoolean(false)
63
+ var urlRequest: UrlRequest? = null
64
+
65
+ // Create timeout handler
66
+ val timeout = object : AsyncTimeout() {
67
+ override fun timedOut() {
68
+ if (completed.compareAndSet(false, true)) {
69
+ urlRequest?.cancel()
70
+ }
71
+ }
72
+ }
73
+
74
+ // Configure timeout from request
75
+ req.timeoutMs?.let { timeoutMs ->
76
+ if (timeoutMs > 0) {
77
+ timeout.timeout(timeoutMs.toLong(), TimeUnit.MILLISECONDS)
78
+ }
79
+ }
80
+
81
+ val callback = object : UrlRequest.Callback() {
82
+ private val buffer = ByteBuffer.allocateDirect(16 * 1024)
83
+ private val out = java.io.ByteArrayOutputStream()
84
+
85
+ override fun onRedirectReceived(request: UrlRequest, info: UrlResponseInfo, newLocationUrl: String) {
86
+ request.followRedirect()
87
+ }
88
+
89
+ override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) {
90
+ buffer.clear()
91
+ request.read(buffer)
92
+ }
93
+
94
+ override fun onReadCompleted(request: UrlRequest, info: UrlResponseInfo, byteBuffer: ByteBuffer) {
95
+ byteBuffer.flip()
96
+ val bytes = ByteArray(byteBuffer.remaining())
97
+ byteBuffer.get(bytes)
98
+ out.write(bytes)
99
+ byteBuffer.clear()
100
+ request.read(byteBuffer)
101
+ }
102
+
103
+ override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
104
+ if (!completed.compareAndSet(false, true)) return
105
+ timeout.exit()
106
+
107
+ try {
108
+ val headersArr: Array<NitroHeader> =
109
+ info.allHeadersAsList.map { NitroHeader(it.key, it.value) }.toTypedArray()
110
+ val status = info.httpStatusCode
111
+ val bytes = out.toByteArray()
112
+ val contentType = info.allHeaders["Content-Type"] ?: info.allHeaders["content-type"]
113
+ val charset = run {
114
+ val ct = contentType ?: ""
115
+ val m = Regex("charset=([A-Za-z0-9_\\-:.]+)", RegexOption.IGNORE_CASE).find(ct.toString())
116
+ try {
117
+ if (m != null) java.nio.charset.Charset.forName(m.groupValues[1]) else Charsets.UTF_8
118
+ } catch (_: Throwable) {
119
+ Charsets.UTF_8
120
+ }
121
+ }
122
+ val bodyStr = try {
123
+ String(bytes, charset)
124
+ } catch (_: Throwable) {
125
+ String(bytes, Charsets.UTF_8)
126
+ }
127
+
128
+ val res = NitroResponse(
129
+ url = info.url,
130
+ status = status.toDouble(),
131
+ statusText = info.httpStatusText ?: "",
132
+ ok = status in 200..299,
133
+ redirected = info.url != url,
134
+ headers = headersArr,
135
+ bodyString = bodyStr,
136
+ bodyBytes = null
137
+ )
138
+ onSuccess(res)
139
+ } catch (t: Throwable) {
140
+ onFail(t)
141
+ }
142
+ }
143
+
144
+ override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: CronetException) {
145
+ if (!completed.compareAndSet(false, true)) return
146
+ val timedOut = timeout.exit()
147
+
148
+ val exception = if (timedOut) {
149
+ InterruptedIOException("Request timeout after ${req.timeoutMs}ms").apply {
150
+ initCause(error)
151
+ }
152
+ } else {
153
+ RuntimeException("Cronet failed: ${error.message}", error)
154
+ }
155
+ onFail(exception)
156
+ }
157
+
158
+ override fun onCanceled(request: UrlRequest, info: UrlResponseInfo?) {
159
+ if (!completed.compareAndSet(false, true)) return
160
+ val timedOut = timeout.exit()
161
+
162
+ val exception = if (timedOut) {
163
+ InterruptedIOException("Request timeout after ${req.timeoutMs}ms")
164
+ } else {
165
+ RuntimeException("Cronet canceled")
166
+ }
167
+ onFail(exception)
168
+ }
169
+ }
170
+
171
+ val builder = engine.newUrlRequestBuilder(url, callback, executor)
172
+ val method = req.method?.name ?: "GET"
173
+ builder.setHttpMethod(method)
174
+ req.headers?.forEach { (k, v) -> builder.addHeader(k, v) }
175
+
176
+ val bodyBytes = req.bodyBytes
177
+ val bodyStr = req.bodyString
178
+ if ((bodyBytes != null) || !bodyStr.isNullOrEmpty()) {
179
+ val body: ByteArray = when {
180
+ bodyBytes != null -> ByteArray(1);//bodyBytes.getBuffer(true).toByteArray()
181
+ !bodyStr.isNullOrEmpty() -> bodyStr!!.toByteArray(Charsets.UTF_8)
182
+ else -> ByteArray(0)
183
+ }
184
+ val provider = object : org.chromium.net.UploadDataProvider() {
185
+ private var pos = 0
186
+ override fun getLength(): Long = body.size.toLong()
187
+ override fun read(uploadDataSink: org.chromium.net.UploadDataSink, byteBuffer: ByteBuffer) {
188
+ val remaining = body.size - pos
189
+ val toWrite = minOf(byteBuffer.remaining(), remaining)
190
+ byteBuffer.put(body, pos, toWrite)
191
+ pos += toWrite
192
+ uploadDataSink.onReadSucceeded(false)
193
+ }
194
+ override fun rewind(uploadDataSink: org.chromium.net.UploadDataSink) {
195
+ pos = 0
196
+ uploadDataSink.onRewindSucceeded()
197
+ }
198
+ }
199
+ builder.setUploadDataProvider(provider, executor)
200
+ }
201
+
202
+ urlRequest = builder.build()
203
+ timeout.enter()
204
+ urlRequest.start()
205
+ }
206
+ }
207
+
208
+ // Helper function to add prefetch header to response (reused by both sync/async)
209
+ private fun withPrefetchedHeader(res: NitroResponse): NitroResponse {
210
+ val newHeaders = (res.headers?.toMutableList() ?: mutableListOf())
211
+ newHeaders.add(NitroHeader("nitroPrefetched", "true"))
212
+ return NitroResponse(
213
+ url = res.url,
214
+ status = res.status,
215
+ statusText = res.statusText,
216
+ ok = res.ok,
217
+ redirected = res.redirected,
218
+ headers = newHeaders.toTypedArray(),
219
+ bodyString = res.bodyString,
220
+ bodyBytes = res.bodyBytes
221
+ )
222
+ }
223
+
224
+ override fun requestSync(req: NitroRequest): NitroResponse {
225
+ val key = findPrefetchKey(req)
226
+ if (key != null) {
227
+ FetchCache.getPending(key)?.let { fut ->
228
+ return try {
229
+ withPrefetchedHeader(fut.get()) // blocks until complete
230
+ } catch (e: Exception) {
231
+ throw e.cause ?: e
232
+ }
233
+ }
234
+ FetchCache.getResultIfFresh(key, 5_000L)?.let { cached ->
235
+ return withPrefetchedHeader(cached)
236
+ }
237
+ }
238
+ val latch = java.util.concurrent.CountDownLatch(1)
239
+ var result: NitroResponse? = null
240
+ var error: Throwable? = null
241
+
242
+ fetch(
243
+ req,
244
+ onSuccess = {
245
+ result = it
246
+ latch.countDown()
247
+ },
248
+ onFail = {
249
+ error = it
250
+ latch.countDown()
251
+ }
252
+ )
253
+ latch.await()
254
+ error?.let { throw it }
255
+ return result!!
256
+ }
257
+
258
+ override fun request(req: NitroRequest): Promise<NitroResponse> {
259
+ val promise = Promise<NitroResponse>()
260
+ // Try to serve from prefetch cache/pending first
261
+ val key = findPrefetchKey(req)
262
+ if (key != null) {
263
+ // If a prefetch is currently pending, wait for it
264
+ FetchCache.getPending(key)?.let { fut ->
265
+ fut.whenComplete { res, err ->
266
+ if (err != null) {
267
+ promise.reject(err)
268
+ } else if (res != null) {
269
+ promise.resolve(withPrefetchedHeader(res))
270
+ } else {
271
+ promise.reject(IllegalStateException("Prefetch pending returned null result"))
272
+ }
273
+ }
274
+ return promise
275
+ }
276
+ // If a fresh prefetched result exists (<=5s old), return it immediately
277
+ FetchCache.getResultIfFresh(key, 5_000L)?.let { cached ->
278
+ promise.resolve(withPrefetchedHeader(cached))
279
+ return promise
280
+ }
281
+ }
282
+
283
+ fetch(
284
+ req,
285
+ onSuccess = { promise.resolve(it) },
286
+ onFail = { promise.reject(it) }
287
+ )
288
+ return promise
289
+ }
290
+
291
+ override fun prefetch(req: NitroRequest): Promise<Unit> {
292
+ val promise = Promise<Unit>()
293
+ val key = findPrefetchKey(req)
294
+ if (key.isNullOrEmpty()) {
295
+ promise.reject(IllegalArgumentException("prefetch: missing 'prefetchKey' header"))
296
+ return promise
297
+ }
298
+ // If already have a fresh result, resolve immediately (NON-DESTRUCTIVE CHECK)
299
+ if (FetchCache.hasFreshResult(key, 5_000L)) {
300
+ promise.resolve(Unit)
301
+ return promise
302
+ }
303
+ // If already pending, resolve when it's done
304
+ FetchCache.getPending(key)?.let { fut ->
305
+ fut.whenComplete { _, err -> if (err != null) promise.reject(err) else promise.resolve(Unit) }
306
+ return promise
307
+ }
308
+ // Start new prefetch
309
+ val future = java.util.concurrent.CompletableFuture<NitroResponse>()
310
+ FetchCache.setPending(key, future)
311
+ fetch(
312
+ req,
313
+ onSuccess = { res ->
314
+ try {
315
+ FetchCache.complete(key, res)
316
+ promise.resolve(Unit)
317
+ } catch (t: Throwable) {
318
+ FetchCache.completeExceptionally(key, t)
319
+ promise.reject(t)
320
+ }
321
+ },
322
+ onFail = { err ->
323
+ FetchCache.completeExceptionally(key, err)
324
+ promise.reject(err)
325
+ }
326
+ )
327
+ return promise
328
+ }
329
+
330
+
331
+ }
@@ -0,0 +1,22 @@
1
+ package com.margelo.nitro.nitrofetch
2
+
3
+ import com.facebook.react.TurboReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfoProvider
7
+
8
+ class NitroFetchPackage : TurboReactPackage() {
9
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
10
+ return null
11
+ }
12
+
13
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
14
+ return ReactModuleInfoProvider { HashMap() }
15
+ }
16
+
17
+ companion object {
18
+ init {
19
+ System.loadLibrary("nitrofetch")
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,56 @@
1
+ import Foundation
2
+
3
+ final class FetchCache {
4
+ struct CachedEntry {
5
+ let response: NitroResponse
6
+ let timestampMs: Int64
7
+ }
8
+
9
+ private static let queue = DispatchQueue(label: "nitrofetch.cache", attributes: .concurrent)
10
+ private static var pending: [String: [(Result<NitroResponse, Error>) -> Void]] = [:]
11
+ private static var results: [String: CachedEntry] = [:]
12
+
13
+ static func getPending(_ key: String) -> Bool {
14
+ var has = false
15
+ queue.sync { has = pending[key] != nil }
16
+ return has
17
+ }
18
+
19
+ static func addPending(_ key: String, completion: @escaping (Result<NitroResponse, Error>) -> Void) {
20
+ queue.async(flags: .barrier) {
21
+ var arr = pending[key] ?? []
22
+ arr.append(completion)
23
+ pending[key] = arr
24
+ }
25
+ }
26
+
27
+ static func complete(_ key: String, with result: Result<NitroResponse, Error>) {
28
+ var callbacks: [(Result<NitroResponse, Error>) -> Void] = []
29
+ queue.sync {
30
+ callbacks = pending[key] ?? []
31
+ }
32
+ queue.async(flags: .barrier) {
33
+ pending.removeValue(forKey: key)
34
+ if case let .success(resp) = result {
35
+ results[key] = CachedEntry(response: resp, timestampMs: Int64(Date().timeIntervalSince1970 * 1000))
36
+ }
37
+ }
38
+ callbacks.forEach { $0(result) }
39
+ }
40
+
41
+ static func getResultIfFresh(_ key: String, maxAgeMs: Int64) -> NitroResponse? {
42
+ var out: NitroResponse?
43
+ queue.sync {
44
+ if let entry = results[key] {
45
+ let age = Int64(Date().timeIntervalSince1970 * 1000) - entry.timestampMs
46
+ if age <= maxAgeMs {
47
+ out = entry.response
48
+ } else {
49
+ results.removeValue(forKey: key)
50
+ }
51
+ }
52
+ }
53
+ return out
54
+ }
55
+ }
56
+
@@ -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
+ }
@@ -0,0 +1,45 @@
1
+ import Foundation
2
+
3
+ @objc(NitroAutoPrefetcher)
4
+ public final class NitroAutoPrefetcher: NSObject {
5
+ private static var initialized = false
6
+ private static let queueKey = "nitrofetch_autoprefetch_queue"
7
+ private static let suiteName = "nitro_fetch_storage"
8
+
9
+ @objc
10
+ public static func prefetchOnStart() {
11
+ if initialized { return }
12
+ initialized = true
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 }
17
+ guard let data = raw.data(using: .utf8) else { return }
18
+ guard let arr = try? JSONSerialization.jsonObject(with: data, options: []) as? [Any] else { return }
19
+
20
+ for item in arr {
21
+ guard let obj = item as? [String: Any] else { continue }
22
+ guard let url = obj["url"] as? String, !url.isEmpty else { continue }
23
+ guard let prefetchKey = obj["prefetchKey"] as? String, !prefetchKey.isEmpty else { continue }
24
+ let headersDict = (obj["headers"] as? [String: Any]) ?? [:]
25
+ var headers: [NitroHeader] = headersDict.map { (k, v) in NitroHeader(key: String(describing: k), value: String(describing: v)) }
26
+ headers.append(NitroHeader(key: "prefetchKey", value: prefetchKey))
27
+ let req = NitroRequest(url: url,
28
+ method: nil,
29
+ headers: headers,
30
+ bodyString: nil,
31
+ bodyBytes: nil,
32
+ timeoutMs: nil,
33
+ followRedirects: true)
34
+ Task {
35
+ do { try await NitroFetchClient.prefetchStatic(req) } catch { /* ignore – best effort */ }
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ // Expose a C-ABI symbol the ObjC++ file can call
42
+ @_cdecl("NitroStartSwift")
43
+ public func NitroStartSwift() {
44
+ NitroAutoPrefetcher.prefetchOnStart()
45
+ }
@@ -0,0 +1,27 @@
1
+ #import <Foundation/Foundation.h>
2
+ #if __has_include(<UIKit/UIKit.h>)
3
+ #import <UIKit/UIKit.h>
4
+ #endif
5
+
6
+ // No need to import the Swift header if you don’t want to.
7
+ // Just declare the C entry point:
8
+ extern "C" void NitroStartSwift(void);
9
+
10
+ @interface NitroFetchBootstrapper : NSObject @end
11
+ @implementation NitroFetchBootstrapper
12
+
13
+ + (void)load {
14
+ #if __has_include(<UIKit/UIKit.h>)
15
+ if (NSClassFromString(@"UIApplication")) {
16
+ [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification
17
+ object:nil queue:nil
18
+ usingBlock:^(__unused NSNotification *note) {
19
+ NitroStartSwift(); // <-- call the C symbol
20
+ }];
21
+ dispatch_async(dispatch_get_main_queue(), ^{
22
+ NitroStartSwift();
23
+ });
24
+ }
25
+ #endif
26
+ }
27
+ @end
@@ -0,0 +1,9 @@
1
+ import Foundation
2
+
3
+ final class NitroFetch: HybridNitroFetchSpec {
4
+ func createClient() throws -> (any HybridNitroFetchClientSpec) {
5
+ return NitroFetchClient()
6
+ }
7
+
8
+ }
9
+