From 833879e0e8bfd2410541e3994bd454b57303ec71 Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Tue, 15 Jul 2025 16:08:07 +0530 Subject: [PATCH] feat(logout): implement cache cleanup and service worker unregistration on logout - Added a new utility, CacheCleanup, to handle clearing caches and unregistering the service worker during user logout. - Enhanced the LoggingOutPage to utilize CacheCleanup for clearing local session and caches before redirecting to the login page. - Introduced ModuleErrorBoundary to manage module loading errors, providing user feedback and options to retry or reload the application. - Updated App component to include global error handlers for improved error management related to module loading issues. --- worklenz-frontend/public/sw.js | 22 +++ worklenz-frontend/src/App.tsx | 69 +++++++- .../src/components/ModuleErrorBoundary.tsx | 110 ++++++++++++ .../src/pages/auth/logging-out.tsx | 29 +++- worklenz-frontend/src/utils/cache-cleanup.ts | 163 ++++++++++++++++++ 5 files changed, 381 insertions(+), 12 deletions(-) create mode 100644 worklenz-frontend/src/components/ModuleErrorBoundary.tsx create mode 100644 worklenz-frontend/src/utils/cache-cleanup.ts diff --git a/worklenz-frontend/public/sw.js b/worklenz-frontend/public/sw.js index 526541b0..15dbef76 100644 --- a/worklenz-frontend/public/sw.js +++ b/worklenz-frontend/public/sw.js @@ -331,6 +331,13 @@ self.addEventListener('message', event => { }); break; + case 'LOGOUT': + // Special handler for logout - clear all caches and unregister + handleLogout().then(() => { + event.ports[0].postMessage({ success: true }); + }); + break; + default: console.log('Service Worker: Unknown message type', type); } @@ -342,4 +349,19 @@ async function clearAllCaches() { console.log('Service Worker: All caches cleared'); } +async function handleLogout() { + try { + // Clear all caches + await clearAllCaches(); + + // Unregister the service worker to force fresh registration on next visit + await self.registration.unregister(); + + console.log('Service Worker: Logout handled - caches cleared and unregistered'); + } catch (error) { + console.error('Service Worker: Error during logout handling', error); + throw error; + } +} + console.log('Service Worker: Loaded successfully'); \ No newline at end of file diff --git a/worklenz-frontend/src/App.tsx b/worklenz-frontend/src/App.tsx index aa20e0ed..0f894c3a 100644 --- a/worklenz-frontend/src/App.tsx +++ b/worklenz-frontend/src/App.tsx @@ -5,6 +5,7 @@ import i18next from 'i18next'; // Components import ThemeWrapper from './features/theme/ThemeWrapper'; +import ModuleErrorBoundary from './components/ModuleErrorBoundary'; // Routes import router from './app/routes'; @@ -113,6 +114,60 @@ const App: React.FC = memo(() => { }; }, []); + // Global error handlers for module loading issues + useEffect(() => { + const handleUnhandledRejection = (event: PromiseRejectionEvent) => { + const error = event.reason; + + // Check if this is a module loading error + if ( + error?.message?.includes('Failed to fetch dynamically imported module') || + error?.message?.includes('Loading chunk') || + error?.name === 'ChunkLoadError' + ) { + console.error('Unhandled module loading error:', error); + event.preventDefault(); // Prevent default browser error handling + + // Clear caches and reload + import('./utils/cache-cleanup').then(({ default: CacheCleanup }) => { + CacheCleanup.clearAllCaches() + .then(() => CacheCleanup.forceReload('/auth/login')) + .catch(() => window.location.reload()); + }); + } + }; + + const handleError = (event: ErrorEvent) => { + const error = event.error; + + // Check if this is a module loading error + if ( + error?.message?.includes('Failed to fetch dynamically imported module') || + error?.message?.includes('Loading chunk') || + error?.name === 'ChunkLoadError' + ) { + console.error('Global module loading error:', error); + event.preventDefault(); // Prevent default browser error handling + + // Clear caches and reload + import('./utils/cache-cleanup').then(({ default: CacheCleanup }) => { + CacheCleanup.clearAllCaches() + .then(() => CacheCleanup.forceReload('/auth/login')) + .catch(() => window.location.reload()); + }); + } + }; + + // Add global error handlers + window.addEventListener('unhandledrejection', handleUnhandledRejection); + window.addEventListener('error', handleError); + + return () => { + window.removeEventListener('unhandledrejection', handleUnhandledRejection); + window.removeEventListener('error', handleError); + }; + }, []); + // Register service worker useEffect(() => { registerSW({ @@ -150,12 +205,14 @@ const App: React.FC = memo(() => { return ( }> - + + + ); diff --git a/worklenz-frontend/src/components/ModuleErrorBoundary.tsx b/worklenz-frontend/src/components/ModuleErrorBoundary.tsx new file mode 100644 index 00000000..64c3809b --- /dev/null +++ b/worklenz-frontend/src/components/ModuleErrorBoundary.tsx @@ -0,0 +1,110 @@ +import React, { Component, ErrorInfo, ReactNode } from 'react'; +import { Button, Result } from 'antd'; +import CacheCleanup from '@/utils/cache-cleanup'; + +interface Props { + children: ReactNode; +} + +interface State { + hasError: boolean; + error?: Error; +} + +class ModuleErrorBoundary extends Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error): State { + // Check if this is a module loading error + const isModuleError = + error.message.includes('Failed to fetch dynamically imported module') || + error.message.includes('Loading chunk') || + error.message.includes('Loading CSS chunk') || + error.name === 'ChunkLoadError'; + + if (isModuleError) { + return { hasError: true, error }; + } + + // For other errors, let them bubble up + return { hasError: false }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('Module Error Boundary caught an error:', error, errorInfo); + + // If this is a module loading error, clear caches and reload + if (this.state.hasError) { + this.handleModuleError(); + } + } + + private async handleModuleError() { + try { + console.log('Handling module loading error - clearing caches...'); + + // Clear all caches + await CacheCleanup.clearAllCaches(); + + // Force reload to login page + CacheCleanup.forceReload('/auth/login'); + } catch (cacheError) { + console.error('Failed to handle module error:', cacheError); + // Fallback: just reload the page + window.location.reload(); + } + } + + private handleRetry = async () => { + try { + await CacheCleanup.clearAllCaches(); + CacheCleanup.forceReload('/auth/login'); + } catch (error) { + console.error('Retry failed:', error); + window.location.reload(); + } + }; + + render() { + if (this.state.hasError) { + return ( +
+ + Retry + , + + ]} + /> +
+ ); + } + + return this.props.children; + } +} + +export default ModuleErrorBoundary; \ No newline at end of file diff --git a/worklenz-frontend/src/pages/auth/logging-out.tsx b/worklenz-frontend/src/pages/auth/logging-out.tsx index f4464b8a..c5e94c25 100644 --- a/worklenz-frontend/src/pages/auth/logging-out.tsx +++ b/worklenz-frontend/src/pages/auth/logging-out.tsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'; import { useAuthService } from '@/hooks/useAuth'; import { useMediaQuery } from 'react-responsive'; import { authApiService } from '@/api/auth/auth.api.service'; +import CacheCleanup from '@/utils/cache-cleanup'; const LoggingOutPage = () => { const navigate = useNavigate(); @@ -14,14 +15,30 @@ const LoggingOutPage = () => { useEffect(() => { const logout = async () => { - await auth.signOut(); - await authApiService.logout(); - setTimeout(() => { - window.location.href = '/'; - }, 1500); + try { + // Clear local session + await auth.signOut(); + + // Call backend logout + await authApiService.logout(); + + // Clear all caches using the utility + await CacheCleanup.clearAllCaches(); + + // Force a hard reload to ensure fresh state + setTimeout(() => { + CacheCleanup.forceReload('/auth/login'); + }, 1000); + + } catch (error) { + console.error('Logout error:', error); + // Fallback: force reload to login page + CacheCleanup.forceReload('/auth/login'); + } }; + void logout(); - }, [auth, navigate]); + }, [auth]); const cardStyles = { width: '100%', diff --git a/worklenz-frontend/src/utils/cache-cleanup.ts b/worklenz-frontend/src/utils/cache-cleanup.ts new file mode 100644 index 00000000..9f1313c6 --- /dev/null +++ b/worklenz-frontend/src/utils/cache-cleanup.ts @@ -0,0 +1,163 @@ +/** + * Cache cleanup utilities for logout operations + * Handles clearing of various caches to prevent stale data issues + */ + +export class CacheCleanup { + /** + * Clear all caches including service worker, browser cache, and storage + */ + static async clearAllCaches(): Promise { + try { + console.log('CacheCleanup: Starting cache clearing process...'); + + // Clear browser caches + if ('caches' in window) { + const cacheNames = await caches.keys(); + console.log('CacheCleanup: Found caches:', cacheNames); + + await Promise.all( + cacheNames.map(async cacheName => { + const deleted = await caches.delete(cacheName); + console.log(`CacheCleanup: Deleted cache "${cacheName}":`, deleted); + return deleted; + }) + ); + console.log('CacheCleanup: Browser caches cleared'); + } else { + console.log('CacheCleanup: Cache API not supported'); + } + + // Clear service worker cache + if ('serviceWorker' in navigator) { + const registration = await navigator.serviceWorker.getRegistration(); + if (registration) { + console.log('CacheCleanup: Found service worker registration'); + + // Send logout message to service worker to clear its caches and unregister + if (registration.active) { + try { + console.log('CacheCleanup: Sending LOGOUT message to service worker...'); + await this.sendMessageToServiceWorker('LOGOUT'); + console.log('CacheCleanup: LOGOUT message sent successfully'); + } catch (error) { + console.warn('CacheCleanup: Failed to send logout message to service worker:', error); + // Fallback: try to clear cache manually + try { + console.log('CacheCleanup: Trying fallback CLEAR_CACHE message...'); + await this.sendMessageToServiceWorker('CLEAR_CACHE'); + console.log('CacheCleanup: CLEAR_CACHE message sent successfully'); + } catch (fallbackError) { + console.warn('CacheCleanup: Failed to clear service worker cache:', fallbackError); + } + } + } + + // If service worker is still registered, unregister it + if (registration.active) { + console.log('CacheCleanup: Unregistering service worker...'); + await registration.unregister(); + console.log('CacheCleanup: Service worker unregistered'); + } + } else { + console.log('CacheCleanup: No service worker registration found'); + } + } else { + console.log('CacheCleanup: Service Worker not supported'); + } + + // Clear localStorage and sessionStorage + const localStorageKeys = Object.keys(localStorage); + const sessionStorageKeys = Object.keys(sessionStorage); + + console.log('CacheCleanup: Clearing localStorage keys:', localStorageKeys); + console.log('CacheCleanup: Clearing sessionStorage keys:', sessionStorageKeys); + + localStorage.clear(); + sessionStorage.clear(); + console.log('CacheCleanup: Local storage cleared'); + + console.log('CacheCleanup: Cache clearing process completed successfully'); + + } catch (error) { + console.error('CacheCleanup: Error clearing caches:', error); + throw error; + } + } + + /** + * Send message to service worker + */ + private static async sendMessageToServiceWorker(type: string, payload?: any): Promise { + if (!('serviceWorker' in navigator)) { + throw new Error('Service Worker not supported'); + } + + const registration = await navigator.serviceWorker.getRegistration(); + if (!registration || !registration.active) { + throw new Error('Service Worker not active'); + } + + return new Promise((resolve, reject) => { + const messageChannel = new MessageChannel(); + + messageChannel.port1.onmessage = (event) => { + if (event.data.error) { + reject(event.data.error); + } else { + resolve(event.data); + } + }; + + registration.active!.postMessage( + { type, payload }, + [messageChannel.port2] + ); + + // Timeout after 5 seconds + setTimeout(() => { + reject(new Error('Service Worker message timeout')); + }, 5000); + }); + } + + /** + * Force reload the page to ensure fresh state + */ + static forceReload(url: string = '/auth/login'): void { + // Use replace to prevent back button issues + window.location.replace(url); + } + + /** + * Clear specific cache types + */ + static async clearSpecificCaches(cacheTypes: string[]): Promise { + if (!('caches' in window)) return; + + const cacheNames = await caches.keys(); + const cachesToDelete = cacheNames.filter(name => + cacheTypes.some(type => name.includes(type)) + ); + + await Promise.all( + cachesToDelete.map(cacheName => caches.delete(cacheName)) + ); + } + + /** + * Clear API cache specifically + */ + static async clearAPICache(): Promise { + await this.clearSpecificCaches(['api', 'dynamic']); + } + + /** + * Clear static asset cache + */ + static async clearStaticCache(): Promise { + await this.clearSpecificCaches(['static', 'images']); + } +} + +export default CacheCleanup; \ No newline at end of file