// ── useSyncHook ─────────────────────────────────────────────── // React hook for cross-platform sync with platform-service // Auto-syncs on interval, merges remote changes into Zustand store 'use client'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useTimerStore } from './store'; import { useRoutineStore } from './routine-store'; import { fullSync, isSyncEnabled, setSyncEnabled as _setSyncEnabled, isAuthenticated, setAuthToken, loadOfflineQueue, dtoToTimerPatch, dtoToRoutinePatch, type SyncResult, type SyncConflict, } from './platform-sync'; import type { Timer } from './timer-engine'; import type { Routine } from './routines'; const SYNC_INTERVAL_MS = 60_000; // 1 minute export interface UseSyncReturn { isSyncing: boolean; syncEnabled: boolean; lastSyncDate: string | null; pendingChanges: number; lastError: string | null; conflicts: SyncConflict[]; // Actions syncNow: () => Promise; setSyncEnabled: (enabled: boolean) => void; login: (token: string) => void; logout: () => void; } export function useSync(): UseSyncReturn { const [isSyncing, setIsSyncing] = useState(false); const [syncEnabled, setSyncEnabledState] = useState(false); const [lastSyncDate, setLastSyncDate] = useState(null); const [pendingChanges, setPendingChanges] = useState(0); const [lastError, setLastError] = useState(null); const [conflicts, setConflicts] = useState([]); const intervalRef = useRef | null>(null); // Load initial state useEffect(() => { setSyncEnabledState(isSyncEnabled()); setPendingChanges(loadOfflineQueue().length); setLastSyncDate( typeof window !== 'undefined' ? localStorage.getItem('chronomind-platform-last-sync') : null ); }, []); // Perform sync and merge results into store const syncNow = useCallback(async (): Promise => { setIsSyncing(true); setLastError(null); try { const result = await fullSync(); if (result.error) { setLastError(result.error); } // Merge pulled timers into Zustand store if (result.pulled.length > 0) { const store = useTimerStore.getState(); const currentTimers = [...store.timers]; for (const dto of result.pulled) { const patch = dtoToTimerPatch(dto); const existingIdx = currentTimers.findIndex((t) => t.id === dto.id); if (existingIdx >= 0) { // Merge: remote wins if syncVersion >= local currentTimers[existingIdx] = { ...currentTimers[existingIdx], ...patch, } as Timer; } else { // New timer from remote — add it currentTimers.push(patch as Timer); } } useTimerStore.setState({ timers: currentTimers }); } // Merge pulled routines into routine store if (result.pulledRoutines.length > 0) { const routineStore = useRoutineStore.getState(); const currentRoutines = [...routineStore.routines]; for (const dto of result.pulledRoutines) { const patch = dtoToRoutinePatch(dto); const existingIdx = currentRoutines.findIndex((r) => r.id === dto.id); if (existingIdx >= 0) { currentRoutines[existingIdx] = { ...currentRoutines[existingIdx], ...patch, } as Routine; } else { currentRoutines.push(patch as Routine); } } useRoutineStore.setState({ routines: currentRoutines }); } if (result.conflicts.length > 0) { setConflicts((prev) => [...prev, ...result.conflicts]); } setPendingChanges(loadOfflineQueue().length); setLastSyncDate( typeof window !== 'undefined' ? localStorage.getItem('chronomind-platform-last-sync') : null ); return result; } finally { setIsSyncing(false); } }, []); // Auto-sync interval useEffect(() => { if (syncEnabled && isAuthenticated()) { // Initial sync on enable syncNow(); intervalRef.current = setInterval(() => { syncNow(); }, SYNC_INTERVAL_MS); } return () => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }; }, [syncEnabled, syncNow]); const setSyncEnabled = useCallback( (enabled: boolean) => { _setSyncEnabled(enabled); setSyncEnabledState(enabled); }, [] ); const login = useCallback( (token: string) => { setAuthToken(token); setSyncEnabled(true); }, [setSyncEnabled] ); const logout = useCallback(() => { setAuthToken(null); setSyncEnabled(false); setConflicts([]); }, [setSyncEnabled]); return { isSyncing, syncEnabled, lastSyncDate, pendingChanges, lastError, conflicts, syncNow, setSyncEnabled, login, logout, }; }