react-native-network-lens 0.0.0-stage → 0.1.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 (180) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +207 -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 +207 -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 +194 -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 +251 -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 +90 -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 +240 -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 +264 -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 +313 -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 +120 -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
@@ -0,0 +1,53 @@
1
+ import {
2
+ StyleSheet,
3
+ Text,
4
+ View,
5
+ type StyleProp,
6
+ type ViewStyle,
7
+ } from 'react-native';
8
+
9
+ interface Props {
10
+ label: string;
11
+ color: string;
12
+ filled?: boolean;
13
+ small?: boolean;
14
+ style?: StyleProp<ViewStyle>;
15
+ }
16
+
17
+ export const Pill = ({ label, color, filled, small, style }: Props) => (
18
+ <View
19
+ style={[
20
+ styles.pill,
21
+ small && styles.pillSmall,
22
+ { borderColor: color },
23
+ filled ? { backgroundColor: color } : styles.outline,
24
+ style,
25
+ ]}
26
+ >
27
+ <Text
28
+ numberOfLines={1}
29
+ style={[
30
+ styles.text,
31
+ small && styles.textSmall,
32
+ filled ? styles.textFilled : { color },
33
+ ]}
34
+ >
35
+ {label}
36
+ </Text>
37
+ </View>
38
+ );
39
+
40
+ const styles = StyleSheet.create({
41
+ pill: {
42
+ borderWidth: 1,
43
+ borderRadius: 6,
44
+ paddingHorizontal: 6,
45
+ paddingVertical: 2,
46
+ alignSelf: 'flex-start',
47
+ },
48
+ pillSmall: { paddingHorizontal: 4, paddingVertical: 1, borderRadius: 4 },
49
+ outline: { backgroundColor: 'transparent' },
50
+ text: { fontSize: 12, fontWeight: '700', letterSpacing: 0.3 },
51
+ textSmall: { fontSize: 11 },
52
+ textFilled: { color: '#ffffff' },
53
+ });
@@ -0,0 +1,97 @@
1
+ import {
2
+ StyleSheet,
3
+ Text,
4
+ TextInput,
5
+ View,
6
+ type StyleProp,
7
+ type ViewStyle,
8
+ } from 'react-native';
9
+ import { useTheme } from '../../theme';
10
+ import { IconButton } from './Button';
11
+
12
+ interface Props {
13
+ value: string;
14
+ onChangeText: (text: string) => void;
15
+ placeholder?: string;
16
+ /** When provided, shows "n/m" and previous/next buttons. */
17
+ matches?: {
18
+ count: number;
19
+ index: number;
20
+ onPrev: () => void;
21
+ onNext: () => void;
22
+ };
23
+ style?: StyleProp<ViewStyle>;
24
+ }
25
+
26
+ export const SearchInput = ({
27
+ value,
28
+ onChangeText,
29
+ placeholder = 'Search',
30
+ matches,
31
+ style,
32
+ }: Props) => {
33
+ const theme = useTheme();
34
+ return (
35
+ <View
36
+ style={[
37
+ styles.container,
38
+ { backgroundColor: theme.colors.surfaceAlt },
39
+ style,
40
+ ]}
41
+ >
42
+ <TextInput
43
+ value={value}
44
+ onChangeText={onChangeText}
45
+ placeholder={placeholder}
46
+ placeholderTextColor={theme.colors.textMuted}
47
+ accessibilityLabel={placeholder}
48
+ autoCapitalize="none"
49
+ autoCorrect={false}
50
+ returnKeyType="search"
51
+ onSubmitEditing={matches?.onNext}
52
+ style={[styles.input, { color: theme.colors.text }]}
53
+ />
54
+ {matches && value.length > 0 && (
55
+ <Text style={[styles.count, { color: theme.colors.textMuted }]}>
56
+ {matches.count === 0 ? '0' : `${matches.index + 1}/${matches.count}`}
57
+ </Text>
58
+ )}
59
+ {matches && (
60
+ <>
61
+ <IconButton
62
+ glyph="↑"
63
+ accessibilityLabel="Previous match"
64
+ onPress={matches.onPrev}
65
+ disabled={!matches.count}
66
+ />
67
+ <IconButton
68
+ glyph="↓"
69
+ accessibilityLabel="Next match"
70
+ onPress={matches.onNext}
71
+ disabled={!matches.count}
72
+ />
73
+ </>
74
+ )}
75
+ {value.length > 0 && (
76
+ <IconButton
77
+ glyph="✕"
78
+ accessibilityLabel="Clear search"
79
+ onPress={() => onChangeText('')}
80
+ />
81
+ )}
82
+ </View>
83
+ );
84
+ };
85
+
86
+ const styles = StyleSheet.create({
87
+ container: {
88
+ flexDirection: 'row',
89
+ alignItems: 'center',
90
+ borderRadius: 10,
91
+ paddingLeft: 10,
92
+ paddingRight: 2,
93
+ minHeight: 38,
94
+ },
95
+ input: { flex: 1, fontSize: 14, paddingVertical: 6, paddingHorizontal: 0 },
96
+ count: { fontSize: 12, marginHorizontal: 4, fontVariant: ['tabular-nums'] },
97
+ });
@@ -0,0 +1,133 @@
1
+ import type { ReactNode } from 'react';
2
+ import { StyleSheet, Text, View } from 'react-native';
3
+ import { useTheme } from '../../theme';
4
+
5
+ export const Section = ({
6
+ title,
7
+ right,
8
+ children,
9
+ }: {
10
+ title: string;
11
+ right?: ReactNode;
12
+ children: ReactNode;
13
+ }) => {
14
+ const theme = useTheme();
15
+ return (
16
+ <View style={styles.section}>
17
+ <View style={styles.header}>
18
+ <Text style={[styles.title, { color: theme.colors.textMuted }]}>
19
+ {title.toUpperCase()}
20
+ </Text>
21
+ {right}
22
+ </View>
23
+ <View style={[styles.body, { backgroundColor: theme.colors.surface }]}>
24
+ {children}
25
+ </View>
26
+ </View>
27
+ );
28
+ };
29
+
30
+ export const KeyValueList = ({
31
+ entries,
32
+ emptyLabel = 'None',
33
+ mono,
34
+ }: {
35
+ entries: [string, string][];
36
+ emptyLabel?: string;
37
+ mono?: boolean;
38
+ }) => {
39
+ const theme = useTheme();
40
+ if (entries.length === 0) {
41
+ return (
42
+ <Text style={[styles.empty, { color: theme.colors.textMuted }]}>
43
+ {emptyLabel}
44
+ </Text>
45
+ );
46
+ }
47
+ return (
48
+ <View>
49
+ {entries.map(([key, value], index) => (
50
+ <View
51
+ key={`${key}-${index}`}
52
+ style={[
53
+ styles.row,
54
+ index > 0 && {
55
+ borderTopColor: theme.colors.border,
56
+ borderTopWidth: StyleSheet.hairlineWidth,
57
+ },
58
+ ]}
59
+ >
60
+ <Text
61
+ selectable
62
+ style={[styles.key, { color: theme.colors.textMuted }]}
63
+ >
64
+ {key}
65
+ </Text>
66
+ <Text
67
+ selectable
68
+ style={[
69
+ styles.value,
70
+ { color: theme.colors.text },
71
+ mono && [styles.mono, { fontFamily: theme.fonts.mono }],
72
+ ]}
73
+ >
74
+ {value}
75
+ </Text>
76
+ </View>
77
+ ))}
78
+ </View>
79
+ );
80
+ };
81
+
82
+ export const EmptyState = ({
83
+ title,
84
+ subtitle,
85
+ }: {
86
+ title: string;
87
+ subtitle?: string;
88
+ }) => {
89
+ const theme = useTheme();
90
+ return (
91
+ <View style={styles.emptyState}>
92
+ <Text style={[styles.emptyTitle, { color: theme.colors.text }]}>
93
+ {title}
94
+ </Text>
95
+ {subtitle ? (
96
+ <Text style={[styles.emptySubtitle, { color: theme.colors.textMuted }]}>
97
+ {subtitle}
98
+ </Text>
99
+ ) : null}
100
+ </View>
101
+ );
102
+ };
103
+
104
+ const styles = StyleSheet.create({
105
+ section: { marginTop: 14, paddingHorizontal: 12 },
106
+ header: {
107
+ flexDirection: 'row',
108
+ alignItems: 'center',
109
+ justifyContent: 'space-between',
110
+ marginBottom: 6,
111
+ paddingHorizontal: 2,
112
+ },
113
+ title: { fontSize: 11, fontWeight: '700', letterSpacing: 0.8 },
114
+ body: { borderRadius: 10, paddingHorizontal: 12, paddingVertical: 4 },
115
+ row: { paddingVertical: 8 },
116
+ key: { fontSize: 12, fontWeight: '600', marginBottom: 2 },
117
+ value: { fontSize: 14 },
118
+ mono: { fontSize: 12 },
119
+ empty: { fontSize: 13, paddingVertical: 10, fontStyle: 'italic' },
120
+ emptyState: {
121
+ flex: 1,
122
+ alignItems: 'center',
123
+ justifyContent: 'center',
124
+ padding: 32,
125
+ },
126
+ emptyTitle: { fontSize: 16, fontWeight: '600', textAlign: 'center' },
127
+ emptySubtitle: {
128
+ fontSize: 13,
129
+ textAlign: 'center',
130
+ marginTop: 6,
131
+ lineHeight: 18,
132
+ },
133
+ });
@@ -0,0 +1,79 @@
1
+ import {
2
+ Pressable,
3
+ StyleSheet,
4
+ Text,
5
+ View,
6
+ type StyleProp,
7
+ type ViewStyle,
8
+ } from 'react-native';
9
+ import { useTheme } from '../../theme';
10
+
11
+ interface Props<T extends string> {
12
+ options: { value: T; label: string; badge?: boolean }[];
13
+ value: T;
14
+ onChange: (value: T) => void;
15
+ style?: StyleProp<ViewStyle>;
16
+ }
17
+
18
+ export const SegmentedControl = <T extends string>({
19
+ options,
20
+ value,
21
+ onChange,
22
+ style,
23
+ }: Props<T>) => {
24
+ const theme = useTheme();
25
+ return (
26
+ <View
27
+ style={[
28
+ styles.container,
29
+ { backgroundColor: theme.colors.surfaceAlt },
30
+ style,
31
+ ]}
32
+ >
33
+ {options.map((option) => {
34
+ const selected = option.value === value;
35
+ return (
36
+ <Pressable
37
+ key={option.value}
38
+ accessibilityRole="tab"
39
+ accessibilityState={{ selected }}
40
+ onPress={() => onChange(option.value)}
41
+ style={[
42
+ styles.segment,
43
+ selected && { backgroundColor: theme.colors.surface },
44
+ ]}
45
+ >
46
+ <Text
47
+ numberOfLines={1}
48
+ style={[
49
+ styles.label,
50
+ {
51
+ color: selected ? theme.colors.text : theme.colors.textMuted,
52
+ },
53
+ selected && styles.labelSelected,
54
+ ]}
55
+ >
56
+ {option.label}
57
+ {option.badge && (
58
+ <Text style={{ color: theme.colors.primary }}> •</Text>
59
+ )}
60
+ </Text>
61
+ </Pressable>
62
+ );
63
+ })}
64
+ </View>
65
+ );
66
+ };
67
+
68
+ const styles = StyleSheet.create({
69
+ container: { flexDirection: 'row', borderRadius: 10, padding: 3 },
70
+ segment: {
71
+ flex: 1,
72
+ paddingVertical: 7,
73
+ paddingHorizontal: 6,
74
+ borderRadius: 8,
75
+ alignItems: 'center',
76
+ },
77
+ label: { fontSize: 13, fontWeight: '500' },
78
+ labelSelected: { fontWeight: '700' },
79
+ });
@@ -0,0 +1,6 @@
1
+ export { Pill } from './Pill';
2
+ export { TextButton, IconButton } from './Button';
3
+ export { SegmentedControl } from './SegmentedControl';
4
+ export { SearchInput } from './SearchInput';
5
+ export { Section, KeyValueList, EmptyState } from './Section';
6
+ export { ActionSheet, type ActionItem } from './ActionSheet';
@@ -0,0 +1,293 @@
1
+ import NetworkRequest from './NetworkRequest';
2
+ import * as XHRInterceptor from './XHRInterceptor';
3
+ import type { NetworkLensOptions, RequestMethod } from '../types';
4
+ import debounce from '../utils/debounce';
5
+ import { extractHost } from '../utils/url';
6
+ import { byteLength } from '../utils/format';
7
+ import { warn } from '../utils/logger';
8
+
9
+ export const DEFAULT_MAX_REQUESTS = 500;
10
+
11
+ /** Best-effort byte size when the server sent no Content-Length. */
12
+ const measureResponse = (response: unknown): number => {
13
+ if (typeof response === 'string') return byteLength(response);
14
+ if (response && typeof response === 'object') {
15
+ const sized = response as { size?: unknown; byteLength?: unknown };
16
+ if (typeof sized.size === 'number') return sized.size; // Blob
17
+ if (typeof sized.byteLength === 'number') return sized.byteLength; // ArrayBuffer
18
+ try {
19
+ return byteLength(JSON.stringify(response));
20
+ } catch {
21
+ return 0;
22
+ }
23
+ }
24
+ return 0;
25
+ };
26
+ export const DEFAULT_REFRESH_RATE = 50;
27
+
28
+ type Listener = () => void;
29
+
30
+ /**
31
+ * Central store. Holds the captured requests (newest first) and publishes an
32
+ * immutable snapshot that React reads through `useSyncExternalStore`.
33
+ */
34
+ export default class Logger {
35
+ private requests: NetworkRequest[] = [];
36
+ private snapshot: NetworkRequest[] = [];
37
+ private byXhrId = new Map<number, NetworkRequest>();
38
+ private listeners = new Set<Listener>();
39
+ private nextId = 0;
40
+ private maxRequests = DEFAULT_MAX_REQUESTS;
41
+ private refreshRate = DEFAULT_REFRESH_RATE;
42
+ private ignoredHosts?: Set<string>;
43
+ private ignoredUrls?: Set<string>;
44
+ private ignoredPatterns?: RegExp[];
45
+ private paused = false;
46
+ private dirty = false;
47
+ private publishDebounced = debounce(() => this.publish(), this.refreshRate);
48
+
49
+ /** True while XMLHttpRequest is being intercepted by this logger. */
50
+ enabled = false;
51
+
52
+ // ---------------------------------------------------------------------
53
+ // Subscriptions
54
+ // ---------------------------------------------------------------------
55
+
56
+ subscribe = (listener: Listener): (() => void) => {
57
+ this.listeners.add(listener);
58
+ return () => {
59
+ this.listeners.delete(listener);
60
+ };
61
+ };
62
+
63
+ getSnapshot = (): NetworkRequest[] => this.snapshot;
64
+
65
+ isPaused = (): boolean => this.paused;
66
+
67
+ private emit() {
68
+ this.listeners.forEach((listener) => listener());
69
+ }
70
+
71
+ private publish() {
72
+ if (this.paused) return;
73
+ if (!this.dirty) return;
74
+ this.dirty = false;
75
+ this.snapshot = this.requests.slice();
76
+ this.emit();
77
+ }
78
+
79
+ private schedulePublish() {
80
+ this.dirty = true;
81
+ this.publishDebounced();
82
+ }
83
+
84
+ // ---------------------------------------------------------------------
85
+ // Interceptor callbacks
86
+ // ---------------------------------------------------------------------
87
+
88
+ private isIgnored(method: string, url: string): boolean {
89
+ if (this.ignoredHosts?.has(extractHost(url))) return true;
90
+ if (this.ignoredUrls?.has(url)) return true;
91
+ if (
92
+ this.ignoredPatterns?.some((pattern) => pattern.test(`${method} ${url}`))
93
+ )
94
+ return true;
95
+ return false;
96
+ }
97
+
98
+ private onOpen = (
99
+ method: RequestMethod,
100
+ url: string,
101
+ xhr: XHRInterceptor.InterceptedXHR
102
+ ) => {
103
+ if (this.isIgnored(method, url)) return;
104
+ const id = this.nextId++;
105
+ xhr.__lensId = id;
106
+ const request = new NetworkRequest(String(id), method, url);
107
+ this.byXhrId.set(id, request);
108
+ this.requests.unshift(request);
109
+ while (this.requests.length > this.maxRequests) {
110
+ const dropped = this.requests.pop();
111
+ if (dropped) this.byXhrId.delete(Number(dropped.id));
112
+ }
113
+ this.schedulePublish();
114
+ };
115
+
116
+ private get = (xhr: XHRInterceptor.InterceptedXHR) =>
117
+ xhr.__lensId === undefined ? undefined : this.byXhrId.get(xhr.__lensId);
118
+
119
+ private onRequestHeader = (
120
+ header: string,
121
+ value: string,
122
+ xhr: XHRInterceptor.InterceptedXHR
123
+ ) => {
124
+ const request = this.get(xhr);
125
+ if (!request) return;
126
+ request.requestHeaders[header] = value;
127
+ };
128
+
129
+ private onSend = (body: unknown, xhr: XHRInterceptor.InterceptedXHR) => {
130
+ const request = this.get(xhr);
131
+ if (!request) return;
132
+ request.update({ startTime: Date.now(), dataSent: body });
133
+ this.schedulePublish();
134
+ };
135
+
136
+ private onHeadersReceived = (
137
+ status: number,
138
+ contentType: string,
139
+ contentLength: number,
140
+ headers: Record<string, string>,
141
+ xhr: XHRInterceptor.InterceptedXHR
142
+ ) => {
143
+ const request = this.get(xhr);
144
+ if (!request) return;
145
+ request.update({
146
+ status,
147
+ responseContentType: contentType,
148
+ responseSize: contentLength,
149
+ responseHeaders: headers,
150
+ });
151
+ this.schedulePublish();
152
+ };
153
+
154
+ private onResponse = (
155
+ status: number,
156
+ timeout: number,
157
+ response: unknown,
158
+ responseURL: string,
159
+ responseType: string,
160
+ xhr: XHRInterceptor.InterceptedXHR
161
+ ) => {
162
+ const request = this.get(xhr);
163
+ if (!request) return;
164
+ const failed = status <= 0;
165
+ request.update({
166
+ endTime: Date.now(),
167
+ status,
168
+ timeout,
169
+ response,
170
+ responseURL,
171
+ responseType,
172
+ state: failed ? 'failed' : 'done',
173
+ failureReason: failed ? (request.failureReason ?? 'error') : undefined,
174
+ });
175
+ if (!failed && !request.responseSize) {
176
+ request.responseSize = measureResponse(response);
177
+ }
178
+ this.schedulePublish();
179
+ };
180
+
181
+ private onFailure = (
182
+ reason: 'error' | 'timeout' | 'abort',
183
+ xhr: XHRInterceptor.InterceptedXHR
184
+ ) => {
185
+ const request = this.get(xhr);
186
+ if (!request) return;
187
+ request.update({
188
+ state: 'failed',
189
+ failureReason: reason,
190
+ endTime: request.endTime || Date.now(),
191
+ });
192
+ this.schedulePublish();
193
+ };
194
+
195
+ // ---------------------------------------------------------------------
196
+ // Public API
197
+ // ---------------------------------------------------------------------
198
+
199
+ start = (options: NetworkLensOptions = {}): void => {
200
+ if (options.enabled === false) {
201
+ if (this.enabled) this.stop();
202
+ return;
203
+ }
204
+ if (this.enabled) return;
205
+
206
+ if (options.maxRequests !== undefined) {
207
+ if (typeof options.maxRequests !== 'number' || options.maxRequests < 1) {
208
+ warn(
209
+ '`maxRequests` must be a number greater than 0. Using the default.'
210
+ );
211
+ } else {
212
+ this.maxRequests = options.maxRequests;
213
+ }
214
+ }
215
+ if (options.refreshRate !== undefined) {
216
+ if (typeof options.refreshRate !== 'number' || options.refreshRate < 1) {
217
+ warn(
218
+ '`refreshRate` must be a number greater than 0. Using the default.'
219
+ );
220
+ } else {
221
+ this.refreshRate = options.refreshRate;
222
+ this.publishDebounced = debounce(
223
+ () => this.publish(),
224
+ this.refreshRate
225
+ );
226
+ }
227
+ }
228
+ this.ignoredHosts = options.ignoredHosts?.length
229
+ ? new Set(options.ignoredHosts)
230
+ : undefined;
231
+ this.ignoredUrls = options.ignoredUrls?.length
232
+ ? new Set(options.ignoredUrls)
233
+ : undefined;
234
+ this.ignoredPatterns = options.ignoredPatterns?.length
235
+ ? options.ignoredPatterns
236
+ : undefined;
237
+
238
+ XHRInterceptor.setCallbacks({
239
+ onOpen: this.onOpen,
240
+ onRequestHeader: this.onRequestHeader,
241
+ onSend: this.onSend,
242
+ onHeadersReceived: this.onHeadersReceived,
243
+ onResponse: this.onResponse,
244
+ onFailure: this.onFailure,
245
+ });
246
+ XHRInterceptor.enableInterception();
247
+ this.enabled = true;
248
+ this.emit();
249
+ };
250
+
251
+ stop = (): void => {
252
+ if (!this.enabled) return;
253
+ XHRInterceptor.disableInterception();
254
+ this.publishDebounced.cancel();
255
+ this.enabled = false;
256
+ this.paused = false;
257
+ this.requests = [];
258
+ this.byXhrId.clear();
259
+ this.nextId = 0;
260
+ this.maxRequests = DEFAULT_MAX_REQUESTS;
261
+ this.refreshRate = DEFAULT_REFRESH_RATE;
262
+ this.ignoredHosts = undefined;
263
+ this.ignoredUrls = undefined;
264
+ this.ignoredPatterns = undefined;
265
+ this.snapshot = [];
266
+ this.dirty = false;
267
+ this.emit();
268
+ };
269
+
270
+ getRequests = (): NetworkRequest[] => this.snapshot;
271
+
272
+ getRequest = (id: string): NetworkRequest | undefined =>
273
+ this.requests.find((request) => request.id === id);
274
+
275
+ clear = (): void => {
276
+ this.requests = [];
277
+ this.byXhrId.clear();
278
+ this.snapshot = [];
279
+ this.dirty = false;
280
+ this.emit();
281
+ };
282
+
283
+ setPaused = (paused: boolean): void => {
284
+ if (this.paused === paused) return;
285
+ this.paused = paused;
286
+ if (!paused) {
287
+ this.dirty = true;
288
+ this.publish();
289
+ } else {
290
+ this.emit();
291
+ }
292
+ };
293
+ }