react-native-nitro-fetch 1.3.2 → 1.4.0

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 (39) hide show
  1. package/NitroFetch.podspec +1 -3
  2. package/README.md +39 -11
  3. package/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt +6 -2
  4. package/android/src/main/java/com/margelo/nitro/nitrofetch/DevToolsReporterImpl.kt +27 -36
  5. package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt +45 -0
  6. package/ios/NitroAutoPrefetcher.swift +4 -0
  7. package/ios/NitroDevToolsReporter.mm +37 -31
  8. package/ios/NitroFetchClient.swift +56 -0
  9. package/lib/module/CurlGenerator.js.map +1 -2
  10. package/lib/module/Headers.js.map +2 -1
  11. package/lib/module/HermesProfiler.js.map +2 -1
  12. package/lib/module/NetworkInspector.js +1 -5
  13. package/lib/module/NetworkInspector.js.map +2 -1
  14. package/lib/module/NitroCronet.nitro.js.map +2 -1
  15. package/lib/module/NitroFetch.nitro.js.map +2 -1
  16. package/lib/module/NitroInstances.js.map +2 -1
  17. package/lib/module/Request.js.map +1 -2
  18. package/lib/module/Response.js.map +2 -2
  19. package/lib/module/fetch.js +147 -1
  20. package/lib/module/fetch.js.map +1 -1
  21. package/lib/module/index.web.js +1 -0
  22. package/lib/module/index.web.js.map +1 -2
  23. package/lib/module/tokenRefresh.js.map +1 -2
  24. package/lib/module/utf8.js.map +1 -2
  25. package/lib/typescript/src/fetch.d.ts.map +1 -1
  26. package/package.json +1 -1
  27. package/src/CurlGenerator.js +26 -23
  28. package/src/Headers.js +116 -108
  29. package/src/HermesProfiler.js +18 -16
  30. package/src/NetworkInspector.js +179 -171
  31. package/src/NitroInstances.js +1 -2
  32. package/src/Request.js +164 -167
  33. package/src/Response.js +242 -244
  34. package/src/fetch.js +842 -706
  35. package/src/fetch.ts +170 -1
  36. package/src/index.js +2 -17
  37. package/src/index.web.js +67 -69
  38. package/src/tokenRefresh.js +77 -75
  39. package/src/utf8.js +27 -28
package/src/fetch.js CHANGED
@@ -1,8 +1,5 @@
1
1
  import 'web-streams-polyfill/polyfill';
2
- import {
3
- NitroFetch as NitroFetchSingleton,
4
- NitroCronetSingleton,
5
- } from './NitroInstances';
2
+ import { NitroFetch as NitroFetchSingleton, NitroCronetSingleton, } from './NitroInstances';
6
3
  import { NativeStorage as NativeStorageSingleton } from './NitroInstances';
7
4
  import { NitroHeaders } from './Headers';
8
5
  import { NitroResponse } from './Response';
@@ -10,777 +7,916 @@ import { NitroRequest as NitroRequestClass } from './Request';
10
7
  import { NetworkInspector } from './NetworkInspector';
11
8
  // No base64: pass strings/ArrayBuffers directly
12
9
  function headersToPairs(headers) {
13
- 'worklet';
14
- if (!headers) return undefined;
15
- const pairs = [];
16
- if (headers instanceof Headers) {
17
- headers.forEach((v, k) => pairs.push({ key: k, value: v }));
18
- return pairs;
19
- }
20
- if (Array.isArray(headers)) {
21
- // Convert tuple pairs to objects if needed
22
- for (const entry of headers) {
23
- if (Array.isArray(entry) && entry.length >= 2) {
24
- pairs.push({ key: String(entry[0]), value: String(entry[1]) });
25
- } else if (
26
- entry &&
27
- typeof entry === 'object' &&
28
- 'key' in entry &&
29
- 'value' in entry
30
- ) {
31
- pairs.push(entry);
32
- }
10
+ 'worklet';
11
+ if (!headers)
12
+ return undefined;
13
+ const pairs = [];
14
+ if (headers instanceof Headers) {
15
+ headers.forEach((v, k) => pairs.push({ key: k, value: v }));
16
+ return pairs;
33
17
  }
34
- return pairs;
35
- }
36
- // Check if it's a plain object (Record<string, string>) first
37
- // Plain objects don't have forEach, so check for its absence
38
- if (typeof headers === 'object' && headers !== null) {
39
- // Check if it's a Headers instance by checking for forEach method
40
- const hasForEach = typeof headers.forEach === 'function';
41
- if (hasForEach) {
42
- // Headers-like object (duck typing)
43
- headers.forEach((v, k) => pairs.push({ key: k, value: v }));
44
- return pairs;
45
- } else {
46
- // Plain object (Record<string, string>)
47
- // Use Object.keys to iterate since Object.entries might not work in worklets
48
- const keys = Object.keys(headers);
49
- for (let i = 0; i < keys.length; i++) {
50
- const k = keys[i];
51
- const v = headers[k];
52
- if (v !== undefined) {
53
- pairs.push({ key: k, value: String(v) });
18
+ if (Array.isArray(headers)) {
19
+ // Convert tuple pairs to objects if needed
20
+ for (const entry of headers) {
21
+ if (Array.isArray(entry) && entry.length >= 2) {
22
+ pairs.push({ key: String(entry[0]), value: String(entry[1]) });
23
+ }
24
+ else if (entry &&
25
+ typeof entry === 'object' &&
26
+ 'key' in entry &&
27
+ 'value' in entry) {
28
+ pairs.push(entry);
29
+ }
54
30
  }
55
- }
56
- return pairs;
31
+ return pairs;
57
32
  }
58
- }
59
- return pairs;
33
+ // Check if it's a plain object (Record<string, string>) first
34
+ // Plain objects don't have forEach, so check for its absence
35
+ if (typeof headers === 'object' && headers !== null) {
36
+ // Check if it's a Headers instance by checking for forEach method
37
+ const hasForEach = typeof headers.forEach === 'function';
38
+ if (hasForEach) {
39
+ // Headers-like object (duck typing)
40
+ headers.forEach((v, k) => pairs.push({ key: k, value: v }));
41
+ return pairs;
42
+ }
43
+ else {
44
+ // Plain object (Record<string, string>)
45
+ // Use Object.keys to iterate since Object.entries might not work in worklets
46
+ const keys = Object.keys(headers);
47
+ for (let i = 0; i < keys.length; i++) {
48
+ const k = keys[i];
49
+ const v = headers[k];
50
+ if (v !== undefined) {
51
+ pairs.push({ key: k, value: String(v) });
52
+ }
53
+ }
54
+ return pairs;
55
+ }
56
+ }
57
+ return pairs;
60
58
  }
61
59
  function serializeFormData(fd) {
62
- const parts = [];
63
- if (typeof fd.getParts === 'function') {
64
- const rnParts = fd.getParts();
65
- for (const part of rnParts) {
66
- if (part.string !== undefined) {
67
- parts.push({ name: part.fieldName, value: String(part.string) });
68
- } else if (part.uri) {
69
- parts.push({
70
- name: part.fieldName,
71
- fileUri: part.uri,
72
- fileName: part.fileName ?? part.name ?? 'file',
73
- mimeType: part.type ?? 'application/octet-stream',
74
- });
75
- }
60
+ const parts = [];
61
+ if (typeof fd.getParts === 'function') {
62
+ const rnParts = fd.getParts();
63
+ for (const part of rnParts) {
64
+ if (part.string !== undefined) {
65
+ parts.push({ name: part.fieldName, value: String(part.string) });
66
+ }
67
+ else if (part.uri) {
68
+ parts.push({
69
+ name: part.fieldName,
70
+ fileUri: part.uri,
71
+ fileName: part.fileName ?? part.name ?? 'file',
72
+ mimeType: part.type ?? 'application/octet-stream',
73
+ });
74
+ }
75
+ }
76
+ return parts;
76
77
  }
78
+ fd.forEach((value, key) => {
79
+ if (typeof value === 'string') {
80
+ parts.push({ name: key, value });
81
+ }
82
+ else if (value && typeof value === 'object') {
83
+ parts.push({
84
+ name: key,
85
+ fileUri: value.uri ?? value.fileUri,
86
+ fileName: value.name ?? value.fileName ?? 'file',
87
+ mimeType: value.type ?? value.mimeType ?? 'application/octet-stream',
88
+ });
89
+ }
90
+ });
77
91
  return parts;
78
- }
79
- fd.forEach((value, key) => {
80
- if (typeof value === 'string') {
81
- parts.push({ name: key, value });
82
- } else if (value && typeof value === 'object') {
83
- parts.push({
84
- name: key,
85
- fileUri: value.uri ?? value.fileUri,
86
- fileName: value.name ?? value.fileName ?? 'file',
87
- mimeType: value.type ?? value.mimeType ?? 'application/octet-stream',
88
- });
89
- }
90
- });
91
- return parts;
92
92
  }
93
93
  function isFormData(body) {
94
- if (typeof FormData !== 'undefined' && body instanceof FormData) return true;
95
- if (
96
- body &&
97
- typeof body === 'object' &&
98
- typeof body.append === 'function' &&
99
- typeof body.getParts === 'function'
100
- ) {
101
- return true;
102
- }
103
- return false;
94
+ if (typeof FormData !== 'undefined' && body instanceof FormData)
95
+ return true;
96
+ if (body &&
97
+ typeof body === 'object' &&
98
+ typeof body.append === 'function' &&
99
+ typeof body.getParts === 'function') {
100
+ return true;
101
+ }
102
+ return false;
104
103
  }
105
104
  function normalizeBody(body) {
106
- 'worklet';
107
- if (body == null) return undefined;
108
- if (typeof body === 'string') return { bodyString: body };
109
- if (isFormData(body)) {
110
- return { bodyFormData: serializeFormData(body) };
111
- }
112
- if (body instanceof URLSearchParams) return { bodyString: body.toString() };
113
- if (typeof ArrayBuffer !== 'undefined' && body instanceof ArrayBuffer)
114
- return { bodyBytes: body };
115
- if (ArrayBuffer.isView(body)) {
116
- const view = body;
117
- return {
118
- //@ts-ignore
119
- bodyBytes: view.buffer.slice(
120
- view.byteOffset,
121
- view.byteOffset + view.byteLength
122
- ),
123
- };
124
- }
125
- throw new Error('Unsupported body type for nitro fetch');
105
+ 'worklet';
106
+ if (body == null)
107
+ return undefined;
108
+ if (typeof body === 'string')
109
+ return { bodyString: body };
110
+ if (isFormData(body)) {
111
+ return { bodyFormData: serializeFormData(body) };
112
+ }
113
+ if (body instanceof URLSearchParams)
114
+ return { bodyString: body.toString() };
115
+ if (typeof ArrayBuffer !== 'undefined' && body instanceof ArrayBuffer)
116
+ return { bodyBytes: body };
117
+ if (ArrayBuffer.isView(body)) {
118
+ const view = body;
119
+ return {
120
+ //@ts-ignore
121
+ bodyBytes: view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength),
122
+ };
123
+ }
124
+ throw new Error('Unsupported body type for nitro fetch');
126
125
  }
127
126
  const NitroFetchHybrid = NitroFetchSingleton;
128
127
  let client;
129
128
  function ensureClient() {
130
- if (client) return client;
131
- try {
132
- client = NitroFetchHybrid.createClient();
133
- } catch (err) {
134
- console.error('Failed to create NitroFetch client', err);
135
- // native not ready; keep undefined
136
- }
137
- return client;
129
+ if (client)
130
+ return client;
131
+ try {
132
+ client = NitroFetchHybrid.createClient();
133
+ }
134
+ catch (err) {
135
+ console.error('Failed to create NitroFetch client', err);
136
+ // native not ready; keep undefined
137
+ }
138
+ return client;
138
139
  }
139
140
  function buildNitroRequest(input, init) {
140
- 'worklet';
141
- let url;
142
- let method;
143
- let headersInit;
144
- let body;
145
- let redirectOption = init?.redirect ?? 'follow';
146
- let cacheOption = init?.cache;
147
- if (input instanceof NitroRequestClass) {
148
- url = input.url;
149
- method = init?.method ?? input.method;
150
- headersInit = init?.headers ?? input.headers;
151
- body = init?.body ?? input.body ?? null;
152
- if (!init?.redirect) redirectOption = input.redirect;
153
- if (!init?.cache) cacheOption = input.cache;
154
- } else if (typeof input === 'string' || input instanceof URL) {
155
- url = String(input);
156
- method = init?.method;
157
- headersInit = init?.headers;
158
- body = init?.body ?? null;
159
- } else {
160
- // Standard Request object
161
- url = input.url;
162
- method = input.method;
163
- headersInit = input.headers;
164
- body = init?.body ?? null;
165
- }
166
- const headers = headersToPairs(headersInit) ?? [];
167
- const normalized = normalizeBody(body);
168
- // Inject cache-control headers based on cache option
169
- if (cacheOption === 'no-store') {
170
- headers.push({ key: 'Cache-Control', value: 'no-store' });
171
- } else if (cacheOption === 'no-cache') {
172
- headers.push({ key: 'Cache-Control', value: 'no-cache' });
173
- } else if (cacheOption === 'reload') {
174
- headers.push({ key: 'Cache-Control', value: 'no-cache' });
175
- headers.push({ key: 'Pragma', value: 'no-cache' });
176
- }
177
- // Determine followRedirects based on redirect option
178
- const followRedirects = redirectOption === 'follow';
179
- const prefetchCacheTtlMs =
180
- typeof init?.prefetchCacheTtlMs === 'number'
181
- ? init.prefetchCacheTtlMs
182
- : undefined;
183
- return {
184
- url,
185
- method: method?.toUpperCase() ?? 'GET',
186
- headers: headers.length > 0 ? headers : undefined,
187
- bodyString: normalized?.bodyString,
188
- bodyBytes: undefined,
189
- bodyFormData: normalized?.bodyFormData,
190
- followRedirects,
191
- prefetchCacheTtlMs,
192
- };
141
+ 'worklet';
142
+ let url;
143
+ let method;
144
+ let headersInit;
145
+ let body;
146
+ let redirectOption = init?.redirect ?? 'follow';
147
+ let cacheOption = init?.cache;
148
+ if (input instanceof NitroRequestClass) {
149
+ url = input.url;
150
+ method = init?.method ?? input.method;
151
+ headersInit = init?.headers ?? input.headers;
152
+ body = init?.body ?? input.body ?? null;
153
+ if (!init?.redirect)
154
+ redirectOption = input.redirect;
155
+ if (!init?.cache)
156
+ cacheOption = input.cache;
157
+ }
158
+ else if (typeof input === 'string' || input instanceof URL) {
159
+ url = String(input);
160
+ method = init?.method;
161
+ headersInit = init?.headers;
162
+ body = init?.body ?? null;
163
+ }
164
+ else {
165
+ // Standard Request object
166
+ url = input.url;
167
+ method = input.method;
168
+ headersInit = input.headers;
169
+ body = init?.body ?? null;
170
+ }
171
+ const headers = headersToPairs(headersInit) ?? [];
172
+ const normalized = normalizeBody(body);
173
+ // Inject cache-control headers based on cache option
174
+ if (cacheOption === 'no-store') {
175
+ headers.push({ key: 'Cache-Control', value: 'no-store' });
176
+ }
177
+ else if (cacheOption === 'no-cache') {
178
+ headers.push({ key: 'Cache-Control', value: 'no-cache' });
179
+ }
180
+ else if (cacheOption === 'reload') {
181
+ headers.push({ key: 'Cache-Control', value: 'no-cache' });
182
+ headers.push({ key: 'Pragma', value: 'no-cache' });
183
+ }
184
+ // Determine followRedirects based on redirect option
185
+ const followRedirects = redirectOption === 'follow';
186
+ const prefetchCacheTtlMs = typeof init?.prefetchCacheTtlMs === 'number'
187
+ ? init.prefetchCacheTtlMs
188
+ : undefined;
189
+ return {
190
+ url,
191
+ method: method?.toUpperCase() ?? 'GET',
192
+ headers: headers.length > 0 ? headers : undefined,
193
+ bodyString: normalized?.bodyString,
194
+ bodyBytes: undefined,
195
+ bodyFormData: normalized?.bodyFormData,
196
+ followRedirects,
197
+ prefetchCacheTtlMs,
198
+ };
193
199
  }
194
200
  // Pure JS version of buildNitroRequest that doesnt use anything that breaks worklets. TODO: Merge this to use Same logic for Worklets and normal Fetch
195
201
  function headersToPairsPure(headers) {
196
- 'worklet';
197
- if (!headers) return undefined;
198
- const pairs = [];
199
- if (Array.isArray(headers)) {
200
- // Convert tuple pairs to objects if needed
201
- for (const entry of headers) {
202
- if (Array.isArray(entry) && entry.length >= 2) {
203
- pairs.push({ key: String(entry[0]), value: String(entry[1]) });
204
- } else if (
205
- entry &&
206
- typeof entry === 'object' &&
207
- 'key' in entry &&
208
- 'value' in entry
209
- ) {
210
- pairs.push(entry);
211
- }
202
+ 'worklet';
203
+ if (!headers)
204
+ return undefined;
205
+ const pairs = [];
206
+ if (Array.isArray(headers)) {
207
+ // Convert tuple pairs to objects if needed
208
+ for (const entry of headers) {
209
+ if (Array.isArray(entry) && entry.length >= 2) {
210
+ pairs.push({ key: String(entry[0]), value: String(entry[1]) });
211
+ }
212
+ else if (entry &&
213
+ typeof entry === 'object' &&
214
+ 'key' in entry &&
215
+ 'value' in entry) {
216
+ pairs.push(entry);
217
+ }
218
+ }
219
+ return pairs;
212
220
  }
213
- return pairs;
214
- }
215
- // Check if it's a plain object (Record<string, string>) first
216
- // Plain objects don't have forEach, so check for its absence
217
- if (typeof headers === 'object' && headers !== null) {
218
- // Check if it's a Headers instance by checking for forEach method
219
- const hasForEach = typeof headers.forEach === 'function';
220
- if (hasForEach) {
221
- // Headers-like object (duck typing)
222
- headers.forEach((v, k) => pairs.push({ key: k, value: v }));
223
- return pairs;
224
- } else {
225
- // Plain object (Record<string, string>)
226
- // Use Object.keys to iterate since Object.entries might not work in worklets
227
- const keys = Object.keys(headers);
228
- for (let i = 0; i < keys.length; i++) {
229
- const k = keys[i];
230
- const v = headers[k];
231
- if (v !== undefined) {
232
- pairs.push({ key: k, value: String(v) });
221
+ // Check if it's a plain object (Record<string, string>) first
222
+ // Plain objects don't have forEach, so check for its absence
223
+ if (typeof headers === 'object' && headers !== null) {
224
+ // Check if it's a Headers instance by checking for forEach method
225
+ const hasForEach = typeof headers.forEach === 'function';
226
+ if (hasForEach) {
227
+ // Headers-like object (duck typing)
228
+ headers.forEach((v, k) => pairs.push({ key: k, value: v }));
229
+ return pairs;
230
+ }
231
+ else {
232
+ // Plain object (Record<string, string>)
233
+ // Use Object.keys to iterate since Object.entries might not work in worklets
234
+ const keys = Object.keys(headers);
235
+ for (let i = 0; i < keys.length; i++) {
236
+ const k = keys[i];
237
+ const v = headers[k];
238
+ if (v !== undefined) {
239
+ pairs.push({ key: k, value: String(v) });
240
+ }
241
+ }
242
+ return pairs;
233
243
  }
234
- }
235
- return pairs;
236
244
  }
237
- }
238
- return pairs;
245
+ return pairs;
239
246
  }
240
247
  // Pure JS version of buildNitroRequest that doesnt use anything that breaks worklets
241
248
  function normalizeBodyPure(body) {
242
- 'worklet';
243
- if (body == null) return undefined;
244
- if (typeof body === 'string') return { bodyString: body };
245
- // Check for URLSearchParams (duck typing)
246
- // It should be an object, have a toString method, and typically append/delete methods
247
- // But mainly we care about toString() returning the query string
248
- if (
249
- typeof body === 'object' &&
250
- body !== null &&
251
- typeof body.toString === 'function' &&
252
- Object.prototype.toString.call(body) === '[object URLSearchParams]'
253
- ) {
254
- return { bodyString: body.toString() };
255
- }
256
- // Check for ArrayBuffer (using toString tag to avoid instanceof)
257
- if (
258
- typeof ArrayBuffer !== 'undefined' &&
259
- Object.prototype.toString.call(body) === '[object ArrayBuffer]'
260
- ) {
261
- return { bodyBytes: body };
262
- }
263
- if (ArrayBuffer.isView(body)) {
264
- const view = body;
265
- return {
266
- //@ts-ignore
267
- bodyBytes: view.buffer.slice(
268
- view.byteOffset,
269
- view.byteOffset + view.byteLength
270
- ),
271
- };
272
- }
273
- throw new Error(
274
- 'Unsupported body type for nitro fetch worklet (FormData is not available in worklets)'
275
- );
249
+ 'worklet';
250
+ if (body == null)
251
+ return undefined;
252
+ if (typeof body === 'string')
253
+ return { bodyString: body };
254
+ // Check for URLSearchParams (duck typing)
255
+ // It should be an object, have a toString method, and typically append/delete methods
256
+ // But mainly we care about toString() returning the query string
257
+ if (typeof body === 'object' &&
258
+ body !== null &&
259
+ typeof body.toString === 'function' &&
260
+ Object.prototype.toString.call(body) === '[object URLSearchParams]') {
261
+ return { bodyString: body.toString() };
262
+ }
263
+ // Check for ArrayBuffer (using toString tag to avoid instanceof)
264
+ if (typeof ArrayBuffer !== 'undefined' &&
265
+ Object.prototype.toString.call(body) === '[object ArrayBuffer]') {
266
+ return { bodyBytes: body };
267
+ }
268
+ if (ArrayBuffer.isView(body)) {
269
+ const view = body;
270
+ return {
271
+ //@ts-ignore
272
+ bodyBytes: view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength),
273
+ };
274
+ }
275
+ throw new Error('Unsupported body type for nitro fetch worklet (FormData is not available in worklets)');
276
276
  }
277
277
  // Pure JS version of buildNitroRequest that doesnt use anything that breaks worklets
278
278
  export function buildNitroRequestPure(input, init) {
279
- 'worklet';
280
- let url;
281
- let method;
282
- let headersInit;
283
- let body;
284
- // Check if input is URL-like without instanceof
285
- const isUrlObject =
286
- typeof input === 'object' &&
287
- input !== null &&
288
- Object.prototype.toString.call(input) === '[object URL]';
289
- if (typeof input === 'string' || isUrlObject) {
290
- url = String(input);
291
- method = init?.method;
292
- headersInit = init?.headers;
293
- body = init?.body ?? null;
294
- } else {
295
- // Request object
296
- const req = input;
297
- url = req.url;
298
- method = req.method;
299
- headersInit = req.headers;
300
- // Clone body if needed – Request objects in RN typically allow direct access
301
- body = init?.body ?? null;
302
- }
303
- const headers = headersToPairsPure(headersInit);
304
- const normalized = normalizeBodyPure(body);
305
- const prefetchCacheTtlMs =
306
- typeof init?.prefetchCacheTtlMs === 'number'
307
- ? init.prefetchCacheTtlMs
308
- : undefined;
309
- return {
310
- url,
311
- method: method?.toUpperCase() ?? 'GET',
312
- headers,
313
- bodyString: normalized?.bodyString,
314
- // Only include bodyBytes when provided to avoid signaling upload data unintentionally
315
- bodyBytes: undefined,
316
- followRedirects: true,
317
- prefetchCacheTtlMs,
318
- };
279
+ 'worklet';
280
+ let url;
281
+ let method;
282
+ let headersInit;
283
+ let body;
284
+ // Check if input is URL-like without instanceof
285
+ const isUrlObject = typeof input === 'object' &&
286
+ input !== null &&
287
+ Object.prototype.toString.call(input) === '[object URL]';
288
+ if (typeof input === 'string' || isUrlObject) {
289
+ url = String(input);
290
+ method = init?.method;
291
+ headersInit = init?.headers;
292
+ body = init?.body ?? null;
293
+ }
294
+ else {
295
+ // Request object
296
+ const req = input;
297
+ url = req.url;
298
+ method = req.method;
299
+ headersInit = req.headers;
300
+ // Clone body if needed – Request objects in RN typically allow direct access
301
+ body = init?.body ?? null;
302
+ }
303
+ const headers = headersToPairsPure(headersInit);
304
+ const normalized = normalizeBodyPure(body);
305
+ const prefetchCacheTtlMs = typeof init?.prefetchCacheTtlMs === 'number'
306
+ ? init.prefetchCacheTtlMs
307
+ : undefined;
308
+ return {
309
+ url,
310
+ method: method?.toUpperCase() ?? 'GET',
311
+ headers,
312
+ bodyString: normalized?.bodyString,
313
+ // Only include bodyBytes when provided to avoid signaling upload data unintentionally
314
+ bodyBytes: undefined,
315
+ followRedirects: true,
316
+ prefetchCacheTtlMs,
317
+ };
319
318
  }
320
319
  function createAbortError() {
321
- const err = new Error('The operation was aborted.');
322
- err.name = 'AbortError';
323
- return err;
320
+ const err = new Error('The operation was aborted.');
321
+ err.name = 'AbortError';
322
+ return err;
324
323
  }
325
324
  async function resolveRequestBody(input, init) {
326
- if (typeof input === 'string' || input instanceof URL) return init;
327
- if (input instanceof NitroRequestClass) return init;
328
- if (init?.body != null) return init;
329
- const req = input;
330
- if (typeof req.clone !== 'function') return init;
331
- const method = (init?.method ?? req.method ?? 'GET').toUpperCase();
332
- if (method === 'GET' || method === 'HEAD') return init;
333
- try {
334
- const text = await req.clone().text();
335
- if (text.length === 0) return init;
336
- return { ...(init ?? {}), body: text };
337
- } catch {
338
- return init;
339
- }
325
+ if (typeof input === 'string' || input instanceof URL)
326
+ return init;
327
+ if (input instanceof NitroRequestClass)
328
+ return init;
329
+ if (init?.body != null)
330
+ return init;
331
+ const req = input;
332
+ if (typeof req.clone !== 'function')
333
+ return init;
334
+ const method = (init?.method ?? req.method ?? 'GET').toUpperCase();
335
+ if (method === 'GET' || method === 'HEAD')
336
+ return init;
337
+ try {
338
+ const text = await req.clone().text();
339
+ if (text.length === 0)
340
+ return init;
341
+ return { ...(init ?? {}), body: text };
342
+ }
343
+ catch {
344
+ return init;
345
+ }
340
346
  }
341
347
  async function resolveBlobBody(init) {
342
- if (!init?.body) return init;
343
- if (typeof Blob !== 'undefined' && init.body instanceof Blob) {
344
- const blob = init.body;
345
- const text = await new Promise((resolve, reject) => {
346
- const reader = new FileReader();
347
- reader.onload = () => resolve(reader.result);
348
- reader.onerror = () => reject(reader.error);
349
- reader.readAsText(blob);
350
- });
351
- // Auto-set Content-Type from Blob.type if not already provided
352
- let headers = init.headers;
353
- if (blob.type) {
354
- const pairs = headersToPairs(headers) ?? [];
355
- const hasContentType = pairs.some(
356
- (h) => h.key.toLowerCase() === 'content-type'
357
- );
358
- if (!hasContentType) {
359
- pairs.push({ key: 'Content-Type', value: blob.type });
360
- headers = pairs.map((h) => [h.key, h.value]);
361
- }
362
- }
363
- return { ...init, body: text, headers };
364
- }
365
- return init;
348
+ if (!init?.body)
349
+ return init;
350
+ if (typeof Blob !== 'undefined' && init.body instanceof Blob) {
351
+ const blob = init.body;
352
+ const text = await new Promise((resolve, reject) => {
353
+ const reader = new FileReader();
354
+ reader.onload = () => resolve(reader.result);
355
+ reader.onerror = () => reject(reader.error);
356
+ reader.readAsText(blob);
357
+ });
358
+ // Auto-set Content-Type from Blob.type if not already provided
359
+ let headers = init.headers;
360
+ if (blob.type) {
361
+ const pairs = headersToPairs(headers) ?? [];
362
+ const hasContentType = pairs.some((h) => h.key.toLowerCase() === 'content-type');
363
+ if (!hasContentType) {
364
+ pairs.push({ key: 'Content-Type', value: blob.type });
365
+ headers = pairs.map((h) => [h.key, h.value]);
366
+ }
367
+ }
368
+ return { ...init, body: text, headers };
369
+ }
370
+ return init;
366
371
  }
367
- async function nitroFetchRaw(input, init) {
368
- const signal = init?.signal;
369
- // Fast-abort: reject synchronously before any bridge work.
370
- if (signal?.aborted) {
371
- throw createAbortError();
372
- }
373
- // Extract body from standard Request when init.body is absent (ky/undici pattern)
374
- init = await resolveRequestBody(input, init);
375
- // Resolve Blob body to string before passing to sync buildNitroRequest
376
- init = await resolveBlobBody(init);
377
- const hasNative = typeof NitroFetchHybrid?.createClient === 'function';
378
- if (!hasNative) {
379
- // Fallback path not supported for raw; use global fetch and synthesize minimal shape
380
- // @ts-ignore: global fetch exists in RN
381
- const res = await fetch(input, init);
382
- const url = res.url ?? String(input);
383
- const bytes = await res.arrayBuffer();
384
- const headers = [];
385
- res.headers.forEach((v, k) => headers.push({ key: k, value: v }));
372
+ // http(s) -> native client; anything else is a local resource (hot path).
373
+ function isHttpUrl(url) {
374
+ if (url.startsWith('http://') || url.startsWith('https://'))
375
+ return true;
376
+ const c = url.charCodeAt(0);
377
+ if (c !== 104 && c !== 72)
378
+ return false; // not 'h'/'H'
379
+ return /^https?:/i.test(url);
380
+ }
381
+ function getUrlString(input) {
382
+ if (typeof input === 'string')
383
+ return input;
384
+ if (input instanceof URL)
385
+ return input.toString();
386
+ const u = input?.url;
387
+ return typeof u === 'string' ? u : String(input);
388
+ }
389
+ function base64ToBytes(b64) {
390
+ const decode = globalThis.atob;
391
+ if (typeof decode === 'function') {
392
+ const bin = decode(b64);
393
+ const out = new Uint8Array(bin.length);
394
+ for (let i = 0; i < bin.length; i++)
395
+ out[i] = bin.charCodeAt(i);
396
+ return out;
397
+ }
398
+ // base64 fallback for runtimes without a global atob.
399
+ /* eslint-disable no-bitwise */
400
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
401
+ const clean = b64.replace(/[^A-Za-z0-9+/]/g, '');
402
+ const out = new Uint8Array(Math.floor((clean.length * 3) / 4));
403
+ let p = 0;
404
+ let buf = 0;
405
+ let bits = 0;
406
+ for (let i = 0; i < clean.length; i++) {
407
+ buf = (buf << 6) | chars.indexOf(clean[i]);
408
+ bits += 6;
409
+ if (bits >= 8) {
410
+ bits -= 8;
411
+ out[p++] = (buf >> bits) & 0xff;
412
+ }
413
+ }
414
+ return out;
415
+ /* eslint-enable no-bitwise */
416
+ }
417
+ // Cached: our nitro-text-decoder if the app bundles it (aliased require keeps it optional, not a dep), else a global TextDecoder.
418
+ let _decoder;
419
+ function resolveTextDecoder() {
420
+ if (_decoder !== undefined)
421
+ return _decoder;
422
+ try {
423
+ const dynamicRequire = require;
424
+ const mod = dynamicRequire('react-native-nitro-text-decoder');
425
+ if (mod && typeof mod.TextDecoder === 'function') {
426
+ _decoder = new mod.TextDecoder('utf-8', { fatal: true });
427
+ return _decoder;
428
+ }
429
+ }
430
+ catch {
431
+ // optional, not bundled
432
+ }
433
+ const GlobalTextDecoder = globalThis
434
+ .TextDecoder;
435
+ if (typeof GlobalTextDecoder === 'function') {
436
+ _decoder = new GlobalTextDecoder('utf-8', { fatal: true });
437
+ return _decoder;
438
+ }
439
+ _decoder = null;
440
+ return _decoder;
441
+ }
442
+ // data: text via a TextDecoder; null (bytes-only) + a one-time warn if none.
443
+ let _warnedNoTextDecoder = false;
444
+ function decodeUtf8(bytes) {
445
+ const decoder = resolveTextDecoder();
446
+ if (decoder) {
447
+ try {
448
+ return decoder.decode(bytes);
449
+ }
450
+ catch {
451
+ return null; // invalid UTF-8 -> keep bytes only
452
+ }
453
+ }
454
+ if (!_warnedNoTextDecoder) {
455
+ _warnedNoTextDecoder = true;
456
+ console.warn('[nitro-fetch] Reading a data: URL as text needs a TextDecoder. Install ' +
457
+ 'react-native-nitro-text-decoder or expose a global TextDecoder. The ' +
458
+ 'body is still available via response.arrayBuffer()/bytes().');
459
+ }
460
+ return null;
461
+ }
462
+ // Decode a data: URL into a synthetic 200 response, entirely in JS.
463
+ function decodeDataUrl(url) {
464
+ const comma = url.indexOf(',');
465
+ if (comma < 0)
466
+ throw new TypeError('Failed to fetch: invalid data: URL');
467
+ const meta = url.slice(5, comma); // strip leading "data:"
468
+ const rawData = url.slice(comma + 1);
469
+ const isBase64 = /;base64\s*$/i.test(meta);
470
+ const mediaType = (isBase64 ? meta.replace(/;base64\s*$/i, '') : meta).trim() ||
471
+ 'text/plain;charset=US-ASCII';
472
+ let bodyString;
473
+ let bodyBytes;
474
+ let length;
475
+ if (isBase64) {
476
+ const bytes = base64ToBytes(rawData);
477
+ length = bytes.byteLength;
478
+ // bytes for arrayBuffer/bytes; string for text/json when a decoder exists.
479
+ bodyBytes = bytes.buffer;
480
+ const decoded = decodeUtf8(bytes);
481
+ if (decoded != null)
482
+ bodyString = decoded;
483
+ }
484
+ else {
485
+ bodyString = decodeURIComponent(rawData);
486
+ length =
487
+ typeof TextEncoder !== 'undefined'
488
+ ? new TextEncoder().encode(bodyString).length
489
+ : bodyString.length;
490
+ }
386
491
  return {
387
- url,
388
- status: res.status,
389
- statusText: res.statusText,
390
- ok: res.ok,
391
- redirected: res.redirected ?? false,
392
- headers,
393
- bodyBytes: bytes,
394
- bodyString: undefined,
395
- }; // bleee
396
- }
397
- const req = buildNitroRequest(input, init);
398
- // Inspector: record start (zero cost when disabled — single boolean check)
399
- let inspectorId;
400
- if (NetworkInspector.isEnabled()) {
401
- inspectorId = String(Date.now()) + '-' + String(Math.random()).slice(2, 8);
402
- NetworkInspector._recordStart(
403
- inspectorId,
404
- req.url,
405
- req.method ?? 'GET',
406
- req.headers ?? [],
407
- req.bodyString
408
- );
409
- }
410
- // Only allocate a requestId when a signal is present — zero overhead otherwise.
411
- const requestId = signal ? String(Math.random()) : undefined;
412
- if (requestId) req.requestId = requestId;
413
- ensureClient();
414
- if (!client || typeof client.request !== 'function')
415
- throw new Error('NitroFetch client not available');
416
- // Wire up the abort listener with { once: true } so it auto-removes
417
- // after firing, avoiding a dangling reference to the client closure.
418
- let abortListener;
419
- if (signal && requestId) {
420
- abortListener = () => {
421
- try {
422
- client.cancelRequest(requestId);
423
- } catch {
424
- // Client may already be torn down — swallow.
425
- }
492
+ url,
493
+ status: 200,
494
+ statusText: 'OK',
495
+ ok: true,
496
+ redirected: false,
497
+ headers: [
498
+ { key: 'Content-Type', value: mediaType },
499
+ { key: 'Content-Length', value: String(length) },
500
+ ],
501
+ bodyString,
502
+ bodyBytes,
426
503
  };
427
- signal.addEventListener('abort', abortListener, { once: true });
428
- }
429
- try {
430
- const res = await client.request(req);
431
- if (inspectorId) {
432
- NetworkInspector._recordEnd(
433
- inspectorId,
434
- res.status,
435
- res.statusText,
436
- res.headers ?? [],
437
- res.bodyString?.length ?? 0,
438
- undefined,
439
- res.bodyString ?? undefined
440
- );
441
- }
442
- return res;
443
- } catch (e) {
444
- if (inspectorId) {
445
- NetworkInspector._recordEnd(inspectorId, 0, '', [], 0, String(e));
446
- }
447
- // If the signal was aborted (either before or during the request),
448
- // surface a spec-compliant AbortError regardless of what native threw.
504
+ }
505
+ // Non-http(s): decode data: in JS, reject blob:, read file/content/path natively.
506
+ async function fetchLocalResource(req) {
507
+ const url = req.url;
508
+ if (url.startsWith('data:'))
509
+ return decodeDataUrl(url);
510
+ if (url.startsWith('blob:')) {
511
+ throw new TypeError('nitro-fetch cannot read blob: URLs (the React Native blob registry is not ' +
512
+ 'reachable from native). Read blobs with the platform fetch/FileReader instead.');
513
+ }
514
+ ensureClient();
515
+ if (!client || typeof client.request !== 'function') {
516
+ throw new Error('NitroFetch client not available');
517
+ }
518
+ return client.request(req);
519
+ }
520
+ async function nitroFetchRaw(input, init) {
521
+ const signal = init?.signal;
522
+ // Fast-abort: reject synchronously before any bridge work.
449
523
  if (signal?.aborted) {
450
- throw createAbortError();
524
+ throw createAbortError();
525
+ }
526
+ // Extract body from standard Request when init.body is absent (ky/undici pattern)
527
+ init = await resolveRequestBody(input, init);
528
+ // Resolve Blob body to string before passing to sync buildNitroRequest
529
+ init = await resolveBlobBody(init);
530
+ const hasNative = typeof NitroFetchHybrid?.createClient === 'function';
531
+ if (!hasNative) {
532
+ // Fallback path not supported for raw; use global fetch and synthesize minimal shape
533
+ // @ts-ignore: global fetch exists in RN
534
+ const res = await fetch(input, init);
535
+ const url = res.url ?? String(input);
536
+ const bytes = await res.arrayBuffer();
537
+ const headers = [];
538
+ res.headers.forEach((v, k) => headers.push({ key: k, value: v }));
539
+ return {
540
+ url,
541
+ status: res.status,
542
+ statusText: res.statusText,
543
+ ok: res.ok,
544
+ redirected: res.redirected ?? false,
545
+ headers,
546
+ bodyBytes: bytes,
547
+ bodyString: undefined,
548
+ }; // bleee
549
+ }
550
+ const req = buildNitroRequest(input, init);
551
+ // Route non-http(s) (data:/file://content://scheme-less) off the HTTP client.
552
+ if (!isHttpUrl(req.url)) {
553
+ return fetchLocalResource(req);
554
+ }
555
+ // Inspector: record start (zero cost when disabled — single boolean check)
556
+ let inspectorId;
557
+ if (NetworkInspector.isEnabled()) {
558
+ inspectorId = String(Date.now()) + '-' + String(Math.random()).slice(2, 8);
559
+ NetworkInspector._recordStart(inspectorId, req.url, req.method ?? 'GET', req.headers ?? [], req.bodyString);
560
+ }
561
+ // Only allocate a requestId when a signal is present — zero overhead otherwise.
562
+ const requestId = signal ? String(Math.random()) : undefined;
563
+ if (requestId)
564
+ req.requestId = requestId;
565
+ ensureClient();
566
+ if (!client || typeof client.request !== 'function')
567
+ throw new Error('NitroFetch client not available');
568
+ // Wire up the abort listener with { once: true } so it auto-removes
569
+ // after firing, avoiding a dangling reference to the client closure.
570
+ let abortListener;
571
+ if (signal && requestId) {
572
+ abortListener = () => {
573
+ try {
574
+ client.cancelRequest(requestId);
575
+ }
576
+ catch {
577
+ // Client may already be torn down — swallow.
578
+ }
579
+ };
580
+ signal.addEventListener('abort', abortListener, { once: true });
581
+ }
582
+ try {
583
+ const res = await client.request(req);
584
+ if (inspectorId) {
585
+ NetworkInspector._recordEnd(inspectorId, res.status, res.statusText, res.headers ?? [], res.bodyString?.length ?? 0, undefined, res.bodyString ?? undefined);
586
+ }
587
+ return res;
451
588
  }
452
- throw e;
453
- } finally {
454
- // Idempotent cleanup removeEventListener is a no-op if the listener
455
- // already fired (thanks to { once: true }) or was never added.
456
- if (signal && abortListener) {
457
- signal.removeEventListener('abort', abortListener);
589
+ catch (e) {
590
+ if (inspectorId) {
591
+ NetworkInspector._recordEnd(inspectorId, 0, '', [], 0, String(e));
592
+ }
593
+ // If the signal was aborted (either before or during the request),
594
+ // surface a spec-compliant AbortError regardless of what native threw.
595
+ if (signal?.aborted) {
596
+ throw createAbortError();
597
+ }
598
+ throw e;
599
+ }
600
+ finally {
601
+ // Idempotent cleanup — removeEventListener is a no-op if the listener
602
+ // already fired (thanks to { once: true }) or was never added.
603
+ if (signal && abortListener) {
604
+ signal.removeEventListener('abort', abortListener);
605
+ }
458
606
  }
459
- }
460
607
  }
461
608
  // NitroHeaders is now imported from './Headers'
462
609
  async function nitroStreamFetch(input, init) {
463
- const url = typeof input === 'string' ? input : String(input);
464
- const method = init?.method?.toUpperCase() ?? 'GET';
465
- const headers = headersToPairs(init?.headers);
466
- // Inspector: record start
467
- let inspectorId;
468
- if (NetworkInspector.isEnabled()) {
469
- inspectorId = String(Date.now()) + '-' + String(Math.random()).slice(2, 8);
470
- NetworkInspector._recordStart(
471
- inspectorId,
472
- url,
473
- method,
474
- headers ?? [],
475
- typeof init?.body === 'string' ? init.body : undefined
476
- );
477
- }
478
- const builder = NitroCronetSingleton.newUrlRequestBuilder(url);
479
- builder.setHttpMethod(method);
480
- headers?.forEach((h) => builder.addHeader(h.key, h.value));
481
- const body = init?.body;
482
- if (body != null) {
483
- if (typeof body === 'string') builder.setUploadBody(body);
484
- else if (body instanceof ArrayBuffer) builder.setUploadBody(body);
485
- }
486
- return new Promise((resolveResponse, rejectResponse) => {
487
- let streamController;
488
- const stream = new ReadableStream({
489
- start(controller) {
490
- streamController = controller;
491
- },
492
- });
493
- let responseResolved = false;
494
- let streamBytesReceived = 0;
495
- builder.onResponseStarted((info) => {
496
- if (responseResolved) return;
497
- responseResolved = true;
498
- const status = info.httpStatusCode;
499
- const responseHeaders = new NitroHeaders(
500
- Object.entries(info.allHeaders).map(([key, value]) => ({ key, value }))
501
- );
502
- const response = new NitroResponse({
503
- url: info.url,
504
- ok: status >= 200 && status < 300,
505
- status,
506
- statusText: info.httpStatusText,
507
- headers: responseHeaders,
508
- redirected: false,
509
- body: stream,
510
- });
511
- resolveResponse(response);
512
- // Android/Cronet: kick off the first buffer read.
513
- // iOS/URLSession handles reading automatically so this is a no-op there.
514
- request.read();
515
- });
516
- builder.onReadCompleted((_info, byteBuffer, bytesRead) => {
517
- const chunk = new Uint8Array(byteBuffer, 0, bytesRead).slice();
518
- streamBytesReceived += bytesRead;
519
- streamController.enqueue(chunk);
520
- if (!request.isDone()) {
521
- request.read();
522
- }
523
- });
524
- builder.onSucceeded((_info) => {
525
- streamController.close();
526
- if (inspectorId) {
527
- const info = _info;
528
- const status = info?.httpStatusCode ?? 0;
529
- const hdrs = info?.allHeadersAsList ?? [];
530
- NetworkInspector._recordEnd(
531
- inspectorId,
532
- status,
533
- info?.httpStatusText ?? '',
534
- hdrs,
535
- streamBytesReceived
536
- );
537
- }
538
- });
539
- builder.onFailed((_info, error) => {
540
- const err = new Error(error.message);
541
- if (inspectorId) {
542
- NetworkInspector._recordEnd(inspectorId, 0, '', [], 0, error.message);
543
- }
544
- if (!responseResolved) {
545
- responseResolved = true;
546
- rejectResponse(err);
547
- } else {
548
- streamController.error(err);
549
- }
550
- });
551
- builder.onCanceled(() => {
552
- const err = createAbortError();
553
- if (inspectorId) {
554
- NetworkInspector._recordEnd(
555
- inspectorId,
556
- 0,
557
- '',
558
- [],
559
- 0,
560
- 'Request canceled'
561
- );
562
- }
563
- if (!responseResolved) {
564
- responseResolved = true;
565
- rejectResponse(err);
566
- } else {
567
- streamController.error(err);
568
- }
610
+ const url = typeof input === 'string' ? input : String(input);
611
+ const method = init?.method?.toUpperCase() ?? 'GET';
612
+ const headers = headersToPairs(init?.headers);
613
+ // Inspector: record start
614
+ let inspectorId;
615
+ if (NetworkInspector.isEnabled()) {
616
+ inspectorId = String(Date.now()) + '-' + String(Math.random()).slice(2, 8);
617
+ NetworkInspector._recordStart(inspectorId, url, method, headers ?? [], typeof init?.body === 'string' ? init.body : undefined);
618
+ }
619
+ const builder = NitroCronetSingleton.newUrlRequestBuilder(url);
620
+ builder.setHttpMethod(method);
621
+ headers?.forEach((h) => builder.addHeader(h.key, h.value));
622
+ const body = init?.body;
623
+ if (body != null) {
624
+ if (typeof body === 'string')
625
+ builder.setUploadBody(body);
626
+ else if (body instanceof ArrayBuffer)
627
+ builder.setUploadBody(body);
628
+ }
629
+ return new Promise((resolveResponse, rejectResponse) => {
630
+ let streamController;
631
+ const stream = new ReadableStream({
632
+ start(controller) {
633
+ streamController = controller;
634
+ },
635
+ });
636
+ let responseResolved = false;
637
+ let streamBytesReceived = 0;
638
+ builder.onResponseStarted((info) => {
639
+ if (responseResolved)
640
+ return;
641
+ responseResolved = true;
642
+ const status = info.httpStatusCode;
643
+ const responseHeaders = new NitroHeaders(Object.entries(info.allHeaders).map(([key, value]) => ({ key, value })));
644
+ const response = new NitroResponse({
645
+ url: info.url,
646
+ ok: status >= 200 && status < 300,
647
+ status,
648
+ statusText: info.httpStatusText,
649
+ headers: responseHeaders,
650
+ redirected: false,
651
+ body: stream,
652
+ });
653
+ resolveResponse(response);
654
+ // Android/Cronet: kick off the first buffer read.
655
+ // iOS/URLSession handles reading automatically so this is a no-op there.
656
+ request.read();
657
+ });
658
+ builder.onReadCompleted((_info, byteBuffer, bytesRead) => {
659
+ const chunk = new Uint8Array(byteBuffer, 0, bytesRead).slice();
660
+ streamBytesReceived += bytesRead;
661
+ streamController.enqueue(chunk);
662
+ if (!request.isDone()) {
663
+ request.read();
664
+ }
665
+ });
666
+ builder.onSucceeded((_info) => {
667
+ streamController.close();
668
+ if (inspectorId) {
669
+ const info = _info;
670
+ const status = info?.httpStatusCode ?? 0;
671
+ const hdrs = info?.allHeadersAsList ?? [];
672
+ NetworkInspector._recordEnd(inspectorId, status, info?.httpStatusText ?? '', hdrs, streamBytesReceived);
673
+ }
674
+ });
675
+ builder.onFailed((_info, error) => {
676
+ const err = new Error(error.message);
677
+ if (inspectorId) {
678
+ NetworkInspector._recordEnd(inspectorId, 0, '', [], 0, error.message);
679
+ }
680
+ if (!responseResolved) {
681
+ responseResolved = true;
682
+ rejectResponse(err);
683
+ }
684
+ else {
685
+ streamController.error(err);
686
+ }
687
+ });
688
+ builder.onCanceled(() => {
689
+ const err = createAbortError();
690
+ if (inspectorId) {
691
+ NetworkInspector._recordEnd(inspectorId, 0, '', [], 0, 'Request canceled');
692
+ }
693
+ if (!responseResolved) {
694
+ responseResolved = true;
695
+ rejectResponse(err);
696
+ }
697
+ else {
698
+ streamController.error(err);
699
+ }
700
+ });
701
+ const request = builder.build();
702
+ request.start();
569
703
  });
570
- const request = builder.build();
571
- request.start();
572
- });
573
704
  }
574
705
  export async function nitroFetch(input, init) {
575
- // Merge defaults from NitroRequestClass if input is one
576
- if (input instanceof NitroRequestClass) {
577
- init = {
578
- ...init,
579
- signal: init?.signal ?? input.signal,
580
- redirect: init?.redirect ?? input.redirect,
581
- cache: init?.cache ?? input.cache,
582
- };
583
- }
584
- if (init?.stream === true) {
585
- return nitroStreamFetch(input, init);
586
- }
587
- const redirectOption = init?.redirect ?? 'follow';
588
- const res = await nitroFetchRaw(input, init);
589
- // Handle redirect: "error" — if we got a 3xx back (followRedirects was false), throw
590
- if (redirectOption === 'error' && res.status >= 300 && res.status < 400) {
591
- throw new TypeError(
592
- `redirect mode is "error": redirected request to "${res.url}"`
593
- );
594
- }
595
- const response = new NitroResponse({
596
- url: res.url,
597
- status: res.status,
598
- statusText: res.statusText,
599
- ok: res.ok,
600
- redirected: res.redirected,
601
- headers: res.headers,
602
- bodyBytes: res.bodyBytes,
603
- bodyString: res.bodyString,
604
- });
605
- return response;
706
+ // Merge defaults from NitroRequestClass if input is one
707
+ if (input instanceof NitroRequestClass) {
708
+ init = {
709
+ ...init,
710
+ signal: init?.signal ?? input.signal,
711
+ redirect: init?.redirect ?? input.redirect,
712
+ cache: init?.cache ?? input.cache,
713
+ };
714
+ }
715
+ // Streaming is http(s)-only; local URLs fall through to nitroFetchRaw (check runs only when streaming).
716
+ if (init?.stream === true && isHttpUrl(getUrlString(input))) {
717
+ return nitroStreamFetch(input, init);
718
+ }
719
+ const redirectOption = init?.redirect ?? 'follow';
720
+ const res = await nitroFetchRaw(input, init);
721
+ // Handle redirect: "error" if we got a 3xx back (followRedirects was false), throw
722
+ if (redirectOption === 'error' && res.status >= 300 && res.status < 400) {
723
+ throw new TypeError(`redirect mode is "error": redirected request to "${res.url}"`);
724
+ }
725
+ const response = new NitroResponse({
726
+ url: res.url,
727
+ status: res.status,
728
+ statusText: res.statusText,
729
+ ok: res.ok,
730
+ redirected: res.redirected,
731
+ headers: res.headers,
732
+ bodyBytes: res.bodyBytes,
733
+ bodyString: res.bodyString,
734
+ });
735
+ return response;
606
736
  }
607
737
  // Start a native prefetch. Requires a `prefetchKey` header on the request.
608
738
  export async function prefetch(input, init) {
609
- // If native implementation is not present yet, do nothing
610
- const hasNative = typeof NitroFetchHybrid?.createClient === 'function';
611
- if (!hasNative) return;
612
- // Build NitroRequest and ensure prefetchKey header exists
613
- const req = buildNitroRequest(input, init);
614
- const hasKey =
615
- req.headers?.some((h) => h.key.toLowerCase() === 'prefetchkey') ?? false;
616
- // Also support passing prefetchKey via non-standard field on init
617
- const fromInit = init?.prefetchKey;
618
- if (!hasKey && fromInit) {
619
- req.headers = (req.headers ?? []).concat([
620
- { key: 'prefetchKey', value: fromInit },
621
- ]);
622
- }
623
- const finalHasKey = req.headers?.some(
624
- (h) => h.key.toLowerCase() === 'prefetchkey'
625
- );
626
- if (!finalHasKey) {
627
- throw new Error('prefetch requires a "prefetchKey" header');
628
- }
629
- // Ensure client and call native prefetch
630
- ensureClient();
631
- if (!client || typeof client.prefetch !== 'function') return;
632
- await client.prefetch(req);
739
+ // If native implementation is not present yet, do nothing
740
+ const hasNative = typeof NitroFetchHybrid?.createClient === 'function';
741
+ if (!hasNative)
742
+ return;
743
+ // Build NitroRequest and ensure prefetchKey header exists
744
+ const req = buildNitroRequest(input, init);
745
+ const hasKey = req.headers?.some((h) => h.key.toLowerCase() === 'prefetchkey') ?? false;
746
+ // Also support passing prefetchKey via non-standard field on init
747
+ const fromInit = init?.prefetchKey;
748
+ if (!hasKey && fromInit) {
749
+ req.headers = (req.headers ?? []).concat([
750
+ { key: 'prefetchKey', value: fromInit },
751
+ ]);
752
+ }
753
+ const finalHasKey = req.headers?.some((h) => h.key.toLowerCase() === 'prefetchkey');
754
+ if (!finalHasKey) {
755
+ throw new Error('prefetch requires a "prefetchKey" header');
756
+ }
757
+ // Ensure client and call native prefetch
758
+ ensureClient();
759
+ if (!client || typeof client.prefetch !== 'function')
760
+ return;
761
+ await client.prefetch(req);
633
762
  }
634
763
  const AUTOPREFETCH_QUEUE_KEY = 'nitrofetch_autoprefetch_queue';
635
764
  // Persist a request to storage so native can prefetch it on app start.
636
765
  export async function prefetchOnAppStart(input, init) {
637
- // Resolve request and prefetchKey
638
- const req = buildNitroRequest(input, init);
639
- const fromHeader = req.headers?.find(
640
- (h) => h.key.toLowerCase() === 'prefetchkey'
641
- )?.value;
642
- const fromInit = init?.prefetchKey;
643
- const prefetchKey = fromHeader ?? fromInit;
644
- if (!prefetchKey) {
645
- throw new Error(
646
- 'prefetchOnAppStart requires a "prefetchKey" (header or init.prefetchKey)'
647
- );
648
- }
649
- // Convert headers to a plain object for storage
650
- const headersObj = (req.headers ?? []).reduce((acc, { key, value }) => {
651
- acc[String(key)] = String(value);
652
- return acc;
653
- }, {});
654
- const entry = {
655
- url: req.url,
656
- prefetchKey,
657
- headers: headersObj,
658
- };
659
- if (req.method && req.method !== 'GET') entry.method = req.method;
660
- if (req.bodyString !== undefined) entry.bodyString = req.bodyString;
661
- if (typeof req.bodyBytes === 'string' && req.bodyBytes.length > 0)
662
- entry.bodyBytes = req.bodyBytes;
663
- if (req.bodyFormData && req.bodyFormData.length > 0)
664
- entry.bodyFormData = req.bodyFormData;
665
- if (typeof req.timeoutMs === 'number') entry.timeoutMs = req.timeoutMs;
666
- if (req.followRedirects === false) entry.followRedirects = false;
667
- if (typeof req.prefetchCacheTtlMs === 'number')
668
- entry.prefetchCacheTtlMs = req.prefetchCacheTtlMs;
669
- // Write or append to storage queue
670
- try {
671
- let arr = [];
766
+ // Resolve request and prefetchKey
767
+ const req = buildNitroRequest(input, init);
768
+ const fromHeader = req.headers?.find((h) => h.key.toLowerCase() === 'prefetchkey')?.value;
769
+ const fromInit = init?.prefetchKey;
770
+ const prefetchKey = fromHeader ?? fromInit;
771
+ if (!prefetchKey) {
772
+ throw new Error('prefetchOnAppStart requires a "prefetchKey" (header or init.prefetchKey)');
773
+ }
774
+ // Convert headers to a plain object for storage
775
+ const headersObj = (req.headers ?? []).reduce((acc, { key, value }) => {
776
+ acc[String(key)] = String(value);
777
+ return acc;
778
+ }, {});
779
+ const entry = {
780
+ url: req.url,
781
+ prefetchKey,
782
+ headers: headersObj,
783
+ };
784
+ if (req.method && req.method !== 'GET')
785
+ entry.method = req.method;
786
+ if (req.bodyString !== undefined)
787
+ entry.bodyString = req.bodyString;
788
+ if (typeof req.bodyBytes === 'string' && req.bodyBytes.length > 0)
789
+ entry.bodyBytes = req.bodyBytes;
790
+ if (req.bodyFormData && req.bodyFormData.length > 0)
791
+ entry.bodyFormData = req.bodyFormData;
792
+ if (typeof req.timeoutMs === 'number')
793
+ entry.timeoutMs = req.timeoutMs;
794
+ if (req.followRedirects === false)
795
+ entry.followRedirects = false;
796
+ if (typeof req.prefetchCacheTtlMs === 'number')
797
+ entry.prefetchCacheTtlMs = req.prefetchCacheTtlMs;
798
+ // Write or append to storage queue
672
799
  try {
673
- const raw = NativeStorageSingleton.getString(AUTOPREFETCH_QUEUE_KEY);
674
- if (raw) arr = JSON.parse(raw);
675
- if (!Array.isArray(arr)) arr = [];
676
- } catch {
677
- arr = [];
678
- }
679
- if (arr.some((e) => e && e.prefetchKey === prefetchKey)) {
680
- arr = arr.filter((e) => e && e.prefetchKey !== prefetchKey);
681
- }
682
- arr.push(entry);
683
- NativeStorageSingleton.setString(
684
- AUTOPREFETCH_QUEUE_KEY,
685
- JSON.stringify(arr)
686
- );
687
- } catch (e) {
688
- console.warn('Failed to persist prefetch queue', e);
689
- }
800
+ let arr = [];
801
+ try {
802
+ const raw = NativeStorageSingleton.getString(AUTOPREFETCH_QUEUE_KEY);
803
+ if (raw)
804
+ arr = JSON.parse(raw);
805
+ if (!Array.isArray(arr))
806
+ arr = [];
807
+ }
808
+ catch {
809
+ arr = [];
810
+ }
811
+ if (arr.some((e) => e && e.prefetchKey === prefetchKey)) {
812
+ arr = arr.filter((e) => e && e.prefetchKey !== prefetchKey);
813
+ }
814
+ arr.push(entry);
815
+ NativeStorageSingleton.setString(AUTOPREFETCH_QUEUE_KEY, JSON.stringify(arr));
816
+ }
817
+ catch (e) {
818
+ console.warn('Failed to persist prefetch queue', e);
819
+ }
690
820
  }
691
821
  // Remove one entry (by prefetchKey) from the auto-prefetch queue.
692
822
  export async function removeFromAutoPrefetch(prefetchKey) {
693
- try {
694
- let arr = [];
695
823
  try {
696
- const raw = NativeStorageSingleton.getString(AUTOPREFETCH_QUEUE_KEY);
697
- if (raw) arr = JSON.parse(raw);
698
- if (!Array.isArray(arr)) arr = [];
699
- } catch {
700
- arr = [];
701
- }
702
- const next = arr.filter((e) => e && e.prefetchKey !== prefetchKey);
703
- if (next.length === 0) {
704
- NativeStorageSingleton.removeString(AUTOPREFETCH_QUEUE_KEY);
705
- } else if (next.length !== arr.length) {
706
- NativeStorageSingleton.setString(
707
- AUTOPREFETCH_QUEUE_KEY,
708
- JSON.stringify(next)
709
- );
710
- }
711
- } catch (e) {
712
- console.warn('Failed to remove from prefetch queue', e);
713
- }
824
+ let arr = [];
825
+ try {
826
+ const raw = NativeStorageSingleton.getString(AUTOPREFETCH_QUEUE_KEY);
827
+ if (raw)
828
+ arr = JSON.parse(raw);
829
+ if (!Array.isArray(arr))
830
+ arr = [];
831
+ }
832
+ catch {
833
+ arr = [];
834
+ }
835
+ const next = arr.filter((e) => e && e.prefetchKey !== prefetchKey);
836
+ if (next.length === 0) {
837
+ NativeStorageSingleton.removeString(AUTOPREFETCH_QUEUE_KEY);
838
+ }
839
+ else if (next.length !== arr.length) {
840
+ NativeStorageSingleton.setString(AUTOPREFETCH_QUEUE_KEY, JSON.stringify(next));
841
+ }
842
+ }
843
+ catch (e) {
844
+ console.warn('Failed to remove from prefetch queue', e);
845
+ }
714
846
  }
715
847
  // Remove all entries from the auto-prefetch queue.
716
848
  export async function removeAllFromAutoprefetch() {
717
- NativeStorageSingleton.setString(AUTOPREFETCH_QUEUE_KEY, JSON.stringify([]));
849
+ NativeStorageSingleton.setString(AUTOPREFETCH_QUEUE_KEY, JSON.stringify([]));
718
850
  }
719
851
  export function __readAutoPrefetchQueue() {
720
- try {
721
- const raw = NativeStorageSingleton.getString(AUTOPREFETCH_QUEUE_KEY);
722
- if (!raw) return [];
723
- const parsed = JSON.parse(raw);
724
- return Array.isArray(parsed) ? parsed : [];
725
- } catch {
726
- return [];
727
- }
852
+ try {
853
+ const raw = NativeStorageSingleton.getString(AUTOPREFETCH_QUEUE_KEY);
854
+ if (!raw)
855
+ return [];
856
+ const parsed = JSON.parse(raw);
857
+ return Array.isArray(parsed) ? parsed : [];
858
+ }
859
+ catch {
860
+ return [];
861
+ }
728
862
  }
729
863
  let nitroRuntime;
730
864
  function ensureWorkletRuntime(name = 'nitro-fetch') {
731
- try {
732
- const { createWorkletRuntime } = require('react-native-worklets');
733
- nitroRuntime = nitroRuntime ?? createWorkletRuntime(name);
734
- return nitroRuntime;
735
- } catch {
736
- console.warn('react-native-worklets not available');
737
- return undefined;
738
- }
865
+ try {
866
+ const { createWorkletRuntime } = require('react-native-worklets');
867
+ nitroRuntime = nitroRuntime ?? createWorkletRuntime(name);
868
+ return nitroRuntime;
869
+ }
870
+ catch {
871
+ console.warn('react-native-worklets not available');
872
+ return undefined;
873
+ }
739
874
  }
740
875
  export async function nitroFetchOnWorklet(input, init, mapWorklet, options) {
741
- const preferBytes = options?.preferBytes === true; // default true
742
- let runOnRuntimeAsync;
743
- let rt;
744
- try {
745
- rt = ensureWorkletRuntime(options?.runtimeName);
746
- const worklets = require('react-native-worklets');
747
- runOnRuntimeAsync = worklets.runOnRuntimeAsync;
748
- } catch {
749
- // Module not available
750
- }
751
- // Fallback: if runtime is not available, do the work on JS
752
- if (!runOnRuntimeAsync || !rt) {
753
- console.warn('nitroFetchOnWorklet: no runtime, mapping on JS thread');
754
- const res = await nitroFetchRaw(input, init);
755
- const payload = {
756
- url: res.url,
757
- status: res.status,
758
- statusText: res.statusText,
759
- ok: res.ok,
760
- redirected: res.redirected,
761
- headers: res.headers,
762
- bodyBytes: preferBytes ? res.bodyBytes : undefined,
763
- bodyString: preferBytes ? undefined : res.bodyString,
764
- };
765
- return mapWorklet(payload);
766
- }
767
- return await runOnRuntimeAsync(rt, () => {
768
- 'worklet';
769
- const nitroFetchClient = NitroFetchHybrid.createClient();
770
- const request = buildNitroRequestPure(input, init);
771
- const res = nitroFetchClient.requestSync(request);
772
- const payload = {
773
- url: res.url,
774
- status: res.status,
775
- statusText: res.statusText,
776
- ok: res.ok,
777
- redirected: res.redirected,
778
- headers: res.headers,
779
- bodyBytes: preferBytes ? res.bodyBytes : undefined,
780
- bodyString: preferBytes ? undefined : res.bodyString,
781
- };
782
- return mapWorklet(payload);
783
- });
876
+ const preferBytes = options?.preferBytes === true; // default true
877
+ let runOnRuntimeAsync;
878
+ let rt;
879
+ try {
880
+ rt = ensureWorkletRuntime(options?.runtimeName);
881
+ const worklets = require('react-native-worklets');
882
+ runOnRuntimeAsync = worklets.runOnRuntimeAsync;
883
+ }
884
+ catch {
885
+ // Module not available
886
+ }
887
+ // Fallback: if runtime is not available, do the work on JS
888
+ if (!runOnRuntimeAsync || !rt) {
889
+ console.warn('nitroFetchOnWorklet: no runtime, mapping on JS thread');
890
+ const res = await nitroFetchRaw(input, init);
891
+ const payload = {
892
+ url: res.url,
893
+ status: res.status,
894
+ statusText: res.statusText,
895
+ ok: res.ok,
896
+ redirected: res.redirected,
897
+ headers: res.headers,
898
+ bodyBytes: preferBytes ? res.bodyBytes : undefined,
899
+ bodyString: preferBytes ? undefined : res.bodyString,
900
+ };
901
+ return mapWorklet(payload);
902
+ }
903
+ return await runOnRuntimeAsync(rt, () => {
904
+ 'worklet';
905
+ const nitroFetchClient = NitroFetchHybrid.createClient();
906
+ const request = buildNitroRequestPure(input, init);
907
+ const res = nitroFetchClient.requestSync(request);
908
+ const payload = {
909
+ url: res.url,
910
+ status: res.status,
911
+ statusText: res.statusText,
912
+ ok: res.ok,
913
+ redirected: res.redirected,
914
+ headers: res.headers,
915
+ bodyBytes: preferBytes ? res.bodyBytes : undefined,
916
+ bodyString: preferBytes ? undefined : res.bodyString,
917
+ };
918
+ return mapWorklet(payload);
919
+ });
784
920
  }
785
921
  export { NitroHeaders } from './Headers';
786
922
  export { NitroResponse } from './Response';