Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d3330880c0 | |||
| dbbfd313ae | |||
| eabee2d658 | |||
| 95cd681380 |
16
changelog.md
16
changelog.md
@@ -1,5 +1,21 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-12-04 - 7.7.1 - fix(web_serviceworker)
|
||||
Standardize DeesComms message format in service worker backend
|
||||
|
||||
- Add createMessage helper to generate consistent TypedRequest-shaped messages (includes messageId and correlation.id/phase).
|
||||
- Replace inline postMessage payloads with createMessage(...) calls across ServiceworkerBackend (status updates, new-version broadcasts, alerts, event pushes, metrics updates, resource-cached notifications).
|
||||
- Improves message consistency and enables easier correlation/tracing of DeesComms messages; behavior should remain backward-compatible.
|
||||
|
||||
## 2025-12-04 - 7.7.0 - feat(typedserver)
|
||||
Add SPA fallback support to TypedServer
|
||||
|
||||
- Introduce new IServerOptions.spaFallback boolean to enable SPA routing fallback.
|
||||
- When enabled, GET requests for paths without a file extension will serve serveDir/index.html.
|
||||
- Preserves existing HTML injection behavior: injectReload still injects devtools script and typedserver metadata into <head> when enabled.
|
||||
- Responses from SPA fallback include Cache-Control: no-cache and appHash header for cache-busting; falls through to 404 on errors.
|
||||
- Non-file routes that contain a dot (.) are not considered for SPA fallback to avoid interfering with asset requests.
|
||||
|
||||
## 2025-12-04 - 7.6.0 - feat(typedserver)
|
||||
Remove legacy Express-based servertools, drop express deps, and refactor TypedServer to SmartServe + typedrouter with CORS support
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@api.global/typedserver",
|
||||
"version": "7.6.0",
|
||||
"version": "7.7.1",
|
||||
"description": "A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@api.global/typedserver',
|
||||
version: '7.6.0',
|
||||
version: '7.7.1',
|
||||
description: 'A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.'
|
||||
}
|
||||
|
||||
@@ -56,6 +56,12 @@ export interface IServerOptions {
|
||||
feedMetadata?: plugins.smartfeed.IFeedOptions;
|
||||
articleGetterFunction?: () => Promise<plugins.tsclass.content.IArticle[]>;
|
||||
blockWaybackMachine?: boolean;
|
||||
|
||||
/**
|
||||
* SPA fallback - serve index.html for non-file routes (e.g., /login, /dashboard)
|
||||
* Useful for single-page applications with client-side routing
|
||||
*/
|
||||
spaFallback?: boolean;
|
||||
}
|
||||
|
||||
export type THttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'ALL';
|
||||
@@ -491,6 +497,41 @@ export class TypedServer {
|
||||
});
|
||||
}
|
||||
|
||||
// SPA fallback - serve index.html for non-file routes
|
||||
if (this.options.spaFallback && this.options.serveDir && method === 'GET' && !path.includes('.')) {
|
||||
try {
|
||||
const indexPath = plugins.path.join(this.options.serveDir, 'index.html');
|
||||
let html = await plugins.fsInstance.file(indexPath).encoding('utf8').read() as string;
|
||||
|
||||
// Inject reload script if enabled
|
||||
if (this.options.injectReload && html.includes('<head>')) {
|
||||
const injection = `<head>
|
||||
<!-- injected by @apiglobal/typedserver start -->
|
||||
<script async defer type="module" src="/typedserver/devtools"></script>
|
||||
<script>
|
||||
globalThis.typedserver = {
|
||||
lastReload: ${this.lastReload},
|
||||
versionInfo: ${JSON.stringify({}, null, 2)},
|
||||
}
|
||||
</script>
|
||||
<!-- injected by @apiglobal/typedserver stop -->
|
||||
`;
|
||||
html = html.replace('<head>', injection);
|
||||
}
|
||||
|
||||
return new Response(html, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
appHash: this.serveHash,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
// Fall through to 404
|
||||
}
|
||||
}
|
||||
|
||||
// Not found
|
||||
return new Response('Not Found', { status: 404 });
|
||||
}
|
||||
|
||||
@@ -52,6 +52,22 @@ export class ServiceworkerBackend {
|
||||
private pendingMetricsUpdate = false;
|
||||
private readonly METRICS_THROTTLE_MS = 500;
|
||||
|
||||
/**
|
||||
* Helper to create properly formatted TypedRequest messages for DeesComms
|
||||
*/
|
||||
private createMessage<T>(method: string, request: T): any {
|
||||
const id = `${method}_${Date.now()}`;
|
||||
return {
|
||||
method,
|
||||
request,
|
||||
messageId: id,
|
||||
correlation: {
|
||||
id,
|
||||
phase: 'request' as const
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
constructor(optionsArg: {
|
||||
self: any;
|
||||
purgeCache: (reqArg: interfaces.serviceworker.IRequest_PurgeServiceWorkerCache['request']) => Promise<interfaces.serviceworker.IRequest_PurgeServiceWorkerCache['response']>;
|
||||
@@ -244,11 +260,7 @@ export class ServiceworkerBackend {
|
||||
*/
|
||||
public async broadcastStatusUpdate(status: interfaces.serviceworker.IStatusUpdate): Promise<void> {
|
||||
try {
|
||||
await this.deesComms.postMessage({
|
||||
method: 'serviceworker_statusUpdate',
|
||||
request: status,
|
||||
messageId: `sw_status_${Date.now()}`
|
||||
});
|
||||
await this.deesComms.postMessage(this.createMessage('serviceworker_statusUpdate', status));
|
||||
logger.log('info', `Status update broadcast: ${status.source}:${status.type} - ${status.message}`);
|
||||
} catch (error) {
|
||||
logger.log('warn', `Failed to broadcast status update: ${error}`);
|
||||
@@ -290,11 +302,7 @@ export class ServiceworkerBackend {
|
||||
|
||||
// Send update message via DeesComms
|
||||
// This will be picked up by clients that have registered a handler for 'serviceworker_newVersion'
|
||||
await this.deesComms.postMessage({
|
||||
method: 'serviceworker_newVersion',
|
||||
request: {},
|
||||
messageId: `sw_update_${Date.now()}`
|
||||
});
|
||||
await this.deesComms.postMessage(this.createMessage('serviceworker_newVersion', {}));
|
||||
|
||||
// As a fallback, also use the clients API to reload clients that might not catch the broadcast
|
||||
const clients = await this.swSelf.clients.matchAll({ type: 'window' });
|
||||
@@ -360,11 +368,7 @@ export class ServiceworkerBackend {
|
||||
|
||||
// Send message to clients who might be able to show an actual alert
|
||||
try {
|
||||
await this.deesComms.postMessage({
|
||||
method: 'serviceworker_alert',
|
||||
request: { message: alertText },
|
||||
messageId: `sw_alert_${Date.now()}`
|
||||
});
|
||||
await this.deesComms.postMessage(this.createMessage('serviceworker_alert', { message: alertText }));
|
||||
logger.log('info', `Alert message sent to clients: ${alertText}`);
|
||||
} catch (error) {
|
||||
logger.log('error', `Failed to send alert to clients: ${error instanceof Error ? error.message : String(error)}`);
|
||||
@@ -381,11 +385,7 @@ export class ServiceworkerBackend {
|
||||
*/
|
||||
public async pushEvent(entry: interfaces.serviceworker.IEventLogEntry): Promise<void> {
|
||||
try {
|
||||
await this.deesComms.postMessage({
|
||||
method: 'serviceworker_eventLogged',
|
||||
request: entry,
|
||||
messageId: `sw_event_${entry.id}`
|
||||
});
|
||||
await this.deesComms.postMessage(this.createMessage('serviceworker_eventLogged', entry));
|
||||
logger.log('note', `Pushed event to clients: ${entry.type}`);
|
||||
} catch (error) {
|
||||
logger.log('warn', `Failed to push event: ${error}`);
|
||||
@@ -446,11 +446,7 @@ export class ServiceworkerBackend {
|
||||
};
|
||||
|
||||
try {
|
||||
await this.deesComms.postMessage({
|
||||
method: 'serviceworker_metricsUpdate',
|
||||
request: snapshot,
|
||||
messageId: `sw_metrics_${Date.now()}`
|
||||
});
|
||||
await this.deesComms.postMessage(this.createMessage('serviceworker_metricsUpdate', snapshot));
|
||||
} catch (error) {
|
||||
logger.log('warn', `Failed to push metrics update: ${error}`);
|
||||
}
|
||||
@@ -461,11 +457,7 @@ export class ServiceworkerBackend {
|
||||
*/
|
||||
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()}`
|
||||
});
|
||||
await this.deesComms.postMessage(this.createMessage('serviceworker_resourceCached', { url, contentType, size, cached }));
|
||||
} catch (error) {
|
||||
logger.log('warn', `Failed to push resource cached: ${error}`);
|
||||
}
|
||||
|
||||
@@ -110,6 +110,13 @@ export class ServiceWorker {
|
||||
done.reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle wake-up scenario: if already activated, connect immediately
|
||||
// (install/activate events don't fire on wake-up)
|
||||
if (selfArg.registration?.active) {
|
||||
logger.log('info', 'SW woke up (already activated) - connecting to server');
|
||||
this.connectToServer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,5 +16,6 @@ export interface ServiceWindow extends Window {
|
||||
location: any;
|
||||
skipWaiting: any;
|
||||
clients: any;
|
||||
registration?: ServiceWorkerRegistration;
|
||||
}
|
||||
declare var self: Window;
|
||||
Reference in New Issue
Block a user