Compare commits

...

14 Commits

10 changed files with 4385 additions and 1326 deletions

View File

@ -1,5 +1,52 @@
# Changelog
## 2025-02-03 - 3.0.58 - fix(network-manager)
Refined network management logic for better offline handling.
- Improved logic to handle missing connections more gracefully.
- Added detailed online/offline connection status logging.
- Implemented a check for stale cache with a grace period for offline scenarios.
- Network requests now use optimized retries and timeouts.
## 2025-02-03 - 3.0.57 - fix(updateManager)
Refine cache management for service worker updates.
- Ensured cache is forcibly updated if older than defined maximum age.
- Implemented interval checks and forced updates for cache staleness.
- Updated version information and cache timestamps upon forced updates or validations.
## 2025-02-03 - 3.0.56 - fix(cachemanager)
Adjust cache control headers and fix redundant code
- Remove duplicate assetbroker URLs in the cache evaluation logic.
- Update cache control headers to improve caching behavior.
- Increase the timeout for fetch operations to improve compatibility.
## 2025-01-28 - 3.0.55 - fix(server)
Fix response content manipulation for HTML files with injectReload
- Moved fileString declaration inside HTML file handling block to prevent unnecessary string conversion for non-HTML files.
- Corrected responseContent assignment to ensure modified HTML strings are converted back to Buffer format.
## 2025-01-28 - 3.0.54 - fix(servertools)
Fixed an issue with compression results handling in HandlerStatic where content was always being written even if not compressed.
- Corrected the double writing of response in HandlerStatic.
- Ensured that file buffers are only conditionally written based on compression availability.
## 2024-12-26 - 3.0.53 - fix(infohtml)
Remove Sentry script and logo from HTML template
- Removed Sentry script from the HTML template.
- Removed Lossless GmbH logo and contact info.
- Updated footer link to point to foss.global.
## 2024-12-25 - 3.0.52 - fix(dependencies)
Bump package versions in dependencies and exports.
- Updated package dependencies to their latest versions.
- Added './infohtml' in package exports.
## 2024-08-27 - 3.0.51 - fix(core)
Update dependencies and fix service worker cache manager and task manager functionalities

View File

@ -1,10 +1,11 @@
{
"name": "@api.global/typedserver",
"version": "3.0.51",
"version": "3.0.58",
"description": "A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.",
"type": "module",
"exports": {
".": "./dist_ts/index.js",
"./infohtml": "./dist_ts/infohtml/index.js",
"./backend": "./dist_ts/index.js",
"./edgeworker": "./dist_ts_edgeworker/index.js",
"./web_inject": "./dist_ts_web_inject/index.js",
@ -57,51 +58,51 @@
],
"homepage": "https://github.com/pushrocks/easyserve",
"dependencies": {
"@api.global/typedrequest": "^3.0.30",
"@api.global/typedrequest": "^3.1.10",
"@api.global/typedrequest-interfaces": "^3.0.19",
"@api.global/typedsocket": "^3.0.1",
"@cloudflare/workers-types": "^4.20240821.1",
"@cloudflare/workers-types": "^4.20241224.0",
"@design.estate/dees-comms": "^1.0.27",
"@push.rocks/lik": "^6.0.15",
"@push.rocks/lik": "^6.1.0",
"@push.rocks/smartchok": "^1.0.34",
"@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartenv": "^5.0.12",
"@push.rocks/smartfeed": "^1.0.11",
"@push.rocks/smartfile": "^11.0.21",
"@push.rocks/smartfile": "^11.0.23",
"@push.rocks/smartjson": "^5.0.20",
"@push.rocks/smartlog": "^3.0.7",
"@push.rocks/smartlog-destination-devtools": "^1.0.12",
"@push.rocks/smartlog-interfaces": "^3.0.2",
"@push.rocks/smartmanifest": "^2.0.2",
"@push.rocks/smartmatch": "^2.0.0",
"@push.rocks/smartmime": "^2.0.2",
"@push.rocks/smartntml": "^2.0.4",
"@push.rocks/smartmime": "^2.0.4",
"@push.rocks/smartntml": "^2.0.8",
"@push.rocks/smartopen": "^2.0.0",
"@push.rocks/smartpath": "^5.0.18",
"@push.rocks/smartpromise": "^4.0.4",
"@push.rocks/smartrequest": "^2.0.22",
"@push.rocks/smartrequest": "^2.0.23",
"@push.rocks/smartrx": "^3.0.7",
"@push.rocks/smartsitemap": "^2.0.3",
"@push.rocks/smartstream": "^3.0.44",
"@push.rocks/smarttime": "^4.0.8",
"@push.rocks/smartstream": "^3.2.5",
"@push.rocks/smarttime": "^4.1.1",
"@push.rocks/taskbuffer": "^3.1.7",
"@push.rocks/webrequest": "^3.0.37",
"@push.rocks/webstore": "^2.0.20",
"@tsclass/tsclass": "^4.1.2",
"@tsclass/tsclass": "^4.2.0",
"@types/express": "^4.17.21",
"body-parser": "^1.20.2",
"body-parser": "^1.20.3",
"cors": "^2.8.5",
"express": "^4.19.2",
"express": "^4.21.2",
"express-force-ssl": "^0.3.2",
"lit": "^3.2.0"
"lit": "^3.2.1"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.1.84",
"@git.zone/tsbundle": "^2.0.15",
"@git.zone/tsrun": "^1.2.49",
"@git.zone/tsbuild": "^2.2.0",
"@git.zone/tsbundle": "^2.1.0",
"@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^1.0.90",
"@push.rocks/tapbundle": "^5.0.24",
"@types/node": "^22.5.0"
"@push.rocks/tapbundle": "^5.5.3",
"@types/node": "^22.10.2"
},
"private": false,
"browserslist": [

5387
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -135,8 +135,8 @@ export class TypedServer {
'/*',
new servertools.HandlerStatic(this.options.serveDir, {
responseModifier: async (responseArg) => {
let fileString = responseArg.responseContent.toString();
if (plugins.path.parse(responseArg.path).ext === '.html') {
let fileString = responseArg.responseContent.toString();
const fileStringArray = fileString.split('<head>');
if (this.options.injectReload && fileStringArray.length === 2) {
fileStringArray[0] = `${fileStringArray[0]}<head>
@ -152,6 +152,7 @@ export class TypedServer {
`;
fileString = fileStringArray.join('');
console.log('injected typedserver script.');
responseArg.responseContent = Buffer.from(fileString);
} else if (this.options.injectReload) {
console.log('Could not insert typedserver script');
}
@ -164,7 +165,7 @@ export class TypedServer {
return {
headers,
path: responseArg.path,
responseContent: Buffer.from(fileString),
responseContent: responseArg.responseContent,
};
},
serveIndexHtmlDefault: true,

View File

@ -96,26 +96,9 @@ export const simpleInfo = async (
name="viewport"
content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height, target-densitydpi=device-dpi"
/>
<script
src="https://browser.sentry-cdn.com/5.4.0/bundle.min.js"
crossorigin="anonymous"
></script>
<script>
if (optionsArg.sentryDsn && optionsArg.sentryMessage) {
Sentry.init({
dsn: '${optionsArg.sentryDsn}',
// ...
});
Sentry.setExtra('location', window.location.href);
Sentry.captureMessage('${optionsArg.sentryMessage} @ ' + window.location.host);
}
</script>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
<div class="logo">
<img src="https://assetbroker.lossless.one/brandfiles/lossless/svg-minimal-bright.svg" />
</div>
<div class="content">
${(() => {
const returnArray: plugins.smartntml.deesElement.TemplateResult[] = [];
@ -129,15 +112,6 @@ export const simpleInfo = async (
} else {
returnArray.push(html` <div class="maintext">${optionsArg.text}</div> `);
}
if (optionsArg.sentryDsn && optionsArg.sentryMessage) {
returnArray.push(
html`<div class="addontext">
We recorded this event. Should you continue to see this page against your
expectations, feel free to mail us at
<a href="mailto:hello@lossless.com">hello@lossless.com</a>
</div>`
);
}
if (optionsArg.redirectTo) {
returnArray.push(
html`<div class="addontext">
@ -149,9 +123,7 @@ export const simpleInfo = async (
})()}
</div>
<div class="legal">
<a href="https://lossless.com">Lossless GmbH</a> / &copy 2014-${new Date().getFullYear()}
/ <a href="https://lossless.gmbh">Legal Info</a> /
<a href="https://lossless.gmbh">Privacy Policy</a>
<a href="https://foss.global">learn more about foss.global</a> / &copy 2014-${new Date().getFullYear()} Task Venture Capital GmbH
</div>
</body>
</html>

View File

@ -134,8 +134,10 @@ export class HandlerStatic extends Handler {
res.status(200);
if (compressionResult?.compressionMethod) {
res.header('Content-Encoding', compressionResult.compressionMethod);
res.write(compressionResult.result);
} else {
res.write(fileBuffer);
}
res.write(compressionResult.result);
res.end();
});
}

View File

@ -98,8 +98,6 @@ export class CacheManager {
!originalRequest.url.includes('/api/') &&
!originalRequest.url.includes('smartserve/reloadcheck'))) ||
originalRequest.url.includes('https://assetbroker.') ||
originalRequest.url.includes('https://assetbroker.') ||
originalRequest.url.includes('https://assetbroker.') ||
originalRequest.url.includes('https://unpkg.com') ||
originalRequest.url.includes('https://fonts.googleapis.com') ||
originalRequest.url.includes('https://fonts.gstatic.com')
@ -150,9 +148,11 @@ export class CacheManager {
headers.set(key, value);
}
});
headers.set('Cache-Control', 'no-cache, no-store, must-revalidate');
// Prevent browser caching while allowing service worker caching
headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
headers.set('Pragma', 'no-cache');
headers.set('Expires', '0');
headers.set('Surrogate-Control', 'no-store');
await cache.put(matchRequest, new Response(responseToPutToCache.body, {
...responseToPutToCache,
headers
@ -215,7 +215,7 @@ export class CacheManager {
// lets get a new response for comparison
const clonedRequest = requestArg.clone();
const response = await plugins.smartpromise.timeoutWrap(fetch(clonedRequest), 1000);
const response = await plugins.smartpromise.timeoutWrap(fetch(clonedRequest), 5000); // Increased timeout for better mobile compatibility
if (response && response.status >= 200 && response.status < 300) {
await runtimeCache.delete(requestArg);
await runtimeCache.put(requestArg, response);

View File

@ -1,18 +1,37 @@
import * as plugins from './plugins.js';
import { ServiceWorker } from './classes.serviceworker.js';
import { logger } from './logging.js';
export class NetworkManager {
public serviceWorkerRef: ServiceWorker;
public webRequest: plugins.webrequest.WebRequest;
private isOffline: boolean = false;
private lastOnlineCheck: number = 0;
private readonly ONLINE_CHECK_INTERVAL = 30000; // 30 seconds
public previousState: string;
constructor(serviceWorkerRefArg: ServiceWorker) {
this.serviceWorkerRef = serviceWorkerRefArg;
this.webRequest = new plugins.webrequest.WebRequest();
// Listen for connection changes
this.getConnection()?.addEventListener('change', () => {
this.updateConnectionStatus();
});
// Listen for online/offline events
self.addEventListener('online', () => {
this.isOffline = false;
logger.log('info', 'Device is now online');
this.updateConnectionStatus();
});
self.addEventListener('offline', () => {
this.isOffline = true;
logger.log('warn', 'Device is now offline');
this.updateConnectionStatus();
});
}
/**
@ -28,6 +47,81 @@ export class NetworkManager {
}
public updateConnectionStatus() {
console.log(`Connection type changed from ${this.previousState} to ${this.getEffectiveType()}`);
const currentType = this.getEffectiveType();
logger.log('info', `Connection type changed from ${this.previousState} to ${currentType}`);
this.previousState = currentType;
}
/**
* Checks if the device is currently online by attempting to contact the server
* @returns Promise<boolean> true if online, false if offline
*/
public async checkOnlineStatus(): Promise<boolean> {
const now = Date.now();
// Only check if enough time has passed since last check
if (now - this.lastOnlineCheck < this.ONLINE_CHECK_INTERVAL) {
return !this.isOffline;
}
try {
const response = await fetch('/sw-typedrequest', {
method: 'HEAD',
cache: 'no-cache'
});
this.isOffline = false;
this.lastOnlineCheck = now;
return true;
} catch (error) {
this.isOffline = true;
this.lastOnlineCheck = now;
logger.log('warn', 'Device appears to be offline');
return false;
}
}
/**
* Makes a network request with offline handling
* @param request The request to make
* @param options Additional options
* @returns Promise<Response>
*/
public async makeRequest<T>(request: Request | string, options: {
timeoutMs?: number;
retries?: number;
backoffMs?: number;
} = {}): Promise<Response> {
const {
timeoutMs = 5000,
retries = 1,
backoffMs = 1000
} = options;
let lastError: Error;
for (let i = 0; i <= retries; i++) {
try {
const isOnline = await this.checkOnlineStatus();
if (!isOnline) {
throw new Error('Device is offline');
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
const response = await fetch(request, {
...typeof request === 'string' ? {} : request,
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (error) {
lastError = error;
if (i < retries) {
await new Promise(resolve => setTimeout(resolve, backoffMs * (i + 1)));
}
}
}
throw lastError;
}
}

View File

@ -17,24 +17,55 @@ export class UpdateManager {
/**
* checks wether an update is needed
*/
private readonly MAX_CACHE_AGE = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
private readonly MIN_CHECK_INTERVAL = 100000; // 100 seconds in milliseconds
private readonly OFFLINE_GRACE_PERIOD = 7 * 24 * 60 * 60 * 1000; // 7 days grace period when offline
private lastCacheTimestamp: number = 0;
public async checkUpdate(cacheManager: CacheManager): Promise<boolean> {
const lswVersionInfoKey = 'versionInfo';
const cacheTimestampKey = 'cacheTimestamp';
// Initialize or load version info
if (!this.lastVersionInfo && !(await this.serviceworkerRef.store.check(lswVersionInfoKey))) {
this.lastVersionInfo = {
appHash: '',
appSemVer: 'v0.0.0',
};
} else if (
!this.lastVersionInfo &&
(await this.serviceworkerRef.store.check(lswVersionInfoKey))
) {
} else if (!this.lastVersionInfo && (await this.serviceworkerRef.store.check(lswVersionInfoKey))) {
this.lastVersionInfo = await this.serviceworkerRef.store.get(lswVersionInfoKey);
}
// Load or initialize cache timestamp
if (await this.serviceworkerRef.store.check(cacheTimestampKey)) {
this.lastCacheTimestamp = await this.serviceworkerRef.store.get(cacheTimestampKey);
}
const now = Date.now();
const millisSinceLastCheck = now - this.lastUpdateCheck;
if (millisSinceLastCheck < 100000) {
// TODO account for being offline
const cacheAge = now - this.lastCacheTimestamp;
// Check if we need to handle stale cache
if (cacheAge > this.MAX_CACHE_AGE) {
const isOnline = await this.serviceworkerRef.networkManager.checkOnlineStatus();
if (isOnline) {
logger.log('info', `Cache is older than ${this.MAX_CACHE_AGE}ms, forcing update...`);
await this.forceUpdate(cacheManager);
return true;
} else if (cacheAge > this.OFFLINE_GRACE_PERIOD) {
// If we're offline and beyond grace period, warn but continue serving cached content
logger.log('warn', `Cache is stale and device is offline. Cache age: ${cacheAge}ms. Using cached content with warning.`);
// We could potentially show a warning to the user here
return false;
} else {
logger.log('info', `Cache is stale but device is offline. Within grace period. Using cached content.`);
return false;
}
}
// Regular update check interval
if (millisSinceLastCheck < this.MIN_CHECK_INTERVAL && cacheAge < this.MAX_CACHE_AGE) {
return false;
}
logger.log('info', 'checking for update of the app by comparing app hashes...');
@ -49,9 +80,17 @@ export class UpdateManager {
this.performAsyncUpdateDebouncedTask.trigger();
this.lastVersionInfo = currentVersionInfo;
await this.serviceworkerRef.store.set(lswVersionInfoKey, this.lastVersionInfo);
// Update cache timestamp
this.lastCacheTimestamp = now;
await this.serviceworkerRef.store.set('cacheTimestamp', now);
} else {
logger.log('ok', 'caches are still valid, performing revalidation in a bit...');
this.performAsyncCacheRevalidationDebouncedTask.trigger();
// Update cache timestamp after successful revalidation
this.lastCacheTimestamp = now;
await this.serviceworkerRef.store.set('cacheTimestamp', now);
}
}
@ -59,17 +98,49 @@ export class UpdateManager {
* gets the apphash from the server
*/
public async getVersionInfoFromServer() {
const getAppHashRequest = new plugins.typedrequest.TypedRequest<
interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo
>('/sw-typedrequest', 'serviceworker_versionInfo');
const result = await getAppHashRequest.fire({});
return result;
try {
const getAppHashRequest = new plugins.typedrequest.TypedRequest<
interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo
>('/sw-typedrequest', 'serviceworker_versionInfo');
// Use networkManager for the request with retries and timeout
const response = await this.serviceworkerRef.networkManager.makeRequest('/sw-typedrequest', {
timeoutMs: 5000,
retries: 2,
backoffMs: 1000
});
const result = await response.json();
return result;
} catch (error) {
logger.log('warn', `Failed to get version info from server: ${error.message}`);
throw error;
}
}
// tasks
/**
* this task is executed once we know that there is a new version available
*/
private async forceUpdate(cacheManager: CacheManager) {
try {
logger.log('info', 'Forcing cache update due to staleness');
const currentVersionInfo = await this.getVersionInfoFromServer();
// Only proceed with cache cleaning if we successfully got new version info
await this.serviceworkerRef.cacheManager.cleanCaches('Cache is stale, forcing update.');
this.lastVersionInfo = currentVersionInfo;
await this.serviceworkerRef.store.set('versionInfo', this.lastVersionInfo);
this.lastCacheTimestamp = Date.now();
await this.serviceworkerRef.store.set('cacheTimestamp', this.lastCacheTimestamp);
await this.serviceworkerRef.leleServiceWorkerBackend.triggerReloadAll();
} catch (error) {
logger.log('error', `Failed to force update: ${error.message}. Keeping existing cache.`);
// If update fails, we'll keep using the existing cache
throw error;
}
}
public performAsyncUpdateDebouncedTask = new plugins.taskbuffer.TaskDebounced({
name: 'performAsyncUpdate',
taskFunction: async () => {