fix(core): update

This commit is contained in:
2024-05-11 12:51:20 +02:00
parent fedb37ee16
commit d225a9584f
42 changed files with 1435 additions and 522 deletions

View File

@@ -0,0 +1,8 @@
/**
* autocreated commitinfo by @pushrocks/commitinfo
*/
export const commitinfo = {
name: '@losslessone_private/lole-serviceworker',
version: '1.0.206',
description: 'serviceworker implementation for lossless websites'
}

View File

@@ -0,0 +1,53 @@
import * as plugins from './plugins.js';
import * as interfaces from '../dist_ts_interfaces/index.js';
/**
* This class is meant to be used only on the backend side
*/
export class ServiceworkerBackend {
public deesComms = new plugins.deesComms.DeesComms();
constructor(optionsArg: {
self: any;
purgeCache: (reqArg: interfaces.serviceworker.IRequest_PurgeServiceWorkerCache['request']) => Promise<interfaces.serviceworker.IRequest_PurgeServiceWorkerCache['response']>;
}) {
// lets handle wakestuff
optionsArg.self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'wakeUpCall') {
console.log('sw-backend: got wake up call');
}
});
this.deesComms.createTypedHandler<interfaces.serviceworker.IRequest_Client_Serviceworker_ConnectionPolling>('broadcastConnectionPolling', async reqArg => {
return {
serviceworkerId: '123'
};
})
this.deesComms.createTypedHandler<interfaces.serviceworker.IRequest_PurgeServiceWorkerCache>('purgeServiceWorkerCache', async reqArg => {
console.log(`Executing purge cache in serviceworker backend.`)
return await optionsArg.purgeCache?.(reqArg);
});
}
/**
* reloads all clients
*/
public async triggerReloadAll() {
}
/**
* display notification
*/
public async addNotification(notificationArg: {
title: string;
body: string;
}) {
}
public async alert(alertText: string) {
}
}

View File

@@ -0,0 +1,20 @@
export * from '../dist_ts_interfaces/index.js';
// =============================
// Interfaces for the service worker
// =============================
// tslint:disable-next-line: interface-name
export interface ServiceEvent extends Event {
request: any;
respondWith: any;
waitUntil: any;
}
// tslint:disable-next-line: interface-name
export interface ServiceWindow extends Window {
addEventListener: any;
location: any;
skipWaiting: any;
clients: any;
}
declare var self: Window;

View File

@@ -0,0 +1,8 @@
// TypeScript declatations
import * as env from './env.js';
declare var self: env.ServiceWindow;
import { LosslessServiceWorker } from './serviceworker.classes.serviceworker.js';
const losslessServiceWorkerInstance = new LosslessServiceWorker(self);

View File

@@ -0,0 +1,25 @@
// @losslessone_private scope
import * as interfaces from '../dist_ts_interfaces/index.js';
export { interfaces };
// @apiglobal scope
import * as typedrequest from '@api.global/typedrequest';
export { typedrequest };
// @pushrocks scope
import * as smartdelay from '@push.rocks/smartdelay';
import * as smartpromise from '@push.rocks/smartpromise';
import * as webrequest from '@push.rocks/webrequest';
import * as webstore from '@push.rocks/webstore';
import * as taskbuffer from '@push.rocks/taskbuffer';
export { smartdelay, smartpromise, webrequest, webstore, taskbuffer };
// @design.estate scope
import * as deesComms from '@design.estate/dees-comms';
export {
deesComms,
}

View File

@@ -0,0 +1,224 @@
import * as plugins from './plugins.js';
import * as interfaces from './env.js';
import { logger } from './serviceworker.logging.js';
import { LosslessServiceWorker } from './serviceworker.classes.serviceworker.js';
export class CacheManager {
public losslessServiceWorkerRef: LosslessServiceWorker;
public usedCacheNames = {
runtimeCacheName: 'runtime'
};
constructor(losslessServiceWorkerRefArg: LosslessServiceWorker) {
this.losslessServiceWorkerRef = losslessServiceWorkerRefArg;
this._setupCache();
}
private _setupCache = () => {
const createMatchRequest = (requestArg: Request) => {
// lets create a matchRequest
let matchRequest: Request;
if (requestArg.url.startsWith(this.losslessServiceWorkerRef.serviceWindowRef.location.origin)) {
// internal request
matchRequest = requestArg;
} else {
matchRequest = new Request(requestArg.url, {
...requestArg.clone(),
mode: 'cors'
});
}
return matchRequest;
};
/**
* creates a 500 response
*/
const create500Response = async (requestArg: Request, responseArg: Response) => {
return new Response(
`
<html>
<head>
<style>
.note {
padding: 10px;
color: #fff;
background: #000;
border-bottom: 1px solid #e4002b;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="note">
<strong>serviceworker running, but status 500</strong><br>
</div>
serviceworker is unable to fetch this request<br>
Here is some info about the request/response pair:<br>
<br>
requestUrl: ${requestArg.url}<br>
responseType: ${responseArg.type}<br>
responseBody: ${await responseArg.clone().text()}<br>
</body>
</html>
`,
{
headers: {
"Content-Type": "text/html"
},
status: 500
}
);
};
// A list of local resources we always want to be cached.
this.losslessServiceWorkerRef.serviceWindowRef.addEventListener('fetch', async (fetchEventArg: any) => {
// Lets block scopes we don't want to be passing through the serviceworker
const parsedUrl = new URL(fetchEventArg.request.url)
if (
parsedUrl.hostname.includes('paddle.com')
|| parsedUrl.hostname.includes('paypal.com')
|| parsedUrl.hostname.includes('reception.lossless.one')
|| parsedUrl.pathname.startsWith('/socket.io')
) {
logger.log('note',`serviceworker not active for ${parsedUrl.toString()}`);
return;
}
// lets continue for the rest
const done = plugins.smartpromise.defer<Response>();
fetchEventArg.respondWith(done.promise);
const originalRequest: Request = fetchEventArg.request;
if (
(originalRequest.method === 'GET' &&
(originalRequest.url.startsWith(this.losslessServiceWorkerRef.serviceWindowRef.location.origin) &&
!originalRequest.url.includes('/api/') &&
!originalRequest.url.includes('smartserve/reloadcheck'))) ||
originalRequest.url.includes('https://assetbroker.lossless.one/public') ||
originalRequest.url.includes('https://assetbroker.lossless.one/brandfiles') ||
originalRequest.url.includes('https://assetbroker.lossless.one/websites') ||
originalRequest.url.includes('https://unpkg.com') ||
originalRequest.url.includes('https://fonts.googleapis.com') ||
originalRequest.url.includes('https://fonts.gstatic.com')
) {
// lets see if things need to be updated
// not waiting here
this.losslessServiceWorkerRef.updateManager.checkUpdate(this);
// this code block is executed for local requests
const matchRequest = createMatchRequest(originalRequest);
const cachedResponse = await caches.match(matchRequest);
if (cachedResponse) {
logger.log('ok', `CACHED: found cached response for ${matchRequest.url}`);
done.resolve(cachedResponse);
return;
}
// in case there is no cached response
logger.log('info', `NOTYETCACHED: trying to cache ${matchRequest.url}`);
const newResponse: Response = await fetch(matchRequest).catch(async err => {
return await create500Response(matchRequest, new Response(err.message));
});
// fill cache
// Put a copy of the response in the runtime cache.
if (newResponse.status > 299 || newResponse.type === 'opaque') {
logger.log(
'error',
`NOTCACHED: can't cache response for ${matchRequest.url} due to status ${
newResponse.status
} and type ${newResponse.type}`
);
done.resolve(await create500Response(matchRequest, newResponse));
} else {
const cache = await caches.open(this.usedCacheNames.runtimeCacheName);
const responseToPutToCache = newResponse.clone();
const headers = new Headers();
responseToPutToCache.headers.forEach((value, key) => {
if (
value !== 'Cache-Control'
&& value !== 'cache-control'
&& value !== 'Expires'
&& value !== 'expires'
&& value !== 'Pragma'
&& value !== 'pragma'
) {
headers.set(key, value);
}
});
headers.set('Cache-Control', 'no-cache, no-store, must-revalidate');
headers.set('Pragma', 'no-cache');
headers.set('Expires', '0');
await cache.put(matchRequest, new Response(responseToPutToCache.body, {
...responseToPutToCache,
headers
}));
logger.log(
'ok',
`NOWCACHED: cached response for ${matchRequest.url} for subsequent requests!`
);
done.resolve(newResponse);
}
} else {
// this code block is executed for remote requests
logger.log(
'ok',
`NOTCACHED: not caching any responses for ${
originalRequest.url
}. Fetching from origin now...`
);
done.resolve(
await fetch(originalRequest).catch(async err => {
return await create500Response(originalRequest, new Response(err.message));
})
);
}
});
}
/**
* update caches
* @param reasonArg
*/
/**
* cleans all caches
* should only be run when running a new service worker
* @param reasonArg
*/
public cleanCaches = async (reasonArg = 'no reason given') => {
logger.log('info', `MAJOR CACHEEVENT: cleaning caches now! Reason: ${reasonArg}`);
const cacheNames = await caches.keys();
const deletePromises = cacheNames.map(cacheToDelete => {
const deletePromise = caches.delete(cacheToDelete);
deletePromise.then(() => {
logger.log('ok', `Deleted cache ${cacheToDelete}`);
});
return deletePromise;
});
await Promise.all(deletePromises);
}
/**
* revalidate cache
*/
public async revalidateCache() {
const runtimeCache = await caches.open(this.usedCacheNames.runtimeCacheName);
const cacheKeys = await runtimeCache.keys();
for (const requestArg of cacheKeys) {
const cachedResponse = runtimeCache.match(requestArg);
// lets get a new response for comparison
const clonedRequest = requestArg.clone();
const response = await plugins.smartpromise.timeoutWrap(fetch(clonedRequest), 1000);
if (response && response.status >= 200 && response.status < 300) {
await runtimeCache.delete(requestArg);
await runtimeCache.put(requestArg, response);
}
}
}
}

View File

@@ -0,0 +1,33 @@
import * as plugins from './plugins.js';
import { LosslessServiceWorker } from './serviceworker.classes.serviceworker.js';
export class NetworkManager {
public serviceWorkerRef: LosslessServiceWorker;
public webRequest: plugins.webrequest.WebRequest;
public previousState: string;
constructor(serviceWorkerRefArg: LosslessServiceWorker) {
this.serviceWorkerRef = serviceWorkerRefArg;
this.webRequest = new plugins.webrequest.WebRequest();
this.getConnection()?.addEventListener('change', () => {
this.updateConnectionStatus();
});
}
/**
* gets the connection
*/
public getConnection() {
const navigatorLocal: any = self.navigator;
return navigatorLocal?.connection;
}
public getEffectiveType() {
return this.getConnection()?.effectiveType || '4g';
}
public updateConnectionStatus() {
console.log(`Connection type changed from ${this.previousState} to ${this.getEffectiveType()}`);
}
}

View File

@@ -0,0 +1,75 @@
import * as plugins from './plugins.js';
import * as interfaces from './env.js';
// imports
import { CacheManager } from './serviceworker.classes.cachemanager.js';
import { Deferred } from '@push.rocks/smartpromise';
import { logger } from './serviceworker.logging.js';
// imported classes
import { UpdateManager } from './serviceworker.classes.updatemanager.js';
import { NetworkManager } from './serviceworker.classes.networkmanager.js';
import { TaskManager } from './serviceworker.classes.taskmanager.js';
export class LosslessServiceWorker {
// STATIC
// INSTANCE
public serviceWindowRef: interfaces.ServiceWindow;
public leleServiceWorkerBackend: plugins.leleServiceworker.LosslessServiceworkerBackend;
public cacheManager: CacheManager;
public updateManager: UpdateManager;
public networkManager: NetworkManager;
public taskManager: TaskManager;
public store: plugins.webstore.WebStore;
constructor(selfArg: interfaces.ServiceWindow) {
logger.log('info', `Service worker instantiating at ${Date.now()}`);
this.serviceWindowRef = selfArg;
this.leleServiceWorkerBackend = plugins.leleServiceworker.getServiceWorkerBackend({
self: selfArg,
purgeCache: async (reqArg) => {
await this.cacheManager.cleanCaches(),
logger.log('info', `cleaned caches in serviceworker as per request from frontend.`);
return {}
}
});
this.updateManager = new UpdateManager(this);
this.networkManager = new NetworkManager(this);
this.taskManager = new TaskManager(this);
this.cacheManager = new CacheManager(this);
this.store = new plugins.webstore.WebStore({
dbName: 'losslessServiceworker',
storeName: 'losslessServiceworker',
});
// =================================
// Installation and Activation
// =================================
this.serviceWindowRef.addEventListener('install', async (event: interfaces.ServiceEvent) => {
const done = new Deferred();
event.waitUntil(done.promise);
// its important to not go async before event.waitUntil
done.resolve();
logger.log('success', `service worker installed! TimeStamp = ${new Date().toISOString()}`);
selfArg.skipWaiting();
logger.log('note', `Called skip waiting!`);
});
this.serviceWindowRef.addEventListener('activate', async (event: interfaces.ServiceEvent) => {
const done = new Deferred();
event.waitUntil(done.promise);
// its important to not go async before event.waitUntil
await selfArg.clients.claim();
await this.cacheManager.cleanCaches('new service worker loaded! :)');
done.resolve();
});
}
}

View File

@@ -0,0 +1,15 @@
import * as plugins from './plugins.js';
import { LosslessServiceWorker } from './serviceworker.classes.serviceworker.js';
/**
* Taskmanager
* should use times allocated by browser
*/
export class TaskManager {
public serviceworkerRef: LosslessServiceWorker;
constructor(serviceWorkerRefArg: LosslessServiceWorker) {
this.serviceworkerRef = serviceWorkerRefArg;
}
}

View File

@@ -0,0 +1 @@
import * as plugins from './plugins.js';

View File

@@ -0,0 +1,90 @@
import * as plugins from './plugins.js';
import { LosslessServiceWorker } from './serviceworker.classes.serviceworker.js';
import { logger } from './serviceworker.logging.js';
import { CacheManager } from './serviceworker.classes.cachemanager.js';
export class UpdateManager {
public lastUpdateCheck: number = 0;
public lastVersionInfo: plugins.lointServiceworker.IRequest_Serviceworker_Backend_VersionInfo['response'];
public serviceworkerRef: LosslessServiceWorker;
constructor(serviceWorkerRefArg: LosslessServiceWorker) {
this.serviceworkerRef = serviceWorkerRefArg;
}
/**
* checks wether an update is needed
*/
public async checkUpdate(cacheManager: CacheManager): Promise<boolean> {
const lswVersionInfoKey = 'versionInfo';
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))
) {
this.lastVersionInfo = await this.serviceworkerRef.store.get(lswVersionInfoKey);
}
const now = Date.now();
const millisSinceLastCheck = now - this.lastUpdateCheck;
if (millisSinceLastCheck < 100000) {
// TODO account for being offline
return false;
}
logger.log('info', 'checking for update of the app by comparing app hashes...');
this.lastUpdateCheck = now;
const currentVersionInfo = await this.getVersionInfoFromServer();
logger.log('info', `old versionInfo: ${JSON.stringify(this.lastVersionInfo)}`);
logger.log('info', `current versionInfo: ${JSON.stringify(currentVersionInfo)}`);
const needsUpdate = this.lastVersionInfo.appHash !== currentVersionInfo.appHash ? true : false;
if (needsUpdate) {
logger.log('info', 'Caches need to be updated');
logger.log('info', 'starting a debounced update task');
this.performAsyncUpdateDebouncedTask.trigger();
this.lastVersionInfo = currentVersionInfo;
await this.serviceworkerRef.store.set(lswVersionInfoKey, this.lastVersionInfo);
} else {
logger.log('ok', 'caches are still valid, performing revalidation in a bit...');
this.performAsyncCacheRevalidationDebouncedTask.trigger();
}
}
/**
* gets the apphash from the server
*/
public async getVersionInfoFromServer() {
const getAppHashRequest = new plugins.typedrequest.TypedRequest<
plugins.lointServiceworker.IRequest_Serviceworker_Backend_VersionInfo
>('/lsw-typedrequest', 'serviceworker_versionInfo');
const result = await getAppHashRequest.fire({});
return result;
}
// tasks
/**
* this task is executed once we know that there is a new version available
*/
public performAsyncUpdateDebouncedTask = new plugins.taskbuffer.TaskDebounced({
name: 'performAsyncUpdate',
taskFunction: async () => {
logger.log('info', 'trying to update PWA with serviceworker');
await this.serviceworkerRef.cacheManager.cleanCaches('a new app version has been communicated by the server.');
// lets notify all current clients about the update
await this.serviceworkerRef.leleServiceWorkerBackend.triggerReloadAll();
},
debounceTimeInMillis: 2000,
});
public performAsyncCacheRevalidationDebouncedTask = new plugins.taskbuffer.TaskDebounced({
name: 'performAsyncCacheRevalidation',
taskFunction: async () => {
await this.serviceworkerRef.cacheManager.revalidateCache();
},
debounceTimeInMillis: 6000
});
}

View File

@@ -0,0 +1,17 @@
import * as smartlog from '@push.rocks/smartlog';
import { SmartlogDestinationDevtools } from '@push.rocks/smartlog-destination-devtools';
export const logger = new smartlog.Smartlog({
logContext: {
company: 'Lossless GmbH',
companyunit: 'Lossless Cloud',
containerName: 'web',
environment: 'production',
runtime: 'chrome',
zone: 'servezone'
},
minimumLogLevel: 'info'
});
logger.addLogDestination(new SmartlogDestinationDevtools());
logger.log('note', 'serviceworker console initialized!');