react-native-nitro-fetch 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/LICENSE +20 -0
  2. package/NitroFetch.podspec +30 -0
  3. package/README.md +134 -0
  4. package/android/CMakeLists.txt +70 -0
  5. package/android/build.gradle +131 -0
  6. package/android/gradle.properties +5 -0
  7. package/android/src/main/AndroidManifest.xml +2 -0
  8. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  9. package/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt +91 -0
  10. package/android/src/main/java/com/margelo/nitro/nitrofetch/FetchCache.kt +48 -0
  11. package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetch.kt +94 -0
  12. package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt +256 -0
  13. package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchPackage.kt +22 -0
  14. package/ios/FetchCache.swift +56 -0
  15. package/ios/NitroAutoPrefetcher.swift +81 -0
  16. package/ios/NitroBootstrap.mm +27 -0
  17. package/ios/NitroFetch.swift +9 -0
  18. package/ios/NitroFetchClient.swift +205 -0
  19. package/lib/module/NitroFetch.nitro.js +7 -0
  20. package/lib/module/NitroFetch.nitro.js.map +1 -0
  21. package/lib/module/NitroInstances.js +6 -0
  22. package/lib/module/NitroInstances.js.map +1 -0
  23. package/lib/module/fetch.js +441 -0
  24. package/lib/module/fetch.js.map +1 -0
  25. package/lib/module/index.js +12 -0
  26. package/lib/module/index.js.map +1 -0
  27. package/lib/module/package.json +1 -0
  28. package/lib/typescript/package.json +1 -0
  29. package/lib/typescript/src/NitroFetch.nitro.d.ts +39 -0
  30. package/lib/typescript/src/NitroFetch.nitro.d.ts.map +1 -0
  31. package/lib/typescript/src/NitroInstances.d.ts +3 -0
  32. package/lib/typescript/src/NitroInstances.d.ts.map +1 -0
  33. package/lib/typescript/src/fetch.d.ts +26 -0
  34. package/lib/typescript/src/fetch.d.ts.map +1 -0
  35. package/lib/typescript/src/index.d.ts +6 -0
  36. package/lib/typescript/src/index.d.ts.map +1 -0
  37. package/nitro.json +21 -0
  38. package/nitrogen/generated/android/c++/JHybridNitroFetchClientSpec.cpp +96 -0
  39. package/nitrogen/generated/android/c++/JHybridNitroFetchClientSpec.hpp +65 -0
  40. package/nitrogen/generated/android/c++/JHybridNitroFetchSpec.cpp +49 -0
  41. package/nitrogen/generated/android/c++/JHybridNitroFetchSpec.hpp +64 -0
  42. package/nitrogen/generated/android/c++/JNitroHeader.hpp +57 -0
  43. package/nitrogen/generated/android/c++/JNitroRequest.hpp +103 -0
  44. package/nitrogen/generated/android/c++/JNitroRequestMethod.hpp +74 -0
  45. package/nitrogen/generated/android/c++/JNitroResponse.hpp +105 -0
  46. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/HybridNitroFetchClientSpec.kt +56 -0
  47. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/HybridNitroFetchSpec.kt +52 -0
  48. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroHeader.kt +32 -0
  49. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroRequest.kt +47 -0
  50. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroRequestMethod.kt +26 -0
  51. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroResponse.kt +50 -0
  52. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/nitrofetchOnLoad.kt +35 -0
  53. package/nitrogen/generated/android/nitrofetch+autolinking.cmake +80 -0
  54. package/nitrogen/generated/android/nitrofetch+autolinking.gradle +27 -0
  55. package/nitrogen/generated/android/nitrofetchOnLoad.cpp +54 -0
  56. package/nitrogen/generated/android/nitrofetchOnLoad.hpp +25 -0
  57. package/nitrogen/generated/ios/NitroFetch+autolinking.rb +60 -0
  58. package/nitrogen/generated/ios/NitroFetch-Swift-Cxx-Bridge.cpp +73 -0
  59. package/nitrogen/generated/ios/NitroFetch-Swift-Cxx-Bridge.hpp +298 -0
  60. package/nitrogen/generated/ios/NitroFetch-Swift-Cxx-Umbrella.hpp +67 -0
  61. package/nitrogen/generated/ios/NitroFetchAutolinking.mm +41 -0
  62. package/nitrogen/generated/ios/NitroFetchAutolinking.swift +40 -0
  63. package/nitrogen/generated/ios/c++/HybridNitroFetchClientSpecSwift.cpp +11 -0
  64. package/nitrogen/generated/ios/c++/HybridNitroFetchClientSpecSwift.hpp +101 -0
  65. package/nitrogen/generated/ios/c++/HybridNitroFetchSpecSwift.cpp +11 -0
  66. package/nitrogen/generated/ios/c++/HybridNitroFetchSpecSwift.hpp +75 -0
  67. package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
  68. package/nitrogen/generated/ios/swift/Func_void_NitroResponse.swift +47 -0
  69. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
  70. package/nitrogen/generated/ios/swift/HybridNitroFetchClientSpec.swift +50 -0
  71. package/nitrogen/generated/ios/swift/HybridNitroFetchClientSpec_cxx.swift +149 -0
  72. package/nitrogen/generated/ios/swift/HybridNitroFetchSpec.swift +49 -0
  73. package/nitrogen/generated/ios/swift/HybridNitroFetchSpec_cxx.swift +126 -0
  74. package/nitrogen/generated/ios/swift/NitroHeader.swift +46 -0
  75. package/nitrogen/generated/ios/swift/NitroRequest.swift +199 -0
  76. package/nitrogen/generated/ios/swift/NitroRequestMethod.swift +60 -0
  77. package/nitrogen/generated/ios/swift/NitroResponse.swift +155 -0
  78. package/nitrogen/generated/shared/c++/HybridNitroFetchClientSpec.cpp +22 -0
  79. package/nitrogen/generated/shared/c++/HybridNitroFetchClientSpec.hpp +68 -0
  80. package/nitrogen/generated/shared/c++/HybridNitroFetchSpec.cpp +21 -0
  81. package/nitrogen/generated/shared/c++/HybridNitroFetchSpec.hpp +64 -0
  82. package/nitrogen/generated/shared/c++/NitroHeader.hpp +71 -0
  83. package/nitrogen/generated/shared/c++/NitroRequest.hpp +101 -0
  84. package/nitrogen/generated/shared/c++/NitroRequestMethod.hpp +96 -0
  85. package/nitrogen/generated/shared/c++/NitroResponse.hpp +102 -0
  86. package/package.json +177 -0
  87. package/src/NitroFetch.nitro.ts +57 -0
  88. package/src/NitroInstances.ts +8 -0
  89. package/src/fetch.ts +426 -0
  90. package/src/index.tsx +17 -0
@@ -0,0 +1,256 @@
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.nio.ByteBuffer
12
+ import java.util.concurrent.Executor
13
+
14
+ fun ByteBuffer.toByteArray(): ByteArray {
15
+ // duplicate to avoid modifying the original buffer's position
16
+ val dup = this.duplicate()
17
+ dup.clear() // sets position=0, limit=capacity
18
+ val arr = ByteArray(dup.remaining())
19
+ dup.get(arr)
20
+ return arr
21
+ }
22
+
23
+ @DoNotStrip
24
+ class NitroFetchClient(private val engine: CronetEngine, private val executor: Executor) : HybridNitroFetchClientSpec() {
25
+
26
+ private fun findPrefetchKey(req: NitroRequest): String? {
27
+ val h = req.headers ?: return null
28
+ for (pair in h) {
29
+ val k = pair.key
30
+ val v = pair.value
31
+ if (k.equals("prefetchKey", ignoreCase = true)) return v
32
+ }
33
+ return null
34
+ }
35
+
36
+ companion object {
37
+ @JvmStatic
38
+ fun fetch(
39
+ req: NitroRequest,
40
+ onSuccess: (NitroResponse) -> Unit,
41
+ onFail: (Throwable) -> Unit
42
+ ) {
43
+ try {
44
+ val engine = NitroFetch.getEngine()
45
+ val executor = NitroFetch.ioExecutor
46
+ startCronet(engine, executor, req, onSuccess, onFail)
47
+ } catch (t: Throwable) {
48
+ onFail(t)
49
+ }
50
+ }
51
+
52
+ private fun startCronet(
53
+ engine: CronetEngine,
54
+ executor: Executor,
55
+ req: NitroRequest,
56
+ onSuccess: (NitroResponse) -> Unit,
57
+ onFail: (Throwable) -> Unit
58
+ ) {
59
+ val url = req.url
60
+ val callback = object : UrlRequest.Callback() {
61
+ private val buffer = ByteBuffer.allocateDirect(16 * 1024)
62
+ private val out = java.io.ByteArrayOutputStream()
63
+
64
+ override fun onRedirectReceived(request: UrlRequest, info: UrlResponseInfo, newLocationUrl: String) {
65
+ request.followRedirect()
66
+ }
67
+
68
+ override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) {
69
+ buffer.clear()
70
+ request.read(buffer)
71
+ }
72
+
73
+ override fun onReadCompleted(request: UrlRequest, info: UrlResponseInfo, byteBuffer: ByteBuffer) {
74
+ byteBuffer.flip()
75
+ val bytes = ByteArray(byteBuffer.remaining())
76
+ byteBuffer.get(bytes)
77
+ out.write(bytes)
78
+ byteBuffer.clear()
79
+ request.read(byteBuffer)
80
+ }
81
+
82
+ override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
83
+ try {
84
+ val headersArr: Array<NitroHeader> =
85
+ info.allHeadersAsList.map { NitroHeader(it.key, it.value) }.toTypedArray()
86
+ val status = info.httpStatusCode
87
+ val bytes = out.toByteArray()
88
+ val contentType = info.allHeaders["Content-Type"] ?: info.allHeaders["content-type"]
89
+ val charset = run {
90
+ val ct = contentType ?: ""
91
+ val m = Regex("charset=([A-Za-z0-9_\\-:.]+)", RegexOption.IGNORE_CASE).find(ct.toString())
92
+ try {
93
+ if (m != null) java.nio.charset.Charset.forName(m.groupValues[1]) else Charsets.UTF_8
94
+ } catch (_: Throwable) {
95
+ Charsets.UTF_8
96
+ }
97
+ }
98
+ val bodyStr = try { String(bytes, charset) } catch (_: Throwable) { String(bytes, Charsets.UTF_8) }
99
+ val res = NitroResponse(
100
+ url = info.url,
101
+ status = status.toDouble(),
102
+ statusText = info.httpStatusText ?: "",
103
+ ok = status in 200..299,
104
+ redirected = info.url != url,
105
+ headers = headersArr,
106
+ bodyString = bodyStr,
107
+ bodyBytes = null
108
+ )
109
+ onSuccess(res)
110
+ } catch (t: Throwable) {
111
+ onFail(t)
112
+ }
113
+ }
114
+
115
+ override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: CronetException) {
116
+ onFail(RuntimeException("Cronet failed: ${error.message}", error))
117
+ }
118
+
119
+ override fun onCanceled(request: UrlRequest, info: UrlResponseInfo?) {
120
+ onFail(RuntimeException("Cronet canceled"))
121
+ }
122
+ }
123
+
124
+ val builder = engine.newUrlRequestBuilder(url, callback, executor)
125
+ val method = req.method?.name ?: "GET"
126
+ builder.setHttpMethod(method)
127
+ req.headers?.forEach { (k, v) -> builder.addHeader(k, v) }
128
+ val bodyBytes = req.bodyBytes
129
+ val bodyStr = req.bodyString
130
+ if ((bodyBytes != null) || !bodyStr.isNullOrEmpty()) {
131
+ val body: ByteArray = when {
132
+ bodyBytes != null -> bodyBytes.getBuffer(true).toByteArray()
133
+ !bodyStr.isNullOrEmpty() -> bodyStr!!.toByteArray(Charsets.UTF_8)
134
+ else -> ByteArray(0)
135
+ }
136
+ val provider = object : org.chromium.net.UploadDataProvider() {
137
+ private var pos = 0
138
+ override fun getLength(): Long = body.size.toLong()
139
+ override fun read(uploadDataSink: org.chromium.net.UploadDataSink, byteBuffer: ByteBuffer) {
140
+ val remaining = body.size - pos
141
+ val toWrite = minOf(byteBuffer.remaining(), remaining)
142
+ byteBuffer.put(body, pos, toWrite)
143
+ pos += toWrite
144
+ uploadDataSink.onReadSucceeded(false)
145
+ }
146
+ override fun rewind(uploadDataSink: org.chromium.net.UploadDataSink) {
147
+ pos = 0
148
+ uploadDataSink.onRewindSucceeded()
149
+ }
150
+ }
151
+ builder.setUploadDataProvider(provider, executor)
152
+ }
153
+ val request = builder.build()
154
+ request.start()
155
+ }
156
+ }
157
+
158
+ override fun request(req: NitroRequest): Promise<NitroResponse> {
159
+ val promise = Promise<NitroResponse>()
160
+ // Try to serve from prefetch cache/pending first
161
+ val key = findPrefetchKey(req)
162
+ if (key != null) {
163
+ // If a prefetch is currently pending, wait for it
164
+ FetchCache.getPending(key)?.let { fut ->
165
+ fun withPrefetchedHeader(res: NitroResponse): NitroResponse {
166
+ val newHeaders = (res.headers?.toMutableList() ?: mutableListOf())
167
+ newHeaders.add(NitroHeader("nitroPrefetched", "true"))
168
+ return NitroResponse(
169
+ url = res.url,
170
+ status = res.status,
171
+ statusText = res.statusText,
172
+ ok = res.ok,
173
+ redirected = res.redirected,
174
+ headers = newHeaders.toTypedArray(),
175
+ bodyString = res.bodyString,
176
+ bodyBytes = res.bodyBytes
177
+ )
178
+ }
179
+ fut.whenComplete { res, err ->
180
+ if (err != null) {
181
+ promise.reject(err)
182
+ } else if (res != null) {
183
+ promise.resolve(withPrefetchedHeader(res))
184
+ } else {
185
+ promise.reject(IllegalStateException("Prefetch pending returned null result"))
186
+ }
187
+ }
188
+ return promise
189
+ }
190
+ // If a fresh prefetched result exists (<=5s old), return it immediately
191
+ FetchCache.getResultIfFresh(key, 5_000L)?.let { cached ->
192
+ val newHeaders = (cached.headers?.toMutableList() ?: mutableListOf())
193
+ newHeaders.add(NitroHeader("nitroPrefetched", "true"))
194
+ val wrapped = NitroResponse(
195
+ url = cached.url,
196
+ status = cached.status,
197
+ statusText = cached.statusText,
198
+ ok = cached.ok,
199
+ redirected = cached.redirected,
200
+ headers = newHeaders.toTypedArray(),
201
+ bodyString = cached.bodyString,
202
+ bodyBytes = cached.bodyBytes
203
+ )
204
+ promise.resolve(wrapped)
205
+ return promise
206
+ }
207
+ }
208
+ fetch(
209
+ req,
210
+ onSuccess = { promise.resolve(it) },
211
+ onFail = { promise.reject(it) }
212
+ )
213
+ return promise
214
+ }
215
+
216
+ override fun prefetch(req: NitroRequest): Promise<Unit> {
217
+ val promise = Promise<Unit>()
218
+ val key = findPrefetchKey(req)
219
+ if (key.isNullOrEmpty()) {
220
+ promise.reject(IllegalArgumentException("prefetch: missing 'prefetchKey' header"))
221
+ return promise
222
+ }
223
+ // If already have a fresh result, resolve immediately
224
+ FetchCache.getResultIfFresh(key, 5_000L)?.let {
225
+ promise.resolve(Unit)
226
+ return promise
227
+ }
228
+ // If already pending, resolve when it's done
229
+ FetchCache.getPending(key)?.let { fut ->
230
+ fut.whenComplete { _, err -> if (err != null) promise.reject(err) else promise.resolve(Unit) }
231
+ return promise
232
+ }
233
+ // Start new prefetch
234
+ val future = java.util.concurrent.CompletableFuture<NitroResponse>()
235
+ FetchCache.setPending(key, future)
236
+ fetch(
237
+ req,
238
+ onSuccess = { res ->
239
+ try {
240
+ FetchCache.complete(key, res)
241
+ promise.resolve(Unit)
242
+ } catch (t: Throwable) {
243
+ FetchCache.completeExceptionally(key, t)
244
+ promise.reject(t)
245
+ }
246
+ },
247
+ onFail = { err ->
248
+ FetchCache.completeExceptionally(key, err)
249
+ promise.reject(err)
250
+ }
251
+ )
252
+ return promise
253
+ }
254
+
255
+
256
+ }
@@ -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,81 @@
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
+
8
+ @objc
9
+ public static func prefetchOnStart() {
10
+ if initialized { return }
11
+ initialized = true
12
+ guard let raw = readMMKVString(forKey: queueKey), !raw.isEmpty else { return }
13
+ guard let data = raw.data(using: .utf8) else { return }
14
+ guard let arr = try? JSONSerialization.jsonObject(with: data, options: []) as? [Any] else { return }
15
+
16
+ for item in arr {
17
+ guard let obj = item as? [String: Any] else { continue }
18
+ guard let url = obj["url"] as? String, !url.isEmpty else { continue }
19
+ guard let prefetchKey = obj["prefetchKey"] as? String, !prefetchKey.isEmpty else { continue }
20
+ let headersDict = (obj["headers"] as? [String: Any]) ?? [:]
21
+ var headers: [NitroHeader] = headersDict.map { (k, v) in NitroHeader(key: String(describing: k), value: String(describing: v)) }
22
+ headers.append(NitroHeader(key: "prefetchKey", value: prefetchKey))
23
+ let req = NitroRequest(url: url,
24
+ method: nil,
25
+ headers: headers,
26
+ bodyString: nil,
27
+ bodyBytes: nil,
28
+ timeoutMs: nil,
29
+ followRedirects: true)
30
+ Task {
31
+ do { try await NitroFetchClient.prefetchStatic(req) } catch { /* ignore – best effort */ }
32
+ }
33
+ }
34
+ }
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
+ }
76
+
77
+ // Expose a C-ABI symbol the ObjC++ file can call
78
+ @_cdecl("NitroStartSwift")
79
+ public func NitroStartSwift() {
80
+ NitroAutoPrefetcher.prefetchOnStart()
81
+ }
@@ -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
+
@@ -0,0 +1,205 @@
1
+ import Foundation
2
+ import NitroModules
3
+
4
+ final class NitroFetchClient: HybridNitroFetchClientSpec {
5
+ func request(req: NitroRequest) throws -> Promise<NitroResponse> {
6
+ let promise = Promise<NitroResponse>.init()
7
+ Task {
8
+ do {
9
+ let response = try await NitroFetchClient.requestStatic(req)
10
+ promise.resolve(withResult: response)
11
+ } catch {
12
+ promise.reject(withError: error)
13
+ }
14
+ }
15
+ return promise
16
+ }
17
+
18
+ func prefetch(req: NitroRequest) throws -> Promise<Void> {
19
+ let promise = Promise<Void>.init()
20
+ Task {
21
+ do {
22
+ try await NitroFetchClient.prefetchStatic(req)
23
+ promise.resolve(withResult: ())
24
+ } catch {
25
+ promise.reject(withError: error)
26
+ }
27
+
28
+ }
29
+ return promise
30
+ }
31
+
32
+ // Shared URLSession for static operations
33
+ private static let session: URLSession = {
34
+ let config = URLSessionConfiguration.default
35
+ config.requestCachePolicy = .useProtocolCachePolicy
36
+ config.urlCache = URLCache(memoryCapacity: 32 * 1024 * 1024,
37
+ diskCapacity: 100 * 1024 * 1024,
38
+ diskPath: "nitrofetch_urlcache")
39
+ return URLSession(configuration: config)
40
+ }()
41
+
42
+ private static func findPrefetchKey(_ req: NitroRequest) -> String? {
43
+ guard let headers = req.headers else { return nil }
44
+ for h in headers {
45
+ if h.key.caseInsensitiveCompare("prefetchKey") == .orderedSame {
46
+ return h.value
47
+ }
48
+ }
49
+ return nil
50
+ }
51
+
52
+ // MARK: - Static API usable from native bootstrap
53
+
54
+
55
+ public class func requestStatic(_ req: NitroRequest) async throws -> NitroResponse {
56
+ if let key = findPrefetchKey(req) {
57
+ // If a prefetched result is fresh, return immediately
58
+ if let cached = FetchCache.getResultIfFresh(key, maxAgeMs: 5_000) {
59
+ var headers = cached.headers ?? []
60
+ headers.append(NitroHeader(key: "nitroPrefetched", value: "true"))
61
+ return NitroResponse(url: cached.url,
62
+ status: cached.status,
63
+ statusText: cached.statusText,
64
+ ok: cached.ok,
65
+ redirected: cached.redirected,
66
+ headers: headers,
67
+ bodyString: cached.bodyString,
68
+ bodyBytes: cached.bodyBytes)
69
+ }
70
+
71
+ // If a prefetch is already pending, await and reuse its result
72
+ if FetchCache.getPending(key) {
73
+ return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<NitroResponse, Error>) in
74
+ FetchCache.addPending(key) { result in
75
+ switch result {
76
+ case .success(let res):
77
+ // Mirror Android: mark response as coming from prefetch
78
+ var headers = res.headers ?? []
79
+ headers.append(NitroHeader(key: "nitroPrefetched", value: "true"))
80
+ let wrapped = NitroResponse(url: res.url,
81
+ status: res.status,
82
+ statusText: res.statusText,
83
+ ok: res.ok,
84
+ redirected: res.redirected,
85
+ headers: headers,
86
+ bodyString: res.bodyString,
87
+ bodyBytes: res.bodyBytes)
88
+ continuation.resume(returning: wrapped)
89
+ case .failure(let err):
90
+ continuation.resume(throwing: err)
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ let (urlRequest, finalURL) = try buildURLRequest(req)
98
+ let (data, response) = try await session.data(for: urlRequest)
99
+ guard let http = response as? HTTPURLResponse else {
100
+ throw NSError(domain: "NitroFetch", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid response"])
101
+ }
102
+
103
+ let headersPairs: [NitroHeader] = http.allHeaderFields.compactMap { k, v in
104
+ guard let key = k as? String else { return nil }
105
+ return NitroHeader(key: key, value: String(describing: v))
106
+ }
107
+
108
+ // Choose bodyString by default (matching Android’s first pass)
109
+ let charset = NitroFetchClient.detectCharset(from: http) ?? String.Encoding.utf8
110
+ let bodyStr = String(data: data, encoding: charset) ?? String(data: data, encoding: .utf8)
111
+
112
+ let res = NitroResponse(
113
+ url: finalURL?.absoluteString ?? http.url?.absoluteString ?? req.url,
114
+ status: Double(http.statusCode),
115
+ statusText: HTTPURLResponse.localizedString(forStatusCode: http.statusCode),
116
+ ok: (200...299).contains(http.statusCode),
117
+ redirected: (finalURL?.absoluteString ?? http.url?.absoluteString ?? req.url) != req.url,
118
+ headers: headersPairs,
119
+ bodyString: bodyStr,
120
+ bodyBytes: nil
121
+ )
122
+
123
+ // Do not write to cache here; only prefetch should populate the cache
124
+
125
+ return res
126
+ }
127
+
128
+ public class func prefetchStatic(_ req: NitroRequest) async throws {
129
+ guard let key = findPrefetchKey(req) else {
130
+ throw NSError(domain: "NitroFetch", code: -2, userInfo: [NSLocalizedDescriptionKey: "prefetch: missing 'prefetchKey' header"])
131
+ }
132
+
133
+ if FetchCache.getResultIfFresh(key, maxAgeMs: 5_000) != nil {
134
+ return // already have a fresh result
135
+ }
136
+
137
+ if FetchCache.getPending(key) {
138
+ return // already pending
139
+ }
140
+
141
+ // Mark pending and start the request
142
+ FetchCache.addPending(key) { _ in /* ignored here */ }
143
+ Task.detached {
144
+ do {
145
+ let (urlRequest, finalURL) = try buildURLRequest(req)
146
+ let (data, response) = try await session.data(for: urlRequest)
147
+ guard let http = response as? HTTPURLResponse else {
148
+ throw NSError(domain: "NitroFetch", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid response"])
149
+ }
150
+ let headersPairs: [NitroHeader] = http.allHeaderFields.compactMap { k, v in
151
+ guard let key = k as? String else { return nil }
152
+ return NitroHeader(key: key, value: String(describing: v))
153
+ }
154
+ let charset = NitroFetchClient.detectCharset(from: http) ?? .utf8
155
+ let bodyStr = String(data: data, encoding: charset) ?? String(data: data, encoding: .utf8)
156
+ let res = NitroResponse(
157
+ url: finalURL?.absoluteString ?? http.url?.absoluteString ?? req.url,
158
+ status: Double(http.statusCode),
159
+ statusText: HTTPURLResponse.localizedString(forStatusCode: http.statusCode),
160
+ ok: (200...299).contains(http.statusCode),
161
+ redirected: (finalURL?.absoluteString ?? http.url?.absoluteString ?? req.url) != req.url,
162
+ headers: headersPairs,
163
+ bodyString: bodyStr,
164
+ bodyBytes: nil
165
+ )
166
+ FetchCache.complete(key, with: .success(res))
167
+ } catch {
168
+ FetchCache.complete(key, with: .failure(error))
169
+ }
170
+ }
171
+ }
172
+
173
+ private static func reqToHttpMethod(_ req: NitroRequest) -> String? {
174
+ return req.method?.stringValue
175
+ }
176
+
177
+ private static func buildURLRequest(_ req: NitroRequest) throws -> (URLRequest, URL?) {
178
+ guard let url = URL(string: req.url) else {
179
+ throw NSError(domain: "NitroFetch", code: -3, userInfo: [NSLocalizedDescriptionKey: "Invalid URL: \(req.url)"])
180
+ }
181
+ var r = URLRequest(url: url)
182
+ if let m = req.method?.rawValue { r.httpMethod = reqToHttpMethod(req) }
183
+ if let headers = req.headers {
184
+ for h in headers { r.addValue(h.value, forHTTPHeaderField: h.key) }
185
+ }
186
+ if let s = req.bodyString {
187
+ r.httpBody = s.data(using: .utf8)
188
+ }
189
+ if let t = req.timeoutMs, t > 0 { r.timeoutInterval = TimeInterval(t) / 1000.0 }
190
+ return (r, nil)
191
+ }
192
+
193
+ private static func detectCharset(from http: HTTPURLResponse) -> String.Encoding? {
194
+ if let ct = http.value(forHTTPHeaderField: "Content-Type")?.lowercased() {
195
+ if let range = ct.range(of: "charset=") {
196
+ let charset = String(ct[range.upperBound...]).trimmingCharacters(in: .whitespaces)
197
+ let mapped = CFStringConvertIANACharSetNameToEncoding(charset as CFString)
198
+ if mapped != kCFStringEncodingInvalidId {
199
+ return String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(mapped))
200
+ }
201
+ }
202
+ }
203
+ return nil
204
+ }
205
+ }
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+
3
+ // Minimal request/response types to model WHATWG fetch without streaming.
4
+
5
+ ;
6
+ export {};
7
+ //# sourceMappingURL=NitroFetch.nitro.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":[],"sourceRoot":"../../src","sources":["NitroFetch.nitro.ts"],"mappings":";;AAEA;;AAaC;AAAC","ignoreList":[]}
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+
3
+ import { NitroModules } from 'react-native-nitro-modules';
4
+ // Create singletons once per JS runtime
5
+ export const NitroFetch = NitroModules.createHybridObject('NitroFetch');
6
+ //# sourceMappingURL=NitroInstances.js.map
@@ -0,0 +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":[]}