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:
@@ -331,6 +331,13 @@ self.addEventListener('message', event => {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'LOGOUT':
|
||||||
|
// Special handler for logout - clear all caches and unregister
|
||||||
|
handleLogout().then(() => {
|
||||||
|
event.ports[0].postMessage({ success: true });
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.log('Service Worker: Unknown message type', type);
|
console.log('Service Worker: Unknown message type', type);
|
||||||
}
|
}
|
||||||
@@ -342,4 +349,19 @@ async function clearAllCaches() {
|
|||||||
console.log('Service Worker: All caches cleared');
|
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');
|
console.log('Service Worker: Loaded successfully');
|
||||||
@@ -5,6 +5,7 @@ import i18next from 'i18next';
|
|||||||
|
|
||||||
// Components
|
// Components
|
||||||
import ThemeWrapper from './features/theme/ThemeWrapper';
|
import ThemeWrapper from './features/theme/ThemeWrapper';
|
||||||
|
import ModuleErrorBoundary from './components/ModuleErrorBoundary';
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
import router from './app/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
|
// Register service worker
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
registerSW({
|
registerSW({
|
||||||
@@ -150,12 +205,14 @@ const App: React.FC = memo(() => {
|
|||||||
return (
|
return (
|
||||||
<Suspense fallback={<SuspenseFallback />}>
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
<ThemeWrapper>
|
<ThemeWrapper>
|
||||||
<RouterProvider
|
<ModuleErrorBoundary>
|
||||||
router={router}
|
<RouterProvider
|
||||||
future={{
|
router={router}
|
||||||
v7_startTransition: true,
|
future={{
|
||||||
}}
|
v7_startTransition: true,
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</ModuleErrorBoundary>
|
||||||
</ThemeWrapper>
|
</ThemeWrapper>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
110
worklenz-frontend/src/components/ModuleErrorBoundary.tsx
Normal file
110
worklenz-frontend/src/components/ModuleErrorBoundary.tsx
Normal 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;
|
||||||
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useAuthService } from '@/hooks/useAuth';
|
import { useAuthService } from '@/hooks/useAuth';
|
||||||
import { useMediaQuery } from 'react-responsive';
|
import { useMediaQuery } from 'react-responsive';
|
||||||
import { authApiService } from '@/api/auth/auth.api.service';
|
import { authApiService } from '@/api/auth/auth.api.service';
|
||||||
|
import CacheCleanup from '@/utils/cache-cleanup';
|
||||||
|
|
||||||
const LoggingOutPage = () => {
|
const LoggingOutPage = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -14,14 +15,30 @@ const LoggingOutPage = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
await auth.signOut();
|
try {
|
||||||
await authApiService.logout();
|
// Clear local session
|
||||||
setTimeout(() => {
|
await auth.signOut();
|
||||||
window.location.href = '/';
|
|
||||||
}, 1500);
|
// 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();
|
void logout();
|
||||||
}, [auth, navigate]);
|
}, [auth]);
|
||||||
|
|
||||||
const cardStyles = {
|
const cardStyles = {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|||||||
163
worklenz-frontend/src/utils/cache-cleanup.ts
Normal file
163
worklenz-frontend/src/utils/cache-cleanup.ts
Normal 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;
|
||||||
Reference in New Issue
Block a user