diff --git a/changelog.md b/changelog.md index 4223266..2c9cc9b 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2025-12-04 - 6.6.0 - feat(web_serviceworker) +Enable service worker dashboard speedtests via TypedSocket, expose ServiceWorker instance to dashboard, and add server-side speedtest handler + +- Add `serviceworker_speedtest` typed handler in TypedServer to support download/upload/latency tests from service workers +- Export `getServiceWorkerInstance` from the web_serviceworker entrypoint so other modules (dashboard) can access the running ServiceWorker instance +- Make ServiceWorker.typedsocket and ServiceWorker.typedrouter public to allow the dashboard to create and fire TypedSocket requests +- Update dashboard to run latency, download and upload tests over TypedSocket instead of POSTing to /sw-typedrequest +- Deprecate legacy servertools.Server.addTypedSocket (now a no-op) and recommend using TypedServer with SmartServe integration for WebSocket support + ## 2025-12-04 - 6.5.0 - feat(serviceworker) Add server-driven service worker cache invalidation and TypedSocket integration diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 3f6491c..79fe259 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@api.global/typedserver', - version: '6.5.0', + version: '6.6.0', description: 'A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.' } diff --git a/ts/classes.typedserver.ts b/ts/classes.typedserver.ts index 9bee2a5..ecb55cc 100644 --- a/ts/classes.typedserver.ts +++ b/ts/classes.typedserver.ts @@ -306,6 +306,35 @@ export class TypedServer { }; }) ); + + // Speedtest handler for service worker dashboard + this.typedrouter.addTypedHandler( + new plugins.typedrequest.TypedHandler('serviceworker_speedtest', async (reqArg) => { + const startTime = Date.now(); + const payloadSizeKB = reqArg.payloadSizeKB || 100; + const sizeBytes = payloadSizeKB * 1024; + let payload: string | undefined; + let bytesTransferred = 0; + + switch (reqArg.type) { + case 'download': + payload = 'x'.repeat(sizeBytes); + bytesTransferred = sizeBytes; + break; + case 'upload': + bytesTransferred = reqArg.payload?.length || 0; + break; + case 'latency': + bytesTransferred = 1; + break; + } + + const durationMs = Date.now() - startTime; + const speedMbps = durationMs > 0 ? (bytesTransferred * 8) / (durationMs * 1000) : 0; + + return { durationMs, bytesTransferred, speedMbps, timestamp: Date.now(), payload }; + }) + ); } catch (error) { console.error('Failed to initialize TypedSocket:', error); } diff --git a/ts_web_serviceworker/classes.dashboard.ts b/ts_web_serviceworker/classes.dashboard.ts index fac04f6..987257e 100644 --- a/ts_web_serviceworker/classes.dashboard.ts +++ b/ts_web_serviceworker/classes.dashboard.ts @@ -1,4 +1,6 @@ import { getMetricsCollector } from './classes.metrics.js'; +import { getServiceWorkerInstance } from './index.js'; +import * as interfaces from './env.js'; /** * Dashboard generator that creates a terminal-like metrics display @@ -43,65 +45,50 @@ export class DashboardGenerator { } = { isOnline: false }; try { + const sw = getServiceWorkerInstance(); + + // Check if TypedSocket is connected + if (!sw.typedsocket) { + results.error = 'TypedSocket not connected'; + return new Response(JSON.stringify(results), { + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store', + }, + }); + } + + // Create typed request for speedtest + const speedtestRequest = sw.typedsocket.createTypedRequest< + interfaces.serviceworker.IRequest_Serviceworker_Speedtest + >('serviceworker_speedtest'); + // Latency test const latencyStart = Date.now(); - const latencyResponse = await fetch('/sw-typedrequest', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - method: 'serviceworker_speedtest', - request: { type: 'latency' }, - }), - }); - - if (latencyResponse.ok) { - await latencyResponse.json(); // Consume response - const latencyDuration = Date.now() - latencyStart; - results.latency = { durationMs: latencyDuration, speedMbps: 0 }; - metrics.recordSpeedtest('latency', latencyDuration); - results.isOnline = true; - metrics.setOnlineStatus(true); - } + await speedtestRequest.fire({ type: 'latency' }); + const latencyDuration = Date.now() - latencyStart; + results.latency = { durationMs: latencyDuration, speedMbps: 0 }; + metrics.recordSpeedtest('latency', latencyDuration); + results.isOnline = true; + metrics.setOnlineStatus(true); // Download test (100KB) const downloadStart = Date.now(); - const downloadResponse = await fetch('/sw-typedrequest', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - method: 'serviceworker_speedtest', - request: { type: 'download', payloadSizeKB: 100 }, - }), - }); - - if (downloadResponse.ok) { - const downloadData = await downloadResponse.json(); - const downloadDuration = Date.now() - downloadStart; - const bytesTransferred = downloadData.response?.payload?.length || 0; - // Speed in Mbps: (bytes * 8) / (ms / 1000) / 1000000 = bytes * 8 / ms / 1000 - const downloadSpeedMbps = downloadDuration > 0 ? (bytesTransferred * 8) / (downloadDuration * 1000) : 0; - results.download = { durationMs: downloadDuration, speedMbps: downloadSpeedMbps, bytesTransferred }; - metrics.recordSpeedtest('download', downloadSpeedMbps); - } + const downloadResult = await speedtestRequest.fire({ type: 'download', payloadSizeKB: 100 }); + const downloadDuration = Date.now() - downloadStart; + const bytesTransferred = downloadResult.payload?.length || 0; + const downloadSpeedMbps = downloadDuration > 0 ? (bytesTransferred * 8) / (downloadDuration * 1000) : 0; + results.download = { durationMs: downloadDuration, speedMbps: downloadSpeedMbps, bytesTransferred }; + metrics.recordSpeedtest('download', downloadSpeedMbps); // Upload test (100KB) const uploadPayload = 'x'.repeat(100 * 1024); const uploadStart = Date.now(); - const uploadResponse = await fetch('/sw-typedrequest', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - method: 'serviceworker_speedtest', - request: { type: 'upload', payload: uploadPayload }, - }), - }); - - if (uploadResponse.ok) { - const uploadDuration = Date.now() - uploadStart; - const uploadSpeedMbps = uploadDuration > 0 ? (uploadPayload.length * 8) / (uploadDuration * 1000) : 0; - results.upload = { durationMs: uploadDuration, speedMbps: uploadSpeedMbps, bytesTransferred: uploadPayload.length }; - metrics.recordSpeedtest('upload', uploadSpeedMbps); - } + await speedtestRequest.fire({ type: 'upload', payload: uploadPayload }); + const uploadDuration = Date.now() - uploadStart; + const uploadSpeedMbps = uploadDuration > 0 ? (uploadPayload.length * 8) / (uploadDuration * 1000) : 0; + results.upload = { durationMs: uploadDuration, speedMbps: uploadSpeedMbps, bytesTransferred: uploadPayload.length }; + metrics.recordSpeedtest('upload', uploadSpeedMbps); } catch (error) { results.error = error instanceof Error ? error.message : String(error); diff --git a/ts_web_serviceworker/classes.serviceworker.ts b/ts_web_serviceworker/classes.serviceworker.ts index 26bd63e..6548d42 100644 --- a/ts_web_serviceworker/classes.serviceworker.ts +++ b/ts_web_serviceworker/classes.serviceworker.ts @@ -28,8 +28,8 @@ export class ServiceWorker { public store: plugins.webstore.WebStore; // TypedSocket connection for server communication - private typedsocket: plugins.typedsocket.TypedSocket; - private typedrouter = new plugins.typedrequest.TypedRouter(); + public typedsocket: plugins.typedsocket.TypedSocket; + public typedrouter = new plugins.typedrequest.TypedRouter(); constructor(selfArg: interfaces.ServiceWindow) { logger.log('info', `Service worker instantiating at ${Date.now()}`); diff --git a/ts_web_serviceworker/index.ts b/ts_web_serviceworker/index.ts index 1e753b6..14a622a 100644 --- a/ts_web_serviceworker/index.ts +++ b/ts_web_serviceworker/index.ts @@ -5,3 +5,6 @@ declare var self: env.ServiceWindow; import { ServiceWorker } from './classes.serviceworker.js'; const sw = new ServiceWorker(self); + +// Export getter for service worker instance (used by dashboard for TypedSocket access) +export const getServiceWorkerInstance = (): ServiceWorker => sw;