Initial Commit
This commit is contained in:
254
frontend/src/core/sync/data-sync.ts
Normal file
254
frontend/src/core/sync/data-sync.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
/**
|
||||
* @ai-summary Data synchronization layer integrating React Query with Zustand stores
|
||||
*/
|
||||
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
import { useNavigationStore } from '../store/navigation';
|
||||
import { useUserStore } from '../store/user';
|
||||
import { Vehicle } from '../../features/vehicles/types/vehicles.types';
|
||||
|
||||
interface SyncConfig {
|
||||
enableCrossTabs: boolean;
|
||||
enableOptimisticUpdates: boolean;
|
||||
enableBackgroundSync: boolean;
|
||||
syncInterval: number;
|
||||
}
|
||||
|
||||
export class DataSyncManager {
|
||||
private queryClient: QueryClient;
|
||||
private config: SyncConfig;
|
||||
private syncInterval?: NodeJS.Timeout;
|
||||
private isOnline: boolean = navigator.onLine;
|
||||
|
||||
constructor(queryClient: QueryClient, config: Partial<SyncConfig> = {}) {
|
||||
this.queryClient = queryClient;
|
||||
this.config = {
|
||||
enableCrossTabs: true,
|
||||
enableOptimisticUpdates: true,
|
||||
enableBackgroundSync: true,
|
||||
syncInterval: 30000, // 30 seconds
|
||||
...config,
|
||||
};
|
||||
|
||||
this.initializeSync();
|
||||
}
|
||||
|
||||
private initializeSync() {
|
||||
// Listen to online/offline events
|
||||
window.addEventListener('online', this.handleOnline.bind(this));
|
||||
window.addEventListener('offline', this.handleOffline.bind(this));
|
||||
|
||||
// Cross-tab synchronization
|
||||
if (this.config.enableCrossTabs) {
|
||||
this.initializeCrossTabSync();
|
||||
}
|
||||
|
||||
// Background sync
|
||||
if (this.config.enableBackgroundSync) {
|
||||
this.startBackgroundSync();
|
||||
}
|
||||
}
|
||||
|
||||
private handleOnline() {
|
||||
this.isOnline = true;
|
||||
useUserStore.getState().setOnlineStatus(true);
|
||||
|
||||
// Trigger cache revalidation when coming back online
|
||||
this.queryClient.invalidateQueries();
|
||||
console.log('DataSync: Back online, revalidating cache');
|
||||
}
|
||||
|
||||
private handleOffline() {
|
||||
this.isOnline = false;
|
||||
useUserStore.getState().setOnlineStatus(false);
|
||||
console.log('DataSync: Offline mode enabled');
|
||||
}
|
||||
|
||||
private initializeCrossTabSync() {
|
||||
// Listen for storage changes from other tabs
|
||||
window.addEventListener('storage', (event) => {
|
||||
if (event.key?.startsWith('motovaultpro-')) {
|
||||
// Another tab updated store data
|
||||
if (event.key.includes('user-context')) {
|
||||
// User data changed in another tab - sync React Query cache
|
||||
this.syncUserDataFromStorage();
|
||||
} else if (event.key.includes('mobile-navigation')) {
|
||||
// Navigation state changed - could affect cache keys
|
||||
this.syncNavigationFromStorage();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async syncUserDataFromStorage() {
|
||||
try {
|
||||
const userData = useUserStore.getState().userProfile;
|
||||
if (userData) {
|
||||
// Update query cache with latest user data
|
||||
this.queryClient.setQueryData(['user', userData.id], userData);
|
||||
console.log('DataSync: User data synchronized from another tab');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('DataSync: Failed to sync user data from storage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async syncNavigationFromStorage() {
|
||||
try {
|
||||
const navigationState = useNavigationStore.getState();
|
||||
|
||||
// If the selected vehicle changed in another tab, preload its data
|
||||
if (navigationState.selectedVehicleId) {
|
||||
await this.queryClient.prefetchQuery({
|
||||
queryKey: ['vehicles', navigationState.selectedVehicleId],
|
||||
queryFn: () => this.fetchVehicleById(navigationState.selectedVehicleId!),
|
||||
});
|
||||
console.log('DataSync: Vehicle data preloaded from navigation sync');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('DataSync: Failed to sync navigation from storage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private startBackgroundSync() {
|
||||
this.syncInterval = setInterval(() => {
|
||||
if (this.isOnline) {
|
||||
this.performBackgroundSync();
|
||||
}
|
||||
}, this.config.syncInterval);
|
||||
}
|
||||
|
||||
private async performBackgroundSync() {
|
||||
try {
|
||||
// Update last sync timestamp
|
||||
useUserStore.getState().updateLastSync();
|
||||
|
||||
// Strategically refresh critical data
|
||||
const navigationState = useNavigationStore.getState();
|
||||
|
||||
// If on vehicles screen, refresh vehicles data
|
||||
if (navigationState.activeScreen === 'Vehicles') {
|
||||
await this.queryClient.invalidateQueries({ queryKey: ['vehicles'] });
|
||||
}
|
||||
|
||||
// If viewing specific vehicle, refresh its data
|
||||
if (navigationState.selectedVehicleId) {
|
||||
await this.queryClient.invalidateQueries({
|
||||
queryKey: ['vehicles', navigationState.selectedVehicleId]
|
||||
});
|
||||
}
|
||||
|
||||
console.log('DataSync: Background sync completed');
|
||||
} catch (error) {
|
||||
console.warn('DataSync: Background sync failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to fetch vehicle by ID (would normally import from vehicles API)
|
||||
private async fetchVehicleById(id: string): Promise<Vehicle | null> {
|
||||
try {
|
||||
const response = await fetch(`/api/vehicles/${id}`, {
|
||||
headers: {
|
||||
'Authorization': this.getAuthHeader(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.warn(`Failed to fetch vehicle ${id}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private getAuthHeader(): string {
|
||||
// This would integrate with Auth0 token from interceptor
|
||||
// For now, return empty string as token is handled by axios interceptor
|
||||
return '';
|
||||
}
|
||||
|
||||
// Public methods for optimistic updates
|
||||
public async optimisticVehicleUpdate(vehicleId: string, updates: Partial<Vehicle>) {
|
||||
if (!this.config.enableOptimisticUpdates) return;
|
||||
|
||||
try {
|
||||
// Optimistically update query cache
|
||||
this.queryClient.setQueryData(['vehicles', vehicleId], (old: Vehicle | undefined) => {
|
||||
if (!old) return old;
|
||||
return { ...old, ...updates };
|
||||
});
|
||||
|
||||
// Also update the vehicles list cache
|
||||
this.queryClient.setQueryData(['vehicles'], (old: Vehicle[] | undefined) => {
|
||||
if (!old) return old;
|
||||
return old.map(vehicle =>
|
||||
vehicle.id === vehicleId ? { ...vehicle, ...updates } : vehicle
|
||||
);
|
||||
});
|
||||
|
||||
console.log('DataSync: Optimistic vehicle update applied');
|
||||
} catch (error) {
|
||||
console.warn('DataSync: Optimistic update failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
public async prefetchForNavigation(targetScreen: string) {
|
||||
try {
|
||||
switch (targetScreen) {
|
||||
case 'Vehicles':
|
||||
// Prefetch vehicles list if not already cached
|
||||
await this.queryClient.prefetchQuery({
|
||||
queryKey: ['vehicles'],
|
||||
queryFn: () => this.fetchVehicles(),
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
});
|
||||
break;
|
||||
|
||||
case 'Log Fuel':
|
||||
// Prefetch vehicles for fuel logging dropdown
|
||||
await this.queryClient.prefetchQuery({
|
||||
queryKey: ['vehicles'],
|
||||
queryFn: () => this.fetchVehicles(),
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
// No specific prefetching needed
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('DataSync: Prefetch failed for', targetScreen, error);
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchVehicles(): Promise<Vehicle[]> {
|
||||
try {
|
||||
const response = await fetch('/api/vehicles', {
|
||||
headers: {
|
||||
'Authorization': this.getAuthHeader(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch vehicles:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public cleanup() {
|
||||
if (this.syncInterval) {
|
||||
clearInterval(this.syncInterval);
|
||||
}
|
||||
|
||||
window.removeEventListener('online', this.handleOnline);
|
||||
window.removeEventListener('offline', this.handleOffline);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user