Compare commits
51 Commits
Author | SHA1 | Date | |
---|---|---|---|
ee67c68c17 | |||
8fb2d8b3e8 | |||
75c89b040b | |||
b6d0843e3e | |||
1c5e2845d1 | |||
7798bf7e0a | |||
76db7d1733 | |||
1db472ab01 | |||
23e88030be | |||
1644cbbfad | |||
84e214d087 | |||
0bb7e438d5 | |||
1ce6d2ab01 | |||
d225a9584f | |||
fedb37ee16 | |||
e99196b227 | |||
3d6723d06c | |||
fd7aadaf79 | |||
5e4878e492 | |||
64d4fb011d | |||
6671bbe793 | |||
14bfa3f62f | |||
9e4413c276 | |||
dd91691064 | |||
bf4794c06f | |||
7e04474a66 | |||
907d51a842 | |||
2114ff28c0 | |||
fd9431f82b | |||
e8c1a66e15 | |||
1e98fd99f4 | |||
da6aa9827c | |||
ca3b8a4580 | |||
a7ddb6b6a8 | |||
e1dfe30273 | |||
754fa38fe8 | |||
7e59146e73 | |||
3b550eacf7 | |||
6342895320 | |||
b6a4095a53 | |||
622da78180 | |||
21938e5f20 | |||
99427f5835 | |||
552a15bb2f | |||
b0efc48b96 | |||
8c3aad69a0 | |||
fb2692b50e | |||
65c868aefe | |||
11df25f028 | |||
efb4229f58 | |||
61dcc6badc |
@ -5,12 +5,31 @@
|
||||
"gitzone": {
|
||||
"projectType": "npm",
|
||||
"module": {
|
||||
"githost": "gitlab.com",
|
||||
"githost": "code.foss.global",
|
||||
"gitscope": "pushrocks",
|
||||
"gitrepo": "typedserver",
|
||||
"description": "easy serving of static files",
|
||||
"description": "A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.",
|
||||
"npmPackagename": "@api.global/typedserver",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"TypeScript",
|
||||
"static file serving",
|
||||
"live reload",
|
||||
"compression",
|
||||
"typed requests",
|
||||
"HTTP server",
|
||||
"SSL",
|
||||
"cors",
|
||||
"express middleware",
|
||||
"proxy",
|
||||
"sitemap",
|
||||
"feeds",
|
||||
"robots.txt",
|
||||
"compression (gzip, deflate, brotli)"
|
||||
]
|
||||
}
|
||||
},
|
||||
"tsdoc": {
|
||||
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||
}
|
||||
}
|
80
package.json
80
package.json
@ -1,22 +1,42 @@
|
||||
{
|
||||
"name": "@api.global/typedserver",
|
||||
"version": "3.0.11",
|
||||
"description": "easy serving of static files",
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
"version": "3.0.36",
|
||||
"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",
|
||||
"./ts": "./dist_ts/index.js",
|
||||
"./ts_edgeworker": "./dist_ts_edgeworker",
|
||||
"./ts_web_inject": "./dist_ts_web_inject/index.js",
|
||||
"./ts_web_serviceworker": "./dist_ts_web_serviceworker",
|
||||
"./ts_web_serviceworker_client": "./dist_ts_web_serviceworker_client"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "npm run build && tstest test/",
|
||||
"build": "tsbuild --web --allowimplicitany && tsbundle --from ./ts_web/index.ts --to ./dist_ts_web/bundle.js",
|
||||
"buildDocs": "tsdoc"
|
||||
"build": "tsbuild tsfolders --web --allowimplicitany --skiplibcheck && npm run bundle",
|
||||
"bundle": "tsbundle --from ./ts_web_inject/index.ts --to ./dist_ts_web_inject/bundle.js && tsbundle --from ./ts_web_serviceworker/index.ts --to ./dist_ts_web_serviceworker/serviceworker.bundle.js",
|
||||
"interfaces": "tsbuild interfaces --web --allowimplicitany --skiplibcheck",
|
||||
"docs": "tsdoc aidoc"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/pushrocks/easyserve.git"
|
||||
},
|
||||
"keywords": [
|
||||
"serve",
|
||||
"browser-sync"
|
||||
"TypeScript",
|
||||
"static file serving",
|
||||
"live reload",
|
||||
"compression",
|
||||
"typed requests",
|
||||
"HTTP server",
|
||||
"SSL",
|
||||
"cors",
|
||||
"express middleware",
|
||||
"proxy",
|
||||
"sitemap",
|
||||
"feeds",
|
||||
"robots.txt",
|
||||
"compression (gzip, deflate, brotli)"
|
||||
],
|
||||
"author": "Lossless GmbH <office@lossless.com> (https://lossless.com)",
|
||||
"license": "MIT",
|
||||
@ -37,43 +57,51 @@
|
||||
],
|
||||
"homepage": "https://github.com/pushrocks/easyserve",
|
||||
"dependencies": {
|
||||
"@api.global/typedrequest": "^3.0.4",
|
||||
"@api.global/typedrequest-interfaces": "^3.0.1",
|
||||
"@api.global/typedsocket": "^3.0.0",
|
||||
"@push.rocks/lik": "^6.0.12",
|
||||
"@push.rocks/smartchok": "^1.0.23",
|
||||
"@api.global/typedrequest": "^3.0.23",
|
||||
"@api.global/typedrequest-interfaces": "^3.0.19",
|
||||
"@api.global/typedsocket": "^3.0.1",
|
||||
"@cloudflare/workers-types": "^4.20240512.0",
|
||||
"@design.estate/dees-comms": "^1.0.24",
|
||||
"@push.rocks/lik": "^6.0.15",
|
||||
"@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.4",
|
||||
"@push.rocks/smartfile": "^11.0.14",
|
||||
"@push.rocks/smartjson": "^5.0.19",
|
||||
"@push.rocks/smartlog": "^3.0.3",
|
||||
"@push.rocks/smartlog-destination-devtools": "^1.0.10",
|
||||
"@push.rocks/smartlog-interfaces": "^3.0.0",
|
||||
"@push.rocks/smartmanifest": "^2.0.2",
|
||||
"@push.rocks/smartmatch": "^2.0.0",
|
||||
"@push.rocks/smartmime": "^1.0.5",
|
||||
"@push.rocks/smartntml": "^2.0.4",
|
||||
"@push.rocks/smartopen": "^2.0.0",
|
||||
"@push.rocks/smartpath": "^5.0.11",
|
||||
"@push.rocks/smartpath": "^5.0.18",
|
||||
"@push.rocks/smartpromise": "^4.0.2",
|
||||
"@push.rocks/smartrequest": "^2.0.21",
|
||||
"@push.rocks/smartrequest": "^2.0.22",
|
||||
"@push.rocks/smartrx": "^3.0.7",
|
||||
"@push.rocks/smartsitemap": "^2.0.3",
|
||||
"@push.rocks/smartstream": "^3.0.30",
|
||||
"@push.rocks/smartstream": "^3.0.35",
|
||||
"@push.rocks/smarttime": "^4.0.6",
|
||||
"@push.rocks/webstore": "^2.0.13",
|
||||
"@tsclass/tsclass": "^4.0.46",
|
||||
"@push.rocks/taskbuffer": "^3.1.7",
|
||||
"@push.rocks/webrequest": "^3.0.37",
|
||||
"@push.rocks/webstore": "^2.0.14",
|
||||
"@tsclass/tsclass": "^4.0.54",
|
||||
"@types/express": "^4.17.21",
|
||||
"body-parser": "^1.20.2",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"express": "^4.19.2",
|
||||
"express-force-ssl": "^0.3.2",
|
||||
"lit": "^3.1.0"
|
||||
"lit": "^3.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.1.66",
|
||||
"@git.zone/tsbundle": "^2.0.8",
|
||||
"@git.zone/tsbuild": "^2.1.76",
|
||||
"@git.zone/tsbundle": "^2.0.15",
|
||||
"@git.zone/tsrun": "^1.2.44",
|
||||
"@git.zone/tstest": "^1.0.84",
|
||||
"@push.rocks/tapbundle": "^5.0.15",
|
||||
"@types/node": "^20.10.7"
|
||||
"@git.zone/tstest": "^1.0.90",
|
||||
"@push.rocks/tapbundle": "^5.0.23",
|
||||
"@types/node": "^20.12.11"
|
||||
},
|
||||
"private": false,
|
||||
"browserslist": [
|
||||
|
8610
pnpm-lock.yaml
generated
8610
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
1
readme.hints.md
Normal file
1
readme.hints.md
Normal file
@ -0,0 +1 @@
|
||||
|
154
readme.md
154
readme.md
@ -1,49 +1,133 @@
|
||||
# @pushrocks/typedserver
|
||||
easy serving of static files
|
||||
```markdown
|
||||
# @api.global/typedserver
|
||||
Easy serving of static files
|
||||
|
||||
## Availabililty and Links
|
||||
* [npmjs.org (npm package)](https://www.npmjs.com/package/@apiglobal/typedserver)
|
||||
* [gitlab.com (source)](https://gitlab.com/pushrocks/typedserver)
|
||||
* [github.com (source mirror)](https://github.com/pushrocks/typedserver)
|
||||
* [docs (typedoc)](https://pushrocks.gitlab.io/typedserver/)
|
||||
## Install
|
||||
To install @api.global/typedserver, run the following command in your terminal:
|
||||
|
||||
## Status for master
|
||||
```bash
|
||||
npm install @api.global/typedserver --save
|
||||
```
|
||||
|
||||
Status Category | Status Badge
|
||||
-- | --
|
||||
GitLab Pipelines | [](https://lossless.cloud)
|
||||
GitLab Pipline Test Coverage | [](https://lossless.cloud)
|
||||
npm | [](https://lossless.cloud)
|
||||
Snyk | [](https://lossless.cloud)
|
||||
TypeScript Support | [](https://lossless.cloud)
|
||||
node Support | [](https://nodejs.org/dist/latest-v10.x/docs/api/)
|
||||
Code Style | [](https://lossless.cloud)
|
||||
PackagePhobia (total standalone install weight) | [](https://lossless.cloud)
|
||||
PackagePhobia (package size on registry) | [](https://lossless.cloud)
|
||||
BundlePhobia (total size when bundled) | [](https://lossless.cloud)
|
||||
This will add `@api.global/typedserver` to your project's dependencies.
|
||||
|
||||
## Usage
|
||||
|
||||
Use TypeScript for best in class instellisense.
|
||||
`@api.global/typedserver` is designed to make serving static files and handling web requests in a TypeScript environment easy and efficient. It leverages Express under the hood, providing a powerful API for web server creation with additional utilities for live reloading, typed requests/responses, and more, embracing TypeScript's static typing advantages.
|
||||
|
||||
```javascript
|
||||
import { TypedServer } from '@apiglobal/typedserver';
|
||||
### Setting up a Basic Web Server
|
||||
|
||||
let myTypedserver = new TypedServer('/some/path/to/webroot', 8080);
|
||||
myTypedserver.start().then(() => {
|
||||
// this is executed when server is running guaranteed
|
||||
myTypedserver.stop(); // .stop() will work even if not waiting for server to be fully started
|
||||
});
|
||||
The following example demonstrates how to set up a basic web server serving files from a directory.
|
||||
|
||||
myTypedserver.reload(); // reloads all connected browsers of this instance
|
||||
```typescript
|
||||
import { TypedServer } from '@api.global/typedserver';
|
||||
|
||||
const serverOptions = {
|
||||
port: 8080, // Port to listen on
|
||||
serveDir: 'public', // Directory to serve static files from
|
||||
watch: true, // Enable live reloading of changes
|
||||
injectReload: true, // Inject live reload script into served HTML files
|
||||
cors: true // Enable CORS
|
||||
};
|
||||
|
||||
const typedServer = new TypedServer(serverOptions);
|
||||
|
||||
async function startServer() {
|
||||
await typedServer.start();
|
||||
console.log(`Server is running on http://localhost:${serverOptions.port}`);
|
||||
}
|
||||
|
||||
startServer().catch(console.error);
|
||||
```
|
||||
|
||||
## Contribution
|
||||
In the example above, `TypedServer` is instantiated with an `IServerOptions` object specifying options like the port to listen on (`8080`), the directory containing static files to serve (`public`), and live reload features. Calling `start()` on the `typedServer` instance initiates the server.
|
||||
|
||||
We are always happy for code contributions. If you are not the code contributing type that is ok. Still, maintaining Open Source repositories takes considerable time and thought. If you like the quality of what we do and our modules are useful to you we would appreciate a little monthly contribution: You can [contribute one time](https://lossless.link/contribute-onetime) or [contribute monthly](https://lossless.link/contribute). :)
|
||||
### Using Typed Requests and Responses
|
||||
|
||||
For further information read the linked docs at the top of this readme.
|
||||
`TypedServer` supports typed requests and responses, making API development more robust and maintainable. Define your request and response types, and use them to type-check incoming requests and their responses.
|
||||
|
||||
## Legal
|
||||
> MIT licensed | **©** [Task Venture Capital GmbH](https://task.vc)
|
||||
| By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy)
|
||||
First, define the types:
|
||||
|
||||
```typescript
|
||||
// Define a request type
|
||||
interface MyCustomRequest {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
// Define a response type
|
||||
interface MyCustomResponse {
|
||||
userName: string;
|
||||
}
|
||||
```
|
||||
|
||||
Next, set up a route to handle requests using these types:
|
||||
|
||||
```typescript
|
||||
import { TypedRouter, TypedHandler } from '@api.global/typedrequest';
|
||||
|
||||
// Instantiate a TypedRouter
|
||||
const typedRouter = new TypedRouter();
|
||||
|
||||
// Register a route with request and response types
|
||||
typedRouter.addTypedHandler<MyCustomRequest, MyCustomResponse>(
|
||||
new TypedHandler('getUser', async (requestData) => {
|
||||
// Implement your logic here. For example, fetch user data from a database.
|
||||
const userData = { userName: 'John Doe' }; // Dummy implementation
|
||||
return { response: userData };
|
||||
})
|
||||
);
|
||||
|
||||
// Bind the typed router to the server
|
||||
typedServer.useTypedRouter(typedRouter);
|
||||
|
||||
// Now, the route is set up to handle requests with type checking
|
||||
```
|
||||
|
||||
This example shows defining types for requests and responses, creating a `TypedRouter`, and adding a route with typed handling. This feature brings the benefits of TypeScript's static type checking to server-side logic, improving the development experience.
|
||||
|
||||
### Enabling SSL/TLS
|
||||
|
||||
To enable SSL/TLS, configure the `TypedServer` with the SSL options, including the paths to your SSL certificate and key files:
|
||||
|
||||
```typescript
|
||||
const serverOptions = {
|
||||
port: 443,
|
||||
serveDir: 'public',
|
||||
watch: true,
|
||||
injectReload: true,
|
||||
cors: true,
|
||||
privateKey: fs.readFileSync('path/to/ssl/private.key'),
|
||||
publicKey: fs.readFileSync('path/to/ssl/certificate.crt')
|
||||
};
|
||||
|
||||
const typedServer = new TypedServer(serverOptions);
|
||||
startServer().catch(console.error);
|
||||
```
|
||||
|
||||
Replace `'path/to/ssl/private.key'` and `'path/to/ssl/certificate.crt'` with the actual paths to your SSL key and certificate files. This setup ensures that your server communicates over HTTPS, encrypting the data transmitted between the client and the server.
|
||||
|
||||
### Conclusion
|
||||
|
||||
`@api.global/typedserver` offers a streamlined way to set up a web server with TypeScript, featuring static file serving, live reloading, typed request/response handling, and SSL support. This guide covers the basic usage, but `TypedServer` is highly configurable, catering to various hosting and development needs.
|
||||
```
|
||||
|
||||
For a deeper dive into the API and more advanced configurations, refer to the official documentation and type definitions included in the package.
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
### Trademarks
|
||||
|
||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Registered at District court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
||||
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@api.global/typedserver',
|
||||
version: '3.0.11',
|
||||
description: 'easy serving of static files'
|
||||
version: '3.0.36',
|
||||
description: 'A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.'
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import * as plugins from './typedserver.plugins.js';
|
||||
import * as paths from './typedserver.paths.js';
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
import * as plugins from './plugins.js';
|
||||
import * as paths from './paths.js';
|
||||
import * as interfaces from '../dist_ts_interfaces/index.js';
|
||||
import * as servertools from './servertools/index.js';
|
||||
import { type TCompressionMethod } from './servertools/classes.compressor.js';
|
||||
|
||||
export interface IServerOptions {
|
||||
/**
|
||||
@ -14,6 +15,16 @@ export interface IServerOptions {
|
||||
*/
|
||||
injectReload?: boolean;
|
||||
|
||||
/**
|
||||
* enable compression
|
||||
*/
|
||||
enableCompression?: boolean;
|
||||
|
||||
/**
|
||||
* choose a preferred compression method
|
||||
*/
|
||||
preferredCompressionMethod?: TCompressionMethod;
|
||||
|
||||
/**
|
||||
* watch the serve directory?
|
||||
*/
|
||||
@ -92,7 +103,7 @@ export class TypedServer {
|
||||
case 'devtools':
|
||||
res.setHeader('Content-Type', 'text/javascript');
|
||||
res.status(200);
|
||||
res.write(plugins.smartfile.fs.toStringSync(paths.bundlePath));
|
||||
res.write(plugins.smartfile.fs.toStringSync(paths.injectBundlePath));
|
||||
res.end();
|
||||
break;
|
||||
case 'reloadcheck':
|
||||
@ -157,6 +168,8 @@ export class TypedServer {
|
||||
};
|
||||
},
|
||||
serveIndexHtmlDefault: true,
|
||||
enableCompression: this.options.enableCompression,
|
||||
preferredCompressionMethod: this.options.preferredCompressionMethod,
|
||||
})
|
||||
);
|
||||
} else if (this.options.injectReload) {
|
||||
@ -165,7 +178,7 @@ export class TypedServer {
|
||||
);
|
||||
}
|
||||
if (this.options.watch && this.options.serveDir) {
|
||||
this.smartchokInstance = new plugins.smartchok.Smartchok([this.options.serveDir], {});
|
||||
this.smartchokInstance = new plugins.smartchok.Smartchok([this.options.serveDir]);
|
||||
await this.smartchokInstance.start();
|
||||
(await this.smartchokInstance.getObservableFor('change')).subscribe(async () => {
|
||||
await this.createServeDirHash();
|
@ -1,10 +1,15 @@
|
||||
import * as plugins from './typedserver.plugins.js';
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
import * as servertools from './servertools/index.js';
|
||||
|
||||
export { servertools };
|
||||
|
||||
export * from './typedserver.classes.typedserver.js';
|
||||
export * from './classes.typedserver.js';
|
||||
// Type helpers
|
||||
export type Request = plugins.express.Request;
|
||||
export type Response = plugins.express.Response;
|
||||
|
||||
|
||||
// lets export utilityservers
|
||||
import * as utilityservers from './utilityservers/index.js';
|
||||
export { utilityservers };
|
||||
|
8
ts/infohtml/00_commitinfo_data.ts
Normal file
8
ts/infohtml/00_commitinfo_data.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @pushrocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@losslessone_private/lole-infohtml',
|
||||
version: '1.0.39',
|
||||
description: 'html for displaying infos at lossless'
|
||||
}
|
44
ts/infohtml/index.ts
Normal file
44
ts/infohtml/index.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import * as plugins from './infohtml.plugins.js';
|
||||
|
||||
import { simpleInfo } from './template.js';
|
||||
|
||||
export interface IHtmlInfoOptions {
|
||||
text: string;
|
||||
heading?: string;
|
||||
title?: string;
|
||||
sentryMessage?: string;
|
||||
sentryDsn?: string;
|
||||
redirectTo?: string;
|
||||
}
|
||||
|
||||
export class InfoHtml {
|
||||
// STATIC
|
||||
public static async fromSimpleText(textArg: string) {
|
||||
const infohtmlInstance = new InfoHtml({
|
||||
text: textArg,
|
||||
heading: null,
|
||||
});
|
||||
await infohtmlInstance.init();
|
||||
return infohtmlInstance;
|
||||
}
|
||||
|
||||
public static async fromOptions(optionsArg: IHtmlInfoOptions) {
|
||||
const infohtmlInstance = new InfoHtml(optionsArg);
|
||||
await infohtmlInstance.init();
|
||||
return infohtmlInstance;
|
||||
}
|
||||
|
||||
// INSTANCE
|
||||
public options: IHtmlInfoOptions;
|
||||
public smartntmlInstance: plugins.smartntml.Smartntml;
|
||||
public htmlString: string;
|
||||
constructor(optionsArg: IHtmlInfoOptions) {
|
||||
this.options = optionsArg;
|
||||
}
|
||||
|
||||
public async init() {
|
||||
this.smartntmlInstance = new plugins.smartntml.Smartntml();
|
||||
this.htmlString = await simpleInfo(this.smartntmlInstance, this.options);
|
||||
return this.htmlString;
|
||||
}
|
||||
}
|
3
ts/infohtml/infohtml.plugins.ts
Normal file
3
ts/infohtml/infohtml.plugins.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import * as smartntml from '@push.rocks/smartntml';
|
||||
|
||||
export { smartntml };
|
160
ts/infohtml/template.ts
Normal file
160
ts/infohtml/template.ts
Normal file
@ -0,0 +1,160 @@
|
||||
import * as plugins from './infohtml.plugins.js';
|
||||
import { type IHtmlInfoOptions } from './index.js';
|
||||
|
||||
export const simpleInfo = async (
|
||||
smartntmlInstanceArg: plugins.smartntml.Smartntml,
|
||||
optionsArg: IHtmlInfoOptions
|
||||
) => {
|
||||
const html = plugins.smartntml.deesElement.html;
|
||||
const htmlTemplate = await plugins.smartntml.deesElement.html`
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>${optionsArg.title}</title>
|
||||
<script>
|
||||
setTimeout(() => {
|
||||
const redirectUrl = '${optionsArg.redirectTo}';
|
||||
if (redirectUrl) {
|
||||
window.location = redirectUrl;
|
||||
}
|
||||
}, 5000);
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0px;
|
||||
background: #000000;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
min-height: 100vh;
|
||||
min-width: 100vw;
|
||||
border: 1px solid #e4002b;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 150px;
|
||||
padding-top: 70px;
|
||||
margin: 0px auto 30px auto;
|
||||
}
|
||||
|
||||
.content {
|
||||
text-align: center;
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.content .maintext {
|
||||
margin: 10px;
|
||||
color: #ffffff;
|
||||
background: #333;
|
||||
display: block;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.content .maintext h1 {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.content .addontext {
|
||||
margin: 10px;
|
||||
color: #ffffff;
|
||||
background: #222;
|
||||
display: block;
|
||||
padding: 10px 15px;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.content .text h1 {
|
||||
margin: 0px;
|
||||
font-weight: 100;
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.content .text ul {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.legal {
|
||||
color: #fff;
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
width: 100vw;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
<meta
|
||||
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[] = [];
|
||||
if (optionsArg.heading) {
|
||||
returnArray.push(html`
|
||||
<div class="maintext">
|
||||
<h1>${optionsArg.heading}</h1>
|
||||
${optionsArg.text}
|
||||
</div>
|
||||
`);
|
||||
} 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">
|
||||
We will redirect you to ${optionsArg.redirectTo} in a few seconds.
|
||||
</div>`
|
||||
);
|
||||
}
|
||||
return returnArray;
|
||||
})()}
|
||||
</div>
|
||||
<div class="legal">
|
||||
<a href="https://lossless.com">Lossless GmbH</a> / © 2014-${new Date().getFullYear()}
|
||||
/ <a href="https://lossless.gmbh">Legal Info</a> /
|
||||
<a href="https://lossless.gmbh">Privacy Policy</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
return smartntmlInstanceArg.renderTemplateResult(htmlTemplate);
|
||||
};
|
11
ts/paths.ts
Normal file
11
ts/paths.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export const packageDir = plugins.path.join(
|
||||
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
||||
'../'
|
||||
);
|
||||
|
||||
export const injectBundleDir = plugins.path.join(packageDir, './dist_ts_web_inject');
|
||||
export const injectBundlePath = plugins.path.join(injectBundleDir, './bundle.js');
|
||||
|
||||
export const serviceworkerBundleDir = plugins.path.join(packageDir, './dist_ts_web_serviceworker');
|
@ -3,6 +3,7 @@ import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import * as net from 'net';
|
||||
import * as path from 'path';
|
||||
import * as stream from 'stream';
|
||||
import * as zlib from 'zlib';
|
||||
|
||||
export { http, https, net, path, zlib };
|
||||
@ -25,6 +26,7 @@ import * as smartchok from '@push.rocks/smartchok';
|
||||
import * as smartdelay from '@push.rocks/smartdelay';
|
||||
import * as smartfeed from '@push.rocks/smartfeed';
|
||||
import * as smartfile from '@push.rocks/smartfile';
|
||||
import * as smartjson from '@push.rocks/smartjson';
|
||||
import * as smartmanifest from '@push.rocks/smartmanifest';
|
||||
import * as smartmime from '@push.rocks/smartmime';
|
||||
import * as smartopen from '@push.rocks/smartopen';
|
||||
@ -42,6 +44,7 @@ export {
|
||||
smartdelay,
|
||||
smartfeed,
|
||||
smartfile,
|
||||
smartjson,
|
||||
smartmanifest,
|
||||
smartmime,
|
||||
smartopen,
|
@ -1,45 +1,90 @@
|
||||
import * as plugins from '../typedserver.plugins.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
export type TCompressionMethod = 'gzip' | 'deflate' | 'br' | 'none';
|
||||
export interface ICompressionResult {
|
||||
result: Buffer;
|
||||
compressionMethod: TCompressionMethod;
|
||||
}
|
||||
|
||||
export class Compressor {
|
||||
private _cache: Map<string, Buffer>;
|
||||
private MAX_CACHE_SIZE: number = 100 * 1024 * 1024; // 100 MB
|
||||
|
||||
constructor() {
|
||||
this._cache = new Map<string, Buffer>();
|
||||
}
|
||||
|
||||
private _addToCache(key: string, value: Buffer) {
|
||||
this._cache.set(key, value);
|
||||
this._manageCacheSize();
|
||||
}
|
||||
|
||||
private _manageCacheSize() {
|
||||
let currentSize = Array.from(this._cache.values()).reduce((acc, buffer) => acc + buffer.length, 0);
|
||||
|
||||
while (currentSize > this.MAX_CACHE_SIZE) {
|
||||
const firstKey = this._cache.keys().next().value;
|
||||
const firstValue = this._cache.get(firstKey)!;
|
||||
currentSize -= firstValue.length;
|
||||
this._cache.delete(firstKey);
|
||||
}
|
||||
}
|
||||
|
||||
public async compressContent(
|
||||
content: Buffer,
|
||||
method: 'gzip' | 'deflate' | 'br' | 'none'
|
||||
): Promise<Buffer> {
|
||||
const cacheKey = content.toString('base64') + method;
|
||||
const cachedResult = this._cache.get(cacheKey);
|
||||
|
||||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const callback = (err: Error | null, result: Buffer) => {
|
||||
if (err) reject(err);
|
||||
else {
|
||||
this._addToCache(cacheKey, result);
|
||||
resolve(result);
|
||||
}
|
||||
};
|
||||
|
||||
switch (method) {
|
||||
case 'gzip':
|
||||
plugins.zlib.gzip(content, (err, result) => {
|
||||
if (err) reject(err);
|
||||
else resolve(result);
|
||||
});
|
||||
plugins.zlib.gzip(content, {
|
||||
level: 1,
|
||||
},callback,);
|
||||
break;
|
||||
case 'br':
|
||||
plugins.zlib.brotliCompress(content, (err, result) => {
|
||||
if (err) reject(err);
|
||||
else resolve(result);
|
||||
});
|
||||
plugins.zlib.brotliCompress(content, {}, callback);
|
||||
break;
|
||||
case 'deflate':
|
||||
plugins.zlib.deflate(content, (err, result) => {
|
||||
if (err) reject(err);
|
||||
else resolve(result);
|
||||
});
|
||||
plugins.zlib.deflate(content, callback);
|
||||
break;
|
||||
default:
|
||||
this._addToCache(cacheKey, content);
|
||||
resolve(content);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public determineCompression(acceptEncoding: string | string[]) {
|
||||
public determineCompression(acceptEncoding: string | string[], preferredCompressionMethodsArg: TCompressionMethod[] = []) {
|
||||
// Ensure acceptEncoding is a single string
|
||||
const encodingString = Array.isArray(acceptEncoding)
|
||||
? acceptEncoding.join(', ')
|
||||
: acceptEncoding;
|
||||
|
||||
let compressionMethod: 'gzip' | 'deflate' | 'br' | 'none' = 'none';
|
||||
let compressionMethod: TCompressionMethod = 'none';
|
||||
|
||||
// Check and prioritize compression methods
|
||||
// Prioritize preferred compression methods if provided
|
||||
for (const preferredMethod of preferredCompressionMethodsArg) {
|
||||
if (new RegExp(`\\b${preferredMethod}\\b`).test(encodingString)) {
|
||||
return preferredMethod;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to default prioritization if no preferred method matches
|
||||
if (/\bbr\b/.test(encodingString)) {
|
||||
compressionMethod = 'br';
|
||||
} else if (/\bgzip\b/.test(encodingString)) {
|
||||
@ -51,9 +96,9 @@ export class Compressor {
|
||||
return compressionMethod;
|
||||
}
|
||||
|
||||
public async maybeCompress(requestHeaders: plugins.http.IncomingHttpHeaders, content: Buffer) {
|
||||
public async maybeCompress(requestHeaders: plugins.http.IncomingHttpHeaders, content: Buffer, preferredCompressionMethodsArg?: TCompressionMethod[]): Promise<ICompressionResult> {
|
||||
const acceptEncoding = requestHeaders['accept-encoding'];
|
||||
const compressionMethod = this.determineCompression(acceptEncoding);
|
||||
const compressionMethod = this.determineCompression(acceptEncoding, preferredCompressionMethodsArg);
|
||||
const result = await this.compressContent(content, compressionMethod);
|
||||
return {
|
||||
result,
|
||||
@ -62,15 +107,21 @@ export class Compressor {
|
||||
}
|
||||
|
||||
public createCompressionStream(method: 'gzip' | 'deflate' | 'br' | 'none') {
|
||||
let compressionStream: any;
|
||||
switch (method) {
|
||||
case 'gzip':
|
||||
return plugins.zlib.createGzip();
|
||||
compressionStream = plugins.zlib.createGzip();
|
||||
case 'br':
|
||||
return plugins.zlib.createBrotliCompress();
|
||||
compressionStream = plugins.zlib.createBrotliCompress({
|
||||
chunkSize: 16 * 1024,
|
||||
params: {
|
||||
|
||||
},
|
||||
});
|
||||
case 'deflate':
|
||||
return plugins.zlib.createDeflate();
|
||||
compressionStream = plugins.zlib.createDeflate();
|
||||
default:
|
||||
throw new Error('Invalid compression method');
|
||||
compressionStream = plugins.smartstream.createPassThrough();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Handler } from './classes.handler.js';
|
||||
import { Server } from './classes.server.js';
|
||||
import * as plugins from '../typedserver.plugins.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
export class Feed {
|
||||
public smartexpressRef: Server;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import * as plugins from '../typedserver.plugins.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import { type Request, type Response } from 'express';
|
||||
|
||||
export interface IHandlerFunction {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as plugins from '../typedserver.plugins.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import { Handler } from './classes.handler.js';
|
||||
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||
|
||||
export class HandlerProxy extends Handler {
|
||||
/**
|
||||
|
@ -1,8 +1,8 @@
|
||||
import * as plugins from '../typedserver.plugins.js';
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||
|
||||
import { Handler } from './classes.handler.js';
|
||||
import { Compressor } from './classes.compressor.js';
|
||||
import { Compressor, type TCompressionMethod, type ICompressionResult } from './classes.compressor.js';
|
||||
|
||||
export class HandlerStatic extends Handler {
|
||||
public compressor = new Compressor();
|
||||
@ -13,6 +13,8 @@ export class HandlerStatic extends Handler {
|
||||
responseModifier?: interfaces.TResponseModifier;
|
||||
headers?: { [key: string]: string };
|
||||
serveIndexHtmlDefault?: boolean;
|
||||
enableCompression?: boolean;
|
||||
preferredCompressionMethod?: TCompressionMethod;
|
||||
}
|
||||
) {
|
||||
super('GET', async (req, res) => {
|
||||
@ -118,10 +120,21 @@ export class HandlerStatic extends Handler {
|
||||
}
|
||||
|
||||
// lets finally deal with compression
|
||||
const compressionResult = await this.compressor.maybeCompress(requestHeaders, fileBuffer);
|
||||
let compressionResult: ICompressionResult;
|
||||
|
||||
if (optionsArg && optionsArg.enableCompression) {
|
||||
compressionResult = await this.compressor.maybeCompress(requestHeaders, fileBuffer, [optionsArg.preferredCompressionMethod]);
|
||||
} else {
|
||||
compressionResult = {
|
||||
compressionMethod: 'none',
|
||||
result: fileBuffer,
|
||||
};
|
||||
}
|
||||
|
||||
res.status(200);
|
||||
res.header('Content-Encoding', compressionResult.compressionMethod);
|
||||
if (compressionResult?.compressionMethod) {
|
||||
res.header('Content-Encoding', compressionResult.compressionMethod);
|
||||
}
|
||||
res.write(compressionResult.result);
|
||||
res.end();
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as plugins from '../typedserver.plugins.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import { Handler } from './classes.handler.js';
|
||||
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||
|
||||
export class HandlerTypedRouter extends Handler {
|
||||
/**
|
||||
@ -11,7 +11,9 @@ export class HandlerTypedRouter extends Handler {
|
||||
constructor(typedrouter: plugins.typedrequest.TypedRouter) {
|
||||
super('POST', async (req, res) => {
|
||||
const response = await typedrouter.routeAndAddResponse(req.body);
|
||||
res.json(response);
|
||||
res.type('json');
|
||||
res.write(plugins.smartjson.stringify(response));
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import * as plugins from '../typedserver.plugins.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import { Handler } from './classes.handler.js';
|
||||
import { Server } from './classes.server.js';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import * as plugins from '../typedserver.plugins.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
import { Route } from './classes.route.js';
|
||||
import { Handler } from './classes.handler.js';
|
||||
@ -9,7 +9,7 @@ import { setupRobots } from './tools.robots.js';
|
||||
import { setupManifest } from './tools.manifest.js';
|
||||
import { Sitemap } from './classes.sitemap.js';
|
||||
import { Feed } from './classes.feed.js';
|
||||
import { type IServerOptions } from '../typedserver.classes.typedserver.js';
|
||||
import { type IServerOptions } from '../classes.typedserver.js';
|
||||
export type TServerStatus = 'initiated' | 'running' | 'stopped';
|
||||
|
||||
/**
|
||||
@ -142,7 +142,24 @@ export class Server {
|
||||
});
|
||||
|
||||
// body parsing
|
||||
this.expressAppInstance.use(plugins.bodyParser.json({ limit: 100000000 })); // for parsing application/json
|
||||
this.expressAppInstance.use(async (req, res, next) => {
|
||||
if (req.headers['content-type'] === 'application/json') {
|
||||
let data = '';
|
||||
req.on('data', chunk => {
|
||||
data += chunk;
|
||||
});
|
||||
req.on('end', () => {
|
||||
try {
|
||||
req.body = plugins.smartjson.parse(data);
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(400).send('Invalid JSON');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
this.expressAppInstance.use(plugins.bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
|
||||
|
||||
// robots
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Server } from './classes.server.js';
|
||||
import { Handler } from './classes.handler.js';
|
||||
import * as plugins from '../typedserver.plugins.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import { type IUrlInfo } from '@push.rocks/smartsitemap';
|
||||
|
||||
export class Sitemap {
|
||||
|
@ -4,3 +4,9 @@ export * from './classes.handler.js';
|
||||
export * from './classes.handlerstatic.js';
|
||||
export * from './classes.handlerproxy.js';
|
||||
export * from './classes.handlertypedrouter.js';
|
||||
export * from './classes.compressor.js';
|
||||
import * as serviceworker from './tools.serviceworker.js';
|
||||
|
||||
export {
|
||||
serviceworker,
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import * as plugins from '../typedserver.plugins.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
export const setupManifest = async (
|
||||
expressInstanceArg: plugins.express.Application,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import * as plugins from '../typedserver.plugins.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import { Server } from './classes.server.js';
|
||||
import { Handler } from './classes.handler.js';
|
||||
|
||||
|
60
ts/servertools/tools.serviceworker.ts
Normal file
60
ts/servertools/tools.serviceworker.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as paths from '../paths.js';
|
||||
|
||||
import * as interfaces from '../../dist_ts_interfaces/index.js'
|
||||
import { Handler } from './classes.handler.js';
|
||||
import type { TypedServer } from '../classes.typedserver.js';
|
||||
import { HandlerTypedRouter } from './classes.handlertypedrouter.js';
|
||||
|
||||
const swBundleJs: string = plugins.smartfile.fs.toStringSync(
|
||||
plugins.path.join(paths.serviceworkerBundleDir, './serviceworker.bundle.js')
|
||||
);
|
||||
const swBundleJsMap: string = plugins.smartfile.fs.toStringSync(
|
||||
plugins.path.join(paths.serviceworkerBundleDir, './serviceworker.bundle.js.map')
|
||||
);
|
||||
let swVersionInfo: interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response'] =
|
||||
null;
|
||||
const serviceworkerHandler = new Handler(
|
||||
'GET',
|
||||
async (req, res) => {
|
||||
if (req.path === '/serviceworker.bundle.js') {
|
||||
res.status(200);
|
||||
res.set('Content-Type', 'text/javascript');
|
||||
res.write(swBundleJs + '\n' + `/** appSemVer: ${swVersionInfo?.appSemVer || 'not set'} */`);
|
||||
} else if (req.path === '/serviceworker.bundle.js.map') {
|
||||
res.status(200);
|
||||
res.set('Content-Type', 'application/json');
|
||||
res.write(swBundleJsMap);
|
||||
}
|
||||
res.end();
|
||||
}
|
||||
);
|
||||
|
||||
export const addServiceWorkerRoute = (
|
||||
typedserverInstance: TypedServer,
|
||||
swDataFunc: () => interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response']
|
||||
) => {
|
||||
// lets the version info as unique string;
|
||||
swVersionInfo = swDataFunc();
|
||||
|
||||
// the basic stuff
|
||||
typedserverInstance.server.addRoute('/serviceworker.js*', serviceworkerHandler);
|
||||
|
||||
// the typed stuff
|
||||
const typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo>(
|
||||
'serviceworker_versionInfo',
|
||||
async (req) => {
|
||||
const versionInfoResponse = swDataFunc();
|
||||
return versionInfoResponse;
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
typedserverInstance.server.addRoute(
|
||||
'/sw-typedrequest',
|
||||
new HandlerTypedRouter(typedrouter)
|
||||
);
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import * as plugins from '../typedserver.plugins.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import { Server } from './classes.server.js';
|
||||
import { Handler } from './classes.handler.js';
|
||||
|
||||
|
@ -1,8 +0,0 @@
|
||||
import * as plugins from './typedserver.plugins.js';
|
||||
|
||||
export const packageDir = plugins.path.join(
|
||||
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
||||
'../'
|
||||
);
|
||||
|
||||
export const bundlePath = plugins.path.join(packageDir, './dist_ts_web/bundle.js');
|
51
ts/utilityservers/classes.serviceserver.ts
Normal file
51
ts/utilityservers/classes.serviceserver.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { TypedServer } from '../classes.typedserver.js';
|
||||
import * as servertools from '../servertools/index.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
export interface ILoleServiceServerConstructorOptions {
|
||||
addCustomRoutes?: (serverArg: servertools.Server) => Promise<any>;
|
||||
serviceName: string;
|
||||
serviceVersion: string;
|
||||
serviceDomain: string;
|
||||
port?: number;
|
||||
}
|
||||
|
||||
// the main service server
|
||||
export class ServiceServer {
|
||||
public options: ILoleServiceServerConstructorOptions;
|
||||
public typedServer: TypedServer;
|
||||
|
||||
constructor(optionsArg: ILoleServiceServerConstructorOptions) {
|
||||
this.options = optionsArg;
|
||||
}
|
||||
|
||||
public async start() {
|
||||
console.log('starting lole-serviceserver...')
|
||||
this.typedServer = new TypedServer({
|
||||
cors: true,
|
||||
domain: this.options.serviceDomain,
|
||||
forceSsl: false,
|
||||
port: this.options.port || 3000,
|
||||
robots: true,
|
||||
defaultAnswer: async () => {
|
||||
const InfoHtml = (await import('../infohtml/index.js')).InfoHtml;
|
||||
return (
|
||||
await InfoHtml.fromSimpleText(
|
||||
`${this.options.serviceName} (version ${this.options.serviceVersion})`
|
||||
)
|
||||
).htmlString;
|
||||
},
|
||||
});
|
||||
|
||||
// lets add any custom routes
|
||||
if (this.options.addCustomRoutes) {
|
||||
await this.options.addCustomRoutes(this.typedServer.server);
|
||||
}
|
||||
|
||||
await this.typedServer.start();
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
await this.typedServer.stop();
|
||||
}
|
||||
}
|
137
ts/utilityservers/classes.websiteserver.ts
Normal file
137
ts/utilityservers/classes.websiteserver.ts
Normal file
@ -0,0 +1,137 @@
|
||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||
import { type IServerOptions, TypedServer } from '../classes.typedserver.js';
|
||||
import type { Request, Response } from '../index.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as servertools from '../servertools/index.js';
|
||||
|
||||
export interface IUtilityWebsiteServerConstructorOptions {
|
||||
addCustomRoutes?: (serverArg: servertools.Server) => Promise<any>;
|
||||
appSemVer?: string;
|
||||
domain: string;
|
||||
serveDir: string;
|
||||
feedMetadata: IServerOptions['feedMetadata'];
|
||||
}
|
||||
|
||||
/**
|
||||
* the utility website server implements a best practice server for websites
|
||||
* It supports:
|
||||
* * live reload
|
||||
* * compression
|
||||
* * serviceworker
|
||||
* * pwa manifest
|
||||
*/
|
||||
export class UtilityWebsiteServer {
|
||||
public options: IUtilityWebsiteServerConstructorOptions;
|
||||
public typedserver: TypedServer;
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
constructor(optionsArg: IUtilityWebsiteServerConstructorOptions) {
|
||||
this.options = optionsArg;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public async start(portArg = 3000) {
|
||||
this.typedserver = new TypedServer({
|
||||
cors: true,
|
||||
injectReload: true,
|
||||
watch: true,
|
||||
serveDir: this.options.serveDir,
|
||||
enableCompression: true,
|
||||
preferredCompressionMethod: 'gzip',
|
||||
domain: this.options.domain,
|
||||
forceSsl: false,
|
||||
manifest: {
|
||||
name: this.options.domain,
|
||||
short_name: this.options.domain,
|
||||
start_url: '/',
|
||||
display_override: ['window-controls-overlay'],
|
||||
lang: 'en',
|
||||
background_color: '#000000',
|
||||
scope: '/',
|
||||
},
|
||||
port: portArg,
|
||||
|
||||
// features
|
||||
robots: true,
|
||||
sitemap: true,
|
||||
});
|
||||
|
||||
let lswData: interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response'] =
|
||||
{
|
||||
appHash: 'xxxxxx',
|
||||
appSemVer: this.options.appSemVer || 'x.x.x',
|
||||
};
|
||||
|
||||
// -> /lsw* - anything regarding serviceworker
|
||||
servertools.serviceworker.addServiceWorkerRoute(this.typedserver, () => {
|
||||
return lswData;
|
||||
});
|
||||
|
||||
// lets add ads.txt
|
||||
this.typedserver.server.addRoute(
|
||||
'/ads.txt',
|
||||
new servertools.Handler('GET', async (req, res) => {
|
||||
res.type('txt/plain');
|
||||
const adsTxt =
|
||||
['google.com, pub-4104137977476459, DIRECT, f08c47fec0942fa0'].join('\n') + '\n';
|
||||
res.write(adsTxt);
|
||||
res.end();
|
||||
})
|
||||
);
|
||||
|
||||
this.typedserver.server.addRoute(
|
||||
'/assetbroker/manifest/:manifestAsset',
|
||||
new servertools.Handler('GET', async (req, res) => {
|
||||
let manifestAssetName = req.params.manifestAsset;
|
||||
if (manifestAssetName === 'favicon.png') {
|
||||
manifestAssetName = `favicon_${this.options.domain
|
||||
.replace('.', '')
|
||||
.replace('losslesscom', 'lossless')}@2x_transparent.png`;
|
||||
}
|
||||
const fullOriginAssetUrl = `https://assetbroker.lossless.one/brandfiles/00general/${manifestAssetName}`;
|
||||
console.log(`Getting ${manifestAssetName} from ${fullOriginAssetUrl}`);
|
||||
const dataBuffer: Buffer = (await plugins.smartrequest.getBinary(fullOriginAssetUrl)).body;
|
||||
res.type('.png');
|
||||
res.write(dataBuffer);
|
||||
res.end();
|
||||
})
|
||||
);
|
||||
|
||||
// lets add any custom routes
|
||||
if (this.options.addCustomRoutes) {
|
||||
await this.options.addCustomRoutes(this.typedserver.server);
|
||||
}
|
||||
|
||||
// -> /* - serve the files
|
||||
this.typedserver.serveDirHashSubject.subscribe((appHash: string) => {
|
||||
lswData = {
|
||||
appHash,
|
||||
appSemVer: '1.0.0',
|
||||
};
|
||||
});
|
||||
|
||||
// lets setup the typedrouter chain
|
||||
this.typedserver.typedrouter.addTypedRouter(this.typedrouter);
|
||||
|
||||
// lets start everything
|
||||
console.log('routes are all set. Startin up now!');
|
||||
await this.typedserver.start();
|
||||
console.log('typedserver started!');
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
await this.typedserver.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* allows you to hanlde requests from other server instances without the need to listen for yourself
|
||||
* note smartexpress allows you start the instance wuith passing >>false<< as second parameter to .start();
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
public async handleRequest(req: Request, res: Response) {
|
||||
await this.typedserver.server.handleReqRes(req, res);
|
||||
}
|
||||
}
|
0
ts/utilityservers/index.ts
Normal file
0
ts/utilityservers/index.ts
Normal file
8
ts_edgeworker/00_commitinfo_data.ts
Normal file
8
ts_edgeworker/00_commitinfo_data.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @pushrocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: 'cloudflare-workers',
|
||||
version: '1.0.192',
|
||||
description: 'cloudflare-workers'
|
||||
}
|
83
ts_edgeworker/analytics/analyzer.ts
Normal file
83
ts_edgeworker/analytics/analyzer.ts
Normal file
@ -0,0 +1,83 @@
|
||||
|
||||
import type { EdgeWorker } from '../classes.edgeworker.js';
|
||||
import type { WorkerEvent } from '../classes.workerevent.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import { SmartlogDestination } from './smartlog.js';
|
||||
|
||||
export interface IAnalyticsData {
|
||||
requestAgent: string;
|
||||
requestUrl: string;
|
||||
requestMethod: string;
|
||||
requestStartTime: number;
|
||||
responseStatus: number;
|
||||
responseEndTime: number;
|
||||
}
|
||||
|
||||
export class Analyzer {
|
||||
cworkerEventRef: WorkerEvent;
|
||||
|
||||
public data: IAnalyticsData = {
|
||||
requestAgent: 'unknown',
|
||||
requestMethod: 'unknown',
|
||||
requestUrl: 'unknown',
|
||||
requestStartTime: 0,
|
||||
responseStatus: 0,
|
||||
responseEndTime: 0,
|
||||
};
|
||||
|
||||
public finishedDeferred = plugins.smartpromise.defer();
|
||||
|
||||
constructor(cworkerEventRefArg: WorkerEvent) {
|
||||
this.cworkerEventRef = cworkerEventRefArg;
|
||||
this.smartlog.addLogDestination(new SmartlogDestination(this.cworkerEventRef.options.edgeWorkerRef));
|
||||
}
|
||||
public smartlog = new plugins.smartlog.Smartlog({
|
||||
logContext: {
|
||||
environment: 'production',
|
||||
runtime: "cloudflare_workers",
|
||||
zone: 'servezone',
|
||||
company: 'Lossless GmbH',
|
||||
companyunit: 'Lossless Cloud',
|
||||
containerName: 'cloudflare_workers'
|
||||
}
|
||||
});
|
||||
|
||||
public setRequestData (optionsArg: {
|
||||
requestAgent: string;
|
||||
requestUrl: string;
|
||||
requestMethod: string;
|
||||
}) {
|
||||
this.data = {
|
||||
...this.data,
|
||||
...{
|
||||
requestAgent: optionsArg.requestAgent,
|
||||
requestUrl: optionsArg.requestUrl,
|
||||
requestMethod: optionsArg.requestMethod,
|
||||
requestStartTime: Date.now()
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
public setResponseData(optionsArg: {
|
||||
responseStatus: number,
|
||||
responseEndTime: number,
|
||||
}) {
|
||||
this.data = {
|
||||
...this.data,
|
||||
...{
|
||||
responseStatus: optionsArg.responseStatus,
|
||||
responseEndTime: optionsArg.responseEndTime
|
||||
}
|
||||
};
|
||||
this.sendLogs();
|
||||
}
|
||||
|
||||
public async sendLogs() {
|
||||
await this.smartlog.log('info', `
|
||||
Got a ${this.data.requestMethod} request from ${this.data.requestAgent} to
|
||||
${this.data.requestUrl}
|
||||
that took ${this.data.responseEndTime - this.data.requestStartTime}ms to resolve with status ${this.data.responseStatus}.`, this.data);
|
||||
this.finishedDeferred.resolve();
|
||||
}
|
||||
}
|
26
ts_edgeworker/analytics/smartlog.ts
Normal file
26
ts_edgeworker/analytics/smartlog.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import * as smartlogInterfaces from '@push.rocks/smartlog-interfaces';
|
||||
import type { EdgeWorker } from '../classes.edgeworker.js';
|
||||
|
||||
export class SmartlogDestination implements smartlogInterfaces.ILogDestination {
|
||||
public edgeWorkerRef: EdgeWorker;
|
||||
|
||||
constructor(edgeworkerRefArg: EdgeWorker) {
|
||||
this.edgeWorkerRef = edgeworkerRefArg;
|
||||
}
|
||||
|
||||
public async handleLog(logPackageArg: smartlogInterfaces.ILogPackage) {
|
||||
if (this.edgeWorkerRef.options.smartlogConfig) {
|
||||
const requestBody: smartlogInterfaces.ILogPackageAuthenticated = {
|
||||
auth: this.edgeWorkerRef.options.smartlogConfig.token,
|
||||
logPackage: logPackageArg,
|
||||
};
|
||||
await fetch(this.edgeWorkerRef.options.smartlogConfig.endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
67
ts_edgeworker/classes.domainrouter.ts
Normal file
67
ts_edgeworker/classes.domainrouter.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
import * as plugins from './plugins.js';
|
||||
import { WorkerEvent } from './classes.workerevent.js';
|
||||
|
||||
import * as domainInstructions from './domaininstructions/index.js';
|
||||
|
||||
export class DomainRouter {
|
||||
private smartmatches: plugins.smartmatch.SmartMatch[] = [];
|
||||
|
||||
constructor() {
|
||||
for (const key of Object.keys(domainInstructions.instructionObject)) {
|
||||
this.smartmatches.push(new plugins.smartmatch.SmartMatch(key));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param cworkerevent
|
||||
*/
|
||||
public routeToResponder(cworkerevent: WorkerEvent) {
|
||||
const match = this.smartmatches.find(smartmatchArg => {
|
||||
return smartmatchArg.match(cworkerevent.request.url);
|
||||
});
|
||||
cworkerevent.responderInstruction = match
|
||||
? domainInstructions.instructionObject[match.wildcard]
|
||||
: {
|
||||
type: 'cache'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* rendertronRouter
|
||||
*/
|
||||
public checkWetherReRouteToRendertron(cworkerevent: WorkerEvent) {
|
||||
let needsRendertron = false;
|
||||
for (const botAgentIdentifier of domainInstructions.botUserAgents) {
|
||||
if (needsRendertron) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
cworkerevent.request.headers.get('user-agent') &&
|
||||
cworkerevent.request.headers.get('user-agent').toLowerCase().includes(botAgentIdentifier.toLowerCase()) &&
|
||||
!cworkerevent.request.url.includes('lossless.one')
|
||||
) {
|
||||
needsRendertron = true;
|
||||
}
|
||||
}
|
||||
if (needsRendertron) {
|
||||
cworkerevent.routedThroughRendertron = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* check wether this is a preflight request that should be handled
|
||||
*/
|
||||
public checkWetherIsPreflight (cworkerevent: WorkerEvent) {
|
||||
if (
|
||||
cworkerevent.request.method === 'OPTIONS' &&
|
||||
cworkerevent.request.headers.get('Origin') !== null &&
|
||||
cworkerevent.request.headers.get('Access-Control-Request-Method') !== null &&
|
||||
cworkerevent.request.headers.get('Access-Control-Request-Headers') !== null
|
||||
) {
|
||||
cworkerevent.isPreflight = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
73
ts_edgeworker/classes.edgeworker.ts
Normal file
73
ts_edgeworker/classes.edgeworker.ts
Normal file
@ -0,0 +1,73 @@
|
||||
// imports
|
||||
import { WorkerEvent } from './classes.workerevent.js';
|
||||
import { DomainRouter } from './classes.domainrouter.js';
|
||||
import * as plugins from './plugins.js';
|
||||
import * as responders from './responders/index.js';
|
||||
|
||||
export interface IEdgeWorkerOptions {
|
||||
smartlogConfig?: {
|
||||
endpoint: string;
|
||||
token: string;
|
||||
}
|
||||
}
|
||||
|
||||
export class EdgeWorker {
|
||||
public options: IEdgeWorkerOptions;
|
||||
domainRouter: DomainRouter;
|
||||
|
||||
constructor(optionsArg: IEdgeWorkerOptions = {}) {
|
||||
this.options = optionsArg;
|
||||
this.domainRouter = new DomainRouter();
|
||||
addEventListener('fetch', this.fetchFunction as any);
|
||||
}
|
||||
|
||||
public async fetchFunction (eventArg: plugins.cloudflareTypes.FetchEvent) {
|
||||
if (new URL(eventArg.request.url).pathname.startsWith('/socket.io')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cworkerEvent = new WorkerEvent({
|
||||
edgeWorkerRef: this,
|
||||
event: eventArg,
|
||||
passThroughOnException: true
|
||||
});
|
||||
|
||||
// lets answer basic reuest things
|
||||
responders.timeoutResponder(cworkerEvent);
|
||||
cworkerEvent.hasResponse ? null : await responders.urlFormattingResponder(cworkerEvent);
|
||||
|
||||
// lets route the domain
|
||||
this.domainRouter.routeToResponder(cworkerEvent);
|
||||
this.domainRouter.checkWetherReRouteToRendertron(cworkerEvent);
|
||||
this.domainRouter.checkWetherIsPreflight(cworkerEvent);
|
||||
|
||||
// guardresponder
|
||||
cworkerEvent.hasResponse ? null : await responders.guardResponder(cworkerEvent);
|
||||
|
||||
// lets process all requests that need rendertron
|
||||
cworkerEvent.hasResponse ? null : await responders.rendertronResponder(cworkerEvent);
|
||||
|
||||
// lets process all requests that are preflight requests
|
||||
cworkerEvent.hasResponse ? null : await responders.preflightResponder(cworkerEvent);
|
||||
|
||||
switch (cworkerEvent.responderInstruction.type) {
|
||||
case 'cache':
|
||||
cworkerEvent.hasResponse ? null : await responders.cacheResponder(cworkerEvent);
|
||||
break;
|
||||
case 'origin':
|
||||
cworkerEvent.hasResponse ? null : await responders.originResponder(cworkerEvent);
|
||||
break;
|
||||
case 'redirect':
|
||||
cworkerEvent.hasResponse ? null : await responders.adsTxtResponder(cworkerEvent);
|
||||
break;
|
||||
case 'static':
|
||||
cworkerEvent.hasResponse ? null : await responders.staticResponder(cworkerEvent);
|
||||
break;
|
||||
case 'ads.txt':
|
||||
cworkerEvent.hasResponse ? null : await responders.adsTxtResponder(cworkerEvent);
|
||||
break;
|
||||
}
|
||||
// cworkerEvent.hasResponse ? null : await responders.kvResponder(cworkerEvent);
|
||||
cworkerEvent.hasResponse ? null : await responders.errorResponder(cworkerEvent);
|
||||
};
|
||||
}
|
37
ts_edgeworker/classes.kvhandler.ts
Normal file
37
ts_edgeworker/classes.kvhandler.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
declare var lokv: plugins.cloudflareTypes.KVNamespace;
|
||||
|
||||
/**
|
||||
* an abstraction for the workerd KV store
|
||||
*/
|
||||
export class KVHandler {
|
||||
private getSafeIdentifier(urlString: string) {
|
||||
return encodeURI(urlString);
|
||||
}
|
||||
|
||||
async getFromKv(keyIdentifier: string) {
|
||||
const key = this.getSafeIdentifier(keyIdentifier);
|
||||
const valueString = await lokv.get(key);
|
||||
return valueString;
|
||||
}
|
||||
|
||||
async putInKv(keyIdentifier: string, valueForStorage: string) {
|
||||
const key = this.getSafeIdentifier(keyIdentifier);
|
||||
const value = valueForStorage;
|
||||
await lokv.put(key, value);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* deletes a key/value from the cache
|
||||
* @param keyIdentifier
|
||||
*/
|
||||
async deleteInKv(keyIdentifier: string) {
|
||||
const cacheKey = this.getSafeIdentifier(keyIdentifier);
|
||||
await lokv.delete(cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
export const kvHandlerInstance = new KVHandler();
|
46
ts_edgeworker/classes.responsekv.ts
Normal file
46
ts_edgeworker/classes.responsekv.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { kvHandlerInstance } from './classes.kvhandler.js';
|
||||
|
||||
declare var lokv: plugins.cloudflareTypes.KVNamespace;
|
||||
|
||||
interface IKVResponseObject {
|
||||
headers: { [key: string]: string };
|
||||
version: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export class ResponseKv {
|
||||
public async storeResponse(urlIdentifier: string, responseArg: any) {
|
||||
const headers: { [key: string]: string } = {};
|
||||
for (const kv of responseArg.headers.entries()) {
|
||||
headers[kv[0]] = kv[1];
|
||||
}
|
||||
const kvResponseForStorage: IKVResponseObject = {
|
||||
headers,
|
||||
version: '1.0.0',
|
||||
body: await responseArg.text()
|
||||
};
|
||||
await kvHandlerInstance.putInKv(urlIdentifier, JSON.stringify(kvResponseForStorage));
|
||||
}
|
||||
|
||||
public async getResponse(urlIdentifier: string): Promise<Response> {
|
||||
const kvValue = await kvHandlerInstance.getFromKv(urlIdentifier);
|
||||
if (kvValue) {
|
||||
let kvResponse: IKVResponseObject;
|
||||
try {
|
||||
kvResponse = JSON.parse(kvValue);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return null;
|
||||
}
|
||||
const headers = new Headers();
|
||||
for (const key of Object.keys(kvResponse.headers)) {
|
||||
headers.append(key, kvResponse.headers[key]);
|
||||
}
|
||||
headers.append('SERVEZONE_ROUTE', 'CLOUDFLARE_EDGE_LOKV');
|
||||
return new Response(kvResponse.body, { headers: headers });
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
102
ts_edgeworker/classes.workerevent.ts
Normal file
102
ts_edgeworker/classes.workerevent.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
import * as plugins from './plugins.js';
|
||||
import * as helpers from './helpers/index.js';
|
||||
import { DomainRouter } from './classes.domainrouter.js';
|
||||
import { Analyzer } from './analytics/analyzer.js';
|
||||
import type { EdgeWorker } from './classes.edgeworker.js';
|
||||
|
||||
export interface ICworkerEventOptions {
|
||||
event: plugins.cloudflareTypes.FetchEvent
|
||||
edgeWorkerRef: EdgeWorker;
|
||||
passThroughOnException?: boolean;
|
||||
}
|
||||
|
||||
|
||||
export class WorkerEvent {
|
||||
public options: ICworkerEventOptions;
|
||||
|
||||
public analyzer: Analyzer;
|
||||
private responseDeferred: plugins.smartpromise.Deferred<any>;
|
||||
private waitUntilDeferred: plugins.smartpromise.Deferred<any>;
|
||||
|
||||
private response: Response = null;
|
||||
private waitList = [];
|
||||
|
||||
// routing settings
|
||||
public responderInstruction: interfaces.IResponderInstruction;
|
||||
public routedThroughRendertron: boolean = false;
|
||||
public isPreflight: boolean = false;
|
||||
|
||||
public request: plugins.cloudflareTypes.Request;
|
||||
|
||||
public parsedUrl: URL;
|
||||
|
||||
constructor(optionsArg: ICworkerEventOptions) {
|
||||
this.options = optionsArg;
|
||||
|
||||
// lets create an Analyzer for this request
|
||||
this.analyzer = new Analyzer(this);
|
||||
|
||||
// lets make sure we always answer
|
||||
this.options.passThroughOnException ? this.options.event.passThroughOnException() : null;
|
||||
|
||||
// lets set up some better asnyc behaviour
|
||||
this.waitUntilDeferred = plugins.smartpromise.defer();
|
||||
this.responseDeferred = plugins.smartpromise.defer();
|
||||
this.addToWaitList(this.analyzer.finishedDeferred.promise);
|
||||
|
||||
// lets entangle the event with this class instance
|
||||
this.request = this.options.event.request;
|
||||
|
||||
// lets start with analytics
|
||||
this.analyzer.setRequestData({
|
||||
requestAgent: this.request.headers.get('user-agent'),
|
||||
requestMethod: this.request.method,
|
||||
requestUrl: this.request.url
|
||||
});
|
||||
|
||||
|
||||
this.options.event.respondWith(this.responseDeferred.promise);
|
||||
this.options.event.waitUntil(this.waitUntilDeferred.promise);
|
||||
|
||||
// lets parse the url
|
||||
this.parsedUrl = new URL(this.request.url);
|
||||
|
||||
// lets check the waitlist
|
||||
this.checkWaitList();
|
||||
console.log(`Got request for ${this.request.url}`);
|
||||
}
|
||||
|
||||
get hasResponse () {
|
||||
let returnValue: boolean;
|
||||
this.response ? returnValue = true : returnValue = false;
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
public addToWaitList(promiseArg: Promise<any>) {
|
||||
this.waitList.push(promiseArg);
|
||||
}
|
||||
|
||||
private async checkWaitList() {
|
||||
await this.responseDeferred.promise;
|
||||
const currentWaitList = this.waitList;
|
||||
this.waitList = [];
|
||||
await Promise.all(currentWaitList);
|
||||
if (this.waitList.length > 0) {
|
||||
this.checkWaitList();
|
||||
} else {
|
||||
this.waitUntilDeferred.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
public setResponse (responseArg: Response) {
|
||||
this.response = responseArg;
|
||||
this.responseDeferred.resolve(responseArg);
|
||||
this.analyzer.setResponseData({
|
||||
responseStatus: this.response.status,
|
||||
responseEndTime: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
36
ts_edgeworker/domaininstructions/botuseragents.ts
Normal file
36
ts_edgeworker/domaininstructions/botuseragents.ts
Normal file
@ -0,0 +1,36 @@
|
||||
export const botUserAgents = [
|
||||
// Baidu
|
||||
'baiduspider',
|
||||
'embedly',
|
||||
|
||||
// Facebook
|
||||
'facebookexternalhit',
|
||||
|
||||
// Ghost
|
||||
'Ghost',
|
||||
|
||||
// Microsoft
|
||||
'bingbot',
|
||||
'BingPreview',
|
||||
'linkedinbot',
|
||||
'MissinglettrBot',
|
||||
'msnbot',
|
||||
'outbrain',
|
||||
'pinterest',
|
||||
'quora link preview',
|
||||
'rogerbot',
|
||||
'showyoubot',
|
||||
'slackbot',
|
||||
'TelegramBot',
|
||||
|
||||
// Twitter
|
||||
'twitterbot',
|
||||
'vkShare',
|
||||
'W3C_Validator',
|
||||
|
||||
// WhatsApp
|
||||
'whatsapp',
|
||||
|
||||
// woorank
|
||||
'woorank'
|
||||
];
|
7
ts_edgeworker/domaininstructions/domaininstructions.ts
Normal file
7
ts_edgeworker/domaininstructions/domaininstructions.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
|
||||
export const instructionObject: { [key: string]: interfaces.IResponderInstruction } = {
|
||||
'*/ads.txt': {
|
||||
type: 'ads.txt',
|
||||
}
|
||||
};
|
2
ts_edgeworker/domaininstructions/index.ts
Normal file
2
ts_edgeworker/domaininstructions/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './botuseragents.js';
|
||||
export * from './domaininstructions.js';
|
9
ts_edgeworker/helpers/checks.ts
Normal file
9
ts_edgeworker/helpers/checks.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
declare var lokv: plugins.cloudflareTypes.KVNamespace;
|
||||
export const checkLokv = () => {
|
||||
if (!lokv) {
|
||||
throw new Error('lokv not defined!');
|
||||
} else {
|
||||
console.log('lokv present!');
|
||||
}
|
||||
};
|
1
ts_edgeworker/helpers/index.ts
Normal file
1
ts_edgeworker/helpers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './checks.js';
|
1
ts_edgeworker/index.ts
Normal file
1
ts_edgeworker/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './classes.edgeworker.js';
|
9
ts_edgeworker/interfaces/custom.ts
Normal file
9
ts_edgeworker/interfaces/custom.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { WorkerEvent } from "../classes.workerevent.js";
|
||||
|
||||
export interface IResponderInstruction {
|
||||
type: 'origin' | 'cache' | 'static' | 'redirect' | 'ads.txt';
|
||||
cacheClientSideForMin?: number;
|
||||
redirectUrl?: string;
|
||||
}
|
||||
|
||||
export type TRequestResponser = (workerEventArg: WorkerEvent) => Promise<void>;
|
1
ts_edgeworker/interfaces/index.ts
Normal file
1
ts_edgeworker/interfaces/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './custom.js';
|
21
ts_edgeworker/plugins.ts
Normal file
21
ts_edgeworker/plugins.ts
Normal file
@ -0,0 +1,21 @@
|
||||
// @pushrocks scope
|
||||
import * as smartdelay from '@push.rocks/smartdelay';
|
||||
import * as smartlog from '@push.rocks/smartlog';
|
||||
import * as smartlogInterfaces from '@push.rocks/smartlog-interfaces';
|
||||
import * as smartmatch from '@push.rocks/smartmatch';
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
|
||||
export {
|
||||
smartdelay,
|
||||
smartlog,
|
||||
smartlogInterfaces,
|
||||
smartmatch,
|
||||
smartpromise
|
||||
};
|
||||
|
||||
// cloudflarea
|
||||
import * as cloudflareTypes from '@cloudflare/workers-types';
|
||||
|
||||
export {
|
||||
cloudflareTypes
|
||||
}
|
14
ts_edgeworker/responders/adstxt.responder.ts
Normal file
14
ts_edgeworker/responders/adstxt.responder.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import { WorkerEvent } from '../classes.workerevent.js';
|
||||
|
||||
export const adsTxtResponder: interfaces.TRequestResponser = async (cWorkerEventArg: WorkerEvent) => {
|
||||
if (cWorkerEventArg.responderInstruction.type === 'ads.txt') {
|
||||
const response = new Response('google.com, pub-4104137977476459, DIRECT, f08c47fec0942fa0\n', {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain; charset=utf-8'
|
||||
}
|
||||
})
|
||||
cWorkerEventArg.setResponse(response);
|
||||
}
|
||||
|
||||
};
|
92
ts_edgeworker/responders/cache.responder.ts
Normal file
92
ts_edgeworker/responders/cache.responder.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import { WorkerEvent } from '../classes.workerevent.js';
|
||||
import { kvHandlerInstance } from '../classes.kvhandler.js';
|
||||
|
||||
declare const fetch: plugins.cloudflareTypes.Fetcher['fetch'];
|
||||
|
||||
declare var caches: any;
|
||||
export const cacheResponder: interfaces.TRequestResponser = async (cworkerEventArg: WorkerEvent) => {
|
||||
const host = cworkerEventArg.request.headers.get('Host');
|
||||
const appHashKey = `${host.toLowerCase()}_appHash`;
|
||||
const appHash = await kvHandlerInstance.getFromKv(appHashKey);
|
||||
|
||||
const cache = caches.default;
|
||||
let response: Response = await cache.match(cworkerEventArg.request);
|
||||
|
||||
if (
|
||||
response &&
|
||||
response.headers.get('appHash') &&
|
||||
response.headers.get('appHash') !== appHash
|
||||
) {
|
||||
response = null;
|
||||
}
|
||||
|
||||
if (response) {
|
||||
cworkerEventArg.setResponse(response);
|
||||
} else {
|
||||
response = await handleNewRequest(cworkerEventArg.request);
|
||||
if (response) {
|
||||
cworkerEventArg.addToWaitList(new Promise<void>(async (resolve, reject) => {
|
||||
const newAppHash = response.headers.get('appHash');
|
||||
if (newAppHash) {
|
||||
await kvHandlerInstance.putInKv(appHashKey, newAppHash);
|
||||
}
|
||||
resolve();
|
||||
}));
|
||||
cworkerEventArg.addToWaitList(buildCacheResponse(cache, cworkerEventArg.request, response));
|
||||
cworkerEventArg.setResponse(response);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Request} originalRequest
|
||||
*/
|
||||
const handleNewRequest = async (originalRequest: plugins.cloudflareTypes.Request): Promise<Response> => {
|
||||
console.log('answering from origin');
|
||||
const originResponse: any = await fetch(
|
||||
originalRequest
|
||||
);
|
||||
|
||||
// lets capture status
|
||||
if (originResponse.status > 399) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const responseClientPassThroughStream = new TransformStream();
|
||||
originResponse.body.pipeTo(responseClientPassThroughStream.writable);
|
||||
|
||||
// build response for client
|
||||
const clientHeaders = new Headers();
|
||||
for (const kv of originResponse.headers.entries()) {
|
||||
clientHeaders.append(kv[0], kv[1]);
|
||||
}
|
||||
clientHeaders.append('SERVEZONE_ROUTE', 'LOSSLESS_EDGE_ORIGIN_INITIAL');
|
||||
const responseForClient = new Response(responseClientPassThroughStream.readable, {
|
||||
...originResponse,
|
||||
headers: clientHeaders
|
||||
});
|
||||
|
||||
// lets return the responses
|
||||
return responseForClient;
|
||||
};
|
||||
|
||||
const buildCacheResponse = async (cache, matchRequest: plugins.cloudflareTypes.Request, originResponse: any) => {
|
||||
const cacheHeaders = new Headers();
|
||||
for (const kv of originResponse.headers.entries()) {
|
||||
cacheHeaders.append(kv[0], kv[1]);
|
||||
}
|
||||
cacheHeaders.delete('SERVEZONE_ROUTE');
|
||||
cacheHeaders.append('SERVEZONE_ROUTE', 'LOSSLESS_EDGE_CACHE');
|
||||
cacheHeaders.delete('Cache-Control');
|
||||
cacheHeaders.append('Cache-Control', 'public, max-age=60');
|
||||
cacheHeaders.delete('Expires');
|
||||
cacheHeaders.append('Expires', new Date(Date.now() + 60 * 1000).toUTCString());
|
||||
|
||||
const responseForCache = new Response(await originResponse.clone().body, {
|
||||
...originResponse,
|
||||
headers: cacheHeaders
|
||||
});
|
||||
await cache.put(matchRequest, responseForCache);
|
||||
};
|
6
ts_edgeworker/responders/error.responder.ts
Normal file
6
ts_edgeworker/responders/error.responder.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import { WorkerEvent } from '../classes.workerevent.js';
|
||||
export const errorResponder: interfaces.TRequestResponser = async (cWorkerEvent: WorkerEvent) => {
|
||||
const errorResponse = await fetch('https://nullresolve.lossless.one/status/firewall');
|
||||
cWorkerEvent.setResponse(errorResponse);
|
||||
};
|
8
ts_edgeworker/responders/guard.responder.ts
Normal file
8
ts_edgeworker/responders/guard.responder.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import { WorkerEvent } from '../classes.workerevent.js';
|
||||
export const guardResponder: interfaces.TRequestResponser = async (cWorkerEvent: WorkerEvent) => {
|
||||
if (cWorkerEvent.parsedUrl.pathname.endsWith('.map')) {
|
||||
const errorResponse = await fetch('https://nullresolve.lossless.one/status/firewall');
|
||||
cWorkerEvent.setResponse(errorResponse);
|
||||
}
|
||||
};
|
12
ts_edgeworker/responders/index.ts
Normal file
12
ts_edgeworker/responders/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export * from './adstxt.responder.js';
|
||||
export * from './cache.responder.js';
|
||||
export * from './urlformatting.responder.js';
|
||||
export * from './error.responder.js';
|
||||
export * from './guard.responder.js';
|
||||
export * from './kv.responder.js';
|
||||
export * from './origin.responder.js';
|
||||
export * from './preflight.responder.js';
|
||||
export * from './redirect.reponder.js';
|
||||
export * from './rendertron.responder.js';
|
||||
export * from './static.responder.js';
|
||||
export * from './timeout.responder.js';
|
34
ts_edgeworker/responders/kv.responder.ts
Normal file
34
ts_edgeworker/responders/kv.responder.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import { WorkerEvent } from '../classes.workerevent.js';
|
||||
import { ResponseKv } from '../classes.responsekv.js';
|
||||
|
||||
declare const fetch: plugins.cloudflareTypes.Fetcher['fetch'];
|
||||
|
||||
export const kvResponder: interfaces.TRequestResponser = async (cworkerEventArg: WorkerEvent) => {
|
||||
const responseKvInstance = new ResponseKv();
|
||||
let response = await responseKvInstance.getResponse(cworkerEventArg.request.url);
|
||||
if (response) {
|
||||
console.log('Got response from KV');
|
||||
} else {
|
||||
response = await handleNewRequest(cworkerEventArg.request, responseKvInstance);
|
||||
}
|
||||
cworkerEventArg.setResponse(response);
|
||||
};
|
||||
|
||||
const handleNewRequest = async (request: plugins.cloudflareTypes.Request, responseKvInstance: ResponseKv) => {
|
||||
const originResponse: any = await fetch(request);
|
||||
// build response for cache
|
||||
const cacheHeaders = new Headers();
|
||||
for (const kv of originResponse.headers.entries()) {
|
||||
cacheHeaders.append(kv[0], kv[1]);
|
||||
}
|
||||
cacheHeaders.append('SERVEZONE_ROUTE', 'LOSSLESS_EDGE_KVRESPONSE');
|
||||
cacheHeaders.append('Cache-Control', 'max-age=600');
|
||||
const responseForKV = new Response(await originResponse.body, {
|
||||
...originResponse,
|
||||
headers: cacheHeaders
|
||||
});
|
||||
await responseKvInstance.storeResponse(request.url, responseForKV.clone());
|
||||
return responseForKV.clone();
|
||||
};
|
31
ts_edgeworker/responders/origin.responder.ts
Normal file
31
ts_edgeworker/responders/origin.responder.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import { WorkerEvent } from '../classes.workerevent.js';
|
||||
|
||||
declare const fetch: plugins.cloudflareTypes.Fetcher['fetch'];
|
||||
|
||||
export const originResponder: interfaces.TRequestResponser = async (eventArg: WorkerEvent) => {
|
||||
const originResponse: any = await fetch(eventArg.request);
|
||||
// lets capture status
|
||||
if (originResponse.status > 399) {
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
for (const kv of originResponse.headers.entries()) {
|
||||
headers.append(kv[0], kv[1]);
|
||||
}
|
||||
headers.append('SERVEZONE_ROUTE', 'LOSSLESS_EDGE_FASTORIGIN');
|
||||
|
||||
const responsePassThroughStream = new TransformStream();
|
||||
originResponse.body.pipeTo(responsePassThroughStream.writable);
|
||||
|
||||
// response
|
||||
|
||||
const responseForClient = new Response(responsePassThroughStream.readable, {
|
||||
...originResponse,
|
||||
headers,
|
||||
});
|
||||
|
||||
eventArg.setResponse(responseForClient);
|
||||
};
|
18
ts_edgeworker/responders/preflight.responder.ts
Normal file
18
ts_edgeworker/responders/preflight.responder.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import { WorkerEvent } from '../classes.workerevent.js';
|
||||
|
||||
export const preflightResponder: interfaces.TRequestResponser = async (eventArg: WorkerEvent) => {
|
||||
if (eventArg.isPreflight) {
|
||||
const corsHeaders = new Headers();
|
||||
corsHeaders.append('SERVEZONE_ROUTE', 'LOSSLESS_EDGE_PREFLIGHT');
|
||||
corsHeaders.append('Access-Control-Allow-Origin', '*');
|
||||
corsHeaders.append('Access-Control-Allow-Methods', '*');
|
||||
corsHeaders.append('Access-Control-Allow-Headers', '*');
|
||||
|
||||
eventArg.setResponse(
|
||||
new Response(null, {
|
||||
headers: corsHeaders,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
9
ts_edgeworker/responders/redirect.reponder.ts
Normal file
9
ts_edgeworker/responders/redirect.reponder.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import { WorkerEvent } from '../classes.workerevent.js';
|
||||
|
||||
export const redirectResponder: interfaces.TRequestResponser = async (cWorkerEventArg: WorkerEvent) => {
|
||||
if (cWorkerEventArg.responderInstruction.type === 'redirect') {
|
||||
cWorkerEventArg.setResponse(Response.redirect(cWorkerEventArg.responderInstruction.redirectUrl, 302));
|
||||
}
|
||||
|
||||
};
|
22
ts_edgeworker/responders/rendertron.responder.ts
Normal file
22
ts_edgeworker/responders/rendertron.responder.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { WorkerEvent } from '../classes.workerevent.js';
|
||||
|
||||
export const rendertronResponder = async (cworkerevent: WorkerEvent) => {
|
||||
if (cworkerevent.routedThroughRendertron) {
|
||||
const oldHeaders: any = cworkerevent.request.headers;
|
||||
const rendertronHeaders = new Headers();
|
||||
for (const kv of oldHeaders.entries()) {
|
||||
const headerName = kv[0];
|
||||
const headerValue = headerName === 'user-agent' ? 'Lossless Rendertron' : kv[1];
|
||||
rendertronHeaders.append(headerName, headerValue);
|
||||
}
|
||||
const rendertronRequest = new Request(
|
||||
`https://rendertron.lossless.one/render/${cworkerevent.request.url}`,
|
||||
{
|
||||
method: cworkerevent.request.method,
|
||||
headers: rendertronHeaders
|
||||
}
|
||||
);
|
||||
cworkerevent.setResponse(await fetch(rendertronRequest));
|
||||
}
|
||||
};
|
31
ts_edgeworker/responders/static.responder.ts
Normal file
31
ts_edgeworker/responders/static.responder.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import { WorkerEvent } from '../classes.workerevent.js';
|
||||
|
||||
export const staticResponder: interfaces.TRequestResponser = async (cWorkerEventArg: WorkerEvent) => {
|
||||
if (cWorkerEventArg.responderInstruction.type === 'static') {
|
||||
const originResponse: any = await fetch(
|
||||
`https://statichost.lossless.one/resolve?url=${encodeURI(cWorkerEventArg.request.url)}`
|
||||
);
|
||||
|
||||
const cacheHeaders = new Headers();
|
||||
for (const kv of originResponse.headers.entries()) {
|
||||
cacheHeaders.append(kv[0], kv[1]);
|
||||
}
|
||||
cacheHeaders.delete('SERVEZONE_ROUTE');
|
||||
cacheHeaders.append('SERVEZONE_ROUTE', 'LOSSLESS_EDGE_STATICHOST');
|
||||
|
||||
if (cWorkerEventArg.responderInstruction.cacheClientSideForMin) {
|
||||
cacheHeaders.delete('Cache-Control');
|
||||
cacheHeaders.append('Cache-Control', `public, max-age=${cWorkerEventArg.responderInstruction.cacheClientSideForMin * 60}`);
|
||||
cacheHeaders.delete('Expires');
|
||||
cacheHeaders.append('Expires', new Date(Date.now() + cWorkerEventArg.responderInstruction.cacheClientSideForMin * 1000).toUTCString());
|
||||
}
|
||||
|
||||
const responseForClient = new Response(await originResponse.clone().body, {
|
||||
...originResponse,
|
||||
headers: cacheHeaders
|
||||
});
|
||||
|
||||
cWorkerEventArg.setResponse(responseForClient);
|
||||
}
|
||||
};
|
23
ts_edgeworker/responders/timeout.responder.ts
Normal file
23
ts_edgeworker/responders/timeout.responder.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import { WorkerEvent } from '../classes.workerevent.js';
|
||||
|
||||
export const timeoutResponder: interfaces.TRequestResponser = async (cWorkerEvent: WorkerEvent) => {
|
||||
await plugins.smartdelay.delayFor(10000);
|
||||
if (cWorkerEvent.routedThroughRendertron) {
|
||||
await plugins.smartdelay.delayFor(10000);
|
||||
}
|
||||
if (!cWorkerEvent.hasResponse) {
|
||||
const errorResponse = await fetch(
|
||||
`https://nullresolve.lossless.one/custom?title=${encodeURI(
|
||||
`Lossless Network: Request Cancellation!`
|
||||
)}&heading=${encodeURI(`Error: Request Cancellation`)}&text=${encodeURI(
|
||||
`Lossless Network could not decide how to respond to this request within 5 seconds. Therefore it timed out and has been canceled.
|
||||
<p>requestUrl: ${cWorkerEvent.request.url}<br>
|
||||
requestTime: ${Date.now()}<br>
|
||||
referenceNumber: xxxxxx</p>`
|
||||
)}`
|
||||
);
|
||||
cWorkerEvent.setResponse(errorResponse);
|
||||
}
|
||||
};
|
21
ts_edgeworker/responders/urlformatting.responder.ts
Normal file
21
ts_edgeworker/responders/urlformatting.responder.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import { WorkerEvent } from '../classes.workerevent.js';
|
||||
|
||||
export const urlFormattingResponder: interfaces.TRequestResponser = async (eventArg: WorkerEvent) => {
|
||||
let shouldCorrect = false;
|
||||
const correctedUrl = new URL(eventArg.request.url);
|
||||
if (eventArg.parsedUrl.hostname.startsWith('www.')) {
|
||||
shouldCorrect = true;
|
||||
correctedUrl.hostname = eventArg.parsedUrl.hostname.substring(
|
||||
4,
|
||||
eventArg.parsedUrl.hostname.length
|
||||
);
|
||||
}
|
||||
if (eventArg.parsedUrl.protocol.startsWith('http:')) {
|
||||
shouldCorrect = true;
|
||||
correctedUrl.protocol = 'https:';
|
||||
}
|
||||
if (shouldCorrect) {
|
||||
eventArg.setResponse(Response.redirect(`${correctedUrl.protocol}//${correctedUrl.host}${correctedUrl.pathname}${correctedUrl.search}`, 301));
|
||||
}
|
||||
};
|
7
ts_edgeworker/versionhandler.ts
Normal file
7
ts_edgeworker/versionhandler.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
|
||||
export class VersionHandler {
|
||||
|
||||
}
|
||||
|
||||
export const versionHandlerInstance = new VersionHandler();
|
@ -1,3 +1,9 @@
|
||||
export * from './requestmodifier.js';
|
||||
export * from './responsemodifier.js';
|
||||
export * from './typedrequests.js';
|
||||
|
||||
import * as serviceworker from './serviceworker.js';
|
||||
|
||||
export {
|
||||
serviceworker,
|
||||
}
|
5
ts_interfaces/plugins.ts
Normal file
5
ts_interfaces/plugins.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import * as typedrequestInterfaces from '@api.global/typedrequest-interfaces';
|
||||
|
||||
export {
|
||||
typedrequestInterfaces,
|
||||
}
|
127
ts_interfaces/serviceworker.ts
Normal file
127
ts_interfaces/serviceworker.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export interface CacheStorage {
|
||||
keys: () => Promise<string[]>;
|
||||
match: any;
|
||||
open: any;
|
||||
delete: any;
|
||||
}
|
||||
export declare var caches: CacheStorage;
|
||||
|
||||
|
||||
// =============================
|
||||
// Interfaces for communication
|
||||
// =============================
|
||||
|
||||
export interface IMessage_Serviceworker_Client_UpdateInfo
|
||||
extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IMessage_Serviceworker_Client_UpdateInfo
|
||||
> {
|
||||
method: 'serviceworker_newVersion';
|
||||
request: {
|
||||
appVersion: string;
|
||||
appHash: string;
|
||||
};
|
||||
response: {};
|
||||
}
|
||||
|
||||
export interface IMessage_Serviceworker_Client_RequestReload
|
||||
extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IMessage_Serviceworker_Client_RequestReload
|
||||
> {
|
||||
method: 'serviceworker_requestReload';
|
||||
request: {};
|
||||
response: {};
|
||||
}
|
||||
|
||||
export interface IRequest_Serviceworker_Backend_VersionInfo
|
||||
extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IRequest_Serviceworker_Backend_VersionInfo
|
||||
> {
|
||||
method: 'serviceworker_versionInfo';
|
||||
request: {};
|
||||
response: {
|
||||
appHash: string;
|
||||
appSemVer: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ===============
|
||||
// web
|
||||
// ===============
|
||||
/**
|
||||
* purges the service workers cache
|
||||
*/
|
||||
export interface IRequest_PurgeServiceWorkerCache extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IRequest_PurgeServiceWorkerCache
|
||||
> {
|
||||
method: 'purgeServiceWorkerCache';
|
||||
request: {};
|
||||
response: {};
|
||||
}
|
||||
|
||||
/**
|
||||
* updates the info in all connected tabs
|
||||
*/
|
||||
export interface IMessage_Serviceworker_Client_UpdateInfo
|
||||
extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IMessage_Serviceworker_Client_UpdateInfo
|
||||
> {
|
||||
method: 'serviceworker_newVersion';
|
||||
request: {
|
||||
appVersion: string;
|
||||
appHash: string;
|
||||
};
|
||||
response: {};
|
||||
}
|
||||
|
||||
/**
|
||||
* requests all clients to reload
|
||||
*/
|
||||
export interface IMessage_Serviceworker_Client_RequestReload
|
||||
extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IMessage_Serviceworker_Client_RequestReload
|
||||
> {
|
||||
method: 'serviceworker_requestReload';
|
||||
request: {};
|
||||
response: {};
|
||||
}
|
||||
|
||||
/**
|
||||
* updates version infos
|
||||
*/
|
||||
export interface IRequest_Serviceworker_Backend_VersionInfo
|
||||
extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IRequest_Serviceworker_Backend_VersionInfo
|
||||
> {
|
||||
method: 'serviceworker_versionInfo';
|
||||
request: {};
|
||||
response: {
|
||||
appHash: string;
|
||||
appSemVer: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* ensures a stable connection between clients and the serviceworker
|
||||
*/
|
||||
export interface IRequest_Client_Serviceworker_ConnectionPolling
|
||||
extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IRequest_Client_Serviceworker_ConnectionPolling
|
||||
> {
|
||||
method: 'broadcastConnectionPolling',
|
||||
request: {
|
||||
tabId: string;
|
||||
},
|
||||
response: {
|
||||
serviceworkerId: string;
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
/**
|
||||
* autocreated commitinfo by @pushrocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@api.global/typedserver',
|
||||
version: '3.0.11',
|
||||
description: 'easy serving of static files'
|
||||
}
|
8
ts_web_inject/00_commitinfo_data.ts
Normal file
8
ts_web_inject/00_commitinfo_data.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @pushrocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@api.global/typedserver',
|
||||
version: '3.0.29',
|
||||
description: 'A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.'
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import * as plugins from './typedserver_web.plugins.js';
|
||||
import * as interfaces from '../ts/interfaces/index.js';
|
||||
import * as interfaces from '../dist_ts_interfaces/index.js';
|
||||
import { logger } from './typedserver_web.logger.js';
|
||||
logger.log('info', `TypedServer-Devtools initialized!`);
|
||||
|
8
ts_web_serviceworker/00_commitinfo_data.ts
Normal file
8
ts_web_serviceworker/00_commitinfo_data.ts
Normal 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'
|
||||
}
|
53
ts_web_serviceworker/classes.backend.ts
Normal file
53
ts_web_serviceworker/classes.backend.ts
Normal 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) {
|
||||
|
||||
}
|
||||
}
|
20
ts_web_serviceworker/env.ts
Normal file
20
ts_web_serviceworker/env.ts
Normal 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;
|
8
ts_web_serviceworker/index.ts
Normal file
8
ts_web_serviceworker/index.ts
Normal 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);
|
||||
|
25
ts_web_serviceworker/plugins.ts
Normal file
25
ts_web_serviceworker/plugins.ts
Normal 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,
|
||||
}
|
224
ts_web_serviceworker/serviceworker.classes.cachemanager.ts
Normal file
224
ts_web_serviceworker/serviceworker.classes.cachemanager.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
33
ts_web_serviceworker/serviceworker.classes.networkmanager.ts
Normal file
33
ts_web_serviceworker/serviceworker.classes.networkmanager.ts
Normal 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()}`);
|
||||
}
|
||||
}
|
76
ts_web_serviceworker/serviceworker.classes.serviceworker.ts
Normal file
76
ts_web_serviceworker/serviceworker.classes.serviceworker.ts
Normal file
@ -0,0 +1,76 @@
|
||||
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';
|
||||
import { ServiceworkerBackend } from './classes.backend.js';
|
||||
|
||||
export class LosslessServiceWorker {
|
||||
// STATIC
|
||||
|
||||
// INSTANCE
|
||||
public serviceWindowRef: interfaces.ServiceWindow;
|
||||
public leleServiceWorkerBackend: ServiceworkerBackend;
|
||||
|
||||
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 = new ServiceworkerBackend({
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
15
ts_web_serviceworker/serviceworker.classes.taskmanager.ts
Normal file
15
ts_web_serviceworker/serviceworker.classes.taskmanager.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1 @@
|
||||
import * as plugins from './plugins.js';
|
91
ts_web_serviceworker/serviceworker.classes.updatemanager.ts
Normal file
91
ts_web_serviceworker/serviceworker.classes.updatemanager.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as interfaces from '../dist_ts_interfaces/index.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: interfaces.serviceworker.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<
|
||||
interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo
|
||||
>('/sw-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
|
||||
});
|
||||
}
|
17
ts_web_serviceworker/serviceworker.logging.ts
Normal file
17
ts_web_serviceworker/serviceworker.logging.ts
Normal 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!');
|
8
ts_web_serviceworker_client/00_commitinfo_data.ts
Normal file
8
ts_web_serviceworker_client/00_commitinfo_data.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @pushrocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@losslessone_private/lele-serviceworker',
|
||||
version: '1.0.58',
|
||||
description: 'the mainthread of the serviceworker'
|
||||
}
|
24
ts_web_serviceworker_client/index.ts
Normal file
24
ts_web_serviceworker_client/index.ts
Normal file
@ -0,0 +1,24 @@
|
||||
// types
|
||||
import type * as interfaces from '../dist_ts_interfaces/index.js';
|
||||
export type {
|
||||
interfaces
|
||||
}
|
||||
|
||||
// ====================================
|
||||
// imports
|
||||
// ====================================
|
||||
|
||||
import { logger } from './serviceworker.logging.js';
|
||||
logger.log('note', 'mainthread console initialized!');
|
||||
|
||||
import { LosslessServiceworker } from './lele-serviceworker.classes.serviceworker.js';
|
||||
|
||||
export type {
|
||||
LosslessServiceworker
|
||||
}
|
||||
|
||||
export const getServiceWorker = async () => {
|
||||
const losslessServiceWorkerInstance = await LosslessServiceworker.createServiceWorker(); // lets setup the service worker
|
||||
logger.log('ok', 'service worker ready!'); // and wait for it to be ready
|
||||
return losslessServiceWorkerInstance;
|
||||
};
|
@ -0,0 +1,58 @@
|
||||
import * as plugins from './lele-serviceworker.plugins.js';
|
||||
import * as interfaces from '../dist_ts_interfaces/index.js';
|
||||
import { logger } from './serviceworker.logging.js';
|
||||
|
||||
/**
|
||||
* MessageManager implements two ways of serviceworker communication
|
||||
* * the serviceWorker method
|
||||
* * the deesComms method using BroadcastChannel
|
||||
*/
|
||||
export class ActionManager {
|
||||
public deesComms = new plugins.deesComms.DeesComms();
|
||||
|
||||
constructor() {
|
||||
// lets define handlers on the client/tab side
|
||||
this.deesComms.createTypedHandler<interfaces.serviceworker.IMessage_Serviceworker_Client_UpdateInfo>('serviceworker_newVersion', async req => {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 200);
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
public async waitForServiceWorkerConnection () {
|
||||
console.log('waiting for service worker connection...')
|
||||
const tr = this.deesComms.createTypedRequest<interfaces.serviceworker.IRequest_Client_Serviceworker_ConnectionPolling>('broadcastConnectionPolling');
|
||||
let connected = false;
|
||||
while (!connected) {
|
||||
tr.fire({
|
||||
tabId: '123'
|
||||
}).then(response => {
|
||||
if (response.serviceworkerId) {
|
||||
console.log('connected to serviceworker!');
|
||||
connected = true;
|
||||
}
|
||||
}).catch();
|
||||
await plugins.smartdelay.delayFor(777);
|
||||
if (!connected) {
|
||||
// lets wake it up.
|
||||
navigator.serviceWorker.controller.postMessage({
|
||||
type: 'wakeUpCall',
|
||||
});
|
||||
}
|
||||
}
|
||||
console.log('ok, got serviceworker connection.')
|
||||
}
|
||||
|
||||
public async purgeServiceWorkerCache () {
|
||||
const tr = this.deesComms.createTypedRequest<interfaces.serviceworker.IRequest_PurgeServiceWorkerCache>('purgeServiceWorkerCache');
|
||||
const response = await tr.fire({});
|
||||
return response;
|
||||
}
|
||||
|
||||
public async getVersionInfo () {
|
||||
const tr = this.deesComms.createTypedRequest<interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo>('serviceworker_versionInfo');
|
||||
const response = await tr.fire({});
|
||||
return response;
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import * as plugins from './lele-serviceworker.plugins.js';
|
||||
import { LosslessServiceworker } from './lele-serviceworker.classes.serviceworker.js';
|
||||
|
||||
export class GlobalSW {
|
||||
losslessSw: LosslessServiceworker;
|
||||
constructor(losslessServiceWorkerInstanceArg: LosslessServiceworker) {
|
||||
this.losslessSw = losslessServiceWorkerInstanceArg;
|
||||
globalThis.globalSw = this;
|
||||
};
|
||||
|
||||
/**
|
||||
* purges the cache of the app's serviceworker
|
||||
* @returns
|
||||
*/
|
||||
public async purgeCache() {
|
||||
await this.losslessSw.actionManager.waitForServiceWorkerConnection();
|
||||
console.log(`purgeCache() was executed via globalThis.globalSw`);
|
||||
const result = await this.losslessSw.actionManager.purgeServiceWorkerCache();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* attempts to reload the app
|
||||
*/
|
||||
public async reloadApp() {
|
||||
await this.purgeCache();
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import * as plugins from './lele-serviceworker.plugins.js';
|
||||
import * as interfaces from '../dist_ts_interfaces/index.js';
|
||||
import { logger } from "./serviceworker.logging.js";
|
||||
|
||||
export class NotificationManager {
|
||||
|
||||
constructor() {
|
||||
// this.askPermission();
|
||||
}
|
||||
|
||||
public askPermission () {
|
||||
Notification.requestPermission((status) => {
|
||||
console.log('Notification permission status:', status);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
import * as plugins from './lele-serviceworker.plugins.js';
|
||||
import * as interfaces from '../dist_ts_interfaces/index.js';
|
||||
import { logger } from "./serviceworker.logging.js";
|
||||
import { NotificationManager } from './lele-serviceworker.classes.notificationmanager.js';
|
||||
import { ActionManager } from './lele-serviceworker.classes.actionmanager.js';
|
||||
import { GlobalSW } from './lele-serviceworker.classes.globalsw.js'
|
||||
|
||||
export class LosslessServiceworker {
|
||||
// STATIC
|
||||
public static async createServiceWorker(): Promise<LosslessServiceworker> {
|
||||
if ('serviceWorker' in navigator) {
|
||||
try {
|
||||
logger.log('info', 'trying to register serviceworker');
|
||||
// this is some magic for Parcel to not pick up the serviceworker
|
||||
const serviceworkerInNavigator: ServiceWorkerContainer = navigator.serviceWorker;
|
||||
const swRegistration: ServiceWorkerRegistration = await serviceworkerInNavigator.register('/lsw.js', {
|
||||
scope: '/',
|
||||
updateViaCache: 'none'
|
||||
});
|
||||
plugins.smartdelay.delayFor(2000).then(async () => {
|
||||
swRegistration.onupdatefound = () => {
|
||||
logger.log('info', 'update found for service worker!');
|
||||
logger.log('warn', 'trying to find convenient time to update');
|
||||
};
|
||||
while(true) {
|
||||
await plugins.smartdelay.delayFor(60000);
|
||||
swRegistration.update();
|
||||
}
|
||||
});
|
||||
logger.log('ok', 'serviceworker registered');
|
||||
await navigator.serviceWorker.ready;
|
||||
logger.log('ok', 'serviceworker is ready!');
|
||||
await this.waitForController();
|
||||
const losslessServiceWorkerInstance = new LosslessServiceworker();
|
||||
return losslessServiceWorkerInstance;
|
||||
} catch (err) {
|
||||
// sentry integration here
|
||||
console.log(err);
|
||||
console.log(err.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async waitForController() {
|
||||
const done = new plugins.smartpromise.Deferred();
|
||||
const checkReady = () => {
|
||||
if(navigator.serviceWorker.controller) {
|
||||
logger.log('ok', 'controller is ready');
|
||||
done.resolve();
|
||||
} else {
|
||||
logger.log('warn', 'controller not ready');
|
||||
}
|
||||
};
|
||||
navigator.serviceWorker.oncontrollerchange = checkReady;
|
||||
checkReady();
|
||||
await done.promise;
|
||||
}
|
||||
|
||||
// INSTANCE
|
||||
public notificationManager: NotificationManager;
|
||||
public actionManager: ActionManager;
|
||||
public globalSw: GlobalSW;
|
||||
|
||||
constructor() {
|
||||
this.notificationManager = new NotificationManager();
|
||||
this.actionManager = new ActionManager();
|
||||
this.globalSw = new GlobalSW(this);
|
||||
}
|
||||
}
|
22
ts_web_serviceworker_client/lele-serviceworker.plugins.ts
Normal file
22
ts_web_serviceworker_client/lele-serviceworker.plugins.ts
Normal file
@ -0,0 +1,22 @@
|
||||
// @apiglobal scope
|
||||
import * as typedrequestInterfaces from '@api.global/typedrequest-interfaces';
|
||||
|
||||
export { typedrequestInterfaces };
|
||||
|
||||
// designestate
|
||||
import * as deesComms from '@design.estate/dees-comms';
|
||||
|
||||
export {
|
||||
deesComms
|
||||
};
|
||||
|
||||
// @pushrocks scope
|
||||
import * as smartdelay from '@push.rocks/smartdelay';
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
import * as webstore from '@push.rocks/webstore';
|
||||
|
||||
export {
|
||||
smartdelay,
|
||||
smartpromise,
|
||||
webstore
|
||||
};
|
16
ts_web_serviceworker_client/serviceworker.logging.ts
Normal file
16
ts_web_serviceworker_client/serviceworker.logging.ts
Normal file
@ -0,0 +1,16 @@
|
||||
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());
|
@ -1,9 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "nodenext",
|
||||
"esModuleInterop": true
|
||||
}
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true
|
||||
},
|
||||
"exclude": [
|
||||
"dist_*/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
|
Reference in New Issue
Block a user