fix(ts_web_inject): improve ReloadChecker resilience and TypedSocket handling

This commit is contained in:
2026-03-03 21:32:06 +00:00
parent e80619bac6
commit 7003d3fbf9
3 changed files with 74 additions and 46 deletions

View File

@@ -1,5 +1,16 @@
# Changelog
## 2026-03-03 - 8.4.2 - fix(ts_web_inject)
improve ReloadChecker resilience and TypedSocket handling
- Added retry counters and limits for service worker subscription (MAX_SW_RETRIES) and traffic logging setup (MAX_TRAFFIC_LOGGING_RETRIES) to avoid infinite retry loops
- Made TypedSocket connection non-blocking by connecting in the background (createClient().then(...).catch(...)) so HTTP polling isn't blocked
- Introduced typedsocketConnected flag to track WS state and avoid double-connects; early-return if typedsocket exists
- Adjusted connection lifecycle handling: only trigger reload check when reconnect completes; set backendConnectionLost/state appropriately
- Adaptive polling interval: poll every 5s when WebSocket is not connected, otherwise 120s
- Added safer failure handling for TypedSocket connection with warning logs on errors
- Simplified/changed several logger levels/messages (info -> ok/warn) and removed some noisy info logs
## 2026-03-03 - 8.4.1 - fix(statuspill)
wait for document.body before appending status pill when script loads before <body> is parsed; defer via DOMContentLoaded or requestAnimationFrame

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@api.global/typedserver',
version: '8.4.1',
version: '8.4.2',
description: 'A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.'
}

View File

@@ -1,7 +1,7 @@
import * as plugins from './typedserver_web.plugins.js';
import * as interfaces from '../dist_ts_interfaces/index.js';
import { logger } from './typedserver_web.logger.js';
logger.log('info', `TypedServer-Devtools initialized!`);
// TypedServer-Devtools loaded
import { TypedserverStatusPill } from './typedserver_web.statuspill.js';
@@ -51,6 +51,9 @@ export class ReloadChecker {
window.location.reload();
}
private swRetryCount = 0;
private static readonly MAX_SW_RETRIES = 5;
/**
* Subscribe to service worker status updates
*/
@@ -67,13 +70,12 @@ export class ReloadChecker {
timestamp: status.timestamp,
});
});
logger.log('info', 'Subscribed to service worker status updates');
logger.log('ok', 'Subscribed to service worker status updates');
// Get initial SW status
this.fetchServiceWorkerStatus();
} else {
logger.log('note', 'Service worker client not available yet, will retry...');
// Retry after a delay
} else if (this.swRetryCount < ReloadChecker.MAX_SW_RETRIES) {
this.swRetryCount++;
setTimeout(() => this.subscribeToServiceWorker(), 2000);
}
}
@@ -109,7 +111,6 @@ export class ReloadChecker {
* starts the reload checker
*/
public async performHttpRequest() {
logger.log('info', 'performing http check...');
(await this.store.get(this.storeKey))
? null
: await this.store.set(this.storeKey, globalThis.typedserver.lastReload);
@@ -201,22 +202,29 @@ export class ReloadChecker {
}
}
public async connectTypedsocket() {
if (!this.typedsocket) {
this.typedrouter.addTypedHandler<interfaces.IReq_PushLatestServerChangeTime>(
new plugins.typedrequest.TypedHandler('pushLatestServerChangeTime', async (dataArg) => {
this.checkReload(dataArg.time);
return {};
})
);
this.typedsocket = await plugins.typedsocket.TypedSocket.createClient(
this.typedrouter,
plugins.typedsocket.TypedSocket.useWindowLocationOriginUrl()
);
private typedsocketConnected = false;
public connectTypedsocket() {
if (this.typedsocket) return;
this.typedrouter.addTypedHandler<interfaces.IReq_PushLatestServerChangeTime>(
new plugins.typedrequest.TypedHandler('pushLatestServerChangeTime', async (dataArg) => {
this.checkReload(dataArg.time);
return {};
})
);
// Connect in the background — never block the HTTP polling loop
plugins.typedsocket.TypedSocket.createClient(
this.typedrouter,
plugins.typedsocket.TypedSocket.useWindowLocationOriginUrl()
).then(async (socket) => {
this.typedsocket = socket;
this.typedsocketConnected = true;
await this.typedsocket.setTag('typedserver_frontend', {});
this.typedsocket.statusSubject.subscribe(async (statusArg) => {
console.log(`typedsocket status: ${statusArg}`);
if (statusArg === 'disconnected' || statusArg === 'reconnecting') {
this.typedsocketConnected = false;
this.backendConnectionLost = true;
this.statusPill.updateStatus({
source: 'backend',
@@ -225,35 +233,39 @@ export class ReloadChecker {
persist: true,
timestamp: Date.now(),
});
} else if (statusArg === 'connected' && this.backendConnectionLost) {
this.backendConnectionLost = false;
this.statusPill.updateStatus({
source: 'backend',
type: 'connected',
message: 'TypedSocket connected',
persist: false,
timestamp: Date.now(),
});
// lets check if a reload is necessary
const getLatestServerChangeTime =
this.typedsocket.createTypedRequest<interfaces.IReq_GetLatestServerChangeTime>(
'getLatestServerChangeTime'
);
const response = await getLatestServerChangeTime.fire({});
this.checkReload(response.time);
} else if (statusArg === 'connected') {
this.typedsocketConnected = true;
if (this.backendConnectionLost) {
this.backendConnectionLost = false;
this.statusPill.updateStatus({
source: 'backend',
type: 'connected',
message: 'TypedSocket connected',
persist: false,
timestamp: Date.now(),
});
// lets check if a reload is necessary
const getLatestServerChangeTime =
this.typedsocket.createTypedRequest<interfaces.IReq_GetLatestServerChangeTime>(
'getLatestServerChangeTime'
);
const response = await getLatestServerChangeTime.fire({});
this.checkReload(response.time);
}
}
});
logger.log('success', `ReloadChecker connected through typedsocket!`);
logger.log('ok', `ReloadChecker connected via TypedSocket`);
// Enable traffic logging for sw-dash
this.enableTrafficLogging();
}
}).catch((err) => {
logger.log('warn', `TypedSocket connection failed: ${err}`);
});
}
public started = false;
public async start() {
this.started = true;
logger.log('info', `starting ReloadChecker...`);
// Subscribe to service worker status updates
this.subscribeToServiceWorker();
@@ -261,11 +273,12 @@ export class ReloadChecker {
while (this.started) {
const response = await this.performHttpRequest();
if (response?.status === 200) {
logger.log('info', `ReloadChecker reached backend!`);
await this.checkReload(parseInt(await response.text()));
await this.connectTypedsocket();
this.connectTypedsocket();
}
await plugins.smartdelay.delayFor(120000);
// Poll more frequently when WebSocket isn't connected (fallback detection)
const pollInterval = this.typedsocketConnected ? 120000 : 5000;
await plugins.smartdelay.delayFor(pollInterval);
}
}
@@ -282,20 +295,24 @@ export class ReloadChecker {
}
}
private trafficLoggingRetryCount = 0;
private static readonly MAX_TRAFFIC_LOGGING_RETRIES = 5;
/**
* Enable TypedRequest traffic logging to the service worker
* Sets up global hooks on TypedRouter to capture all request/response traffic
*/
public enableTrafficLogging(): void {
if (this.trafficLoggingEnabled) {
logger.log('note', 'Traffic logging already enabled');
return;
}
// Check if service worker client is available
if (!globalThis.globalSw?.actionManager) {
logger.log('note', 'Service worker client not available, will retry traffic logging setup...');
setTimeout(() => this.enableTrafficLogging(), 2000);
if (this.trafficLoggingRetryCount < ReloadChecker.MAX_TRAFFIC_LOGGING_RETRIES) {
this.trafficLoggingRetryCount++;
setTimeout(() => this.enableTrafficLogging(), 2000);
}
return;
}
@@ -320,7 +337,7 @@ export class ReloadChecker {
});
this.trafficLoggingEnabled = true;
logger.log('success', 'TypedRequest traffic logging enabled');
logger.log('ok', 'Traffic logging enabled');
}
}