react-native-nitro-fetch 1.3.1 → 1.3.3
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 +1 -3
- package/README.md +38 -0
- package/android/build.gradle +12 -0
- package/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt +148 -55
- package/android/src/main/java/com/margelo/nitro/nitrofetch/DevToolsReporterImpl.kt +27 -36
- package/ios/NitroAutoPrefetcher.swift +149 -53
- package/ios/NitroDevToolsReporter.mm +37 -31
- package/lib/module/CurlGenerator.js.map +2 -1
- package/lib/module/Headers.js.map +2 -1
- package/lib/module/HermesProfiler.js.map +1 -1
- package/lib/module/NetworkInspector.js +1 -1
- package/lib/module/NetworkInspector.js.map +2 -1
- package/lib/module/NitroCronet.nitro.js.map +2 -1
- package/lib/module/NitroFetch.nitro.js.map +1 -2
- package/lib/module/NitroInstances.js.map +2 -1
- package/lib/module/Response.js.map +1 -2
- package/lib/module/fetch.js +8 -10
- package/lib/module/fetch.js.map +2 -1
- package/lib/module/index.js.map +2 -2
- package/lib/module/index.web.js +2 -1
- package/lib/module/tokenRefresh.js.map +2 -1
- package/lib/module/utf8.js.map +1 -2
- package/lib/typescript/src/fetch.d.ts.map +1 -1
- package/lib/typescript/src/tokenRefresh.d.ts +14 -0
- package/lib/typescript/src/tokenRefresh.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/CurlGenerator.js +23 -26
- package/src/Headers.js +108 -116
- package/src/HermesProfiler.js +16 -18
- package/src/NetworkInspector.js +171 -179
- package/src/NitroInstances.js +2 -1
- package/src/Request.js +167 -164
- package/src/Response.js +244 -242
- package/src/fetch.js +708 -693
- package/src/fetch.ts +15 -16
- package/src/index.js +17 -2
- package/src/index.web.js +69 -67
- package/src/tokenRefresh.js +75 -77
- package/src/tokenRefresh.ts +16 -0
- package/src/utf8.js +28 -27
package/NitroFetch.podspec
CHANGED
|
@@ -22,9 +22,7 @@ Pod::Spec.new do |s|
|
|
|
22
22
|
|
|
23
23
|
s.dependency 'React-jsi'
|
|
24
24
|
s.dependency 'React-callinvoker'
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
# Expose the DevTools reporter Obj-C facade to Swift via the pod's umbrella module.
|
|
25
|
+
|
|
28
26
|
s.public_header_files = [
|
|
29
27
|
"ios/NitroDevToolsReporter.h",
|
|
30
28
|
"ios/NitroAutoPrefetcher.h",
|
package/README.md
CHANGED
|
@@ -134,8 +134,46 @@ registerTokenRefresh({
|
|
|
134
134
|
- Default `responseType` is `'json'`. Use **`mappings`** to copy fields from the JSON body into header names (dot paths supported, e.g. `data.token`).
|
|
135
135
|
- Use **`compositeHeaders`** to build a header from a template and multiple JSON paths (`{{placeholder}}` in the template).
|
|
136
136
|
- For a plain-text body, set `responseType: 'text'` and use **`textHeader`** / optional **`textTemplate`** (with `{{value}}`).
|
|
137
|
+
- To inject into the prefetch's **JSON body** or **form-data** (the `fetch` path only), use **`bodyMappings`** / **`formDataMappings`** (see below).
|
|
137
138
|
|
|
139
|
+
**Example: header + JSON body + form-data mappings**
|
|
138
140
|
|
|
141
|
+
One config can fan the same refreshed value into all three destinations. Each mapping only applies to a prefetch whose body it matches — `bodyMappings` won't synthesize a JSON body on a GET/form request, and `formDataMappings` won't make a request multipart.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import { registerTokenRefresh } from 'react-native-nitro-fetch'
|
|
145
|
+
|
|
146
|
+
// Refresh endpoint returns e.g. { "data": { "accessToken": "abc123" } }
|
|
147
|
+
registerTokenRefresh({
|
|
148
|
+
target: 'fetch',
|
|
149
|
+
url: 'https://api.example.com/oauth/token',
|
|
150
|
+
method: 'POST',
|
|
151
|
+
responseType: 'json',
|
|
152
|
+
|
|
153
|
+
// 1) header mapping → sets a request header
|
|
154
|
+
mappings: [
|
|
155
|
+
{ jsonPath: 'data.accessToken', header: 'Authorization', valueTemplate: 'Bearer {{value}}' },
|
|
156
|
+
],
|
|
157
|
+
|
|
158
|
+
// 2) JSON-body mapping → sets a (nested) dot-path key in the prefetch's JSON body
|
|
159
|
+
bodyMappings: [
|
|
160
|
+
{ jsonPath: 'data.accessToken', bodyPath: 'auth.token' },
|
|
161
|
+
],
|
|
162
|
+
|
|
163
|
+
// 3) form-data mapping → replaces/appends a multipart field by name
|
|
164
|
+
formDataMappings: [
|
|
165
|
+
{ jsonPath: 'data.accessToken', field: 'token' },
|
|
166
|
+
],
|
|
167
|
+
|
|
168
|
+
onFailure: 'useStoredHeaders',
|
|
169
|
+
})
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Result, per prefetch:
|
|
173
|
+
- a JSON prefetch of `{ "deviceId": "d-1" }` → `Authorization: Bearer abc123` **and** body `{ "deviceId": "d-1", "auth": { "token": "abc123" } }`
|
|
174
|
+
- a form-data prefetch → `Authorization: Bearer abc123` **and** a `token=abc123` part
|
|
175
|
+
|
|
176
|
+
> For `responseType: 'text'`, the body/form equivalents of `textHeader` are **`bodyTextPath`** / **`formDataTextField`**.
|
|
139
177
|
|
|
140
178
|
**Example: token refresh + WebSocket prewarm**
|
|
141
179
|
|
package/android/build.gradle
CHANGED
|
@@ -85,6 +85,18 @@ android {
|
|
|
85
85
|
release {
|
|
86
86
|
minifyEnabled false
|
|
87
87
|
}
|
|
88
|
+
|
|
89
|
+
// Reverting due to build failure
|
|
90
|
+
// // The app's RN-managed `debugOptimized` build type (added by the React Native
|
|
91
|
+
// // gradle plugin, app-only) falls back to this library's `release` variant, where
|
|
92
|
+
// // BuildConfig.DEBUG is false — which constant-folds out the native DevTools/CDP
|
|
93
|
+
// // network reporting in NitroFetchClient.kt. Define a matching debuggable variant
|
|
94
|
+
// // so debugOptimized links a BuildConfig.DEBUG=true build and requests surface in
|
|
95
|
+
// // the React Native DevTools Network panel.
|
|
96
|
+
// debugOptimized {
|
|
97
|
+
// initWith debug
|
|
98
|
+
// matchingFallbacks += ["release"]
|
|
99
|
+
// }
|
|
88
100
|
}
|
|
89
101
|
|
|
90
102
|
lintOptions {
|
|
@@ -65,20 +65,14 @@ object AutoPrefetcher {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
if (initialized) {
|
|
68
|
-
// late path — kick a single immediate prefetch with cached
|
|
68
|
+
// late path — kick a single immediate prefetch with cached tokens
|
|
69
69
|
try {
|
|
70
70
|
val prefs = context.applicationContext
|
|
71
71
|
.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
72
|
-
val
|
|
73
|
-
val tokenHeaders: Map<String, String> = if (!cacheRaw.isNullOrEmpty()) {
|
|
74
|
-
try {
|
|
75
|
-
val co = JSONObject(cacheRaw)
|
|
76
|
-
co.keys().asSequence().associateWith { k -> co.optString(k, "") }
|
|
77
|
-
} catch (_: Throwable) { emptyMap() }
|
|
78
|
-
} else emptyMap()
|
|
72
|
+
val tokens = deserializeCache(NitroFetchSecureAtRest.getDecryptedForPrefs(prefs, KEY_TOKEN_CACHE))
|
|
79
73
|
|
|
80
74
|
val single = JSONArray().apply { put(entry) }
|
|
81
|
-
startPrefetches(single,
|
|
75
|
+
startPrefetches(single, tokens)
|
|
82
76
|
} catch (_: Throwable) {}
|
|
83
77
|
}
|
|
84
78
|
}
|
|
@@ -105,13 +99,11 @@ object AutoPrefetcher {
|
|
|
105
99
|
|
|
106
100
|
val refreshed = callTokenRefreshSync(refreshConfig)
|
|
107
101
|
|
|
108
|
-
val
|
|
109
|
-
android.util.Log.d("NitroFetch", "[TokenRefresh] ✅ Success — got ${refreshed.size} header(s)")
|
|
110
|
-
refreshed.forEach { (k, v) -> android.util.Log.d("NitroFetch", "[TokenRefresh] $k: $v") }
|
|
111
|
-
// Cache fresh
|
|
112
|
-
|
|
113
|
-
refreshed.forEach { (k, v) -> cacheJson.put(k, v) }
|
|
114
|
-
NitroFetchSecureAtRest.putEncrypted(prefs, KEY_TOKEN_CACHE, cacheJson.toString())
|
|
102
|
+
val tokens: TokenRefreshResult = if (refreshed != null) {
|
|
103
|
+
android.util.Log.d("NitroFetch", "[TokenRefresh] ✅ Success — got ${refreshed.headers.size} header(s)")
|
|
104
|
+
refreshed.headers.forEach { (k, v) -> android.util.Log.d("NitroFetch", "[TokenRefresh] $k: $v") }
|
|
105
|
+
// Cache fresh tokens for useStoredHeaders fallback on next cold start
|
|
106
|
+
NitroFetchSecureAtRest.putEncrypted(prefs, KEY_TOKEN_CACHE, serializeCache(refreshed))
|
|
115
107
|
refreshed
|
|
116
108
|
} else {
|
|
117
109
|
android.util.Log.d("NitroFetch", "[TokenRefresh] ❌ Refresh failed — onFailure: $onFailure")
|
|
@@ -119,36 +111,28 @@ object AutoPrefetcher {
|
|
|
119
111
|
android.util.Log.d("NitroFetch", "[TokenRefresh] Skipping all prefetches")
|
|
120
112
|
return@Thread
|
|
121
113
|
}
|
|
122
|
-
// Use last cached
|
|
123
|
-
val
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
val co = JSONObject(cacheRaw)
|
|
127
|
-
co.keys().asSequence().associateWith { k -> co.optString(k, "") }
|
|
128
|
-
} catch (_: Throwable) { emptyMap() }
|
|
129
|
-
} else {
|
|
130
|
-
emptyMap()
|
|
131
|
-
}
|
|
132
|
-
android.util.Log.d("NitroFetch", "[TokenRefresh] Using cached headers (${cached.size} header(s))")
|
|
114
|
+
// Use last cached tokens (or empty if none cached yet)
|
|
115
|
+
val cached = deserializeCache(NitroFetchSecureAtRest.getDecryptedForPrefs(prefs, KEY_TOKEN_CACHE))
|
|
116
|
+
android.util.Log.d("NitroFetch", "[TokenRefresh] Using cached headers (${cached.headers.size} header(s))")
|
|
133
117
|
cached
|
|
134
118
|
}
|
|
135
119
|
|
|
136
120
|
android.util.Log.d("NitroFetch", "[TokenRefresh] Injecting token headers into ${arr.length()} prefetch URL(s)")
|
|
137
|
-
startPrefetches(arr,
|
|
121
|
+
startPrefetches(arr, tokens)
|
|
138
122
|
} catch (_: Throwable) {
|
|
139
123
|
// Best-effort — never crash the app
|
|
140
124
|
}
|
|
141
125
|
}.start()
|
|
142
126
|
} else {
|
|
143
127
|
// No token refresh config — proceed on current thread (Cronet is async)
|
|
144
|
-
startPrefetches(arr,
|
|
128
|
+
startPrefetches(arr, TokenRefreshResult.EMPTY)
|
|
145
129
|
}
|
|
146
130
|
} catch (_: Throwable) {
|
|
147
131
|
// ignore – prefetch-on-start is best-effort
|
|
148
132
|
}
|
|
149
133
|
}
|
|
150
134
|
|
|
151
|
-
private fun startPrefetches(arr: JSONArray,
|
|
135
|
+
private fun startPrefetches(arr: JSONArray, tokens: TokenRefreshResult) {
|
|
152
136
|
for (i in 0 until arr.length()) {
|
|
153
137
|
val o = arr.optJSONObject(i) ?: continue
|
|
154
138
|
val url = o.optString("url", null) ?: continue
|
|
@@ -160,12 +144,12 @@ object AutoPrefetcher {
|
|
|
160
144
|
headersObj.keys().forEachRemaining { k ->
|
|
161
145
|
merged[k] = headersObj.optString(k, "")
|
|
162
146
|
}
|
|
163
|
-
|
|
147
|
+
tokens.headers.forEach { (k, v) -> merged[k] = v }
|
|
164
148
|
merged["prefetchKey"] = prefetchKey
|
|
165
149
|
|
|
166
150
|
android.util.Log.d("NitroFetch", "[TokenRefresh] Prefetching $url with ${merged.size} header(s)")
|
|
167
151
|
merged.forEach { (k, v) -> android.util.Log.d("NitroFetch", "[TokenRefresh] $k: $v") }
|
|
168
|
-
val req = buildNitroRequestFromEntry(url, merged, o)
|
|
152
|
+
val req = buildNitroRequestFromEntry(url, merged, o, tokens)
|
|
169
153
|
|
|
170
154
|
if (FetchCache.getPending(prefetchKey) != null) continue
|
|
171
155
|
val entryTtlMs = if (o.has("prefetchCacheTtlMs") && !o.isNull("prefetchCacheTtlMs")) {
|
|
@@ -237,6 +221,7 @@ object AutoPrefetcher {
|
|
|
237
221
|
url: String,
|
|
238
222
|
mergedHeaders: Map<String, String>,
|
|
239
223
|
entry: JSONObject?,
|
|
224
|
+
tokens: TokenRefreshResult = TokenRefreshResult.EMPTY,
|
|
240
225
|
): NitroRequest {
|
|
241
226
|
val headerObjs = mergedHeaders.map { (k, v) -> NitroHeader(k, v) }.toTypedArray()
|
|
242
227
|
|
|
@@ -244,9 +229,10 @@ object AutoPrefetcher {
|
|
|
244
229
|
val method: NitroRequestMethod? = methodStr?.let {
|
|
245
230
|
runCatching { NitroRequestMethod.valueOf(it) }.getOrNull()
|
|
246
231
|
}
|
|
247
|
-
val
|
|
232
|
+
val rawBodyString = entry
|
|
248
233
|
?.takeIf { it.has("bodyString") && !it.isNull("bodyString") }
|
|
249
234
|
?.optString("bodyString")
|
|
235
|
+
val bodyString = injectBodyFields(rawBodyString, tokens.bodyFields)
|
|
250
236
|
val bodyBytes = entry
|
|
251
237
|
?.takeIf { it.has("bodyBytes") && !it.isNull("bodyBytes") }
|
|
252
238
|
?.optString("bodyBytes")
|
|
@@ -261,8 +247,8 @@ object AutoPrefetcher {
|
|
|
261
247
|
?.optDouble("prefetchCacheTtlMs")
|
|
262
248
|
|
|
263
249
|
val formArr = entry?.optJSONArray("bodyFormData")
|
|
264
|
-
val
|
|
265
|
-
|
|
250
|
+
val baseParts: List<NitroFormDataPart> = formArr?.let { ja ->
|
|
251
|
+
List(ja.length()) { i ->
|
|
266
252
|
val p = ja.optJSONObject(i) ?: JSONObject()
|
|
267
253
|
NitroFormDataPart(
|
|
268
254
|
name = p.optString("name", ""),
|
|
@@ -272,7 +258,9 @@ object AutoPrefetcher {
|
|
|
272
258
|
mimeType = if (p.has("mimeType") && !p.isNull("mimeType")) p.optString("mimeType") else null
|
|
273
259
|
)
|
|
274
260
|
}
|
|
275
|
-
}
|
|
261
|
+
} ?: emptyList()
|
|
262
|
+
val bodyFormData: Array<NitroFormDataPart>? =
|
|
263
|
+
injectFormFields(baseParts, tokens.formFields)?.toTypedArray()
|
|
276
264
|
|
|
277
265
|
return NitroRequest(
|
|
278
266
|
url = url,
|
|
@@ -290,7 +278,17 @@ object AutoPrefetcher {
|
|
|
290
278
|
|
|
291
279
|
// MARK: - Token refresh (synchronous, runs on background thread)
|
|
292
280
|
|
|
293
|
-
private
|
|
281
|
+
private data class TokenRefreshResult(
|
|
282
|
+
val headers: Map<String, String>,
|
|
283
|
+
val bodyFields: Map<String, String>,
|
|
284
|
+
val formFields: Map<String, String>,
|
|
285
|
+
) {
|
|
286
|
+
companion object {
|
|
287
|
+
val EMPTY = TokenRefreshResult(emptyMap(), emptyMap(), emptyMap())
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private fun callTokenRefreshSync(config: JSONObject): TokenRefreshResult? {
|
|
294
292
|
return try {
|
|
295
293
|
val urlStr = config.optString("url", null) ?: return null
|
|
296
294
|
val method = config.optString("method", "POST")
|
|
@@ -338,33 +336,31 @@ object AutoPrefetcher {
|
|
|
338
336
|
body: String,
|
|
339
337
|
responseType: String,
|
|
340
338
|
config: JSONObject
|
|
341
|
-
):
|
|
342
|
-
val
|
|
339
|
+
): TokenRefreshResult {
|
|
340
|
+
val headers = mutableMapOf<String, String>()
|
|
341
|
+
val bodyFields = mutableMapOf<String, String>()
|
|
342
|
+
val formFields = mutableMapOf<String, String>()
|
|
343
343
|
|
|
344
344
|
if (responseType == "text") {
|
|
345
345
|
val textHeader = config.optString("textHeader", null)
|
|
346
346
|
if (textHeader != null) {
|
|
347
347
|
val textTemplate = config.optString("textTemplate", null)
|
|
348
|
-
|
|
348
|
+
headers[textHeader] = textTemplate?.replace("{{value}}", body) ?: body
|
|
349
349
|
}
|
|
350
|
-
|
|
350
|
+
config.optString("bodyTextPath", null)?.let { bodyFields[it] = body }
|
|
351
|
+
config.optString("formDataTextField", null)?.let { formFields[it] = body }
|
|
352
|
+
return TokenRefreshResult(headers, bodyFields, formFields)
|
|
351
353
|
}
|
|
352
354
|
|
|
353
355
|
// JSON
|
|
354
|
-
val json = try { JSONObject(body) } catch (_: Throwable) {
|
|
355
|
-
|
|
356
|
-
val mappings = config.optJSONArray("mappings")
|
|
357
|
-
if (mappings != null) {
|
|
358
|
-
for (i in 0 until mappings.length()) {
|
|
359
|
-
val m = mappings.optJSONObject(i) ?: continue
|
|
360
|
-
val jsonPath = m.optString("jsonPath", null) ?: continue
|
|
361
|
-
val header = m.optString("header", null) ?: continue
|
|
362
|
-
val value = getNestedField(json, jsonPath) ?: continue
|
|
363
|
-
val tmpl = m.optString("valueTemplate", null)
|
|
364
|
-
result[header] = tmpl?.replace("{{value}}", value) ?: value
|
|
365
|
-
}
|
|
356
|
+
val json = try { JSONObject(body) } catch (_: Throwable) {
|
|
357
|
+
return TokenRefreshResult(headers, bodyFields, formFields)
|
|
366
358
|
}
|
|
367
359
|
|
|
360
|
+
collectMappings(json, config.optJSONArray("mappings"), "header", headers)
|
|
361
|
+
collectMappings(json, config.optJSONArray("bodyMappings"), "bodyPath", bodyFields)
|
|
362
|
+
collectMappings(json, config.optJSONArray("formDataMappings"), "field", formFields)
|
|
363
|
+
|
|
368
364
|
val compositeHeaders = config.optJSONArray("compositeHeaders")
|
|
369
365
|
if (compositeHeaders != null) {
|
|
370
366
|
for (i in 0 until compositeHeaders.length()) {
|
|
@@ -377,11 +373,29 @@ object AutoPrefetcher {
|
|
|
377
373
|
val val2 = getNestedField(json, paths.optString(ph, ""))
|
|
378
374
|
built = built.replace("{{$ph}}", val2 ?: "")
|
|
379
375
|
}
|
|
380
|
-
|
|
376
|
+
headers[header] = built
|
|
381
377
|
}
|
|
382
378
|
}
|
|
383
379
|
|
|
384
|
-
return
|
|
380
|
+
return TokenRefreshResult(headers, bodyFields, formFields)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// jsonPath -> value (optionally templated), keyed by the mapping's `destKey` field.
|
|
384
|
+
private fun collectMappings(
|
|
385
|
+
json: JSONObject,
|
|
386
|
+
arr: JSONArray?,
|
|
387
|
+
destKey: String,
|
|
388
|
+
into: MutableMap<String, String>,
|
|
389
|
+
) {
|
|
390
|
+
if (arr == null) return
|
|
391
|
+
for (i in 0 until arr.length()) {
|
|
392
|
+
val m = arr.optJSONObject(i) ?: continue
|
|
393
|
+
val jsonPath = m.optString("jsonPath", null) ?: continue
|
|
394
|
+
val dest = m.optString(destKey, null) ?: continue
|
|
395
|
+
val value = getNestedField(json, jsonPath) ?: continue
|
|
396
|
+
val tmpl = m.optString("valueTemplate", null)
|
|
397
|
+
into[dest] = tmpl?.replace("{{value}}", value) ?: value
|
|
398
|
+
}
|
|
385
399
|
}
|
|
386
400
|
|
|
387
401
|
private fun getNestedField(obj: JSONObject, dotPath: String): String? {
|
|
@@ -393,4 +407,83 @@ object AutoPrefetcher {
|
|
|
393
407
|
}
|
|
394
408
|
return current.toString()
|
|
395
409
|
}
|
|
410
|
+
|
|
411
|
+
private fun setNestedField(root: JSONObject, dotPath: String, value: String) {
|
|
412
|
+
val parts = dotPath.split(".")
|
|
413
|
+
var current = root
|
|
414
|
+
for (i in 0 until parts.size - 1) {
|
|
415
|
+
val key = parts[i]
|
|
416
|
+
val existing = current.optJSONObject(key)
|
|
417
|
+
current = existing ?: JSONObject().also { current.put(key, it) }
|
|
418
|
+
}
|
|
419
|
+
current.put(parts.last(), value)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private fun injectBodyFields(rawBody: String?, fields: Map<String, String>): String? {
|
|
423
|
+
if (fields.isEmpty()) return rawBody
|
|
424
|
+
// Don't synthesize a JSON body where there wasn't one (e.g. a GET or a
|
|
425
|
+
// form-data request) — only rewrite an existing JSON body.
|
|
426
|
+
if (rawBody.isNullOrEmpty()) return rawBody
|
|
427
|
+
val root = try {
|
|
428
|
+
JSONObject(rawBody)
|
|
429
|
+
} catch (_: Throwable) {
|
|
430
|
+
return rawBody
|
|
431
|
+
}
|
|
432
|
+
fields.forEach { (path, value) -> setNestedField(root, path, value) }
|
|
433
|
+
return root.toString()
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private fun injectFormFields(
|
|
437
|
+
parts: List<NitroFormDataPart>,
|
|
438
|
+
fields: Map<String, String>,
|
|
439
|
+
): List<NitroFormDataPart>? {
|
|
440
|
+
if (fields.isEmpty()) return parts.ifEmpty { null }
|
|
441
|
+
// Don't synthesize a multipart body where there wasn't one.
|
|
442
|
+
if (parts.isEmpty()) return null
|
|
443
|
+
val result = parts.toMutableList()
|
|
444
|
+
fields.forEach { (name, value) ->
|
|
445
|
+
val idx = result.indexOfFirst { it.name == name }
|
|
446
|
+
if (idx >= 0) {
|
|
447
|
+
val old = result[idx]
|
|
448
|
+
result[idx] = NitroFormDataPart(
|
|
449
|
+
name = old.name, value = value,
|
|
450
|
+
fileUri = null, fileName = old.fileName, mimeType = old.mimeType
|
|
451
|
+
)
|
|
452
|
+
} else {
|
|
453
|
+
result.add(NitroFormDataPart(name, value, null, null, null))
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return result
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private fun mapToJson(map: Map<String, String>): JSONObject {
|
|
460
|
+
val o = JSONObject()
|
|
461
|
+
map.forEach { (k, v) -> o.put(k, v) }
|
|
462
|
+
return o
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private fun jsonToMap(obj: JSONObject?): Map<String, String> {
|
|
466
|
+
if (obj == null) return emptyMap()
|
|
467
|
+
return obj.keys().asSequence().associateWith { k -> obj.optString(k, "") }
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private fun serializeCache(result: TokenRefreshResult): String =
|
|
471
|
+
JSONObject().apply {
|
|
472
|
+
put("headers", mapToJson(result.headers))
|
|
473
|
+
put("bodyFields", mapToJson(result.bodyFields))
|
|
474
|
+
put("formFields", mapToJson(result.formFields))
|
|
475
|
+
}.toString()
|
|
476
|
+
|
|
477
|
+
private fun deserializeCache(raw: String?): TokenRefreshResult {
|
|
478
|
+
if (raw.isNullOrEmpty()) return TokenRefreshResult.EMPTY
|
|
479
|
+
val o = try { JSONObject(raw) } catch (_: Throwable) { return TokenRefreshResult.EMPTY }
|
|
480
|
+
if (!o.has("headers") && !o.has("bodyFields") && !o.has("formFields")) {
|
|
481
|
+
return TokenRefreshResult(jsonToMap(o), emptyMap(), emptyMap())
|
|
482
|
+
}
|
|
483
|
+
return TokenRefreshResult(
|
|
484
|
+
headers = jsonToMap(o.optJSONObject("headers")),
|
|
485
|
+
bodyFields = jsonToMap(o.optJSONObject("bodyFields")),
|
|
486
|
+
formFields = jsonToMap(o.optJSONObject("formFields")),
|
|
487
|
+
)
|
|
488
|
+
}
|
|
396
489
|
}
|
|
@@ -1,67 +1,58 @@
|
|
|
1
1
|
package com.margelo.nitro.nitrofetch
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* from loading — the facade itself stays intact and degrades to a no-op.
|
|
7
|
-
*
|
|
8
|
-
* Once loaded, every call here is a plain JVM static invoke. No reflection, no boxing,
|
|
9
|
-
* no method-handle lookups. The `@Suppress` annotations bypass RN's `internal` visibility
|
|
10
|
-
* (RN has no public surface here) — this is the documented integration point.
|
|
11
|
-
*/
|
|
12
|
-
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
|
3
|
+
import java.lang.reflect.Method
|
|
4
|
+
|
|
5
|
+
|
|
13
6
|
internal class DevToolsReporterImpl : DevToolsReporter.Impl {
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
private val cls = Class.forName("com.facebook.react.modules.network.InspectorNetworkReporter")
|
|
8
|
+
|
|
9
|
+
private fun m(name: String): Method = cls.methods.first { it.name == name }
|
|
10
|
+
|
|
11
|
+
// All targets are `public static final` on the reporter object — invoke with a null receiver.
|
|
12
|
+
private val mIsDebuggingEnabled = m("isDebuggingEnabled")
|
|
13
|
+
private val mReportRequestStart = m("reportRequestStart")
|
|
14
|
+
private val mReportConnectionTiming = m("reportConnectionTiming")
|
|
15
|
+
private val mReportResponseStart = m("reportResponseStart")
|
|
16
|
+
private val mReportDataReceivedImpl = m("reportDataReceivedImpl")
|
|
17
|
+
private val mReportResponseEnd = m("reportResponseEnd")
|
|
18
|
+
private val mReportRequestFailed = m("reportRequestFailed")
|
|
19
|
+
private val mMaybeStoreResponseBody = m("maybeStoreResponseBody")
|
|
20
|
+
private val mMaybeStoreResponseBodyIncremental = m("maybeStoreResponseBodyIncremental")
|
|
21
|
+
|
|
22
|
+
override fun isDebuggingEnabled(): Boolean = mIsDebuggingEnabled.invoke(null) as Boolean
|
|
16
23
|
|
|
17
24
|
override fun reportRequestStart(
|
|
18
25
|
requestId: String, url: String, method: String,
|
|
19
26
|
headers: Map<String, String>, body: String, encodedDataLength: Long
|
|
20
27
|
) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
)
|
|
24
|
-
com.facebook.react.modules.network.InspectorNetworkReporter.reportConnectionTiming(
|
|
25
|
-
requestId, headers
|
|
26
|
-
)
|
|
28
|
+
mReportRequestStart.invoke(null, requestId, url, method, headers, body, encodedDataLength)
|
|
29
|
+
mReportConnectionTiming.invoke(null, requestId, headers)
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
override fun reportResponseStart(
|
|
30
33
|
requestId: String, url: String, statusCode: Int,
|
|
31
34
|
headers: Map<String, String>, expectedDataLength: Long
|
|
32
35
|
) {
|
|
33
|
-
|
|
34
|
-
requestId, url, statusCode, headers, expectedDataLength
|
|
35
|
-
)
|
|
36
|
+
mReportResponseStart.invoke(null, requestId, url, statusCode, headers, expectedDataLength)
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
override fun reportDataReceived(requestId: String, length: Int) {
|
|
39
|
-
|
|
40
|
-
requestId, length
|
|
41
|
-
)
|
|
40
|
+
mReportDataReceivedImpl.invoke(null, requestId, length)
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
override fun reportResponseEnd(requestId: String, encodedDataLength: Long) {
|
|
45
|
-
|
|
46
|
-
requestId, encodedDataLength
|
|
47
|
-
)
|
|
44
|
+
mReportResponseEnd.invoke(null, requestId, encodedDataLength)
|
|
48
45
|
}
|
|
49
46
|
|
|
50
47
|
override fun reportRequestFailed(requestId: String, cancelled: Boolean) {
|
|
51
|
-
|
|
52
|
-
requestId, cancelled
|
|
53
|
-
)
|
|
48
|
+
mReportRequestFailed.invoke(null, requestId, cancelled)
|
|
54
49
|
}
|
|
55
50
|
|
|
56
51
|
override fun storeResponseBody(requestId: String, body: String, base64Encoded: Boolean) {
|
|
57
|
-
|
|
58
|
-
requestId, body, base64Encoded
|
|
59
|
-
)
|
|
52
|
+
mMaybeStoreResponseBody.invoke(null, requestId, body, base64Encoded)
|
|
60
53
|
}
|
|
61
54
|
|
|
62
55
|
override fun storeResponseBodyIncremental(requestId: String, data: String) {
|
|
63
|
-
|
|
64
|
-
requestId, data
|
|
65
|
-
)
|
|
56
|
+
mMaybeStoreResponseBodyIncremental.invoke(null, requestId, data)
|
|
66
57
|
}
|
|
67
58
|
}
|