fix(serviceworker): Improve error handling and logging in service worker backend and network manager; update multiple dependency versions and packageManager settings.

This commit is contained in:
Philipp Kunz 2025-04-11 09:45:41 +00:00
parent 42e8e575d8
commit 84f7d8d4a0
7 changed files with 2015 additions and 1582 deletions

View File

@ -1,5 +1,14 @@
# Changelog
## 2025-04-11 - 3.0.71 - fix(serviceworker)
Improve error handling and logging in service worker backend and network manager; update multiple dependency versions and packageManager settings.
- Upgrade dependency versions in package.json (e.g. @cloudflare/workers-types, @push.rocks/smartfile, @push.rocks/smartpromise, @push.rocks/smartrequest, @tsclass/tsclass, and @types/express)
- Add packageManager field to package.json
- Enhance error handling in ServiceworkerBackend (using try/catch and detailed logging) during client reload, notification display, and alert message sending
- Improve network request handling by clearing timeouts and converting errors reliably in NetworkManager
- Wrap service worker install and activate event handlers with try/catch to log errors appropriately
## 2025-03-16 - 3.0.70 - fix(TypedServer)
Improve error handling in server startup and response buffering. Validate configuration for reload injections, wrap file watching and TypedSocket initialization in try/catch blocks, enhance client notification and stop procedures, and ensure proper Buffer conversion in the proxy handler.

View File

@ -61,14 +61,14 @@
"@api.global/typedrequest": "^3.1.10",
"@api.global/typedrequest-interfaces": "^3.0.19",
"@api.global/typedsocket": "^3.0.1",
"@cloudflare/workers-types": "^4.20241224.0",
"@cloudflare/workers-types": "^4.20250410.0",
"@design.estate/dees-comms": "^1.0.27",
"@push.rocks/lik": "^6.1.0",
"@push.rocks/smartchok": "^1.0.34",
"@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartenv": "^5.0.12",
"@push.rocks/smartfeed": "^1.0.11",
"@push.rocks/smartfile": "^11.0.23",
"@push.rocks/smartfile": "^11.2.0",
"@push.rocks/smartjson": "^5.0.20",
"@push.rocks/smartlog": "^3.0.7",
"@push.rocks/smartlog-destination-devtools": "^1.0.12",
@ -79,8 +79,8 @@
"@push.rocks/smartntml": "^2.0.8",
"@push.rocks/smartopen": "^2.0.0",
"@push.rocks/smartpath": "^5.0.18",
"@push.rocks/smartpromise": "^4.0.4",
"@push.rocks/smartrequest": "^2.0.23",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrequest": "^2.1.0",
"@push.rocks/smartrx": "^3.0.7",
"@push.rocks/smartsitemap": "^2.0.3",
"@push.rocks/smartstream": "^3.2.5",
@ -88,8 +88,8 @@
"@push.rocks/taskbuffer": "^3.1.7",
"@push.rocks/webrequest": "^3.0.37",
"@push.rocks/webstore": "^2.0.20",
"@tsclass/tsclass": "^4.2.0",
"@types/express": "^4.17.21",
"@tsclass/tsclass": "^8.2.0",
"@types/express": "^5.0.1",
"body-parser": "^1.20.3",
"cors": "^2.8.5",
"express": "^4.21.2",
@ -97,15 +97,16 @@
"lit": "^3.2.1"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.2.0",
"@git.zone/tsbundle": "^2.1.0",
"@git.zone/tsbuild": "^2.3.2",
"@git.zone/tsbundle": "^2.2.5",
"@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^1.0.90",
"@push.rocks/tapbundle": "^5.5.3",
"@types/node": "^22.10.2"
"@git.zone/tstest": "^1.0.96",
"@push.rocks/tapbundle": "^5.6.3",
"@types/node": "^22.14.0"
},
"private": false,
"browserslist": [
"last 1 chrome versions"
]
],
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
}

3388
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@api.global/typedserver',
version: '3.0.70',
version: '3.0.71',
description: 'A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.'
}

View File

@ -1,5 +1,40 @@
import * as plugins from './plugins.js';
import * as interfaces from '../dist_ts_interfaces/index.js';
import { logger } from './logging.js';
// Add type definitions for ServiceWorker APIs
declare global {
interface ServiceWorkerGlobalScope extends EventTarget {
clients: Clients;
registration: ServiceWorkerRegistration;
}
// Define Clients interface
interface Clients {
matchAll(options?: ClientQueryOptions): Promise<Client[]>;
openWindow(url: string): Promise<WindowClient>;
claim(): Promise<void>;
get(id: string): Promise<Client | undefined>;
}
interface ClientQueryOptions {
includeUncontrolled?: boolean;
type?: 'window' | 'worker' | 'sharedworker' | 'all';
}
interface Client {
id: string;
type: 'window' | 'worker' | 'sharedworker';
url: string;
}
interface WindowClient extends Client {
focused: boolean;
visibilityState: 'hidden' | 'visible' | 'prerender' | 'unloaded';
focus(): Promise<WindowClient>;
navigate(url: string): Promise<WindowClient>;
}
}
/**
* This class is meant to be used only on the backend side
@ -34,7 +69,33 @@ export class ServiceworkerBackend {
* reloads all clients
*/
public async triggerReloadAll() {
try {
logger.log('info', 'Triggering reload for all clients due to new version');
// Send update message via DeesComms
// This will be picked up by clients that have registered a handler for 'serviceworker_newVersion'
await this.deesComms.postMessage({
method: 'serviceworker_newVersion',
request: {},
messageId: `sw_update_${Date.now()}`
});
// As a fallback, also use the clients API to reload clients that might not catch the broadcast
// We need to type-cast self since TypeScript doesn't recognize ServiceWorker API
const swSelf = self as unknown as ServiceWorkerGlobalScope;
const clients = await swSelf.clients.matchAll({ type: 'window' });
logger.log('info', `Found ${clients.length} clients to reload`);
for (const client of clients) {
if ('navigate' in client) {
// For modern browsers, navigate to the same URL to trigger reload
(client as any).navigate(client.url);
logger.log('info', `Navigated client to: ${client.url}`);
}
}
} catch (error) {
logger.log('error', `Failed to reload clients: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
@ -44,10 +105,51 @@ export class ServiceworkerBackend {
title: string;
body: string;
}) {
try {
// Check if we have permission to show notifications
const permission = self.Notification?.permission;
if (permission !== 'granted') {
logger.log('warn', `Cannot show notification: permission is ${permission}`);
return;
}
// Type-cast self to ServiceWorkerGlobalScope
const swSelf = self as unknown as ServiceWorkerGlobalScope;
// Use type assertion for notification options to include vibrate
const options = {
body: notificationArg.body,
icon: '/favicon.ico', // Assuming there's a favicon
badge: '/favicon.ico',
vibrate: [200, 100, 200]
} as NotificationOptions;
await swSelf.registration.showNotification(notificationArg.title, options);
logger.log('info', `Notification shown: ${notificationArg.title}`);
} catch (error) {
logger.log('error', `Failed to show notification: ${error instanceof Error ? error.message : String(error)}`);
}
}
public async alert(alertText: string) {
// Since we can't directly show alerts from service worker context,
// we'll use notifications as a fallback
await this.addNotification({
title: 'Alert',
body: alertText
});
// Send message to clients who might be able to show an actual alert
try {
await this.deesComms.postMessage({
method: 'serviceworker_alert',
request: { message: alertText },
messageId: `sw_alert_${Date.now()}`
});
logger.log('info', `Alert message sent to clients: ${alertText}`);
} catch (error) {
logger.log('error', `Failed to send alert to clients: ${error instanceof Error ? error.message : String(error)}`);
}
}
}

View File

@ -96,32 +96,56 @@ export class NetworkManager {
backoffMs = 1000
} = options;
let lastError: Error;
let lastError: Error | unknown;
for (let i = 0; i <= retries; i++) {
let timeoutId: number | undefined;
const controller = new AbortController();
try {
const isOnline = await this.checkOnlineStatus();
if (!isOnline) {
throw new Error('Device is offline');
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
// Set up timeout
timeoutId = setTimeout(() => controller.abort(), timeoutMs) as unknown as number;
const response = await fetch(request, {
...typeof request === 'string' ? {} : request,
signal: controller.signal
});
// Clear timeout on successful response
clearTimeout(timeoutId);
return response;
} catch (error) {
// Always clear timeout, even on error
if (timeoutId) {
clearTimeout(timeoutId);
}
lastError = error;
logger.log('warn', `Request attempt ${i+1}/${retries+1} failed: ${error instanceof Error ? error.message : String(error)}`);
// Check if this was an abort error (timeout)
if (error instanceof Error && error.name === 'AbortError') {
logger.log('warn', `Request timed out after ${timeoutMs}ms`);
}
// Retry with backoff if we have retries left
if (i < retries) {
await new Promise(resolve => setTimeout(resolve, backoffMs * (i + 1)));
const backoffTime = backoffMs * (i + 1);
logger.log('info', `Retrying in ${backoffTime}ms...`);
await new Promise(resolve => setTimeout(resolve, backoffTime));
}
}
}
throw lastError;
// Convert lastError to Error if it isn't already
const finalError = lastError instanceof Error
? lastError
: new Error(typeof lastError === 'string' ? lastError : 'Unknown error during request');
throw finalError;
}
}
}

View File

@ -57,10 +57,15 @@ export class ServiceWorker {
const done = new Deferred();
event.waitUntil(done.promise);
// its important to not go async before event.waitUntil
done.resolve();
logger.log('success', `service worker installed! TimeStamp = ${new Date().toISOString()}`);
selfArg.skipWaiting();
logger.log('note', `Called skip waiting!`);
try {
logger.log('success', `service worker installed! TimeStamp = ${new Date().toISOString()}`);
selfArg.skipWaiting();
logger.log('note', `Called skip waiting!`);
done.resolve();
} catch (error) {
logger.log('error', `Service worker installation error: ${error}`);
done.reject(error);
}
});
this.serviceWindowRef.addEventListener('activate', async (event: interfaces.ServiceEvent) => {
@ -68,9 +73,19 @@ export class ServiceWorker {
event.waitUntil(done.promise);
// its important to not go async before event.waitUntil
await selfArg.clients.claim();
await this.cacheManager.cleanCaches('new service worker loaded! :)');
done.resolve();
try {
await selfArg.clients.claim();
logger.log('ok', 'Clients claimed successfully');
await this.cacheManager.cleanCaches('new service worker loaded! :)');
logger.log('ok', 'Caches cleaned successfully');
done.resolve();
logger.log('success', `Service worker activated at ${new Date().toISOString()}`);
} catch (error) {
logger.log('error', `Service worker activation error: ${error}`);
done.reject(error);
}
});
}
}
}