react-native-nitro-fetch 0.3.0-beta.1 → 1.0.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 (61) 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/app.plugin.js +1 -0
  6. package/expo/plugins/dist/index.d.ts +2 -0
  7. package/expo/plugins/dist/index.js +11 -0
  8. package/expo/plugins/dist/withAndroid.d.ts +2 -0
  9. package/expo/plugins/dist/withAndroid.js +21 -0
  10. package/ios/FetchCache.swift +14 -7
  11. package/ios/NitroFetchClient.swift +36 -1
  12. package/lib/module/CurlGenerator.js +28 -0
  13. package/lib/module/CurlGenerator.js.map +1 -0
  14. package/lib/module/Headers.js +95 -0
  15. package/lib/module/Headers.js.map +1 -0
  16. package/lib/module/HermesProfiler.js +28 -0
  17. package/lib/module/HermesProfiler.js.map +1 -0
  18. package/lib/module/NetworkInspector.js +184 -0
  19. package/lib/module/NetworkInspector.js.map +1 -0
  20. package/lib/module/Request.js +120 -0
  21. package/lib/module/Request.js.map +1 -0
  22. package/lib/module/Response.js +236 -0
  23. package/lib/module/Response.js.map +1 -0
  24. package/lib/module/fetch.js +143 -56
  25. package/lib/module/fetch.js.map +1 -1
  26. package/lib/module/index.js +6 -0
  27. package/lib/module/index.js.map +1 -1
  28. package/lib/module/utf8.js +29 -0
  29. package/lib/module/utf8.js.map +1 -0
  30. package/lib/typescript/expo/plugins/src/index.d.ts +3 -0
  31. package/lib/typescript/expo/plugins/src/index.d.ts.map +1 -0
  32. package/lib/typescript/expo/plugins/src/withAndroid.d.ts +3 -0
  33. package/lib/typescript/expo/plugins/src/withAndroid.d.ts.map +1 -0
  34. package/lib/typescript/src/CurlGenerator.d.ts +13 -0
  35. package/lib/typescript/src/CurlGenerator.d.ts.map +1 -0
  36. package/lib/typescript/src/Headers.d.ts +19 -0
  37. package/lib/typescript/src/Headers.d.ts.map +1 -0
  38. package/lib/typescript/src/HermesProfiler.d.ts +6 -0
  39. package/lib/typescript/src/HermesProfiler.d.ts.map +1 -0
  40. package/lib/typescript/src/NetworkInspector.d.ts +96 -0
  41. package/lib/typescript/src/NetworkInspector.d.ts.map +1 -0
  42. package/lib/typescript/src/Request.d.ts +48 -0
  43. package/lib/typescript/src/Request.d.ts.map +1 -0
  44. package/lib/typescript/src/Response.d.ts +56 -0
  45. package/lib/typescript/src/Response.d.ts.map +1 -0
  46. package/lib/typescript/src/fetch.d.ts +11 -3
  47. package/lib/typescript/src/fetch.d.ts.map +1 -1
  48. package/lib/typescript/src/index.d.ts +13 -1
  49. package/lib/typescript/src/index.d.ts.map +1 -1
  50. package/lib/typescript/src/utf8.d.ts +3 -0
  51. package/lib/typescript/src/utf8.d.ts.map +1 -0
  52. package/package.json +11 -5
  53. package/src/CurlGenerator.ts +44 -0
  54. package/src/Headers.ts +127 -0
  55. package/src/HermesProfiler.ts +37 -0
  56. package/src/NetworkInspector.ts +278 -0
  57. package/src/Request.ts +187 -0
  58. package/src/Response.ts +335 -0
  59. package/src/fetch.ts +186 -75
  60. package/src/index.tsx +22 -1
  61. package/src/utf8.ts +40 -0
@@ -0,0 +1,278 @@
1
+ import { generateCurl } from './CurlGenerator';
2
+
3
+ export interface NetworkEntry {
4
+ id: string;
5
+ type: 'http';
6
+ url: string;
7
+ method: string;
8
+ requestHeaders: Array<{ key: string; value: string }>;
9
+ requestBody: string | undefined;
10
+ requestBodySize: number;
11
+ status: number;
12
+ statusText: string;
13
+ responseHeaders: Array<{ key: string; value: string }>;
14
+ responseBody?: string;
15
+ responseBodySize: number;
16
+ startTime: number;
17
+ endTime: number;
18
+ duration: number;
19
+ curl: string;
20
+ error?: string;
21
+ }
22
+
23
+ export interface WebSocketMessage {
24
+ direction: 'sent' | 'received';
25
+ data: string;
26
+ size: number;
27
+ isBinary: boolean;
28
+ timestamp: number;
29
+ }
30
+
31
+ export interface WebSocketEntry {
32
+ id: string;
33
+ type: 'websocket';
34
+ url: string;
35
+ protocols: string[];
36
+ requestHeaders: Array<{ key: string; value: string }>;
37
+ startTime: number;
38
+ endTime: number;
39
+ duration: number;
40
+ readyState: string;
41
+ messages: WebSocketMessage[];
42
+ messagesSent: number;
43
+ messagesReceived: number;
44
+ bytesSent: number;
45
+ bytesReceived: number;
46
+ closeCode?: number;
47
+ closeReason?: string;
48
+ error?: string;
49
+ }
50
+
51
+ export type InspectorEntry = NetworkEntry | WebSocketEntry;
52
+
53
+ export type NetworkEntryCallback = (entry: InspectorEntry) => void;
54
+
55
+ class NetworkInspectorImpl {
56
+ private _enabled: boolean = false;
57
+ private _entries: InspectorEntry[] = [];
58
+ private _maxEntries: number = 500;
59
+ private _maxBodyCapture: number = 4096;
60
+ private _listeners: Set<NetworkEntryCallback> = new Set();
61
+
62
+ enable(options?: { maxEntries?: number; maxBodyCapture?: number }): void {
63
+ this._enabled = true;
64
+ if (options?.maxEntries != null) this._maxEntries = options.maxEntries;
65
+ if (options?.maxBodyCapture != null)
66
+ this._maxBodyCapture = options.maxBodyCapture;
67
+ }
68
+
69
+ disable(): void {
70
+ this._enabled = false;
71
+ }
72
+
73
+ isEnabled(): boolean {
74
+ return this._enabled;
75
+ }
76
+
77
+ getEntries(): ReadonlyArray<InspectorEntry> {
78
+ return this._entries;
79
+ }
80
+
81
+ getHttpEntries(): ReadonlyArray<NetworkEntry> {
82
+ return this._entries.filter((e): e is NetworkEntry => e.type === 'http');
83
+ }
84
+
85
+ getWebSocketEntries(): ReadonlyArray<WebSocketEntry> {
86
+ return this._entries.filter(
87
+ (e): e is WebSocketEntry => e.type === 'websocket'
88
+ );
89
+ }
90
+
91
+ getEntry(id: string): InspectorEntry | undefined {
92
+ return this._entries.find((e) => e.id === id);
93
+ }
94
+
95
+ clear(): void {
96
+ this._entries = [];
97
+ }
98
+
99
+ onEntry(callback: NetworkEntryCallback): () => void {
100
+ this._listeners.add(callback);
101
+ return () => {
102
+ this._listeners.delete(callback);
103
+ };
104
+ }
105
+
106
+ private _notify(entry: InspectorEntry): void {
107
+ for (const cb of this._listeners) {
108
+ try {
109
+ cb(entry);
110
+ } catch {
111
+ // swallow listener errors
112
+ }
113
+ }
114
+ }
115
+
116
+ private _trimEntries(): void {
117
+ if (this._entries.length > this._maxEntries) {
118
+ this._entries.shift();
119
+ }
120
+ }
121
+
122
+ // --- HTTP recording ---
123
+
124
+ _recordStart(
125
+ id: string,
126
+ url: string,
127
+ method: string,
128
+ headers: Array<{ key: string; value: string }>,
129
+ body?: string
130
+ ): void {
131
+ if (!this._enabled) return;
132
+ const bodySize = body ? body.length : 0;
133
+ const entry: NetworkEntry = {
134
+ id,
135
+ type: 'http',
136
+ url,
137
+ method,
138
+ requestHeaders: headers.map((h) => ({ key: h.key, value: h.value })),
139
+ requestBody: body ? body.slice(0, this._maxBodyCapture) : undefined,
140
+ requestBodySize: bodySize,
141
+ status: 0,
142
+ statusText: '',
143
+ responseHeaders: [],
144
+ responseBodySize: 0,
145
+ startTime: performance.now(),
146
+ endTime: 0,
147
+ duration: 0,
148
+ curl: generateCurl({ url, method, headers, body }),
149
+ };
150
+ this._entries.push(entry);
151
+ this._trimEntries();
152
+ }
153
+
154
+ _recordEnd(
155
+ id: string,
156
+ status: number,
157
+ statusText: string,
158
+ headers: Array<{ key: string; value: string }>,
159
+ bodySize: number,
160
+ error?: string,
161
+ responseBody?: string
162
+ ): void {
163
+ if (!this._enabled) return;
164
+ const entry = this._entries.find(
165
+ (e) => e.id === id && e.type === 'http'
166
+ ) as NetworkEntry | undefined;
167
+ if (!entry) return;
168
+ entry.status = status;
169
+ entry.statusText = statusText;
170
+ entry.responseHeaders = headers.map((h) => ({
171
+ key: h.key,
172
+ value: h.value,
173
+ }));
174
+ entry.responseBodySize = bodySize;
175
+ entry.endTime = performance.now();
176
+ entry.duration = entry.endTime - entry.startTime;
177
+ if (error) entry.error = error;
178
+ if (responseBody != null) {
179
+ entry.responseBody = responseBody.slice(0, this._maxBodyCapture);
180
+ }
181
+ this._notify(entry);
182
+ }
183
+
184
+ // --- WebSocket recording ---
185
+
186
+ _recordWsOpen(
187
+ id: string,
188
+ url: string,
189
+ protocols: string[],
190
+ headers: Array<{ key: string; value: string }>
191
+ ): void {
192
+ if (!this._enabled) return;
193
+ const entry: WebSocketEntry = {
194
+ id,
195
+ type: 'websocket',
196
+ url,
197
+ protocols,
198
+ requestHeaders: headers.map((h) => ({ key: h.key, value: h.value })),
199
+ startTime: performance.now(),
200
+ endTime: 0,
201
+ duration: 0,
202
+ readyState: 'CONNECTING',
203
+ messages: [],
204
+ messagesSent: 0,
205
+ messagesReceived: 0,
206
+ bytesSent: 0,
207
+ bytesReceived: 0,
208
+ };
209
+ this._entries.push(entry);
210
+ this._trimEntries();
211
+ this._notify(entry);
212
+ }
213
+
214
+ _recordWsConnected(id: string): void {
215
+ if (!this._enabled) return;
216
+ const entry = this._entries.find(
217
+ (e) => e.id === id && e.type === 'websocket'
218
+ ) as WebSocketEntry | undefined;
219
+ if (!entry) return;
220
+ entry.readyState = 'OPEN';
221
+ this._notify(entry);
222
+ }
223
+
224
+ _recordWsMessage(
225
+ id: string,
226
+ direction: 'sent' | 'received',
227
+ data: string,
228
+ size: number,
229
+ isBinary: boolean
230
+ ): void {
231
+ if (!this._enabled) return;
232
+ const entry = this._entries.find(
233
+ (e) => e.id === id && e.type === 'websocket'
234
+ ) as WebSocketEntry | undefined;
235
+ if (!entry) return;
236
+ entry.messages.push({
237
+ direction,
238
+ data: data.slice(0, this._maxBodyCapture),
239
+ size,
240
+ isBinary,
241
+ timestamp: performance.now(),
242
+ });
243
+ if (direction === 'sent') {
244
+ entry.messagesSent++;
245
+ entry.bytesSent += size;
246
+ } else {
247
+ entry.messagesReceived++;
248
+ entry.bytesReceived += size;
249
+ }
250
+ this._notify(entry);
251
+ }
252
+
253
+ _recordWsClose(id: string, code: number, reason: string): void {
254
+ if (!this._enabled) return;
255
+ const entry = this._entries.find(
256
+ (e) => e.id === id && e.type === 'websocket'
257
+ ) as WebSocketEntry | undefined;
258
+ if (!entry) return;
259
+ entry.readyState = 'CLOSED';
260
+ entry.closeCode = code;
261
+ entry.closeReason = reason;
262
+ entry.endTime = performance.now();
263
+ entry.duration = entry.endTime - entry.startTime;
264
+ this._notify(entry);
265
+ }
266
+
267
+ _recordWsError(id: string, error: string): void {
268
+ if (!this._enabled) return;
269
+ const entry = this._entries.find(
270
+ (e) => e.id === id && e.type === 'websocket'
271
+ ) as WebSocketEntry | undefined;
272
+ if (!entry) return;
273
+ entry.error = error;
274
+ this._notify(entry);
275
+ }
276
+ }
277
+
278
+ export const NetworkInspector = new NetworkInspectorImpl();
package/src/Request.ts ADDED
@@ -0,0 +1,187 @@
1
+ import { NitroHeaders } from './Headers';
2
+ import { stringToUTF8, utf8ToString } from './utf8';
3
+
4
+ export type RequestRedirect = 'follow' | 'error' | 'manual';
5
+ export type RequestCache =
6
+ | 'default'
7
+ | 'no-store'
8
+ | 'no-cache'
9
+ | 'reload'
10
+ | 'force-cache'
11
+ | 'only-if-cached';
12
+
13
+ export interface NitroRequestInit {
14
+ method?: string;
15
+ headers?: HeadersInit | NitroHeaders;
16
+ body?: BodyInit | null;
17
+ redirect?: RequestRedirect;
18
+ signal?: AbortSignal | null;
19
+ cache?: RequestCache;
20
+ credentials?: RequestCredentials;
21
+ mode?: RequestMode;
22
+ referrer?: string;
23
+ referrerPolicy?: ReferrerPolicy;
24
+ integrity?: string;
25
+ keepalive?: boolean;
26
+ }
27
+
28
+ export class NitroRequest {
29
+ readonly url: string;
30
+ readonly method: string;
31
+ readonly headers: NitroHeaders;
32
+ readonly redirect: RequestRedirect;
33
+ readonly signal: AbortSignal;
34
+ readonly cache: RequestCache;
35
+ readonly credentials: RequestCredentials;
36
+ readonly mode: RequestMode;
37
+ readonly referrer: string;
38
+ readonly referrerPolicy: ReferrerPolicy;
39
+ readonly integrity: string;
40
+ readonly keepalive: boolean;
41
+ readonly destination: string;
42
+
43
+ private _body: BodyInit | null;
44
+ private _bodyUsed: boolean = false;
45
+
46
+ constructor(input: string | URL | NitroRequest, init?: NitroRequestInit) {
47
+ if (input instanceof NitroRequest) {
48
+ // Clone from another NitroRequest
49
+ this.url = init?.method ? input.url : input.url; // URL always from input
50
+ this.method = (init?.method ?? input.method).toUpperCase();
51
+ this.headers = new NitroHeaders(
52
+ init?.headers
53
+ ? init.headers instanceof NitroHeaders
54
+ ? init.headers
55
+ : (init.headers as any)
56
+ : input.headers
57
+ );
58
+ this.redirect = init?.redirect ?? input.redirect;
59
+ this.signal = init?.signal ?? input.signal;
60
+ this.cache = init?.cache ?? input.cache;
61
+ this.credentials = init?.credentials ?? input.credentials;
62
+ this.mode = init?.mode ?? input.mode;
63
+ this.referrer = init?.referrer ?? input.referrer;
64
+ this.referrerPolicy = init?.referrerPolicy ?? input.referrerPolicy;
65
+ this.integrity = init?.integrity ?? input.integrity;
66
+ this.keepalive = init?.keepalive ?? input.keepalive;
67
+ this._body = init?.body !== undefined ? (init.body ?? null) : input._body;
68
+ } else {
69
+ this.url = String(input);
70
+ this.method = (init?.method ?? 'GET').toUpperCase();
71
+ this.headers = new NitroHeaders(
72
+ init?.headers
73
+ ? init.headers instanceof NitroHeaders
74
+ ? init.headers
75
+ : (init.headers as any)
76
+ : undefined
77
+ );
78
+ this.redirect = init?.redirect ?? 'follow';
79
+ this.signal = init?.signal ?? new AbortController().signal;
80
+ this.cache = init?.cache ?? 'default';
81
+ this.credentials = init?.credentials ?? 'same-origin';
82
+ this.mode = init?.mode ?? 'cors';
83
+ this.referrer = init?.referrer ?? 'about:client';
84
+ this.referrerPolicy = init?.referrerPolicy ?? '';
85
+ this.integrity = init?.integrity ?? '';
86
+ this.keepalive = init?.keepalive ?? false;
87
+ this._body = init?.body ?? null;
88
+ }
89
+
90
+ this.destination = '';
91
+ }
92
+
93
+ get bodyUsed(): boolean {
94
+ return this._bodyUsed;
95
+ }
96
+
97
+ get body(): ReadableStream<Uint8Array> | null {
98
+ if (this._body == null) return null;
99
+ const bodyBytes = this._getBodyBytes();
100
+ if (!bodyBytes) return null;
101
+ return new ReadableStream<Uint8Array>({
102
+ start(controller) {
103
+ controller.enqueue(new Uint8Array(bodyBytes));
104
+ controller.close();
105
+ },
106
+ });
107
+ }
108
+
109
+ private _throwIfBodyUsed(): void {
110
+ if (this._bodyUsed) {
111
+ throw new TypeError('Body has already been consumed.');
112
+ }
113
+ }
114
+
115
+ private _getBodyBytes(): ArrayBuffer | undefined {
116
+ if (this._body == null) return undefined;
117
+ if (typeof this._body === 'string') {
118
+ const encoded = stringToUTF8(this._body);
119
+ return (encoded.buffer as ArrayBuffer).slice(
120
+ encoded.byteOffset,
121
+ encoded.byteOffset + encoded.byteLength
122
+ );
123
+ }
124
+ if (this._body instanceof ArrayBuffer) return this._body;
125
+ if (ArrayBuffer.isView(this._body)) {
126
+ const view = this._body;
127
+ return (view.buffer as ArrayBuffer).slice(
128
+ view.byteOffset,
129
+ view.byteOffset + view.byteLength
130
+ );
131
+ }
132
+ return undefined;
133
+ }
134
+
135
+ private _getBodyString(): string {
136
+ if (this._body == null) return '';
137
+ if (typeof this._body === 'string') return this._body;
138
+ const bytes = this._getBodyBytes();
139
+ if (bytes) return utf8ToString(new Uint8Array(bytes));
140
+ return '';
141
+ }
142
+
143
+ async text(): Promise<string> {
144
+ this._throwIfBodyUsed();
145
+ this._bodyUsed = true;
146
+ return this._getBodyString();
147
+ }
148
+
149
+ async json(): Promise<any> {
150
+ this._throwIfBodyUsed();
151
+ this._bodyUsed = true;
152
+ const t = this._getBodyString();
153
+ return JSON.parse(t || '{}');
154
+ }
155
+
156
+ async arrayBuffer(): Promise<ArrayBuffer> {
157
+ this._throwIfBodyUsed();
158
+ this._bodyUsed = true;
159
+ return this._getBodyBytes() ?? new ArrayBuffer(0);
160
+ }
161
+
162
+ async blob(): Promise<Blob> {
163
+ this._throwIfBodyUsed();
164
+ this._bodyUsed = true;
165
+ const buffer = this._getBodyBytes() ?? new ArrayBuffer(0);
166
+ const contentType = this.headers.get('content-type') ?? '';
167
+ return new Blob([buffer], { type: contentType });
168
+ }
169
+
170
+ async bytes(): Promise<Uint8Array> {
171
+ this._throwIfBodyUsed();
172
+ this._bodyUsed = true;
173
+ const buffer = this._getBodyBytes() ?? new ArrayBuffer(0);
174
+ return new Uint8Array(buffer);
175
+ }
176
+
177
+ clone(): NitroRequest {
178
+ if (this._bodyUsed) {
179
+ throw new TypeError('Cannot clone a Request whose body has been used.');
180
+ }
181
+ return new NitroRequest(this);
182
+ }
183
+
184
+ async formData(): Promise<never> {
185
+ throw new TypeError('formData() is not supported in NitroRequest');
186
+ }
187
+ }