Compare commits

..

15 Commits
v8.0.0 ... main

Author SHA1 Message Date
5e41ab63e4 v8.4.2
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-03 21:32:06 +00:00
7003d3fbf9 fix(ts_web_inject): improve ReloadChecker resilience and TypedSocket handling 2026-03-03 21:32:06 +00:00
e80619bac6 v8.4.1
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-03 21:17:44 +00:00
e290744451 fix(statuspill): wait for document.body before appending status pill when script loads before <body> is parsed; defer via DOMContentLoaded or requestAnimationFrame 2026-03-03 21:17:44 +00:00
c5c45f668f v8.4.0
Some checks failed
Default (tags) / security (push) Failing after 2s
Default (tags) / test (push) Failing after 2s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-24 22:26:45 +00:00
aa748e0d82 feat(utilityservers): add injectReload and noCache options and enable dev features by default 2026-02-24 22:26:45 +00:00
14c8d83ab5 v8.3.1
Some checks failed
Default (tags) / security (push) Failing after 2s
Default (tags) / test (push) Failing after 2s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-24 16:49:44 +00:00
d66b7648a8 fix(typedserver): no changes detected — no version bump needed 2026-02-24 16:49:44 +00:00
657bdfb403 feat(websiteserver): add bundledContent pass-through and make serveDir optional
UtilityWebsiteServer now forwards bundledContent to TypedServer for in-memory serving.
serveDir is optional; dev features (injectReload, watch, noCache) only activate with serveDir.
2026-02-24 16:48:56 +00:00
e1b2a13395 v8.3.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-23 22:42:04 +00:00
c753206456 feat(typedserver): add noCache option to disable client-side caching and set no-cache headers on responses 2026-01-23 22:42:04 +00:00
4cc1efb7cc v8.2.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-23 18:58:23 +00:00
9641a00174 feat(typedserver): serve bundled in-memory content with caching and reload injection 2026-01-23 18:58:23 +00:00
75b4570742 v8.1.0
Some checks failed
Default (tags) / security (push) Failing after 15s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-22 15:18:33 +00:00
fac6384807 feat(types): export IRequestContext type from @push.rocks/smartserve for consumers to use in route handlers 2025-12-22 15:18:33 +00:00
8 changed files with 343 additions and 73 deletions

View File

@@ -1,5 +1,59 @@
# 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
- Guard against missing document.body to avoid errors when the script runs before the <body> is parsed
- Retry showing on DOMContentLoaded if the document is still loading
- Fallback to requestAnimationFrame to schedule the show on the next frame if DOM is already parsed
## 2026-02-24 - 8.4.0 - feat(utilityservers)
add injectReload and noCache options and enable dev features by default
- Adds optional configuration properties 'injectReload' and 'noCache' to the website server options interface.
- Dev features (injectReload and noCache) are no longer only enabled when serveDir is set; they now default to true when not explicitly provided.
- This changes default runtime behavior: live-reload injection and disabled browser caching may be enabled for servers that previously did not have them — consumers should set options explicitly to preserve previous behavior.
## 2026-02-24 - 8.3.1 - fix(typedserver)
no changes detected — no version bump needed
- No files changed in the diff
- Current package version: 8.3.0
## 2026-01-23 - 8.3.0 - feat(typedserver)
add noCache option to disable client-side caching and set no-cache headers on responses
- Introduces an optional noCache?: boolean in the server options interface
- applyResponseHeaders now sets Cache-Control, Pragma, and Expires headers when noCache is true
- Existing CORS and security header behavior unchanged when noCache is not set or false
## 2026-01-23 - 8.2.0 - feat(typedserver)
serve bundled in-memory content with caching and reload injection
- Add IBundledContentItem and IServerOptions.bundledContent to accept tsbundle output (path + contentBase64).
- Initialize in-memory bundled content map with MIME type detection, SHA-256-based ETag, size and combined app hash.
- Serve bundled files with proper Content-Type, ETag, Cache-Control, support HEAD and conditional 304 responses.
- Inject live-reload script into bundled HTML responses when injectReload is enabled; modify SPA fallback to prefer bundled index.html.
- Require serveDir or bundledContent when injectReload is enabled; prefer in-memory bundled content over filesystem requests.
- Add helper methods: initializeBundledContent, getMimeType, serveBundledContent and serveBundledHtmlWithInjection; log initialization.
## 2025-12-22 - 8.1.0 - feat(types)
export IRequestContext type from @push.rocks/smartserve for consumers to use in route handlers
- Adds a type export: IRequestContext from @push.rocks/smartserve
- Type-only change — no runtime or behavioral changes
## 2025-12-20 - 8.0.0 - BREAKING CHANGE(typedserver)
migrate route handlers to use IRequestContext and lazy body parsers

View File

@@ -1,6 +1,6 @@
{
"name": "@api.global/typedserver",
"version": "8.0.0",
"version": "8.4.2",
"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: '8.0.0',
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

@@ -75,12 +75,26 @@ export interface ISecurityHeaders {
crossOriginResourcePolicy?: 'same-site' | 'same-origin' | 'cross-origin';
}
/**
* Bundled content item - matches tsbundle output format
*/
export interface IBundledContentItem {
path: string;
contentBase64: string;
}
export interface IServerOptions {
/**
* serve a particular directory
*/
serveDir?: string;
/**
* Bundled content to serve from memory (higher priority than filesystem)
* Accepts array format from tsbundle: { path: string; contentBase64: string }[]
*/
bundledContent?: IBundledContentItem[];
/**
* inject a reload script that takes care of live reloading
*/
@@ -143,6 +157,12 @@ export interface IServerOptions {
* Set to true for defaults (brotli + gzip), false to disable, or provide detailed config
*/
compression?: plugins.smartserve.ICompressionConfig | boolean;
/**
* Disable all client-side caching by setting appropriate headers
* Useful for development or when content changes frequently
*/
noCache?: boolean;
}
export type THttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'ALL';
@@ -173,6 +193,15 @@ export class TypedServer {
// File server for static files
private fileServer: plugins.smartserve.FileServer;
// Bundled content map for O(1) lookup
private bundledContentMap: Map<string, {
content: Uint8Array;
etag: string;
mimeType: string;
size: number;
}> = new Map();
private bundledContentHash: string = '';
public lastReload: number = Date.now();
public ended = false;
@@ -209,14 +238,139 @@ export class TypedServer {
plugins.smartserve.ControllerRegistry.addRoute(path, method, handler);
}
/**
* Initialize bundled content from base64-encoded files
*/
private async initializeBundledContent(): Promise<void> {
if (!this.options.bundledContent?.length) return;
const hashParts: string[] = [];
for (const item of this.options.bundledContent) {
let path = item.path.replace(/^\/+/, '') || 'index.html';
// Decode base64 to Uint8Array
const binary = atob(item.contentBase64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
// Generate ETag from content hash
const hashBuffer = await crypto.subtle.digest('SHA-256', bytes);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const etag = '"' + hashArray.slice(0, 16).map(b => b.toString(16).padStart(2, '0')).join('') + '"';
// Get MIME type
const mimeType = this.getMimeType(path);
this.bundledContentMap.set(path, { content: bytes, etag, mimeType, size: bytes.length });
hashParts.push(etag);
}
// Combined hash for cache busting
this.bundledContentHash = hashParts.join('').slice(0, 12);
if (!this.options.serveDir) this.serveHash = this.bundledContentHash;
console.log(`Initialized ${this.bundledContentMap.size} bundled files (hash: ${this.bundledContentHash})`);
}
/**
* Get MIME type for a file path
*/
private getMimeType(path: string): string {
const ext = path.split('.').pop()?.toLowerCase() || '';
const mimeTypes: Record<string, string> = {
'html': 'text/html; charset=utf-8',
'js': 'application/javascript; charset=utf-8',
'mjs': 'application/javascript; charset=utf-8',
'css': 'text/css; charset=utf-8',
'json': 'application/json; charset=utf-8',
'png': 'image/png',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'gif': 'image/gif',
'svg': 'image/svg+xml',
'ico': 'image/x-icon',
'woff': 'font/woff',
'woff2': 'font/woff2',
'ttf': 'font/ttf',
'eot': 'application/vnd.ms-fontobject',
};
return mimeTypes[ext] || 'application/octet-stream';
}
/**
* Serve bundled content from memory
*/
private async serveBundledContent(request: Request, pathname: string): Promise<Response | null> {
if (this.bundledContentMap.size === 0) return null;
let path = pathname.replace(/^\/+/, '');
if (!path || path.endsWith('/')) path = (path || '') + 'index.html';
const entry = this.bundledContentMap.get(path);
if (!entry) return null;
const headers = new Headers({
'Content-Type': entry.mimeType,
'ETag': entry.etag,
'Cache-Control': 'public, max-age=31536000, immutable',
});
if (this.bundledContentHash) headers.set('appHash', this.bundledContentHash);
// Conditional request
const ifNoneMatch = request.headers.get('If-None-Match');
if (ifNoneMatch === entry.etag) {
return new Response(null, { status: 304, headers });
}
if (request.method === 'HEAD') {
headers.set('Content-Length', entry.size.toString());
return new Response(null, { status: 200, headers });
}
// HTML injection for reload
if (this.options.injectReload && entry.mimeType.includes('text/html')) {
return this.serveBundledHtmlWithInjection(entry, headers);
}
headers.set('Content-Length', entry.size.toString());
return new Response(Buffer.from(entry.content), { status: 200, headers });
}
/**
* Serve bundled HTML with reload script injection
*/
private serveBundledHtmlWithInjection(
entry: { content: Uint8Array; mimeType: string },
headers: Headers
): Response {
let html = new TextDecoder().decode(entry.content);
if (html.includes('<head>')) {
html = html.replace('<head>', `<head>
<!-- injected by @apiglobal/typedserver -->
<script async defer type="module" src="/typedserver/devtools"></script>
<script>globalThis.typedserver = { lastReload: ${this.lastReload} }</script>`);
}
headers.set('Cache-Control', 'no-cache, no-store, must-revalidate');
headers.delete('ETag');
const content = new TextEncoder().encode(html);
headers.set('Content-Length', content.length.toString());
return new Response(content, { status: 200, headers });
}
/**
* inits and starts the server
*/
public async start() {
// Validate essential configuration before starting
if (this.options.injectReload && !this.options.serveDir) {
if (this.options.injectReload && !this.options.serveDir && !this.options.bundledContent) {
throw new Error(
'You set to inject the reload script without a serve dir. This is not supported at the moment.'
'injectReload requires serveDir or bundledContent'
);
}
@@ -242,6 +396,9 @@ export class TypedServer {
});
}
// Initialize bundled content from memory
await this.initializeBundledContent();
// Initialize decorated controllers
if (this.options.injectReload) {
this.devToolsController = new DevToolsController({
@@ -484,11 +641,18 @@ export class TypedServer {
}
/**
* Apply all configured headers (CORS, security) to a response
* Apply all configured headers (CORS, security, cache control) to a response
*/
private applyResponseHeaders(response: Response): Response {
const headers = new Headers(response.headers);
// No-cache headers
if (this.options.noCache) {
headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0');
headers.set('Pragma', 'no-cache');
headers.set('Expires', '0');
}
// CORS headers
if (this.options.cors) {
headers.set('Access-Control-Allow-Origin', '*');
@@ -627,6 +791,12 @@ export class TypedServer {
}
}
// Try bundled content first (in-memory, faster)
if (this.bundledContentMap.size > 0 && (method === 'GET' || method === 'HEAD')) {
const bundledResponse = await this.serveBundledContent(request, path);
if (bundledResponse) return bundledResponse;
}
// HTML injection for reload (if enabled)
if (this.options.injectReload && this.options.serveDir) {
const response = await this.handleHtmlWithInjection(request);
@@ -655,14 +825,22 @@ 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;
if (this.options.spaFallback && method === 'GET' && !path.includes('.')) {
// Try bundled index.html first
if (this.bundledContentMap.has('index.html')) {
const response = await this.serveBundledContent(request, '/index.html');
if (response) return response;
}
// Inject reload script if enabled
if (this.options.injectReload && html.includes('<head>')) {
const injection = `<head>
// Fall back to filesystem
if (this.options.serveDir) {
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>
@@ -673,19 +851,20 @@ export class TypedServer {
</script>
<!-- injected by @apiglobal/typedserver stop -->
`;
html = html.replace('<head>', injection);
}
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
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
}
}
}

View File

@@ -5,3 +5,6 @@ export * from './classes.typedserver.js';
// lets export utilityservers
import * as utilityservers from './utilityservers/index.js';
export { utilityservers };
// Export IRequestContext for consumers to use in route handlers
export type { IRequestContext } from '@push.rocks/smartserve';

View File

@@ -1,5 +1,5 @@
import * as interfaces from '../../dist_ts_interfaces/index.js';
import { type IServerOptions, type ISecurityHeaders, TypedServer } from '../classes.typedserver.js';
import { type IServerOptions, type ISecurityHeaders, type IBundledContentItem, TypedServer } from '../classes.typedserver.js';
import * as plugins from '../plugins.js';
export interface IUtilityWebsiteServerConstructorOptions {
@@ -10,7 +10,9 @@ export interface IUtilityWebsiteServerConstructorOptions {
/** Domain name for the website */
domain: string;
/** Directory to serve static files from */
serveDir: string;
serveDir?: string;
/** Bundled content to serve from memory (base64-encoded files from tsbundle) */
bundledContent?: IBundledContentItem[];
/** RSS feed metadata */
feedMetadata?: IServerOptions['feedMetadata'];
/** Enable/disable CORS (default: true) */
@@ -27,6 +29,10 @@ export interface IUtilityWebsiteServerConstructorOptions {
adsTxt?: string[];
/** Response compression configuration (default: enabled with brotli + gzip) */
compression?: plugins.smartserve.ICompressionConfig | boolean;
/** Disable browser caching (default: true when serveDir is set) */
noCache?: boolean;
/** Inject live-reload devtools script into HTML (default: true when serveDir is set) */
injectReload?: boolean;
}
/**
@@ -55,12 +61,14 @@ export class UtilityWebsiteServer {
// Core settings
cors: this.options.cors ?? true,
serveDir: this.options.serveDir,
bundledContent: this.options.bundledContent,
domain: this.options.domain,
port,
// Development features
injectReload: true,
watch: true,
injectReload: this.options.injectReload ?? true,
watch: !!this.options.serveDir,
noCache: this.options.noCache ?? true,
// SPA support (enabled by default for modern web apps)
spaFallback: this.options.spaFallback ?? true,

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

View File

@@ -311,6 +311,15 @@ export class TypedserverStatusPill extends LitElement {
*/
public show(): void {
if (!this.appended) {
if (!document.body) {
// Script loaded before <body> was parsed (async module) — wait for DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.show(), { once: true });
} else {
requestAnimationFrame(() => this.show());
}
return;
}
document.body.appendChild(this);
this.appended = true;
}