UNPKG

react-native-devtools-sync

Version:

A tool for syncing React Query state to an external Dev Tools

630 lines 31.7 kB
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