Files
maternal-app/maternal-web/store/middleware/offlineMiddleware.ts
Andrei 7cb2ff97de
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled
feat: Implement offline-first Redux architecture with optimistic updates
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>
2025-10-01 19:24:46 +00:00

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);
};
};