react-native-devtools-sync
Version:
A tool for syncing React Query state to an external Dev Tools
630 lines • 31.7 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { useEffect, useRef } from "react";
import { onlineManager } from "@tanstack/react-query";
import { log } from "./utils/logger";
import { Dehydrate } from "./hydration";
import { setupFetchInterceptor, setupXHRInterceptor, setupWebSocketInterceptor } from "./sendNetworkRequest";
import { executeExpoCommand } from "./executeExpoCommand";
import { useMySocket } from "./useMySocket";
function shouldProcessMessage({ targetDeviceId, currentDeviceId, }) {
return targetDeviceId === currentDeviceId || targetDeviceId === "All";
}
/**
* Verifies if the React Query version is compatible with dev tools
*/
function checkVersion(queryClient) {
var _a, _b, _c, _d;
// Basic version check
const version = (_d = (_c = (_b = (_a = queryClient).getDefaultOptions) === null || _b === void 0 ? void 0 : _b.call(_a)) === null || _c === void 0 ? void 0 : _c.queries) === null || _d === void 0 ? void 0 : _d.version;
if (version &&
!version.toString().startsWith("4") &&
!version.toString().startsWith("5")) {
log("This version of React Query has not been tested with the dev tools plugin. Some features might not work as expected.", true, "warn");
}
}
/**
* Hook used by mobile devices to sync query state with the external dashboard
*
* Handles:
* - Connection to the socket server
* - Responding to dashboard requests
* - Processing query actions from the dashboard
* - Sending query state updates to the dashboard
*/
export function useSyncQueriesExternal({ queryClient, deviceName, socketURL, extraDeviceInfo, platform, deviceId, enableLogs = false, storage, networkMonitoring, }) {
// ==========================================================
// Validate deviceId
// ==========================================================
if (!(deviceId === null || deviceId === void 0 ? void 0 : deviceId.trim())) {
throw new Error(`[${deviceName}] deviceId is required and must not be empty. This ID must persist across app restarts, especially if you have multiple devices of the same type. If you only have one iOS and one Android device, you can use 'ios' and 'android'.`);
}
// ==========================================================
// Persistent device ID - used to identify this device
// across app restarts
// ==========================================================
const logPrefix = `[${deviceName}]`;
// ==========================================================
// Socket connection - Handles connection to the socket server and
// event listeners for the socket server
// ==========================================================
const { connect, disconnect, isConnected, socket } = useMySocket({
deviceName,
socketURL,
persistentDeviceId: deviceId,
extraDeviceInfo,
platform,
enableLogs,
});
// Use refs to track state and cleanup functions
const prevConnectedRef = useRef(false);
const removeFetchInterceptorRef = useRef(null);
const removeXHRInterceptorRef = useRef(null);
const removeWebSocketInterceptorRef = useRef(null);
// Helper function to send storage state to the dashboard
const sendStorageState = () => __awaiter(this, void 0, void 0, function* () {
if (!storage || !socket || !deviceId) {
return;
}
try {
const keys = yield storage.getAllKeys();
const items = [];
for (const key of keys) {
const value = yield storage.getItem(key);
items.push({ key, value: value || '' });
}
const syncMessage = {
type: "async-storage-state",
state: {
items,
timestamp: Date.now()
},
persistentDeviceId: deviceId
};
socket.emit('async-storage-sync', syncMessage);
log(`${logPrefix} Sent storage state to dashboard (${items.length} items)`, enableLogs);
}
catch (error) {
log(`${logPrefix} Error sending storage state: ${error}`, enableLogs, "error");
}
});
useEffect(() => {
// Check React Query version if queryClient is provided
if (queryClient) {
checkVersion(queryClient);
}
// Only log connection state changes to reduce noise
if (prevConnectedRef.current !== isConnected) {
if (!isConnected) {
log(`${logPrefix} Not connected to external dashboard`, enableLogs);
}
else {
log(`${deviceName} Connected to external dashboard`, enableLogs);
}
prevConnectedRef.current = isConnected;
}
// Don't proceed with setting up event handlers if not connected
if (!isConnected || !socket) {
return;
}
// ==========================================================
// Event Handlers
// ==========================================================
// ==========================================================
// React Query specific event handlers
// ==========================================================
let initialStateSubscription;
let queryActionSubscription;
let onlineManagerSubscription;
let unsubscribe = () => { }; // Default no-op function
// Only set up React Query specific handlers if queryClient is provided
if (queryClient) {
// ==========================================================
// Handle initial state requests from dashboard
// ==========================================================
initialStateSubscription = socket.on("request-initial-state", () => {
if (!deviceId) {
log(`${logPrefix} No persistent device ID found`, enableLogs, "warn");
return;
}
log(`${logPrefix} Dashboard is requesting initial state`, enableLogs);
const dehydratedState = Dehydrate(queryClient);
const syncMessage = {
type: "dehydrated-state",
state: dehydratedState,
isOnlineManagerOnline: onlineManager.isOnline(),
persistentDeviceId: deviceId,
};
socket.emit("query-sync", syncMessage);
log(`[${deviceName}] Sent initial state to dashboard (${dehydratedState.queries.length} queries)`, enableLogs);
});
// ==========================================================
// Online manager handler - Handle device internet connection state changes
// ==========================================================
onlineManagerSubscription = socket.on("online-manager", (message) => {
const { action, targetDeviceId } = message;
if (!deviceId) {
log(`${logPrefix} No persistent device ID found`, enableLogs, "warn");
return;
}
// Only process if this message targets the current device
if (!shouldProcessMessage({
targetDeviceId: targetDeviceId,
currentDeviceId: deviceId,
})) {
return;
}
log(`[${deviceName}] Received online-manager action: ${action}`, enableLogs);
switch (action) {
case "ACTION-ONLINE-MANAGER-ONLINE": {
log(`${logPrefix} Set online state: ONLINE`, enableLogs);
onlineManager.setOnline(true);
break;
}
case "ACTION-ONLINE-MANAGER-OFFLINE": {
log(`${logPrefix} Set online state: OFFLINE`, enableLogs);
onlineManager.setOnline(false);
break;
}
}
});
// ==========================================================
// Query Actions handler - Process actions from the dashboard
// ==========================================================
queryActionSubscription = socket.on("query-action", (message) => {
const { queryHash, queryKey, data, action, targetDeviceId } = message;
if (!deviceId) {
log(`[${deviceName}] No persistent device ID found`, enableLogs, "warn");
return;
}
// Skip if not targeted at this device
if (!shouldProcessMessage({
targetDeviceId: targetDeviceId,
currentDeviceId: deviceId,
})) {
return;
}
log(`${logPrefix} Received query action: ${action} for query ${queryHash}`, enableLogs);
// If action is clear cache do the action here before moving on
if (action === "ACTION-CLEAR-MUTATION-CACHE") {
queryClient.getMutationCache().clear();
log(`${logPrefix} Cleared mutation cache`, enableLogs);
return;
}
if (action === "ACTION-CLEAR-QUERY-CACHE") {
queryClient.getQueryCache().clear();
log(`${logPrefix} Cleared query cache`, enableLogs);
return;
}
const activeQuery = queryClient.getQueryCache().get(queryHash);
if (!activeQuery) {
log(`${logPrefix} Query with hash ${queryHash} not found`, enableLogs, "warn");
return;
}
switch (action) {
case "ACTION-DATA-UPDATE": {
log(`${logPrefix} Updating data for query:`, enableLogs);
queryClient.setQueryData(queryKey, data, {
updatedAt: Date.now(),
});
break;
}
case "ACTION-TRIGGER-ERROR": {
log(`${logPrefix} Triggering error state for query:`, enableLogs);
const error = new Error("Unknown error from devtools");
const __previousQueryOptions = activeQuery.options;
activeQuery.setState({
status: "error",
error,
fetchMeta: Object.assign(Object.assign({}, activeQuery.state.fetchMeta), {
// @ts-expect-error This does exist
__previousQueryOptions }),
});
break;
}
case "ACTION-RESTORE-ERROR": {
log(`${logPrefix} Restoring from error state for query:`, enableLogs);
queryClient.resetQueries(activeQuery);
break;
}
case "ACTION-TRIGGER-LOADING": {
if (!activeQuery)
return;
log(`${logPrefix} Triggering loading state for query:`, enableLogs);
const __previousQueryOptions = activeQuery.options;
// Trigger a fetch in order to trigger suspense as well.
activeQuery.fetch(Object.assign(Object.assign({}, __previousQueryOptions), { queryFn: () => {
return new Promise(() => {
// Never resolve - simulates perpetual loading
});
}, gcTime: -1 }));
activeQuery.setState({
data: undefined,
status: "pending",
fetchMeta: Object.assign(Object.assign({}, activeQuery.state.fetchMeta), {
// @ts-expect-error This does exist
__previousQueryOptions }),
});
break;
}
case "ACTION-RESTORE-LOADING": {
log(`${logPrefix} Restoring from loading state for query:`, enableLogs);
const previousState = activeQuery.state;
const previousOptions = activeQuery.state.fetchMeta
? activeQuery.state.fetchMeta.__previousQueryOptions
: null;
activeQuery.cancel({ silent: true });
activeQuery.setState(Object.assign(Object.assign({}, previousState), { fetchStatus: "idle", fetchMeta: null }));
if (previousOptions) {
activeQuery.fetch(previousOptions);
}
break;
}
case "ACTION-RESET": {
log(`${logPrefix} Resetting query:`, enableLogs);
queryClient.resetQueries(activeQuery);
break;
}
case "ACTION-REMOVE": {
log(`${logPrefix} Removing query:`, enableLogs);
queryClient.removeQueries(activeQuery);
break;
}
case "ACTION-REFETCH": {
log(`${logPrefix} Refetching query:`, enableLogs);
const promise = activeQuery.fetch();
promise.catch((error) => {
// Log fetch errors but don't propagate them
log(`[${deviceName}] Refetch error for ${queryHash}:`, enableLogs, "error");
});
break;
}
case "ACTION-INVALIDATE": {
log(`${logPrefix} Invalidating query:`, enableLogs);
queryClient.invalidateQueries(activeQuery);
break;
}
case "ACTION-ONLINE-MANAGER-ONLINE": {
log(`${logPrefix} Setting online state: ONLINE`, enableLogs);
onlineManager.setOnline(true);
break;
}
case "ACTION-ONLINE-MANAGER-OFFLINE": {
log(`${logPrefix} Setting online state: OFFLINE`, enableLogs);
onlineManager.setOnline(false);
break;
}
}
});
// ==========================================================
// Subscribe to query changes and sync to dashboard
// ==========================================================
unsubscribe = queryClient.getQueryCache().subscribe(() => {
if (!deviceId) {
log(`${logPrefix} No persistent device ID found`, enableLogs, "warn");
return;
}
// Dehydrate the current state
const dehydratedState = Dehydrate(queryClient);
// Create sync message
const syncMessage = {
type: "dehydrated-state",
state: dehydratedState,
isOnlineManagerOnline: onlineManager.isOnline(),
persistentDeviceId: deviceId,
};
// Send message to dashboard
socket.emit("query-sync", syncMessage);
});
}
// ==========================================================
// Storage handlers - Process storage actions from the dashboard
// ==========================================================
const asyncStorageActionSubscription = socket.on("async-storage-action", (message) => __awaiter(this, void 0, void 0, function* () {
const { action, targetDeviceId, key, value } = message;
if (!deviceId) {
log(`${logPrefix} No persistent device ID found`, enableLogs, "warn");
return;
}
// Skip if not targeted at this device
if (!shouldProcessMessage({
targetDeviceId: targetDeviceId,
currentDeviceId: deviceId,
})) {
return;
}
log(`${logPrefix} Received storage action: ${action}${key ? ` for key ${key}` : ''}`, enableLogs);
// If storage is provided, handle the action directly
if (storage) {
try {
switch (action) {
case 'GET_ALL_KEYS':
yield sendStorageState();
break;
case 'GET_ITEM':
if (key) {
const value = yield storage.getItem(key);
log(`${logPrefix} Got storage item: ${key} = ${value}`, enableLogs);
// After getting the item, send the full state back
yield sendStorageState();
}
break;
case 'SET_ITEM':
if (key && value !== undefined) {
yield storage.setItem(key, value);
log(`${logPrefix} Set storage item: ${key} = ${value}`, enableLogs);
// After setting the item, send the updated state back
yield sendStorageState();
}
break;
case 'REMOVE_ITEM':
if (key) {
yield storage.removeItem(key);
log(`${logPrefix} Removed storage item: ${key}`, enableLogs);
// After removing the item, send the updated state back
yield sendStorageState();
}
break;
case 'CLEAR_ALL':
yield storage.clear();
log(`${logPrefix} Cleared all storage items`, enableLogs);
// After clearing all items, send the updated state back
yield sendStorageState();
break;
}
}
catch (error) {
log(`${logPrefix} Error handling storage action: ${error}`, enableLogs, "error");
}
}
else {
// If storage is not provided, emit the event for the app to handle
socket.emit("async-storage-action-received", message);
log(`${logPrefix} Emitted async-storage-action-received event for app to handle`, enableLogs);
}
}));
// ==========================================================
// Handle storage state requests from dashboard
// ==========================================================
const asyncStorageRequestSubscription = socket.on("request-async-storage", (message) => __awaiter(this, void 0, void 0, function* () {
const { targetDeviceId } = message;
if (!deviceId) {
log(`${logPrefix} No persistent device ID found`, enableLogs, "warn");
return;
}
// Skip if not targeted at this device
if (!shouldProcessMessage({
targetDeviceId: targetDeviceId,
currentDeviceId: deviceId,
})) {
return;
}
log(`${logPrefix} Dashboard is requesting storage state`, enableLogs);
// If storage is provided, handle the request directly
if (storage) {
yield sendStorageState();
}
else {
// If storage is not provided, emit the event for the app to handle
socket.emit("request-async-storage-received", { type: "request-async-storage" });
log(`${logPrefix} Emitted request-async-storage-received event for app to handle`, enableLogs);
}
}));
// ==========================================================
// Network Monitoring - Set up interceptors if enabled
// ==========================================================
if (networkMonitoring) {
// Set up fetch interceptor if enabled
if (networkMonitoring.fetch) {
log(`${logPrefix} Setting up fetch interceptor`, enableLogs);
removeFetchInterceptorRef.current = setupFetchInterceptor(socket, deviceId, enableLogs);
}
// Set up XHR interceptor if enabled
if (networkMonitoring.xhr) {
log(`${logPrefix} Setting up XHR interceptor`, enableLogs);
removeXHRInterceptorRef.current = setupXHRInterceptor(socket, deviceId, enableLogs);
}
// Set up WebSocket interceptor if enabled
if (networkMonitoring.websocket) {
log(`${logPrefix} Setting up WebSocket interceptor`, enableLogs);
removeWebSocketInterceptorRef.current = setupWebSocketInterceptor(socket, deviceId, enableLogs);
}
}
// ==========================================================
// Network Monitoring - Handle network monitoring actions
// ==========================================================
const networkMonitoringSubscription = socket.on("network-monitoring-action", (message) => {
const { action, targetDeviceId } = message;
if (!deviceId) {
log(`${logPrefix} No persistent device ID found`, enableLogs, "warn");
return;
}
// Only process if this message targets the current device
if (!shouldProcessMessage({
targetDeviceId: targetDeviceId,
currentDeviceId: deviceId,
})) {
return;
}
log(`${logPrefix} Received network-monitoring action: ${action}`, enableLogs);
switch (action) {
case "ACTION-ENABLE-NETWORK-MONITORING": {
log(`${logPrefix} Enabling network monitoring`, enableLogs);
// Set up fetch interceptor if not already set up
if (!removeFetchInterceptorRef.current) {
removeFetchInterceptorRef.current = setupFetchInterceptor(socket, deviceId, enableLogs);
}
// Set up XHR interceptor if not already set up
if (!removeXHRInterceptorRef.current) {
removeXHRInterceptorRef.current = setupXHRInterceptor(socket, deviceId, enableLogs);
}
// Set up WebSocket interceptor if not already set up
if (!removeWebSocketInterceptorRef.current) {
removeWebSocketInterceptorRef.current = setupWebSocketInterceptor(socket, deviceId, enableLogs);
}
break;
}
case "ACTION-DISABLE-NETWORK-MONITORING": {
log(`${logPrefix} Disabling network monitoring`, enableLogs);
// Clean up fetch interceptor
if (removeFetchInterceptorRef.current) {
removeFetchInterceptorRef.current();
removeFetchInterceptorRef.current = null;
}
// Clean up XHR interceptor
if (removeXHRInterceptorRef.current) {
removeXHRInterceptorRef.current();
removeXHRInterceptorRef.current = null;
}
// Clean up WebSocket interceptor
if (removeWebSocketInterceptorRef.current) {
removeWebSocketInterceptorRef.current();
removeWebSocketInterceptorRef.current = null;
}
break;
}
}
});
// ==========================================================
// Handle network monitoring requests from dashboard
// ==========================================================
const networkRequestSubscription = socket.on("request-network-monitoring", (message) => {
const { targetDeviceId } = message;
if (!deviceId) {
log(`${logPrefix} No persistent device ID found`, enableLogs, "warn");
return;
}
// Only process if this message targets the current device
if (!shouldProcessMessage({
targetDeviceId: targetDeviceId,
currentDeviceId: deviceId,
})) {
return;
}
log(`${logPrefix} Dashboard is requesting network monitoring state`, enableLogs);
// Your app should implement this functionality to send current network requests
// For example: sendCurrentNetworkRequests();
});
// ==========================================================
// Handle Expo command actions from the dashboard
// ==========================================================
const expoCommandActionSubscription = socket.on("expo-command-action", (message) => __awaiter(this, void 0, void 0, function* () {
const { command, targetDeviceId, commandId } = message;
if (!deviceId) {
log(`${logPrefix} No persistent device ID found`, enableLogs, "warn");
return;
}
// Only process if this message targets the current device
if (!shouldProcessMessage({
targetDeviceId: targetDeviceId,
currentDeviceId: deviceId,
})) {
return;
}
log(`${logPrefix} Received Expo command action: ${command} (ID: ${commandId})`, enableLogs);
// Create a command object
const expoCommand = {
id: commandId,
type: command,
status: 'pending',
timestamp: Date.now(),
deviceId: targetDeviceId,
};
// Execute the command
yield executeExpoCommand(expoCommand, socket, deviceId, enableLogs);
}));
// ==========================================================
// Handle Expo DevTools status requests from dashboard
// ==========================================================
const expoDevToolsRequestSubscription = socket.on("request-expo-devtools-status", (message) => {
const { targetDeviceId } = message;
if (!deviceId) {
log(`${logPrefix} No persistent device ID found`, enableLogs, "warn");
return;
}
// Only process if this message targets the current device
if (!shouldProcessMessage({
targetDeviceId: targetDeviceId,
currentDeviceId: deviceId,
})) {
return;
}
log(`${logPrefix} Dashboard is requesting Expo DevTools status`, enableLogs);
// Send a success response to indicate that Expo DevTools is available
const resultMessage = {
type: 'expo-command-result',
command: {
id: 'status-check',
type: 'reload', // Doesn't matter for status check
status: 'success',
timestamp: Date.now(),
deviceId: targetDeviceId,
result: { expoDevToolsAvailable: true },
},
persistentDeviceId: deviceId,
};
socket.emit('expo-command-result', resultMessage);
});
// ==========================================================
// Cleanup function to unsubscribe from all events
// ==========================================================
return () => {
log(`${logPrefix} Cleaning up event listeners`, enableLogs);
queryActionSubscription === null || queryActionSubscription === void 0 ? void 0 : queryActionSubscription.off();
initialStateSubscription === null || initialStateSubscription === void 0 ? void 0 : initialStateSubscription.off();
onlineManagerSubscription === null || onlineManagerSubscription === void 0 ? void 0 : onlineManagerSubscription.off();
asyncStorageActionSubscription === null || asyncStorageActionSubscription === void 0 ? void 0 : asyncStorageActionSubscription.off();
asyncStorageRequestSubscription === null || asyncStorageRequestSubscription === void 0 ? void 0 : asyncStorageRequestSubscription.off();
if (networkMonitoringSubscription) {
networkMonitoringSubscription.off();
}
if (networkRequestSubscription) {
networkRequestSubscription.off();
}
if (expoCommandActionSubscription) {
expoCommandActionSubscription.off();
}
if (expoDevToolsRequestSubscription) {
expoDevToolsRequestSubscription.off();
}
// Clean up network interceptors
if (removeFetchInterceptorRef.current) {
removeFetchInterceptorRef.current();
removeFetchInterceptorRef.current = null;
}
if (removeXHRInterceptorRef.current) {
removeXHRInterceptorRef.current();
removeXHRInterceptorRef.current = null;
}
if (removeWebSocketInterceptorRef.current) {
removeWebSocketInterceptorRef.current();
removeWebSocketInterceptorRef.current = null;
}
unsubscribe();
};
}, [
queryClient,
socket,
deviceName,
isConnected,
deviceId,
enableLogs,
logPrefix,
storage,
networkMonitoring,
]);
return {
connect,
disconnect,
isConnected,
socket,
};
}
//# sourceMappingURL=useSyncQueriesExternal.js.map