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;
|
||||
|
||||
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');
|
||||
@@ -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>
|
||||
<ModuleErrorBoundary>
|
||||
<RouterProvider
|
||||
router={router}
|
||||
future={{
|
||||
v7_startTransition: true,
|
||||
}}
|
||||
/>
|
||||
</ModuleErrorBoundary>
|
||||
</ThemeWrapper>
|
||||
</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 { 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 () => {
|
||||
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(() => {
|
||||
window.location.href = '/';
|
||||
}, 1500);
|
||||
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%',
|
||||
|
||||
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