react-query-external-sync
Version:
A tool for syncing React Query state to an external Dev Tools
275 lines • 13.6 kB
JavaScript
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