feat(serviceworker): Add real-time service worker push updates and DeesComms integration (metrics, events, resource caching)

This commit is contained in:
2025-12-04 16:25:51 +00:00
parent 951a48cf88
commit 299e3ac33f
10 changed files with 438 additions and 40 deletions

View File

@@ -47,6 +47,11 @@ export class ServiceworkerBackend {
private swSelf: ServiceWorkerGlobalScope;
private clientUpdateInterval: ReturnType<typeof setInterval> | null = null;
// Throttling for metrics updates (max 1 per 500ms)
private metricsUpdateThrottle: ReturnType<typeof setTimeout> | null = null;
private pendingMetricsUpdate = false;
private readonly METRICS_THROTTLE_MS = 500;
constructor(optionsArg: {
self: any;
purgeCache: (reqArg: interfaces.serviceworker.IRequest_PurgeServiceWorkerCache['request']) => Promise<interfaces.serviceworker.IRequest_PurgeServiceWorkerCache['response']>;
@@ -352,7 +357,7 @@ export class ServiceworkerBackend {
title: 'Alert',
body: alertText
});
// Send message to clients who might be able to show an actual alert
try {
await this.deesComms.postMessage({
@@ -365,4 +370,104 @@ export class ServiceworkerBackend {
logger.log('error', `Failed to send alert to clients: ${error instanceof Error ? error.message : String(error)}`);
}
}
// ===============
// Push methods for real-time updates
// ===============
/**
* Pushes a new event log entry to all connected clients
* Called immediately when an event is logged
*/
public async pushEvent(entry: interfaces.serviceworker.IEventLogEntry): Promise<void> {
try {
await this.deesComms.postMessage({
method: 'serviceworker_eventLogged',
request: entry,
messageId: `sw_event_${entry.id}`
});
logger.log('note', `Pushed event to clients: ${entry.type}`);
} catch (error) {
logger.log('warn', `Failed to push event: ${error}`);
}
}
/**
* Pushes a metrics update to all connected clients
* Throttled to max 1 update per 500ms to prevent spam
*/
public pushMetricsUpdate(): void {
// Mark that we have a pending update
this.pendingMetricsUpdate = true;
// If we're already throttling, just wait for the next window
if (this.metricsUpdateThrottle) {
return;
}
// Send the update and start throttle window
this.sendMetricsUpdate();
this.metricsUpdateThrottle = setTimeout(() => {
this.metricsUpdateThrottle = null;
// If there was a pending update during the throttle window, send it now
if (this.pendingMetricsUpdate) {
this.sendMetricsUpdate();
}
}, this.METRICS_THROTTLE_MS);
}
/**
* Actually sends the metrics update via DeesComms
*/
private async sendMetricsUpdate(): Promise<void> {
this.pendingMetricsUpdate = false;
const metrics = getMetricsCollector();
const metricsData = metrics.getMetrics();
const snapshot: interfaces.serviceworker.IMetricsSnapshot = {
cache: {
hits: metricsData.cache.hits,
misses: metricsData.cache.misses,
errors: metricsData.cache.errors,
bytesServedFromCache: metricsData.cache.bytesServedFromCache,
bytesFetched: metricsData.cache.bytesFetched,
},
network: {
totalRequests: metricsData.network.totalRequests,
successfulRequests: metricsData.network.successfulRequests,
failedRequests: metricsData.network.failedRequests,
},
cacheHitRate: metrics.getCacheHitRate(),
networkSuccessRate: metrics.getNetworkSuccessRate(),
resourceCount: metrics.getResourceCount(),
uptime: metricsData.uptime,
timestamp: Date.now(),
};
try {
await this.deesComms.postMessage({
method: 'serviceworker_metricsUpdate',
request: snapshot,
messageId: `sw_metrics_${Date.now()}`
});
} catch (error) {
logger.log('warn', `Failed to push metrics update: ${error}`);
}
}
/**
* Pushes notification when a resource is cached
*/
public async pushResourceCached(url: string, contentType: string, size: number, cached: boolean): Promise<void> {
try {
await this.deesComms.postMessage({
method: 'serviceworker_resourceCached',
request: { url, contentType, size, cached },
messageId: `sw_resource_${Date.now()}`
});
} catch (error) {
logger.log('warn', `Failed to push resource cached: ${error}`);
}
}
}

View File

@@ -1,4 +1,5 @@
import { logger } from './logging.js';
import { getServiceWorkerBackend } from './init.js';
/**
* Interface for cache metrics
@@ -178,6 +179,20 @@ export class MetricsCollector {
this.startTime = Date.now();
}
/**
* Triggers a push metrics update to all connected clients (throttled in backend)
*/
private triggerPushUpdate(): void {
try {
const backend = getServiceWorkerBackend();
if (backend) {
backend.pushMetricsUpdate();
}
} catch (error) {
// Silently ignore - push is best-effort
}
}
/**
* Gets the singleton instance
*/
@@ -196,11 +211,13 @@ export class MetricsCollector {
this.cacheHits++;
this.bytesServedFromCache += bytes;
logger.log('note', `[Metrics] Cache hit: ${url} (${bytes} bytes)`);
this.triggerPushUpdate();
}
public recordCacheMiss(url: string): void {
this.cacheMisses++;
logger.log('note', `[Metrics] Cache miss: ${url}`);
this.triggerPushUpdate();
}
public recordCacheError(url: string, error?: string): void {
@@ -224,11 +241,13 @@ export class MetricsCollector {
this.successfulRequests++;
this.totalBytesTransferred += bytes;
this.recordResponseTime(url, duration);
this.triggerPushUpdate();
}
public recordRequestFailure(url: string, error?: string): void {
this.failedRequests++;
logger.log('warn', `[Metrics] Request failed: ${url} - ${error || 'unknown'}`);
this.triggerPushUpdate();
}
public recordTimeout(url: string, duration: number): void {

View File

@@ -1,6 +1,7 @@
import * as plugins from './plugins.js';
import { logger } from './logging.js';
import type { serviceworker } from '../dist_ts_interfaces/index.js';
import { getServiceWorkerBackend } from './init.js';
type ICumulativeMetrics = serviceworker.ICumulativeMetrics;
type IEventLogEntry = serviceworker.IEventLogEntry;
@@ -85,27 +86,20 @@ 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
@@ -113,9 +107,7 @@ 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}`);
// Don't throw - allow SW to continue even if persistent store fails
this.initialized = true; // Mark as initialized to prevent retry loops
@@ -273,7 +265,6 @@ 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(),
@@ -285,19 +276,13 @@ 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
@@ -306,12 +291,20 @@ 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}`);
// Push event to connected clients via DeesComms
try {
const backend = getServiceWorkerBackend();
if (backend) {
await backend.pushEvent(entry);
}
} catch (pushError) {
// Don't fail the log operation if push fails
logger.log('warn', `[PersistentStore] Failed to push event: ${pushError}`);
}
} catch (error) {
console.error('[PersistentStore] Failed to log event:', error);
logger.log('error', `[PersistentStore] Failed to log event: ${error}`);
}
}

View File

@@ -4,7 +4,10 @@ import * as env from './env.js';
declare var self: env.ServiceWindow;
import { ServiceWorker } from './classes.serviceworker.js';
import type { ServiceworkerBackend } from './classes.backend.js';
const sw = new ServiceWorker(self);
export const getServiceWorkerInstance = (): ServiceWorker => sw;
export const getServiceWorkerBackend = (): ServiceworkerBackend => sw.leleServiceWorkerBackend;