fix(web_serviceworker): Improve service worker persistence, metrics and caching robustness

This commit is contained in:
2025-12-04 15:33:47 +00:00
parent 5bc24ad88b
commit 8b7fe245f0
6 changed files with 48 additions and 5 deletions

View File

@@ -1,5 +1,15 @@
# Changelog
## 2025-12-04 - 7.4.1 - fix(web_serviceworker)
Improve service worker persistence, metrics and caching robustness
- Ensure persistent store is initialized before use in dashboard handlers and service worker activation/handlers (calls to persistentStore.init())
- Make serveCumulativeMetrics async and align fetchEvent.respondWith usage (remove unnecessary Promise.resolve)
- Change persistent WebStore database name to 'losslessServiceworkerPersistent' to separate durable store from runtime store
- Make PersistentStore.init() more resilient: add detailed logging, avoid throwing on init failure, mark initialized to prevent retry loops, and start periodic save only after load
- Ensure logEvent awaits initialization and adds defensive logging around reading/writing the event log
- Add request deduplication logic and improved cache handling in CacheManager (fetchWithDeduplication usage and safer respondWith)
## 2025-12-04 - 7.4.0 - feat(serviceworker)
Add persistent event store, cumulative metrics and dashboard events UI for service worker observability

View File

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

View File

@@ -237,7 +237,7 @@ export class CacheManager {
}
if (parsedUrl.pathname === '/sw-dash/cumulative-metrics') {
const dashboard = getDashboardGenerator();
fetchEventArg.respondWith(Promise.resolve(dashboard.serveCumulativeMetrics()));
fetchEventArg.respondWith(dashboard.serveCumulativeMetrics());
return;
}
// DELETE method for clearing events

View File

@@ -52,6 +52,7 @@ export class DashboardGenerator {
*/
public async serveEventLog(searchParams: URLSearchParams): Promise<Response> {
const persistentStore = getPersistentStore();
await persistentStore.init();
const limit = searchParams.get('limit') ? parseInt(searchParams.get('limit')!, 10) : undefined;
const type = searchParams.get('type') as TEventType | undefined;
@@ -72,6 +73,7 @@ export class DashboardGenerator {
*/
public async serveEventCount(searchParams: URLSearchParams): Promise<Response> {
const persistentStore = getPersistentStore();
await persistentStore.init();
const since = searchParams.get('since') ? parseInt(searchParams.get('since')!, 10) : Date.now() - 3600000; // Default: last hour
@@ -88,8 +90,9 @@ export class DashboardGenerator {
/**
* Serves cumulative metrics
*/
public serveCumulativeMetrics(): Response {
public async serveCumulativeMetrics(): Promise<Response> {
const persistentStore = getPersistentStore();
await persistentStore.init();
const metrics = persistentStore.getCumulativeMetrics();
return new Response(JSON.stringify(metrics), {
@@ -105,6 +108,7 @@ export class DashboardGenerator {
*/
public async clearEventLog(): Promise<Response> {
const persistentStore = getPersistentStore();
await persistentStore.init();
const success = await persistentStore.clearEventLog();
return new Response(JSON.stringify({ success }), {
@@ -126,6 +130,7 @@ export class DashboardGenerator {
public async runSpeedtest(): Promise<Response> {
const metrics = getMetricsCollector();
const persistentStore = getPersistentStore();
await persistentStore.init();
const results: {
latency?: { durationMs: number };
download?: { durationMs: number; speedMbps: number; bytesTransferred: number };

View File

@@ -66,7 +66,7 @@ export class PersistentStore {
private constructor() {
this.store = new plugins.webstore.WebStore({
dbName: 'losslessServiceworker',
dbName: 'losslessServiceworkerPersistent',
storeName: 'persistentStore',
});
}
@@ -85,19 +85,27 @@ export class PersistentStore {
* Initializes the store and starts periodic saving
*/
public async init(): Promise<void> {
console.log('[PersistentStore] init() called, initialized:', this.initialized);
if (this.initialized) {
console.log('[PersistentStore] Already initialized, returning early');
return;
}
try {
console.log('[PersistentStore] Calling store.init()...');
// Initialize the WebStore (required before using any methods)
await this.store.init();
console.log('[PersistentStore] store.init() completed successfully');
await this.loadCumulativeMetrics();
console.log('[PersistentStore] loadCumulativeMetrics() completed');
// Increment restart count
if (this.cumulativeMetrics) {
this.cumulativeMetrics.swRestartCount++;
this.isDirty = true;
await this.saveCumulativeMetrics();
console.log('[PersistentStore] Saved cumulative metrics after restart count increment');
}
// Start periodic save
@@ -105,9 +113,12 @@ export class PersistentStore {
this.initialized = true;
logger.log('ok', '[PersistentStore] Initialized successfully');
console.log('[PersistentStore] Initialization complete');
} catch (error) {
console.error('[PersistentStore] Failed to initialize:', error);
logger.log('error', `[PersistentStore] Failed to initialize: ${error}`);
throw error;
// Don't throw - allow SW to continue even if persistent store fails
this.initialized = true; // Mark as initialized to prevent retry loops
}
}
@@ -262,6 +273,7 @@ export class PersistentStore {
message: string,
details?: Record<string, any>
): Promise<void> {
console.log('[PersistentStore] logEvent called:', type, message);
const entry: IEventLogEntry = {
id: generateId(),
timestamp: Date.now(),
@@ -271,10 +283,21 @@ export class PersistentStore {
};
try {
// Ensure initialized
if (!this.initialized) {
console.log('[PersistentStore] Not initialized, calling init() first');
await this.init();
}
let events: IEventLogEntry[] = [];
console.log('[PersistentStore] Checking if event log exists...');
if (await this.store.check(this.EVENT_LOG_KEY)) {
console.log('[PersistentStore] Event log exists, loading...');
events = await this.store.get(this.EVENT_LOG_KEY);
console.log('[PersistentStore] Loaded', events.length, 'events');
} else {
console.log('[PersistentStore] Event log does not exist, creating new one');
}
// Add new entry
@@ -283,9 +306,12 @@ export class PersistentStore {
// Apply retention policy
events = this.applyRetentionPolicy(events);
console.log('[PersistentStore] Saving', events.length, 'events...');
await this.store.set(this.EVENT_LOG_KEY, events);
console.log('[PersistentStore] Events saved successfully');
logger.log('note', `[PersistentStore] Logged event: ${type} - ${message}`);
} catch (error) {
console.error('[PersistentStore] Failed to log event:', error);
logger.log('error', `[PersistentStore] Failed to log event: ${error}`);
}
}

View File

@@ -95,6 +95,7 @@ export class ServiceWorker {
// Log activation event
const persistentStore = getPersistentStore();
await persistentStore.init(); // Ensure store is initialized (safe to call multiple times)
await persistentStore.logEvent('sw_activated', 'Service worker activated', {
timestamp: new Date().toISOString(),
});
@@ -123,6 +124,7 @@ export class ServiceWorker {
// Log cache invalidation event (survives)
const persistentStore = getPersistentStore();
await persistentStore.init(); // Ensure store is initialized
await persistentStore.logEvent('cache_invalidated', `Cache invalidated: ${reqArg.reason}`, {
reason: reqArg.reason,
timestamp: reqArg.timestamp,