react-native-nitro-fetch 0.3.0-beta.0 → 0.3.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 (51) hide show
  1. package/NitroFetch.podspec +8 -0
  2. package/android/build.gradle +3 -0
  3. package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt +46 -2
  4. package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchPackage.kt +2 -2
  5. package/ios/NitroFetchClient.swift +36 -1
  6. package/lib/module/CurlGenerator.js +28 -0
  7. package/lib/module/CurlGenerator.js.map +1 -0
  8. package/lib/module/Headers.js +95 -0
  9. package/lib/module/Headers.js.map +1 -0
  10. package/lib/module/HermesProfiler.js +28 -0
  11. package/lib/module/HermesProfiler.js.map +1 -0
  12. package/lib/module/NetworkInspector.js +184 -0
  13. package/lib/module/NetworkInspector.js.map +1 -0
  14. package/lib/module/Request.js +120 -0
  15. package/lib/module/Request.js.map +1 -0
  16. package/lib/module/Response.js +236 -0
  17. package/lib/module/Response.js.map +1 -0
  18. package/lib/module/fetch.js +143 -56
  19. package/lib/module/fetch.js.map +1 -1
  20. package/lib/module/index.js +6 -0
  21. package/lib/module/index.js.map +1 -1
  22. package/lib/module/utf8.js +29 -0
  23. package/lib/module/utf8.js.map +1 -0
  24. package/lib/typescript/src/CurlGenerator.d.ts +13 -0
  25. package/lib/typescript/src/CurlGenerator.d.ts.map +1 -0
  26. package/lib/typescript/src/Headers.d.ts +19 -0
  27. package/lib/typescript/src/Headers.d.ts.map +1 -0
  28. package/lib/typescript/src/HermesProfiler.d.ts +6 -0
  29. package/lib/typescript/src/HermesProfiler.d.ts.map +1 -0
  30. package/lib/typescript/src/NetworkInspector.d.ts +96 -0
  31. package/lib/typescript/src/NetworkInspector.d.ts.map +1 -0
  32. package/lib/typescript/src/Request.d.ts +48 -0
  33. package/lib/typescript/src/Request.d.ts.map +1 -0
  34. package/lib/typescript/src/Response.d.ts +56 -0
  35. package/lib/typescript/src/Response.d.ts.map +1 -0
  36. package/lib/typescript/src/fetch.d.ts +11 -3
  37. package/lib/typescript/src/fetch.d.ts.map +1 -1
  38. package/lib/typescript/src/index.d.ts +13 -1
  39. package/lib/typescript/src/index.d.ts.map +1 -1
  40. package/lib/typescript/src/utf8.d.ts +3 -0
  41. package/lib/typescript/src/utf8.d.ts.map +1 -0
  42. package/package.json +1 -1
  43. package/src/CurlGenerator.ts +44 -0
  44. package/src/Headers.ts +127 -0
  45. package/src/HermesProfiler.ts +37 -0
  46. package/src/NetworkInspector.ts +278 -0
  47. package/src/Request.ts +187 -0
  48. package/src/Response.ts +335 -0
  49. package/src/fetch.ts +186 -75
  50. package/src/index.tsx +22 -1
  51. package/src/utf8.ts +40 -0
package/src/fetch.ts CHANGED
@@ -3,8 +3,8 @@ import type {
3
3
  NitroFetch as NitroFetchModule,
4
4
  NitroFormDataPart,
5
5
  NitroHeader,
6
- NitroRequest,
7
- NitroResponse,
6
+ NitroRequest as NitroRequestNative,
7
+ NitroResponse as NitroResponseNative,
8
8
  } from './NitroFetch.nitro';
9
9
  import {
10
10
  boxedNitroFetch,
@@ -12,6 +12,11 @@ import {
12
12
  NitroCronetSingleton,
13
13
  } from './NitroInstances';
14
14
  import { NativeStorage as NativeStorageSingleton } from './NitroInstances';
15
+ import { NitroHeaders } from './Headers';
16
+ import { NitroResponse } from './Response';
17
+ import { NitroRequest as NitroRequestClass } from './Request';
18
+ import type { RequestRedirect, RequestCache } from './Request';
19
+ import { NetworkInspector } from './NetworkInspector';
15
20
 
16
21
  // No base64: pass strings/ArrayBuffers directly
17
22
 
@@ -163,39 +168,63 @@ function ensureClient() {
163
168
 
164
169
  function buildNitroRequest(
165
170
  input: RequestInfo | URL,
166
- init?: RequestInit
167
- ): NitroRequest {
171
+ init?: RequestInit & { redirect?: RequestRedirect; cache?: RequestCache }
172
+ ): NitroRequestNative {
168
173
  'worklet';
169
174
  let url: string;
170
175
  let method: string | undefined;
171
176
  let headersInit: HeadersInit | undefined;
172
177
  let body: BodyInit | null | undefined;
178
+ let redirectOption: RequestRedirect =
179
+ (init?.redirect as RequestRedirect) ?? 'follow';
180
+ let cacheOption: RequestCache | undefined = init?.cache as
181
+ | RequestCache
182
+ | undefined;
173
183
 
174
- if (typeof input === 'string' || input instanceof URL) {
184
+ if (input instanceof NitroRequestClass) {
185
+ url = input.url;
186
+ method = init?.method ?? input.method;
187
+ headersInit = init?.headers ?? (input.headers as any);
188
+ body = init?.body ?? input.body ?? null;
189
+ if (!init?.redirect) redirectOption = input.redirect;
190
+ if (!init?.cache) cacheOption = input.cache;
191
+ } else if (typeof input === 'string' || input instanceof URL) {
175
192
  url = String(input);
176
193
  method = init?.method;
177
194
  headersInit = init?.headers;
178
195
  body = init?.body ?? null;
179
196
  } else {
180
- // Request object
197
+ // Standard Request object
181
198
  url = input.url;
182
199
  method = input.method;
183
200
  headersInit = input.headers as any;
184
- // Clone body if needed – Request objects in RN typically allow direct access
185
201
  body = init?.body ?? null;
186
202
  }
187
203
 
188
- const headers = headersToPairs(headersInit);
204
+ const headers = headersToPairs(headersInit) ?? [];
189
205
  const normalized = normalizeBody(body);
190
206
 
207
+ // Inject cache-control headers based on cache option
208
+ if (cacheOption === 'no-store') {
209
+ headers.push({ key: 'Cache-Control', value: 'no-store' });
210
+ } else if (cacheOption === 'no-cache') {
211
+ headers.push({ key: 'Cache-Control', value: 'no-cache' });
212
+ } else if (cacheOption === 'reload') {
213
+ headers.push({ key: 'Cache-Control', value: 'no-cache' });
214
+ headers.push({ key: 'Pragma', value: 'no-cache' });
215
+ }
216
+
217
+ // Determine followRedirects based on redirect option
218
+ const followRedirects = redirectOption === 'follow';
219
+
191
220
  return {
192
221
  url,
193
222
  method: (method?.toUpperCase() as any) ?? 'GET',
194
- headers,
223
+ headers: headers.length > 0 ? headers : undefined,
195
224
  bodyString: normalized?.bodyString,
196
225
  bodyBytes: undefined as any,
197
226
  bodyFormData: normalized?.bodyFormData,
198
- followRedirects: true,
227
+ followRedirects,
199
228
  };
200
229
  }
201
230
 
@@ -297,7 +326,7 @@ function normalizeBodyPure(
297
326
  export function buildNitroRequestPure(
298
327
  input: RequestInfo | URL,
299
328
  init?: RequestInit
300
- ): NitroRequest {
329
+ ): NitroRequestNative {
301
330
  'worklet';
302
331
  let url: string;
303
332
  let method: string | undefined;
@@ -345,10 +374,39 @@ function createAbortError(): Error {
345
374
  return err;
346
375
  }
347
376
 
377
+ async function resolveBlobBody(
378
+ init: RequestInit | undefined
379
+ ): Promise<RequestInit | undefined> {
380
+ if (!init?.body) return init;
381
+ if (typeof Blob !== 'undefined' && init.body instanceof Blob) {
382
+ const blob = init.body as Blob;
383
+ const text = await new Promise<string>((resolve, reject) => {
384
+ const reader = new FileReader();
385
+ reader.onload = () => resolve(reader.result as string);
386
+ reader.onerror = () => reject(reader.error);
387
+ reader.readAsText(blob);
388
+ });
389
+ // Auto-set Content-Type from Blob.type if not already provided
390
+ let headers = init.headers;
391
+ if (blob.type) {
392
+ const pairs = headersToPairs(headers) ?? [];
393
+ const hasContentType = pairs.some(
394
+ (h) => h.key.toLowerCase() === 'content-type'
395
+ );
396
+ if (!hasContentType) {
397
+ pairs.push({ key: 'Content-Type', value: blob.type });
398
+ headers = pairs.map((h) => [h.key, h.value] as [string, string]);
399
+ }
400
+ }
401
+ return { ...init, body: text, headers };
402
+ }
403
+ return init;
404
+ }
405
+
348
406
  async function nitroFetchRaw(
349
407
  input: RequestInfo | URL,
350
408
  init?: RequestInit
351
- ): Promise<NitroResponse> {
409
+ ): Promise<NitroResponseNative> {
352
410
  const signal = init?.signal as AbortSignal | undefined | null;
353
411
 
354
412
  // Fast-abort: reject synchronously before any bridge work.
@@ -356,6 +414,9 @@ async function nitroFetchRaw(
356
414
  throw createAbortError();
357
415
  }
358
416
 
417
+ // Resolve Blob body to string before passing to sync buildNitroRequest
418
+ init = await resolveBlobBody(init);
419
+
359
420
  const hasNative =
360
421
  typeof (NitroFetchHybrid as any)?.createClient === 'function';
361
422
  if (!hasNative) {
@@ -375,11 +436,24 @@ async function nitroFetchRaw(
375
436
  headers,
376
437
  bodyBytes: bytes,
377
438
  bodyString: undefined,
378
- } as any as NitroResponse; // bleee
439
+ } as any as NitroResponseNative; // bleee
379
440
  }
380
441
 
381
442
  const req = buildNitroRequest(input, init);
382
443
 
444
+ // Inspector: record start (zero cost when disabled — single boolean check)
445
+ let inspectorId: string | undefined;
446
+ if (NetworkInspector.isEnabled()) {
447
+ inspectorId = String(Date.now()) + '-' + String(Math.random()).slice(2, 8);
448
+ NetworkInspector._recordStart(
449
+ inspectorId,
450
+ req.url,
451
+ req.method ?? 'GET',
452
+ req.headers ?? [],
453
+ req.bodyString
454
+ );
455
+ }
456
+
383
457
  // Only allocate a requestId when a signal is present — zero overhead otherwise.
384
458
  const requestId = signal ? String(Math.random()) : undefined;
385
459
  if (requestId) req.requestId = requestId;
@@ -403,9 +477,23 @@ async function nitroFetchRaw(
403
477
  }
404
478
 
405
479
  try {
406
- const res: NitroResponse = await client.request(req);
480
+ const res: NitroResponseNative = await client.request(req);
481
+ if (inspectorId) {
482
+ NetworkInspector._recordEnd(
483
+ inspectorId,
484
+ res.status,
485
+ res.statusText,
486
+ res.headers ?? [],
487
+ res.bodyString?.length ?? 0,
488
+ undefined,
489
+ res.bodyString ?? undefined
490
+ );
491
+ }
407
492
  return res;
408
493
  } catch (e) {
494
+ if (inspectorId) {
495
+ NetworkInspector._recordEnd(inspectorId, 0, '', [], 0, String(e));
496
+ }
409
497
  // If the signal was aborted (either before or during the request),
410
498
  // surface a spec-compliant AbortError regardless of what native threw.
411
499
  if (signal?.aborted) {
@@ -421,42 +509,7 @@ async function nitroFetchRaw(
421
509
  }
422
510
  }
423
511
 
424
- // Simple Headers-like class that supports get() method
425
- class NitroHeaders {
426
- private _headers: Map<string, string>;
427
-
428
- constructor(headers: NitroHeader[]) {
429
- this._headers = new Map();
430
- for (const { key, value } of headers) {
431
- // Headers are case-insensitive, normalize to lowercase
432
- this._headers.set(key.toLowerCase(), value);
433
- }
434
- }
435
-
436
- get(name: string): string | null {
437
- return this._headers.get(name.toLowerCase()) ?? null;
438
- }
439
-
440
- has(name: string): boolean {
441
- return this._headers.has(name.toLowerCase());
442
- }
443
-
444
- forEach(callback: (value: string, key: string) => void): void {
445
- this._headers.forEach(callback);
446
- }
447
-
448
- entries(): IterableIterator<[string, string]> {
449
- return this._headers.entries();
450
- }
451
-
452
- keys(): IterableIterator<string> {
453
- return this._headers.keys();
454
- }
455
-
456
- values(): IterableIterator<string> {
457
- return this._headers.values();
458
- }
459
- }
512
+ // NitroHeaders is now imported from './Headers'
460
513
 
461
514
  async function nitroStreamFetch(
462
515
  input: RequestInfo | URL,
@@ -466,6 +519,19 @@ async function nitroStreamFetch(
466
519
  const method = init?.method?.toUpperCase() ?? 'GET';
467
520
  const headers = headersToPairs(init?.headers);
468
521
 
522
+ // Inspector: record start
523
+ let inspectorId: string | undefined;
524
+ if (NetworkInspector.isEnabled()) {
525
+ inspectorId = String(Date.now()) + '-' + String(Math.random()).slice(2, 8);
526
+ NetworkInspector._recordStart(
527
+ inspectorId,
528
+ url,
529
+ method,
530
+ headers ?? [],
531
+ typeof init?.body === 'string' ? init.body : undefined
532
+ );
533
+ }
534
+
469
535
  const builder = NitroCronetSingleton.newUrlRequestBuilder(url);
470
536
  builder.setHttpMethod(method);
471
537
  headers?.forEach((h) => builder.addHeader(h.key, h.value));
@@ -486,6 +552,7 @@ async function nitroStreamFetch(
486
552
  });
487
553
 
488
554
  let responseResolved = false;
555
+ let streamBytesReceived = 0;
489
556
 
490
557
  builder.onResponseStarted((info) => {
491
558
  if (responseResolved) return;
@@ -494,9 +561,7 @@ async function nitroStreamFetch(
494
561
  const responseHeaders = new NitroHeaders(
495
562
  Object.entries(info.allHeaders).map(([key, value]) => ({ key, value }))
496
563
  );
497
- // Use a plain object — React Native's Response constructor does not
498
- // propagate a ReadableStream to .body, so we set it explicitly.
499
- const response: any = {
564
+ const response = new NitroResponse({
500
565
  url: info.url,
501
566
  ok: status >= 200 && status < 300,
502
567
  status,
@@ -504,9 +569,8 @@ async function nitroStreamFetch(
504
569
  headers: responseHeaders,
505
570
  redirected: false,
506
571
  body: stream,
507
- bodyUsed: false,
508
- };
509
- resolveResponse(response as Response);
572
+ });
573
+ resolveResponse(response as unknown as Response);
510
574
  // Android/Cronet: kick off the first buffer read.
511
575
  // iOS/URLSession handles reading automatically so this is a no-op there.
512
576
  request.read();
@@ -514,16 +578,34 @@ async function nitroStreamFetch(
514
578
 
515
579
  builder.onReadCompleted((_info, byteBuffer, bytesRead) => {
516
580
  const chunk = new Uint8Array(byteBuffer, 0, bytesRead).slice();
581
+ streamBytesReceived += bytesRead;
517
582
  streamController.enqueue(chunk);
518
583
  if (!request.isDone()) {
519
584
  request.read();
520
585
  }
521
586
  });
522
587
 
523
- builder.onSucceeded(() => streamController.close());
588
+ builder.onSucceeded((_info) => {
589
+ streamController.close();
590
+ if (inspectorId) {
591
+ const info = _info as any;
592
+ const status = info?.httpStatusCode ?? 0;
593
+ const hdrs = info?.allHeadersAsList ?? [];
594
+ NetworkInspector._recordEnd(
595
+ inspectorId,
596
+ status,
597
+ info?.httpStatusText ?? '',
598
+ hdrs,
599
+ streamBytesReceived
600
+ );
601
+ }
602
+ });
524
603
 
525
604
  builder.onFailed((_info, error) => {
526
605
  const err = new Error(error.message);
606
+ if (inspectorId) {
607
+ NetworkInspector._recordEnd(inspectorId, 0, '', [], 0, error.message);
608
+ }
527
609
  if (!responseResolved) {
528
610
  responseResolved = true;
529
611
  rejectResponse(err);
@@ -534,6 +616,16 @@ async function nitroStreamFetch(
534
616
 
535
617
  builder.onCanceled(() => {
536
618
  const err = createAbortError();
619
+ if (inspectorId) {
620
+ NetworkInspector._recordEnd(
621
+ inspectorId,
622
+ 0,
623
+ '',
624
+ [],
625
+ 0,
626
+ 'Request canceled'
627
+ );
628
+ }
537
629
  if (!responseResolved) {
538
630
  responseResolved = true;
539
631
  rejectResponse(err);
@@ -549,33 +641,48 @@ async function nitroStreamFetch(
549
641
 
550
642
  export async function nitroFetch(
551
643
  input: RequestInfo | URL,
552
- init?: RequestInit & { stream?: boolean }
644
+ init?: RequestInit & {
645
+ stream?: boolean;
646
+ redirect?: RequestRedirect;
647
+ cache?: RequestCache;
648
+ }
553
649
  ): Promise<Response> {
650
+ // Merge defaults from NitroRequestClass if input is one
651
+ if (input instanceof NitroRequestClass) {
652
+ init = {
653
+ ...init,
654
+ signal: init?.signal ?? input.signal,
655
+ redirect: (init?.redirect as RequestRedirect) ?? input.redirect,
656
+ cache: (init?.cache as RequestCache) ?? input.cache,
657
+ } as any;
658
+ }
659
+
554
660
  if ((init as any)?.stream === true) {
555
661
  return nitroStreamFetch(input, init);
556
662
  }
557
- const res = await nitroFetchRaw(input, init);
558
663
 
559
- const headersObj = new NitroHeaders(res.headers);
664
+ const redirectOption: RequestRedirect =
665
+ (init?.redirect as RequestRedirect) ?? 'follow';
666
+ const res = await nitroFetchRaw(input, init);
560
667
 
561
- const bodyBytes = res.bodyBytes;
562
- const bodyString = res.bodyString;
668
+ // Handle redirect: "error" — if we got a 3xx back (followRedirects was false), throw
669
+ if (redirectOption === 'error' && res.status >= 300 && res.status < 400) {
670
+ throw new TypeError(
671
+ `redirect mode is "error": redirected request to "${res.url}"`
672
+ );
673
+ }
563
674
 
564
- const makeLight = (): any => ({
675
+ const response = new NitroResponse({
565
676
  url: res.url,
566
- ok: res.ok,
567
677
  status: res.status,
568
678
  statusText: res.statusText,
679
+ ok: res.ok,
569
680
  redirected: res.redirected,
570
- headers: headersObj,
571
- arrayBuffer: async () => bodyBytes,
572
- text: async () => bodyString,
573
- json: async () => JSON.parse(bodyString ?? '{}'),
574
- clone: () => makeLight(),
681
+ headers: res.headers,
682
+ bodyBytes: res.bodyBytes as unknown as ArrayBuffer | undefined,
683
+ bodyString: res.bodyString,
575
684
  });
576
-
577
- const light: any = makeLight();
578
- return light as Response;
685
+ return response as unknown as Response;
579
686
  }
580
687
 
581
688
  // Start a native prefetch. Requires a `prefetchKey` header on the request.
@@ -778,8 +885,12 @@ export async function nitroFetchOnWorklet<T>(
778
885
  });
779
886
  }
780
887
 
888
+ export type { NitroFormDataPart } from './NitroFetch.nitro';
781
889
  export type {
782
- NitroFormDataPart,
783
- NitroRequest,
784
- NitroResponse,
890
+ NitroRequest as NitroRequestNativeType,
891
+ NitroResponse as NitroResponseNativeType,
785
892
  } from './NitroFetch.nitro';
893
+ export { NitroHeaders } from './Headers';
894
+ export { NitroResponse } from './Response';
895
+ export { NitroRequest as NitroRequestClass } from './Request';
896
+ export type { RequestRedirect, RequestCache } from './Request';
package/src/index.tsx CHANGED
@@ -6,7 +6,16 @@ export {
6
6
  removeFromAutoPrefetch,
7
7
  removeAllFromAutoprefetch,
8
8
  } from './fetch';
9
- export type { NitroFormDataPart, NitroRequest, NitroResponse } from './fetch';
9
+ export type { NitroFormDataPart } from './fetch';
10
+ export type {
11
+ NitroRequestNativeType as NitroRequest,
12
+ NitroResponseNativeType as NitroResponse,
13
+ } from './fetch';
14
+ export type { RequestRedirect, RequestCache } from './fetch';
15
+ export type { BodyInit, ResponseInit } from './Response';
16
+ export { NitroHeaders as Headers } from './Headers';
17
+ export { NitroResponse as Response } from './Response';
18
+ export { NitroRequest as Request } from './Request';
10
19
  export { NitroFetch } from './NitroInstances';
11
20
  export {
12
21
  registerTokenRefresh,
@@ -17,6 +26,18 @@ export {
17
26
  applyTemplate,
18
27
  } from './tokenRefresh';
19
28
  export type { TokenRefreshConfig } from './tokenRefresh';
29
+ export { NetworkInspector } from './NetworkInspector';
30
+ export type {
31
+ NetworkEntry,
32
+ NetworkEntryCallback,
33
+ WebSocketEntry,
34
+ WebSocketMessage,
35
+ InspectorEntry,
36
+ } from './NetworkInspector';
37
+ export { generateCurl } from './CurlGenerator';
38
+ export type { CurlOptions } from './CurlGenerator';
39
+ export { profileFetch } from './HermesProfiler';
40
+ export type { ProfileResult } from './HermesProfiler';
20
41
  import './fetch';
21
42
 
22
43
  // Keep legacy export to avoid breaking any local tests/usages during scaffolding.
package/src/utf8.ts ADDED
@@ -0,0 +1,40 @@
1
+ let _TextEncoder: typeof TextEncoder | undefined;
2
+ let _TextDecoder: typeof TextDecoder | undefined;
3
+
4
+ try {
5
+ _TextEncoder =
6
+ typeof TextEncoder !== 'undefined'
7
+ ? TextEncoder
8
+ : require('react-native-nitro-text-decoder').TextEncoder;
9
+ } catch {
10
+ /* resolved at first use */
11
+ }
12
+
13
+ try {
14
+ _TextDecoder =
15
+ typeof TextDecoder !== 'undefined'
16
+ ? TextDecoder
17
+ : require('react-native-nitro-text-decoder').TextDecoder;
18
+ } catch {
19
+ /* resolved at first use */
20
+ }
21
+
22
+ export function stringToUTF8(str: string): Uint8Array {
23
+ if (!_TextEncoder) {
24
+ console.warn(
25
+ 'stringToUTF8: TextEncoder not available. Install react-native-nitro-text-decoder.'
26
+ );
27
+ return new Uint8Array(0);
28
+ }
29
+ return new _TextEncoder().encode(str);
30
+ }
31
+
32
+ export function utf8ToString(bytes: Uint8Array): string {
33
+ if (!_TextDecoder) {
34
+ console.warn(
35
+ 'utf8ToString: TextDecoder not available. Install react-native-nitro-text-decoder.'
36
+ );
37
+ return '';
38
+ }
39
+ return new _TextDecoder().decode(bytes);
40
+ }