feat(serviceworker): Add real-time service worker push updates and DeesComms integration (metrics, events, resource caching)
This commit is contained in:
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user