Implemented comprehensive offline-first state management: Redux Store Setup: - Configure Redux Toolkit with @redux-offline/redux-offline - Setup redux-persist with IndexedDB (localforage) - Custom offline config with exponential backoff retry - Normalized state with entity adapters State Slices: - activitiesSlice: Normalized activities with optimistic CRUD - childrenSlice: Normalized children with optimistic CRUD - networkSlice: Network status and connection quality - offlineSlice: Sync queue and pending actions Middleware: - offlineMiddleware: Queue actions when offline - syncMiddleware: Process pending actions when online - Conflict resolution strategies (SERVER_WINS, CLIENT_WINS, LAST_WRITE_WINS, MERGE) - Version-based conflict detection Features: - Optimistic updates for immediate UI feedback - Automatic sync queue with retry logic (5 retries max) - Network detection (browser events + periodic checks) - Connection quality monitoring (excellent/good/poor/offline) - Latency tracking - Conflict resolution with multiple strategies - Entity versioning for optimistic updates Components: - NetworkStatusIndicator: Full-screen status banner - NetworkStatusBadge: Compact app bar badge - ReduxProvider: Provider with network detection setup Custom Hooks: - useAppDispatch/useAppSelector: Typed Redux hooks - useIsOnline: Check online status - useHasPendingSync: Check for pending actions - useSyncStatus: Get sync progress info - useOptimisticAction: Combine optimistic + actual actions - useNetworkQuality: Get connection quality - useIsOptimistic: Check if entity is being synced Documentation: - Comprehensive README with usage examples - Architecture overview - Best practices guide - Troubleshooting section State Structure: - Normalized entities with byId/allIds - Optimistic metadata (_optimistic, _localId, _version) - Entity adapters with memoized selectors - Offline queue persistence to IndexedDB 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
93 lines
2.6 KiB
TypeScript
93 lines
2.6 KiB
TypeScript
import { Middleware } from '@reduxjs/toolkit';
|
|
import { RootState } from '../store';
|
|
import { setOnlineStatus } from '../slices/networkSlice';
|
|
import { addPendingAction, removePendingAction, incrementRetryCount } from '../slices/offlineSlice';
|
|
|
|
/**
|
|
* Offline middleware - intercepts actions and queues them when offline
|
|
*/
|
|
export const offlineMiddleware: Middleware<{}, RootState> = (store) => (next) => (action: any) => {
|
|
const state = store.getState();
|
|
const isOnline = state.network?.isOnline ?? true;
|
|
|
|
// Check if this is an async action that should be queued
|
|
const isOfflineableAction =
|
|
action.type?.includes('/create') ||
|
|
action.type?.includes('/update') ||
|
|
action.type?.includes('/delete');
|
|
|
|
// If offline and action should be queued
|
|
if (!isOnline && isOfflineableAction && action.meta?.requestId) {
|
|
console.log('[Offline Middleware] Queuing action:', action.type);
|
|
|
|
// Queue the action
|
|
store.dispatch(
|
|
addPendingAction({
|
|
type: action.type,
|
|
payload: action.payload,
|
|
})
|
|
);
|
|
|
|
// Still process the optimistic update
|
|
return next(action);
|
|
}
|
|
|
|
// If online, process normally
|
|
return next(action);
|
|
};
|
|
|
|
/**
|
|
* Network status detector - listens for online/offline events
|
|
*/
|
|
export const setupNetworkDetection = (dispatch: any) => {
|
|
if (typeof window === 'undefined') return;
|
|
|
|
const handleOnline = () => {
|
|
console.log('[Network] Connection restored');
|
|
dispatch(setOnlineStatus(true));
|
|
};
|
|
|
|
const handleOffline = () => {
|
|
console.log('[Network] Connection lost');
|
|
dispatch(setOnlineStatus(false));
|
|
};
|
|
|
|
// Listen to browser online/offline events
|
|
window.addEventListener('online', handleOnline);
|
|
window.addEventListener('offline', handleOffline);
|
|
|
|
// Periodic connectivity check
|
|
const checkConnectivity = async () => {
|
|
try {
|
|
const startTime = Date.now();
|
|
const response = await fetch('/api/health', {
|
|
method: 'HEAD',
|
|
cache: 'no-cache',
|
|
});
|
|
const latency = Date.now() - startTime;
|
|
|
|
if (response.ok) {
|
|
dispatch(setOnlineStatus(true));
|
|
// You could also dispatch latency here
|
|
} else {
|
|
dispatch(setOnlineStatus(false));
|
|
}
|
|
} catch (error) {
|
|
dispatch(setOnlineStatus(false));
|
|
}
|
|
};
|
|
|
|
// Check connectivity every 30 seconds
|
|
const intervalId = setInterval(checkConnectivity, 30000);
|
|
|
|
// Initial check
|
|
checkConnectivity();
|
|
|
|
// Cleanup function
|
|
return () => {
|
|
window.removeEventListener('online', handleOnline);
|
|
window.removeEventListener('offline', handleOffline);
|
|
clearInterval(intervalId);
|
|
};
|
|
};
|