feat(serviceworker): Add real-time service worker push updates and DeesComms integration (metrics, events, resource caching)
This commit is contained in:
@@ -3,6 +3,9 @@ import { LitElement, html, css } from 'lit';
|
||||
import type { CSSResult, TemplateResult } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
|
||||
// DeesComms for push communication
|
||||
import * as deesComms from '@design.estate/dees-comms';
|
||||
|
||||
export {
|
||||
LitElement,
|
||||
html,
|
||||
@@ -10,6 +13,7 @@ export {
|
||||
customElement,
|
||||
property,
|
||||
state,
|
||||
deesComms,
|
||||
};
|
||||
|
||||
export type { CSSResult, TemplateResult };
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { LitElement, html, css, state, customElement } from './plugins.js';
|
||||
import { LitElement, html, css, state, customElement, deesComms } from './plugins.js';
|
||||
import type { CSSResult, TemplateResult } from './plugins.js';
|
||||
import { sharedStyles, terminalStyles, navStyles } from './sw-dash-styles.js';
|
||||
import type { IMetricsData } from './sw-dash-overview.js';
|
||||
import type { ICachedResource } from './sw-dash-urls.js';
|
||||
import type { IDomainStats } from './sw-dash-domains.js';
|
||||
import type { IContentTypeStats } from './sw-dash-types.js';
|
||||
import type { serviceworker } from '../dist_ts_interfaces/index.js';
|
||||
|
||||
// Import components to register them
|
||||
import './sw-dash-overview.js';
|
||||
@@ -134,39 +135,194 @@ export class SwDashApp extends LitElement {
|
||||
resourceCount: 0
|
||||
};
|
||||
@state() accessor lastRefresh = new Date().toLocaleTimeString();
|
||||
@state() accessor isConnected = false;
|
||||
|
||||
private refreshInterval: ReturnType<typeof setInterval> | null = null;
|
||||
// DeesComms for receiving push updates from service worker
|
||||
private comms: deesComms.DeesComms | null = null;
|
||||
|
||||
// Heartbeat interval (30 seconds) for SW health check
|
||||
private heartbeatInterval: ReturnType<typeof setInterval> | null = null;
|
||||
private readonly HEARTBEAT_INTERVAL_MS = 30000;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.loadMetrics();
|
||||
this.loadResourceData();
|
||||
// Auto-refresh every 2 seconds
|
||||
this.refreshInterval = setInterval(() => {
|
||||
this.loadMetrics();
|
||||
if (this.currentView !== 'overview') {
|
||||
this.loadResourceData();
|
||||
}
|
||||
}, 2000);
|
||||
// Initial HTTP seed request to wake up SW and get initial data
|
||||
this.loadInitialData();
|
||||
// Setup push listeners via DeesComms
|
||||
this.setupPushListeners();
|
||||
// Start heartbeat for SW health check
|
||||
this.startHeartbeat();
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval);
|
||||
if (this.heartbeatInterval) {
|
||||
clearInterval(this.heartbeatInterval);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadMetrics(): Promise<void> {
|
||||
/**
|
||||
* Initial HTTP request to seed data and wake up service worker
|
||||
*/
|
||||
private async loadInitialData(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch('/sw-dash/metrics');
|
||||
this.metrics = await response.json();
|
||||
// Fetch metrics (wakes up SW)
|
||||
const metricsResponse = await fetch('/sw-dash/metrics');
|
||||
this.metrics = await metricsResponse.json();
|
||||
this.lastRefresh = new Date().toLocaleTimeString();
|
||||
this.isConnected = true;
|
||||
|
||||
// Also load resources
|
||||
const resourcesResponse = await fetch('/sw-dash/resources');
|
||||
this.resourceData = await resourcesResponse.json();
|
||||
} catch (err) {
|
||||
console.error('Failed to load metrics:', err);
|
||||
console.error('Failed to load initial data:', err);
|
||||
this.isConnected = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup DeesComms handlers for receiving push updates
|
||||
*/
|
||||
private setupPushListeners(): void {
|
||||
this.comms = new deesComms.DeesComms();
|
||||
|
||||
// Handle metrics push updates
|
||||
this.comms.createTypedHandler<serviceworker.IMessage_Serviceworker_MetricsUpdate>(
|
||||
'serviceworker_metricsUpdate',
|
||||
async (snapshot) => {
|
||||
// Update metrics from push
|
||||
if (this.metrics) {
|
||||
this.metrics = {
|
||||
...this.metrics,
|
||||
cache: {
|
||||
...this.metrics.cache,
|
||||
hits: snapshot.cache.hits,
|
||||
misses: snapshot.cache.misses,
|
||||
errors: snapshot.cache.errors,
|
||||
bytesServedFromCache: snapshot.cache.bytesServedFromCache,
|
||||
bytesFetched: snapshot.cache.bytesFetched,
|
||||
},
|
||||
network: {
|
||||
...this.metrics.network,
|
||||
totalRequests: snapshot.network.totalRequests,
|
||||
successfulRequests: snapshot.network.successfulRequests,
|
||||
failedRequests: snapshot.network.failedRequests,
|
||||
},
|
||||
cacheHitRate: snapshot.cacheHitRate,
|
||||
networkSuccessRate: snapshot.networkSuccessRate,
|
||||
resourceCount: snapshot.resourceCount,
|
||||
uptime: snapshot.uptime,
|
||||
};
|
||||
} else {
|
||||
// If no metrics yet, create minimal structure
|
||||
this.metrics = {
|
||||
cache: {
|
||||
hits: snapshot.cache.hits,
|
||||
misses: snapshot.cache.misses,
|
||||
errors: snapshot.cache.errors,
|
||||
bytesServedFromCache: snapshot.cache.bytesServedFromCache,
|
||||
bytesFetched: snapshot.cache.bytesFetched,
|
||||
averageResponseTime: 0,
|
||||
},
|
||||
network: {
|
||||
totalRequests: snapshot.network.totalRequests,
|
||||
successfulRequests: snapshot.network.successfulRequests,
|
||||
failedRequests: snapshot.network.failedRequests,
|
||||
timeouts: 0,
|
||||
averageLatency: 0,
|
||||
totalBytesTransferred: 0,
|
||||
},
|
||||
update: {
|
||||
totalChecks: 0,
|
||||
successfulChecks: 0,
|
||||
failedChecks: 0,
|
||||
updatesFound: 0,
|
||||
updatesApplied: 0,
|
||||
lastCheckTimestamp: 0,
|
||||
lastUpdateTimestamp: 0,
|
||||
},
|
||||
connection: {
|
||||
connectedClients: 0,
|
||||
totalConnectionAttempts: 0,
|
||||
successfulConnections: 0,
|
||||
failedConnections: 0,
|
||||
},
|
||||
speedtest: {
|
||||
lastDownloadSpeedMbps: 0,
|
||||
lastUploadSpeedMbps: 0,
|
||||
lastLatencyMs: 0,
|
||||
lastTestTimestamp: 0,
|
||||
testCount: 0,
|
||||
isOnline: true,
|
||||
},
|
||||
startTime: Date.now() - snapshot.uptime,
|
||||
uptime: snapshot.uptime,
|
||||
cacheHitRate: snapshot.cacheHitRate,
|
||||
networkSuccessRate: snapshot.networkSuccessRate,
|
||||
resourceCount: snapshot.resourceCount,
|
||||
};
|
||||
}
|
||||
this.lastRefresh = new Date().toLocaleTimeString();
|
||||
this.isConnected = true;
|
||||
return {};
|
||||
}
|
||||
);
|
||||
|
||||
// Handle event log push updates - dispatch to events component
|
||||
this.comms.createTypedHandler<serviceworker.IMessage_Serviceworker_EventLogged>(
|
||||
'serviceworker_eventLogged',
|
||||
async (entry) => {
|
||||
// Dispatch custom event for sw-dash-events component
|
||||
this.dispatchEvent(new CustomEvent('event-logged', {
|
||||
detail: entry,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
return {};
|
||||
}
|
||||
);
|
||||
|
||||
// Handle resource cached push updates
|
||||
this.comms.createTypedHandler<serviceworker.IMessage_Serviceworker_ResourceCached>(
|
||||
'serviceworker_resourceCached',
|
||||
async (resource) => {
|
||||
// Update resource count optimistically
|
||||
if (resource.cached && this.metrics) {
|
||||
this.metrics = {
|
||||
...this.metrics,
|
||||
resourceCount: this.metrics.resourceCount + 1,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Heartbeat to check SW health periodically
|
||||
*/
|
||||
private startHeartbeat(): void {
|
||||
this.heartbeatInterval = setInterval(async () => {
|
||||
try {
|
||||
const response = await fetch('/sw-dash/metrics');
|
||||
if (response.ok) {
|
||||
this.isConnected = true;
|
||||
// Optionally refresh full metrics periodically
|
||||
this.metrics = await response.json();
|
||||
this.lastRefresh = new Date().toLocaleTimeString();
|
||||
} else {
|
||||
this.isConnected = false;
|
||||
}
|
||||
} catch {
|
||||
this.isConnected = false;
|
||||
}
|
||||
}, this.HEARTBEAT_INTERVAL_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load resource data on demand (when switching to urls/domains/types view)
|
||||
*/
|
||||
private async loadResourceData(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch('/sw-dash/resources');
|
||||
@@ -184,8 +340,8 @@ export class SwDashApp extends LitElement {
|
||||
}
|
||||
|
||||
private handleSpeedtestComplete(_e: CustomEvent): void {
|
||||
// Refresh metrics after speedtest
|
||||
this.loadMetrics();
|
||||
// Refresh metrics after speedtest via HTTP
|
||||
this.loadInitialData();
|
||||
}
|
||||
|
||||
private formatUptime(ms: number): string {
|
||||
|
||||
@@ -197,9 +197,42 @@ export class SwDashEvents extends LitElement {
|
||||
@state() accessor page = 1;
|
||||
private readonly pageSize = 50;
|
||||
|
||||
// Bound event handler reference for cleanup
|
||||
private boundEventHandler: ((e: Event) => void) | null = null;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.loadEvents();
|
||||
// Listen for pushed events from parent
|
||||
this.setupPushEventListener();
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
// Clean up event listener
|
||||
if (this.boundEventHandler) {
|
||||
window.removeEventListener('event-logged', this.boundEventHandler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up listener for pushed events from service worker (via sw-dash-app)
|
||||
*/
|
||||
private setupPushEventListener(): void {
|
||||
this.boundEventHandler = (e: Event) => {
|
||||
const customEvent = e as CustomEvent<IEventLogEntry>;
|
||||
const newEvent = customEvent.detail;
|
||||
|
||||
// Only add if it matches current filter (or filter is 'all')
|
||||
if (this.filter === 'all' || newEvent.type === this.filter) {
|
||||
// Prepend new event to the list
|
||||
this.events = [newEvent, ...this.events];
|
||||
this.totalCount++;
|
||||
}
|
||||
};
|
||||
|
||||
// Listen at window level since events bubble up with composed: true
|
||||
window.addEventListener('event-logged', this.boundEventHandler);
|
||||
}
|
||||
|
||||
private async loadEvents(): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user