serviceWorker Service
The serviceWorker
service provides comprehensive service worker functionality for offline-first Progressive Web Applications (PWAs). It handles request interception, parameter extraction, and automatic fallback to network requests when server-side rendering is not available.
Interface
{
// Lifecycle methods
start(): void,
// Request parameter extraction
extractParams(request: Request): Promise<object>,
extractUrl(request: Request): URL,
extractUrlParams(request: Request): object,
extractHeaders(request: Request): object,
extractBodyParams(request: Request): Promise<object>,
// Service properties
version: Promise<string>,
callHandler: CallHandler,
// Meta configuration
meta(): void // Configures service for client-side inclusion
}
Description
The serviceWorker
service is a specialized service that runs in the browser's service worker context to provide offline-first functionality. It intercepts network requests and attempts to handle them server-side using the same request handling pipeline that runs on the server, falling back to network requests when necessary.
Key Features
- Request Interception: Automatically intercepts all fetch requests in the service worker scope
- Parameter Extraction: Extracts URL parameters, headers, and body data from requests
- Server-Side Rendering: Attempts to render responses using the same callHandler pipeline as the server
- Graceful Fallback: Falls back to network requests when server-side rendering fails
- Version Management: Includes version tracking for cache busting and updates
- Client-Side Bundle: Automatically included in service worker bundles
Examples
Basic Service Worker Integration
// Automatically started in service worker context
// In pinstripe initialization, this runs automatically:
if(typeof window == 'undefined' && typeof addEventListener == 'function'){
Workspace.run(({ serviceWorker }) => serviceWorker.start());
}
Service Worker Bundle Generation
// Generate service worker JavaScript bundle
export default {
async render(){
const { js } = await this.bundler.build('serviceWorker');
return [200, {
'content-type': 'text/javascript'
}, [
`${js}\n//# sourceMappingURL=/service-worker.js.map`
]];
}
}
Service Worker Registration in HTML
// Automatically included in shell HTML
export default {
async render(){
const version = await this.version;
const urlSearchParams = new URLSearchParams({ version });
return this.renderHtml`
<!DOCTYPE html>
<html>
<head>
<meta name="pinstripe-service-worker-url"
content="/service_worker.js?${urlSearchParams}">
</head>
<body>
<!-- Content -->
</body>
</html>
`;
}
}
Client-Side Service Worker Registration
// Automatic registration when service worker is supported
if (typeof navigator != 'undefined' && "serviceWorker" in navigator) {
(async () => {
try {
let scriptUrl = Component.instanceFor(document).head
.find('meta[name="pinstripe-service-worker-url"]')?.params.content;
if(!scriptUrl) return;
const registration = await navigator.serviceWorker.getRegistration(scriptUrl);
if(registration) await registration.unregister();
await navigator.serviceWorker.register(scriptUrl, {
scope: "./",
updateViaCache: "none"
});
} catch (error) {
console.error(`Service worker registration failed with ${error}`);
}
})();
}
Custom Parameter Extraction
// Service worker with custom parameter handling
export default {
meta(){
this.addToClient();
},
create(){
return this;
},
async customRequestHandler(request){
// Extract parameters using serviceWorker methods
const params = await this.serviceWorker.extractParams(request);
const url = this.serviceWorker.extractUrl(request);
const headers = this.serviceWorker.extractHeaders(request);
// Custom processing
params.customData = await this.processRequest(request);
// Handle with callHandler
const [status, responseHeaders, body] = await this.callHandler.handleCall(params);
return new Response(body, { status, headers: responseHeaders });
}
}
Offline-First Request Handling
// The service worker automatically handles requests like this:
addEventListener("fetch", (event) => {
event.respondWith((async () => {
const request1 = event.request.clone();
const request2 = event.request.clone();
try {
// Extract parameters from request
const params = await this.extractParams(request1);
// Try to handle server-side
const [status, headers, body] = await this.callHandler.handleCall(params);
// Return server-side response if successful
if(status >= 200 && status < 300) {
return new Response(body, { status, headers });
}
// Fallback to network
return fetch(request2);
} catch (error) {
if(!(error instanceof MissingResourceError)) throw error;
console.log(error);
return fetch(request2);
}
})());
});
Version-Aware Service Worker
// Service worker with version logging
export default {
async connectedCallback(){
// Access version information
const version = await this.serviceWorker.version;
console.log(`Service worker version: ${version}`);
// Version is automatically used for cache busting
this.updateCacheStrategy(version);
}
}
Request Type Handling
// Handle different request types
export default {
async handleRequest(request){
const params = await this.serviceWorker.extractParams(request);
// params includes:
// - URL parameters from query string
// - Body parameters (form data, JSON)
// - _method: HTTP method
// - _url: parsed URL object
// - _headers: request headers
switch(params._method) {
case 'GET':
return this.handleGetRequest(params);
case 'POST':
return this.handlePostRequest(params);
case 'PUT':
return this.handlePutRequest(params);
default:
return this.handleGenericRequest(params);
}
}
}
Error Handling and Fallback
// Custom error handling in service worker context
export default {
async processWithFallback(request){
try {
const params = await this.serviceWorker.extractParams(request);
const [status, headers, body] = await this.callHandler.handleCall(params);
if(status >= 200 && status < 300) {
return new Response(body, { status, headers });
}
// Handle error responses
throw new Error(`Server returned ${status}`);
} catch (error) {
console.warn('Service worker handling failed:', error);
// Fallback strategies
if(navigator.onLine) {
return fetch(request);
} else {
return this.getCachedResponse(request) ||
new Response('Offline', { status: 503 });
}
}
}
}
Advanced Usage Patterns
Progressive Enhancement
The service worker automatically enhances applications with offline functionality without requiring changes to existing server-side code.
API Consistency
Requests handled by the service worker use the same parameter extraction and handling logic as server-side requests, ensuring consistent behavior.
Development vs Production
The service worker includes version information that automatically updates in development mode, enabling seamless updates during development.
Multi-Format Support
The service worker can handle various request formats:
- URL-encoded form data
- JSON payloads
- Multipart form data
- Query parameters
Implementation Details
- Automatic Registration: Service workers are automatically registered when supported
- Bundle Integration: Automatically included in
serviceWorker
bundle target - Version Cache Busting: Uses version service for automatic cache invalidation
- Graceful Degradation: Falls back to network requests when offline functionality isn't available
- Error Boundaries: Handles errors gracefully with network fallback
Use Cases
- Progressive Web Apps: Enable offline functionality for web applications
- Performance Optimization: Cache and serve frequently accessed content
- Network Resilience: Provide fallback responses when networks are unreliable
- Development Experience: Consistent behavior between server and client environments
- Static Site Generation: Use same rendering pipeline for static and dynamic content
Best Practices
- Always Provide Fallback: Ensure network requests work when service worker fails
- Version Management: Use the version service for proper cache invalidation
- Error Handling: Log errors appropriately but don't break user experience
- Performance: Be mindful of service worker overhead for simple requests
- Testing: Test both online and offline scenarios
Related Services
- bundler: Generates service worker JavaScript bundles
- callHandler: Processes requests using the same pipeline as the server
- version: Provides version information for cache busting
- environment: Determines runtime environment for conditional behavior
Limitations
- Browser Support: Only available in browsers that support service workers
- HTTPS Requirement: Service workers require HTTPS in production
- Scope Limitations: Service worker scope is limited by registration path
- Resource Constraints: Limited by browser memory and storage quotas
Bundle Integration
The service worker is automatically included in the serviceWorker
bundle target and can be built using:
const { js, map } = await this.bundler.build('serviceWorker');
This bundle includes all necessary services and handles the service worker lifecycle automatically.