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.
Files changed (60) hide show
  1. package/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt +114 -31
  2. package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt +43 -6
  3. package/ios/NitroAutoPrefetcher.h +21 -0
  4. package/ios/NitroAutoPrefetcher.swift +149 -27
  5. package/ios/NitroFetchClient.swift +17 -6
  6. package/lib/module/CurlGenerator.js.map +1 -1
  7. package/lib/module/HermesProfiler.js.map +2 -1
  8. package/lib/module/NetworkInspector.js +0 -4
  9. package/lib/module/NetworkInspector.js.map +1 -1
  10. package/lib/module/NitroCronet.nitro.js.map +1 -1
  11. package/lib/module/NitroFetch.nitro.js.map +1 -0
  12. package/lib/module/NitroInstances.js.map +1 -1
  13. package/lib/module/Response.js +3 -4
  14. package/lib/module/Response.js.map +2 -1
  15. package/lib/module/fetch.js +26 -6
  16. package/lib/module/fetch.js.map +1 -1
  17. package/lib/module/index.js +1 -1
  18. package/lib/module/index.js.map +2 -1
  19. package/lib/module/index.web.js +1 -2
  20. package/lib/module/utf8.js +19 -8
  21. package/lib/module/utf8.js.map +2 -1
  22. package/lib/typescript/src/NitroFetch.nitro.d.ts +2 -1
  23. package/lib/typescript/src/NitroFetch.nitro.d.ts.map +1 -1
  24. package/lib/typescript/src/Response.d.ts.map +1 -1
  25. package/lib/typescript/src/fetch.d.ts +4 -1
  26. package/lib/typescript/src/fetch.d.ts.map +1 -1
  27. package/lib/typescript/src/index.d.ts +1 -1
  28. package/lib/typescript/src/index.d.ts.map +1 -1
  29. package/lib/typescript/src/utf8.d.ts.map +1 -1
  30. package/nitrogen/generated/android/c++/JHybridNitroFetchClientSpec.cpp +2 -0
  31. package/nitrogen/generated/android/c++/JNitroRequest.hpp +5 -1
  32. package/nitrogen/generated/android/c++/JNitroResponse.hpp +7 -5
  33. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroRequest.kt +5 -2
  34. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroResponse.kt +3 -3
  35. package/nitrogen/generated/ios/NitroFetch-Swift-Cxx-Bridge.hpp +15 -0
  36. package/nitrogen/generated/ios/c++/HybridNitroFetchClientSpecSwift.hpp +4 -0
  37. package/nitrogen/generated/ios/swift/NitroRequest.swift +19 -1
  38. package/nitrogen/generated/ios/swift/NitroResponse.swift +8 -8
  39. package/nitrogen/generated/shared/c++/NitroRequest.hpp +5 -1
  40. package/nitrogen/generated/shared/c++/NitroResponse.hpp +6 -5
  41. package/package.json +18 -3
  42. package/src/CurlGenerator.js +31 -0
  43. package/src/Headers.js +127 -0
  44. package/src/HermesProfiler.js +22 -0
  45. package/src/NetworkInspector.js +183 -0
  46. package/src/NitroCronet.nitro.js +1 -0
  47. package/src/NitroFetch.nitro.js +1 -0
  48. package/src/NitroFetch.nitro.ts +4 -2
  49. package/src/NitroInstances.js +6 -0
  50. package/src/Request.js +173 -0
  51. package/src/Response.js +258 -0
  52. package/src/Response.ts +2 -1
  53. package/src/fetch.js +772 -0
  54. package/src/fetch.ts +46 -9
  55. package/src/index.js +10 -0
  56. package/src/index.tsx +1 -0
  57. package/src/index.web.js +104 -0
  58. package/src/tokenRefresh.js +104 -0
  59. package/src/utf8.js +40 -0
  60. 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 headerObjs = merged.map { (k, v) -> NitroHeader(k, v) }.toTypedArray()
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 (FetchCache.hasFreshResult(prefetchKey, 5_000L)) continue
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
- val bodyStr = try { String(bytes, charset) } catch (_: Throwable) { String(bytes, Charsets.UTF_8) }
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 = null
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 (<=5s old), return it immediately
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 = NitroRequest(
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 = NitroRequest(
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: nil
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: nil
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.ts"],"mappings":";;AASA,SAASA,WAAWA,CAACC,GAAW,EAAU;EACxC,IAAI,0BAA0B,CAACC,IAAI,CAACD,GAAG,CAAC,EAAE,OAAOA,GAAG;EACpD,OAAO,GAAG,GAAGA,GAAG,CAACE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,GAAG;AAC/C;AAEA,OAAO,SAASC,YAAYA,CAACC,OAAoB,EAAU;EACzD,MAAMC,KAAe,GAAG,CAAC,MAAM,CAAC;EAEhC,IAAID,OAAO,CAACE,MAAM,IAAIF,OAAO,CAACE,MAAM,KAAK,KAAK,EAAE;IAC9CD,KAAK,CAACE,IAAI,CAAC,IAAI,EAAER,WAAW,CAACK,OAAO,CAACE,MAAM,CAAC,CAAC;EAC/C;EAEA,IAAIF,OAAO,CAACI,OAAO,EAAE;IACnB,KAAK,MAAMC,CAAC,IAAIL,OAAO,CAACI,OAAO,EAAE;MAC/B,IAAIC,CAAC,CAACC,GAAG,CAACC,WAAW,CAAC,CAAC,KAAK,aAAa,EAAE;MAC3CN,KAAK,CAACE,IAAI,CAAC,IAAI,EAAER,WAAW,CAAC,GAAGU,CAAC,CAACC,GAAG,KAAKD,CAAC,CAACG,KAAK,EAAE,CAAC,CAAC;IACvD;EACF;EAEA,IAAIR,OAAO,CAACS,IAAI,EAAE;IAChB,MAAMC,MAAM,GAAG,MAAM;IACrB,MAAMC,SAAS,GACbX,OAAO,CAACS,IAAI,CAACG,MAAM,GAAGF,MAAM,GACxBV,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;EAC1C;EAEA,IAAIX,OAAO,CAACc,OAAO,EAAEb,KAAK,CAACE,IAAI,CAAC,IAAI,CAAC;EACrC,IAAIH,OAAO,CAACe,UAAU,EAAEd,KAAK,CAACE,IAAI,CAAC,cAAc,CAAC;EAElDF,KAAK,CAACE,IAAI,CAACR,WAAW,CAACK,OAAO,CAACgB,GAAG,CAAC,CAAC;EAEpC,OAAOf,KAAK,CAACgB,IAAI,CAAC,GAAG,CAAC;AACxB","ignoreList":[]}
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.ts"],"mappings":";;AAaA,OAAO,eAAeA,YAAYA,CAChCC,EAAoB,EACpBC,UAAmB,EACQ;EAC3B,MAAMC,MAAM,GAAGC,MAAM,CAACC,cAAc;EACpC,IAAI,CAACF,MAAM,EAAE;IACX,MAAMG,MAAM,GAAG,MAAML,EAAE,CAAC,CAAC;IACzB,OAAO;MAAEK;IAAO,CAAC;EACnB;EAEA,MAAMC,IAAI,GAAGL,UAAU,IAAI,2BAA2BM,IAAI,CAACC,GAAG,CAAC,CAAC,aAAa;EAC7EN,MAAM,CAACO,sBAAsB,CAAC,CAAC;EAC/B,IAAI;IACF,MAAMJ,MAAM,GAAG,MAAML,EAAE,CAAC,CAAC;IACzB,OAAO;MAAEK,MAAM;MAAEK,WAAW,EAAEJ;IAAK,CAAC;EACtC,CAAC,SAAS;IACRJ,MAAM,CAACS,uBAAuB,CAAC,CAAC;IAChC,IAAI;MACFT,MAAM,CAACU,oBAAoB,CAACN,IAAI,CAAC;IACnC,CAAC,CAAC,MAAM;MACN;IAAA;EAEJ;AACF","ignoreList":[]}
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 = {