From 43a335ab3aa36f0255c7b1df6d43c56217c78429 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Thu, 4 Dec 2025 13:42:19 +0000 Subject: [PATCH] BREAKING CHANGE(serviceworker): Move serviceworker speedtest to time-based chunked transfers and update dashboard/server contract --- changelog.md | 9 ++++ ts/00_commitinfo_data.ts | 2 +- ts/classes.typedserver.ts | 20 ++++---- ts/servertools/tools.serviceworker.ts | 25 ++++----- ts_interfaces/serviceworker.ts | 18 ++++--- ts_web_serviceworker/classes.dashboard.ts | 62 +++++++++++++++-------- 6 files changed, 83 insertions(+), 53 deletions(-) diff --git a/changelog.md b/changelog.md index 13911f3..c7996d5 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2025-12-04 - 7.0.0 - BREAKING CHANGE(serviceworker) +Move serviceworker speedtest to time-based chunked transfers and update dashboard/server contract + +- Change speedtest protocol to time-based chunk transfers: new request types 'download_chunk' and 'upload_chunk' plus 'latency'. Clients should call chunk requests in a loop for the desired test duration. +- IRequest_Serviceworker_Speedtest interface updated: request fields renamed/changed (chunkSizeKB, payload) and response no longer includes durationMs or speedMbps — server now returns bytesTransferred, timestamp, and optional payload. +- TypedServer speedtest handler updated to support 'download_chunk' and 'upload_chunk' semantics and to return bytesTransferred/timestamp/payload only (removed server-side duration/speed calculation). +- Dashboard runSpeedtest now performs time-based tests (TEST_DURATION_MS = 5000, CHUNK_SIZE_KB = 64) by repeatedly requesting chunks and computing throughput on the client side. +- Documentation/comments updated to clarify new speedtest behavior and default chunk sizes. + ## 2025-12-04 - 6.8.1 - fix(web_serviceworker) Move service worker initialization to init.ts and remove exports from service worker entrypoint to avoid ESM bundle output diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index b413903..f6f64b4 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.8.1', + version: '7.0.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 ecb55cc..0772a74 100644 --- a/ts/classes.typedserver.ts +++ b/ts/classes.typedserver.ts @@ -308,31 +308,31 @@ export class TypedServer { ); // Speedtest handler for service worker dashboard + // Client calls this in a loop for the test duration to get accurate time-based measurements this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler('serviceworker_speedtest', async (reqArg) => { - const startTime = Date.now(); - const payloadSizeKB = reqArg.payloadSizeKB || 100; - const sizeBytes = payloadSizeKB * 1024; + const chunkSizeKB = reqArg.chunkSizeKB || 64; + const sizeBytes = chunkSizeKB * 1024; let payload: string | undefined; let bytesTransferred = 0; switch (reqArg.type) { - case 'download': + case 'download_chunk': + // Generate chunk data for download test payload = 'x'.repeat(sizeBytes); bytesTransferred = sizeBytes; break; - case 'upload': + case 'upload_chunk': + // Acknowledge received upload data bytesTransferred = reqArg.payload?.length || 0; break; case 'latency': - bytesTransferred = 1; + // Simple ping - minimal data + bytesTransferred = 0; break; } - const durationMs = Date.now() - startTime; - const speedMbps = durationMs > 0 ? (bytesTransferred * 8) / (durationMs * 1000) : 0; - - return { durationMs, bytesTransferred, speedMbps, timestamp: Date.now(), payload }; + return { bytesTransferred, timestamp: Date.now(), payload }; }) ); } catch (error) { diff --git a/ts/servertools/tools.serviceworker.ts b/ts/servertools/tools.serviceworker.ts index 71fb739..c518b12 100644 --- a/ts/servertools/tools.serviceworker.ts +++ b/ts/servertools/tools.serviceworker.ts @@ -84,43 +84,36 @@ export const addServiceWorkerRoute = ( ) ); - // Speedtest handler for measuring connection speed + // Speedtest handler for measuring connection speed (time-based chunked approach) typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'serviceworker_speedtest', async (reqArg) => { - const startTime = Date.now(); - const payloadSizeKB = reqArg.payloadSizeKB || 100; - const sizeBytes = payloadSizeKB * 1024; + const chunkSizeKB = reqArg.chunkSizeKB || 64; + const sizeBytes = chunkSizeKB * 1024; let payload: string | undefined; let bytesTransferred = 0; switch (reqArg.type) { - case 'download': - // Generate random payload for download test + case 'download_chunk': + // Generate chunk payload for download test payload = 'x'.repeat(sizeBytes); bytesTransferred = sizeBytes; break; - case 'upload': + case 'upload_chunk': // For upload, measure bytes received from client bytesTransferred = reqArg.payload?.length || 0; break; case 'latency': - // Minimal payload for latency test - bytesTransferred = 1; + // Simple ping - no payload needed + bytesTransferred = 0; break; } - const durationMs = Date.now() - startTime; - // Speed in Mbps: (bytes * 8 bits/byte) / (ms * 1000 to get seconds) / 1,000,000 for Mbps - const speedMbps = durationMs > 0 ? (bytesTransferred * 8) / (durationMs * 1000) : 0; - return { - durationMs, bytesTransferred, - speedMbps, timestamp: Date.now(), - payload, // Only for download tests + payload, // Only for download_chunk tests }; } ) diff --git a/ts_interfaces/serviceworker.ts b/ts_interfaces/serviceworker.ts index 45670f0..aa0598e 100644 --- a/ts_interfaces/serviceworker.ts +++ b/ts_interfaces/serviceworker.ts @@ -215,6 +215,14 @@ export interface IRequest_Serviceworker_CacheInvalidate /** * Speedtest request between service worker and backend + * + * Types: + * - 'latency': Simple ping to measure round-trip time + * - 'download_chunk': Request a chunk of data (64KB default) + * - 'upload_chunk': Send a chunk of data to server + * + * The client runs a loop calling download_chunk or upload_chunk + * until the desired test duration (e.g., 5 seconds) elapses. */ export interface IRequest_Serviceworker_Speedtest extends plugins.typedrequestInterfaces.implementsTR< @@ -223,15 +231,13 @@ export interface IRequest_Serviceworker_Speedtest > { method: 'serviceworker_speedtest'; request: { - type: 'download' | 'upload' | 'latency'; - payloadSizeKB?: number; // Size of test payload in KB (default: 100) - payload?: string; // For upload tests, the payload to send + type: 'latency' | 'download_chunk' | 'upload_chunk'; + chunkSizeKB?: number; // Size of chunk in KB (default: 64) + payload?: string; // For upload_chunk, the data to send }; response: { - durationMs: number; bytesTransferred: number; - speedMbps: number; timestamp: number; - payload?: string; // For download tests, the payload received + payload?: string; // For download_chunk, the data received }; } \ No newline at end of file diff --git a/ts_web_serviceworker/classes.dashboard.ts b/ts_web_serviceworker/classes.dashboard.ts index 46a26e4..6ed33f3 100644 --- a/ts_web_serviceworker/classes.dashboard.ts +++ b/ts_web_serviceworker/classes.dashboard.ts @@ -43,13 +43,18 @@ export class DashboardGenerator { }); } + // Speedtest configuration + private static readonly TEST_DURATION_MS = 5000; // 5 seconds per test + private static readonly CHUNK_SIZE_KB = 64; // 64KB chunks + /** - * Runs a speedtest and returns the results + * Runs a time-based speedtest and returns the results + * Each test (download/upload) runs for TEST_DURATION_MS, transferring chunks continuously */ public async runSpeedtest(): Promise { const metrics = getMetricsCollector(); const results: { - latency?: { durationMs: number; speedMbps: number }; + latency?: { durationMs: number }; download?: { durationMs: number; speedMbps: number; bytesTransferred: number }; upload?: { durationMs: number; speedMbps: number; bytesTransferred: number }; error?: string; @@ -75,32 +80,49 @@ export class DashboardGenerator { interfaces.serviceworker.IRequest_Serviceworker_Speedtest >('serviceworker_speedtest'); - // Latency test + // Latency test - simple ping const latencyStart = Date.now(); await speedtestRequest.fire({ type: 'latency' }); const latencyDuration = Date.now() - latencyStart; - results.latency = { durationMs: latencyDuration, speedMbps: 0 }; + results.latency = { durationMs: latencyDuration }; metrics.recordSpeedtest('latency', latencyDuration); results.isOnline = true; metrics.setOnlineStatus(true); - // Download test (100KB) - const downloadStart = Date.now(); - 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); + // Download test - request chunks for TEST_DURATION_MS + { + const downloadStart = Date.now(); + let totalBytes = 0; + while (Date.now() - downloadStart < DashboardGenerator.TEST_DURATION_MS) { + const chunkResult = await speedtestRequest.fire({ + type: 'download_chunk', + chunkSizeKB: DashboardGenerator.CHUNK_SIZE_KB, + }); + totalBytes += chunkResult.bytesTransferred; + } + const downloadDuration = Date.now() - downloadStart; + const downloadSpeedMbps = downloadDuration > 0 ? (totalBytes * 8) / (downloadDuration * 1000) : 0; + results.download = { durationMs: downloadDuration, speedMbps: downloadSpeedMbps, bytesTransferred: totalBytes }; + metrics.recordSpeedtest('download', downloadSpeedMbps); + } - // Upload test (100KB) - const uploadPayload = 'x'.repeat(100 * 1024); - const uploadStart = Date.now(); - 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); + // Upload test - send chunks for TEST_DURATION_MS + { + const uploadPayload = 'x'.repeat(DashboardGenerator.CHUNK_SIZE_KB * 1024); + const uploadStart = Date.now(); + let totalBytes = 0; + while (Date.now() - uploadStart < DashboardGenerator.TEST_DURATION_MS) { + const chunkResult = await speedtestRequest.fire({ + type: 'upload_chunk', + payload: uploadPayload, + }); + totalBytes += chunkResult.bytesTransferred; + } + const uploadDuration = Date.now() - uploadStart; + const uploadSpeedMbps = uploadDuration > 0 ? (totalBytes * 8) / (uploadDuration * 1000) : 0; + results.upload = { durationMs: uploadDuration, speedMbps: uploadSpeedMbps, bytesTransferred: totalBytes }; + metrics.recordSpeedtest('upload', uploadSpeedMbps); + } } catch (error) { results.error = error instanceof Error ? error.message : String(error);