feat(pwa): implement service worker and PWA enhancements

- Added service worker (sw.js) for offline functionality, caching strategies, and performance improvements.
- Registered service worker in App component to manage updates and offline readiness.
- Introduced ServiceWorkerStatus component to display connection status and provide cache management controls.
- Created manifest.json for PWA configuration, including app name, icons, and display settings.
- Updated index.html with PWA meta tags and links to support mobile web app capabilities.
- Refactored authentication guards to utilize useAuthStatus hook for improved user state management.
- Removed deprecated unregister-sw.js file to streamline service worker management.
This commit is contained in:
chamiakJ
2025-07-10 14:07:03 +05:30
parent bb8e6ee60f
commit bcfa18b1e8
16 changed files with 1238 additions and 187 deletions

View File

@@ -0,0 +1,259 @@
# Service Worker Implementation
This directory contains the service worker implementation for Worklenz, providing offline functionality, caching, and performance improvements.
## Files Overview
- **`sw.js`** (in `/public/`) - The main service worker file
- **`serviceWorkerRegistration.ts`** - Registration and management utilities
- **`ServiceWorkerStatus.tsx`** (in `/components/`) - React component for SW status
## Features
### 🔄 Caching Strategies
1. **Cache First** - Static assets (JS, CSS, images)
- Serves from cache first, falls back to network
- Perfect for unchanging resources
2. **Network First** - API requests
- Tries network first, falls back to cache
- Ensures fresh data when online
3. **Stale While Revalidate** - HTML pages
- Serves cached version immediately
- Updates cache in background
### 📱 PWA Features
- **Offline Support** - App works without internet
- **Installable** - Can be installed on devices
- **Background Sync** - Sync data when online (framework ready)
- **Push Notifications** - Real-time notifications (framework ready)
## Usage
### Basic Integration
The service worker is automatically registered in `App.tsx`:
```tsx
import { registerSW } from './utils/serviceWorkerRegistration';
useEffect(() => {
registerSW({
onSuccess: (registration) => {
console.log('SW registered successfully');
},
onUpdate: (registration) => {
// Show update notification to user
},
onOfflineReady: () => {
console.log('App ready for offline use');
}
});
}, []);
```
### Using the Hook
```tsx
import { useServiceWorker } from '../utils/serviceWorkerRegistration';
const MyComponent = () => {
const { isOffline, swManager, clearCache, forceUpdate } = useServiceWorker();
return (
<div>
<p>Status: {isOffline ? 'Offline' : 'Online'}</p>
<button onClick={clearCache}>Clear Cache</button>
<button onClick={forceUpdate}>Update App</button>
</div>
);
};
```
### Status Component
```tsx
import ServiceWorkerStatus from '../components/service-worker-status/ServiceWorkerStatus';
// Minimal offline indicator
<ServiceWorkerStatus minimal />
// Full status with controls
<ServiceWorkerStatus showControls />
```
## Configuration
### Cacheable Resources
Edit the patterns in `sw.js`:
```javascript
// API endpoints that can be cached
const CACHEABLE_API_PATTERNS = [
/\/api\/project-categories/,
/\/api\/task-statuses/,
// Add more patterns...
];
// Resources that should never be cached
const NEVER_CACHE_PATTERNS = [
/\/api\/auth\/login/,
/\/socket\.io/,
// Add more patterns...
];
```
### Cache Names
Update version to force cache refresh:
```javascript
const CACHE_VERSION = 'v1.0.1'; // Increment when deploying
```
## Development
### Testing Offline
1. Open DevTools → Application → Service Workers
2. Check "Offline" to simulate offline mode
3. Verify app still functions
### Debugging
```javascript
// Check service worker status
navigator.serviceWorker.ready.then(registration => {
console.log('SW ready:', registration);
});
// Check cache contents
caches.keys().then(names => {
console.log('Cache names:', names);
});
```
### Cache Management
```javascript
// Clear all caches
caches.keys().then(names =>
Promise.all(names.map(name => caches.delete(name)))
);
// Clear specific cache
caches.delete('worklenz-api-v1.0.0');
```
## Best Practices
### 1. Cache Strategy Selection
- **Static Assets**: Cache First (fast loading)
- **API Data**: Network First (fresh data)
- **User Content**: Network Only (always fresh)
- **App Shell**: Cache First (instant loading)
### 2. Cache Invalidation
- Increment `CACHE_VERSION` when deploying
- Use versioned URLs for assets
- Set appropriate cache headers
### 3. Offline UX
- Show offline indicators
- Queue actions for later sync
- Provide meaningful offline messages
- Cache critical user data
### 4. Performance
- Cache only necessary resources
- Set cache size limits
- Clean up old caches regularly
- Monitor cache usage
## Monitoring
### Storage Usage
```javascript
// Check storage quota
navigator.storage.estimate().then(estimate => {
console.log('Used:', estimate.usage);
console.log('Quota:', estimate.quota);
});
```
### Cache Hit Rate
Monitor in DevTools → Network:
- Look for "from ServiceWorker" requests
- Check cache effectiveness
## Troubleshooting
### Common Issues
1. **SW not updating**
- Hard refresh (Ctrl+Shift+R)
- Clear browser cache
- Check CACHE_VERSION
2. **Resources not caching**
- Verify URL patterns
- Check NEVER_CACHE_PATTERNS
- Ensure HTTPS in production
3. **Offline features not working**
- Verify SW registration
- Check browser support
- Test cache strategies
### Reset Service Worker
```javascript
// Unregister and reload
navigator.serviceWorker.getRegistrations().then(registrations => {
registrations.forEach(registration => registration.unregister());
window.location.reload();
});
```
## Browser Support
- ✅ Chrome 45+
- ✅ Firefox 44+
- ✅ Safari 11.1+
- ✅ Edge 17+
- ❌ Internet Explorer
## Future Enhancements
1. **Background Sync**
- Queue offline actions
- Sync when online
2. **Push Notifications**
- Task assignments
- Project updates
- Deadline reminders
3. **Advanced Caching**
- Intelligent prefetching
- ML-based cache eviction
- Compression
4. **Offline Analytics**
- Track offline usage
- Cache hit rates
- Performance metrics
---
*Last updated: January 2025*

View File

@@ -0,0 +1,273 @@
// Service Worker Registration Utility
// Handles registration, updates, and error handling
import React from 'react';
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
window.location.hostname === '[::1]' ||
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
);
type Config = {
onSuccess?: (registration: ServiceWorkerRegistration) => void;
onUpdate?: (registration: ServiceWorkerRegistration) => void;
onOfflineReady?: () => void;
onError?: (error: Error) => void;
};
export function registerSW(config?: Config) {
if ('serviceWorker' in navigator) {
// Only register in production or when explicitly testing
const swUrl = '/sw.js';
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
} else {
console.log('Service workers are not supported in this browser.');
}
}
function registerValidSW(swUrl: string, config?: Config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
console.log('Service Worker registered successfully:', registration);
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
if (config && config.onOfflineReady) {
config.onOfflineReady();
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
if (config && config.onError) {
config.onError(error);
}
});
}
function checkValidServiceWorker(swUrl: string, config?: Config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' },
})
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregisterSW() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then(registration => {
registration.unregister();
console.log('Service Worker unregistered successfully');
})
.catch(error => {
console.error('Error during service worker unregistration:', error);
});
}
}
// Utility to communicate with service worker
export class ServiceWorkerManager {
private registration: ServiceWorkerRegistration | null = null;
constructor(registration?: ServiceWorkerRegistration) {
this.registration = registration || null;
}
// Send message to service worker
async sendMessage(type: string, payload?: any): Promise<any> {
if (!this.registration || !this.registration.active) {
throw new Error('Service Worker not available');
}
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);
}
};
this.registration!.active!.postMessage(
{ type, payload },
[messageChannel.port2]
);
// Timeout after 5 seconds
setTimeout(() => {
reject(new Error('Service Worker message timeout'));
}, 5000);
});
}
// Get service worker version
async getVersion(): Promise<string> {
try {
const response = await this.sendMessage('GET_VERSION');
return response.version;
} catch (error) {
console.error('Failed to get service worker version:', error);
return 'unknown';
}
}
// Clear all caches
async clearCache(): Promise<boolean> {
try {
await this.sendMessage('CLEAR_CACHE');
return true;
} catch (error) {
console.error('Failed to clear cache:', error);
return false;
}
}
// Force update service worker
async forceUpdate(): Promise<void> {
if (!this.registration) return;
try {
await this.registration.update();
await this.sendMessage('SKIP_WAITING');
window.location.reload();
} catch (error) {
console.error('Failed to force update service worker:', error);
throw error;
}
}
// Check if app is running offline
isOffline(): boolean {
return !navigator.onLine;
}
// Get cache storage estimate
async getCacheSize(): Promise<StorageEstimate | null> {
if ('storage' in navigator && 'estimate' in navigator.storage) {
try {
return await navigator.storage.estimate();
} catch (error) {
console.error('Failed to get storage estimate:', error);
}
}
return null;
}
}
// Hook to use service worker in React components
export function useServiceWorker() {
const [isOffline, setIsOffline] = React.useState(!navigator.onLine);
const [swManager, setSWManager] = React.useState<ServiceWorkerManager | null>(null);
React.useEffect(() => {
const handleOnline = () => setIsOffline(false);
const handleOffline = () => setIsOffline(true);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
// Register service worker
registerSW({
onSuccess: (registration) => {
setSWManager(new ServiceWorkerManager(registration));
},
onUpdate: (registration) => {
// You could show a toast here asking user to refresh
console.log('New version available');
setSWManager(new ServiceWorkerManager(registration));
},
onOfflineReady: () => {
console.log('App ready for offline use');
}
});
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return {
isOffline,
swManager,
clearCache: () => swManager?.clearCache(),
forceUpdate: () => swManager?.forceUpdate(),
getVersion: () => swManager?.getVersion(),
};
}