import { Middleware } from '@reduxjs/toolkit'; import { RootState } from '../store'; import { removePendingAction, incrementRetryCount, setSyncInProgress, updateLastSyncTime } from '../slices/offlineSlice'; import { setOnlineStatus } from '../slices/networkSlice'; /** * Sync middleware - processes pending actions when coming back online */ export const syncMiddleware: Middleware<{}, RootState> = (store) => (next) => (action: any) => { // Check if we just came back online if (action.type === 'network/setOnlineStatus' && action.payload === true) { const state = store.getState(); const pendingActions = state.offline?.pendingActions || []; if (pendingActions.length > 0) { console.log(`[Sync] Processing ${pendingActions.length} pending actions`); store.dispatch(setSyncInProgress(true)); // Process pending actions sequentially processPendingActions(store, pendingActions); } } return next(action); }; /** * Process pending actions with retry logic */ async function processPendingActions(store: any, pendingActions: any[]) { const MAX_RETRIES = 5; for (const pendingAction of pendingActions) { try { console.log(`[Sync] Processing action: ${pendingAction.type}`, pendingAction); // Reconstruct the action const action = { type: pendingAction.type, payload: pendingAction.payload, }; // Dispatch the action (which will make the API call) await store.dispatch(action); // If successful, remove from queue store.dispatch(removePendingAction(pendingAction.id)); console.log(`[Sync] Successfully synced action: ${pendingAction.type}`); } catch (error) { console.error(`[Sync] Failed to sync action: ${pendingAction.type}`, error); // Increment retry count store.dispatch(incrementRetryCount(pendingAction.id)); // If max retries reached, remove from queue if (pendingAction.retryCount >= MAX_RETRIES) { console.error(`[Sync] Max retries reached for action: ${pendingAction.type}`); store.dispatch(removePendingAction(pendingAction.id)); } } } // Mark sync as complete store.dispatch(setSyncInProgress(false)); store.dispatch(updateLastSyncTime()); console.log('[Sync] Sync complete'); } /** * Conflict resolution strategies */ export enum ConflictStrategy { SERVER_WINS = 'server_wins', CLIENT_WINS = 'client_wins', LAST_WRITE_WINS = 'last_write_wins', MERGE = 'merge', } /** * Resolve conflicts between local and server data */ export function resolveConflict( localData: T, serverData: T, strategy: ConflictStrategy = ConflictStrategy.LAST_WRITE_WINS ): T { switch (strategy) { case ConflictStrategy.SERVER_WINS: return serverData; case ConflictStrategy.CLIENT_WINS: return localData; case ConflictStrategy.LAST_WRITE_WINS: // Compare timestamps const localTime = localData.updatedAt ? new Date(localData.updatedAt).getTime() : 0; const serverTime = serverData.updatedAt ? new Date(serverData.updatedAt).getTime() : 0; return serverTime > localTime ? serverData : localData; case ConflictStrategy.MERGE: // Simple merge strategy - prefer server for system fields, local for user fields return { ...serverData, ...localData, // System fields from server id: serverData.id, createdAt: serverData.createdAt, updatedAt: serverData.updatedAt, // Metadata _version: Math.max(localData._version || 0, serverData._version || 0) + 1, }; default: return serverData; } } /** * Version-based conflict detection */ export function hasConflict( localData: T, serverData: T ): boolean { if (!localData._version || !serverData._version) { return false; } return localData._version !== serverData._version; }