@rozenite/network-activity-plugin 1.0.0-alpha.7 → 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 (96) hide show
  1. package/dist/App.html +2 -2
  2. package/dist/assets/{App-CIflVb88.js → App-CA1Fbh0I.js} +12009 -10809
  3. package/dist/assets/{App-Czu6Vt2P.css → App-DoHQsY5s.css} +43 -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 +8 -4
  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 +6 -3
  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/getHttpHeaderValue.d.ts +2 -0
  40. package/dist/src/ui/utils/getId.d.ts +1 -0
  41. package/dist/src/ui/utils/getStatusColor.d.ts +1 -0
  42. package/dist/useNetworkActivityDevTools.cjs +433 -34
  43. package/dist/useNetworkActivityDevTools.js +431 -34
  44. package/package.json +19 -8
  45. package/src/react-native/{network-inspector.ts → http/network-inspector.ts} +14 -32
  46. package/src/react-native/{xml-request.d.ts → http/xml-request.d.ts} +1 -0
  47. package/src/react-native/sse/event-source.ts +25 -0
  48. package/src/react-native/sse/sse-inspector.ts +117 -0
  49. package/src/react-native/sse/sse-interceptor.ts +162 -0
  50. package/src/react-native/sse/types.ts +9 -0
  51. package/src/react-native/useNetworkActivityDevTools.ts +75 -1
  52. package/src/react-native/utils.ts +43 -0
  53. package/src/react-native/websocket/websocket-inspector.ts +180 -0
  54. package/src/react-native/websocket/websocket-interceptor.d.ts +4 -0
  55. package/src/react-native/websocket/websocket-interceptor.ts +166 -0
  56. package/src/shared/client.ts +10 -4
  57. package/src/shared/sse-events.ts +44 -0
  58. package/src/shared/websocket-events.ts +79 -0
  59. package/src/ui/components/Badge.tsx +1 -1
  60. package/src/ui/components/Button.tsx +1 -1
  61. package/src/ui/components/Input.tsx +1 -1
  62. package/src/ui/components/JsonTree.tsx +13 -0
  63. package/src/ui/components/JsonTreeCopyableItem.tsx +33 -0
  64. package/src/ui/components/RequestList.tsx +42 -123
  65. package/src/ui/components/ScrollArea.tsx +1 -1
  66. package/src/ui/components/Separator.tsx +1 -1
  67. package/src/ui/components/SidePanel.tsx +323 -0
  68. package/src/ui/components/Tabs.tsx +2 -2
  69. package/src/ui/components/Toolbar.tsx +45 -0
  70. package/src/ui/hooks/useCopyToClipboard.ts +28 -0
  71. package/src/ui/state/derived.ts +112 -0
  72. package/src/ui/state/hooks.ts +44 -0
  73. package/src/ui/state/model.ts +129 -0
  74. package/src/ui/state/store.ts +559 -0
  75. package/src/ui/tabs/CookiesTab.tsx +168 -179
  76. package/src/ui/tabs/HeadersTab.tsx +24 -31
  77. package/src/ui/tabs/MessagesTab.tsx +276 -0
  78. package/src/ui/tabs/RequestTab.tsx +28 -31
  79. package/src/ui/tabs/ResponseTab.tsx +10 -12
  80. package/src/ui/tabs/SSEMessagesTab.tsx +213 -0
  81. package/src/ui/tabs/TimingTab.tsx +33 -44
  82. package/src/ui/types.ts +6 -2
  83. package/src/ui/utils/assert.ts +5 -0
  84. package/src/ui/utils/copyToClipboard.ts +3 -0
  85. package/src/ui/utils/getHttpHeaderValue.ts +14 -0
  86. package/src/ui/utils/getId.ts +10 -0
  87. package/src/ui/utils/getStatusColor.ts +15 -0
  88. package/src/ui/views/InspectorView.tsx +24 -320
  89. package/tailwind.config.ts +3 -0
  90. package/vite.config.ts +12 -0
  91. /package/dist/src/react-native/{network-requests-registry.d.ts → http/network-requests-registry.d.ts} +0 -0
  92. /package/dist/src/react-native/{xhr-interceptor.d.ts → http/xhr-interceptor.d.ts} +0 -0
  93. /package/dist/src/ui/{utils.d.ts → utils/cn.d.ts} +0 -0
  94. /package/src/react-native/{network-requests-registry.ts → http/network-requests-registry.ts} +0 -0
  95. /package/src/react-native/{xhr-interceptor.ts → http/xhr-interceptor.ts} +0 -0
  96. /package/src/ui/{utils.ts → utils/cn.ts} +0 -0
@@ -1,31 +1,10 @@
1
- import { NetworkActivityDevToolsClient } from '../shared/client';
1
+ import { HttpMethod, NetworkActivityDevToolsClient } from '../../shared/client';
2
+ import { getContentType } from '../utils';
2
3
  import { getNetworkRequestsRegistry } from './network-requests-registry';
3
4
  import { XHRInterceptor } from './xhr-interceptor';
4
5
 
5
6
  const networkRequestsRegistry = getNetworkRequestsRegistry();
6
7
 
7
- const getContentType = (request: XMLHttpRequest): string => {
8
- const responseHeaders = request.responseHeaders;
9
- const responseType = request.responseType;
10
-
11
- if (responseHeaders?.['content-type']) {
12
- return responseHeaders['content-type'].split(';')[0].trim();
13
- }
14
-
15
- switch (responseType) {
16
- case 'arraybuffer':
17
- case 'blob':
18
- return 'application/octet-stream';
19
- case 'text':
20
- case '':
21
- return 'text/plain';
22
- case 'json':
23
- return 'application/json';
24
- case 'document':
25
- return 'text/html';
26
- }
27
- };
28
-
29
8
  const getResponseSize = (request: XMLHttpRequest): number => {
30
9
  if (typeof request.response === 'object') {
31
10
  return request.response.size;
@@ -39,7 +18,8 @@ const getResponseBody = async (
39
18
  ): Promise<string | null> => {
40
19
  const responseType = request.responseType;
41
20
 
42
- if (responseType === 'text') {
21
+ // Response type is empty in certain cases, like when using axios.
22
+ if (responseType === '' || responseType === 'text') {
43
23
  return request.responseText as string;
44
24
  }
45
25
 
@@ -114,6 +94,8 @@ export const getNetworkInspector = (
114
94
  const sendTime = Date.now();
115
95
 
116
96
  const requestId = generateRequestId();
97
+ request._rozeniteRequestId = requestId;
98
+
117
99
  const initiator = getInitiatorFromStack();
118
100
 
119
101
  networkRequestsRegistry.addEntry(requestId, request);
@@ -122,10 +104,10 @@ export const getNetworkInspector = (
122
104
 
123
105
  pluginClient.send('request-sent', {
124
106
  requestId: requestId,
125
- timestamp: sendTime / 1000,
107
+ timestamp: sendTime,
126
108
  request: {
127
109
  url: request._url as string,
128
- method: request._method as string,
110
+ method: request._method as HttpMethod,
129
111
  headers: request._headers,
130
112
  postData: data,
131
113
  },
@@ -142,16 +124,16 @@ export const getNetworkInspector = (
142
124
  request.addEventListener('load', () => {
143
125
  pluginClient.send('response-received', {
144
126
  requestId: requestId,
145
- timestamp: Date.now() / 1000,
127
+ timestamp: Date.now(),
146
128
  type: 'XHR',
147
129
  response: {
148
130
  url: request._url as string,
149
131
  status: request.status,
150
132
  statusText: request.statusText,
151
- headers: request.responseHeaders as Record<string, string>,
133
+ headers: request.responseHeaders || {},
152
134
  contentType: getContentType(request),
153
135
  size: getResponseSize(request),
154
- responseTime: Date.now() / 1000,
136
+ responseTime: Date.now(),
155
137
  },
156
138
  });
157
139
  });
@@ -159,7 +141,7 @@ export const getNetworkInspector = (
159
141
  request.addEventListener('loadend', () => {
160
142
  pluginClient.send('request-completed', {
161
143
  requestId: requestId,
162
- timestamp: Date.now() / 1000,
144
+ timestamp: Date.now(),
163
145
  duration: Date.now() - sendTime,
164
146
  size: getResponseSize(request),
165
147
  ttfb,
@@ -169,7 +151,7 @@ export const getNetworkInspector = (
169
151
  request.addEventListener('error', () => {
170
152
  pluginClient.send('request-failed', {
171
153
  requestId: requestId,
172
- timestamp: Date.now() / 1000,
154
+ timestamp: Date.now(),
173
155
  type: 'XHR',
174
156
  error: 'Failed',
175
157
  canceled: false,
@@ -179,7 +161,7 @@ export const getNetworkInspector = (
179
161
  request.addEventListener('abort', () => {
180
162
  pluginClient.send('request-failed', {
181
163
  requestId: requestId,
182
- timestamp: Date.now() / 1000,
164
+ timestamp: Date.now(),
183
165
  type: 'XHR',
184
166
  error: 'Aborted',
185
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
+ };