Compare commits

...

177 Commits

Author SHA1 Message Date
0fecf69420 3.0.58 2025-02-03 00:30:18 +01:00
804537c059 fix(network-manager): Refined network management logic for better offline handling. 2025-02-03 00:30:18 +01:00
aebcbe4a61 3.0.57 2025-02-03 00:25:06 +01:00
c5cb8c1f01 fix(updateManager): Refine cache management for service worker updates. 2025-02-03 00:25:05 +01:00
8202ce6227 3.0.56 2025-02-03 00:16:59 +01:00
4598bd0e25 fix(cachemanager): Adjust cache control headers and fix redundant code 2025-02-03 00:16:58 +01:00
021c980a4f 3.0.55 2025-01-28 10:53:43 +01:00
c7dca75827 fix(server): Fix response content manipulation for HTML files with injectReload 2025-01-28 10:53:42 +01:00
4f7b2888ab 3.0.54 2025-01-28 10:32:17 +01:00
e552a48c02 fix(servertools): Fixed an issue with compression results handling in HandlerStatic where content was always being written even if not compressed. 2025-01-28 10:32:17 +01:00
2ea4139974 3.0.53 2024-12-26 00:09:18 +01:00
e225c693a8 fix(infohtml): Remove Sentry script and logo from HTML template 2024-12-26 00:09:18 +01:00
6393336ea6 3.0.52 2024-12-25 23:57:19 +01:00
d7158734d2 fix(dependencies): Bump package versions in dependencies and exports. 2024-12-25 23:57:18 +01:00
557724718c 3.0.51 2024-08-27 11:22:14 +02:00
d7a9b26873 fix(core): Update dependencies and fix service worker cache manager and task manager functionalities 2024-08-27 11:22:13 +02:00
511de8040a 3.0.50 2024-05-25 03:06:18 +02:00
952e95f82f fix(core): update 2024-05-25 03:06:17 +02:00
42115cb6be 3.0.49 2024-05-25 03:04:25 +02:00
e1206bdf4c fix(core): update 2024-05-25 03:04:25 +02:00
e32e7272ba 3.0.48 2024-05-25 02:49:46 +02:00
3f317fffd5 fix(core): update 2024-05-25 02:49:46 +02:00
a49309566c 3.0.47 2024-05-25 02:35:24 +02:00
0fb1d54e06 fix(core): update 2024-05-25 02:35:23 +02:00
f31ca98b2c 3.0.46 2024-05-25 02:34:44 +02:00
dfcda87196 fix(core): update 2024-05-25 02:34:43 +02:00
108bcb41bf 3.0.45 2024-05-25 02:29:09 +02:00
1b18961539 fix(core): update 2024-05-25 02:29:08 +02:00
4fcfd0f52c 3.0.44 2024-05-25 01:28:56 +02:00
8f1464c97e fix(core): update 2024-05-25 01:28:56 +02:00
96a88911a7 3.0.43 2024-05-25 01:24:03 +02:00
1d5af30e78 fix(core): update 2024-05-25 01:24:02 +02:00
8fe5b6985c 3.0.42 2024-05-23 15:54:34 +02:00
72e02bd611 fix(core): update 2024-05-23 15:54:33 +02:00
fb7c1242a9 3.0.41 2024-05-23 15:28:42 +02:00
360766d8b4 fix(core): update 2024-05-23 15:28:41 +02:00
9968dda0fa 3.0.40 2024-05-23 15:21:47 +02:00
77b9e41bdb fix(core): update 2024-05-23 15:21:46 +02:00
8bea58b434 3.0.39 2024-05-23 14:54:26 +02:00
bd9397eb13 fix(core): update 2024-05-23 14:54:25 +02:00
6e7316d2b1 3.0.38 2024-05-23 14:51:29 +02:00
cba65bfb81 fix(core): update 2024-05-23 14:51:28 +02:00
f06f25b4db 3.0.37 2024-05-17 17:20:29 +02:00
316625c41b fix(core): update 2024-05-17 17:20:29 +02:00
ee67c68c17 3.0.36 2024-05-14 15:33:06 +02:00
8fb2d8b3e8 fix(core): update 2024-05-14 15:33:05 +02:00
75c89b040b 3.0.35 2024-05-14 15:28:10 +02:00
b6d0843e3e fix(core): update 2024-05-14 15:28:09 +02:00
1c5e2845d1 3.0.34 2024-05-14 03:24:03 +02:00
7798bf7e0a fix(core): update 2024-05-14 03:24:03 +02:00
76db7d1733 3.0.33 2024-05-14 02:18:42 +02:00
1db472ab01 fix(core): update 2024-05-14 02:18:42 +02:00
23e88030be 3.0.32 2024-05-13 23:24:09 +02:00
1644cbbfad fix(core): update 2024-05-13 23:24:08 +02:00
84e214d087 3.0.31 2024-05-11 12:52:43 +02:00
0bb7e438d5 fix(core): update 2024-05-11 12:52:42 +02:00
1ce6d2ab01 3.0.30 2024-05-11 12:51:21 +02:00
d225a9584f fix(core): update 2024-05-11 12:51:20 +02:00
fedb37ee16 3.0.29 2024-04-19 01:01:52 +02:00
e99196b227 fix(core): update 2024-04-19 01:01:51 +02:00
3d6723d06c 3.0.28 2024-04-19 01:01:29 +02:00
fd7aadaf79 fix(core): update 2024-04-19 01:01:29 +02:00
5e4878e492 update documentation 2024-04-14 19:00:39 +02:00
64d4fb011d 3.0.27 2024-03-03 10:36:46 +01:00
6671bbe793 fix(core): update 2024-03-03 10:36:45 +01:00
14bfa3f62f 3.0.26 2024-03-01 00:14:34 +01:00
9e4413c276 fix(core): update 2024-03-01 00:14:34 +01:00
dd91691064 3.0.25 2024-02-29 18:58:10 +01:00
bf4794c06f fix(core): update 2024-02-29 18:58:09 +01:00
7e04474a66 3.0.24 2024-02-24 18:33:33 +01:00
907d51a842 fix(core): update 2024-02-24 18:33:33 +01:00
2114ff28c0 3.0.23 2024-02-21 01:16:39 +01:00
fd9431f82b fix(core): update 2024-02-21 01:16:39 +01:00
e8c1a66e15 3.0.22 2024-02-21 01:06:53 +01:00
1e98fd99f4 fix(core): update 2024-02-21 01:06:52 +01:00
da6aa9827c 3.0.21 2024-02-20 17:30:47 +01:00
ca3b8a4580 fix(core): update 2024-02-20 17:30:46 +01:00
a7ddb6b6a8 3.0.20 2024-01-19 20:52:00 +01:00
e1dfe30273 fix(core): update 2024-01-19 20:51:59 +01:00
754fa38fe8 3.0.19 2024-01-09 11:38:52 +01:00
7e59146e73 fix(core): update 2024-01-09 11:38:51 +01:00
3b550eacf7 3.0.18 2024-01-09 11:38:19 +01:00
6342895320 fix(core): update 2024-01-09 11:38:18 +01:00
b6a4095a53 3.0.17 2024-01-09 11:38:06 +01:00
622da78180 fix(core): update 2024-01-09 11:38:06 +01:00
21938e5f20 3.0.16 2024-01-09 10:25:04 +01:00
99427f5835 fix(core): update 2024-01-09 10:25:03 +01:00
552a15bb2f 3.0.15 2024-01-09 10:21:02 +01:00
b0efc48b96 fix(core): update 2024-01-09 10:21:01 +01:00
8c3aad69a0 3.0.14 2024-01-09 10:14:07 +01:00
fb2692b50e fix(core): update 2024-01-09 10:14:06 +01:00
65c868aefe 3.0.13 2024-01-08 15:34:54 +01:00
11df25f028 fix(core): update 2024-01-08 15:34:53 +01:00
efb4229f58 3.0.12 2024-01-08 14:43:12 +01:00
61dcc6badc fix(core): update 2024-01-08 14:43:11 +01:00
585da9bc79 3.0.11 2024-01-08 14:35:35 +01:00
60a2efaecb fix(core): update 2024-01-08 14:35:34 +01:00
2c8f550830 3.0.10 2024-01-07 14:50:14 +01:00
c9688159e5 fix(core): update 2024-01-07 14:50:14 +01:00
a710473d33 3.0.9 2023-11-06 13:56:03 +01:00
61c62672fc fix(core): update 2023-11-06 13:56:02 +01:00
1a7150e1f8 3.0.8 2023-11-06 11:29:53 +01:00
f35360adba fix(core): update 2023-11-06 11:29:52 +01:00
9774567dc0 3.0.7 2023-10-23 16:52:32 +02:00
529c5feeb1 fix(core): update 2023-10-23 16:52:31 +02:00
d2cac36a6e 3.0.6 2023-10-20 18:13:11 +02:00
2cdef55f13 fix(core): update 2023-10-20 18:13:10 +02:00
05444b757b 3.0.5 2023-09-21 00:48:05 +02:00
1ef5c0da06 fix(core): update 2023-09-21 00:48:04 +02:00
00b4108803 3.0.4 2023-08-06 18:15:02 +02:00
7e75cccbcb fix(core): update 2023-08-06 18:15:01 +02:00
f93d10d394 3.0.3 2023-08-06 17:45:31 +02:00
d949a05c79 fix(core): update 2023-08-06 17:45:30 +02:00
d281569bbb 3.0.2 2023-08-06 15:51:35 +02:00
ef06dd138e fix(core): update 2023-08-06 15:51:34 +02:00
60b610fc4a 3.0.1 2023-08-03 20:50:18 +02:00
b4e9bd5174 fix(core): update 2023-08-03 20:50:18 +02:00
1754524184 3.0.0 2023-08-03 20:45:10 +02:00
618f382ce9 BREAKING CHANGE(core): update 2023-08-03 20:45:09 +02:00
ef9883f100 2.0.65 2023-07-02 11:35:37 +02:00
99db788d11 fix(core): update 2023-07-02 11:35:36 +02:00
f7966e1f58 2.0.64 2023-07-02 02:09:04 +02:00
9828f7bc13 fix(core): update 2023-07-02 02:09:04 +02:00
dab87b274d 2.0.63 2023-07-02 01:53:11 +02:00
85171cb736 fix(core): update 2023-07-02 01:53:10 +02:00
0fd5e0a209 2.0.62 2023-07-02 01:46:53 +02:00
eadab07f17 fix(core): update 2023-07-02 01:46:53 +02:00
378592acc3 2.0.61 2023-07-02 01:38:50 +02:00
f885e49e34 fix(core): update 2023-07-02 01:38:49 +02:00
078730153d 2.0.60 2023-07-02 01:23:41 +02:00
4467ab76aa fix(core): update 2023-07-02 01:23:41 +02:00
a0bbf31f75 2.0.59 2023-07-01 18:25:27 +02:00
13e9ac7a98 fix(core): update 2023-07-01 18:25:27 +02:00
0ec00a5404 2.0.58 2023-07-01 17:23:50 +02:00
b0f48ba598 fix(core): update 2023-07-01 17:23:49 +02:00
ec4a51668c 2.0.57 2023-07-01 12:29:36 +02:00
07739bec27 fix(core): update 2023-07-01 12:29:35 +02:00
9aebd59c08 2.0.56 2023-07-01 11:55:35 +02:00
be7f4c503e fix(core): update 2023-07-01 11:55:35 +02:00
e1e1d4bf65 2.0.55 2023-07-01 11:49:40 +02:00
20ecb86a9e fix(core): update 2023-07-01 11:49:39 +02:00
83890d7cab 2.0.54 2023-06-12 01:06:22 +02:00
4c87ea8273 fix(core): update 2023-06-12 01:06:21 +02:00
4be625a0d9 2.0.53 2023-04-10 00:55:17 +02:00
c305ca517a fix(core): update 2023-04-10 00:55:16 +02:00
23dccae01b 2.0.52 2023-04-09 23:04:36 +02:00
d5f8d215a2 fix(core): update 2023-04-09 23:04:35 +02:00
3d4f8d1bbe 2.0.51 2023-04-04 17:23:50 +02:00
4724629efa fix(core): update 2023-04-04 17:23:49 +02:00
ff9aea12c3 2.0.50 2023-04-04 17:14:54 +02:00
910b9a495e fix(core): update 2023-04-04 17:14:54 +02:00
7fdf0a71a7 2.0.49 2023-04-01 15:12:55 +02:00
bf2c6660f2 fix(core): update 2023-04-01 15:12:54 +02:00
49afc16422 2.0.48 2023-03-31 18:04:22 +02:00
bb6f239075 fix(core): update 2023-03-31 18:04:21 +02:00
5bd5916696 2.0.47 2023-03-31 15:27:00 +02:00
62df38d083 fix(core): update 2023-03-31 15:26:59 +02:00
d7fe947107 2.0.46 2023-03-31 15:10:43 +02:00
dd426a4ca4 fix(core): update 2023-03-31 15:10:42 +02:00
2a2d4dabe4 2.0.45 2023-03-31 13:18:23 +02:00
830682d382 fix(core): update 2023-03-31 13:18:23 +02:00
d160a92bee 2.0.44 2023-03-30 19:44:44 +02:00
cc421c3321 fix(core): update 2023-03-30 19:44:44 +02:00
92ecef30d3 2.0.43 2023-03-30 19:42:55 +02:00
de4aab6df0 fix(core): update 2023-03-30 19:42:55 +02:00
5624e9dc1f 2.0.42 2023-03-30 19:40:41 +02:00
c7e40e4cde fix(core): update 2023-03-30 19:40:41 +02:00
0704febfc0 2.0.41 2023-03-30 17:44:10 +02:00
b8b1a61ae5 fix(core): update 2023-03-30 17:44:10 +02:00
45155bbce0 2.0.40 2023-03-30 16:44:46 +02:00
35bcf717cb fix(core): update 2023-03-30 16:44:45 +02:00
4d3be1064d 2.0.39 2023-03-30 16:42:02 +02:00
8ee72f9380 fix(core): update 2023-03-30 16:42:02 +02:00
049ceb7d15 2.0.38 2023-03-30 15:15:49 +02:00
87a5337db3 fix(core): update 2023-03-30 15:15:48 +02:00
2372e36367 2.0.37 2023-03-30 12:17:42 +02:00
748fc931c6 fix(core): update 2023-03-30 12:17:41 +02:00
106 changed files with 12960 additions and 3431 deletions

View 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

View 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

View File

@ -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

201
changelog.md Normal file
View File

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

View File

@ -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"
}
}

View File

@ -1,22 +1,43 @@
{
"name": "@apiglobal/typedserver",
"version": "2.0.36",
"description": "easy serving of static files",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",
"name": "@api.global/typedserver",
"version": "3.0.58",
"description": "A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.",
"type": "module",
"exports": {
".": "./dist_ts/index.js",
"./infohtml": "./dist_ts/infohtml/index.js",
"./backend": "./dist_ts/index.js",
"./edgeworker": "./dist_ts_edgeworker/index.js",
"./web_inject": "./dist_ts_web_inject/index.js",
"./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 +58,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.1.10",
"@api.global/typedrequest-interfaces": "^3.0.19",
"@api.global/typedsocket": "^3.0.1",
"@cloudflare/workers-types": "^4.20241224.0",
"@design.estate/dees-comms": "^1.0.27",
"@push.rocks/lik": "^6.1.0",
"@push.rocks/smartchok": "^1.0.34",
"@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartenv": "^5.0.12",
"@push.rocks/smartfeed": "^1.0.11",
"@push.rocks/smartfile": "^11.0.23",
"@push.rocks/smartjson": "^5.0.20",
"@push.rocks/smartlog": "^3.0.7",
"@push.rocks/smartlog-destination-devtools": "^1.0.12",
"@push.rocks/smartlog-interfaces": "^3.0.2",
"@push.rocks/smartmanifest": "^2.0.2",
"@push.rocks/smartmatch": "^2.0.0",
"@push.rocks/smartmime": "^2.0.4",
"@push.rocks/smartntml": "^2.0.8",
"@push.rocks/smartopen": "^2.0.0",
"@push.rocks/smartpath": "^5.0.18",
"@push.rocks/smartpromise": "^4.0.4",
"@push.rocks/smartrequest": "^2.0.23",
"@push.rocks/smartrx": "^3.0.7",
"@push.rocks/smartsitemap": "^2.0.3",
"@push.rocks/smartstream": "^3.2.5",
"@push.rocks/smarttime": "^4.1.1",
"@push.rocks/taskbuffer": "^3.1.7",
"@push.rocks/webrequest": "^3.0.37",
"@push.rocks/webstore": "^2.0.20",
"@tsclass/tsclass": "^4.2.0",
"@types/express": "^4.17.21",
"body-parser": "^1.20.3",
"cors": "^2.8.5",
"express": "^4.21.2",
"express-force-ssl": "^0.3.2",
"lit": "^3.2.1"
},
"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.2.0",
"@git.zone/tsbundle": "^2.1.0",
"@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^1.0.90",
"@push.rocks/tapbundle": "^5.5.3",
"@types/node": "^22.10.2"
},
"private": false,
"browserslist": [

11363
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

1
readme.hints.md Normal file
View File

@ -0,0 +1 @@

154
readme.md
View File

@ -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 | [![pipeline status](https://gitlab.com/pushrocks/typedserver/badges/master/pipeline.svg)](https://lossless.cloud)
GitLab Pipline Test Coverage | [![coverage report](https://gitlab.com/pushrocks/typedserver/badges/master/coverage.svg)](https://lossless.cloud)
npm | [![npm downloads per month](https://badgen.net/npm/dy/@apiglobal/typedserver)](https://lossless.cloud)
Snyk | [![Known Vulnerabilities](https://badgen.net/snyk/pushrocks/typedserver)](https://lossless.cloud)
TypeScript Support | [![TypeScript](https://badgen.net/badge/TypeScript/>=%203.x/blue?icon=typescript)](https://lossless.cloud)
node Support | [![node](https://img.shields.io/badge/node->=%2010.x.x-blue.svg)](https://nodejs.org/dist/latest-v10.x/docs/api/)
Code Style | [![Code Style](https://badgen.net/badge/style/prettier/purple)](https://lossless.cloud)
PackagePhobia (total standalone install weight) | [![PackagePhobia](https://badgen.net/packagephobia/install/@apiglobal/typedserver)](https://lossless.cloud)
PackagePhobia (package size on registry) | [![PackagePhobia](https://badgen.net/packagephobia/publish/@apiglobal/typedserver)](https://lossless.cloud)
BundlePhobia (total size when bundled) | [![BundlePhobia](https://badgen.net/bundlephobia/minzip/@apiglobal/typedserver)](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 | **&copy;** [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.

View File

@ -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
View 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();

View File

@ -1,8 +1,8 @@
/**
* autocreated commitinfo by @pushrocks/commitinfo
* autocreated commitinfo by @push.rocks/commitinfo
*/
export const commitinfo = {
name: '@apiglobal/typedserver',
version: '2.0.36',
description: 'easy serving of static files'
name: '@api.global/typedserver',
version: '3.0.58',
description: 'A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.'
}

246
ts/classes.typedserver.ts Normal file
View File

@ -0,0 +1,246 @@
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) => {
if (plugins.path.parse(responseArg.path).ext === '.html') {
let fileString = responseArg.responseContent.toString();
const fileStringArray = fileString.split('<head>');
if (this.options.injectReload && fileStringArray.length === 2) {
fileStringArray[0] = `${fileStringArray[0]}<head>
<!-- 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.');
responseArg.responseContent = Buffer.from(fileString);
} 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: responseArg.responseContent,
};
},
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);
}
}

View File

@ -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 };

View 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
View 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;
}
}

View File

@ -0,0 +1,3 @@
import * as smartntml from '@push.rocks/smartntml';
export { smartntml };

132
ts/infohtml/template.ts Normal file
View File

@ -0,0 +1,132 @@
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"
/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
<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.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://foss.global">learn more about foss.global</a> / &copy 2014-${new Date().getFullYear()} Task Venture Capital GmbH
</div>
</body>
</html>
`;
return smartntmlInstanceArg.renderTemplateResult(htmlTemplate);
};

View File

@ -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
View 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
View 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 };

View 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();
}
}
}

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

View 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;
}
}

View 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();
});
}
}

View File

@ -0,0 +1,144 @@
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);
} else {
res.write(fileBuffer);
}
res.end();
});
}
}

View 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();
});
}
}

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

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

View 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
View 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,
}

View 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();
});
};

View 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
`);
})
);
};

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

View 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;
};

View File

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

View File

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

View File

@ -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,
};

View 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();
}
}

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

View File

@ -0,0 +1,2 @@
export * from './classes.serviceserver.js';
export * from './classes.websiteserver.js';

View File

@ -0,0 +1,8 @@
/**
* autocreated commitinfo by @pushrocks/commitinfo
*/
export const commitinfo = {
name: 'cloudflare-workers',
version: '1.0.192',
description: 'cloudflare-workers'
}

View 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();
}
}

View 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),
});
}
}
}

View 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;
}
}
}

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

View 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();

View 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;
}
}
}

View 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()
});
}
}

View 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'
];

View File

@ -0,0 +1,7 @@
import * as interfaces from '../interfaces/index.js';
export const instructionObject: { [key: string]: interfaces.IResponderInstruction } = {
'*/ads.txt': {
type: 'ads.txt',
}
};

View File

@ -0,0 +1,2 @@
export * from './botuseragents.js';
export * from './domaininstructions.js';

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

View File

@ -0,0 +1 @@
export * from './checks.js';

1
ts_edgeworker/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './classes.edgeworker.js';

View 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>;

View File

@ -0,0 +1 @@
export * from './custom.js';

21
ts_edgeworker/plugins.ts Normal file
View 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
}

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

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

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

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

View 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';

View 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();
};

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

View 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,
})
);
}
};

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

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

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

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

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

View 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
View 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
View File

@ -0,0 +1,5 @@
import * as typedrequestInterfaces from '@api.global/typedrequest-interfaces';
export {
typedrequestInterfaces,
}

View 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;
}>;

View 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;
}>;

View 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;
}
}

View 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;
};
}

View File

@ -1,8 +0,0 @@
/**
* autocreated commitinfo by @pushrocks/commitinfo
*/
export const commitinfo = {
name: '@apiglobal/typedserver',
version: '2.0.36',
description: 'easy serving of static files'
}

View File

@ -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,
};

View 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.'
}

View File

@ -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!`);
}
}

View File

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

View 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,
};

View File

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

View File

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

View File

@ -0,0 +1,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://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);
}
});
// Prevent browser caching while allowing service worker caching
headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
headers.set('Pragma', 'no-cache');
headers.set('Expires', '0');
headers.set('Surrogate-Control', 'no-store');
await cache.put(matchRequest, new Response(responseToPutToCache.body, {
...responseToPutToCache,
headers
}));
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), 5000); // Increased timeout for better mobile compatibility
if (response && response.status >= 200 && response.status < 300) {
await runtimeCache.delete(requestArg);
await runtimeCache.put(requestArg, response);
}
}
}
}

View File

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

View File

@ -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();
});
}
}

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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);

View File

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

View File

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

View File

@ -0,0 +1,8 @@
/**
* autocreated commitinfo by @pushrocks/commitinfo
*/
export const commitinfo = {
name: '@losslessone_private/lele-serviceworker',
version: '1.0.58',
description: 'the mainthread of the serviceworker'
}

View 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;
}
}

View 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