Compare commits

..

4 Commits

Author SHA1 Message Date
d3330880c0 v7.7.1
Some checks failed
Default (tags) / security (push) Failing after 16s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-04 18:07:52 +00:00
dbbfd313ae fix(web_serviceworker): Standardize DeesComms message format in service worker backend 2025-12-04 18:07:52 +00:00
eabee2d658 v7.7.0
Some checks failed
Default (tags) / security (push) Failing after 16s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-04 17:26:34 +00:00
95cd681380 feat(typedserver): Add SPA fallback support to TypedServer 2025-12-04 17:26:34 +00:00
7 changed files with 89 additions and 32 deletions

View File

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

View File

@@ -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": {

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,5 +16,6 @@ export interface ServiceWindow extends Window {
location: any;
skipWaiting: any;
clients: any;
registration?: ServiceWorkerRegistration;
}
declare var self: Window;