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.
- package/LICENSE +20 -0
- package/NitroFetch.podspec +30 -0
- package/README.md +134 -0
- package/android/CMakeLists.txt +70 -0
- package/android/build.gradle +131 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt +91 -0
- package/android/src/main/java/com/margelo/nitro/nitrofetch/FetchCache.kt +48 -0
- package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetch.kt +94 -0
- package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt +256 -0
- package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchPackage.kt +22 -0
- package/ios/FetchCache.swift +56 -0
- package/ios/NitroAutoPrefetcher.swift +81 -0
- package/ios/NitroBootstrap.mm +27 -0
- package/ios/NitroFetch.swift +9 -0
- package/ios/NitroFetchClient.swift +205 -0
- package/lib/module/NitroFetch.nitro.js +7 -0
- package/lib/module/NitroFetch.nitro.js.map +1 -0
- package/lib/module/NitroInstances.js +6 -0
- package/lib/module/NitroInstances.js.map +1 -0
- package/lib/module/fetch.js +441 -0
- package/lib/module/fetch.js.map +1 -0
- package/lib/module/index.js +12 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NitroFetch.nitro.d.ts +39 -0
- package/lib/typescript/src/NitroFetch.nitro.d.ts.map +1 -0
- package/lib/typescript/src/NitroInstances.d.ts +3 -0
- package/lib/typescript/src/NitroInstances.d.ts.map +1 -0
- package/lib/typescript/src/fetch.d.ts +26 -0
- package/lib/typescript/src/fetch.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +6 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/nitro.json +21 -0
- package/nitrogen/generated/android/c++/JHybridNitroFetchClientSpec.cpp +96 -0
- package/nitrogen/generated/android/c++/JHybridNitroFetchClientSpec.hpp +65 -0
- package/nitrogen/generated/android/c++/JHybridNitroFetchSpec.cpp +49 -0
- package/nitrogen/generated/android/c++/JHybridNitroFetchSpec.hpp +64 -0
- package/nitrogen/generated/android/c++/JNitroHeader.hpp +57 -0
- package/nitrogen/generated/android/c++/JNitroRequest.hpp +103 -0
- package/nitrogen/generated/android/c++/JNitroRequestMethod.hpp +74 -0
- package/nitrogen/generated/android/c++/JNitroResponse.hpp +105 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/HybridNitroFetchClientSpec.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/HybridNitroFetchSpec.kt +52 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroHeader.kt +32 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroRequest.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroRequestMethod.kt +26 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroResponse.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/nitrofetchOnLoad.kt +35 -0
- package/nitrogen/generated/android/nitrofetch+autolinking.cmake +80 -0
- package/nitrogen/generated/android/nitrofetch+autolinking.gradle +27 -0
- package/nitrogen/generated/android/nitrofetchOnLoad.cpp +54 -0
- package/nitrogen/generated/android/nitrofetchOnLoad.hpp +25 -0
- package/nitrogen/generated/ios/NitroFetch+autolinking.rb +60 -0
- package/nitrogen/generated/ios/NitroFetch-Swift-Cxx-Bridge.cpp +73 -0
- package/nitrogen/generated/ios/NitroFetch-Swift-Cxx-Bridge.hpp +298 -0
- package/nitrogen/generated/ios/NitroFetch-Swift-Cxx-Umbrella.hpp +67 -0
- package/nitrogen/generated/ios/NitroFetchAutolinking.mm +41 -0
- package/nitrogen/generated/ios/NitroFetchAutolinking.swift +40 -0
- package/nitrogen/generated/ios/c++/HybridNitroFetchClientSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNitroFetchClientSpecSwift.hpp +101 -0
- package/nitrogen/generated/ios/c++/HybridNitroFetchSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNitroFetchSpecSwift.hpp +75 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_NitroResponse.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridNitroFetchClientSpec.swift +50 -0
- package/nitrogen/generated/ios/swift/HybridNitroFetchClientSpec_cxx.swift +149 -0
- package/nitrogen/generated/ios/swift/HybridNitroFetchSpec.swift +49 -0
- package/nitrogen/generated/ios/swift/HybridNitroFetchSpec_cxx.swift +126 -0
- package/nitrogen/generated/ios/swift/NitroHeader.swift +46 -0
- package/nitrogen/generated/ios/swift/NitroRequest.swift +199 -0
- package/nitrogen/generated/ios/swift/NitroRequestMethod.swift +60 -0
- package/nitrogen/generated/ios/swift/NitroResponse.swift +155 -0
- package/nitrogen/generated/shared/c++/HybridNitroFetchClientSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridNitroFetchClientSpec.hpp +68 -0
- package/nitrogen/generated/shared/c++/HybridNitroFetchSpec.cpp +21 -0
- package/nitrogen/generated/shared/c++/HybridNitroFetchSpec.hpp +64 -0
- package/nitrogen/generated/shared/c++/NitroHeader.hpp +71 -0
- package/nitrogen/generated/shared/c++/NitroRequest.hpp +101 -0
- package/nitrogen/generated/shared/c++/NitroRequestMethod.hpp +96 -0
- package/nitrogen/generated/shared/c++/NitroResponse.hpp +102 -0
- package/package.json +177 -0
- package/src/NitroFetch.nitro.ts +57 -0
- package/src/NitroInstances.ts +8 -0
- package/src/fetch.ts +426 -0
- 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,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 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../src","sources":["NitroFetch.nitro.ts"],"mappings":";;AAEA;;AAaC;AAAC","ignoreList":[]}
|
|
@@ -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":[]}
|