@rozenite/network-activity-plugin 1.0.0-alpha.8 → 1.0.0-alpha.9

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 (87) hide show
  1. package/dist/App.html +2 -2
  2. package/dist/assets/{App-lNMijPJ4.js → App-CA1Fbh0I.js} +11995 -10804
  3. package/dist/assets/{App-R2ZMH9wJ.css → App-DoHQsY5s.css} +46 -0
  4. package/dist/event-source.cjs +22 -0
  5. package/dist/event-source.js +23 -0
  6. package/dist/rozenite.json +1 -1
  7. package/dist/src/react-native/{network-inspector.d.ts → http/network-inspector.d.ts} +1 -1
  8. package/dist/src/react-native/sse/event-source.d.ts +2 -0
  9. package/dist/src/react-native/sse/sse-inspector.d.ts +9 -0
  10. package/dist/src/react-native/sse/sse-interceptor.d.ts +36 -0
  11. package/dist/src/react-native/sse/types.d.ts +6 -0
  12. package/dist/src/react-native/utils.d.ts +6 -0
  13. package/dist/src/react-native/websocket/websocket-inspector.d.ts +9 -0
  14. package/dist/src/react-native/websocket/websocket-interceptor.d.ts +74 -0
  15. package/dist/src/shared/client.d.ts +5 -2
  16. package/dist/src/shared/sse-events.d.ts +35 -0
  17. package/dist/src/shared/websocket-events.d.ts +60 -0
  18. package/dist/src/ui/components/Badge.d.ts +1 -1
  19. package/dist/src/ui/components/Button.d.ts +1 -1
  20. package/dist/src/ui/components/JsonTreeCopyableItem.d.ts +7 -0
  21. package/dist/src/ui/components/RequestList.d.ts +6 -26
  22. package/dist/src/ui/components/SidePanel.d.ts +1 -0
  23. package/dist/src/ui/components/Toolbar.d.ts +1 -0
  24. package/dist/src/ui/hooks/useCopyToClipboard.d.ts +4 -0
  25. package/dist/src/ui/state/derived.d.ts +5 -0
  26. package/dist/src/ui/state/hooks.d.ts +17 -0
  27. package/dist/src/ui/state/model.d.ts +98 -0
  28. package/dist/src/ui/state/store.d.ts +24 -0
  29. package/dist/src/ui/tabs/CookiesTab.d.ts +3 -6
  30. package/dist/src/ui/tabs/HeadersTab.d.ts +3 -15
  31. package/dist/src/ui/tabs/MessagesTab.d.ts +5 -0
  32. package/dist/src/ui/tabs/RequestTab.d.ts +2 -7
  33. package/dist/src/ui/tabs/ResponseTab.d.ts +2 -8
  34. package/dist/src/ui/tabs/SSEMessagesTab.d.ts +5 -0
  35. package/dist/src/ui/tabs/TimingTab.d.ts +3 -5
  36. package/dist/src/ui/types.d.ts +4 -1
  37. package/dist/src/ui/utils/assert.d.ts +1 -0
  38. package/dist/src/ui/utils/copyToClipboard.d.ts +1 -0
  39. package/dist/src/ui/utils/getId.d.ts +1 -0
  40. package/dist/src/ui/utils/getStatusColor.d.ts +1 -0
  41. package/dist/useNetworkActivityDevTools.cjs +423 -34
  42. package/dist/useNetworkActivityDevTools.js +421 -34
  43. package/package.json +19 -8
  44. package/src/react-native/{network-inspector.ts → http/network-inspector.ts} +13 -34
  45. package/src/react-native/{xml-request.d.ts → http/xml-request.d.ts} +1 -0
  46. package/src/react-native/sse/event-source.ts +25 -0
  47. package/src/react-native/sse/sse-inspector.ts +117 -0
  48. package/src/react-native/sse/sse-interceptor.ts +162 -0
  49. package/src/react-native/sse/types.ts +9 -0
  50. package/src/react-native/useNetworkActivityDevTools.ts +75 -1
  51. package/src/react-native/utils.ts +43 -0
  52. package/src/react-native/websocket/websocket-inspector.ts +180 -0
  53. package/src/react-native/websocket/websocket-interceptor.d.ts +4 -0
  54. package/src/react-native/websocket/websocket-interceptor.ts +166 -0
  55. package/src/shared/client.ts +6 -2
  56. package/src/shared/sse-events.ts +44 -0
  57. package/src/shared/websocket-events.ts +79 -0
  58. package/src/ui/components/JsonTree.tsx +13 -0
  59. package/src/ui/components/JsonTreeCopyableItem.tsx +33 -0
  60. package/src/ui/components/RequestList.tsx +42 -124
  61. package/src/ui/components/SidePanel.tsx +323 -0
  62. package/src/ui/components/Tabs.tsx +1 -1
  63. package/src/ui/components/Toolbar.tsx +45 -0
  64. package/src/ui/hooks/useCopyToClipboard.ts +28 -0
  65. package/src/ui/state/derived.ts +112 -0
  66. package/src/ui/state/hooks.ts +44 -0
  67. package/src/ui/state/model.ts +129 -0
  68. package/src/ui/state/store.ts +559 -0
  69. package/src/ui/tabs/CookiesTab.tsx +162 -176
  70. package/src/ui/tabs/HeadersTab.tsx +23 -30
  71. package/src/ui/tabs/MessagesTab.tsx +276 -0
  72. package/src/ui/tabs/RequestTab.tsx +8 -13
  73. package/src/ui/tabs/ResponseTab.tsx +6 -10
  74. package/src/ui/tabs/SSEMessagesTab.tsx +213 -0
  75. package/src/ui/tabs/TimingTab.tsx +30 -43
  76. package/src/ui/types.ts +4 -1
  77. package/src/ui/utils/assert.ts +5 -0
  78. package/src/ui/utils/copyToClipboard.ts +3 -0
  79. package/src/ui/utils/getId.ts +10 -0
  80. package/src/ui/utils/getStatusColor.ts +15 -0
  81. package/src/ui/views/InspectorView.tsx +24 -320
  82. package/tailwind.config.ts +3 -0
  83. package/vite.config.ts +12 -0
  84. /package/dist/src/react-native/{network-requests-registry.d.ts → http/network-requests-registry.d.ts} +0 -0
  85. /package/dist/src/react-native/{xhr-interceptor.d.ts → http/xhr-interceptor.d.ts} +0 -0
  86. /package/src/react-native/{network-requests-registry.ts → http/network-requests-registry.ts} +0 -0
  87. /package/src/react-native/{xhr-interceptor.ts → http/xhr-interceptor.ts} +0 -0
@@ -1,34 +1,10 @@
1
- import { NetworkActivityDevToolsClient } from '../shared/client';
2
- import { getHttpHeaderValue } from '../ui/utils/getHttpHeaderValue';
1
+ import { HttpMethod, NetworkActivityDevToolsClient } from '../../shared/client';
2
+ import { getContentType } from '../utils';
3
3
  import { getNetworkRequestsRegistry } from './network-requests-registry';
4
4
  import { XHRInterceptor } from './xhr-interceptor';
5
5
 
6
6
  const networkRequestsRegistry = getNetworkRequestsRegistry();
7
7
 
8
- const getContentType = (request: XMLHttpRequest): string => {
9
- const responseHeaders = request.responseHeaders;
10
- const responseType = request.responseType;
11
-
12
- const contentType = getHttpHeaderValue(responseHeaders || {}, 'content-type');
13
-
14
- if (contentType) {
15
- return contentType.split(';')[0].trim();
16
- }
17
-
18
- switch (responseType) {
19
- case 'arraybuffer':
20
- case 'blob':
21
- return 'application/octet-stream';
22
- case 'text':
23
- case '':
24
- return 'text/plain';
25
- case 'json':
26
- return 'application/json';
27
- case 'document':
28
- return 'text/html';
29
- }
30
- };
31
-
32
8
  const getResponseSize = (request: XMLHttpRequest): number => {
33
9
  if (typeof request.response === 'object') {
34
10
  return request.response.size;
@@ -42,7 +18,8 @@ const getResponseBody = async (
42
18
  ): Promise<string | null> => {
43
19
  const responseType = request.responseType;
44
20
 
45
- if (responseType === 'text') {
21
+ // Response type is empty in certain cases, like when using axios.
22
+ if (responseType === '' || responseType === 'text') {
46
23
  return request.responseText as string;
47
24
  }
48
25
 
@@ -117,6 +94,8 @@ export const getNetworkInspector = (
117
94
  const sendTime = Date.now();
118
95
 
119
96
  const requestId = generateRequestId();
97
+ request._rozeniteRequestId = requestId;
98
+
120
99
  const initiator = getInitiatorFromStack();
121
100
 
122
101
  networkRequestsRegistry.addEntry(requestId, request);
@@ -125,10 +104,10 @@ export const getNetworkInspector = (
125
104
 
126
105
  pluginClient.send('request-sent', {
127
106
  requestId: requestId,
128
- timestamp: sendTime / 1000,
107
+ timestamp: sendTime,
129
108
  request: {
130
109
  url: request._url as string,
131
- method: request._method as string,
110
+ method: request._method as HttpMethod,
132
111
  headers: request._headers,
133
112
  postData: data,
134
113
  },
@@ -145,7 +124,7 @@ export const getNetworkInspector = (
145
124
  request.addEventListener('load', () => {
146
125
  pluginClient.send('response-received', {
147
126
  requestId: requestId,
148
- timestamp: Date.now() / 1000,
127
+ timestamp: Date.now(),
149
128
  type: 'XHR',
150
129
  response: {
151
130
  url: request._url as string,
@@ -154,7 +133,7 @@ export const getNetworkInspector = (
154
133
  headers: request.responseHeaders || {},
155
134
  contentType: getContentType(request),
156
135
  size: getResponseSize(request),
157
- responseTime: Date.now() / 1000,
136
+ responseTime: Date.now(),
158
137
  },
159
138
  });
160
139
  });
@@ -162,7 +141,7 @@ export const getNetworkInspector = (
162
141
  request.addEventListener('loadend', () => {
163
142
  pluginClient.send('request-completed', {
164
143
  requestId: requestId,
165
- timestamp: Date.now() / 1000,
144
+ timestamp: Date.now(),
166
145
  duration: Date.now() - sendTime,
167
146
  size: getResponseSize(request),
168
147
  ttfb,
@@ -172,7 +151,7 @@ export const getNetworkInspector = (
172
151
  request.addEventListener('error', () => {
173
152
  pluginClient.send('request-failed', {
174
153
  requestId: requestId,
175
- timestamp: Date.now() / 1000,
154
+ timestamp: Date.now(),
176
155
  type: 'XHR',
177
156
  error: 'Failed',
178
157
  canceled: false,
@@ -182,7 +161,7 @@ export const getNetworkInspector = (
182
161
  request.addEventListener('abort', () => {
183
162
  pluginClient.send('request-failed', {
184
163
  requestId: requestId,
185
- timestamp: Date.now() / 1000,
164
+ timestamp: Date.now(),
186
165
  type: 'XHR',
187
166
  error: 'Aborted',
188
167
  canceled: true,
@@ -9,6 +9,7 @@ declare type BlobData = {
9
9
 
10
10
  declare global {
11
11
  interface XMLHttpRequest {
12
+ _rozeniteRequestId: string;
12
13
  _requestId?: number;
13
14
  _subscriptions: Array<EventSubscription>;
14
15
  _aborted: boolean;
@@ -0,0 +1,25 @@
1
+ import type EventSource from 'react-native-sse';
2
+
3
+ const NOOP = () => {
4
+ // noop
5
+ };
6
+
7
+ const MOCK_EVENT_SOURCE = class {
8
+ open = NOOP;
9
+ close = NOOP;
10
+ addEventListener = NOOP;
11
+ removeEventListener = NOOP;
12
+ dispatch = NOOP;
13
+ removeAllEventListeners = NOOP;
14
+ };
15
+
16
+ export const getEventSource = (): typeof EventSource => {
17
+ try {
18
+ const { default: EventSource } = require('react-native-sse');
19
+ return EventSource;
20
+ } catch {
21
+ // This is a workaround for the fact that Vite doesn't support require() calls for in-project dependencies.
22
+ // We are going to return a mock object, so the code will work fine, but it will not be able to intercept SSE requests.
23
+ return MOCK_EVENT_SOURCE;
24
+ }
25
+ };
@@ -0,0 +1,117 @@
1
+ import { createNanoEvents } from 'nanoevents';
2
+ import { SSEInterceptor } from './sse-interceptor';
3
+ import { EventSourceWithInternals } from './types';
4
+ import { SSEEvent, SSEEventMap } from '../../shared/sse-events';
5
+ import { getContentType } from '../utils';
6
+
7
+ type NanoEventsMap = {
8
+ [K in keyof SSEEventMap]: (data: SSEEventMap[K]) => void;
9
+ };
10
+
11
+ export type SSEInspector = {
12
+ enable: () => void;
13
+ disable: () => void;
14
+ isEnabled: () => boolean;
15
+ dispose: () => void;
16
+ on: <TEventType extends keyof SSEEventMap>(
17
+ event: TEventType,
18
+ callback: (data: SSEEventMap[TEventType]) => void
19
+ ) => () => void;
20
+ };
21
+
22
+ export const getSSEInspector = (): SSEInspector => {
23
+ const eventEmitter = createNanoEvents<NanoEventsMap>();
24
+
25
+ const getRequestId = (eventSource: EventSourceWithInternals): string => {
26
+ const requestId = eventSource._xhr?._rozeniteRequestId;
27
+
28
+ if (!requestId) {
29
+ throw new Error(
30
+ 'No request ID found for EventSource. This should never happen!'
31
+ );
32
+ }
33
+
34
+ return requestId;
35
+ };
36
+
37
+ return {
38
+ enable: () => {
39
+ SSEInterceptor.setOpenEventCallback((_, eventSource) => {
40
+ const sseEventSource = eventSource as EventSourceWithInternals;
41
+ const requestId = getRequestId(sseEventSource);
42
+ const sseXhr = sseEventSource._xhr as XMLHttpRequest;
43
+
44
+ const event: SSEEvent = {
45
+ type: 'sse-open',
46
+ requestId,
47
+ timestamp: Date.now(),
48
+ response: {
49
+ url: sseXhr._url as string,
50
+ status: sseXhr.status,
51
+ statusText: sseXhr.statusText,
52
+ headers: sseXhr.responseHeaders || {},
53
+ contentType: getContentType(sseXhr),
54
+ size: 0,
55
+ responseTime: Date.now(),
56
+ },
57
+ };
58
+ eventEmitter.emit('sse-open', event);
59
+ });
60
+
61
+ SSEInterceptor.setMessageCallback((messageEvent, eventSource) => {
62
+ const sseEventSource = eventSource as EventSourceWithInternals;
63
+ const requestId = getRequestId(sseEventSource);
64
+
65
+ const event: SSEEvent = {
66
+ type: 'sse-message',
67
+ requestId,
68
+ timestamp: Date.now(),
69
+ data: messageEvent.data || '',
70
+ };
71
+ eventEmitter.emit('sse-message', event);
72
+ });
73
+
74
+ SSEInterceptor.setErrorCallback((errorEvent, eventSource) => {
75
+ const sseEventSource = eventSource as EventSourceWithInternals;
76
+ const requestId = getRequestId(sseEventSource);
77
+
78
+ const event: SSEEvent = {
79
+ type: 'sse-error',
80
+ requestId,
81
+ timestamp: Date.now(),
82
+ error: {
83
+ type: errorEvent.type,
84
+ message:
85
+ errorEvent.type === 'timeout' ? 'Timeout' : errorEvent.message,
86
+ },
87
+ };
88
+ eventEmitter.emit('sse-error', event);
89
+ });
90
+
91
+ SSEInterceptor.setCloseCallback((_, eventSource) => {
92
+ const sseEventSource = eventSource as EventSourceWithInternals;
93
+ const requestId = getRequestId(sseEventSource);
94
+
95
+ const event: SSEEvent = {
96
+ type: 'sse-close',
97
+ requestId,
98
+ timestamp: Date.now(),
99
+ };
100
+ eventEmitter.emit('sse-close', event);
101
+ });
102
+
103
+ SSEInterceptor.enableInterception();
104
+ },
105
+ disable: () => {
106
+ SSEInterceptor.disableInterception();
107
+ },
108
+ isEnabled: () => SSEInterceptor.isInterceptorEnabled(),
109
+ dispose: () => {
110
+ eventEmitter.events = {};
111
+ },
112
+ on: <TEventType extends keyof SSEEventMap>(
113
+ event: TEventType,
114
+ callback: (data: SSEEventMap[TEventType]) => void
115
+ ) => eventEmitter.on(event, callback as NanoEventsMap[TEventType]),
116
+ };
117
+ };
@@ -0,0 +1,162 @@
1
+ import type EventSource from 'react-native-sse';
2
+ import type {
3
+ MessageEvent,
4
+ ErrorEvent,
5
+ OpenEvent,
6
+ CloseEvent,
7
+ TimeoutEvent,
8
+ ExceptionEvent,
9
+ } from 'react-native-sse';
10
+ import { EventSourceWithInternals } from './types';
11
+ import { getEventSource } from './event-source';
12
+
13
+ export type SSEInterceptorConnectCallback = (
14
+ url: string,
15
+ request: EventSource
16
+ ) => void;
17
+
18
+ export type SSEInterceptorMessageCallback = (
19
+ event: MessageEvent,
20
+ request: EventSource
21
+ ) => void;
22
+
23
+ export type SSEInterceptorErrorCallback = (
24
+ error: ErrorEvent | TimeoutEvent | ExceptionEvent,
25
+ request: EventSource
26
+ ) => void;
27
+
28
+ export type SSEInterceptorOpenEventCallback = (
29
+ event: OpenEvent,
30
+ request: EventSource
31
+ ) => void;
32
+
33
+ export type SSEInterceptorCloseCallback = (
34
+ event: CloseEvent,
35
+ request: EventSource
36
+ ) => void;
37
+
38
+ let connectCallback: SSEInterceptorConnectCallback | null;
39
+ let messageCallback: SSEInterceptorMessageCallback | null;
40
+ let errorCallback: SSEInterceptorErrorCallback | null;
41
+ let openEventCallback: SSEInterceptorOpenEventCallback | null;
42
+ let closeCallback: SSEInterceptorCloseCallback | null;
43
+
44
+ let isInterceptorEnabled = false;
45
+
46
+ const eventSourceClass = getEventSource();
47
+
48
+ // Store original EventSource open method
49
+ const originalOpen = eventSourceClass.prototype.open;
50
+
51
+ /**
52
+ * A network interceptor which monkey-patches EventSource open method
53
+ * to gather all SSE connections and events, in order to show their
54
+ * information in the Network Activity panel.
55
+ */
56
+ export const SSEInterceptor = {
57
+ /**
58
+ * Invoked when EventSource.open() is called (connection attempt starting).
59
+ */
60
+ setConnectCallback(callback: SSEInterceptorConnectCallback) {
61
+ connectCallback = callback;
62
+ },
63
+
64
+ /**
65
+ * Invoked when a message event is received.
66
+ */
67
+ setMessageCallback(callback: SSEInterceptorMessageCallback) {
68
+ messageCallback = callback;
69
+ },
70
+
71
+ /**
72
+ * Invoked when an error event occurs.
73
+ */
74
+ setErrorCallback(callback: SSEInterceptorErrorCallback) {
75
+ errorCallback = callback;
76
+ },
77
+
78
+ /**
79
+ * Invoked when the connection is successfully opened (open event fired).
80
+ */
81
+ setOpenEventCallback(callback: SSEInterceptorOpenEventCallback) {
82
+ openEventCallback = callback;
83
+ },
84
+
85
+ /**
86
+ * Invoked when the connection is closed.
87
+ */
88
+ setCloseCallback(callback: SSEInterceptorCloseCallback) {
89
+ closeCallback = callback;
90
+ },
91
+
92
+ isInterceptorEnabled(): boolean {
93
+ return isInterceptorEnabled;
94
+ },
95
+
96
+ enableInterception() {
97
+ if (isInterceptorEnabled) {
98
+ return;
99
+ }
100
+
101
+ // Override EventSource open method to intercept SSE connections
102
+ eventSourceClass.prototype.open = function (
103
+ this: EventSourceWithInternals
104
+ ) {
105
+ // Invoke connect callback
106
+ if (connectCallback) {
107
+ connectCallback(this.url, this);
108
+ }
109
+
110
+ // Add event listeners to intercept all events
111
+ this.addEventListener('open', (event: OpenEvent) => {
112
+ if (openEventCallback) {
113
+ openEventCallback(event, this);
114
+ }
115
+ });
116
+
117
+ this.addEventListener('message', (event: MessageEvent) => {
118
+ if (messageCallback) {
119
+ messageCallback(event, this);
120
+ }
121
+ });
122
+
123
+ this.addEventListener(
124
+ 'error',
125
+ (event: ErrorEvent | TimeoutEvent | ExceptionEvent) => {
126
+ if (errorCallback) {
127
+ errorCallback(event, this);
128
+ }
129
+ }
130
+ );
131
+
132
+ this.addEventListener('close', (event: CloseEvent) => {
133
+ if (closeCallback) {
134
+ closeCallback(event, this);
135
+ }
136
+ });
137
+
138
+ // Call original open method
139
+ return originalOpen.call(this);
140
+ };
141
+
142
+ isInterceptorEnabled = true;
143
+ },
144
+
145
+ // Unpatch EventSource open method and remove the callbacks.
146
+ disableInterception() {
147
+ if (!isInterceptorEnabled) {
148
+ return;
149
+ }
150
+ isInterceptorEnabled = false;
151
+
152
+ // Restore original open method
153
+ eventSourceClass.prototype.open = originalOpen;
154
+
155
+ // Clear callbacks
156
+ connectCallback = null;
157
+ messageCallback = null;
158
+ errorCallback = null;
159
+ openEventCallback = null;
160
+ closeCallback = null;
161
+ },
162
+ };
@@ -0,0 +1,9 @@
1
+ import type EventSource from 'react-native-sse';
2
+
3
+ export interface EventSourceWithInternals<E extends string = never>
4
+ extends EventSource<E> {
5
+ url: string;
6
+
7
+ /** Used internally to mark the underlying XHR to skip it in XHR interceptor. */
8
+ _xhr?: XMLHttpRequest;
9
+ }
@@ -1,7 +1,12 @@
1
1
  import { useEffect } from 'react';
2
2
  import { useRozeniteDevToolsClient } from '@rozenite/plugin-bridge';
3
- import { getNetworkInspector } from './network-inspector';
3
+ import { getNetworkInspector } from './http/network-inspector';
4
4
  import { NetworkActivityEventMap } from '../shared/client';
5
+ import { getWebSocketInspector } from './websocket/websocket-inspector';
6
+ import { WebSocketEventMap } from '../shared/websocket-events';
7
+ import { UnionToTuple } from './utils';
8
+ import { getSSEInspector } from './sse/sse-inspector';
9
+ import { SSEEventMap } from '../shared/sse-events';
5
10
 
6
11
  export const useNetworkActivityDevTools = () => {
7
12
  const client = useRozeniteDevToolsClient<NetworkActivityEventMap>({
@@ -20,5 +25,74 @@ export const useNetworkActivityDevTools = () => {
20
25
  };
21
26
  }, [client]);
22
27
 
28
+ useEffect(() => {
29
+ if (!client) {
30
+ return;
31
+ }
32
+
33
+ const eventsToForward: UnionToTuple<keyof WebSocketEventMap> = [
34
+ 'websocket-connect',
35
+ 'websocket-open',
36
+ 'websocket-close',
37
+ 'websocket-message-sent',
38
+ 'websocket-message-received',
39
+ 'websocket-error',
40
+ 'websocket-connection-status-changed',
41
+ ];
42
+ const websocketInspector = getWebSocketInspector();
43
+
44
+ eventsToForward.forEach((event) => {
45
+ websocketInspector.on(event, (event) => {
46
+ client.send(event.type, event);
47
+ });
48
+ });
49
+
50
+ client.onMessage('network-enable', () => {
51
+ websocketInspector.enable();
52
+ });
53
+
54
+ client.onMessage('network-disable', () => {
55
+ websocketInspector.disable();
56
+ });
57
+
58
+ return () => {
59
+ // Subscriptions will be disposed by the inspector
60
+ websocketInspector.dispose();
61
+ };
62
+ }, [client]);
63
+
64
+ useEffect(() => {
65
+ if (!client) {
66
+ return;
67
+ }
68
+
69
+ const eventsToForward: UnionToTuple<keyof SSEEventMap> = [
70
+ 'sse-open',
71
+ 'sse-message',
72
+ 'sse-error',
73
+ 'sse-close',
74
+ ];
75
+ const sseInspector = getSSEInspector();
76
+
77
+ eventsToForward.forEach((event) => {
78
+ sseInspector.on(event, (event) => {
79
+ client.send(event.type, event);
80
+ });
81
+ });
82
+
83
+ client.onMessage('network-enable', () => {
84
+ sseInspector.enable();
85
+ });
86
+
87
+ client.onMessage('network-disable', () => {
88
+ sseInspector.disable();
89
+ });
90
+
91
+ return () => {
92
+ // Subscriptions will be disposed by the inspector
93
+ sseInspector.dispose();
94
+ };
95
+ }, [client]);
96
+
23
97
  return client;
24
98
  };
@@ -0,0 +1,43 @@
1
+ import { getHttpHeaderValue } from '../ui/utils/getHttpHeaderValue';
2
+
3
+ type UnionToIntersection<U> = (
4
+ U extends unknown ? (k: U) => void : never
5
+ ) extends (k: infer I) => void
6
+ ? I
7
+ : never;
8
+
9
+ type LastOf<T> = UnionToIntersection<
10
+ T extends unknown ? () => T : never
11
+ > extends () => infer R
12
+ ? R
13
+ : never;
14
+
15
+ type Push<T extends unknown[], V> = [...T, V];
16
+
17
+ export type UnionToTuple<T, L = LastOf<T>> = [T] extends [never]
18
+ ? []
19
+ : Push<UnionToTuple<Exclude<T, L>>, L>;
20
+
21
+ export const getContentType = (request: XMLHttpRequest): string => {
22
+ const responseHeaders = request.responseHeaders;
23
+ const responseType = request.responseType;
24
+
25
+ const contentType = getHttpHeaderValue(responseHeaders || {}, 'content-type');
26
+
27
+ if (contentType) {
28
+ return contentType.split(';')[0].trim();
29
+ }
30
+
31
+ switch (responseType) {
32
+ case 'arraybuffer':
33
+ case 'blob':
34
+ return 'application/octet-stream';
35
+ case 'text':
36
+ case '':
37
+ return 'text/plain';
38
+ case 'json':
39
+ return 'application/json';
40
+ case 'document':
41
+ return 'text/html';
42
+ }
43
+ };