react-native-nitro-fetch 1.3.0 → 1.3.2
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/build.gradle +12 -0
- package/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt +258 -82
- package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt +4 -4
- package/ios/NitroAutoPrefetcher.h +21 -0
- package/ios/NitroAutoPrefetcher.swift +292 -74
- package/ios/NitroFetchClient.swift +4 -3
- package/lib/module/CurlGenerator.js.map +2 -1
- package/lib/module/NitroCronet.nitro.js.map +1 -1
- package/lib/module/NitroFetch.nitro.js.map +1 -1
- package/lib/module/Request.js.map +2 -1
- package/lib/module/Response.js.map +2 -1
- package/lib/module/fetch.js +30 -11
- package/lib/module/fetch.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +0 -1
- package/lib/module/index.web.js.map +2 -1
- package/lib/module/tokenRefresh.js +1 -4
- package/lib/module/tokenRefresh.js.map +2 -1
- package/lib/module/utf8.js.map +2 -1
- package/lib/typescript/src/NitroFetch.nitro.d.ts +1 -0
- package/lib/typescript/src/NitroFetch.nitro.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/tokenRefresh.d.ts +14 -0
- package/lib/typescript/src/tokenRefresh.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JNitroRequest.hpp +5 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroRequest.kt +5 -2
- package/nitrogen/generated/ios/swift/NitroRequest.swift +19 -1
- package/nitrogen/generated/shared/c++/NitroRequest.hpp +5 -1
- package/package.json +1 -1
- package/src/CurlGenerator.js +28 -0
- package/src/Headers.js +119 -0
- package/src/HermesProfiler.js +20 -0
- package/src/NetworkInspector.js +175 -0
- package/src/NitroCronet.nitro.js +1 -0
- package/src/NitroFetch.nitro.js +1 -0
- package/src/NitroFetch.nitro.ts +3 -0
- package/src/NitroInstances.js +7 -0
- package/src/Request.js +176 -0
- package/src/Response.js +260 -0
- package/src/fetch.js +787 -0
- package/src/fetch.ts +55 -17
- package/src/index.js +25 -0
- package/src/index.tsx +1 -0
- package/src/index.web.js +106 -0
- package/src/tokenRefresh.js +102 -0
- package/src/tokenRefresh.ts +16 -0
- package/src/utf8.js +41 -0
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 {
|
|
@@ -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) {
|
|
@@ -61,28 +65,14 @@ object AutoPrefetcher {
|
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
if (initialized) {
|
|
64
|
-
// late path — kick a single immediate prefetch with cached
|
|
68
|
+
// late path — kick a single immediate prefetch with cached tokens
|
|
65
69
|
try {
|
|
66
70
|
val prefs = context.applicationContext
|
|
67
71
|
.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
68
|
-
val
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
co.keys().asSequence().associateWith { k -> co.optString(k, "") }
|
|
73
|
-
} catch (_: Throwable) { emptyMap() }
|
|
74
|
-
} else emptyMap()
|
|
75
|
-
|
|
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
|
-
}
|
|
85
|
-
startPrefetches(single, tokenHeaders)
|
|
72
|
+
val tokens = deserializeCache(NitroFetchSecureAtRest.getDecryptedForPrefs(prefs, KEY_TOKEN_CACHE))
|
|
73
|
+
|
|
74
|
+
val single = JSONArray().apply { put(entry) }
|
|
75
|
+
startPrefetches(single, tokens)
|
|
86
76
|
} catch (_: Throwable) {}
|
|
87
77
|
}
|
|
88
78
|
}
|
|
@@ -109,13 +99,11 @@ object AutoPrefetcher {
|
|
|
109
99
|
|
|
110
100
|
val refreshed = callTokenRefreshSync(refreshConfig)
|
|
111
101
|
|
|
112
|
-
val
|
|
113
|
-
android.util.Log.d("NitroFetch", "[TokenRefresh] ✅ Success — got ${refreshed.size} header(s)")
|
|
114
|
-
refreshed.forEach { (k, v) -> android.util.Log.d("NitroFetch", "[TokenRefresh] $k: $v") }
|
|
115
|
-
// Cache fresh
|
|
116
|
-
|
|
117
|
-
refreshed.forEach { (k, v) -> cacheJson.put(k, v) }
|
|
118
|
-
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))
|
|
119
107
|
refreshed
|
|
120
108
|
} else {
|
|
121
109
|
android.util.Log.d("NitroFetch", "[TokenRefresh] ❌ Refresh failed — onFailure: $onFailure")
|
|
@@ -123,36 +111,28 @@ object AutoPrefetcher {
|
|
|
123
111
|
android.util.Log.d("NitroFetch", "[TokenRefresh] Skipping all prefetches")
|
|
124
112
|
return@Thread
|
|
125
113
|
}
|
|
126
|
-
// Use last cached
|
|
127
|
-
val
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
val co = JSONObject(cacheRaw)
|
|
131
|
-
co.keys().asSequence().associateWith { k -> co.optString(k, "") }
|
|
132
|
-
} catch (_: Throwable) { emptyMap() }
|
|
133
|
-
} else {
|
|
134
|
-
emptyMap()
|
|
135
|
-
}
|
|
136
|
-
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))")
|
|
137
117
|
cached
|
|
138
118
|
}
|
|
139
119
|
|
|
140
120
|
android.util.Log.d("NitroFetch", "[TokenRefresh] Injecting token headers into ${arr.length()} prefetch URL(s)")
|
|
141
|
-
startPrefetches(arr,
|
|
121
|
+
startPrefetches(arr, tokens)
|
|
142
122
|
} catch (_: Throwable) {
|
|
143
123
|
// Best-effort — never crash the app
|
|
144
124
|
}
|
|
145
125
|
}.start()
|
|
146
126
|
} else {
|
|
147
127
|
// No token refresh config — proceed on current thread (Cronet is async)
|
|
148
|
-
startPrefetches(arr,
|
|
128
|
+
startPrefetches(arr, TokenRefreshResult.EMPTY)
|
|
149
129
|
}
|
|
150
130
|
} catch (_: Throwable) {
|
|
151
131
|
// ignore – prefetch-on-start is best-effort
|
|
152
132
|
}
|
|
153
133
|
}
|
|
154
134
|
|
|
155
|
-
private fun startPrefetches(arr: JSONArray,
|
|
135
|
+
private fun startPrefetches(arr: JSONArray, tokens: TokenRefreshResult) {
|
|
156
136
|
for (i in 0 until arr.length()) {
|
|
157
137
|
val o = arr.optJSONObject(i) ?: continue
|
|
158
138
|
val url = o.optString("url", null) ?: continue
|
|
@@ -164,26 +144,18 @@ object AutoPrefetcher {
|
|
|
164
144
|
headersObj.keys().forEachRemaining { k ->
|
|
165
145
|
merged[k] = headersObj.optString(k, "")
|
|
166
146
|
}
|
|
167
|
-
|
|
147
|
+
tokens.headers.forEach { (k, v) -> merged[k] = v }
|
|
168
148
|
merged["prefetchKey"] = prefetchKey
|
|
169
149
|
|
|
170
150
|
android.util.Log.d("NitroFetch", "[TokenRefresh] Prefetching $url with ${merged.size} header(s)")
|
|
171
151
|
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
|
-
)
|
|
152
|
+
val req = buildNitroRequestFromEntry(url, merged, o, tokens)
|
|
184
153
|
|
|
185
154
|
if (FetchCache.getPending(prefetchKey) != null) continue
|
|
186
|
-
if (
|
|
155
|
+
val entryTtlMs = if (o.has("prefetchCacheTtlMs") && !o.isNull("prefetchCacheTtlMs")) {
|
|
156
|
+
o.optDouble("prefetchCacheTtlMs").toLong()
|
|
157
|
+
} else 5_000L
|
|
158
|
+
if (FetchCache.hasFreshResult(prefetchKey, entryTtlMs)) continue
|
|
187
159
|
|
|
188
160
|
val future = CompletableFuture<NitroResponse>()
|
|
189
161
|
FetchCache.setPending(prefetchKey, future)
|
|
@@ -205,9 +177,118 @@ object AutoPrefetcher {
|
|
|
205
177
|
}
|
|
206
178
|
}
|
|
207
179
|
|
|
180
|
+
private fun buildEntryJson(
|
|
181
|
+
url: String,
|
|
182
|
+
prefetchKey: String,
|
|
183
|
+
headers: Map<String, String>,
|
|
184
|
+
method: String?,
|
|
185
|
+
bodyString: String?,
|
|
186
|
+
bodyBytes: String?,
|
|
187
|
+
bodyFormData: List<Map<String, String?>>?,
|
|
188
|
+
timeoutMs: Double?,
|
|
189
|
+
followRedirects: Boolean?,
|
|
190
|
+
prefetchCacheTtlMs: Double? = null,
|
|
191
|
+
): JSONObject {
|
|
192
|
+
val headersObj = JSONObject()
|
|
193
|
+
headers.forEach { (k, v) -> headersObj.put(k, v) }
|
|
194
|
+
return JSONObject().apply {
|
|
195
|
+
put("url", url)
|
|
196
|
+
put("prefetchKey", prefetchKey)
|
|
197
|
+
put("headers", headersObj)
|
|
198
|
+
if (method != null && method.isNotEmpty() && method != "GET") put("method", method)
|
|
199
|
+
if (bodyString != null) put("bodyString", bodyString)
|
|
200
|
+
if (bodyBytes != null) put("bodyBytes", bodyBytes)
|
|
201
|
+
if (!bodyFormData.isNullOrEmpty()) {
|
|
202
|
+
val arr = JSONArray()
|
|
203
|
+
bodyFormData.forEach { part ->
|
|
204
|
+
val obj = JSONObject()
|
|
205
|
+
part["name"]?.let { obj.put("name", it) }
|
|
206
|
+
part["value"]?.let { obj.put("value", it) }
|
|
207
|
+
part["fileUri"]?.let { obj.put("fileUri", it) }
|
|
208
|
+
part["fileName"]?.let { obj.put("fileName", it) }
|
|
209
|
+
part["mimeType"]?.let { obj.put("mimeType", it) }
|
|
210
|
+
arr.put(obj)
|
|
211
|
+
}
|
|
212
|
+
put("bodyFormData", arr)
|
|
213
|
+
}
|
|
214
|
+
if (timeoutMs != null) put("timeoutMs", timeoutMs)
|
|
215
|
+
if (followRedirects == false) put("followRedirects", false)
|
|
216
|
+
if (prefetchCacheTtlMs != null) put("prefetchCacheTtlMs", prefetchCacheTtlMs)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private fun buildNitroRequestFromEntry(
|
|
221
|
+
url: String,
|
|
222
|
+
mergedHeaders: Map<String, String>,
|
|
223
|
+
entry: JSONObject?,
|
|
224
|
+
tokens: TokenRefreshResult = TokenRefreshResult.EMPTY,
|
|
225
|
+
): NitroRequest {
|
|
226
|
+
val headerObjs = mergedHeaders.map { (k, v) -> NitroHeader(k, v) }.toTypedArray()
|
|
227
|
+
|
|
228
|
+
val methodStr = entry?.optString("method", "")?.takeIf { it.isNotEmpty() }
|
|
229
|
+
val method: NitroRequestMethod? = methodStr?.let {
|
|
230
|
+
runCatching { NitroRequestMethod.valueOf(it) }.getOrNull()
|
|
231
|
+
}
|
|
232
|
+
val rawBodyString = entry
|
|
233
|
+
?.takeIf { it.has("bodyString") && !it.isNull("bodyString") }
|
|
234
|
+
?.optString("bodyString")
|
|
235
|
+
val bodyString = injectBodyFields(rawBodyString, tokens.bodyFields)
|
|
236
|
+
val bodyBytes = entry
|
|
237
|
+
?.takeIf { it.has("bodyBytes") && !it.isNull("bodyBytes") }
|
|
238
|
+
?.optString("bodyBytes")
|
|
239
|
+
val timeoutMs = entry
|
|
240
|
+
?.takeIf { it.has("timeoutMs") && !it.isNull("timeoutMs") }
|
|
241
|
+
?.optDouble("timeoutMs")
|
|
242
|
+
val followRedirects = entry
|
|
243
|
+
?.takeIf { it.has("followRedirects") && !it.isNull("followRedirects") }
|
|
244
|
+
?.optBoolean("followRedirects")
|
|
245
|
+
val prefetchCacheTtlMs = entry
|
|
246
|
+
?.takeIf { it.has("prefetchCacheTtlMs") && !it.isNull("prefetchCacheTtlMs") }
|
|
247
|
+
?.optDouble("prefetchCacheTtlMs")
|
|
248
|
+
|
|
249
|
+
val formArr = entry?.optJSONArray("bodyFormData")
|
|
250
|
+
val baseParts: List<NitroFormDataPart> = formArr?.let { ja ->
|
|
251
|
+
List(ja.length()) { i ->
|
|
252
|
+
val p = ja.optJSONObject(i) ?: JSONObject()
|
|
253
|
+
NitroFormDataPart(
|
|
254
|
+
name = p.optString("name", ""),
|
|
255
|
+
value = if (p.has("value") && !p.isNull("value")) p.optString("value") else null,
|
|
256
|
+
fileUri = if (p.has("fileUri") && !p.isNull("fileUri")) p.optString("fileUri") else null,
|
|
257
|
+
fileName = if (p.has("fileName") && !p.isNull("fileName")) p.optString("fileName") else null,
|
|
258
|
+
mimeType = if (p.has("mimeType") && !p.isNull("mimeType")) p.optString("mimeType") else null
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
} ?: emptyList()
|
|
262
|
+
val bodyFormData: Array<NitroFormDataPart>? =
|
|
263
|
+
injectFormFields(baseParts, tokens.formFields)?.toTypedArray()
|
|
264
|
+
|
|
265
|
+
return NitroRequest(
|
|
266
|
+
url = url,
|
|
267
|
+
method = method,
|
|
268
|
+
headers = headerObjs,
|
|
269
|
+
bodyString = bodyString,
|
|
270
|
+
bodyBytes = bodyBytes,
|
|
271
|
+
bodyFormData = bodyFormData,
|
|
272
|
+
timeoutMs = timeoutMs,
|
|
273
|
+
followRedirects = followRedirects,
|
|
274
|
+
prefetchCacheTtlMs = prefetchCacheTtlMs,
|
|
275
|
+
requestId = null
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
208
279
|
// MARK: - Token refresh (synchronous, runs on background thread)
|
|
209
280
|
|
|
210
|
-
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? {
|
|
211
292
|
return try {
|
|
212
293
|
val urlStr = config.optString("url", null) ?: return null
|
|
213
294
|
val method = config.optString("method", "POST")
|
|
@@ -255,33 +336,31 @@ object AutoPrefetcher {
|
|
|
255
336
|
body: String,
|
|
256
337
|
responseType: String,
|
|
257
338
|
config: JSONObject
|
|
258
|
-
):
|
|
259
|
-
val
|
|
339
|
+
): TokenRefreshResult {
|
|
340
|
+
val headers = mutableMapOf<String, String>()
|
|
341
|
+
val bodyFields = mutableMapOf<String, String>()
|
|
342
|
+
val formFields = mutableMapOf<String, String>()
|
|
260
343
|
|
|
261
344
|
if (responseType == "text") {
|
|
262
345
|
val textHeader = config.optString("textHeader", null)
|
|
263
346
|
if (textHeader != null) {
|
|
264
347
|
val textTemplate = config.optString("textTemplate", null)
|
|
265
|
-
|
|
348
|
+
headers[textHeader] = textTemplate?.replace("{{value}}", body) ?: body
|
|
266
349
|
}
|
|
267
|
-
|
|
350
|
+
config.optString("bodyTextPath", null)?.let { bodyFields[it] = body }
|
|
351
|
+
config.optString("formDataTextField", null)?.let { formFields[it] = body }
|
|
352
|
+
return TokenRefreshResult(headers, bodyFields, formFields)
|
|
268
353
|
}
|
|
269
354
|
|
|
270
355
|
// JSON
|
|
271
|
-
val json = try { JSONObject(body) } catch (_: Throwable) {
|
|
272
|
-
|
|
273
|
-
val mappings = config.optJSONArray("mappings")
|
|
274
|
-
if (mappings != null) {
|
|
275
|
-
for (i in 0 until mappings.length()) {
|
|
276
|
-
val m = mappings.optJSONObject(i) ?: continue
|
|
277
|
-
val jsonPath = m.optString("jsonPath", null) ?: continue
|
|
278
|
-
val header = m.optString("header", null) ?: continue
|
|
279
|
-
val value = getNestedField(json, jsonPath) ?: continue
|
|
280
|
-
val tmpl = m.optString("valueTemplate", null)
|
|
281
|
-
result[header] = tmpl?.replace("{{value}}", value) ?: value
|
|
282
|
-
}
|
|
356
|
+
val json = try { JSONObject(body) } catch (_: Throwable) {
|
|
357
|
+
return TokenRefreshResult(headers, bodyFields, formFields)
|
|
283
358
|
}
|
|
284
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
|
+
|
|
285
364
|
val compositeHeaders = config.optJSONArray("compositeHeaders")
|
|
286
365
|
if (compositeHeaders != null) {
|
|
287
366
|
for (i in 0 until compositeHeaders.length()) {
|
|
@@ -294,11 +373,29 @@ object AutoPrefetcher {
|
|
|
294
373
|
val val2 = getNestedField(json, paths.optString(ph, ""))
|
|
295
374
|
built = built.replace("{{$ph}}", val2 ?: "")
|
|
296
375
|
}
|
|
297
|
-
|
|
376
|
+
headers[header] = built
|
|
298
377
|
}
|
|
299
378
|
}
|
|
300
379
|
|
|
301
|
-
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
|
+
}
|
|
302
399
|
}
|
|
303
400
|
|
|
304
401
|
private fun getNestedField(obj: JSONObject, dotPath: String): String? {
|
|
@@ -310,4 +407,83 @@ object AutoPrefetcher {
|
|
|
310
407
|
}
|
|
311
408
|
return current.toString()
|
|
312
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
|
+
}
|
|
313
489
|
}
|
|
@@ -403,7 +403,7 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E
|
|
|
403
403
|
throw e.cause ?: e
|
|
404
404
|
}
|
|
405
405
|
}
|
|
406
|
-
FetchCache.getResultIfFresh(key, 5_000L)?.let { cached ->
|
|
406
|
+
FetchCache.getResultIfFresh(key, req.prefetchCacheTtlMs?.toLong() ?: 5_000L)?.let { cached ->
|
|
407
407
|
return withPrefetchedHeader(cached)
|
|
408
408
|
}
|
|
409
409
|
}
|
|
@@ -445,8 +445,8 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E
|
|
|
445
445
|
}
|
|
446
446
|
return promise
|
|
447
447
|
}
|
|
448
|
-
// If a fresh prefetched result exists
|
|
449
|
-
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 ->
|
|
450
450
|
promise.resolve(withPrefetchedHeader(cached))
|
|
451
451
|
return promise
|
|
452
452
|
}
|
|
@@ -479,7 +479,7 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E
|
|
|
479
479
|
return promise
|
|
480
480
|
}
|
|
481
481
|
// If already have a fresh result, resolve immediately (NON-DESTRUCTIVE CHECK)
|
|
482
|
-
if (FetchCache.hasFreshResult(key, 5_000L)) {
|
|
482
|
+
if (FetchCache.hasFreshResult(key, req.prefetchCacheTtlMs?.toLong() ?: 5_000L)) {
|
|
483
483
|
promise.resolve(Unit)
|
|
484
484
|
return promise
|
|
485
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
|