UNPKG

@dataql/react-native

Version:

DataQL React Native SDK with offline-first capabilities and clean API

342 lines (341 loc) 12.3 kB
import { useState, useEffect, useCallback } from "react"; import { useLiveQuery as useDrizzleLiveQuery } from "drizzle-orm/expo-sqlite"; import { cachedData } from "../db/schema"; // Global instances - these would be initialized by the DataQLProvider let globalCacheManager = null; let globalSyncManager = null; let globalDbClient = null; export function setGlobalInstances(cacheManager, syncManager, dbClient) { globalCacheManager = cacheManager; globalSyncManager = syncManager; globalDbClient = dbClient; // Need dbClient for live queries } // Hook for querying data with offline-first approach export function useQuery(tableName, filter) { const [result, setResult] = useState({ data: [], isFromCache: true, }); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const fetchData = useCallback(async () => { if (!globalCacheManager) { setError("DataQL not initialized"); setLoading(false); return; } try { setLoading(true); setError(null); // First, get data from local cache const cacheResult = await globalCacheManager.queryOffline(tableName, filter); setResult(cacheResult); // If online, try to fetch fresh data from server if (globalSyncManager?.getOnlineStatus()) { try { const serverData = await globalSyncManager.fetchFromServer(tableName, filter); // Update local cache with server data // This would require additional cache manager methods for bulk updates setResult({ data: serverData, isFromCache: false, lastUpdated: new Date(), }); } catch (serverError) { // Keep using cached data if server fetch fails console.warn("Failed to fetch from server, using cached data:", serverError); } } } catch (err) { setError(err instanceof Error ? err.message : "Unknown error"); } finally { setLoading(false); } }, [tableName, filter]); useEffect(() => { fetchData(); }, [fetchData]); const refetch = useCallback(() => { fetchData(); }, [fetchData]); return { data: result.data, loading, error, refetch, isFromCache: result.isFromCache, lastUpdated: result.lastUpdated, }; } // DataQL-style live query hook with consistent signature export function useLiveQuery(tableName, filter) { if (!globalDbClient) { throw new Error("DataQL not initialized. Call dataQLClient.initialize() first."); } const db = globalDbClient.getDatabase(); // Query from the cachedData table that DataQL uses internally // This ensures consistency with other DataQL operations try { const { data, error, updatedAt } = useDrizzleLiveQuery(db.select().from(cachedData)); // Filter the data to match the requested table and filter const filteredData = (data || []) .filter((record) => record.tableName === tableName && !record.isDeleted) .map((record) => ({ id: record.itemId, ...record.data, _localId: record.id, _createdAt: record.createdAt, _updatedAt: record.updatedAt, })) .filter((item) => { if (!filter) return true; // Simple filter matching - can be enhanced return Object.keys(filter).every((key) => item[key] === filter[key]); }); return { data: filteredData, error: error ? error.message : null, updatedAt, isFromCache: true, }; } catch (err) { // Fallback to regular useQuery if live query fails console.warn("Live query failed, falling back to regular query:", err); return useQuery(tableName, filter); } } // Hook for mutations (create, update, delete) export function useMutation() { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const create = useCallback(async (tableName, data) => { if (!globalCacheManager) { return { success: false, error: "DataQL not initialized" }; } setLoading(true); setError(null); try { // Use the transparent method that handles offline/online automatically const result = await globalCacheManager.createOffline(tableName, data); if (!result.success) { setError(result.error || "Create failed"); } return result; } catch (err) { const errorMessage = err instanceof Error ? err.message : "Unknown error"; setError(errorMessage); return { success: false, error: errorMessage }; } finally { setLoading(false); } }, []); const update = useCallback(async (tableName, id, data) => { if (!globalCacheManager) { return { success: false, error: "DataQL not initialized" }; } setLoading(true); setError(null); try { // Use the transparent method that handles offline/online automatically const result = await globalCacheManager.updateOffline(tableName, id, data); if (!result.success) { setError(result.error || "Update failed"); } return result; } catch (err) { const errorMessage = err instanceof Error ? err.message : "Unknown error"; setError(errorMessage); return { success: false, error: errorMessage }; } finally { setLoading(false); } }, []); const upsert = useCallback(async (tableName, data) => { if (!globalCacheManager) { return { success: false, error: "DataQL not initialized" }; } setLoading(true); setError(null); try { // Use the transparent method that handles offline/online automatically const result = await globalCacheManager.upsertOffline(tableName, data); if (!result.success) { setError(result.error || "Upsert failed"); } return result; } catch (err) { const errorMessage = err instanceof Error ? err.message : "Unknown error"; setError(errorMessage); return { success: false, error: errorMessage }; } finally { setLoading(false); } }, []); const deleteRecord = useCallback(async (tableName, id) => { if (!globalCacheManager) { return { success: false, error: "DataQL not initialized" }; } setLoading(true); setError(null); try { // Use the transparent method that handles offline/online automatically const result = await globalCacheManager.deleteOffline(tableName, id); if (!result.success) { setError(result.error || "Delete failed"); } return result; } catch (err) { const errorMessage = err instanceof Error ? err.message : "Unknown error"; setError(errorMessage); return { success: false, error: errorMessage }; } finally { setLoading(false); } }, []); // SQL-style alias for create const insert = useCallback(async (tableName, data) => { return create(tableName, data); }, [create]); return { create, insert, update, upsert, delete: deleteRecord, loading, error, }; } // Hook for sync operations and status export function useSync() { const [syncStatus, setSyncStatus] = useState({ isOnline: true, lastSyncTime: null, pendingOperations: 0, failedOperations: 0, syncInProgress: false, }); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // Update sync status const updateSyncStatus = useCallback(async () => { if (!globalCacheManager) return; try { const status = await globalCacheManager.getSyncStatus(); setSyncStatus(status); } catch (err) { console.error("Error updating sync status:", err); } }, []); // Sync event listeners useEffect(() => { if (!globalSyncManager) return; const handleSyncEvent = (event) => { switch (event.type) { case "sync_start": setSyncStatus((prev) => ({ ...prev, syncInProgress: true })); setLoading(true); setError(null); break; case "sync_complete": setSyncStatus((prev) => ({ ...prev, syncInProgress: false, lastSyncTime: event.timestamp, })); setLoading(false); updateSyncStatus(); // Refresh pending operations count break; case "sync_error": setSyncStatus((prev) => ({ ...prev, syncInProgress: false })); setLoading(false); setError(event.error || "Sync error"); break; } }; globalSyncManager.addEventListener("sync_start", handleSyncEvent); globalSyncManager.addEventListener("sync_complete", handleSyncEvent); globalSyncManager.addEventListener("sync_error", handleSyncEvent); // Initial status update updateSyncStatus(); return () => { globalSyncManager?.removeEventListener("sync_start", handleSyncEvent); globalSyncManager?.removeEventListener("sync_complete", handleSyncEvent); globalSyncManager?.removeEventListener("sync_error", handleSyncEvent); }; }, [updateSyncStatus]); // Manual sync trigger const sync = useCallback(async () => { if (!globalSyncManager) { setError("DataQL not initialized"); return false; } setLoading(true); setError(null); try { const success = await globalSyncManager.syncNow(); if (!success) { setError("Sync failed"); } return success; } catch (err) { const errorMessage = err instanceof Error ? err.message : "Sync error"; setError(errorMessage); return false; } finally { setLoading(false); } }, []); // Start/stop auto sync const startAutoSync = useCallback(() => { globalSyncManager?.startAutoSync(); }, []); const stopAutoSync = useCallback(() => { globalSyncManager?.stopAutoSync(); }, []); return { syncStatus, sync, startAutoSync, stopAutoSync, loading, error, isOnline: globalSyncManager?.getOnlineStatus() ?? true, isSyncing: globalSyncManager?.isSyncInProgress() ?? false, }; } // Hook for network status export function useNetworkStatus() { const [isOnline, setIsOnline] = useState(navigator.onLine ?? true); useEffect(() => { const handleOnline = () => setIsOnline(true); const handleOffline = () => setIsOnline(false); if (typeof window !== "undefined") { window.addEventListener("online", handleOnline); window.addEventListener("offline", handleOffline); return () => { window.removeEventListener("online", handleOnline); window.removeEventListener("offline", handleOffline); }; } }, []); return { isOnline }; }