UNPKG

react-query-external-sync

Version:

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

275 lines 13.6 kB
import { useEffect, useRef } from "react"; import { onlineManager } from "@tanstack/react-query"; import { Dehydrate } from "./hydration"; import { useMySocket } from "./useMySocket"; import { log } from "./utils/logger"; 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, }) { // ========================================================== // 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 a ref to track previous connection state to avoid duplicate logs const prevConnectedRef = useRef(false); useEffect(() => { 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 // ========================================================== // ========================================================== // Handle initial state requests from dashboard // ========================================================== const 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 // ========================================================== const 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 // ========================================================== const queryActionSubscription = socket.on("query-action", (message) => { const { queryHash, queryKey, data, action, deviceId } = message; if (!deviceId) { log(`[${deviceName}] No persistent device ID found`, enableLogs, "warn"); return; } // Skip if not targeted at this device if (!shouldProcessMessage({ targetDeviceId: deviceId, currentDeviceId: deviceId, })) { return; } log(`${logPrefix} Received query action: ${action} for query ${queryHash}`, enableLogs); 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 // ========================================================== const 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); }); // ========================================================== // 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(); unsubscribe(); }; }, [queryClient, socket, deviceName, isConnected, deviceId, enableLogs]); return { connect, disconnect, isConnected, socket }; } //# sourceMappingURL=useSyncQueries.js.map