@rozenite/network-activity-plugin 1.0.0-alpha.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.
- package/LICENSE +20 -0
- package/README.md +73 -0
- package/dist/assets/panel-C5YgUUj5.js +54 -0
- package/dist/assets/panel-NCVczPb1.css +1 -0
- package/dist/panel.html +31 -0
- package/dist/react-native.cjs +1 -0
- package/dist/react-native.d.ts +5 -0
- package/dist/react-native.js +174 -0
- package/dist/rozenite.json +1 -0
- package/package.json +31 -0
- package/react-native.ts +7 -0
- package/rozenite.config.ts +8 -0
- package/src/css-modules.d.ts +4 -0
- package/src/react-native/useNetworkActivityDevTools.ts +235 -0
- package/src/types/network.ts +153 -0
- package/src/ui/components.module.css +158 -0
- package/src/ui/components.tsx +219 -0
- package/src/ui/network-details.module.css +57 -0
- package/src/ui/network-details.tsx +134 -0
- package/src/ui/network-list.module.css +122 -0
- package/src/ui/network-list.tsx +145 -0
- package/src/ui/network-toolbar.module.css +9 -0
- package/src/ui/network-toolbar.tsx +40 -0
- package/src/ui/panel.module.css +61 -0
- package/src/ui/panel.tsx +201 -0
- package/src/ui/tanstack-query.tsx +197 -0
- package/src/ui/utils.ts +89 -0
- package/tsconfig.json +25 -0
- package/vite.config.ts +20 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
3
|
+
import { NetworkEntry } from '../types/network';
|
|
4
|
+
import { getStatusColor, getMethodColor, formatDuration, formatFileSize, parseUrl } from './utils';
|
|
5
|
+
import { Badge, Tooltip } from './components';
|
|
6
|
+
import styles from './network-list.module.css';
|
|
7
|
+
|
|
8
|
+
interface NetworkListProps {
|
|
9
|
+
entries: NetworkEntry[];
|
|
10
|
+
selectedRequestId: string | null;
|
|
11
|
+
onSelect: (requestId: string) => void;
|
|
12
|
+
height: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ITEM_HEIGHT = 60; // Height of each network list item
|
|
16
|
+
|
|
17
|
+
export const NetworkList: React.FC<NetworkListProps> = ({
|
|
18
|
+
entries,
|
|
19
|
+
selectedRequestId,
|
|
20
|
+
onSelect,
|
|
21
|
+
height,
|
|
22
|
+
}) => {
|
|
23
|
+
const parentRef = React.useRef<HTMLDivElement>(null);
|
|
24
|
+
|
|
25
|
+
const virtualizer = useVirtualizer({
|
|
26
|
+
count: entries.length,
|
|
27
|
+
getScrollElement: () => parentRef.current,
|
|
28
|
+
estimateSize: () => ITEM_HEIGHT,
|
|
29
|
+
overscan: 5,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const NetworkListItem: React.FC<{ entry: NetworkEntry; index: number }> = ({ entry, index }) => {
|
|
33
|
+
const status = entry.response?.response.status || 0;
|
|
34
|
+
const method = entry.request.request.method;
|
|
35
|
+
const url = entry.request.request.url;
|
|
36
|
+
const { domain, path } = parseUrl(url);
|
|
37
|
+
|
|
38
|
+
// Get size information
|
|
39
|
+
const encodedSize = entry.response?.response.encodedDataLength || 0;
|
|
40
|
+
const decodedSize = entry.response?.response.decodedBodySize || 0;
|
|
41
|
+
const displaySize = decodedSize > 0 ? decodedSize : encodedSize;
|
|
42
|
+
|
|
43
|
+
const isSelected = selectedRequestId === entry.requestId;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
className={isSelected ? styles.listItemSelected : styles.listItem}
|
|
48
|
+
onClick={() => onSelect(entry.requestId)}
|
|
49
|
+
>
|
|
50
|
+
<div className={styles.statusColumn}>
|
|
51
|
+
<Tooltip
|
|
52
|
+
content={`Status: ${status || 'Pending'}`}
|
|
53
|
+
showOnlyWhenTruncated
|
|
54
|
+
variant={status >= 400 ? 'error' : status >= 300 ? 'warning' : 'info'}
|
|
55
|
+
>
|
|
56
|
+
<Badge color={getStatusColor(status)}>
|
|
57
|
+
{status || '...'}
|
|
58
|
+
</Badge>
|
|
59
|
+
</Tooltip>
|
|
60
|
+
</div>
|
|
61
|
+
<div className={styles.methodColumn}>
|
|
62
|
+
<Tooltip
|
|
63
|
+
content={`Method: ${method}`}
|
|
64
|
+
showOnlyWhenTruncated
|
|
65
|
+
variant="info"
|
|
66
|
+
>
|
|
67
|
+
<Badge color={getMethodColor(method)}>
|
|
68
|
+
{method}
|
|
69
|
+
</Badge>
|
|
70
|
+
</Tooltip>
|
|
71
|
+
</div>
|
|
72
|
+
<div className={styles.urlColumn}>
|
|
73
|
+
<Tooltip content={domain} showOnlyWhenTruncated>
|
|
74
|
+
<div className={styles.domainText}>
|
|
75
|
+
{domain}
|
|
76
|
+
</div>
|
|
77
|
+
</Tooltip>
|
|
78
|
+
<Tooltip content={path} showOnlyWhenTruncated>
|
|
79
|
+
<div className={styles.pathText}>
|
|
80
|
+
{path}
|
|
81
|
+
</div>
|
|
82
|
+
</Tooltip>
|
|
83
|
+
<Tooltip content={url} showOnlyWhenTruncated>
|
|
84
|
+
<div className={styles.fullUrlText}>
|
|
85
|
+
{url}
|
|
86
|
+
</div>
|
|
87
|
+
</Tooltip>
|
|
88
|
+
</div>
|
|
89
|
+
<div className={styles.durationColumn}>
|
|
90
|
+
<Tooltip content={`Duration: ${entry.duration ? formatDuration(entry.duration) : 'Pending'}`} showOnlyWhenTruncated>
|
|
91
|
+
<span className={styles.columnText}>
|
|
92
|
+
{entry.duration ? formatDuration(entry.duration) : '...'}
|
|
93
|
+
</span>
|
|
94
|
+
</Tooltip>
|
|
95
|
+
</div>
|
|
96
|
+
<div className={styles.sizeColumn}>
|
|
97
|
+
<Tooltip content={`Size: ${displaySize > 0 ? formatFileSize(displaySize) : 'Unknown'}`} showOnlyWhenTruncated>
|
|
98
|
+
<span className={styles.columnText}>
|
|
99
|
+
{displaySize > 0 ? formatFileSize(displaySize) : '...'}
|
|
100
|
+
</span>
|
|
101
|
+
</Tooltip>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (entries.length === 0) {
|
|
108
|
+
return (
|
|
109
|
+
<div className={styles.emptyContainer} style={{ height }}>
|
|
110
|
+
<div className={styles.emptyText}>
|
|
111
|
+
No network requests recorded
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div
|
|
119
|
+
ref={parentRef}
|
|
120
|
+
className={styles.container}
|
|
121
|
+
style={{ height }}
|
|
122
|
+
>
|
|
123
|
+
<div
|
|
124
|
+
className={styles.virtualContainer}
|
|
125
|
+
style={{ height: `${virtualizer.getTotalSize()}px` }}
|
|
126
|
+
>
|
|
127
|
+
{virtualizer.getVirtualItems().map((virtualItem: any) => (
|
|
128
|
+
<div
|
|
129
|
+
key={virtualItem.key}
|
|
130
|
+
className={styles.virtualItem}
|
|
131
|
+
style={{
|
|
132
|
+
height: `${virtualItem.size}px`,
|
|
133
|
+
transform: `translateY(${virtualItem.start}px)`,
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
<NetworkListItem
|
|
137
|
+
entry={entries[virtualItem.index]}
|
|
138
|
+
index={virtualItem.index}
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
141
|
+
))}
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Button, Toolbar } from './components';
|
|
3
|
+
import styles from './network-toolbar.module.css';
|
|
4
|
+
|
|
5
|
+
interface NetworkToolbarProps {
|
|
6
|
+
isRecording: boolean;
|
|
7
|
+
onToggleRecording: () => void;
|
|
8
|
+
onClear: () => void;
|
|
9
|
+
requestCount: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const NetworkToolbar: React.FC<NetworkToolbarProps> = ({
|
|
13
|
+
isRecording,
|
|
14
|
+
onToggleRecording,
|
|
15
|
+
onClear,
|
|
16
|
+
requestCount,
|
|
17
|
+
}) => {
|
|
18
|
+
return (
|
|
19
|
+
<Toolbar>
|
|
20
|
+
<Button
|
|
21
|
+
onClick={onToggleRecording}
|
|
22
|
+
variant={isRecording ? 'danger' : 'success'}
|
|
23
|
+
size="small"
|
|
24
|
+
className={styles.recordingButton}
|
|
25
|
+
>
|
|
26
|
+
{isRecording ? 'Stop' : 'Start'} Recording
|
|
27
|
+
</Button>
|
|
28
|
+
<Button
|
|
29
|
+
onClick={onClear}
|
|
30
|
+
variant="secondary"
|
|
31
|
+
size="small"
|
|
32
|
+
>
|
|
33
|
+
Clear
|
|
34
|
+
</Button>
|
|
35
|
+
<div className={styles.requestCount}>
|
|
36
|
+
{requestCount} requests
|
|
37
|
+
</div>
|
|
38
|
+
</Toolbar>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
height: 100vh;
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.mainContent {
|
|
9
|
+
flex: 1;
|
|
10
|
+
display: flex;
|
|
11
|
+
overflow: hidden;
|
|
12
|
+
min-height: 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.networkListContainer {
|
|
16
|
+
width: 60%;
|
|
17
|
+
border-right: 1px solid #e0e0e0;
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
min-width: 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.listContent {
|
|
24
|
+
flex: 1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.detailsContainer {
|
|
28
|
+
width: 40%;
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-direction: column;
|
|
31
|
+
min-width: 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.headerStatus {
|
|
35
|
+
width: 60px;
|
|
36
|
+
text-align: center;
|
|
37
|
+
flex-shrink: 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.headerMethod {
|
|
41
|
+
width: 80px;
|
|
42
|
+
text-align: center;
|
|
43
|
+
flex-shrink: 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.headerName {
|
|
47
|
+
flex: 1;
|
|
48
|
+
min-width: 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.headerTime {
|
|
52
|
+
width: 80px;
|
|
53
|
+
text-align: right;
|
|
54
|
+
flex-shrink: 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.headerSize {
|
|
58
|
+
width: 80px;
|
|
59
|
+
text-align: right;
|
|
60
|
+
flex-shrink: 0;
|
|
61
|
+
}
|
package/src/ui/panel.tsx
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import React, { useState, useEffect, useMemo } from 'react';
|
|
2
|
+
import { useRozeniteDevToolsClient } from "@rozenite/plugin-bridge";
|
|
3
|
+
import { NetworkEventMap, NetworkEntry } from '../types/network';
|
|
4
|
+
import styles from './panel.module.css';
|
|
5
|
+
import { NetworkToolbar } from './network-toolbar';
|
|
6
|
+
import { PanelHeader } from './components';
|
|
7
|
+
import { NetworkList } from './network-list';
|
|
8
|
+
import { NetworkDetails } from './network-details';
|
|
9
|
+
|
|
10
|
+
export default function NetworkActivityPanel() {
|
|
11
|
+
const client = useRozeniteDevToolsClient<NetworkEventMap>({
|
|
12
|
+
pluginId: '@rozenite/network-activity-plugin',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const [networkEntries, setNetworkEntries] = useState<Map<string, NetworkEntry>>(new Map());
|
|
16
|
+
const [selectedRequestId, setSelectedRequestId] = useState<string | null>(null);
|
|
17
|
+
const [isRecording, setIsRecording] = useState(true);
|
|
18
|
+
const [containerHeight, setContainerHeight] = useState(0);
|
|
19
|
+
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
20
|
+
|
|
21
|
+
// Convert Map to sorted array for rendering
|
|
22
|
+
const sortedEntries = useMemo(() => {
|
|
23
|
+
return Array.from(networkEntries.values())
|
|
24
|
+
.sort((a, b) => b.startTime - a.startTime);
|
|
25
|
+
}, [networkEntries]);
|
|
26
|
+
|
|
27
|
+
const selectedEntry = selectedRequestId ? networkEntries.get(selectedRequestId) || null : null;
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!client) return;
|
|
31
|
+
if (!isRecording) return;
|
|
32
|
+
|
|
33
|
+
const subscriptions: Array<{ remove: () => void }> = [];
|
|
34
|
+
|
|
35
|
+
// Subscribe to Network events
|
|
36
|
+
subscriptions.push(
|
|
37
|
+
client.onMessage('Network.requestWillBeSent', (payload) => {
|
|
38
|
+
setNetworkEntries(prev => {
|
|
39
|
+
const newMap = new Map(prev);
|
|
40
|
+
newMap.set(payload.requestId, {
|
|
41
|
+
requestId: payload.requestId,
|
|
42
|
+
request: payload,
|
|
43
|
+
status: 'pending',
|
|
44
|
+
startTime: payload.timestamp,
|
|
45
|
+
});
|
|
46
|
+
return newMap;
|
|
47
|
+
});
|
|
48
|
+
})
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
subscriptions.push(
|
|
52
|
+
client.onMessage('Network.requestWillBeSentExtraInfo', (payload) => {
|
|
53
|
+
setNetworkEntries(prev => {
|
|
54
|
+
const newMap = new Map(prev);
|
|
55
|
+
const entry = newMap.get(payload.requestId);
|
|
56
|
+
if (entry) {
|
|
57
|
+
newMap.set(payload.requestId, {
|
|
58
|
+
...entry,
|
|
59
|
+
extraInfo: payload,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return newMap;
|
|
63
|
+
});
|
|
64
|
+
})
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
subscriptions.push(
|
|
68
|
+
client.onMessage('Network.responseReceived', (payload) => {
|
|
69
|
+
setNetworkEntries(prev => {
|
|
70
|
+
const newMap = new Map(prev);
|
|
71
|
+
const entry = newMap.get(payload.requestId);
|
|
72
|
+
if (entry) {
|
|
73
|
+
newMap.set(payload.requestId, {
|
|
74
|
+
...entry,
|
|
75
|
+
response: payload,
|
|
76
|
+
status: 'loading',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return newMap;
|
|
80
|
+
});
|
|
81
|
+
})
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
subscriptions.push(
|
|
85
|
+
client.onMessage('Network.loadingFinished', (payload) => {
|
|
86
|
+
setNetworkEntries(prev => {
|
|
87
|
+
const newMap = new Map(prev);
|
|
88
|
+
const entry = newMap.get(payload.requestId);
|
|
89
|
+
if (entry) {
|
|
90
|
+
const endTime = payload.timestamp;
|
|
91
|
+
const duration = (endTime - entry.startTime) * 1000; // Convert to milliseconds
|
|
92
|
+
newMap.set(payload.requestId, {
|
|
93
|
+
...entry,
|
|
94
|
+
loadingFinished: payload,
|
|
95
|
+
status: 'finished',
|
|
96
|
+
endTime,
|
|
97
|
+
duration,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return newMap;
|
|
101
|
+
});
|
|
102
|
+
})
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
subscriptions.push(
|
|
106
|
+
client.onMessage('Network.loadingFailed', (payload) => {
|
|
107
|
+
setNetworkEntries(prev => {
|
|
108
|
+
const newMap = new Map(prev);
|
|
109
|
+
const entry = newMap.get(payload.requestId);
|
|
110
|
+
if (entry) {
|
|
111
|
+
const endTime = payload.timestamp;
|
|
112
|
+
const duration = (endTime - entry.startTime) * 1000; // Convert to milliseconds
|
|
113
|
+
newMap.set(payload.requestId, {
|
|
114
|
+
...entry,
|
|
115
|
+
loadingFailed: payload,
|
|
116
|
+
status: 'failed',
|
|
117
|
+
endTime,
|
|
118
|
+
duration,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return newMap;
|
|
122
|
+
});
|
|
123
|
+
})
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
return () => {
|
|
127
|
+
subscriptions.forEach(sub => sub.remove());
|
|
128
|
+
};
|
|
129
|
+
}, [client, isRecording]);
|
|
130
|
+
|
|
131
|
+
const clearNetworkLog = () => {
|
|
132
|
+
setNetworkEntries(new Map());
|
|
133
|
+
setSelectedRequestId(null);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const toggleRecording = () => {
|
|
137
|
+
setIsRecording(!isRecording);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const handleSelectRequest = (requestId: string) => {
|
|
141
|
+
setSelectedRequestId(requestId);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Update container height on mount and resize
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
const updateHeight = () => {
|
|
147
|
+
if (containerRef.current) {
|
|
148
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
149
|
+
setContainerHeight(rect.height - 120); // Subtract toolbar and header height
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
updateHeight();
|
|
154
|
+
window.addEventListener('resize', updateHeight);
|
|
155
|
+
return () => window.removeEventListener('resize', updateHeight);
|
|
156
|
+
}, []);
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div
|
|
160
|
+
ref={containerRef}
|
|
161
|
+
className={styles.container}
|
|
162
|
+
>
|
|
163
|
+
{/* Toolbar */}
|
|
164
|
+
<NetworkToolbar
|
|
165
|
+
isRecording={isRecording}
|
|
166
|
+
onToggleRecording={toggleRecording}
|
|
167
|
+
onClear={clearNetworkLog}
|
|
168
|
+
requestCount={sortedEntries.length}
|
|
169
|
+
/>
|
|
170
|
+
|
|
171
|
+
{/* Main Content */}
|
|
172
|
+
<div className={styles.mainContent}>
|
|
173
|
+
{/* Network List */}
|
|
174
|
+
<div className={styles.networkListContainer}>
|
|
175
|
+
{/* List Header */}
|
|
176
|
+
<PanelHeader>
|
|
177
|
+
<div className={styles.headerStatus}>Status</div>
|
|
178
|
+
<div className={styles.headerMethod}>Method</div>
|
|
179
|
+
<div className={styles.headerName}>Name</div>
|
|
180
|
+
<div className={styles.headerTime}>Time</div>
|
|
181
|
+
<div className={styles.headerSize}>Size</div>
|
|
182
|
+
</PanelHeader>
|
|
183
|
+
|
|
184
|
+
<div className={styles.listContent}>
|
|
185
|
+
<NetworkList
|
|
186
|
+
entries={sortedEntries}
|
|
187
|
+
selectedRequestId={selectedRequestId}
|
|
188
|
+
onSelect={handleSelectRequest}
|
|
189
|
+
height={containerHeight}
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
{/* Details Panel */}
|
|
195
|
+
<div className={styles.detailsContainer}>
|
|
196
|
+
<NetworkDetails entry={selectedEntry} />
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { QueryCacheNotifyEvent, MutationCacheNotifyEvent, QueryClient, QueryClientProvider, Query, Mutation } from '@tanstack/react-query';
|
|
2
|
+
import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools';
|
|
3
|
+
import { useRozeniteDevToolsClient } from '@rozenite/plugin-bridge';
|
|
4
|
+
import { useEffect, useRef } from 'react';
|
|
5
|
+
|
|
6
|
+
const queryClient = new QueryClient({
|
|
7
|
+
defaultOptions: {
|
|
8
|
+
queries: {
|
|
9
|
+
queryFn: async () => {
|
|
10
|
+
// Prevent refetch from throwing an error
|
|
11
|
+
return Promise.resolve(null);
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
type DevToolsEventMap = {
|
|
18
|
+
"DEVTOOLS_TO_DEVICE": unknown;
|
|
19
|
+
"DEVICE_TO_DEVTOOLS": QueryCacheNotifyEvent | MutationCacheNotifyEvent;
|
|
20
|
+
"DEVICE_TO_DEVTOOLS_ACK": { requestId: string; success: boolean };
|
|
21
|
+
"DEVICE_TO_DEVTOOLS_INITIAL_DATA": { queries: Query[]; mutations: Mutation[] };
|
|
22
|
+
"DEVTOOLS_TO_DEVICE_INITIAL_DATA_REQUEST": unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const Wrapped = () => {
|
|
26
|
+
const client = useRozeniteDevToolsClient<DevToolsEventMap>({
|
|
27
|
+
pluginId: '@rozenite/tanstack-query-plugin',
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// Track pending acknowledgments to prevent feedback loops
|
|
31
|
+
const pendingAcknowledgment = useRef<Set<string>>(new Set());
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!client) return;
|
|
35
|
+
client.send("DEVTOOLS_TO_DEVICE_INITIAL_DATA_REQUEST", null);
|
|
36
|
+
}, [client]);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!client) return;
|
|
40
|
+
|
|
41
|
+
const handleEvent = (event: Event) => {
|
|
42
|
+
const detail = (event as CustomEvent).detail;
|
|
43
|
+
|
|
44
|
+
// Generate a unique request ID for this DevTools action
|
|
45
|
+
const requestId = `devtools-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
46
|
+
|
|
47
|
+
// Mark that we're waiting for acknowledgment
|
|
48
|
+
pendingAcknowledgment.current.add(requestId);
|
|
49
|
+
|
|
50
|
+
// Add the request ID to the event detail
|
|
51
|
+
const eventWithRequestId = {
|
|
52
|
+
...detail,
|
|
53
|
+
requestId,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
client.send("DEVTOOLS_TO_DEVICE", eventWithRequestId);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
window.addEventListener('@tanstack/query-devtools-event', handleEvent);
|
|
60
|
+
return () => window.removeEventListener('@tanstack/query-devtools-event', handleEvent);
|
|
61
|
+
}, [client])
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!client) return;
|
|
65
|
+
|
|
66
|
+
const ackSubscription = client.onMessage("DEVICE_TO_DEVTOOLS_ACK", (ack) => {
|
|
67
|
+
// Remove the request from pending acknowledgments
|
|
68
|
+
pendingAcknowledgment.current.delete(ack.requestId);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const subscription = client.onMessage("DEVICE_TO_DEVTOOLS", (event) => {
|
|
72
|
+
// Don't reflect events if we're waiting for acknowledgments
|
|
73
|
+
if (pendingAcknowledgment.current.size > 0) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if ('query' in event) {
|
|
78
|
+
const { query, type } = event as QueryCacheNotifyEvent;
|
|
79
|
+
const queryCache = queryClient.getQueryCache();
|
|
80
|
+
|
|
81
|
+
if (type === 'updated') {
|
|
82
|
+
const existingQuery = queryCache.get(query.queryHash);
|
|
83
|
+
if (existingQuery) {
|
|
84
|
+
existingQuery.setState(query.state);
|
|
85
|
+
} else {
|
|
86
|
+
queryCache.build(
|
|
87
|
+
queryClient,
|
|
88
|
+
{
|
|
89
|
+
queryKey: query.queryKey,
|
|
90
|
+
queryHash: query.queryHash,
|
|
91
|
+
},
|
|
92
|
+
query.state
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
} else if (type === 'added') {
|
|
96
|
+
const existingQuery = queryCache.get(query.queryHash);
|
|
97
|
+
if (!existingQuery) {
|
|
98
|
+
// Only add if it doesn't already exist
|
|
99
|
+
queryCache.build(
|
|
100
|
+
queryClient,
|
|
101
|
+
{
|
|
102
|
+
queryKey: query.queryKey,
|
|
103
|
+
queryHash: query.queryHash,
|
|
104
|
+
},
|
|
105
|
+
query.state
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
} else if (type === 'removed') {
|
|
109
|
+
const existingQuery = queryCache.get(query.queryHash);
|
|
110
|
+
if (existingQuery) {
|
|
111
|
+
queryCache.remove(existingQuery);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} else if ('mutation' in event) {
|
|
115
|
+
const { mutation, type } = event as MutationCacheNotifyEvent;
|
|
116
|
+
const mutationCache = queryClient.getMutationCache();
|
|
117
|
+
|
|
118
|
+
if (type === 'added') {
|
|
119
|
+
const existingMutation = mutationCache.find({ mutationKey: mutation.options.mutationKey });
|
|
120
|
+
if (existingMutation) {
|
|
121
|
+
mutationCache.remove(existingMutation);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
mutationCache.build(
|
|
125
|
+
queryClient,
|
|
126
|
+
mutation.options,
|
|
127
|
+
mutation.state
|
|
128
|
+
);
|
|
129
|
+
} else if (type === 'removed') {
|
|
130
|
+
const existingMutation = mutationCache.find({ mutationKey: mutation.options.mutationKey });
|
|
131
|
+
if (existingMutation) {
|
|
132
|
+
mutationCache.remove(existingMutation);
|
|
133
|
+
}
|
|
134
|
+
} else if (type === 'updated') {
|
|
135
|
+
const existingMutation = mutationCache.find({ mutationKey: mutation.options.mutationKey });
|
|
136
|
+
|
|
137
|
+
if (existingMutation) {
|
|
138
|
+
mutationCache.remove(existingMutation);
|
|
139
|
+
mutationCache.build(
|
|
140
|
+
queryClient,
|
|
141
|
+
mutation.options,
|
|
142
|
+
mutation.state
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const initialDataSubscription = client.onMessage("DEVICE_TO_DEVTOOLS_INITIAL_DATA", (event) => {
|
|
150
|
+
// Clear existing data first
|
|
151
|
+
queryClient.clear();
|
|
152
|
+
queryClient.getMutationCache().clear();
|
|
153
|
+
|
|
154
|
+
// Restore queries
|
|
155
|
+
const queryCache = queryClient.getQueryCache();
|
|
156
|
+
event.queries.forEach(query => {
|
|
157
|
+
queryCache.build(
|
|
158
|
+
queryClient,
|
|
159
|
+
{
|
|
160
|
+
queryKey: query.queryKey,
|
|
161
|
+
queryHash: query.queryHash,
|
|
162
|
+
},
|
|
163
|
+
query.state
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Restore mutations
|
|
168
|
+
const mutationCache = queryClient.getMutationCache();
|
|
169
|
+
event.mutations.forEach(mutation => {
|
|
170
|
+
mutationCache.build(
|
|
171
|
+
queryClient,
|
|
172
|
+
mutation.options,
|
|
173
|
+
mutation.state
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return () => {
|
|
179
|
+
subscription.remove();
|
|
180
|
+
ackSubscription.remove();
|
|
181
|
+
initialDataSubscription.remove();
|
|
182
|
+
};
|
|
183
|
+
}, [client, queryClient])
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<ReactQueryDevtoolsPanel />
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export default function TanStackQueryPanel() {
|
|
192
|
+
return (
|
|
193
|
+
<QueryClientProvider client={queryClient}>
|
|
194
|
+
<Wrapped />
|
|
195
|
+
</QueryClientProvider>
|
|
196
|
+
)
|
|
197
|
+
}
|