Compare commits
161 Commits
Author | SHA1 | Date | |
---|---|---|---|
557724718c | |||
d7a9b26873 | |||
511de8040a | |||
952e95f82f | |||
42115cb6be | |||
e1206bdf4c | |||
e32e7272ba | |||
3f317fffd5 | |||
a49309566c | |||
0fb1d54e06 | |||
f31ca98b2c | |||
dfcda87196 | |||
108bcb41bf | |||
1b18961539 | |||
4fcfd0f52c | |||
8f1464c97e | |||
96a88911a7 | |||
1d5af30e78 | |||
8fe5b6985c | |||
72e02bd611 | |||
fb7c1242a9 | |||
360766d8b4 | |||
9968dda0fa | |||
77b9e41bdb | |||
8bea58b434 | |||
bd9397eb13 | |||
6e7316d2b1 | |||
cba65bfb81 | |||
f06f25b4db | |||
316625c41b | |||
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 | |||
585da9bc79 | |||
60a2efaecb | |||
2c8f550830 | |||
c9688159e5 | |||
a710473d33 | |||
61c62672fc | |||
1a7150e1f8 | |||
f35360adba | |||
9774567dc0 | |||
529c5feeb1 | |||
d2cac36a6e | |||
2cdef55f13 | |||
05444b757b | |||
1ef5c0da06 | |||
00b4108803 | |||
7e75cccbcb | |||
f93d10d394 | |||
d949a05c79 | |||
d281569bbb | |||
ef06dd138e | |||
60b610fc4a | |||
b4e9bd5174 | |||
1754524184 | |||
618f382ce9 | |||
ef9883f100 | |||
99db788d11 | |||
f7966e1f58 | |||
9828f7bc13 | |||
dab87b274d | |||
85171cb736 | |||
0fd5e0a209 | |||
eadab07f17 | |||
378592acc3 | |||
f885e49e34 | |||
078730153d | |||
4467ab76aa | |||
a0bbf31f75 | |||
13e9ac7a98 | |||
0ec00a5404 | |||
b0f48ba598 | |||
ec4a51668c | |||
07739bec27 | |||
9aebd59c08 | |||
be7f4c503e | |||
e1e1d4bf65 | |||
20ecb86a9e | |||
83890d7cab | |||
4c87ea8273 | |||
4be625a0d9 | |||
c305ca517a | |||
23dccae01b | |||
d5f8d215a2 | |||
3d4f8d1bbe | |||
4724629efa | |||
ff9aea12c3 | |||
910b9a495e | |||
7fdf0a71a7 | |||
bf2c6660f2 | |||
49afc16422 | |||
bb6f239075 | |||
5bd5916696 | |||
62df38d083 | |||
d7fe947107 | |||
dd426a4ca4 | |||
2a2d4dabe4 | |||
830682d382 | |||
d160a92bee | |||
cc421c3321 | |||
92ecef30d3 | |||
de4aab6df0 | |||
5624e9dc1f | |||
c7e40e4cde | |||
0704febfc0 | |||
b8b1a61ae5 | |||
45155bbce0 | |||
35bcf717cb | |||
4d3be1064d | |||
8ee72f9380 | |||
049ceb7d15 | |||
87a5337db3 |
67
.gitea/workflows/default_nottags.yaml
Normal file
67
.gitea/workflows/default_nottags.yaml
Normal file
@ -0,0 +1,67 @@
|
||||
name: Default (not tags)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags-ignore:
|
||||
- '**'
|
||||
|
||||
env:
|
||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
||||
|
||||
jobs:
|
||||
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install pnpm and npmci
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @shipzone/npmci
|
||||
|
||||
- name: Run npm prepare
|
||||
run: npmci npm prepare
|
||||
|
||||
- name: Audit production dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --prod
|
||||
continue-on-error: true
|
||||
|
||||
- name: Audit development dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --dev
|
||||
continue-on-error: true
|
||||
|
||||
test:
|
||||
if: ${{ always() }}
|
||||
needs: security
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Test stable
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm test
|
||||
|
||||
- name: Test build
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm build
|
110
.gitea/workflows/default_tags.yaml
Normal file
110
.gitea/workflows/default_tags.yaml
Normal file
@ -0,0 +1,110 @@
|
||||
name: Default (tags)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
env:
|
||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
||||
|
||||
jobs:
|
||||
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install pnpm and npmci
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @shipzone/npmci
|
||||
|
||||
- name: Run npm prepare
|
||||
run: npmci npm prepare
|
||||
|
||||
- name: Audit production dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --prod
|
||||
continue-on-error: true
|
||||
|
||||
- name: Audit development dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --dev
|
||||
continue-on-error: true
|
||||
|
||||
test:
|
||||
if: ${{ always() }}
|
||||
needs: security
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Test stable
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm test
|
||||
|
||||
- name: Test build
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm build
|
||||
|
||||
release:
|
||||
needs: test
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Release
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm publish
|
||||
|
||||
metadata:
|
||||
needs: test
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
continue-on-error: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Code quality
|
||||
run: |
|
||||
npmci command npm install -g typescript
|
||||
npmci npm prepare
|
||||
npmci npm install
|
||||
|
||||
- name: Trigger
|
||||
run: npmci trigger
|
||||
|
||||
- name: Build docs and upload artifacts
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
pnpm install -g @git.zone/tsdoc
|
||||
npmci command tsdoc
|
||||
continue-on-error: true
|
128
.gitlab-ci.yml
128
.gitlab-ci.yml
@ -1,128 +0,0 @@
|
||||
# gitzone ci_default
|
||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
|
||||
cache:
|
||||
paths:
|
||||
- .npmci_cache/
|
||||
key: '$CI_BUILD_STAGE'
|
||||
|
||||
stages:
|
||||
- security
|
||||
- test
|
||||
- release
|
||||
- metadata
|
||||
|
||||
before_script:
|
||||
- pnpm install -g pnpm
|
||||
- pnpm install -g @shipzone/npmci
|
||||
- npmci npm prepare
|
||||
|
||||
# ====================
|
||||
# security stage
|
||||
# ====================
|
||||
# ====================
|
||||
# security stage
|
||||
# ====================
|
||||
auditProductionDependencies:
|
||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
stage: security
|
||||
script:
|
||||
- npmci command npm config set registry https://registry.npmjs.org
|
||||
- npmci command pnpm audit --audit-level=high --prod
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
allow_failure: true
|
||||
|
||||
auditDevDependencies:
|
||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
stage: security
|
||||
script:
|
||||
- npmci command npm config set registry https://registry.npmjs.org
|
||||
- npmci command pnpm audit --audit-level=high --dev
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
allow_failure: true
|
||||
|
||||
# ====================
|
||||
# test stage
|
||||
# ====================
|
||||
|
||||
testStable:
|
||||
stage: test
|
||||
script:
|
||||
- npmci node install stable
|
||||
- npmci npm install
|
||||
- npmci npm test
|
||||
coverage: /\d+.?\d+?\%\s*coverage/
|
||||
tags:
|
||||
- docker
|
||||
|
||||
testBuild:
|
||||
stage: test
|
||||
script:
|
||||
- npmci node install stable
|
||||
- npmci npm install
|
||||
- npmci npm build
|
||||
coverage: /\d+.?\d+?\%\s*coverage/
|
||||
tags:
|
||||
- docker
|
||||
|
||||
release:
|
||||
stage: release
|
||||
script:
|
||||
- npmci node install stable
|
||||
- npmci npm publish
|
||||
only:
|
||||
- tags
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
# ====================
|
||||
# metadata stage
|
||||
# ====================
|
||||
codequality:
|
||||
stage: metadata
|
||||
allow_failure: true
|
||||
only:
|
||||
- tags
|
||||
script:
|
||||
- npmci command npm install -g typescript
|
||||
- npmci npm prepare
|
||||
- npmci npm install
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- priv
|
||||
|
||||
trigger:
|
||||
stage: metadata
|
||||
script:
|
||||
- npmci trigger
|
||||
only:
|
||||
- tags
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
pages:
|
||||
stage: metadata
|
||||
script:
|
||||
- npmci node install stable
|
||||
- npmci npm install
|
||||
- npmci command npm run buildDocs
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
only:
|
||||
- tags
|
||||
artifacts:
|
||||
expire_in: 1 week
|
||||
paths:
|
||||
- public
|
||||
allow_failure: true
|
154
changelog.md
Normal file
154
changelog.md
Normal file
@ -0,0 +1,154 @@
|
||||
# Changelog
|
||||
|
||||
## 2024-08-27 - 3.0.51 - fix(core)
|
||||
Update dependencies and fix service worker cache manager and task manager functionalities
|
||||
|
||||
- Updated dependencies in package.json to their latest versions
|
||||
- Enhanced service worker cache manager to include additional scoped URLs
|
||||
- Fixed task manager to start the task manager and added update task functionality
|
||||
- Removed .gitlab-ci.yml from the repository as part of the cleanup
|
||||
|
||||
## 2024-05-25 - 3.0.43 to 3.0.50 - Core
|
||||
Routine updates and bug fixes
|
||||
|
||||
- Updated core functionalities for better performance and stability in versions 3.0.43 to 3.0.50
|
||||
|
||||
## 2024-05-23 - 3.0.37 to 3.0.42 - Core
|
||||
Routine updates and bug fixes
|
||||
|
||||
- Updated core functionalities for better performance and stability in versions 3.0.37 to 3.0.42
|
||||
|
||||
## 2024-05-17 - 3.0.37 - Core
|
||||
Routine update and bug fix
|
||||
|
||||
- Updated core functionalities
|
||||
|
||||
## 2024-05-14 - 3.0.33 to 3.0.36 - Core
|
||||
Routine updates and bug fixes
|
||||
|
||||
- Updated core functionalities for better performance and stability in versions 3.0.33 to 3.0.36
|
||||
|
||||
## 2024-05-13 - 3.0.31 to 3.0.32 - Core
|
||||
Routine updates and bug fixes
|
||||
|
||||
- Updated core functionalities for better performance and stability in versions 3.0.31 to 3.0.32
|
||||
|
||||
## 2024-05-11 - 3.0.29 to 3.0.31 - Core
|
||||
Routine updates and bug fixes
|
||||
|
||||
- Updated core functionalities for better performance and stability in versions 3.0.29 to 3.0.31
|
||||
|
||||
## 2024-04-19 - 3.0.27 to 3.0.28 - Core
|
||||
Routine updates and bug fixes
|
||||
|
||||
- Updated core functionalities for better performance and stability in versions 3.0.27 to 3.0.28
|
||||
|
||||
## 2024-04-14 - 3.0.27 - Documentation
|
||||
Updated Documentation
|
||||
|
||||
- Improved and updated documentation
|
||||
|
||||
## 2024-03-01 - 3.0.25 to 3.0.26 - Core
|
||||
Routine updates and bug fixes
|
||||
|
||||
- Updated core functionalities for better performance and stability in versions 3.0.25 to 3.0.26
|
||||
|
||||
## 2024-02-21 - 3.0.20 to 3.0.24 - Core
|
||||
Routine updates and bug fixes
|
||||
|
||||
- Updated core functionalities for better performance and stability in versions 3.0.20 to 3.0.24
|
||||
|
||||
## 2024-01-19 - 3.0.19 to 3.0.20 - Core
|
||||
Routine updates and bug fixes
|
||||
|
||||
- Updated core functionalities for better performance and stability in versions 3.0.19 to 3.0.20
|
||||
|
||||
## 2024-01-09 - 3.0.14 to 3.0.18 - Core
|
||||
Routine updates and bug fixes
|
||||
|
||||
- Updated core functionalities for better performance and stability in versions 3.0.14 to 3.0.18
|
||||
|
||||
## 2024-01-08 - 3.0.11 to 3.0.13 - Core
|
||||
Routine updates and bug fixes
|
||||
|
||||
- Updated core functionalities for better performance and stability in versions 3.0.11 to 3.0.13
|
||||
|
||||
## 2024-01-07 - 3.0.9 to 3.0.10 - Core
|
||||
Routine updates and bug fixes
|
||||
|
||||
- Updated core functionalities for better performance and stability in versions 3.0.9 to 3.0.10
|
||||
|
||||
## 2023-11-06 - 3.0.8 to 3.0.9 - Core
|
||||
Routine updates and bug fixes
|
||||
|
||||
- Updated core functionalities for better performance and stability in versions 3.0.8 to 3.0.9
|
||||
|
||||
## 2023-10-23 - 3.0.6 to 3.0.7 - Core
|
||||
Routine updates and bug fixes
|
||||
|
||||
- Updated core functionalities for better performance and stability in versions 3.0.6 to 3.0.7
|
||||
|
||||
## 2023-10-20 - 3.0.5 to 3.0.6 - Core
|
||||
Routine updates and bug fixes
|
||||
|
||||
- Updated core functionalities for better performance and stability in versions 3.0.5 to 3.0.6
|
||||
|
||||
## 2023-09-21 - 3.0.4 to 3.0.5 - Core
|
||||
Routine updates and bug fixes
|
||||
|
||||
- Updated core functionalities for better performance and stability in versions 3.0.4 to 3.0.5
|
||||
|
||||
## 2023-08-06 - 3.0.2 to 3.0.3 - Core
|
||||
Routine updates and bug fixes
|
||||
|
||||
- Updated core functionalities for better performance and stability in versions 3.0.2 to 3.0.3
|
||||
|
||||
## 2023-08-03 - 3.0.1 to 3.0.0 - Core
|
||||
Routine updates and bug fixes
|
||||
|
||||
- Updated core functionalities for better performance and stability in versions 3.0.1 to 3.0.0
|
||||
|
||||
## 2023-08-03 - 2.0.65 - Core
|
||||
Breaking change in core update
|
||||
|
||||
- Introduced breaking changes updating core functionalities
|
||||
|
||||
## 2023-07-02 - 2.0.59 to 2.0.64 - Core
|
||||
Routine updates and bug fixes
|
||||
|
||||
- Updated core functionalities for better performance and stability in versions 2.0.59 to 2.0.64
|
||||
|
||||
## 2023-07-01 - 2.0.54 to 2.0.58 - Core
|
||||
Routine updates and bug fixes
|
||||
|
||||
- Updated core functionalities for better performance and stability in versions 2.0.54 to 2.0.58
|
||||
|
||||
## 2023-06-12 - 2.0.53 - Core
|
||||
Routine update and bug fix
|
||||
|
||||
- Updated core functionalities
|
||||
|
||||
## 2023-04-10 - 2.0.52 to 2.0.53 - Core
|
||||
Routine updates and bug fixes
|
||||
|
||||
- Updated core functionalities for better performance and stability in versions 2.0.52 to 2.0.53
|
||||
|
||||
## 2023-04-04 - 2.0.49 to 2.0.51 - Core
|
||||
Routine updates and bug fixes
|
||||
|
||||
- Updated core functionalities for better performance and stability in versions 2.0.49 to 2.0.51
|
||||
|
||||
## 2023-03-31 - 2.0.45 to 2.0.48 - Core
|
||||
Routine updates and bug fixes
|
||||
|
||||
- Updated core functionalities for better performance and stability in versions 2.0.45 to 2.0.48
|
||||
|
||||
## 2023-03-30 - 2.0.37 to 2.0.44 - Core
|
||||
Routine updates and bug fixes
|
||||
|
||||
- Updated core functionalities for better performance and stability in versions 2.0.37 to 2.0.44
|
||||
|
||||
## 2023-03-29 - 2.0.33 to 2.0.36 - Core
|
||||
Routine updates and bug fixes
|
||||
|
||||
- Updated core functionalities for better performance and stability in versions 2.0.33 to 2.0.36
|
@ -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",
|
||||
"npmPackagename": "@apiglobal/typedserver",
|
||||
"license": "MIT"
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
102
package.json
102
package.json
@ -1,22 +1,42 @@
|
||||
{
|
||||
"name": "@apiglobal/typedserver",
|
||||
"version": "2.0.37",
|
||||
"description": "easy serving of static files",
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
"name": "@api.global/typedserver",
|
||||
"version": "3.0.51",
|
||||
"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",
|
||||
"./backend": "./dist_ts/index.js",
|
||||
"./edgeworker": "./dist_ts_edgeworker/index.js",
|
||||
"./web_inject": "./dist_ts_web_inject/index.js",
|
||||
"./web_serviceworker": "./dist_ts_web_serviceworker/index.js",
|
||||
"./web_serviceworker_client": "./dist_ts_web_serviceworker_client/index.js"
|
||||
},
|
||||
"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 && 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,29 +57,51 @@
|
||||
],
|
||||
"homepage": "https://github.com/pushrocks/easyserve",
|
||||
"dependencies": {
|
||||
"@apiglobal/typedrequest": "^2.0.12",
|
||||
"@apiglobal/typedrequest-interfaces": "^2.0.1",
|
||||
"@apiglobal/typedsocket": "^2.0.23",
|
||||
"@pushrocks/smartchok": "^1.0.23",
|
||||
"@pushrocks/smartdelay": "^2.0.13",
|
||||
"@pushrocks/smartexpress": "^4.0.34",
|
||||
"@pushrocks/smartfile": "^10.0.7",
|
||||
"@pushrocks/smartlog": "^3.0.1",
|
||||
"@pushrocks/smartlog-destination-devtools": "^1.0.10",
|
||||
"@pushrocks/smartopen": "^2.0.0",
|
||||
"@pushrocks/smartpath": "^5.0.5",
|
||||
"@pushrocks/smartpromise": "^3.1.7",
|
||||
"@pushrocks/smartrx": "^3.0.0",
|
||||
"@pushrocks/webstore": "^2.0.5",
|
||||
"lit": "^2.7.0"
|
||||
"@api.global/typedrequest": "^3.0.30",
|
||||
"@api.global/typedrequest-interfaces": "^3.0.19",
|
||||
"@api.global/typedsocket": "^3.0.1",
|
||||
"@cloudflare/workers-types": "^4.20240821.1",
|
||||
"@design.estate/dees-comms": "^1.0.27",
|
||||
"@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.21",
|
||||
"@push.rocks/smartjson": "^5.0.20",
|
||||
"@push.rocks/smartlog": "^3.0.7",
|
||||
"@push.rocks/smartlog-destination-devtools": "^1.0.12",
|
||||
"@push.rocks/smartlog-interfaces": "^3.0.2",
|
||||
"@push.rocks/smartmanifest": "^2.0.2",
|
||||
"@push.rocks/smartmatch": "^2.0.0",
|
||||
"@push.rocks/smartmime": "^2.0.2",
|
||||
"@push.rocks/smartntml": "^2.0.4",
|
||||
"@push.rocks/smartopen": "^2.0.0",
|
||||
"@push.rocks/smartpath": "^5.0.18",
|
||||
"@push.rocks/smartpromise": "^4.0.4",
|
||||
"@push.rocks/smartrequest": "^2.0.22",
|
||||
"@push.rocks/smartrx": "^3.0.7",
|
||||
"@push.rocks/smartsitemap": "^2.0.3",
|
||||
"@push.rocks/smartstream": "^3.0.44",
|
||||
"@push.rocks/smarttime": "^4.0.8",
|
||||
"@push.rocks/taskbuffer": "^3.1.7",
|
||||
"@push.rocks/webrequest": "^3.0.37",
|
||||
"@push.rocks/webstore": "^2.0.20",
|
||||
"@tsclass/tsclass": "^4.1.2",
|
||||
"@types/express": "^4.17.21",
|
||||
"body-parser": "^1.20.2",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.19.2",
|
||||
"express-force-ssl": "^0.3.2",
|
||||
"lit": "^3.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@gitzone/tsbuild": "^2.1.63",
|
||||
"@gitzone/tsbundle": "^2.0.6",
|
||||
"@gitzone/tsrun": "^1.2.39",
|
||||
"@gitzone/tstest": "^1.0.72",
|
||||
"@pushrocks/tapbundle": "^5.0.4",
|
||||
"@types/node": "^18.15.11"
|
||||
"@git.zone/tsbuild": "^2.1.84",
|
||||
"@git.zone/tsbundle": "^2.0.15",
|
||||
"@git.zone/tsrun": "^1.2.49",
|
||||
"@git.zone/tstest": "^1.0.90",
|
||||
"@push.rocks/tapbundle": "^5.0.24",
|
||||
"@types/node": "^22.5.0"
|
||||
},
|
||||
"private": false,
|
||||
"browserslist": [
|
||||
|
8428
pnpm-lock.yaml
generated
8428
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 @@
|
||||
# @apiglobal/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.
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { tap, expect } from '@pushrocks/tapbundle';
|
||||
import * as smartpath from '@pushrocks/smartpath';
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import * as smartpath from '@push.rocks/smartpath';
|
||||
|
||||
import { TypedServer } from '../ts/index.js';
|
||||
|
||||
@ -10,6 +10,7 @@ tap.test('should create a valid instance of TypedServer', async () => {
|
||||
port: 3000,
|
||||
serveDir: smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
||||
watch: true,
|
||||
cors: true,
|
||||
});
|
||||
expect(testTypedServer).toBeInstanceOf(TypedServer);
|
||||
});
|
135
test/test.server.ts
Normal file
135
test/test.server.ts
Normal file
@ -0,0 +1,135 @@
|
||||
// tslint:disable-next-line:no-implicit-dependencies
|
||||
import { expect, tap } from '@push.rocks/tapbundle';
|
||||
|
||||
// helper dependencies
|
||||
// tslint:disable-next-line:no-implicit-dependencies
|
||||
|
||||
import * as smartpath from '@push.rocks/smartpath';
|
||||
import * as smartrequest from '@push.rocks/smartrequest';
|
||||
|
||||
import * as typedserver from '../ts/index.js';
|
||||
|
||||
let testServer: typedserver.servertools.Server;
|
||||
let testRoute: typedserver.servertools.Route;
|
||||
let testRoute2: typedserver.servertools.Route;
|
||||
let testHandler: typedserver.servertools.Handler;
|
||||
|
||||
// =================
|
||||
// Test class Server
|
||||
// =================
|
||||
|
||||
tap.test('should create a valid Server', async () => {
|
||||
testServer = new typedserver.servertools.Server({
|
||||
cors: true,
|
||||
domain: 'testing.git.zone',
|
||||
forceSsl: false,
|
||||
appVersion: 'v3.2.1',
|
||||
manifest: {
|
||||
name: 'Test App',
|
||||
short_name: 'testapp',
|
||||
start_url: '/',
|
||||
display: 'standalone',
|
||||
background_color: '#000',
|
||||
theme_color: '#000',
|
||||
scope: '/',
|
||||
lang: 'en',
|
||||
display_override: ['window-controls-overlay'],
|
||||
},
|
||||
feed: true,
|
||||
sitemap: true,
|
||||
robots: true,
|
||||
});
|
||||
expect(testServer).toBeInstanceOf(typedserver.servertools.Server);
|
||||
});
|
||||
|
||||
// ================
|
||||
// Test class Route
|
||||
// ================
|
||||
|
||||
tap.test('should create a valid Route', async () => {
|
||||
testRoute = testServer.addRoute('/someroute');
|
||||
testRoute2 = testServer.addRoute('/someroute/*');
|
||||
expect(testRoute).toBeInstanceOf(typedserver.servertools.Route);
|
||||
});
|
||||
|
||||
// ==================
|
||||
// Test class Handler
|
||||
// ==================
|
||||
|
||||
tap.test('should produce a valid handler', async () => {
|
||||
testHandler = new typedserver.servertools.Handler('POST', (request, response) => {
|
||||
console.log('request body is:');
|
||||
console.log(request.body);
|
||||
response.send('hi');
|
||||
});
|
||||
expect(testHandler).toBeInstanceOf(typedserver.servertools.Handler);
|
||||
});
|
||||
|
||||
tap.test('should add handler to route', async () => {
|
||||
testRoute.addHandler(testHandler);
|
||||
});
|
||||
|
||||
tap.test('should create a valid StaticHandler', async () => {
|
||||
testRoute2.addHandler(
|
||||
new typedserver.servertools.HandlerStatic(
|
||||
smartpath.get.dirnameFromImportMetaUrl(import.meta.url)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
tap.test('should add typedrequest and typedsocket', async () => {
|
||||
const typedrequest = await import('@api.global/typedrequest');
|
||||
|
||||
const typedrouter = new typedrequest.TypedRouter();
|
||||
testServer.addTypedRequest(typedrouter);
|
||||
testServer.addTypedSocket(typedrouter);
|
||||
});
|
||||
|
||||
// =====================
|
||||
// start the server and test the configuration
|
||||
// =====================
|
||||
|
||||
tap.test('should start the server allright', async () => {
|
||||
await testServer.start(3000);
|
||||
});
|
||||
|
||||
// see if a demo request holds up
|
||||
tap.test('should issue a request', async (tools) => {
|
||||
const response = await smartrequest.postJson('http://127.0.0.1:3000/someroute', {
|
||||
headers: {
|
||||
'X-Forwarded-Proto': 'https',
|
||||
},
|
||||
requestBody: {
|
||||
someprop: 'hi',
|
||||
},
|
||||
});
|
||||
console.log(response.body);
|
||||
});
|
||||
|
||||
tap.test('should get a file from disk', async () => {
|
||||
const response = await fetch('http://127.0.0.1:3000/someroute/testresponse.js');
|
||||
console.log(response.status);
|
||||
console.log(response.headers);
|
||||
});
|
||||
|
||||
tap.test('should answer a preflight request', async () => {
|
||||
const response = await fetch('http://127.0.0.1:3000/some/randompath/', {
|
||||
method: 'OPTIONS',
|
||||
});
|
||||
console.log(response.headers);
|
||||
});
|
||||
|
||||
tap.test('should exposer a sitemap', async () => {
|
||||
const response = await fetch('http://127.0.0.1:3000/sitemap');
|
||||
console.log(await response.text());
|
||||
});
|
||||
|
||||
// ========
|
||||
// clean up
|
||||
// ========
|
||||
|
||||
tap.test('should stop the server', async () => {
|
||||
await testServer.stop();
|
||||
});
|
||||
|
||||
tap.start();
|
@ -1,8 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @pushrocks/commitinfo
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@apiglobal/typedserver',
|
||||
version: '2.0.37',
|
||||
description: 'easy serving of static files'
|
||||
name: '@api.global/typedserver',
|
||||
version: '3.0.51',
|
||||
description: 'A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.'
|
||||
}
|
||||
|
245
ts/classes.typedserver.ts
Normal file
245
ts/classes.typedserver.ts
Normal file
@ -0,0 +1,245 @@
|
||||
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 {
|
||||
/**
|
||||
* serve a particular directory
|
||||
*/
|
||||
serveDir?: string;
|
||||
|
||||
/**
|
||||
* inject a reload script that takes care of live reloading
|
||||
*/
|
||||
injectReload?: boolean;
|
||||
|
||||
/**
|
||||
* enable compression
|
||||
*/
|
||||
enableCompression?: boolean;
|
||||
|
||||
/**
|
||||
* choose a preferred compression method
|
||||
*/
|
||||
preferredCompressionMethod?: TCompressionMethod;
|
||||
|
||||
/**
|
||||
* watch the serve directory?
|
||||
*/
|
||||
watch?: boolean;
|
||||
|
||||
cors: boolean;
|
||||
|
||||
/**
|
||||
* a default answer given in case there is no other handler.
|
||||
* @returns
|
||||
*/
|
||||
defaultAnswer?: () => Promise<string>;
|
||||
|
||||
/**
|
||||
* will try to reroute traffic to an ssl connection using headers
|
||||
*/
|
||||
forceSsl?: boolean;
|
||||
/**
|
||||
* allows serving manifests
|
||||
*/
|
||||
manifest?: plugins.smartmanifest.ISmartManifestConstructorOptions;
|
||||
/**
|
||||
* the port to listen on
|
||||
* can be overwritten when actually starting the server
|
||||
*/
|
||||
port?: number | string;
|
||||
publicKey?: string;
|
||||
privateKey?: string;
|
||||
sitemap?: boolean;
|
||||
feed?: boolean;
|
||||
robots?: boolean;
|
||||
domain?: string;
|
||||
/**
|
||||
* convey information about the app being served
|
||||
*/
|
||||
appVersion?: string;
|
||||
feedMetadata?: plugins.smartfeed.IFeedOptions;
|
||||
articleGetterFunction?: () => Promise<plugins.tsclass.content.IArticle[]>;
|
||||
blockWaybackMachine?: boolean;
|
||||
}
|
||||
|
||||
export class TypedServer {
|
||||
// static
|
||||
// nothing here yet
|
||||
|
||||
// instance
|
||||
public options: IServerOptions;
|
||||
public server: servertools.Server;
|
||||
public smartchokInstance: plugins.smartchok.Smartchok;
|
||||
public serveDirHashSubject = new plugins.smartrx.rxjs.ReplaySubject<string>(1);
|
||||
public serveHash: string = '000000';
|
||||
public typedsocket: plugins.typedsocket.TypedSocket;
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
public lastReload: number = Date.now();
|
||||
public ended = false;
|
||||
constructor(optionsArg: IServerOptions) {
|
||||
const standardOptions: IServerOptions = {
|
||||
port: 3000,
|
||||
injectReload: false,
|
||||
serveDir: null,
|
||||
watch: false,
|
||||
cors: true,
|
||||
};
|
||||
this.options = {
|
||||
...standardOptions,
|
||||
...optionsArg,
|
||||
};
|
||||
|
||||
this.server = new servertools.Server(this.options);
|
||||
// add routes to the smartexpress instance
|
||||
this.server.addRoute(
|
||||
'/typedserver/:request',
|
||||
new servertools.Handler('ALL', async (req, res) => {
|
||||
switch (req.params.request) {
|
||||
case 'devtools':
|
||||
res.setHeader('Content-Type', 'text/javascript');
|
||||
res.status(200);
|
||||
res.write(plugins.smartfile.fs.toStringSync(paths.injectBundlePath));
|
||||
res.end();
|
||||
break;
|
||||
case 'reloadcheck':
|
||||
console.log('got request for reloadcheck');
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.status(200);
|
||||
if (this.ended) {
|
||||
res.write('end');
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
res.write(this.lastReload.toString());
|
||||
res.end();
|
||||
}
|
||||
})
|
||||
);
|
||||
this.server.addRoute(
|
||||
'/typedrequest',
|
||||
new servertools.HandlerTypedRouter(this.typedrouter)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* inits and starts the server
|
||||
*/
|
||||
public async start() {
|
||||
if (this.options.serveDir) {
|
||||
this.server.addRoute(
|
||||
'/*',
|
||||
new servertools.HandlerStatic(this.options.serveDir, {
|
||||
responseModifier: async (responseArg) => {
|
||||
let fileString = responseArg.responseContent.toString();
|
||||
if (plugins.path.parse(responseArg.path).ext === '.html') {
|
||||
const fileStringArray = fileString.split('<head>');
|
||||
if (this.options.injectReload && fileStringArray.length === 2) {
|
||||
fileStringArray[0] = `${fileStringArray[0]}<head>
|
||||
<!-- injected by @apiglobal/typedserver start -->
|
||||
<script async defer type="module" src="/typedserver/devtools"></script>
|
||||
<script>
|
||||
globalThis.typedserver = {
|
||||
lastReload: ${this.lastReload},
|
||||
versionInfo: ${JSON.stringify({}, null, 2)},
|
||||
}
|
||||
</script>
|
||||
<!-- injected by @apiglobal/typedserver stop -->
|
||||
`;
|
||||
fileString = fileStringArray.join('');
|
||||
console.log('injected typedserver script.');
|
||||
} else if (this.options.injectReload) {
|
||||
console.log('Could not insert typedserver script');
|
||||
}
|
||||
}
|
||||
const headers = responseArg.headers;
|
||||
headers.appHash = this.serveHash;
|
||||
headers['Cache-Control'] = 'no-cache, no-store, must-revalidate';
|
||||
headers['Pragma'] = 'no-cache';
|
||||
headers['Expires'] = '0';
|
||||
return {
|
||||
headers,
|
||||
path: responseArg.path,
|
||||
responseContent: Buffer.from(fileString),
|
||||
};
|
||||
},
|
||||
serveIndexHtmlDefault: true,
|
||||
enableCompression: this.options.enableCompression,
|
||||
preferredCompressionMethod: this.options.preferredCompressionMethod,
|
||||
})
|
||||
);
|
||||
} else if (this.options.injectReload) {
|
||||
throw new Error(
|
||||
'You set to inject the reload script without a serve dir. This is not supported at the moment.'
|
||||
);
|
||||
}
|
||||
if (this.options.watch && 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();
|
||||
this.reload();
|
||||
});
|
||||
await this.createServeDirHash();
|
||||
}
|
||||
|
||||
// lets start the server
|
||||
await this.server.start();
|
||||
|
||||
this.typedsocket = await plugins.typedsocket.TypedSocket.createServer(
|
||||
this.typedrouter,
|
||||
this.server
|
||||
);
|
||||
|
||||
// lets setup typedrouter
|
||||
this.typedrouter.addTypedHandler<interfaces.IReq_GetLatestServerChangeTime>(
|
||||
new plugins.typedrequest.TypedHandler('getLatestServerChangeTime', async (reqDataArg) => {
|
||||
return {
|
||||
time: this.lastReload,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// console.log('open url in browser');
|
||||
// await plugins.smartopen.openUrl(`http://testing.git.zone:${this.options.port}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* reloads the page
|
||||
*/
|
||||
public async reload() {
|
||||
this.lastReload = Date.now();
|
||||
for (const connectionArg of await this.typedsocket.findAllTargetConnectionsByTag(
|
||||
'typedserver_frontend'
|
||||
)) {
|
||||
const pushTime =
|
||||
this.typedsocket.createTypedRequest<interfaces.IReq_PushLatestServerChangeTime>(
|
||||
'pushLatestServerChangeTime',
|
||||
connectionArg
|
||||
);
|
||||
pushTime.fire({
|
||||
time: this.lastReload,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
this.ended = true;
|
||||
await this.server.stop();
|
||||
await this.typedsocket.stop();
|
||||
if (this.smartchokInstance) {
|
||||
await this.smartchokInstance.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public async createServeDirHash() {
|
||||
const serveDirHash = await plugins.smartfile.fs.fileTreeToHash(this.options.serveDir, '**/*');
|
||||
this.serveHash = serveDirHash;
|
||||
console.log('Current ServeDir hash: ' + serveDirHash);
|
||||
this.serveDirHashSubject.next(serveDirHash);
|
||||
}
|
||||
}
|
16
ts/index.ts
16
ts/index.ts
@ -1 +1,15 @@
|
||||
export * from './typedserver.classes.typedserver.js';
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
import * as servertools from './servertools/index.js';
|
||||
|
||||
export { servertools };
|
||||
|
||||
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);
|
||||
};
|
@ -1,12 +0,0 @@
|
||||
import * as typedrequestInterfaces from '@apiglobal/typedrequest-interfaces';
|
||||
|
||||
export interface IReq_PushLatestServerChangeTime extends typedrequestInterfaces.implementsTR<
|
||||
typedrequestInterfaces.ITypedRequest,
|
||||
IReq_PushLatestServerChangeTime
|
||||
> {
|
||||
method: 'pushLatestServerChangeTime',
|
||||
request: {
|
||||
time: number;
|
||||
};
|
||||
response: {}
|
||||
}
|
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');
|
67
ts/plugins.ts
Normal file
67
ts/plugins.ts
Normal file
@ -0,0 +1,67 @@
|
||||
// node native
|
||||
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 };
|
||||
|
||||
// @tsclass scope
|
||||
import * as tsclass from '@tsclass/tsclass';
|
||||
|
||||
export { tsclass };
|
||||
|
||||
// @apiglobal scope
|
||||
import * as typedrequest from '@api.global/typedrequest';
|
||||
import * as typedrequestInterfaces from '@api.global/typedrequest-interfaces';
|
||||
import * as typedsocket from '@api.global/typedsocket';
|
||||
|
||||
export { typedrequest, typedrequestInterfaces, typedsocket };
|
||||
|
||||
// @pushrocks scope
|
||||
import * as lik from '@push.rocks/lik';
|
||||
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';
|
||||
import * as smartpath from '@push.rocks/smartpath';
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
import * as smartrequest from '@push.rocks/smartrequest';
|
||||
import * as smartrx from '@push.rocks/smartrx';
|
||||
import * as smartsitemap from '@push.rocks/smartsitemap';
|
||||
import * as smartstream from '@push.rocks/smartstream';
|
||||
import * as smarttime from '@push.rocks/smarttime';
|
||||
|
||||
export {
|
||||
lik,
|
||||
smartchok,
|
||||
smartdelay,
|
||||
smartfeed,
|
||||
smartfile,
|
||||
smartjson,
|
||||
smartmanifest,
|
||||
smartmime,
|
||||
smartopen,
|
||||
smartpath,
|
||||
smartpromise,
|
||||
smartrequest,
|
||||
smartsitemap,
|
||||
smartstream,
|
||||
smarttime,
|
||||
smartrx,
|
||||
};
|
||||
|
||||
// express
|
||||
import bodyParser from 'body-parser';
|
||||
import cors from 'cors';
|
||||
import express from 'express';
|
||||
// @ts-ignore
|
||||
import expressForceSsl from 'express-force-ssl';
|
||||
|
||||
export { bodyParser, cors, express, expressForceSsl };
|
127
ts/servertools/classes.compressor.ts
Normal file
127
ts/servertools/classes.compressor.ts
Normal file
@ -0,0 +1,127 @@
|
||||
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, {
|
||||
level: 1,
|
||||
},callback,);
|
||||
break;
|
||||
case 'br':
|
||||
plugins.zlib.brotliCompress(content, {}, callback);
|
||||
break;
|
||||
case 'deflate':
|
||||
plugins.zlib.deflate(content, callback);
|
||||
break;
|
||||
default:
|
||||
this._addToCache(cacheKey, content);
|
||||
resolve(content);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public determineCompression(acceptEncoding: string | string[], preferredCompressionMethodsArg: TCompressionMethod[] = []) {
|
||||
// Ensure acceptEncoding is a single string
|
||||
const encodingString = Array.isArray(acceptEncoding)
|
||||
? acceptEncoding.join(', ')
|
||||
: acceptEncoding;
|
||||
|
||||
let compressionMethod: TCompressionMethod = 'none';
|
||||
|
||||
// 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)) {
|
||||
compressionMethod = 'gzip';
|
||||
} else if (/\bdeflate\b/.test(encodingString)) {
|
||||
compressionMethod = 'deflate';
|
||||
}
|
||||
|
||||
return compressionMethod;
|
||||
}
|
||||
|
||||
public async maybeCompress(requestHeaders: plugins.http.IncomingHttpHeaders, content: Buffer, preferredCompressionMethodsArg?: TCompressionMethod[]): Promise<ICompressionResult> {
|
||||
const acceptEncoding = requestHeaders['accept-encoding'];
|
||||
const compressionMethod = this.determineCompression(acceptEncoding, preferredCompressionMethodsArg);
|
||||
const result = await this.compressContent(content, compressionMethod);
|
||||
return {
|
||||
result,
|
||||
compressionMethod,
|
||||
};
|
||||
}
|
||||
|
||||
public createCompressionStream(method: 'gzip' | 'deflate' | 'br' | 'none') {
|
||||
let compressionStream: any;
|
||||
switch (method) {
|
||||
case 'gzip':
|
||||
compressionStream = plugins.zlib.createGzip();
|
||||
case 'br':
|
||||
compressionStream = plugins.zlib.createBrotliCompress({
|
||||
chunkSize: 16 * 1024,
|
||||
params: {
|
||||
|
||||
},
|
||||
});
|
||||
case 'deflate':
|
||||
compressionStream = plugins.zlib.createDeflate();
|
||||
default:
|
||||
compressionStream = plugins.smartstream.createPassThrough();
|
||||
}
|
||||
}
|
||||
}
|
35
ts/servertools/classes.feed.ts
Normal file
35
ts/servertools/classes.feed.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { Handler } from './classes.handler.js';
|
||||
import { Server } from './classes.server.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
export class Feed {
|
||||
public smartexpressRef: Server;
|
||||
public smartfeedInstance = new plugins.smartfeed.Smartfeed();
|
||||
|
||||
public feedHandler = new Handler('GET', async (req, res) => {
|
||||
if (!this.smartexpressRef.options.feedMetadata) {
|
||||
res.status(500);
|
||||
res.write('feed metadata is missing');
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
if (!this.smartexpressRef.options.articleGetterFunction) {
|
||||
res.status(500);
|
||||
res.write('no article getter function defined.');
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
const xmlString = await this.smartfeedInstance.createFeedFromArticleArray(
|
||||
this.smartexpressRef.options.feedMetadata,
|
||||
await this.smartexpressRef.options.articleGetterFunction()
|
||||
);
|
||||
res.type('.xml');
|
||||
res.write(xmlString);
|
||||
res.end();
|
||||
});
|
||||
|
||||
constructor(smartexpressRefArg: Server) {
|
||||
this.smartexpressRef = smartexpressRefArg;
|
||||
this.smartexpressRef.addRouteBefore('/feed', this.feedHandler);
|
||||
}
|
||||
}
|
17
ts/servertools/classes.handler.ts
Normal file
17
ts/servertools/classes.handler.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { type Request, type Response } from 'express';
|
||||
|
||||
export interface IHandlerFunction {
|
||||
(requestArg: Request, responseArg: Response): void;
|
||||
}
|
||||
|
||||
export type THttpMethods = 'ALL' | 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
|
||||
export class Handler {
|
||||
httpMethod: THttpMethods;
|
||||
handlerFunction: IHandlerFunction;
|
||||
constructor(httpMethodArg: THttpMethods, handlerArg: IHandlerFunction) {
|
||||
this.httpMethod = httpMethodArg;
|
||||
this.handlerFunction = handlerArg;
|
||||
}
|
||||
}
|
77
ts/servertools/classes.handlerproxy.ts
Normal file
77
ts/servertools/classes.handlerproxy.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { Handler } from './classes.handler.js';
|
||||
|
||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||
|
||||
export class HandlerProxy extends Handler {
|
||||
/**
|
||||
* The constuctor of HandlerProxy
|
||||
* @param remoteMountPointArg
|
||||
*/
|
||||
constructor(
|
||||
remoteMountPointArg: string,
|
||||
optionsArg?: {
|
||||
responseModifier?: interfaces.TResponseModifier;
|
||||
headers?: { [key: string]: string };
|
||||
}
|
||||
) {
|
||||
super('ALL', async (req, res) => {
|
||||
const relativeRequestPath = req.path.slice(req.route.path.length - 1);
|
||||
const proxyRequestUrl = remoteMountPointArg + relativeRequestPath;
|
||||
console.log(`proxy ${req.path} to ${proxyRequestUrl}`);
|
||||
let proxiedResponse: plugins.smartrequest.IExtendedIncomingMessage;
|
||||
try {
|
||||
proxiedResponse = await plugins.smartrequest.request(proxyRequestUrl, {
|
||||
method: req.method,
|
||||
autoJsonParse: false,
|
||||
});
|
||||
} catch {
|
||||
res.end('failed to fullfill request');
|
||||
return;
|
||||
}
|
||||
for (const header of Object.keys(proxiedResponse.headers)) {
|
||||
res.set(header, proxiedResponse.headers[header] as string);
|
||||
}
|
||||
|
||||
// set additional headers
|
||||
if (optionsArg && optionsArg.headers) {
|
||||
for (const key of Object.keys(optionsArg.headers)) {
|
||||
res.set(key, optionsArg.headers[key]);
|
||||
}
|
||||
}
|
||||
|
||||
let responseToSend: Buffer = proxiedResponse.body;
|
||||
if (typeof responseToSend !== 'string') {
|
||||
console.log(proxyRequestUrl);
|
||||
console.log(responseToSend);
|
||||
throw new Error(`Proxied response is not a string, but ${typeof responseToSend}`);
|
||||
}
|
||||
|
||||
if (optionsArg && optionsArg.responseModifier) {
|
||||
const modifiedResponse = await optionsArg.responseModifier({
|
||||
headers: res.getHeaders(),
|
||||
path: req.path,
|
||||
responseContent: responseToSend,
|
||||
});
|
||||
|
||||
// headers
|
||||
for (const key of Object.keys(res.getHeaders())) {
|
||||
if (!modifiedResponse.headers[key]) {
|
||||
res.removeHeader(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of Object.keys(modifiedResponse.headers)) {
|
||||
res.setHeader(key, modifiedResponse.headers[key]);
|
||||
}
|
||||
|
||||
// responseContent
|
||||
responseToSend = modifiedResponse.responseContent;
|
||||
}
|
||||
|
||||
res.status(200);
|
||||
res.write(responseToSend);
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
}
|
142
ts/servertools/classes.handlerstatic.ts
Normal file
142
ts/servertools/classes.handlerstatic.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||
|
||||
import { Handler } from './classes.handler.js';
|
||||
import { Compressor, type TCompressionMethod, type ICompressionResult } from './classes.compressor.js';
|
||||
|
||||
export class HandlerStatic extends Handler {
|
||||
public compressor = new Compressor();
|
||||
constructor(
|
||||
pathArg: string,
|
||||
optionsArg?: {
|
||||
requestModifier?: interfaces.TRequestModifier;
|
||||
responseModifier?: interfaces.TResponseModifier;
|
||||
headers?: { [key: string]: string };
|
||||
serveIndexHtmlDefault?: boolean;
|
||||
enableCompression?: boolean;
|
||||
preferredCompressionMethod?: TCompressionMethod;
|
||||
}
|
||||
) {
|
||||
super('GET', async (req, res) => {
|
||||
let requestPath = req.path;
|
||||
let requestHeaders = req.headers;
|
||||
let requestBody = req.body;
|
||||
let travelData: unknown;
|
||||
if (optionsArg && optionsArg.requestModifier) {
|
||||
const modifiedRequest = await optionsArg.requestModifier({
|
||||
headers: requestHeaders,
|
||||
path: requestPath,
|
||||
body: requestBody,
|
||||
});
|
||||
|
||||
requestHeaders = modifiedRequest.headers;
|
||||
requestPath = modifiedRequest.path;
|
||||
requestBody = modifiedRequest.body;
|
||||
travelData = modifiedRequest.travelData;
|
||||
}
|
||||
|
||||
// lets compute some paths
|
||||
let filePath: string = requestPath.slice(req.route.path.length - 1); // lets slice of the root
|
||||
if (requestPath === '') {
|
||||
console.log('replaced root with index.html');
|
||||
filePath = 'index.html';
|
||||
}
|
||||
console.log(filePath);
|
||||
const joinedPath = plugins.path.join(pathArg, filePath);
|
||||
const defaultPath = plugins.path.join(pathArg, 'index.html');
|
||||
let parsedPath = plugins.path.parse(joinedPath);
|
||||
let usedPath: string;
|
||||
|
||||
// important security checks
|
||||
if (
|
||||
requestPath.includes('..') || // don't allow going up the filePath
|
||||
requestPath.includes('~') || // don't allow referencing of home directory
|
||||
!joinedPath.startsWith(pathArg) // make sure the joined path is within the directory
|
||||
) {
|
||||
res.writeHead(500);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// set additional headers
|
||||
if (optionsArg && optionsArg.headers) {
|
||||
for (const key of Object.keys(optionsArg.headers)) {
|
||||
res.set(key, optionsArg.headers[key]);
|
||||
}
|
||||
}
|
||||
|
||||
// lets actually care about serving, if security checks pass
|
||||
let fileBuffer: Buffer;
|
||||
try {
|
||||
fileBuffer = plugins.smartfile.fs.toBufferSync(joinedPath);
|
||||
usedPath = joinedPath;
|
||||
} catch (err) {
|
||||
// try serving index.html instead
|
||||
console.log(`could not resolve ${joinedPath}`);
|
||||
if (optionsArg && optionsArg.serveIndexHtmlDefault) {
|
||||
console.log(`serving default path ${defaultPath} instead of ${joinedPath}`);
|
||||
try {
|
||||
parsedPath = plugins.path.parse(defaultPath);
|
||||
fileBuffer = plugins.smartfile.fs.toBufferSync(defaultPath);
|
||||
usedPath = defaultPath;
|
||||
} catch (err) {
|
||||
res.writeHead(500);
|
||||
res.end('File not found!');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.writeHead(500);
|
||||
res.end('File not found!');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.type(parsedPath.ext);
|
||||
|
||||
const headers = res.getHeaders();
|
||||
|
||||
// lets modify the response at last
|
||||
if (optionsArg && optionsArg.responseModifier) {
|
||||
const modifiedResponse = await optionsArg.responseModifier({
|
||||
headers: res.getHeaders(),
|
||||
path: usedPath,
|
||||
responseContent: fileBuffer,
|
||||
travelData,
|
||||
});
|
||||
|
||||
// headers
|
||||
for (const key of Object.keys(res.getHeaders())) {
|
||||
if (!modifiedResponse.headers[key]) {
|
||||
res.removeHeader(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of Object.keys(modifiedResponse.headers)) {
|
||||
res.setHeader(key, modifiedResponse.headers[key]);
|
||||
}
|
||||
|
||||
// responseContent
|
||||
fileBuffer = modifiedResponse.responseContent;
|
||||
}
|
||||
|
||||
// lets finally deal with compression
|
||||
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);
|
||||
if (compressionResult?.compressionMethod) {
|
||||
res.header('Content-Encoding', compressionResult.compressionMethod);
|
||||
}
|
||||
res.write(compressionResult.result);
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
}
|
19
ts/servertools/classes.handlertypedrouter.ts
Normal file
19
ts/servertools/classes.handlertypedrouter.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { Handler } from './classes.handler.js';
|
||||
|
||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||
|
||||
export class HandlerTypedRouter extends Handler {
|
||||
/**
|
||||
* The constuctor of HandlerProxy
|
||||
* @param remoteMountPointArg
|
||||
*/
|
||||
constructor(typedrouter: plugins.typedrequest.TypedRouter) {
|
||||
super('POST', async (req, res) => {
|
||||
const response = await typedrouter.routeAndAddResponse(req.body);
|
||||
res.type('json');
|
||||
res.write(plugins.smartjson.stringify(response));
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
}
|
37
ts/servertools/classes.route.ts
Normal file
37
ts/servertools/classes.route.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { Handler } from './classes.handler.js';
|
||||
import { Server } from './classes.server.js';
|
||||
|
||||
import { type IRoute as IExpressRoute } from 'express';
|
||||
|
||||
export class Route {
|
||||
public routeString: string;
|
||||
|
||||
/**
|
||||
* an object map of handlers
|
||||
* Why multiple? Because GET, POST, PUT, DELETE, etc. can all have different handlers
|
||||
*/
|
||||
public handlerObjectMap = new plugins.lik.ObjectMap<Handler>();
|
||||
|
||||
public expressMiddlewareObjectMap = new plugins.lik.ObjectMap<any>();
|
||||
public expressRoute: IExpressRoute; // will be set to server route on server start
|
||||
constructor(ServerArg: Server, routeStringArg: string) {
|
||||
this.routeString = routeStringArg;
|
||||
}
|
||||
|
||||
/**
|
||||
* add a handler to do something with requests
|
||||
* @param handlerArg
|
||||
*/
|
||||
public addHandler(handlerArg: Handler) {
|
||||
this.handlerObjectMap.add(handlerArg);
|
||||
}
|
||||
|
||||
/**
|
||||
* add a express middleware
|
||||
* @param middlewareArg
|
||||
*/
|
||||
public addExpressMiddleWare(middlewareArg: plugins.express.Application) {
|
||||
this.expressMiddlewareObjectMap.add(middlewareArg);
|
||||
}
|
||||
}
|
338
ts/servertools/classes.server.ts
Normal file
338
ts/servertools/classes.server.ts
Normal file
@ -0,0 +1,338 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
import { Route } from './classes.route.js';
|
||||
import { Handler } from './classes.handler.js';
|
||||
import { HandlerTypedRouter } from './classes.handlertypedrouter.js';
|
||||
|
||||
// export types
|
||||
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 '../classes.typedserver.js';
|
||||
export type TServerStatus = 'initiated' | 'running' | 'stopped';
|
||||
|
||||
/**
|
||||
* can be used to spawn a server to answer http/https calls
|
||||
* for constructor options see [[IServerOptions]]
|
||||
*/
|
||||
export class Server {
|
||||
public httpServer: plugins.http.Server | plugins.https.Server;
|
||||
public expressAppInstance: plugins.express.Application;
|
||||
public routeObjectMap = new Array<Route>();
|
||||
public options: IServerOptions;
|
||||
public serverStatus: TServerStatus = 'initiated';
|
||||
|
||||
public feed: Feed;
|
||||
public sitemap: Sitemap;
|
||||
|
||||
public executeAfterStartFunctions: (() => Promise<void>)[] = [];
|
||||
|
||||
// do stuff when server is ready
|
||||
private startedDeferred = plugins.smartpromise.defer();
|
||||
// tslint:disable-next-line:member-ordering
|
||||
public startedPromise = this.startedDeferred.promise;
|
||||
|
||||
private socketMap = new plugins.lik.ObjectMap<plugins.net.Socket>();
|
||||
|
||||
constructor(optionsArg: IServerOptions) {
|
||||
this.options = {
|
||||
...optionsArg,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* allows updating of server options
|
||||
* @param optionsArg
|
||||
*/
|
||||
public updateServerOptions(optionsArg: IServerOptions) {
|
||||
Object.assign(this.options, optionsArg);
|
||||
}
|
||||
|
||||
public addTypedRequest(typedrouter: plugins.typedrequest.TypedRouter) {
|
||||
this.addRoute('/typedrequest', new HandlerTypedRouter(typedrouter));
|
||||
}
|
||||
|
||||
public addTypedSocket(typedrouter: plugins.typedrequest.TypedRouter): void {
|
||||
this.executeAfterStartFunctions.push(async () => {
|
||||
plugins.typedsocket.TypedSocket.createServer(typedrouter, this);
|
||||
});
|
||||
}
|
||||
|
||||
public addRoute(routeStringArg: string, handlerArg?: Handler) {
|
||||
const route = new Route(this, routeStringArg);
|
||||
if (handlerArg) {
|
||||
route.addHandler(handlerArg);
|
||||
}
|
||||
this.routeObjectMap.push(route);
|
||||
return route;
|
||||
}
|
||||
|
||||
public addRouteBefore(routeStringArg: string, handlerArg?: Handler) {
|
||||
const route = new Route(this, routeStringArg);
|
||||
if (handlerArg) {
|
||||
route.addHandler(handlerArg);
|
||||
}
|
||||
this.routeObjectMap.unshift(route);
|
||||
return route;
|
||||
}
|
||||
|
||||
/**
|
||||
* starts the server and sets up the routes
|
||||
* @param portArg
|
||||
* @param doListen
|
||||
*/
|
||||
public async start(portArg: number | string = this.options.port, doListen = true) {
|
||||
const done = plugins.smartpromise.defer();
|
||||
|
||||
if (typeof portArg === 'string') {
|
||||
portArg = parseInt(portArg);
|
||||
}
|
||||
|
||||
this.expressAppInstance = plugins.express();
|
||||
if (!this.httpServer && (!this.options.privateKey || !this.options.publicKey)) {
|
||||
console.log('Got no SSL certificates. Please ensure encryption using e.g. a reverse proxy');
|
||||
this.httpServer = plugins.http.createServer(this.expressAppInstance);
|
||||
} else if (!this.httpServer) {
|
||||
console.log('Got SSL certificate. Using it for the http server');
|
||||
this.httpServer = plugins.https.createServer(
|
||||
{
|
||||
key: this.options.privateKey,
|
||||
cert: this.options.publicKey,
|
||||
},
|
||||
this.expressAppInstance
|
||||
);
|
||||
} else {
|
||||
console.log('Using externally supplied http server');
|
||||
}
|
||||
this.httpServer.keepAliveTimeout = 600 * 1000;
|
||||
this.httpServer.headersTimeout = 20 * 1000;
|
||||
|
||||
// general request handlling
|
||||
this.expressAppInstance.use((req, res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
// forceSsl
|
||||
if (this.options.forceSsl) {
|
||||
this.expressAppInstance.set('forceSSLOptions', {
|
||||
enable301Redirects: true,
|
||||
trustXFPHeader: true,
|
||||
sslRequiredMessage: 'SSL Required.',
|
||||
});
|
||||
this.expressAppInstance.use(plugins.expressForceSsl);
|
||||
}
|
||||
|
||||
// cors
|
||||
if (this.options.cors) {
|
||||
const cors = plugins.cors({
|
||||
allowedHeaders: '*',
|
||||
methods: '*',
|
||||
origin: '*',
|
||||
});
|
||||
|
||||
this.expressAppInstance.use(cors);
|
||||
this.expressAppInstance.options('/*', cors);
|
||||
}
|
||||
|
||||
this.expressAppInstance.use((req, res, next) => {
|
||||
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
||||
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
|
||||
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('SERVEZONE_ROUTE', 'LOSSLESS_ORIGIN_CONTAINER');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Expires', new Date(Date.now()).toUTCString());
|
||||
next();
|
||||
});
|
||||
|
||||
// body parsing
|
||||
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
|
||||
if (this.options.robots && this.options.domain) {
|
||||
await setupRobots(this, this.options.domain);
|
||||
}
|
||||
|
||||
// manifest.json
|
||||
if (this.options.manifest) {
|
||||
await setupManifest(this.expressAppInstance, this.options.manifest);
|
||||
}
|
||||
|
||||
// sitemaps
|
||||
if (this.options.sitemap) {
|
||||
this.sitemap = new Sitemap(this);
|
||||
}
|
||||
|
||||
if (this.options.feed) {
|
||||
// feed
|
||||
this.feed = new Feed(this);
|
||||
}
|
||||
|
||||
// appVersion
|
||||
if (this.options.appVersion) {
|
||||
this.expressAppInstance.use((req, res, next) => {
|
||||
res.set('appversion', this.options.appVersion);
|
||||
next();
|
||||
});
|
||||
this.addRoute(
|
||||
'/appversion',
|
||||
new Handler('GET', async (req, res) => {
|
||||
res.write(this.options.appVersion);
|
||||
res.end();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// set up routes in for express
|
||||
await this.routeObjectMap.forEach(async (routeArg) => {
|
||||
console.log(
|
||||
`"${routeArg.routeString}" maps to ${routeArg.handlerObjectMap.getArray().length} handlers`
|
||||
);
|
||||
const expressRoute = this.expressAppInstance.route(routeArg.routeString);
|
||||
routeArg.handlerObjectMap.forEach(async (handler) => {
|
||||
console.log(` -> ${handler.httpMethod}`);
|
||||
switch (handler.httpMethod) {
|
||||
case 'GET':
|
||||
expressRoute.get(handler.handlerFunction);
|
||||
return;
|
||||
case 'POST':
|
||||
expressRoute.post(handler.handlerFunction);
|
||||
return;
|
||||
case 'PUT':
|
||||
expressRoute.put(handler.handlerFunction);
|
||||
return;
|
||||
case 'ALL':
|
||||
expressRoute.all(handler.handlerFunction);
|
||||
return;
|
||||
case 'DELETE':
|
||||
expressRoute.delete(handler.handlerFunction);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (this.options.defaultAnswer) {
|
||||
this.expressAppInstance.get('/', async (request, response) => {
|
||||
response.send(await this.options.defaultAnswer());
|
||||
});
|
||||
}
|
||||
|
||||
this.httpServer.on('connection', (connection: plugins.net.Socket) => {
|
||||
this.socketMap.add(connection);
|
||||
console.log(`added connection. now ${this.socketMap.getArray().length} sockets connected.`);
|
||||
|
||||
const closeListener = () => {
|
||||
console.log('connection closed');
|
||||
cleanupConnection();
|
||||
};
|
||||
|
||||
const errorListener = () => {
|
||||
console.log('connection errored');
|
||||
cleanupConnection();
|
||||
};
|
||||
|
||||
const endListener = () => {
|
||||
console.log('connection ended');
|
||||
cleanupConnection();
|
||||
};
|
||||
|
||||
const timeoutListener = () => {
|
||||
console.log('connection timed out');
|
||||
cleanupConnection();
|
||||
};
|
||||
|
||||
connection.addListener('close', closeListener);
|
||||
connection.addListener('error', errorListener);
|
||||
connection.addListener('end', endListener);
|
||||
connection.addListener('timeout', timeoutListener);
|
||||
|
||||
const cleanupConnection = async () => {
|
||||
connection.removeListener('close', closeListener);
|
||||
connection.removeListener('error', errorListener);
|
||||
connection.removeListener('end', endListener);
|
||||
connection.removeListener('timeout', timeoutListener);
|
||||
|
||||
if (this.socketMap.checkForObject(connection)) {
|
||||
this.socketMap.remove(connection);
|
||||
console.log(`removed connection. ${this.socketMap.getArray().length} sockets remaining.`);
|
||||
await plugins.smartdelay.delayFor(0);
|
||||
if (connection.destroyed === false) {
|
||||
connection.destroy();
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// finally listen on a port
|
||||
if (doListen) {
|
||||
this.httpServer.listen(portArg, '0.0.0.0', () => {
|
||||
console.log(`now listening on ${portArg}!`);
|
||||
this.startedDeferred.resolve();
|
||||
this.serverStatus = 'running';
|
||||
done.resolve();
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
'The server does not listen on a network stack and instead expects to get handed requests by other mechanics'
|
||||
);
|
||||
}
|
||||
await done.promise;
|
||||
for (const executeAfterStartFunction of this.executeAfterStartFunctions) {
|
||||
await executeAfterStartFunction();
|
||||
}
|
||||
}
|
||||
|
||||
public getHttpServer() {
|
||||
return this.httpServer;
|
||||
}
|
||||
|
||||
public getExpressAppInstance() {
|
||||
return this.expressAppInstance;
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
const done = plugins.smartpromise.defer();
|
||||
if (this.httpServer) {
|
||||
this.httpServer.close(async () => {
|
||||
this.serverStatus = 'stopped';
|
||||
done.resolve();
|
||||
});
|
||||
await this.socketMap.forEach(async (socket) => {
|
||||
socket.destroy();
|
||||
});
|
||||
} else {
|
||||
throw new Error('There is no Server to be stopped. Have you started it?');
|
||||
}
|
||||
return await done.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* allows handling requests and responses that come from other
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
public async handleReqRes(req: plugins.express.Request, res: plugins.express.Response) {
|
||||
this.expressAppInstance(req, res);
|
||||
}
|
||||
}
|
68
ts/servertools/classes.sitemap.ts
Normal file
68
ts/servertools/classes.sitemap.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { Server } from './classes.server.js';
|
||||
import { Handler } from './classes.handler.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import { type IUrlInfo } from '@push.rocks/smartsitemap';
|
||||
|
||||
export class Sitemap {
|
||||
public smartexpressRef: Server;
|
||||
public smartSitemap = new plugins.smartsitemap.SmartSitemap();
|
||||
public urls: plugins.smartsitemap.IUrlInfo[] = [];
|
||||
|
||||
/**
|
||||
* handles the normal sitemap request
|
||||
*/
|
||||
public sitemapHandler = new Handler('GET', async (req, res) => {
|
||||
const sitemapXmlString = await this.smartSitemap.createSitemapFromUrlInfoArray(this.urls);
|
||||
res.type('.xml');
|
||||
res.write(sitemapXmlString);
|
||||
res.end();
|
||||
});
|
||||
|
||||
/**
|
||||
* handles the sitemap-news request
|
||||
*/
|
||||
public sitemapNewsHandler = new Handler('GET', async (req, res) => {
|
||||
if (!this.smartexpressRef.options.articleGetterFunction) {
|
||||
res.status(500);
|
||||
res.write('no article getter function defined.');
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
const sitemapNewsXml = await this.smartSitemap.createSitemapNewsFromArticleArray(
|
||||
await this.smartexpressRef.options.articleGetterFunction()
|
||||
);
|
||||
res.type('.xml');
|
||||
res.write(sitemapNewsXml);
|
||||
res.end();
|
||||
});
|
||||
|
||||
constructor(smartexpressRefArg: Server) {
|
||||
this.smartexpressRef = smartexpressRefArg;
|
||||
this.smartexpressRef.addRouteBefore('/sitemap', this.sitemapHandler);
|
||||
this.smartexpressRef.addRouteBefore('/sitemap-news', this.sitemapNewsHandler);
|
||||
|
||||
// lets set the default url
|
||||
if (this.smartexpressRef.options.domain) {
|
||||
this.urls.push({
|
||||
url: `https://${this.smartexpressRef.options.domain}/`,
|
||||
timestamp: Date.now(),
|
||||
frequency: 'daily',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* replaces the current urlsArray
|
||||
* @param urlsArg
|
||||
*/
|
||||
public replaceUrls(urlsArg: IUrlInfo[]) {
|
||||
this.urls = urlsArg;
|
||||
}
|
||||
|
||||
/**
|
||||
* adds urls to the current set of urls
|
||||
*/
|
||||
public addUrls(urlsArg: IUrlInfo[]) {
|
||||
this.urls = this.urls.concat(this.urls, urlsArg);
|
||||
}
|
||||
}
|
12
ts/servertools/index.ts
Normal file
12
ts/servertools/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export * from './classes.server.js';
|
||||
export * from './classes.route.js';
|
||||
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,
|
||||
}
|
14
ts/servertools/tools.manifest.ts
Normal file
14
ts/servertools/tools.manifest.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
export const setupManifest = async (
|
||||
expressInstanceArg: plugins.express.Application,
|
||||
manifestArg: plugins.smartmanifest.ISmartManifestConstructorOptions
|
||||
) => {
|
||||
const smartmanifestInstance = new plugins.smartmanifest.SmartManifest(manifestArg);
|
||||
expressInstanceArg.get('/manifest.json', async (req, res) => {
|
||||
res.status(200);
|
||||
res.type('json');
|
||||
res.write(smartmanifestInstance.jsonString());
|
||||
res.end();
|
||||
});
|
||||
};
|
33
ts/servertools/tools.robots.ts
Normal file
33
ts/servertools/tools.robots.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { Server } from './classes.server.js';
|
||||
import { Handler } from './classes.handler.js';
|
||||
|
||||
export const setupRobots = async (smartexpressRefArg: Server, domainArg: string) => {
|
||||
smartexpressRefArg.addRouteBefore(
|
||||
'/robots.txt',
|
||||
new Handler('GET', async (req, res) => {
|
||||
res.type('text/plain');
|
||||
res.send(`
|
||||
User-agent: Googlebot-News
|
||||
Disallow: /account
|
||||
Disallow: /login
|
||||
|
||||
User-agent: *
|
||||
Disallow: /account
|
||||
Disallow: /login
|
||||
|
||||
${
|
||||
smartexpressRefArg.options.blockWaybackMachine
|
||||
? `
|
||||
User-Agent: ia_archiver
|
||||
Disallow: /
|
||||
`
|
||||
: ``
|
||||
}
|
||||
|
||||
Sitemap: https://${domainArg}/sitemap
|
||||
Sitemap: https://${domainArg}/sitemap-news
|
||||
`);
|
||||
})
|
||||
);
|
||||
};
|
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.*', 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)
|
||||
);
|
||||
};
|
22
ts/servertools/tools.sslredirect.ts
Normal file
22
ts/servertools/tools.sslredirect.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { Server } from './classes.server.js';
|
||||
import { Handler } from './classes.handler.js';
|
||||
|
||||
export const redirectFrom80To443 = async () => {
|
||||
const smartexpressInstance = new Server({
|
||||
cors: true,
|
||||
forceSsl: true,
|
||||
port: 80,
|
||||
});
|
||||
|
||||
smartexpressInstance.addRoute(
|
||||
'*',
|
||||
new Handler('ALL', async (req, res) => {
|
||||
res.redirect('https://' + req.headers.host + req.url);
|
||||
})
|
||||
);
|
||||
|
||||
await smartexpressInstance.start();
|
||||
|
||||
return smartexpressInstance;
|
||||
};
|
@ -1,170 +0,0 @@
|
||||
import * as plugins from './typedserver.plugins.js';
|
||||
import * as paths from './typedserver.paths.js';
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
|
||||
export interface IEasyServerConstructorOptions {
|
||||
serveDir: string;
|
||||
injectReload: boolean;
|
||||
port?: number;
|
||||
watch?: boolean;
|
||||
}
|
||||
|
||||
export class TypedServer {
|
||||
// static
|
||||
// nothing here yet
|
||||
|
||||
// instance
|
||||
public options: IEasyServerConstructorOptions;
|
||||
public smartexpressInstance: plugins.smartexpress.Server;
|
||||
public smartchokInstance: plugins.smartchok.Smartchok;
|
||||
public serveDirHashSubject = new plugins.smartrx.rxjs.ReplaySubject<string>(1);
|
||||
public serveHash: string = '000000';
|
||||
public typedsocket: plugins.typedsocket.TypedSocket;
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
public lastReload: number = Date.now();
|
||||
public ended = false;
|
||||
constructor(optionsArg: IEasyServerConstructorOptions) {
|
||||
const standardOptions: IEasyServerConstructorOptions = {
|
||||
injectReload: true,
|
||||
port: 3000,
|
||||
serveDir: process.cwd(),
|
||||
watch: true,
|
||||
};
|
||||
this.options = {
|
||||
...standardOptions,
|
||||
...optionsArg,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* inits and starts the server
|
||||
*/
|
||||
public async start() {
|
||||
// set the smartexpress instance
|
||||
this.smartexpressInstance = new plugins.smartexpress.Server({
|
||||
port: this.options.port,
|
||||
forceSsl: false,
|
||||
cors: true,
|
||||
});
|
||||
|
||||
// add routes to the smartexpress instance
|
||||
this.smartexpressInstance.addRoute(
|
||||
'/typedserver/:request',
|
||||
new plugins.smartexpress.Handler('ALL', async (req, res) => {
|
||||
switch (req.params.request) {
|
||||
case 'devtools':
|
||||
res.setHeader('Content-Type', 'text/javascript');
|
||||
res.status(200);
|
||||
res.write(plugins.smartfile.fs.toStringSync(paths.bundlePath));
|
||||
res.end();
|
||||
break;
|
||||
case 'reloadcheck':
|
||||
console.log('got request for reloadcheck');
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.status(200);
|
||||
if (this.ended) {
|
||||
res.write('end');
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
res.write(this.lastReload.toString());
|
||||
res.end();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.smartexpressInstance.addRoute(
|
||||
'/*',
|
||||
new plugins.smartexpress.HandlerStatic(this.options.serveDir, {
|
||||
responseModifier: async (responseArg) => {
|
||||
let fileString = responseArg.responseContent;
|
||||
if (plugins.path.parse(responseArg.path).ext === '.html') {
|
||||
const fileStringArray = fileString.split('<head>');
|
||||
if (this.options.injectReload && fileStringArray.length === 2) {
|
||||
fileStringArray[0] = `${fileStringArray[0]}<head>
|
||||
<!-- injected by @apiglobal/typedserver start -->
|
||||
<script async defer type="module" src="/typedserver/devtools"></script>
|
||||
<script>
|
||||
globalThis.typedserver = {
|
||||
lastReload: '${this.lastReload}',
|
||||
versionInfo: ${JSON.stringify({}, null, 2)},
|
||||
}
|
||||
</script>
|
||||
<!-- injected by @apiglobal/typedserver stop -->
|
||||
`;
|
||||
fileString = fileStringArray.join('');
|
||||
console.log('injected typedserver script.');
|
||||
} else if (this.options.injectReload) {
|
||||
console.log('Could not insert typedserver script');
|
||||
}
|
||||
}
|
||||
const headers = responseArg.headers;
|
||||
headers.appHash = this.serveHash;
|
||||
headers['Cache-Control'] = 'no-cache, no-store, must-revalidate';
|
||||
headers['Pragma'] = 'no-cache';
|
||||
headers['Expires'] = '0';
|
||||
return {
|
||||
headers,
|
||||
path: responseArg.path,
|
||||
responseContent: fileString,
|
||||
};
|
||||
},
|
||||
serveIndexHtmlDefault: true,
|
||||
})
|
||||
);
|
||||
|
||||
this.smartchokInstance = new plugins.smartchok.Smartchok([this.options.serveDir], {});
|
||||
if (this.options.watch) {
|
||||
await this.smartchokInstance.start();
|
||||
(await this.smartchokInstance.getObservableFor('change')).subscribe(async () => {
|
||||
await this.createServeDirHash();
|
||||
this.reload();
|
||||
});
|
||||
}
|
||||
|
||||
await this.createServeDirHash();
|
||||
|
||||
// lets start the server
|
||||
await this.smartexpressInstance.start();
|
||||
console.log('open url in browser');
|
||||
|
||||
this.typedsocket = await plugins.typedsocket.TypedSocket.createServer(
|
||||
this.typedrouter,
|
||||
this.smartexpressInstance
|
||||
);
|
||||
|
||||
// await plugins.smartopen.openUrl(`http://testing.git.zone:${this.options.port}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* reloads the page
|
||||
*/
|
||||
public async reload() {
|
||||
this.lastReload = Date.now();
|
||||
for (const connectionArg of await this.typedsocket.findAllTargetConnections(async () => true)) {
|
||||
const pushTime =
|
||||
this.typedsocket.createTypedRequest<interfaces.IReq_PushLatestServerChangeTime>(
|
||||
'pushLatestServerChangeTime',
|
||||
connectionArg
|
||||
);
|
||||
pushTime.fire({
|
||||
time: this.lastReload,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
this.ended = true;
|
||||
await this.smartexpressInstance.stop();
|
||||
await this.typedsocket.stop();
|
||||
await this.smartchokInstance.stop();
|
||||
}
|
||||
|
||||
public async createServeDirHash() {
|
||||
const serveDirHash = await plugins.smartfile.fs.fileTreeToHash(this.options.serveDir, '**/*');
|
||||
this.serveHash = serveDirHash;
|
||||
console.log('Current ServeDir hash: ' + serveDirHash);
|
||||
this.serveDirHashSubject.next(serveDirHash);
|
||||
}
|
||||
}
|
@ -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');
|
@ -1,36 +0,0 @@
|
||||
// node native
|
||||
import * as path from 'path';
|
||||
|
||||
export { path };
|
||||
|
||||
// @apiglobal scope
|
||||
import * as typedrequest from '@apiglobal/typedrequest';
|
||||
import * as typedrequestInterfaces from '@apiglobal/typedrequest-interfaces';
|
||||
import * as typedsocket from '@apiglobal/typedsocket';
|
||||
|
||||
export {
|
||||
typedrequest,
|
||||
typedrequestInterfaces,
|
||||
typedsocket,
|
||||
}
|
||||
|
||||
// @pushrocks scope
|
||||
import * as smartchok from '@pushrocks/smartchok';
|
||||
import * as smartdelay from '@pushrocks/smartdelay';
|
||||
import * as smartexpress from '@pushrocks/smartexpress';
|
||||
import * as smartfile from '@pushrocks/smartfile';
|
||||
import * as smartopen from '@pushrocks/smartopen';
|
||||
import * as smartpath from '@pushrocks/smartpath';
|
||||
import * as smartpromise from '@pushrocks/smartpromise';
|
||||
import * as smartrx from '@pushrocks/smartrx';
|
||||
|
||||
export {
|
||||
smartchok,
|
||||
smartdelay,
|
||||
smartexpress,
|
||||
smartfile,
|
||||
smartopen,
|
||||
smartpath,
|
||||
smartpromise,
|
||||
smartrx,
|
||||
};
|
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 UtilityServiceServer {
|
||||
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);
|
||||
}
|
||||
}
|
2
ts/utilityservers/index.ts
Normal file
2
ts/utilityservers/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './classes.serviceserver.js';
|
||||
export * from './classes.websiteserver.js';
|
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();
|
9
ts_interfaces/index.ts
Normal file
9
ts_interfaces/index.ts
Normal file
@ -0,0 +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,
|
||||
}
|
11
ts_interfaces/requestmodifier.ts
Normal file
11
ts_interfaces/requestmodifier.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export type TRequestModifier = <T>(responseArg: {
|
||||
headers: { [header: string]: string | string[] | undefined };
|
||||
path: string;
|
||||
body: string;
|
||||
travelData?: T;
|
||||
}) => Promise<{
|
||||
headers: { [header: string]: string | string[] | undefined };
|
||||
path: string;
|
||||
body: string;
|
||||
travelData?: T;
|
||||
}>;
|
11
ts_interfaces/responsemodifier.ts
Normal file
11
ts_interfaces/responsemodifier.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export type TResponseModifier = <T>(responseArg: {
|
||||
headers: { [header: string]: number | string | string[] | undefined };
|
||||
path: string;
|
||||
responseContent: Buffer;
|
||||
travelData?: T;
|
||||
}) => Promise<{
|
||||
headers: { [header: string]: number | string | string[] | undefined };
|
||||
path: string;
|
||||
responseContent: Buffer;
|
||||
travelData?: T;
|
||||
}>;
|
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;
|
||||
}
|
||||
}
|
26
ts_interfaces/typedrequests.ts
Normal file
26
ts_interfaces/typedrequests.ts
Normal file
@ -0,0 +1,26 @@
|
||||
// not using the global plugins here to support better bundling...
|
||||
import * as typedrequestInterfaces from '@api.global/typedrequest-interfaces';
|
||||
|
||||
export interface IReq_PushLatestServerChangeTime
|
||||
extends typedrequestInterfaces.implementsTR<
|
||||
typedrequestInterfaces.ITypedRequest,
|
||||
IReq_PushLatestServerChangeTime
|
||||
> {
|
||||
method: 'pushLatestServerChangeTime';
|
||||
request: {
|
||||
time: number;
|
||||
};
|
||||
response: {};
|
||||
}
|
||||
|
||||
export interface IReq_GetLatestServerChangeTime
|
||||
extends typedrequestInterfaces.implementsTR<
|
||||
typedrequestInterfaces.ITypedRequest,
|
||||
IReq_GetLatestServerChangeTime
|
||||
> {
|
||||
method: 'getLatestServerChangeTime';
|
||||
request: {};
|
||||
response: {
|
||||
time: number;
|
||||
};
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
/**
|
||||
* autocreated commitinfo by @pushrocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@apiglobal/typedserver',
|
||||
version: '2.0.37',
|
||||
description: 'easy serving of static files'
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
// @apiglobal scope
|
||||
import * as typedrequest from '@apiglobal/typedrequest';
|
||||
import * as typedsocket from '@apiglobal/typedsocket';
|
||||
|
||||
export {
|
||||
typedrequest,
|
||||
typedsocket,
|
||||
}
|
||||
|
||||
// pushrocks scope
|
||||
import * as smartdelay from '@pushrocks/smartdelay';
|
||||
import * as smartlog from '@pushrocks/smartlog';
|
||||
import * as smartlogDestinationDevtools from '@pushrocks/smartlog-destination-devtools';
|
||||
import * as webstore from '@pushrocks/webstore';
|
||||
|
||||
export {
|
||||
smartdelay,
|
||||
smartlog,
|
||||
smartlogDestinationDevtools,
|
||||
webstore,
|
||||
};
|
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!`);
|
||||
|
||||
@ -61,11 +61,15 @@ export class ReloadChecker {
|
||||
|
||||
public async checkReload(lastServerChange: number) {
|
||||
let reloadJustified = false;
|
||||
(await this.store.get(this.storeKey)) !== lastServerChange ? (reloadJustified = true) : null;
|
||||
let storedLastServerChange = await this.store.get(this.storeKey);
|
||||
if (storedLastServerChange && storedLastServerChange !== lastServerChange) {
|
||||
reloadJustified = true;
|
||||
} else {
|
||||
}
|
||||
|
||||
if (reloadJustified) {
|
||||
this.store.set(this.storeKey, lastServerChange);
|
||||
const reloadText = `about to reload ${
|
||||
const reloadText = `upgrading... ${
|
||||
globalThis.globalSw ? '(purging the sw cache first...)' : ''
|
||||
}`;
|
||||
this.infoscreen.setText(reloadText);
|
||||
@ -98,18 +102,29 @@ export class ReloadChecker {
|
||||
this.typedrouter,
|
||||
plugins.typedsocket.TypedSocket.useWindowLocationOriginUrl()
|
||||
);
|
||||
this.typedsocket.eventSubject.subscribe(eventArg => {
|
||||
this.typedsocket.addTag('typedserver_frontend', {});
|
||||
this.typedsocket.eventSubject.subscribe(async (eventArg) => {
|
||||
console.log(`typedsocket event subscription: ${eventArg}`);
|
||||
if (eventArg === 'disconnected' || eventArg === 'disconnecting' || eventArg === 'timedOut') {
|
||||
if (
|
||||
eventArg === 'disconnected' ||
|
||||
eventArg === 'disconnecting' ||
|
||||
eventArg === 'timedOut'
|
||||
) {
|
||||
this.backendConnectionLost = true;
|
||||
this.infoscreen.setText(`typedsocket ${eventArg}!`)
|
||||
this.infoscreen.setText(`typedsocket ${eventArg}!`);
|
||||
} else if (eventArg === 'connected' && this.backendConnectionLost) {
|
||||
this.backendConnectionLost = false;
|
||||
this.infoscreen.setSuccess('typedsocket connected!')
|
||||
this.infoscreen.setSuccess('typedsocket connected!');
|
||||
// lets check if a reload is necessary
|
||||
const getLatestServerChangeTime =
|
||||
this.typedsocket.createTypedRequest<interfaces.IReq_GetLatestServerChangeTime>(
|
||||
'getLatestServerChangeTime'
|
||||
);
|
||||
const response = await getLatestServerChangeTime.fire({});
|
||||
this.checkReload(response.time);
|
||||
}
|
||||
|
||||
});
|
||||
logger.log('success', `ReloadChecker connected through typedsocket!`)
|
||||
logger.log('success', `ReloadChecker connected through typedsocket!`);
|
||||
}
|
||||
}
|
||||
|
@ -95,10 +95,13 @@ export class TypedserverInfoscreen extends LitElement {
|
||||
|
||||
public async hide() {
|
||||
this.text = '';
|
||||
const mainbox = this.shadowRoot.querySelector('.mainbox');
|
||||
mainbox.classList.add('show');
|
||||
if (this.appended) {
|
||||
const mainbox = this.shadowRoot.querySelector('.mainbox');
|
||||
mainbox.classList.remove('show');
|
||||
}
|
||||
await plugins.smartdelay.delayFor(300);
|
||||
if (this.appended) {
|
||||
this.appended = false;
|
||||
document.body.removeChild(this);
|
||||
}
|
||||
}
|
21
ts_web_inject/typedserver_web.plugins.ts
Normal file
21
ts_web_inject/typedserver_web.plugins.ts
Normal file
@ -0,0 +1,21 @@
|
||||
// @apiglobal scope
|
||||
import * as typedrequest from '@api.global/typedrequest';
|
||||
import * as typedsocket from '@api.global/typedsocket';
|
||||
|
||||
export {
|
||||
typedrequest,
|
||||
typedsocket,
|
||||
}
|
||||
|
||||
// pushrocks scope
|
||||
import * as smartdelay from '@push.rocks/smartdelay';
|
||||
import * as smartlog from '@push.rocks/smartlog';
|
||||
import * as smartlogDestinationDevtools from '@push.rocks/smartlog-destination-devtools';
|
||||
import * as webstore from '@push.rocks/webstore';
|
||||
|
||||
export {
|
||||
smartdelay,
|
||||
smartlog,
|
||||
smartlogDestinationDevtools,
|
||||
webstore,
|
||||
};
|
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) {
|
||||
|
||||
}
|
||||
}
|
225
ts_web_serviceworker/classes.cachemanager.ts
Normal file
225
ts_web_serviceworker/classes.cachemanager.ts
Normal file
@ -0,0 +1,225 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as interfaces from './env.js';
|
||||
import { logger } from './logging.js';
|
||||
import { ServiceWorker } from './classes.serviceworker.js';
|
||||
|
||||
export class CacheManager {
|
||||
public losslessServiceWorkerRef: ServiceWorker;
|
||||
|
||||
public usedCacheNames = {
|
||||
runtimeCacheName: 'runtime'
|
||||
};
|
||||
|
||||
constructor(losslessServiceWorkerRefArg: ServiceWorker) {
|
||||
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 originalRequest: Request = fetchEventArg.request;
|
||||
const parsedUrl = new URL(originalRequest.url);
|
||||
if (
|
||||
parsedUrl.hostname.includes('paddle.com')
|
||||
|| parsedUrl.hostname.includes('paypal.com')
|
||||
|| parsedUrl.hostname.includes('reception.lossless.one')
|
||||
|| parsedUrl.pathname.startsWith('/socket.io')
|
||||
|| originalRequest.url.startsWith('https://umami.')
|
||||
) {
|
||||
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);
|
||||
|
||||
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.') ||
|
||||
originalRequest.url.includes('https://assetbroker.') ||
|
||||
originalRequest.url.includes('https://assetbroker.') ||
|
||||
originalRequest.url.includes('https://unpkg.com') ||
|
||||
originalRequest.url.includes('https://fonts.googleapis.com') ||
|
||||
originalRequest.url.includes('https://fonts.gstatic.com')
|
||||
) {
|
||||
|
||||
// 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/classes.networkmanager.ts
Normal file
33
ts_web_serviceworker/classes.networkmanager.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { ServiceWorker } from './classes.serviceworker.js';
|
||||
|
||||
export class NetworkManager {
|
||||
public serviceWorkerRef: ServiceWorker;
|
||||
public webRequest: plugins.webrequest.WebRequest;
|
||||
|
||||
public previousState: string;
|
||||
|
||||
constructor(serviceWorkerRefArg: ServiceWorker) {
|
||||
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/classes.serviceworker.ts
Normal file
76
ts_web_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 './classes.cachemanager.js';
|
||||
import { Deferred } from '@push.rocks/smartpromise';
|
||||
|
||||
import { logger } from './logging.js';
|
||||
|
||||
// imported classes
|
||||
import { UpdateManager } from './classes.updatemanager.js';
|
||||
import { NetworkManager } from './classes.networkmanager.js';
|
||||
import { TaskManager } from './classes.taskmanager.js';
|
||||
import { ServiceworkerBackend } from './classes.backend.js';
|
||||
|
||||
export class ServiceWorker {
|
||||
// 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();
|
||||
});
|
||||
}
|
||||
}
|
24
ts_web_serviceworker/classes.taskmanager.ts
Normal file
24
ts_web_serviceworker/classes.taskmanager.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { ServiceWorker } from './classes.serviceworker.js';
|
||||
|
||||
/**
|
||||
* Taskmanager
|
||||
* should use times allocated by browser
|
||||
*/
|
||||
export class TaskManager {
|
||||
public serviceworkerRef: ServiceWorker;
|
||||
public taskmanager = new plugins.taskbuffer.TaskManager();
|
||||
|
||||
constructor(serviceWorkerRefArg: ServiceWorker) {
|
||||
this.serviceworkerRef = serviceWorkerRefArg;
|
||||
this.taskmanager.start();
|
||||
}
|
||||
|
||||
public updateTask = new plugins.taskbuffer.Task({
|
||||
name: 'updateTask',
|
||||
taskFunction: async () => {
|
||||
await this.serviceworkerRef.cacheManager.cleanCaches('a new app version has been communicated by the server.');
|
||||
}
|
||||
})
|
||||
|
||||
}
|
1
ts_web_serviceworker/classes.typedrequestmanager.ts
Normal file
1
ts_web_serviceworker/classes.typedrequestmanager.ts
Normal file
@ -0,0 +1 @@
|
||||
import * as plugins from './plugins.js';
|
91
ts_web_serviceworker/classes.updatemanager.ts
Normal file
91
ts_web_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 { ServiceWorker } from './classes.serviceworker.js';
|
||||
import { logger } from './logging.js';
|
||||
import { CacheManager } from './classes.cachemanager.js';
|
||||
|
||||
export class UpdateManager {
|
||||
public lastUpdateCheck: number = 0;
|
||||
public lastVersionInfo: interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response'];
|
||||
|
||||
public serviceworkerRef: ServiceWorker;
|
||||
|
||||
constructor(serviceWorkerRefArg: ServiceWorker) {
|
||||
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
|
||||
});
|
||||
}
|
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;
|
7
ts_web_serviceworker/index.ts
Normal file
7
ts_web_serviceworker/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
// TypeScript declatations
|
||||
import * as env from './env.js';
|
||||
declare var self: env.ServiceWindow;
|
||||
|
||||
import { ServiceWorker } from './classes.serviceworker.js';
|
||||
|
||||
const sw = new ServiceWorker(self);
|
17
ts_web_serviceworker/logging.ts
Normal file
17
ts_web_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!');
|
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,
|
||||
}
|
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'
|
||||
}
|
58
ts_web_serviceworker_client/classes.actionmanager.ts
Normal file
58
ts_web_serviceworker_client/classes.actionmanager.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as interfaces from '../dist_ts_interfaces/index.js';
|
||||
import { logger } from './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;
|
||||
}
|
||||
}
|
29
ts_web_serviceworker_client/classes.globalsw.ts
Normal file
29
ts_web_serviceworker_client/classes.globalsw.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { ServiceworkerClient } from './classes.serviceworkerclient.js';
|
||||
|
||||
export class GlobalSW {
|
||||
losslessSw: ServiceworkerClient;
|
||||
constructor(losslessServiceWorkerInstanceArg: ServiceworkerClient) {
|
||||
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();
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user