feat(web_serviceworker): Enable service worker dashboard speedtests via TypedSocket, expose ServiceWorker instance to dashboard, and add server-side speedtest handler

This commit is contained in:
2025-12-04 12:16:24 +00:00
parent 4bae49cfb0
commit 78a5c53d19
6 changed files with 81 additions and 53 deletions

View File

@@ -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

View File

@@ -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.'
}

View File

@@ -306,6 +306,35 @@ export class TypedServer {
};
})
);
// Speedtest handler for service worker dashboard
this.typedrouter.addTypedHandler<interfaces.serviceworker.IRequest_Serviceworker_Speedtest>(
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);
}

View File

@@ -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);

View File

@@ -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()}`);

View File

@@ -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;