@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.
- package/NitroFetch.podspec +30 -0
- package/android/CMakeLists.txt +70 -0
- package/android/build.gradle +130 -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 +72 -0
- package/android/src/main/java/com/margelo/nitro/nitrofetch/FetchCache.kt +58 -0
- package/android/src/main/java/com/margelo/nitro/nitrofetch/NativeStorage.kt +102 -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 +331 -0
- package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchPackage.kt +22 -0
- package/ios/FetchCache.swift +56 -0
- package/ios/NativeStorage.swift +61 -0
- package/ios/NitroAutoPrefetcher.swift +45 -0
- package/ios/NitroBootstrap.mm +27 -0
- package/ios/NitroFetch.swift +9 -0
- package/ios/NitroFetchClient.swift +230 -0
- package/lib/module/NitroFetch.nitro.js +4 -0
- package/lib/module/NitroFetch.nitro.js.map +1 -0
- package/lib/module/NitroInstances.js +8 -0
- package/lib/module/NitroInstances.js.map +1 -0
- package/lib/module/fetch.js +522 -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/module/type.js +2 -0
- package/lib/module/type.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NitroFetch.nitro.d.ts +48 -0
- package/lib/typescript/src/NitroFetch.nitro.d.ts.map +1 -0
- package/lib/typescript/src/NitroInstances.d.ts +5 -0
- package/lib/typescript/src/NitroInstances.d.ts.map +1 -0
- package/lib/typescript/src/fetch.d.ts +28 -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/lib/typescript/src/type.d.ts +4 -0
- package/lib/typescript/src/type.d.ts.map +1 -0
- package/nitro.json +25 -0
- package/nitrogen/generated/android/c++/JHybridNativeStorageSpec.cpp +54 -0
- package/nitrogen/generated/android/c++/JHybridNativeStorageSpec.hpp +66 -0
- package/nitrogen/generated/android/c++/JHybridNitroFetchClientSpec.cpp +96 -0
- package/nitrogen/generated/android/c++/JHybridNitroFetchClientSpec.hpp +66 -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 +100 -0
- package/nitrogen/generated/android/c++/JNitroRequestMethod.hpp +74 -0
- package/nitrogen/generated/android/c++/JNitroResponse.hpp +102 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/HybridNativeStorageSpec.kt +60 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/HybridNitroFetchClientSpec.kt +60 -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 +85 -0
- package/nitrogen/generated/android/nitrofetch+autolinking.gradle +27 -0
- package/nitrogen/generated/android/nitrofetchOnLoad.cpp +64 -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 +90 -0
- package/nitrogen/generated/ios/NitroFetch-Swift-Cxx-Bridge.hpp +321 -0
- package/nitrogen/generated/ios/NitroFetch-Swift-Cxx-Umbrella.hpp +69 -0
- package/nitrogen/generated/ios/NitroFetchAutolinking.mm +49 -0
- package/nitrogen/generated/ios/NitroFetchAutolinking.swift +55 -0
- package/nitrogen/generated/ios/c++/HybridNativeStorageSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNativeStorageSpecSwift.hpp +85 -0
- package/nitrogen/generated/ios/c++/HybridNitroFetchClientSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNitroFetchClientSpecSwift.hpp +103 -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/HybridNativeStorageSpec.swift +51 -0
- package/nitrogen/generated/ios/swift/HybridNativeStorageSpec_cxx.swift +145 -0
- package/nitrogen/generated/ios/swift/HybridNitroFetchClientSpec.swift +51 -0
- package/nitrogen/generated/ios/swift/HybridNitroFetchClientSpec_cxx.swift +161 -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 +206 -0
- package/nitrogen/generated/ios/swift/NitroRequestMethod.swift +60 -0
- package/nitrogen/generated/ios/swift/NitroResponse.swift +162 -0
- package/nitrogen/generated/shared/c++/HybridNativeStorageSpec.cpp +23 -0
- package/nitrogen/generated/shared/c++/HybridNativeStorageSpec.hpp +64 -0
- package/nitrogen/generated/shared/c++/HybridNitroFetchClientSpec.cpp +23 -0
- package/nitrogen/generated/shared/c++/HybridNitroFetchClientSpec.hpp +69 -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 +98 -0
- package/nitrogen/generated/shared/c++/NitroRequestMethod.hpp +96 -0
- package/nitrogen/generated/shared/c++/NitroResponse.hpp +99 -0
- package/package.json +162 -0
- package/src/NitroFetch.nitro.ts +67 -0
- package/src/NitroInstances.ts +14 -0
- package/src/fetch.ts +603 -0
- package/src/index.tsx +17 -0
- 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
|