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.
This commit is contained in:
chamikaJ
2025-07-15 16:08:07 +05:30
parent cb5610d99b
commit 833879e0e8
5 changed files with 381 additions and 12 deletions

View File

@@ -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');

View File

@@ -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 (
<Suspense fallback={<SuspenseFallback />}>
<ThemeWrapper>
<RouterProvider
router={router}
future={{
v7_startTransition: true,
}}
/>
<ModuleErrorBoundary>
<RouterProvider
router={router}
future={{
v7_startTransition: true,
}}
/>
</ModuleErrorBoundary>
</ThemeWrapper>
</Suspense>
);

View File

@@ -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<Props, State> {
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 (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '100vh',
padding: '20px'
}}>
<Result
status="error"
title="Module Loading Error"
subTitle="There was an issue loading the application. This usually happens after updates or during logout."
extra={[
<Button
type="primary"
key="retry"
onClick={this.handleRetry}
loading={false}
>
Retry
</Button>,
<Button
key="reload"
onClick={() => window.location.reload()}
>
Reload Page
</Button>
]}
/>
</div>
);
}
return this.props.children;
}
}
export default ModuleErrorBoundary;

View File

@@ -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%',

View File

@@ -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<void> {
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<any> {
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<void> {
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<void> {
await this.clearSpecificCaches(['api', 'dynamic']);
}
/**
* Clear static asset cache
*/
static async clearStaticCache(): Promise<void> {
await this.clearSpecificCaches(['static', 'images']);
}
}
export default CacheCleanup;