UNPKG

react-query-external-sync

Version:

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

259 lines 11.6 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 { useCallback, useEffect, useRef, useState } from 'react'; import { useQueries } from '@tanstack/react-query'; import { storageQueryKeys } from './storageQueryKeys'; /** * Hook that creates individual React Query queries for each SecureStore key * This gives you granular control and better performance since each key has its own query * Since SecureStore doesn't have built-in change listeners and doesn't expose getAllKeys(), * this hook uses polling to detect value changes for provided known keys * * @example * // With expo-secure-store * import * as SecureStore from 'expo-secure-store'; * * const queries = useDynamicSecureStorageQueries({ * queryClient, * secureStorage: SecureStore, * knownKeys: ['auth.session', 'auth.email', 'sessionToken', 'knock_push_token'] * }); * * @example * // With react-native-keychain or custom implementation * const customSecureStore = { * getItemAsync: async (key: string) => { * // Your custom implementation * return await Keychain.getInternetCredentials(key); * } * }; * * const queries = useDynamicSecureStorageQueries({ * queryClient, * secureStorage: customSecureStore, * knownKeys: ['auth.session', 'auth.email'] * }); * * // Returns: [ * // { key: 'auth.session', data: { user: {...} }, isLoading: false, error: null }, * // { key: 'auth.email', data: 'user@example.com', isLoading: false, error: null }, * // ... * // ] */ export function useDynamicSecureStorageQueries({ queryClient, secureStorage, pollInterval = 1000, knownKeys, }) { // State to track which keys actually exist in SecureStore const [existingKeys, setExistingKeys] = useState([]); // Use ref to track the current keys to avoid stale closures in polling const existingKeysRef = useRef([]); // Track if we're currently checking keys to prevent concurrent checks const isCheckingKeysRef = useRef(false); // Update ref whenever existingKeys changes useEffect(() => { existingKeysRef.current = existingKeys; }, [existingKeys]); // Helper function to get a single SecureStore value const getSecureStorageValue = useCallback((key) => __awaiter(this, void 0, void 0, function* () { if (!secureStorage) { throw new Error('SecureStorage instance not provided'); } try { const value = yield secureStorage.getItemAsync(key); if (value === null) { return null; } // Try to parse as JSON, fall back to string try { return JSON.parse(value); } catch (_a) { return value; } } catch (error) { console.error('Error getting SecureStore value for key:', key, error); throw error; } }), [secureStorage]); // Helper function to compare arrays const arraysEqual = useCallback((a, b) => { if (a.length !== b.length) return false; const sortedA = [...a].sort(); const sortedB = [...b].sort(); return sortedA.every((val, index) => val === sortedB[index]); }, []); // Function to check which known keys actually exist in SecureStore const refreshExistingKeys = useCallback(() => __awaiter(this, void 0, void 0, function* () { if (!secureStorage || isCheckingKeysRef.current) { return; } isCheckingKeysRef.current = true; try { const existingKeysList = []; // Check each known key to see if it exists yield Promise.all(knownKeys.map((key) => __awaiter(this, void 0, void 0, function* () { try { const value = yield secureStorage.getItemAsync(key); if (value !== null) { existingKeysList.push(key); } } catch (error) { console.error('🔑 [SecureStore Hook] Error checking key:', key, error); } }))); // Only update state if the keys have actually changed const currentKeys = existingKeysRef.current; if (!arraysEqual(existingKeysList, currentKeys)) { setExistingKeys([...existingKeysList]); } } catch (error) { console.error('🔑 [SecureStore Hook] Error checking SecureStore keys:', error); } finally { isCheckingKeysRef.current = false; } }), [knownKeys, arraysEqual, secureStorage]); // Initial load of existing keys useEffect(() => { if (secureStorage) { refreshExistingKeys(); } }, [refreshExistingKeys, secureStorage]); // Set up polling for value changes (since SecureStore doesn't have listeners) useEffect(() => { if (!secureStorage || pollInterval <= 0) { return; } const interval = setInterval(() => __awaiter(this, void 0, void 0, function* () { try { const currentExistingKeys = existingKeysRef.current; // Skip if we're already checking keys if (isCheckingKeysRef.current) { return; } // Check if any known keys have been added or removed const newExistingKeys = []; yield Promise.all(knownKeys.map((key) => __awaiter(this, void 0, void 0, function* () { try { const value = yield secureStorage.getItemAsync(key); if (value !== null) { newExistingKeys.push(key); } } catch (error) { console.error('🔑 [SecureStore Hook] Error checking key during poll:', key, error); } }))); // Check if keys have changed (added/removed) using proper comparison const keysChanged = !arraysEqual(newExistingKeys, currentExistingKeys); if (keysChanged) { console.log('🔄 [SecureStore Hook] SecureStore keys changed!'); console.log('🔄 [SecureStore Hook] Old keys:', currentExistingKeys.length); console.log('🔄 [SecureStore Hook] New keys:', newExistingKeys.length); setExistingKeys([...newExistingKeys]); // Invalidate all SecureStore queries to refresh data queryClient.invalidateQueries({ queryKey: storageQueryKeys.secure.root(), }); } else { // Keys are the same, but check if any values have changed for (const key of currentExistingKeys) { try { // Check if the query exists in the cache first const queryExists = queryClient.getQueryCache().find({ queryKey: storageQueryKeys.secure.key(key) }); // If query doesn't exist (e.g., after cache clear), skip comparison // The useQueries hook will recreate the query automatically if (!queryExists) { continue; } // Get current value from SecureStore const currentValue = yield getSecureStorageValue(key); // Get cached value from React Query const cachedData = queryClient.getQueryData(storageQueryKeys.secure.key(key)); // Only compare if we have cached data (avoid false positives after cache clear) if (cachedData !== undefined) { // Deep comparison using a more robust method const valuesAreDifferent = !deepEqual(currentValue, cachedData); if (valuesAreDifferent) { console.log('🔄 [SecureStore Hook] Value changed for key:', key); // Invalidate this specific query queryClient.invalidateQueries({ queryKey: storageQueryKeys.secure.key(key), }); } } } catch (error) { console.error('🔑 [SecureStore Hook] Error checking value for key:', key, error); } } } } catch (error) { console.error('🔑 [SecureStore Hook] Error polling SecureStore keys:', error); } }), pollInterval); return () => { clearInterval(interval); }; }, [pollInterval, knownKeys, queryClient, getSecureStorageValue, arraysEqual, secureStorage]); // Create individual queries for each existing key const queries = useQueries({ queries: existingKeys.map((key) => ({ queryKey: storageQueryKeys.secure.key(key), queryFn: () => __awaiter(this, void 0, void 0, function* () { const value = yield getSecureStorageValue(key); return value; }), staleTime: pollInterval > 0 ? pollInterval / 2 : 0, // Half the poll interval gcTime: 10 * 60 * 1000, // 10 minutes (longer than AsyncStorage for security) networkMode: 'always', retry: 1, // Retry once for secure storage retryDelay: 200, // 200ms delay between retries })), combine: (results) => { const combinedResults = results.map((result, index) => ({ key: existingKeys[index], data: result.data, isLoading: result.isLoading, error: result.error, })); return combinedResults; }, }, queryClient); // Return empty array if no secureStorage is provided if (!secureStorage) { return []; } return queries; } // Helper function for deep equality comparison function deepEqual(a, b) { if (a === b) return true; if (a === null || b === null || a === undefined || b === undefined) { return a === b; } if (typeof a !== typeof b) return false; if (typeof a !== 'object') return a === b; // For objects, use JSON comparison as fallback but handle edge cases try { return JSON.stringify(a) === JSON.stringify(b); } catch (_a) { return false; } } //# sourceMappingURL=useDynamicSecureStorageQueries.js.map