react-native-nitro-fetch 1.2.1 → 1.3.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/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt +114 -31
- package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt +43 -6
- package/ios/NitroAutoPrefetcher.h +21 -0
- package/ios/NitroAutoPrefetcher.swift +149 -27
- package/ios/NitroFetchClient.swift +17 -6
- package/lib/module/CurlGenerator.js.map +1 -1
- package/lib/module/HermesProfiler.js.map +2 -1
- package/lib/module/NetworkInspector.js +0 -4
- package/lib/module/NetworkInspector.js.map +1 -1
- package/lib/module/NitroCronet.nitro.js.map +1 -1
- package/lib/module/NitroFetch.nitro.js.map +1 -0
- package/lib/module/NitroInstances.js.map +1 -1
- package/lib/module/Response.js +3 -4
- package/lib/module/Response.js.map +2 -1
- package/lib/module/fetch.js +26 -6
- package/lib/module/fetch.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +2 -1
- package/lib/module/index.web.js +1 -2
- package/lib/module/utf8.js +19 -8
- package/lib/module/utf8.js.map +2 -1
- package/lib/typescript/src/NitroFetch.nitro.d.ts +2 -1
- package/lib/typescript/src/NitroFetch.nitro.d.ts.map +1 -1
- package/lib/typescript/src/Response.d.ts.map +1 -1
- package/lib/typescript/src/fetch.d.ts +4 -1
- package/lib/typescript/src/fetch.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/utf8.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JHybridNitroFetchClientSpec.cpp +2 -0
- package/nitrogen/generated/android/c++/JNitroRequest.hpp +5 -1
- package/nitrogen/generated/android/c++/JNitroResponse.hpp +7 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroRequest.kt +5 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroResponse.kt +3 -3
- package/nitrogen/generated/ios/NitroFetch-Swift-Cxx-Bridge.hpp +15 -0
- package/nitrogen/generated/ios/c++/HybridNitroFetchClientSpecSwift.hpp +4 -0
- package/nitrogen/generated/ios/swift/NitroRequest.swift +19 -1
- package/nitrogen/generated/ios/swift/NitroResponse.swift +8 -8
- package/nitrogen/generated/shared/c++/NitroRequest.hpp +5 -1
- package/nitrogen/generated/shared/c++/NitroResponse.hpp +6 -5
- package/package.json +18 -3
- package/src/CurlGenerator.js +31 -0
- package/src/Headers.js +127 -0
- package/src/HermesProfiler.js +22 -0
- package/src/NetworkInspector.js +183 -0
- package/src/NitroCronet.nitro.js +1 -0
- package/src/NitroFetch.nitro.js +1 -0
- package/src/NitroFetch.nitro.ts +4 -2
- package/src/NitroInstances.js +6 -0
- package/src/Request.js +173 -0
- package/src/Response.js +258 -0
- package/src/Response.ts +2 -1
- package/src/fetch.js +772 -0
- package/src/fetch.ts +46 -9
- package/src/index.js +10 -0
- package/src/index.tsx +1 -0
- package/src/index.web.js +104 -0
- package/src/tokenRefresh.js +104 -0
- package/src/utf8.js +40 -0
- package/src/utf8.ts +29 -14
|
@@ -31,9 +31,21 @@ object AutoPrefetcher {
|
|
|
31
31
|
context: Context,
|
|
32
32
|
url: String,
|
|
33
33
|
prefetchKey: String,
|
|
34
|
-
headers: Map<String, String> = emptyMap()
|
|
34
|
+
headers: Map<String, String> = emptyMap(),
|
|
35
|
+
method: String? = null,
|
|
36
|
+
bodyString: String? = null,
|
|
37
|
+
bodyBytes: String? = null,
|
|
38
|
+
bodyFormData: List<Map<String, String?>>? = null,
|
|
39
|
+
timeoutMs: Double? = null,
|
|
40
|
+
followRedirects: Boolean? = null,
|
|
41
|
+
prefetchCacheTtlMs: Double? = null,
|
|
35
42
|
) {
|
|
36
43
|
if (url.isEmpty() || prefetchKey.isEmpty()) return
|
|
44
|
+
val entry = buildEntryJson(
|
|
45
|
+
url, prefetchKey, headers,
|
|
46
|
+
method, bodyString, bodyBytes, bodyFormData, timeoutMs, followRedirects,
|
|
47
|
+
prefetchCacheTtlMs
|
|
48
|
+
)
|
|
37
49
|
try {
|
|
38
50
|
val prefs = context.applicationContext
|
|
39
51
|
.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
@@ -46,14 +58,6 @@ object AutoPrefetcher {
|
|
|
46
58
|
if (o.optString("prefetchKey", "") == prefetchKey) continue
|
|
47
59
|
next.put(o)
|
|
48
60
|
}
|
|
49
|
-
|
|
50
|
-
val headersObj = JSONObject()
|
|
51
|
-
headers.forEach { (k, v) -> headersObj.put(k, v) }
|
|
52
|
-
val entry = JSONObject().apply {
|
|
53
|
-
put("url", url)
|
|
54
|
-
put("prefetchKey", prefetchKey)
|
|
55
|
-
put("headers", headersObj)
|
|
56
|
-
}
|
|
57
61
|
next.put(entry)
|
|
58
62
|
prefs.edit().putString(KEY_QUEUE, next.toString()).apply()
|
|
59
63
|
} catch (_: Throwable) {
|
|
@@ -73,15 +77,7 @@ object AutoPrefetcher {
|
|
|
73
77
|
} catch (_: Throwable) { emptyMap() }
|
|
74
78
|
} else emptyMap()
|
|
75
79
|
|
|
76
|
-
val single = JSONArray().apply {
|
|
77
|
-
val headersObj = JSONObject()
|
|
78
|
-
headers.forEach { (k, v) -> headersObj.put(k, v) }
|
|
79
|
-
put(JSONObject().apply {
|
|
80
|
-
put("url", url)
|
|
81
|
-
put("prefetchKey", prefetchKey)
|
|
82
|
-
put("headers", headersObj)
|
|
83
|
-
})
|
|
84
|
-
}
|
|
80
|
+
val single = JSONArray().apply { put(entry) }
|
|
85
81
|
startPrefetches(single, tokenHeaders)
|
|
86
82
|
} catch (_: Throwable) {}
|
|
87
83
|
}
|
|
@@ -169,21 +165,13 @@ object AutoPrefetcher {
|
|
|
169
165
|
|
|
170
166
|
android.util.Log.d("NitroFetch", "[TokenRefresh] Prefetching $url with ${merged.size} header(s)")
|
|
171
167
|
merged.forEach { (k, v) -> android.util.Log.d("NitroFetch", "[TokenRefresh] $k: $v") }
|
|
172
|
-
val
|
|
173
|
-
val req = NitroRequest(
|
|
174
|
-
url = url,
|
|
175
|
-
method = null,
|
|
176
|
-
headers = headerObjs,
|
|
177
|
-
bodyString = null,
|
|
178
|
-
bodyBytes = null,
|
|
179
|
-
bodyFormData = null,
|
|
180
|
-
timeoutMs = null,
|
|
181
|
-
followRedirects = null,
|
|
182
|
-
requestId = null
|
|
183
|
-
)
|
|
168
|
+
val req = buildNitroRequestFromEntry(url, merged, o)
|
|
184
169
|
|
|
185
170
|
if (FetchCache.getPending(prefetchKey) != null) continue
|
|
186
|
-
if (
|
|
171
|
+
val entryTtlMs = if (o.has("prefetchCacheTtlMs") && !o.isNull("prefetchCacheTtlMs")) {
|
|
172
|
+
o.optDouble("prefetchCacheTtlMs").toLong()
|
|
173
|
+
} else 5_000L
|
|
174
|
+
if (FetchCache.hasFreshResult(prefetchKey, entryTtlMs)) continue
|
|
187
175
|
|
|
188
176
|
val future = CompletableFuture<NitroResponse>()
|
|
189
177
|
FetchCache.setPending(prefetchKey, future)
|
|
@@ -205,6 +193,101 @@ object AutoPrefetcher {
|
|
|
205
193
|
}
|
|
206
194
|
}
|
|
207
195
|
|
|
196
|
+
private fun buildEntryJson(
|
|
197
|
+
url: String,
|
|
198
|
+
prefetchKey: String,
|
|
199
|
+
headers: Map<String, String>,
|
|
200
|
+
method: String?,
|
|
201
|
+
bodyString: String?,
|
|
202
|
+
bodyBytes: String?,
|
|
203
|
+
bodyFormData: List<Map<String, String?>>?,
|
|
204
|
+
timeoutMs: Double?,
|
|
205
|
+
followRedirects: Boolean?,
|
|
206
|
+
prefetchCacheTtlMs: Double? = null,
|
|
207
|
+
): JSONObject {
|
|
208
|
+
val headersObj = JSONObject()
|
|
209
|
+
headers.forEach { (k, v) -> headersObj.put(k, v) }
|
|
210
|
+
return JSONObject().apply {
|
|
211
|
+
put("url", url)
|
|
212
|
+
put("prefetchKey", prefetchKey)
|
|
213
|
+
put("headers", headersObj)
|
|
214
|
+
if (method != null && method.isNotEmpty() && method != "GET") put("method", method)
|
|
215
|
+
if (bodyString != null) put("bodyString", bodyString)
|
|
216
|
+
if (bodyBytes != null) put("bodyBytes", bodyBytes)
|
|
217
|
+
if (!bodyFormData.isNullOrEmpty()) {
|
|
218
|
+
val arr = JSONArray()
|
|
219
|
+
bodyFormData.forEach { part ->
|
|
220
|
+
val obj = JSONObject()
|
|
221
|
+
part["name"]?.let { obj.put("name", it) }
|
|
222
|
+
part["value"]?.let { obj.put("value", it) }
|
|
223
|
+
part["fileUri"]?.let { obj.put("fileUri", it) }
|
|
224
|
+
part["fileName"]?.let { obj.put("fileName", it) }
|
|
225
|
+
part["mimeType"]?.let { obj.put("mimeType", it) }
|
|
226
|
+
arr.put(obj)
|
|
227
|
+
}
|
|
228
|
+
put("bodyFormData", arr)
|
|
229
|
+
}
|
|
230
|
+
if (timeoutMs != null) put("timeoutMs", timeoutMs)
|
|
231
|
+
if (followRedirects == false) put("followRedirects", false)
|
|
232
|
+
if (prefetchCacheTtlMs != null) put("prefetchCacheTtlMs", prefetchCacheTtlMs)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private fun buildNitroRequestFromEntry(
|
|
237
|
+
url: String,
|
|
238
|
+
mergedHeaders: Map<String, String>,
|
|
239
|
+
entry: JSONObject?,
|
|
240
|
+
): NitroRequest {
|
|
241
|
+
val headerObjs = mergedHeaders.map { (k, v) -> NitroHeader(k, v) }.toTypedArray()
|
|
242
|
+
|
|
243
|
+
val methodStr = entry?.optString("method", "")?.takeIf { it.isNotEmpty() }
|
|
244
|
+
val method: NitroRequestMethod? = methodStr?.let {
|
|
245
|
+
runCatching { NitroRequestMethod.valueOf(it) }.getOrNull()
|
|
246
|
+
}
|
|
247
|
+
val bodyString = entry
|
|
248
|
+
?.takeIf { it.has("bodyString") && !it.isNull("bodyString") }
|
|
249
|
+
?.optString("bodyString")
|
|
250
|
+
val bodyBytes = entry
|
|
251
|
+
?.takeIf { it.has("bodyBytes") && !it.isNull("bodyBytes") }
|
|
252
|
+
?.optString("bodyBytes")
|
|
253
|
+
val timeoutMs = entry
|
|
254
|
+
?.takeIf { it.has("timeoutMs") && !it.isNull("timeoutMs") }
|
|
255
|
+
?.optDouble("timeoutMs")
|
|
256
|
+
val followRedirects = entry
|
|
257
|
+
?.takeIf { it.has("followRedirects") && !it.isNull("followRedirects") }
|
|
258
|
+
?.optBoolean("followRedirects")
|
|
259
|
+
val prefetchCacheTtlMs = entry
|
|
260
|
+
?.takeIf { it.has("prefetchCacheTtlMs") && !it.isNull("prefetchCacheTtlMs") }
|
|
261
|
+
?.optDouble("prefetchCacheTtlMs")
|
|
262
|
+
|
|
263
|
+
val formArr = entry?.optJSONArray("bodyFormData")
|
|
264
|
+
val bodyFormData: Array<NitroFormDataPart>? = formArr?.let { ja ->
|
|
265
|
+
Array(ja.length()) { i ->
|
|
266
|
+
val p = ja.optJSONObject(i) ?: JSONObject()
|
|
267
|
+
NitroFormDataPart(
|
|
268
|
+
name = p.optString("name", ""),
|
|
269
|
+
value = if (p.has("value") && !p.isNull("value")) p.optString("value") else null,
|
|
270
|
+
fileUri = if (p.has("fileUri") && !p.isNull("fileUri")) p.optString("fileUri") else null,
|
|
271
|
+
fileName = if (p.has("fileName") && !p.isNull("fileName")) p.optString("fileName") else null,
|
|
272
|
+
mimeType = if (p.has("mimeType") && !p.isNull("mimeType")) p.optString("mimeType") else null
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return NitroRequest(
|
|
278
|
+
url = url,
|
|
279
|
+
method = method,
|
|
280
|
+
headers = headerObjs,
|
|
281
|
+
bodyString = bodyString,
|
|
282
|
+
bodyBytes = bodyBytes,
|
|
283
|
+
bodyFormData = bodyFormData,
|
|
284
|
+
timeoutMs = timeoutMs,
|
|
285
|
+
followRedirects = followRedirects,
|
|
286
|
+
prefetchCacheTtlMs = prefetchCacheTtlMs,
|
|
287
|
+
requestId = null
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
|
|
208
291
|
// MARK: - Token refresh (synchronous, runs on background thread)
|
|
209
292
|
|
|
210
293
|
private fun callTokenRefreshSync(config: JSONObject): Map<String, String>? {
|
|
@@ -14,6 +14,9 @@ import org.chromium.net.UrlResponseInfo
|
|
|
14
14
|
import java.io.ByteArrayOutputStream
|
|
15
15
|
import java.io.File
|
|
16
16
|
import java.nio.ByteBuffer
|
|
17
|
+
import java.nio.charset.Charset
|
|
18
|
+
import java.nio.charset.CharsetDecoder
|
|
19
|
+
import java.nio.charset.CodingErrorAction
|
|
17
20
|
import java.util.UUID
|
|
18
21
|
import java.util.concurrent.ConcurrentHashMap
|
|
19
22
|
import java.util.concurrent.Executor
|
|
@@ -27,6 +30,33 @@ fun ByteBuffer.toByteArray(): ByteArray {
|
|
|
27
30
|
return arr
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
// Strict UTF-8 decoder reused per-thread. Decoding response bodies is on the hot
|
|
34
|
+
// path for every request; allocating a fresh decoder each time is wasteful, and
|
|
35
|
+
// CharsetDecoder is not thread-safe — a ThreadLocal gives us both. REPORT (rather
|
|
36
|
+
// than the default REPLACE) makes invalid UTF-8 throw, which is how we detect a
|
|
37
|
+
// binary body instead of silently corrupting it with U+FFFD replacement chars.
|
|
38
|
+
private val utf8StrictDecoder = ThreadLocal.withInitial {
|
|
39
|
+
Charsets.UTF_8.newDecoder()
|
|
40
|
+
.onMalformedInput(CodingErrorAction.REPORT)
|
|
41
|
+
.onUnmappableCharacter(CodingErrorAction.REPORT)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private fun strictDecoderFor(charset: Charset): CharsetDecoder =
|
|
45
|
+
if (charset == Charsets.UTF_8) {
|
|
46
|
+
utf8StrictDecoder.get()
|
|
47
|
+
} else {
|
|
48
|
+
charset.newDecoder()
|
|
49
|
+
.onMalformedInput(CodingErrorAction.REPORT)
|
|
50
|
+
.onUnmappableCharacter(CodingErrorAction.REPORT)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Wrap raw bytes into a Nitro ArrayBuffer for zero-base64 bridging to JS.
|
|
54
|
+
private fun ByteArray.toArrayBuffer(): ArrayBuffer {
|
|
55
|
+
val ab = ArrayBuffer.allocate(this.size)
|
|
56
|
+
ab.getBuffer(false).put(this)
|
|
57
|
+
return ab
|
|
58
|
+
}
|
|
59
|
+
|
|
30
60
|
@DoNotStrip
|
|
31
61
|
class NitroFetchClient(private val engine: CronetEngine, private val executor: Executor) : HybridNitroFetchClientSpec() {
|
|
32
62
|
|
|
@@ -187,7 +217,14 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E
|
|
|
187
217
|
Charsets.UTF_8
|
|
188
218
|
}
|
|
189
219
|
}
|
|
190
|
-
|
|
220
|
+
// Strict-decode the body as text. If it fails the response is binary,
|
|
221
|
+
// so we bridge the raw bytes as an ArrayBuffer instead — no base64.
|
|
222
|
+
val bodyStr: String? = try {
|
|
223
|
+
strictDecoderFor(charset).decode(ByteBuffer.wrap(bytes)).toString()
|
|
224
|
+
} catch (_: Throwable) { null }
|
|
225
|
+
val bodyBytesAb: ArrayBuffer? = if (bodyStr == null && bytes.isNotEmpty())
|
|
226
|
+
bytes.toArrayBuffer()
|
|
227
|
+
else null
|
|
191
228
|
val res = NitroResponse(
|
|
192
229
|
url = info.url,
|
|
193
230
|
status = status.toDouble(),
|
|
@@ -196,7 +233,7 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E
|
|
|
196
233
|
redirected = info.url != url,
|
|
197
234
|
headers = headersArr,
|
|
198
235
|
bodyString = bodyStr,
|
|
199
|
-
bodyBytes =
|
|
236
|
+
bodyBytes = bodyBytesAb
|
|
200
237
|
)
|
|
201
238
|
onSuccess(res)
|
|
202
239
|
} catch (t: Throwable) {
|
|
@@ -366,7 +403,7 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E
|
|
|
366
403
|
throw e.cause ?: e
|
|
367
404
|
}
|
|
368
405
|
}
|
|
369
|
-
FetchCache.getResultIfFresh(key, 5_000L)?.let { cached ->
|
|
406
|
+
FetchCache.getResultIfFresh(key, req.prefetchCacheTtlMs?.toLong() ?: 5_000L)?.let { cached ->
|
|
370
407
|
return withPrefetchedHeader(cached)
|
|
371
408
|
}
|
|
372
409
|
}
|
|
@@ -408,8 +445,8 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E
|
|
|
408
445
|
}
|
|
409
446
|
return promise
|
|
410
447
|
}
|
|
411
|
-
// If a fresh prefetched result exists
|
|
412
|
-
FetchCache.getResultIfFresh(key, 5_000L)?.let { cached ->
|
|
448
|
+
// If a fresh prefetched result exists, return it immediately
|
|
449
|
+
FetchCache.getResultIfFresh(key, req.prefetchCacheTtlMs?.toLong() ?: 5_000L)?.let { cached ->
|
|
413
450
|
promise.resolve(withPrefetchedHeader(cached))
|
|
414
451
|
return promise
|
|
415
452
|
}
|
|
@@ -442,7 +479,7 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E
|
|
|
442
479
|
return promise
|
|
443
480
|
}
|
|
444
481
|
// If already have a fresh result, resolve immediately (NON-DESTRUCTIVE CHECK)
|
|
445
|
-
if (FetchCache.hasFreshResult(key, 5_000L)) {
|
|
482
|
+
if (FetchCache.hasFreshResult(key, req.prefetchCacheTtlMs?.toLong() ?: 5_000L)) {
|
|
446
483
|
promise.resolve(Unit)
|
|
447
484
|
return promise
|
|
448
485
|
}
|
|
@@ -20,6 +20,27 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
20
20
|
prefetchKey:(NSString *)prefetchKey
|
|
21
21
|
headers:(NSDictionary<NSString *, NSString *> *)headers;
|
|
22
22
|
|
|
23
|
+
+ (void)registerPrefetchWithURL:(NSString *)url
|
|
24
|
+
prefetchKey:(NSString *)prefetchKey
|
|
25
|
+
headers:(NSDictionary<NSString *, NSString *> *)headers
|
|
26
|
+
method:(nullable NSString *)method
|
|
27
|
+
bodyString:(nullable NSString *)bodyString
|
|
28
|
+
bodyBytes:(nullable NSString *)bodyBytes
|
|
29
|
+
bodyFormData:(nullable NSArray<NSDictionary<NSString *, NSString *> *> *)bodyFormData
|
|
30
|
+
timeoutMs:(nullable NSNumber *)timeoutMs
|
|
31
|
+
followRedirects:(nullable NSNumber *)followRedirects;
|
|
32
|
+
|
|
33
|
+
+ (void)registerPrefetchWithURL:(NSString *)url
|
|
34
|
+
prefetchKey:(NSString *)prefetchKey
|
|
35
|
+
headers:(NSDictionary<NSString *, NSString *> *)headers
|
|
36
|
+
method:(nullable NSString *)method
|
|
37
|
+
bodyString:(nullable NSString *)bodyString
|
|
38
|
+
bodyBytes:(nullable NSString *)bodyBytes
|
|
39
|
+
bodyFormData:(nullable NSArray<NSDictionary<NSString *, NSString *> *> *)bodyFormData
|
|
40
|
+
timeoutMs:(nullable NSNumber *)timeoutMs
|
|
41
|
+
followRedirects:(nullable NSNumber *)followRedirects
|
|
42
|
+
prefetchCacheTtlMs:(nullable NSNumber *)prefetchCacheTtlMs;
|
|
43
|
+
|
|
23
44
|
@end
|
|
24
45
|
|
|
25
46
|
NS_ASSUME_NONNULL_END
|
|
@@ -20,8 +20,80 @@ public final class NitroAutoPrefetcher: NSObject {
|
|
|
20
20
|
url: String,
|
|
21
21
|
prefetchKey: String,
|
|
22
22
|
headers: [String: String]
|
|
23
|
+
) {
|
|
24
|
+
registerPrefetchInternal(
|
|
25
|
+
url: url, prefetchKey: prefetchKey, headers: headers,
|
|
26
|
+
method: nil, bodyString: nil, bodyBytes: nil,
|
|
27
|
+
bodyFormData: nil, timeoutMs: nil, followRedirects: nil,
|
|
28
|
+
prefetchCacheTtlMs: nil
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@objc(registerPrefetchWithURL:prefetchKey:headers:method:bodyString:bodyBytes:bodyFormData:timeoutMs:followRedirects:)
|
|
33
|
+
public static func registerPrefetch(
|
|
34
|
+
url: String,
|
|
35
|
+
prefetchKey: String,
|
|
36
|
+
headers: [String: String],
|
|
37
|
+
method: String?,
|
|
38
|
+
bodyString: String?,
|
|
39
|
+
bodyBytes: String?,
|
|
40
|
+
bodyFormData: [[String: String]]?,
|
|
41
|
+
timeoutMs: NSNumber?,
|
|
42
|
+
followRedirects: NSNumber?
|
|
43
|
+
) {
|
|
44
|
+
registerPrefetchInternal(
|
|
45
|
+
url: url, prefetchKey: prefetchKey, headers: headers,
|
|
46
|
+
method: method, bodyString: bodyString, bodyBytes: bodyBytes,
|
|
47
|
+
bodyFormData: bodyFormData,
|
|
48
|
+
timeoutMs: timeoutMs?.doubleValue,
|
|
49
|
+
followRedirects: followRedirects?.boolValue,
|
|
50
|
+
prefetchCacheTtlMs: nil
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@objc(registerPrefetchWithURL:prefetchKey:headers:method:bodyString:bodyBytes:bodyFormData:timeoutMs:followRedirects:prefetchCacheTtlMs:)
|
|
55
|
+
public static func registerPrefetch(
|
|
56
|
+
url: String,
|
|
57
|
+
prefetchKey: String,
|
|
58
|
+
headers: [String: String],
|
|
59
|
+
method: String?,
|
|
60
|
+
bodyString: String?,
|
|
61
|
+
bodyBytes: String?,
|
|
62
|
+
bodyFormData: [[String: String]]?,
|
|
63
|
+
timeoutMs: NSNumber?,
|
|
64
|
+
followRedirects: NSNumber?,
|
|
65
|
+
prefetchCacheTtlMs: NSNumber?
|
|
66
|
+
) {
|
|
67
|
+
registerPrefetchInternal(
|
|
68
|
+
url: url, prefetchKey: prefetchKey, headers: headers,
|
|
69
|
+
method: method, bodyString: bodyString, bodyBytes: bodyBytes,
|
|
70
|
+
bodyFormData: bodyFormData,
|
|
71
|
+
timeoutMs: timeoutMs?.doubleValue,
|
|
72
|
+
followRedirects: followRedirects?.boolValue,
|
|
73
|
+
prefetchCacheTtlMs: prefetchCacheTtlMs?.doubleValue
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private static func registerPrefetchInternal(
|
|
78
|
+
url: String,
|
|
79
|
+
prefetchKey: String,
|
|
80
|
+
headers: [String: String],
|
|
81
|
+
method: String?,
|
|
82
|
+
bodyString: String?,
|
|
83
|
+
bodyBytes: String?,
|
|
84
|
+
bodyFormData: [[String: String]]?,
|
|
85
|
+
timeoutMs: Double?,
|
|
86
|
+
followRedirects: Bool?,
|
|
87
|
+
prefetchCacheTtlMs: Double?
|
|
23
88
|
) {
|
|
24
89
|
if url.isEmpty || prefetchKey.isEmpty { return }
|
|
90
|
+
let entry = buildEntryDict(
|
|
91
|
+
url: url, prefetchKey: prefetchKey, headers: headers,
|
|
92
|
+
method: method, bodyString: bodyString, bodyBytes: bodyBytes,
|
|
93
|
+
bodyFormData: bodyFormData, timeoutMs: timeoutMs,
|
|
94
|
+
followRedirects: followRedirects,
|
|
95
|
+
prefetchCacheTtlMs: prefetchCacheTtlMs
|
|
96
|
+
)
|
|
25
97
|
let userDefaults = UserDefaults(suiteName: suiteName) ?? UserDefaults.standard
|
|
26
98
|
|
|
27
99
|
var arr: [[String: Any]] = []
|
|
@@ -32,11 +104,7 @@ public final class NitroAutoPrefetcher: NSObject {
|
|
|
32
104
|
arr = parsed
|
|
33
105
|
}
|
|
34
106
|
arr.removeAll { ($0["prefetchKey"] as? String) == prefetchKey }
|
|
35
|
-
arr.append(
|
|
36
|
-
"url": url,
|
|
37
|
-
"prefetchKey": prefetchKey,
|
|
38
|
-
"headers": headers,
|
|
39
|
-
])
|
|
107
|
+
arr.append(entry)
|
|
40
108
|
if let data = try? JSONSerialization.data(withJSONObject: arr),
|
|
41
109
|
let str = String(data: data, encoding: .utf8) {
|
|
42
110
|
userDefaults.set(str, forKey: queueKey)
|
|
@@ -55,17 +123,7 @@ public final class NitroAutoPrefetcher: NSObject {
|
|
|
55
123
|
for (k, v) in tokenHeaders { merged[k] = v }
|
|
56
124
|
var hdrs: [NitroHeader] = merged.map { NitroHeader(key: $0.key, value: $0.value) }
|
|
57
125
|
hdrs.append(NitroHeader(key: "prefetchKey", value: prefetchKey))
|
|
58
|
-
let req =
|
|
59
|
-
url: url,
|
|
60
|
-
method: nil,
|
|
61
|
-
headers: hdrs,
|
|
62
|
-
bodyString: nil,
|
|
63
|
-
bodyBytes: nil,
|
|
64
|
-
bodyFormData: nil,
|
|
65
|
-
timeoutMs: nil,
|
|
66
|
-
followRedirects: true,
|
|
67
|
-
requestId: nil
|
|
68
|
-
)
|
|
126
|
+
let req = buildNitroRequest(from: entry, mergedHeaders: hdrs)
|
|
69
127
|
Task {
|
|
70
128
|
do { try await NitroFetchClient.prefetchStatic(req) } catch { /* best-effort */ }
|
|
71
129
|
}
|
|
@@ -143,17 +201,7 @@ public final class NitroAutoPrefetcher: NSObject {
|
|
|
143
201
|
print("[NitroFetch][TokenRefresh] Prefetching \(url) with \(merged.count) header(s)")
|
|
144
202
|
for (k, v) in merged { print("[NitroFetch][TokenRefresh] \(k): \(v)") }
|
|
145
203
|
|
|
146
|
-
let req =
|
|
147
|
-
url: url,
|
|
148
|
-
method: nil,
|
|
149
|
-
headers: headers,
|
|
150
|
-
bodyString: nil,
|
|
151
|
-
bodyBytes: nil,
|
|
152
|
-
bodyFormData: nil,
|
|
153
|
-
timeoutMs: nil,
|
|
154
|
-
followRedirects: true,
|
|
155
|
-
requestId: nil
|
|
156
|
-
)
|
|
204
|
+
let req = buildNitroRequest(from: obj, mergedHeaders: headers)
|
|
157
205
|
Task {
|
|
158
206
|
do { try await NitroFetchClient.prefetchStatic(req) } catch { /* ignore – best effort */ }
|
|
159
207
|
}
|
|
@@ -161,6 +209,80 @@ public final class NitroAutoPrefetcher: NSObject {
|
|
|
161
209
|
}
|
|
162
210
|
}
|
|
163
211
|
|
|
212
|
+
private static func buildEntryDict(
|
|
213
|
+
url: String,
|
|
214
|
+
prefetchKey: String,
|
|
215
|
+
headers: [String: String],
|
|
216
|
+
method: String?,
|
|
217
|
+
bodyString: String?,
|
|
218
|
+
bodyBytes: String?,
|
|
219
|
+
bodyFormData: [[String: String]]?,
|
|
220
|
+
timeoutMs: Double?,
|
|
221
|
+
followRedirects: Bool?,
|
|
222
|
+
prefetchCacheTtlMs: Double? = nil
|
|
223
|
+
) -> [String: Any] {
|
|
224
|
+
var entry: [String: Any] = [
|
|
225
|
+
"url": url,
|
|
226
|
+
"prefetchKey": prefetchKey,
|
|
227
|
+
"headers": headers,
|
|
228
|
+
]
|
|
229
|
+
if let method = method, !method.isEmpty, method != "GET" { entry["method"] = method }
|
|
230
|
+
if let bodyString = bodyString { entry["bodyString"] = bodyString }
|
|
231
|
+
if let bodyBytes = bodyBytes { entry["bodyBytes"] = bodyBytes }
|
|
232
|
+
if let parts = bodyFormData, !parts.isEmpty {
|
|
233
|
+
entry["bodyFormData"] = parts.map { part -> [String: String] in
|
|
234
|
+
var clean: [String: String] = [:]
|
|
235
|
+
if let v = part["name"] { clean["name"] = v }
|
|
236
|
+
if let v = part["value"] { clean["value"] = v }
|
|
237
|
+
if let v = part["fileUri"] { clean["fileUri"] = v }
|
|
238
|
+
if let v = part["fileName"] { clean["fileName"] = v }
|
|
239
|
+
if let v = part["mimeType"] { clean["mimeType"] = v }
|
|
240
|
+
return clean
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if let timeoutMs = timeoutMs { entry["timeoutMs"] = timeoutMs }
|
|
244
|
+
if followRedirects == false { entry["followRedirects"] = false }
|
|
245
|
+
if let prefetchCacheTtlMs = prefetchCacheTtlMs { entry["prefetchCacheTtlMs"] = prefetchCacheTtlMs }
|
|
246
|
+
return entry
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private static func buildNitroRequest(
|
|
250
|
+
from entry: [String: Any],
|
|
251
|
+
mergedHeaders: [NitroHeader]
|
|
252
|
+
) -> NitroRequest {
|
|
253
|
+
let url = (entry["url"] as? String) ?? ""
|
|
254
|
+
let methodStr = entry["method"] as? String
|
|
255
|
+
let method: NitroRequestMethod? = methodStr.flatMap { NitroRequestMethod(fromString: $0) }
|
|
256
|
+
let bodyString = entry["bodyString"] as? String
|
|
257
|
+
let bodyBytes = entry["bodyBytes"] as? String
|
|
258
|
+
let timeoutMs = (entry["timeoutMs"] as? NSNumber)?.doubleValue
|
|
259
|
+
let followRedirects = (entry["followRedirects"] as? Bool) ?? true
|
|
260
|
+
let prefetchCacheTtlMs = (entry["prefetchCacheTtlMs"] as? NSNumber)?.doubleValue
|
|
261
|
+
|
|
262
|
+
let formData: [NitroFormDataPart]? = (entry["bodyFormData"] as? [[String: Any]])?.map { p in
|
|
263
|
+
NitroFormDataPart(
|
|
264
|
+
name: (p["name"] as? String) ?? "",
|
|
265
|
+
value: p["value"] as? String,
|
|
266
|
+
fileUri: p["fileUri"] as? String,
|
|
267
|
+
fileName: p["fileName"] as? String,
|
|
268
|
+
mimeType: p["mimeType"] as? String
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return NitroRequest(
|
|
273
|
+
url: url,
|
|
274
|
+
method: method,
|
|
275
|
+
headers: mergedHeaders,
|
|
276
|
+
bodyString: bodyString,
|
|
277
|
+
bodyBytes: bodyBytes,
|
|
278
|
+
bodyFormData: formData,
|
|
279
|
+
timeoutMs: timeoutMs,
|
|
280
|
+
followRedirects: followRedirects,
|
|
281
|
+
prefetchCacheTtlMs: prefetchCacheTtlMs,
|
|
282
|
+
requestId: nil
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
|
|
164
286
|
// MARK: - Token refresh
|
|
165
287
|
|
|
166
288
|
private static func callTokenRefresh(config: [String: Any]) async throws -> [String: String] {
|
|
@@ -119,7 +119,7 @@ final class NitroFetchClient: HybridNitroFetchClientSpec {
|
|
|
119
119
|
public class func requestStatic(_ req: NitroRequest) async throws -> NitroResponse {
|
|
120
120
|
if let key = findPrefetchKey(req) {
|
|
121
121
|
// If a prefetched result is fresh, return immediately
|
|
122
|
-
if let cached = FetchCache.getResultIfFresh(key, maxAgeMs: 5_000) {
|
|
122
|
+
if let cached = FetchCache.getResultIfFresh(key, maxAgeMs: Int64(req.prefetchCacheTtlMs ?? 5_000)) {
|
|
123
123
|
var headers = cached.headers ?? []
|
|
124
124
|
headers.append(NitroHeader(key: "nitroPrefetched", value: "true"))
|
|
125
125
|
return NitroResponse(url: cached.url,
|
|
@@ -231,9 +231,15 @@ final class NitroFetchClient: HybridNitroFetchClientSpec {
|
|
|
231
231
|
}
|
|
232
232
|
#endif
|
|
233
233
|
|
|
234
|
-
// Choose bodyString by default (matching Android’s first pass)
|
|
234
|
+
// Choose bodyString by default (matching Android’s first pass).
|
|
235
|
+
// For binary responses that can’t be decoded as text, bridge the raw bytes
|
|
236
|
+
// as an ArrayBuffer so arrayBuffer() / bytes() return them with no base64.
|
|
235
237
|
let charset = NitroFetchClient.detectCharset(from: http) ?? String.Encoding.utf8
|
|
236
238
|
let bodyStr = String(data: data, encoding: charset) ?? String(data: data, encoding: .utf8)
|
|
239
|
+
var bodyBytesAb: ArrayBuffer? = nil
|
|
240
|
+
if bodyStr == nil && !data.isEmpty {
|
|
241
|
+
bodyBytesAb = try ArrayBuffer.copy(data: data)
|
|
242
|
+
}
|
|
237
243
|
|
|
238
244
|
let res = NitroResponse(
|
|
239
245
|
url: finalURL?.absoluteString ?? http.url?.absoluteString ?? req.url,
|
|
@@ -243,7 +249,7 @@ final class NitroFetchClient: HybridNitroFetchClientSpec {
|
|
|
243
249
|
redirected: (finalURL?.absoluteString ?? http.url?.absoluteString ?? req.url) != req.url,
|
|
244
250
|
headers: headersPairs,
|
|
245
251
|
bodyString: bodyStr,
|
|
246
|
-
bodyBytes:
|
|
252
|
+
bodyBytes: bodyBytesAb
|
|
247
253
|
)
|
|
248
254
|
|
|
249
255
|
// Do not write to cache here; only prefetch should populate the cache
|
|
@@ -261,7 +267,7 @@ final class NitroFetchClient: HybridNitroFetchClientSpec {
|
|
|
261
267
|
throw NSError(domain: "NitroFetch", code: -2, userInfo: [NSLocalizedDescriptionKey: "prefetch: missing 'prefetchKey' header"])
|
|
262
268
|
}
|
|
263
269
|
|
|
264
|
-
if FetchCache.getResultIfFresh(key, maxAgeMs: 5_000) != nil {
|
|
270
|
+
if FetchCache.getResultIfFresh(key, maxAgeMs: Int64(req.prefetchCacheTtlMs ?? 5_000)) != nil {
|
|
265
271
|
return // already have a fresh result
|
|
266
272
|
}
|
|
267
273
|
|
|
@@ -284,6 +290,10 @@ final class NitroFetchClient: HybridNitroFetchClientSpec {
|
|
|
284
290
|
}
|
|
285
291
|
let charset = NitroFetchClient.detectCharset(from: http) ?? .utf8
|
|
286
292
|
let bodyStr = String(data: data, encoding: charset) ?? String(data: data, encoding: .utf8)
|
|
293
|
+
var bodyBytesAb: ArrayBuffer? = nil
|
|
294
|
+
if bodyStr == nil && !data.isEmpty {
|
|
295
|
+
bodyBytesAb = try ArrayBuffer.copy(data: data)
|
|
296
|
+
}
|
|
287
297
|
let res = NitroResponse(
|
|
288
298
|
url: finalURL?.absoluteString ?? http.url?.absoluteString ?? req.url,
|
|
289
299
|
status: Double(http.statusCode),
|
|
@@ -292,7 +302,7 @@ final class NitroFetchClient: HybridNitroFetchClientSpec {
|
|
|
292
302
|
redirected: (finalURL?.absoluteString ?? http.url?.absoluteString ?? req.url) != req.url,
|
|
293
303
|
headers: headersPairs,
|
|
294
304
|
bodyString: bodyStr,
|
|
295
|
-
bodyBytes:
|
|
305
|
+
bodyBytes: bodyBytesAb
|
|
296
306
|
)
|
|
297
307
|
FetchCache.complete(key, with: .success(res))
|
|
298
308
|
} catch {
|
|
@@ -300,7 +310,8 @@ final class NitroFetchClient: HybridNitroFetchClientSpec {
|
|
|
300
310
|
}
|
|
301
311
|
}
|
|
302
312
|
}
|
|
303
|
-
|
|
313
|
+
|
|
314
|
+
|
|
304
315
|
private static func reqToHttpMethod(_ req: NitroRequest) -> String? {
|
|
305
316
|
return req.method?.stringValue
|
|
306
317
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["shellEscape","str","test","replace","generateCurl","options","parts","method","push","headers","h","key","toLowerCase","value","body","maxLen","truncated","length","slice","verbose","compressed","url","join"],"sourceRoot":"../../src","sources":["CurlGenerator.
|
|
1
|
+
{"version":3,"names":["shellEscape","str","test","replace","generateCurl","options","parts","method","push","headers","h","key","toLowerCase","value","body","maxLen","truncated","length","slice","verbose","compressed","url","join"],"sourceRoot":"../../src","sources":["CurlGenerator.js"],"mappings":";;AAAA,SAASA,WAAWA,CAACC,GAAG,EAAE;EACtB,IAAI,0BAA0B,CAACC,IAAI,CAACD,GAAG,CAAC,EACpC,OAAOA,GAAG;EACd,OAAO,GAAG,GAAGA,GAAG,CAACE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,GAAG;AACjD;AACA,OAAO,SAASC,YAAYA,CAACC,OAAO,EAAE;EAClC,MAAMC,KAAK,GAAG,CAAC,MAAM,CAAC;EACtB,IAAID,OAAO,CAACE,MAAM,IAAIF,OAAO,CAACE,MAAM,KAAK,KAAK,EAAE;IAC5CD,KAAK,CAACE,IAAI,CAAC,IAAI,EAAER,WAAW,CAACK,OAAO,CAACE,MAAM,CAAC,CAAC;EACjD;EACA,IAAIF,OAAO,CAACI,OAAO,EAAE;IACjB,KAAK,MAAMC,CAAC,IAAIL,OAAO,CAACI,OAAO,EAAE;MAC7B,IAAIC,CAAC,CAACC,GAAG,CAACC,WAAW,CAAC,CAAC,KAAK,aAAa,EACrC;MACJN,KAAK,CAACE,IAAI,CAAC,IAAI,EAAER,WAAW,CAAC,GAAGU,CAAC,CAACC,GAAG,KAAKD,CAAC,CAACG,KAAK,EAAE,CAAC,CAAC;IACzD;EACJ;EACA,IAAIR,OAAO,CAACS,IAAI,EAAE;IACd,MAAMC,MAAM,GAAG,MAAM;IACrB,MAAMC,SAAS,GAAGX,OAAO,CAACS,IAAI,CAACG,MAAM,GAAGF,MAAM,GACxCV,OAAO,CAACS,IAAI,CAACI,KAAK,CAAC,CAAC,EAAEH,MAAM,CAAC,GAAG,gBAAgB,GAChDV,OAAO,CAACS,IAAI;IAClBR,KAAK,CAACE,IAAI,CAAC,IAAI,EAAER,WAAW,CAACgB,SAAS,CAAC,CAAC;EAC5C;EACA,IAAIX,OAAO,CAACc,OAAO,EACfb,KAAK,CAACE,IAAI,CAAC,IAAI,CAAC;EACpB,IAAIH,OAAO,CAACe,UAAU,EAClBd,KAAK,CAACE,IAAI,CAAC,cAAc,CAAC;EAC9BF,KAAK,CAACE,IAAI,CAACR,WAAW,CAACK,OAAO,CAACgB,GAAG,CAAC,CAAC;EACpC,OAAOf,KAAK,CAACgB,IAAI,CAAC,GAAG,CAAC;AAC1B","ignoreList":[]}
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
{"version":3,"names":["profileFetch","fn","outputPath","hermes","global","HermesInternal","result","path","Date","now","enableSamplingProfiler","profilePath","disableSamplingProfiler","dumpSamplingProfiler"],"sourceRoot":"../../src","sources":["HermesProfiler.
|
|
1
|
+
{"version":3,"names":["profileFetch","fn","outputPath","hermes","global","HermesInternal","result","path","Date","now","enableSamplingProfiler","profilePath","disableSamplingProfiler","dumpSamplingProfiler"],"sourceRoot":"../../src","sources":["HermesProfiler.js"],"mappings":";;AAAA,OAAO,eAAeA,YAAYA,CAACC,EAAE,EAAEC,UAAU,EAAE;EAC/C,MAAMC,MAAM,GAAGC,MAAM,CAACC,cAAc;EACpC,IAAI,CAACF,MAAM,EAAE;IACT,MAAMG,MAAM,GAAG,MAAML,EAAE,CAAC,CAAC;IACzB,OAAO;MAAEK;IAAO,CAAC;EACrB;EACA,MAAMC,IAAI,GAAGL,UAAU,IAAI,2BAA2BM,IAAI,CAACC,GAAG,CAAC,CAAC,aAAa;EAC7EN,MAAM,CAACO,sBAAsB,CAAC,CAAC;EAC/B,IAAI;IACA,MAAMJ,MAAM,GAAG,MAAML,EAAE,CAAC,CAAC;IACzB,OAAO;MAAEK,MAAM;MAAEK,WAAW,EAAEJ;IAAK,CAAC;EACxC,CAAC,SACO;IACJJ,MAAM,CAACS,uBAAuB,CAAC,CAAC;IAChC,IAAI;MACAT,MAAM,CAACU,oBAAoB,CAACN,IAAI,CAAC;IACrC,CAAC,CACD,MAAM;MACF;IAAA;EAER;AACJ","ignoreList":[]}
|
|
2
|
+
[]}
|
|
@@ -53,9 +53,7 @@ class NetworkInspectorImpl {
|
|
|
53
53
|
this._entries.shift();
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
|
|
57
56
|
// --- HTTP recording ---
|
|
58
|
-
|
|
59
57
|
_recordStart(id, url, method, headers, body) {
|
|
60
58
|
if (!this._enabled) return;
|
|
61
59
|
const bodySize = body ? body.length : 0;
|
|
@@ -106,9 +104,7 @@ class NetworkInspectorImpl {
|
|
|
106
104
|
}
|
|
107
105
|
this._notify(entry);
|
|
108
106
|
}
|
|
109
|
-
|
|
110
107
|
// --- WebSocket recording ---
|
|
111
|
-
|
|
112
108
|
_recordWsOpen(id, url, protocols, headers) {
|
|
113
109
|
if (!this._enabled) return;
|
|
114
110
|
const entry = {
|