react-native-nitro-fetch 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt +114 -31
  2. package/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt +4 -4
  3. package/ios/NitroAutoPrefetcher.h +21 -0
  4. package/ios/NitroAutoPrefetcher.swift +149 -27
  5. package/ios/NitroFetchClient.swift +4 -3
  6. package/lib/module/CurlGenerator.js.map +1 -1
  7. package/lib/module/HermesProfiler.js.map +2 -1
  8. package/lib/module/NetworkInspector.js +0 -4
  9. package/lib/module/NetworkInspector.js.map +1 -1
  10. package/lib/module/NitroCronet.nitro.js.map +1 -1
  11. package/lib/module/NitroFetch.nitro.js.map +1 -0
  12. package/lib/module/NitroInstances.js.map +1 -1
  13. package/lib/module/Response.js +1 -3
  14. package/lib/module/Response.js.map +2 -1
  15. package/lib/module/fetch.js +23 -2
  16. package/lib/module/fetch.js.map +1 -1
  17. package/lib/module/index.js +1 -1
  18. package/lib/module/index.js.map +2 -1
  19. package/lib/module/index.web.js +1 -2
  20. package/lib/module/utf8.js.map +2 -1
  21. package/lib/typescript/src/NitroFetch.nitro.d.ts +1 -0
  22. package/lib/typescript/src/NitroFetch.nitro.d.ts.map +1 -1
  23. package/lib/typescript/src/fetch.d.ts +4 -1
  24. package/lib/typescript/src/fetch.d.ts.map +1 -1
  25. package/lib/typescript/src/index.d.ts +1 -1
  26. package/lib/typescript/src/index.d.ts.map +1 -1
  27. package/nitrogen/generated/android/c++/JNitroRequest.hpp +5 -1
  28. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroRequest.kt +5 -2
  29. package/nitrogen/generated/ios/swift/NitroRequest.swift +19 -1
  30. package/nitrogen/generated/shared/c++/NitroRequest.hpp +5 -1
  31. package/package.json +1 -1
  32. package/src/CurlGenerator.js +31 -0
  33. package/src/Headers.js +127 -0
  34. package/src/HermesProfiler.js +22 -0
  35. package/src/NetworkInspector.js +183 -0
  36. package/src/NitroCronet.nitro.js +1 -0
  37. package/src/NitroFetch.nitro.js +1 -0
  38. package/src/NitroFetch.nitro.ts +3 -0
  39. package/src/NitroInstances.js +6 -0
  40. package/src/Request.js +173 -0
  41. package/src/Response.js +258 -0
  42. package/src/fetch.js +772 -0
  43. package/src/fetch.ts +43 -4
  44. package/src/index.js +10 -0
  45. package/src/index.tsx +1 -0
  46. package/src/index.web.js +104 -0
  47. package/src/tokenRefresh.js +104 -0
  48. package/src/utf8.js +40 -0
package/src/Headers.js ADDED
@@ -0,0 +1,127 @@
1
+ function normalizeName(name) {
2
+ return name.toLowerCase();
3
+ }
4
+ export class NitroHeaders {
5
+ _map;
6
+ constructor(init) {
7
+ this._map = new Map();
8
+ if (!init)
9
+ return;
10
+ if (init instanceof NitroHeaders) {
11
+ init._map.forEach((values, key) => {
12
+ this._map.set(key, [...values]);
13
+ });
14
+ }
15
+ else if (typeof init === 'object' &&
16
+ !Array.isArray(init) &&
17
+ typeof init.forEach === 'function' &&
18
+ typeof init.get === 'function') {
19
+ // Headers-like object (standard Headers or duck-typed)
20
+ init.forEach((value, key) => {
21
+ this._map.set(normalizeName(key), [value]);
22
+ });
23
+ }
24
+ else if (Array.isArray(init)) {
25
+ for (const entry of init) {
26
+ if (Array.isArray(entry) && entry.length >= 2) {
27
+ // [string, string] tuple
28
+ const key = normalizeName(String(entry[0]));
29
+ const value = String(entry[1]);
30
+ const existing = this._map.get(key);
31
+ if (existing)
32
+ existing.push(value);
33
+ else
34
+ this._map.set(key, [value]);
35
+ }
36
+ else if (entry &&
37
+ typeof entry === 'object' &&
38
+ 'key' in entry &&
39
+ 'value' in entry) {
40
+ // NitroHeader object
41
+ const key = normalizeName(entry.key);
42
+ const value = entry.value;
43
+ const existing = this._map.get(key);
44
+ if (existing)
45
+ existing.push(value);
46
+ else
47
+ this._map.set(key, [value]);
48
+ }
49
+ }
50
+ }
51
+ else if (typeof init === 'object' && init !== null) {
52
+ const keys = Object.keys(init);
53
+ for (let i = 0; i < keys.length; i++) {
54
+ const k = keys[i];
55
+ const v = init[k];
56
+ if (v !== undefined) {
57
+ this._map.set(normalizeName(k), [String(v)]);
58
+ }
59
+ }
60
+ }
61
+ }
62
+ append(name, value) {
63
+ const key = normalizeName(name);
64
+ const existing = this._map.get(key);
65
+ if (existing)
66
+ existing.push(value);
67
+ else
68
+ this._map.set(key, [value]);
69
+ }
70
+ delete(name) {
71
+ this._map.delete(normalizeName(name));
72
+ }
73
+ get(name) {
74
+ const values = this._map.get(normalizeName(name));
75
+ if (!values || values.length === 0)
76
+ return null;
77
+ return values.join(', ');
78
+ }
79
+ getSetCookie() {
80
+ return this._map.get('set-cookie') ?? [];
81
+ }
82
+ has(name) {
83
+ return this._map.has(normalizeName(name));
84
+ }
85
+ set(name, value) {
86
+ this._map.set(normalizeName(name), [value]);
87
+ }
88
+ forEach(callback, thisArg) {
89
+ const sortedKeys = Array.from(this._map.keys()).sort();
90
+ for (const key of sortedKeys) {
91
+ callback.call(thisArg, this._map.get(key).join(', '), key, this);
92
+ }
93
+ }
94
+ entries() {
95
+ const map = this._map;
96
+ const sortedKeys = Array.from(map.keys()).sort();
97
+ function* gen() {
98
+ for (const key of sortedKeys) {
99
+ yield [key, map.get(key).join(', ')];
100
+ }
101
+ }
102
+ return gen();
103
+ }
104
+ keys() {
105
+ const map = this._map;
106
+ const sortedKeys = Array.from(map.keys()).sort();
107
+ function* gen() {
108
+ for (const key of sortedKeys) {
109
+ yield key;
110
+ }
111
+ }
112
+ return gen();
113
+ }
114
+ values() {
115
+ const map = this._map;
116
+ const sortedKeys = Array.from(map.keys()).sort();
117
+ function* gen() {
118
+ for (const key of sortedKeys) {
119
+ yield map.get(key).join(', ');
120
+ }
121
+ }
122
+ return gen();
123
+ }
124
+ [Symbol.iterator]() {
125
+ return this.entries();
126
+ }
127
+ }
@@ -0,0 +1,22 @@
1
+ export async function profileFetch(fn, outputPath) {
2
+ const hermes = global.HermesInternal;
3
+ if (!hermes) {
4
+ const result = await fn();
5
+ return { result };
6
+ }
7
+ const path = outputPath ?? `/tmp/nitrofetch-profile-${Date.now()}.cpuprofile`;
8
+ hermes.enableSamplingProfiler();
9
+ try {
10
+ const result = await fn();
11
+ return { result, profilePath: path };
12
+ }
13
+ finally {
14
+ hermes.disableSamplingProfiler();
15
+ try {
16
+ hermes.dumpSamplingProfiler(path);
17
+ }
18
+ catch {
19
+ // Profile dump may fail on some platforms
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,183 @@
1
+ import { generateCurl } from './CurlGenerator';
2
+ class NetworkInspectorImpl {
3
+ _enabled = false;
4
+ _entries = [];
5
+ _maxEntries = 500;
6
+ _maxBodyCapture = 4096;
7
+ _listeners = new Set();
8
+ enable(options) {
9
+ this._enabled = true;
10
+ if (options?.maxEntries != null)
11
+ this._maxEntries = options.maxEntries;
12
+ if (options?.maxBodyCapture != null)
13
+ this._maxBodyCapture = options.maxBodyCapture;
14
+ }
15
+ disable() {
16
+ this._enabled = false;
17
+ }
18
+ isEnabled() {
19
+ return this._enabled;
20
+ }
21
+ getEntries() {
22
+ return this._entries;
23
+ }
24
+ getHttpEntries() {
25
+ return this._entries.filter((e) => e.type === 'http');
26
+ }
27
+ getWebSocketEntries() {
28
+ return this._entries.filter((e) => e.type === 'websocket');
29
+ }
30
+ getEntry(id) {
31
+ return this._entries.find((e) => e.id === id);
32
+ }
33
+ clear() {
34
+ this._entries = [];
35
+ }
36
+ onEntry(callback) {
37
+ this._listeners.add(callback);
38
+ return () => {
39
+ this._listeners.delete(callback);
40
+ };
41
+ }
42
+ _notify(entry) {
43
+ for (const cb of this._listeners) {
44
+ try {
45
+ cb(entry);
46
+ }
47
+ catch {
48
+ // swallow listener errors
49
+ }
50
+ }
51
+ }
52
+ _trimEntries() {
53
+ if (this._entries.length > this._maxEntries) {
54
+ this._entries.shift();
55
+ }
56
+ }
57
+ // --- HTTP recording ---
58
+ _recordStart(id, url, method, headers, body) {
59
+ if (!this._enabled)
60
+ return;
61
+ const bodySize = body ? body.length : 0;
62
+ const entry = {
63
+ id,
64
+ type: 'http',
65
+ url,
66
+ method,
67
+ requestHeaders: headers.map((h) => ({ key: h.key, value: h.value })),
68
+ requestBody: body ? body.slice(0, this._maxBodyCapture) : undefined,
69
+ requestBodySize: bodySize,
70
+ status: 0,
71
+ statusText: '',
72
+ responseHeaders: [],
73
+ responseBodySize: 0,
74
+ startTime: performance.now(),
75
+ endTime: 0,
76
+ duration: 0,
77
+ curl: generateCurl({ url, method, headers, body }),
78
+ };
79
+ this._entries.push(entry);
80
+ this._trimEntries();
81
+ }
82
+ _recordEnd(id, status, statusText, headers, bodySize, error, responseBody) {
83
+ if (!this._enabled)
84
+ return;
85
+ const entry = this._entries.find((e) => e.id === id && e.type === 'http');
86
+ if (!entry)
87
+ return;
88
+ entry.status = status;
89
+ entry.statusText = statusText;
90
+ entry.responseHeaders = headers.map((h) => ({
91
+ key: h.key,
92
+ value: h.value,
93
+ }));
94
+ entry.responseBodySize = bodySize;
95
+ entry.endTime = performance.now();
96
+ entry.duration = entry.endTime - entry.startTime;
97
+ if (error)
98
+ entry.error = error;
99
+ if (responseBody != null) {
100
+ entry.responseBody = responseBody.slice(0, this._maxBodyCapture);
101
+ }
102
+ this._notify(entry);
103
+ }
104
+ // --- WebSocket recording ---
105
+ _recordWsOpen(id, url, protocols, headers) {
106
+ if (!this._enabled)
107
+ return;
108
+ const entry = {
109
+ id,
110
+ type: 'websocket',
111
+ url,
112
+ protocols,
113
+ requestHeaders: headers.map((h) => ({ key: h.key, value: h.value })),
114
+ startTime: performance.now(),
115
+ endTime: 0,
116
+ duration: 0,
117
+ readyState: 'CONNECTING',
118
+ messages: [],
119
+ messagesSent: 0,
120
+ messagesReceived: 0,
121
+ bytesSent: 0,
122
+ bytesReceived: 0,
123
+ };
124
+ this._entries.push(entry);
125
+ this._trimEntries();
126
+ this._notify(entry);
127
+ }
128
+ _recordWsConnected(id) {
129
+ if (!this._enabled)
130
+ return;
131
+ const entry = this._entries.find((e) => e.id === id && e.type === 'websocket');
132
+ if (!entry)
133
+ return;
134
+ entry.readyState = 'OPEN';
135
+ this._notify(entry);
136
+ }
137
+ _recordWsMessage(id, direction, data, size, isBinary) {
138
+ if (!this._enabled)
139
+ return;
140
+ const entry = this._entries.find((e) => e.id === id && e.type === 'websocket');
141
+ if (!entry)
142
+ return;
143
+ entry.messages.push({
144
+ direction,
145
+ data: data.slice(0, this._maxBodyCapture),
146
+ size,
147
+ isBinary,
148
+ timestamp: performance.now(),
149
+ });
150
+ if (direction === 'sent') {
151
+ entry.messagesSent++;
152
+ entry.bytesSent += size;
153
+ }
154
+ else {
155
+ entry.messagesReceived++;
156
+ entry.bytesReceived += size;
157
+ }
158
+ this._notify(entry);
159
+ }
160
+ _recordWsClose(id, code, reason) {
161
+ if (!this._enabled)
162
+ return;
163
+ const entry = this._entries.find((e) => e.id === id && e.type === 'websocket');
164
+ if (!entry)
165
+ return;
166
+ entry.readyState = 'CLOSED';
167
+ entry.closeCode = code;
168
+ entry.closeReason = reason;
169
+ entry.endTime = performance.now();
170
+ entry.duration = entry.endTime - entry.startTime;
171
+ this._notify(entry);
172
+ }
173
+ _recordWsError(id, error) {
174
+ if (!this._enabled)
175
+ return;
176
+ const entry = this._entries.find((e) => e.id === id && e.type === 'websocket');
177
+ if (!entry)
178
+ return;
179
+ entry.error = error;
180
+ this._notify(entry);
181
+ }
182
+ }
183
+ export const NetworkInspector = new NetworkInspectorImpl();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -36,6 +36,9 @@ export interface NitroRequest {
36
36
  // Controls
37
37
  timeoutMs?: number;
38
38
  followRedirects?: boolean; // default true
39
+ // Max age (ms) a prefetch cache entry is considered fresh at read time.
40
+ // Default 5000 when omitted. <= 0 disables cache hits.
41
+ prefetchCacheTtlMs?: number;
39
42
  // Optional ID used for cancellation via cancelRequest()
40
43
  requestId?: string;
41
44
  }
@@ -0,0 +1,6 @@
1
+ import { NitroModules } from 'react-native-nitro-modules';
2
+ // Create singletons once per JS runtime
3
+ export const NitroFetch = NitroModules.createHybridObject('NitroFetch');
4
+ export const NativeStorage = NitroModules.createHybridObject('NativeStorage');
5
+ export const boxedNitroFetch = NitroModules.box(NitroFetch);
6
+ export const NitroCronetSingleton = NitroModules.createHybridObject('NitroCronet');
package/src/Request.js ADDED
@@ -0,0 +1,173 @@
1
+ import { NitroHeaders } from './Headers';
2
+ import { stringToUTF8, utf8ToString } from './utf8';
3
+ export class NitroRequest {
4
+ url;
5
+ method;
6
+ headers;
7
+ redirect;
8
+ signal;
9
+ cache;
10
+ credentials;
11
+ mode;
12
+ referrer;
13
+ referrerPolicy;
14
+ integrity;
15
+ keepalive;
16
+ destination;
17
+ _body;
18
+ _bodyUsed = false;
19
+ constructor(input, init) {
20
+ if (input instanceof NitroRequest) {
21
+ // Clone from another NitroRequest
22
+ this.url = input.url;
23
+ this.method = (init?.method ?? input.method).toUpperCase();
24
+ this.headers = new NitroHeaders(init?.headers
25
+ ? init.headers instanceof NitroHeaders
26
+ ? init.headers
27
+ : init.headers
28
+ : input.headers);
29
+ this.redirect = init?.redirect ?? input.redirect;
30
+ this.signal = init?.signal ?? input.signal;
31
+ this.cache = init?.cache ?? input.cache;
32
+ this.credentials = init?.credentials ?? input.credentials;
33
+ this.mode = init?.mode ?? input.mode;
34
+ this.referrer = init?.referrer ?? input.referrer;
35
+ this.referrerPolicy = init?.referrerPolicy ?? input.referrerPolicy;
36
+ this.integrity = init?.integrity ?? input.integrity;
37
+ this.keepalive = init?.keepalive ?? input.keepalive;
38
+ this._body = init?.body !== undefined ? (init.body ?? null) : input._body;
39
+ }
40
+ else if (typeof input === 'object' &&
41
+ input !== null &&
42
+ 'url' in input &&
43
+ 'method' in input &&
44
+ 'headers' in input &&
45
+ !(input instanceof URL)) {
46
+ // Construct from a Request-like object (standard Request or duck-typed)
47
+ this.url = input.url;
48
+ this.method = (init?.method ?? input.method).toUpperCase();
49
+ this.headers = new NitroHeaders(init?.headers
50
+ ? init.headers instanceof NitroHeaders
51
+ ? init.headers
52
+ : init.headers
53
+ : input.headers);
54
+ this.redirect =
55
+ init?.redirect ?? input.redirect ?? 'follow';
56
+ this.signal = init?.signal ?? input.signal;
57
+ this.cache = init?.cache ?? input.cache ?? 'default';
58
+ this.credentials =
59
+ init?.credentials ?? input.credentials ?? 'same-origin';
60
+ this.mode = init?.mode ?? input.mode ?? 'cors';
61
+ this.referrer = init?.referrer ?? input.referrer ?? 'about:client';
62
+ this.referrerPolicy =
63
+ init?.referrerPolicy ?? input.referrerPolicy ?? '';
64
+ this.integrity = init?.integrity ?? input.integrity ?? '';
65
+ this.keepalive = init?.keepalive ?? input.keepalive ?? false;
66
+ this._body = init?.body ?? null;
67
+ }
68
+ else {
69
+ this.url = String(input);
70
+ this.method = (init?.method ?? 'GET').toUpperCase();
71
+ this.headers = new NitroHeaders(init?.headers
72
+ ? init.headers instanceof NitroHeaders
73
+ ? init.headers
74
+ : init.headers
75
+ : undefined);
76
+ this.redirect = init?.redirect ?? 'follow';
77
+ this.signal = init?.signal ?? new AbortController().signal;
78
+ this.cache = init?.cache ?? 'default';
79
+ this.credentials = init?.credentials ?? 'same-origin';
80
+ this.mode = init?.mode ?? 'cors';
81
+ this.referrer = init?.referrer ?? 'about:client';
82
+ this.referrerPolicy = init?.referrerPolicy ?? '';
83
+ this.integrity = init?.integrity ?? '';
84
+ this.keepalive = init?.keepalive ?? false;
85
+ this._body = init?.body ?? null;
86
+ }
87
+ this.destination = '';
88
+ }
89
+ get bodyUsed() {
90
+ return this._bodyUsed;
91
+ }
92
+ get body() {
93
+ if (this._body == null)
94
+ return null;
95
+ const bodyBytes = this._getBodyBytes();
96
+ if (!bodyBytes)
97
+ return null;
98
+ return new ReadableStream({
99
+ start(controller) {
100
+ controller.enqueue(new Uint8Array(bodyBytes));
101
+ controller.close();
102
+ },
103
+ });
104
+ }
105
+ _throwIfBodyUsed() {
106
+ if (this._bodyUsed) {
107
+ throw new TypeError('Body has already been consumed.');
108
+ }
109
+ }
110
+ _getBodyBytes() {
111
+ if (this._body == null)
112
+ return undefined;
113
+ if (typeof this._body === 'string') {
114
+ const encoded = stringToUTF8(this._body);
115
+ return encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength);
116
+ }
117
+ if (this._body instanceof ArrayBuffer)
118
+ return this._body;
119
+ if (ArrayBuffer.isView(this._body)) {
120
+ const view = this._body;
121
+ return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
122
+ }
123
+ return undefined;
124
+ }
125
+ _getBodyString() {
126
+ if (this._body == null)
127
+ return '';
128
+ if (typeof this._body === 'string')
129
+ return this._body;
130
+ const bytes = this._getBodyBytes();
131
+ if (bytes)
132
+ return utf8ToString(new Uint8Array(bytes));
133
+ return '';
134
+ }
135
+ async text() {
136
+ this._throwIfBodyUsed();
137
+ this._bodyUsed = true;
138
+ return this._getBodyString();
139
+ }
140
+ async json() {
141
+ this._throwIfBodyUsed();
142
+ this._bodyUsed = true;
143
+ const t = this._getBodyString();
144
+ return JSON.parse(t || '{}');
145
+ }
146
+ async arrayBuffer() {
147
+ this._throwIfBodyUsed();
148
+ this._bodyUsed = true;
149
+ return this._getBodyBytes() ?? new ArrayBuffer(0);
150
+ }
151
+ async blob() {
152
+ this._throwIfBodyUsed();
153
+ this._bodyUsed = true;
154
+ const buffer = this._getBodyBytes() ?? new ArrayBuffer(0);
155
+ const contentType = this.headers.get('content-type') ?? '';
156
+ return new Blob([buffer], { type: contentType });
157
+ }
158
+ async bytes() {
159
+ this._throwIfBodyUsed();
160
+ this._bodyUsed = true;
161
+ const buffer = this._getBodyBytes() ?? new ArrayBuffer(0);
162
+ return new Uint8Array(buffer);
163
+ }
164
+ clone() {
165
+ if (this._bodyUsed) {
166
+ throw new TypeError('Cannot clone a Request whose body has been used.');
167
+ }
168
+ return new NitroRequest(this);
169
+ }
170
+ async formData() {
171
+ throw new TypeError('formData() is not supported in NitroRequest');
172
+ }
173
+ }