react-native-network-lens 0.0.0-stage → 0.1.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 (180) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +208 -2
  3. package/lib/module/backHandler.js +31 -0
  4. package/lib/module/backHandler.js.map +1 -0
  5. package/lib/module/components/BodyViewer.js +213 -0
  6. package/lib/module/components/BodyViewer.js.map +1 -0
  7. package/lib/module/components/CodeList.js +104 -0
  8. package/lib/module/components/CodeList.js.map +1 -0
  9. package/lib/module/components/JsonTree/JsonTreeView.js +206 -0
  10. package/lib/module/components/JsonTree/JsonTreeView.js.map +1 -0
  11. package/lib/module/components/JsonTree/model.js +196 -0
  12. package/lib/module/components/JsonTree/model.js.map +1 -0
  13. package/lib/module/components/NetworkLens.js +110 -0
  14. package/lib/module/components/NetworkLens.js.map +1 -0
  15. package/lib/module/components/NetworkLensOverlay.js +158 -0
  16. package/lib/module/components/NetworkLensOverlay.js.map +1 -0
  17. package/lib/module/components/RawTextView.js +119 -0
  18. package/lib/module/components/RawTextView.js.map +1 -0
  19. package/lib/module/components/RequestDetails/RequestDetails.js +256 -0
  20. package/lib/module/components/RequestDetails/RequestDetails.js.map +1 -0
  21. package/lib/module/components/RequestList/FilterBar.js +138 -0
  22. package/lib/module/components/RequestList/FilterBar.js.map +1 -0
  23. package/lib/module/components/RequestList/RequestList.js +163 -0
  24. package/lib/module/components/RequestList/RequestList.js.map +1 -0
  25. package/lib/module/components/RequestList/RequestRow.js +161 -0
  26. package/lib/module/components/RequestList/RequestRow.js.map +1 -0
  27. package/lib/module/components/RequestList/filters.js +57 -0
  28. package/lib/module/components/RequestList/filters.js.map +1 -0
  29. package/lib/module/components/ui/ActionSheet.js +94 -0
  30. package/lib/module/components/ui/ActionSheet.js.map +1 -0
  31. package/lib/module/components/ui/Button.js +104 -0
  32. package/lib/module/components/ui/Button.js.map +1 -0
  33. package/lib/module/components/ui/Pill.js +53 -0
  34. package/lib/module/components/ui/Pill.js.map +1 -0
  35. package/lib/module/components/ui/SearchInput.js +77 -0
  36. package/lib/module/components/ui/SearchInput.js.map +1 -0
  37. package/lib/module/components/ui/Section.js +147 -0
  38. package/lib/module/components/ui/Section.js.map +1 -0
  39. package/lib/module/components/ui/SegmentedControl.js +65 -0
  40. package/lib/module/components/ui/SegmentedControl.js.map +1 -0
  41. package/lib/module/components/ui/index.js +9 -0
  42. package/lib/module/components/ui/index.js.map +1 -0
  43. package/lib/module/core/Logger.js +230 -0
  44. package/lib/module/core/Logger.js.map +1 -0
  45. package/lib/module/core/NetworkRequest.js +155 -0
  46. package/lib/module/core/NetworkRequest.js.map +1 -0
  47. package/lib/module/core/XHRInterceptor.js +104 -0
  48. package/lib/module/core/XHRInterceptor.js.map +1 -0
  49. package/lib/module/core/har.js +72 -0
  50. package/lib/module/core/har.js.map +1 -0
  51. package/lib/module/core/loggerSingleton.js +12 -0
  52. package/lib/module/core/loggerSingleton.js.map +1 -0
  53. package/lib/module/hooks.js +42 -0
  54. package/lib/module/hooks.js.map +1 -0
  55. package/lib/module/index.js +34 -0
  56. package/lib/module/index.js.map +1 -0
  57. package/lib/module/package.json +1 -0
  58. package/lib/module/theme.js +136 -0
  59. package/lib/module/theme.js.map +1 -0
  60. package/lib/module/types.js +2 -0
  61. package/lib/module/types.js.map +1 -0
  62. package/lib/module/utils/body.js +179 -0
  63. package/lib/module/utils/body.js.map +1 -0
  64. package/lib/module/utils/debounce.js +18 -0
  65. package/lib/module/utils/debounce.js.map +1 -0
  66. package/lib/module/utils/format.js +45 -0
  67. package/lib/module/utils/format.js.map +1 -0
  68. package/lib/module/utils/logger.js +6 -0
  69. package/lib/module/utils/logger.js.map +1 -0
  70. package/lib/module/utils/search.js +70 -0
  71. package/lib/module/utils/search.js.map +1 -0
  72. package/lib/module/utils/url.js +50 -0
  73. package/lib/module/utils/url.js.map +1 -0
  74. package/lib/typescript/package.json +1 -0
  75. package/lib/typescript/src/backHandler.d.ts +15 -0
  76. package/lib/typescript/src/backHandler.d.ts.map +1 -0
  77. package/lib/typescript/src/components/BodyViewer.d.ts +16 -0
  78. package/lib/typescript/src/components/BodyViewer.d.ts.map +1 -0
  79. package/lib/typescript/src/components/CodeList.d.ts +21 -0
  80. package/lib/typescript/src/components/CodeList.d.ts.map +1 -0
  81. package/lib/typescript/src/components/JsonTree/JsonTreeView.d.ts +21 -0
  82. package/lib/typescript/src/components/JsonTree/JsonTreeView.d.ts.map +1 -0
  83. package/lib/typescript/src/components/JsonTree/model.d.ts +53 -0
  84. package/lib/typescript/src/components/JsonTree/model.d.ts.map +1 -0
  85. package/lib/typescript/src/components/NetworkLens.d.ts +31 -0
  86. package/lib/typescript/src/components/NetworkLens.d.ts.map +1 -0
  87. package/lib/typescript/src/components/NetworkLensOverlay.d.ts +32 -0
  88. package/lib/typescript/src/components/NetworkLensOverlay.d.ts.map +1 -0
  89. package/lib/typescript/src/components/RawTextView.d.ts +14 -0
  90. package/lib/typescript/src/components/RawTextView.d.ts.map +1 -0
  91. package/lib/typescript/src/components/RequestDetails/RequestDetails.d.ts +9 -0
  92. package/lib/typescript/src/components/RequestDetails/RequestDetails.d.ts.map +1 -0
  93. package/lib/typescript/src/components/RequestList/FilterBar.d.ts +12 -0
  94. package/lib/typescript/src/components/RequestList/FilterBar.d.ts.map +1 -0
  95. package/lib/typescript/src/components/RequestList/RequestList.d.ts +18 -0
  96. package/lib/typescript/src/components/RequestList/RequestList.d.ts.map +1 -0
  97. package/lib/typescript/src/components/RequestList/RequestRow.d.ts +11 -0
  98. package/lib/typescript/src/components/RequestList/RequestRow.d.ts.map +1 -0
  99. package/lib/typescript/src/components/RequestList/filters.d.ts +17 -0
  100. package/lib/typescript/src/components/RequestList/filters.d.ts.map +1 -0
  101. package/lib/typescript/src/components/ui/ActionSheet.d.ts +15 -0
  102. package/lib/typescript/src/components/ui/ActionSheet.d.ts.map +1 -0
  103. package/lib/typescript/src/components/ui/Button.d.ts +21 -0
  104. package/lib/typescript/src/components/ui/Button.d.ts.map +1 -0
  105. package/lib/typescript/src/components/ui/Pill.d.ts +11 -0
  106. package/lib/typescript/src/components/ui/Pill.d.ts.map +1 -0
  107. package/lib/typescript/src/components/ui/SearchInput.d.ts +17 -0
  108. package/lib/typescript/src/components/ui/SearchInput.d.ts.map +1 -0
  109. package/lib/typescript/src/components/ui/Section.d.ts +16 -0
  110. package/lib/typescript/src/components/ui/Section.d.ts.map +1 -0
  111. package/lib/typescript/src/components/ui/SegmentedControl.d.ts +14 -0
  112. package/lib/typescript/src/components/ui/SegmentedControl.d.ts.map +1 -0
  113. package/lib/typescript/src/components/ui/index.d.ts +7 -0
  114. package/lib/typescript/src/components/ui/index.d.ts.map +1 -0
  115. package/lib/typescript/src/core/Logger.d.ts +48 -0
  116. package/lib/typescript/src/core/Logger.d.ts.map +1 -0
  117. package/lib/typescript/src/core/NetworkRequest.d.ts +48 -0
  118. package/lib/typescript/src/core/NetworkRequest.d.ts.map +1 -0
  119. package/lib/typescript/src/core/XHRInterceptor.d.ts +29 -0
  120. package/lib/typescript/src/core/XHRInterceptor.d.ts.map +1 -0
  121. package/lib/typescript/src/core/har.d.ts +65 -0
  122. package/lib/typescript/src/core/har.d.ts.map +1 -0
  123. package/lib/typescript/src/core/loggerSingleton.d.ts +9 -0
  124. package/lib/typescript/src/core/loggerSingleton.d.ts.map +1 -0
  125. package/lib/typescript/src/hooks.d.ts +11 -0
  126. package/lib/typescript/src/hooks.d.ts.map +1 -0
  127. package/lib/typescript/src/index.d.ts +33 -0
  128. package/lib/typescript/src/index.d.ts.map +1 -0
  129. package/lib/typescript/src/theme.d.ts +49 -0
  130. package/lib/typescript/src/theme.d.ts.map +1 -0
  131. package/lib/typescript/src/types.d.ts +80 -0
  132. package/lib/typescript/src/types.d.ts.map +1 -0
  133. package/lib/typescript/src/utils/body.d.ts +19 -0
  134. package/lib/typescript/src/utils/body.d.ts.map +1 -0
  135. package/lib/typescript/src/utils/debounce.d.ts +4 -0
  136. package/lib/typescript/src/utils/debounce.d.ts.map +1 -0
  137. package/lib/typescript/src/utils/format.d.ts +7 -0
  138. package/lib/typescript/src/utils/format.d.ts.map +1 -0
  139. package/lib/typescript/src/utils/logger.d.ts +2 -0
  140. package/lib/typescript/src/utils/logger.d.ts.map +1 -0
  141. package/lib/typescript/src/utils/search.d.ts +17 -0
  142. package/lib/typescript/src/utils/search.d.ts.map +1 -0
  143. package/lib/typescript/src/utils/url.d.ts +15 -0
  144. package/lib/typescript/src/utils/url.d.ts.map +1 -0
  145. package/package.json +186 -4
  146. package/src/backHandler.ts +29 -0
  147. package/src/components/BodyViewer.tsx +250 -0
  148. package/src/components/CodeList.tsx +120 -0
  149. package/src/components/JsonTree/JsonTreeView.tsx +304 -0
  150. package/src/components/JsonTree/model.ts +266 -0
  151. package/src/components/NetworkLens.tsx +176 -0
  152. package/src/components/NetworkLensOverlay.tsx +187 -0
  153. package/src/components/RawTextView.tsx +163 -0
  154. package/src/components/RequestDetails/RequestDetails.tsx +315 -0
  155. package/src/components/RequestList/FilterBar.tsx +168 -0
  156. package/src/components/RequestList/RequestList.tsx +226 -0
  157. package/src/components/RequestList/RequestRow.tsx +150 -0
  158. package/src/components/RequestList/filters.ts +98 -0
  159. package/src/components/ui/ActionSheet.tsx +109 -0
  160. package/src/components/ui/Button.tsx +124 -0
  161. package/src/components/ui/Pill.tsx +53 -0
  162. package/src/components/ui/SearchInput.tsx +97 -0
  163. package/src/components/ui/Section.tsx +133 -0
  164. package/src/components/ui/SegmentedControl.tsx +79 -0
  165. package/src/components/ui/index.ts +6 -0
  166. package/src/core/Logger.ts +293 -0
  167. package/src/core/NetworkRequest.ts +216 -0
  168. package/src/core/XHRInterceptor.ts +179 -0
  169. package/src/core/har.ts +71 -0
  170. package/src/core/loggerSingleton.ts +14 -0
  171. package/src/hooks.ts +51 -0
  172. package/src/index.tsx +56 -0
  173. package/src/theme.ts +174 -0
  174. package/src/types.ts +83 -0
  175. package/src/utils/body.ts +194 -0
  176. package/src/utils/debounce.ts +18 -0
  177. package/src/utils/format.ts +50 -0
  178. package/src/utils/logger.ts +3 -0
  179. package/src/utils/search.ts +72 -0
  180. package/src/utils/url.ts +59 -0
package/src/theme.ts ADDED
@@ -0,0 +1,174 @@
1
+ import { createContext, useContext } from 'react';
2
+ import { Platform, useColorScheme } from 'react-native';
3
+ import type { DeepPartial } from './types';
4
+
5
+ export type ThemeName = 'light' | 'dark' | 'auto';
6
+
7
+ export type Theme = {
8
+ colors: {
9
+ background: string;
10
+ surface: string;
11
+ surfaceAlt: string;
12
+ border: string;
13
+ text: string;
14
+ textMuted: string;
15
+ primary: string;
16
+ onPrimary: string;
17
+ statusGood: string;
18
+ statusRedirect: string;
19
+ statusWarning: string;
20
+ statusBad: string;
21
+ statusPending: string;
22
+ methodGet: string;
23
+ methodPost: string;
24
+ methodPut: string;
25
+ methodPatch: string;
26
+ methodDelete: string;
27
+ methodOther: string;
28
+ jsonKey: string;
29
+ jsonString: string;
30
+ jsonNumber: string;
31
+ jsonBoolean: string;
32
+ jsonNull: string;
33
+ highlight: string;
34
+ highlightActive: string;
35
+ onHighlight: string;
36
+ };
37
+ fonts: {
38
+ mono: string;
39
+ };
40
+ sizes: {
41
+ codeFontSize: number;
42
+ codeLineHeight: number;
43
+ };
44
+ };
45
+
46
+ const mono = Platform.select({
47
+ ios: 'Menlo',
48
+ android: 'monospace',
49
+ default: 'monospace',
50
+ });
51
+
52
+ export const lightTheme: Theme = {
53
+ colors: {
54
+ background: '#f2f3f5',
55
+ surface: '#ffffff',
56
+ surfaceAlt: '#eaecf0',
57
+ border: '#d9dce1',
58
+ text: '#161a21',
59
+ textMuted: '#6b7280',
60
+ primary: '#2563eb',
61
+ onPrimary: '#ffffff',
62
+ statusGood: '#15803d',
63
+ statusRedirect: '#0e7490',
64
+ statusWarning: '#c2410c',
65
+ statusBad: '#b91c1c',
66
+ statusPending: '#6b7280',
67
+ methodGet: '#2563eb',
68
+ methodPost: '#15803d',
69
+ methodPut: '#b45309',
70
+ methodPatch: '#7c3aed',
71
+ methodDelete: '#b91c1c',
72
+ methodOther: '#475569',
73
+ jsonKey: '#881391',
74
+ jsonString: '#c41a16',
75
+ jsonNumber: '#1c00cf',
76
+ jsonBoolean: '#0d22aa',
77
+ jsonNull: '#808080',
78
+ highlight: '#fde68a',
79
+ highlightActive: '#f59e0b',
80
+ onHighlight: '#1f2937',
81
+ },
82
+ fonts: { mono: mono ?? 'monospace' },
83
+ sizes: { codeFontSize: 13, codeLineHeight: 20 },
84
+ };
85
+
86
+ export const darkTheme: Theme = {
87
+ colors: {
88
+ background: '#0f1115',
89
+ surface: '#181b21',
90
+ surfaceAlt: '#22262e',
91
+ border: '#2d323c',
92
+ text: '#e6e8eb',
93
+ textMuted: '#9aa3af',
94
+ primary: '#60a5fa',
95
+ onPrimary: '#0b1220',
96
+ statusGood: '#4ade80',
97
+ statusRedirect: '#22d3ee',
98
+ statusWarning: '#fb923c',
99
+ statusBad: '#f87171',
100
+ statusPending: '#9aa3af',
101
+ methodGet: '#60a5fa',
102
+ methodPost: '#4ade80',
103
+ methodPut: '#fbbf24',
104
+ methodPatch: '#c084fc',
105
+ methodDelete: '#f87171',
106
+ methodOther: '#94a3b8',
107
+ jsonKey: '#e2a8ff',
108
+ jsonString: '#ff8a80',
109
+ jsonNumber: '#9ecbff',
110
+ jsonBoolean: '#9ecbff',
111
+ jsonNull: '#9aa3af',
112
+ highlight: '#5b4a12',
113
+ highlightActive: '#d97706',
114
+ onHighlight: '#fef3c7',
115
+ },
116
+ fonts: { mono: mono ?? 'monospace' },
117
+ sizes: { codeFontSize: 13, codeLineHeight: 20 },
118
+ };
119
+
120
+ export type ThemeInput = ThemeName | DeepPartial<Theme>;
121
+
122
+ export const ThemeContext = createContext<ThemeInput>('auto');
123
+
124
+ const merge = (base: Theme, override: DeepPartial<Theme>): Theme => ({
125
+ colors: { ...base.colors, ...(override.colors as Partial<Theme['colors']>) },
126
+ fonts: { ...base.fonts, ...(override.fonts as Partial<Theme['fonts']>) },
127
+ sizes: { ...base.sizes, ...(override.sizes as Partial<Theme['sizes']>) },
128
+ });
129
+
130
+ export const resolveTheme = (
131
+ input: ThemeInput,
132
+ systemScheme: 'light' | 'dark'
133
+ ): Theme => {
134
+ if (input === 'auto') return systemScheme === 'dark' ? darkTheme : lightTheme;
135
+ if (input === 'dark') return darkTheme;
136
+ if (input === 'light') return lightTheme;
137
+ return merge(systemScheme === 'dark' ? darkTheme : lightTheme, input);
138
+ };
139
+
140
+ export const useTheme = (): Theme => {
141
+ const input = useContext(ThemeContext);
142
+ const scheme = useColorScheme();
143
+ return resolveTheme(input, scheme === 'dark' ? 'dark' : 'light');
144
+ };
145
+
146
+ export const methodColor = (theme: Theme, method: string): string => {
147
+ switch (method) {
148
+ case 'GET':
149
+ return theme.colors.methodGet;
150
+ case 'POST':
151
+ return theme.colors.methodPost;
152
+ case 'PUT':
153
+ return theme.colors.methodPut;
154
+ case 'PATCH':
155
+ return theme.colors.methodPatch;
156
+ case 'DELETE':
157
+ return theme.colors.methodDelete;
158
+ default:
159
+ return theme.colors.methodOther;
160
+ }
161
+ };
162
+
163
+ export const statusColor = (
164
+ theme: Theme,
165
+ status: number,
166
+ failed = false
167
+ ): string => {
168
+ if (failed) return theme.colors.statusBad;
169
+ if (status <= 0) return theme.colors.statusPending;
170
+ if (status < 300) return theme.colors.statusGood;
171
+ if (status < 400) return theme.colors.statusRedirect;
172
+ if (status < 500) return theme.colors.statusWarning;
173
+ return theme.colors.statusBad;
174
+ };
package/src/types.ts ADDED
@@ -0,0 +1,83 @@
1
+ export type Headers = Record<string, string>;
2
+
3
+ export type RequestMethod =
4
+ | 'GET'
5
+ | 'POST'
6
+ | 'PUT'
7
+ | 'PATCH'
8
+ | 'DELETE'
9
+ | 'HEAD'
10
+ | 'OPTIONS'
11
+ | (string & {});
12
+
13
+ /** Lifecycle of a captured request. */
14
+ export type RequestState = 'pending' | 'done' | 'failed';
15
+
16
+ export type FailureReason = 'error' | 'timeout' | 'abort';
17
+
18
+ export type StatusGroup =
19
+ '1xx' | '2xx' | '3xx' | '4xx' | '5xx' | 'pending' | 'failed';
20
+
21
+ export interface NetworkLensOptions {
22
+ /**
23
+ * Master switch. When `false` nothing is intercepted and every UI component
24
+ * renders nothing, so you can wire it to `__DEV__` or a remote feature flag.
25
+ * @default true
26
+ */
27
+ enabled?: boolean;
28
+ /**
29
+ * Max number of requests to keep before the oldest ones are dropped.
30
+ * @default 500
31
+ */
32
+ maxRequests?: number;
33
+ /** Hosts to ignore, e.g. `services.test.com` */
34
+ ignoredHosts?: string[];
35
+ /** Exact urls to ignore, e.g. `https://services.test.com/test` */
36
+ ignoredUrls?: string[];
37
+ /**
38
+ * Patterns to ignore. They are tested against `${method} ${url}`,
39
+ * e.g. `/^GET https:\/\/test\.com\/pages\/.*$/`
40
+ */
41
+ ignoredPatterns?: RegExp[];
42
+ /**
43
+ * How often (ms) the UI is notified about new data.
44
+ * @default 50
45
+ */
46
+ refreshRate?: number;
47
+ }
48
+
49
+ /** @deprecated use `NetworkLensOptions` */
50
+ export type StartNetworkLoggingOptions = NetworkLensOptions;
51
+
52
+ /**
53
+ * A body after we tried to understand it.
54
+ * `text` is always the raw textual representation used by the "Raw" view.
55
+ */
56
+ export type ParsedBody =
57
+ | { kind: 'empty'; text: '' }
58
+ | { kind: 'json'; value: unknown; text: string }
59
+ | { kind: 'form'; value: Record<string, string>; text: string }
60
+ | { kind: 'text'; text: string }
61
+ | { kind: 'binary'; text: string; size: number; mimeType?: string };
62
+
63
+ /** Light-weight projection of a request used by list rows. */
64
+ export interface NetworkRequestRow {
65
+ id: string;
66
+ method: RequestMethod;
67
+ url: string;
68
+ host: string;
69
+ path: string;
70
+ status: number;
71
+ state: RequestState;
72
+ statusGroup: StatusGroup;
73
+ failureReason?: FailureReason;
74
+ duration: number;
75
+ startTime: number;
76
+ responseSize: number;
77
+ gqlOperation?: string;
78
+ updatedAt: number;
79
+ }
80
+
81
+ export type DeepPartial<T> = {
82
+ [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
83
+ };
@@ -0,0 +1,194 @@
1
+ import type { ParsedBody } from '../types';
2
+ import { byteLength } from './format';
3
+ import { parseQueryString } from './url';
4
+
5
+ const EMPTY: ParsedBody = { kind: 'empty', text: '' };
6
+
7
+ type FormDataLike = { _parts?: unknown[] };
8
+
9
+ const isFormDataLike = (value: unknown): value is FormDataLike =>
10
+ !!value &&
11
+ typeof value === 'object' &&
12
+ Array.isArray((value as FormDataLike)._parts);
13
+
14
+ const formPartsToRecord = (parts: unknown[]): Record<string, string> => {
15
+ const result: Record<string, string> = {};
16
+ for (const part of parts) {
17
+ if (!Array.isArray(part)) continue;
18
+ const [name, value] = part as [unknown, unknown];
19
+ const key = String(name);
20
+ if (value && typeof value === 'object') {
21
+ const file = value as { name?: string; type?: string; uri?: string };
22
+ result[key] = `[file${file.name ? ` ${file.name}` : ''}${
23
+ file.type ? ` (${file.type})` : ''
24
+ }]`;
25
+ } else {
26
+ result[key] = String(value);
27
+ }
28
+ }
29
+ return result;
30
+ };
31
+
32
+ const looksLikeUrlEncoded = (text: string, contentType?: string) => {
33
+ if (contentType?.includes('application/x-www-form-urlencoded')) return true;
34
+ if (text.length > 10_000) return false;
35
+ if (/[\s{}[\]"]/.test(text)) return false;
36
+ return /^[^=&]+=[^&]*(&[^=&]+=[^&]*)*$/.test(text);
37
+ };
38
+
39
+ export const stringifyJson = (value: unknown): string => {
40
+ try {
41
+ return JSON.stringify(value, null, 2) ?? '';
42
+ } catch {
43
+ return String(value);
44
+ }
45
+ };
46
+
47
+ export const tryParseJson = (
48
+ text: string
49
+ ): { ok: true; value: unknown } | { ok: false } => {
50
+ const trimmed = text.trim();
51
+ if (!trimmed) return { ok: false };
52
+ const first = trimmed[0];
53
+ if (
54
+ first !== '{' &&
55
+ first !== '[' &&
56
+ first !== '"' &&
57
+ !/^[-\d tfn]/.test(first ?? '')
58
+ ) {
59
+ return { ok: false };
60
+ }
61
+ try {
62
+ return { ok: true, value: JSON.parse(trimmed) };
63
+ } catch {
64
+ return { ok: false };
65
+ }
66
+ };
67
+
68
+ export const parseTextBody = (
69
+ text: string,
70
+ contentType?: string
71
+ ): ParsedBody => {
72
+ if (!text) return EMPTY;
73
+ const json = tryParseJson(text);
74
+ if (
75
+ json.ok &&
76
+ (typeof json.value === 'object' || Array.isArray(json.value))
77
+ ) {
78
+ return { kind: 'json', value: json.value, text: stringifyJson(json.value) };
79
+ }
80
+ if (looksLikeUrlEncoded(text, contentType)) {
81
+ const value = parseQueryString(text);
82
+ return { kind: 'form', value, text: stringifyJson(value) };
83
+ }
84
+ return { kind: 'text', text };
85
+ };
86
+
87
+ const describeBinary = (value: unknown, mimeType?: string): ParsedBody => {
88
+ let size = 0;
89
+ let label = 'binary';
90
+ if (typeof ArrayBuffer !== 'undefined' && value instanceof ArrayBuffer) {
91
+ size = value.byteLength;
92
+ label = 'ArrayBuffer';
93
+ } else if (value && typeof value === 'object') {
94
+ const v = value as { size?: number; byteLength?: number; type?: string };
95
+ size = v.size ?? v.byteLength ?? 0;
96
+ label = typeof v.size === 'number' ? 'Blob' : label;
97
+ mimeType = mimeType || v.type;
98
+ }
99
+ const text = `[${label}${mimeType ? ` ${mimeType}` : ''}${
100
+ size ? `, ${size} bytes` : ''
101
+ }]`;
102
+ return { kind: 'binary', text, size, mimeType };
103
+ };
104
+
105
+ /**
106
+ * Normalises whatever was passed to `xhr.send()`.
107
+ */
108
+ export const parseRequestBody = (
109
+ data: unknown,
110
+ contentType?: string
111
+ ): ParsedBody => {
112
+ if (data === null || data === undefined || data === '') return EMPTY;
113
+ if (typeof data === 'string') return parseTextBody(data, contentType);
114
+ if (isFormDataLike(data)) {
115
+ const value = formPartsToRecord(data._parts ?? []);
116
+ return { kind: 'form', value, text: stringifyJson(value) };
117
+ }
118
+ if (typeof data === 'object') {
119
+ if (
120
+ typeof ArrayBuffer !== 'undefined' &&
121
+ (data instanceof ArrayBuffer || ArrayBuffer.isView(data))
122
+ ) {
123
+ return describeBinary(data, contentType);
124
+ }
125
+ if ('size' in (data as object) && 'type' in (data as object)) {
126
+ return describeBinary(data, contentType);
127
+ }
128
+ return { kind: 'json', value: data, text: stringifyJson(data) };
129
+ }
130
+ return parseTextBody(String(data), contentType);
131
+ };
132
+
133
+ const readBlobAsText = (blob: unknown): Promise<string> =>
134
+ new Promise((resolve, reject) => {
135
+ const Reader = (globalThis as { FileReader?: new () => FileReaderLike })
136
+ .FileReader;
137
+ if (!Reader) {
138
+ reject(new Error('FileReader is not available'));
139
+ return;
140
+ }
141
+ const reader = new Reader();
142
+ reader.onload = () => resolve(String(reader.result ?? ''));
143
+ reader.onerror = () =>
144
+ reject(reader.error ?? new Error('Failed to read blob'));
145
+ try {
146
+ reader.readAsText(blob as Blob);
147
+ } catch (error) {
148
+ reject(error);
149
+ }
150
+ });
151
+
152
+ interface FileReaderLike {
153
+ result: unknown;
154
+ error: unknown;
155
+ onload: (() => void) | null;
156
+ onerror: (() => void) | null;
157
+ readAsText: (blob: Blob) => void;
158
+ }
159
+
160
+ const TEXTUAL_MIME =
161
+ /json|text|xml|javascript|x-www-form-urlencoded|graphql|html/i;
162
+
163
+ /**
164
+ * Normalises an XHR response. Blob responses are only decoded when they look textual.
165
+ */
166
+ export const parseResponseBody = async (
167
+ response: unknown,
168
+ responseType: string,
169
+ contentType?: string
170
+ ): Promise<ParsedBody> => {
171
+ if (response === null || response === undefined || response === '')
172
+ return EMPTY;
173
+ if (typeof response === 'string') return parseTextBody(response, contentType);
174
+ if (responseType === 'blob') {
175
+ const mime = contentType || (response as { type?: string }).type;
176
+ if (!mime || TEXTUAL_MIME.test(mime)) {
177
+ try {
178
+ const text = await readBlobAsText(response);
179
+ if (text) return parseTextBody(text, mime);
180
+ } catch {
181
+ // fall through to the binary description
182
+ }
183
+ }
184
+ return describeBinary(response, mime);
185
+ }
186
+ if (responseType === 'arraybuffer' || response instanceof ArrayBuffer) {
187
+ return describeBinary(response, contentType);
188
+ }
189
+ // responseType 'json' (or anything else that is already an object)
190
+ return { kind: 'json', value: response, text: stringifyJson(response) };
191
+ };
192
+
193
+ export const bodySize = (body: ParsedBody): number =>
194
+ body.kind === 'binary' ? body.size : byteLength(body.text);
@@ -0,0 +1,18 @@
1
+ export default function debounce<T extends (...args: never[]) => void>(
2
+ fn: T,
3
+ wait: number
4
+ ): T & { cancel: () => void } {
5
+ let timer: ReturnType<typeof setTimeout> | undefined;
6
+ const debounced = ((...args: Parameters<T>) => {
7
+ if (timer) clearTimeout(timer);
8
+ timer = setTimeout(() => {
9
+ timer = undefined;
10
+ fn(...args);
11
+ }, wait);
12
+ }) as T & { cancel: () => void };
13
+ debounced.cancel = () => {
14
+ if (timer) clearTimeout(timer);
15
+ timer = undefined;
16
+ };
17
+ return debounced;
18
+ }
@@ -0,0 +1,50 @@
1
+ export const formatBytes = (bytes: number): string => {
2
+ if (!bytes || bytes < 0) return '';
3
+ if (bytes < 1024) return `${bytes} B`;
4
+ const units = ['KB', 'MB', 'GB'];
5
+ let value = bytes / 1024;
6
+ let unit = 0;
7
+ while (value >= 1024 && unit < units.length - 1) {
8
+ value /= 1024;
9
+ unit += 1;
10
+ }
11
+ return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
12
+ };
13
+
14
+ export const formatDuration = (ms: number): string => {
15
+ if (ms < 0) return '';
16
+ if (ms < 1000) return `${Math.round(ms)} ms`;
17
+ return `${(ms / 1000).toFixed(2)} s`;
18
+ };
19
+
20
+ const pad = (num: number) => `0${num}`.slice(-2);
21
+
22
+ export const formatTime = (time: number, withMillis = false): string => {
23
+ if (!time) return '';
24
+ const date = new Date(time);
25
+ const base = `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(
26
+ date.getSeconds()
27
+ )}`;
28
+ if (!withMillis) return base;
29
+ return `${base}.${`00${date.getMilliseconds()}`.slice(-3)}`;
30
+ };
31
+
32
+ export const formatDateTime = (time: number): string => {
33
+ if (!time) return '';
34
+ return new Date(time).toISOString();
35
+ };
36
+
37
+ /** Approximate size of a string in bytes (UTF-8). */
38
+ export const byteLength = (text: string): number => {
39
+ let bytes = 0;
40
+ for (let i = 0; i < text.length; i++) {
41
+ const code = text.charCodeAt(i);
42
+ if (code < 0x80) bytes += 1;
43
+ else if (code < 0x800) bytes += 2;
44
+ else if (code >= 0xd800 && code <= 0xdbff) {
45
+ bytes += 4;
46
+ i += 1;
47
+ } else bytes += 3;
48
+ }
49
+ return bytes;
50
+ };
@@ -0,0 +1,3 @@
1
+ export const warn = (message: string): void => {
2
+ console.warn(`[react-native-network-lens] ${message}`);
3
+ };
@@ -0,0 +1,72 @@
1
+ export interface LineMatch {
2
+ line: number;
3
+ start: number;
4
+ end: number;
5
+ }
6
+
7
+ /** Finds every (case-insensitive) occurrence of `query` in `lines`. */
8
+ export const findLineMatches = (
9
+ lines: readonly string[],
10
+ query: string
11
+ ): LineMatch[] => {
12
+ const needle = query.toLowerCase();
13
+ if (!needle) return [];
14
+ const matches: LineMatch[] = [];
15
+ lines.forEach((line, index) => {
16
+ const haystack = line.toLowerCase();
17
+ let from = 0;
18
+ while (from <= haystack.length - needle.length) {
19
+ const at = haystack.indexOf(needle, from);
20
+ if (at === -1) break;
21
+ matches.push({ line: index, start: at, end: at + needle.length });
22
+ from = at + Math.max(needle.length, 1);
23
+ }
24
+ });
25
+ return matches;
26
+ };
27
+
28
+ export interface TextSegment {
29
+ text: string;
30
+ highlighted: boolean;
31
+ }
32
+
33
+ /** Splits `text` into highlighted / plain segments for the given query. */
34
+ export const splitHighlights = (text: string, query: string): TextSegment[] => {
35
+ const needle = query.toLowerCase();
36
+ if (!needle || !text) return [{ text, highlighted: false }];
37
+ const haystack = text.toLowerCase();
38
+ const segments: TextSegment[] = [];
39
+ let cursor = 0;
40
+ while (cursor < text.length) {
41
+ const at = haystack.indexOf(needle, cursor);
42
+ if (at === -1) break;
43
+ if (at > cursor)
44
+ segments.push({ text: text.slice(cursor, at), highlighted: false });
45
+ segments.push({
46
+ text: text.slice(at, at + needle.length),
47
+ highlighted: true,
48
+ });
49
+ cursor = at + needle.length;
50
+ }
51
+ if (cursor < text.length)
52
+ segments.push({ text: text.slice(cursor), highlighted: false });
53
+ return segments;
54
+ };
55
+
56
+ export const MAX_LINE_CHARS = 2000;
57
+
58
+ /** Splits text into lines, hard-wrapping absurdly long ones so layout stays sane. */
59
+ export const toLines = (text: string): string[] => {
60
+ const lines = text.split(/\r?\n/);
61
+ const result: string[] = [];
62
+ for (const line of lines) {
63
+ if (line.length <= MAX_LINE_CHARS) {
64
+ result.push(line);
65
+ continue;
66
+ }
67
+ for (let i = 0; i < line.length; i += MAX_LINE_CHARS) {
68
+ result.push(line.slice(i, i + MAX_LINE_CHARS));
69
+ }
70
+ }
71
+ return result;
72
+ };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Tiny URL helpers that do not rely on the `URL` global, because React Native's
3
+ * implementation is incomplete (no `searchParams`, throws on relative urls).
4
+ */
5
+
6
+ export interface UrlParts {
7
+ host: string;
8
+ path: string;
9
+ query: Record<string, string>;
10
+ hasQuery: boolean;
11
+ }
12
+
13
+ export const extractHost = (url: string): string => {
14
+ const match = /^[a-z][a-z0-9+.-]*:\/\/([^/?#]+)/i.exec(url);
15
+ return match?.[1] ?? '';
16
+ };
17
+
18
+ export const parseQueryString = (search: string): Record<string, string> => {
19
+ const result: Record<string, string> = {};
20
+ const trimmed = search.replace(/^\?/, '');
21
+ if (!trimmed) return result;
22
+ for (const pair of trimmed.split('&')) {
23
+ if (!pair) continue;
24
+ const eq = pair.indexOf('=');
25
+ const rawKey = eq === -1 ? pair : pair.slice(0, eq);
26
+ const rawValue = eq === -1 ? '' : pair.slice(eq + 1);
27
+ const key = safeDecode(rawKey);
28
+ const value = safeDecode(rawValue);
29
+ // Keep duplicates readable: `a=1&a=2` -> `a: "1, 2"`
30
+ result[key] = key in result ? `${result[key]}, ${value}` : value;
31
+ }
32
+ return result;
33
+ };
34
+
35
+ export const safeDecode = (value: string): string => {
36
+ try {
37
+ return decodeURIComponent(value.replace(/\+/g, ' '));
38
+ } catch {
39
+ return value;
40
+ }
41
+ };
42
+
43
+ export const parseUrl = (url: string): UrlParts => {
44
+ const host = extractHost(url);
45
+ const afterHost = host ? url.slice(url.indexOf(host) + host.length) : url;
46
+ const hashIndex = afterHost.indexOf('#');
47
+ const withoutHash =
48
+ hashIndex === -1 ? afterHost : afterHost.slice(0, hashIndex);
49
+ const qIndex = withoutHash.indexOf('?');
50
+ const path =
51
+ (qIndex === -1 ? withoutHash : withoutHash.slice(0, qIndex)) || '/';
52
+ const search = qIndex === -1 ? '' : withoutHash.slice(qIndex + 1);
53
+ return {
54
+ host,
55
+ path,
56
+ query: parseQueryString(search),
57
+ hasQuery: search.length > 0,
58
+ };
59
+ };