Compare commits
92 Commits
Author | SHA1 | Date | |
---|---|---|---|
6da22ab607 | |||
fb98b3294a | |||
848ef1d3d1 | |||
497b267b43 | |||
d5875d5031 | |||
b06c67ebac | |||
3d7e5c439d | |||
84f7d8d4a0 | |||
42e8e575d8 | |||
d5f7fbbb9a | |||
0dcb9edcbe | |||
85ca50fc8b | |||
b3726cb518 | |||
ec6754be52 | |||
1ced20c887 | |||
3556594501 | |||
dd6babdf81 | |||
75ce27a4bf | |||
435a4a0349 | |||
b1983edcd7 | |||
1a9c656f2e | |||
569fa4fc46 | |||
cbb10d7c19 | |||
ab4c302cea | |||
0017a559ca | |||
270230b0ca | |||
6cedd53d61 | |||
f518300d68 | |||
8f6f177d19 | |||
4e560a9a51 | |||
7999e370f6 | |||
efade7a78e | |||
0fecf69420 | |||
804537c059 | |||
aebcbe4a61 | |||
c5cb8c1f01 | |||
8202ce6227 | |||
4598bd0e25 | |||
021c980a4f | |||
c7dca75827 | |||
4f7b2888ab | |||
e552a48c02 | |||
2ea4139974 | |||
e225c693a8 | |||
6393336ea6 | |||
d7158734d2 | |||
557724718c | |||
d7a9b26873 | |||
511de8040a | |||
952e95f82f | |||
42115cb6be | |||
e1206bdf4c | |||
e32e7272ba | |||
3f317fffd5 | |||
a49309566c | |||
0fb1d54e06 | |||
f31ca98b2c | |||
dfcda87196 | |||
108bcb41bf | |||
1b18961539 | |||
4fcfd0f52c | |||
8f1464c97e | |||
96a88911a7 | |||
1d5af30e78 | |||
8fe5b6985c | |||
72e02bd611 | |||
fb7c1242a9 | |||
360766d8b4 | |||
9968dda0fa | |||
77b9e41bdb | |||
8bea58b434 | |||
bd9397eb13 | |||
6e7316d2b1 | |||
cba65bfb81 | |||
f06f25b4db | |||
316625c41b | |||
ee67c68c17 | |||
8fb2d8b3e8 | |||
75c89b040b | |||
b6d0843e3e | |||
1c5e2845d1 | |||
7798bf7e0a | |||
76db7d1733 | |||
1db472ab01 | |||
23e88030be | |||
1644cbbfad | |||
84e214d087 | |||
0bb7e438d5 | |||
1ce6d2ab01 | |||
d225a9584f | |||
fedb37ee16 | |||
e99196b227 |
128
.gitlab-ci.yml
128
.gitlab-ci.yml
@ -1,128 +0,0 @@
|
||||
# gitzone ci_default
|
||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
|
||||
cache:
|
||||
paths:
|
||||
- .npmci_cache/
|
||||
key: '$CI_BUILD_STAGE'
|
||||
|
||||
stages:
|
||||
- security
|
||||
- test
|
||||
- release
|
||||
- metadata
|
||||
|
||||
before_script:
|
||||
- pnpm install -g pnpm
|
||||
- pnpm install -g @shipzone/npmci
|
||||
- npmci npm prepare
|
||||
|
||||
# ====================
|
||||
# security stage
|
||||
# ====================
|
||||
# ====================
|
||||
# security stage
|
||||
# ====================
|
||||
auditProductionDependencies:
|
||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
stage: security
|
||||
script:
|
||||
- npmci command npm config set registry https://registry.npmjs.org
|
||||
- npmci command pnpm audit --audit-level=high --prod
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
allow_failure: true
|
||||
|
||||
auditDevDependencies:
|
||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
stage: security
|
||||
script:
|
||||
- npmci command npm config set registry https://registry.npmjs.org
|
||||
- npmci command pnpm audit --audit-level=high --dev
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
allow_failure: true
|
||||
|
||||
# ====================
|
||||
# test stage
|
||||
# ====================
|
||||
|
||||
testStable:
|
||||
stage: test
|
||||
script:
|
||||
- npmci node install stable
|
||||
- npmci npm install
|
||||
- npmci npm test
|
||||
coverage: /\d+.?\d+?\%\s*coverage/
|
||||
tags:
|
||||
- docker
|
||||
|
||||
testBuild:
|
||||
stage: test
|
||||
script:
|
||||
- npmci node install stable
|
||||
- npmci npm install
|
||||
- npmci npm build
|
||||
coverage: /\d+.?\d+?\%\s*coverage/
|
||||
tags:
|
||||
- docker
|
||||
|
||||
release:
|
||||
stage: release
|
||||
script:
|
||||
- npmci node install stable
|
||||
- npmci npm publish
|
||||
only:
|
||||
- tags
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
# ====================
|
||||
# metadata stage
|
||||
# ====================
|
||||
codequality:
|
||||
stage: metadata
|
||||
allow_failure: true
|
||||
only:
|
||||
- tags
|
||||
script:
|
||||
- npmci command npm install -g typescript
|
||||
- npmci npm prepare
|
||||
- npmci npm install
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- priv
|
||||
|
||||
trigger:
|
||||
stage: metadata
|
||||
script:
|
||||
- npmci trigger
|
||||
only:
|
||||
- tags
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
pages:
|
||||
stage: metadata
|
||||
script:
|
||||
- npmci node install stable
|
||||
- npmci npm install
|
||||
- npmci command npm run buildDocs
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
only:
|
||||
- tags
|
||||
artifacts:
|
||||
expire_in: 1 week
|
||||
paths:
|
||||
- public
|
||||
allow_failure: true
|
332
changelog.md
Normal file
332
changelog.md
Normal file
@ -0,0 +1,332 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-04-12 - 3.0.74 - fix(commit-info)
|
||||
chore: update commit metadata (no source code changes)
|
||||
|
||||
- Uncommitted diff shows no changes in source files; the commit updates internal commit info only.
|
||||
|
||||
## 2025-04-11 - 3.0.73 - fix(metadata)
|
||||
Update repository URLs and metadata to reflect the new organization scope
|
||||
|
||||
- Changed gitscope from 'pushrocks' to 'api.global' in npmextra.json
|
||||
- Updated repository URL, bugs URL, and homepage in package.json to use code.foss.global/api.global/typedserver
|
||||
|
||||
## 2025-04-11 - 3.0.72 - fix(project)
|
||||
chore: no changes - commit metadata update
|
||||
|
||||
|
||||
## 2025-04-11 - 3.0.71 - fix(serviceworker)
|
||||
Improve error handling and logging in service worker backend and network manager; update multiple dependency versions and packageManager settings.
|
||||
|
||||
- Upgrade dependency versions in package.json (e.g. @cloudflare/workers-types, @push.rocks/smartfile, @push.rocks/smartpromise, @push.rocks/smartrequest, @tsclass/tsclass, and @types/express)
|
||||
- Add packageManager field to package.json
|
||||
- Enhance error handling in ServiceworkerBackend (using try/catch and detailed logging) during client reload, notification display, and alert message sending
|
||||
- Improve network request handling by clearing timeouts and converting errors reliably in NetworkManager
|
||||
- Wrap service worker install and activate event handlers with try/catch to log errors appropriately
|
||||
|
||||
## 2025-03-16 - 3.0.70 - fix(TypedServer)
|
||||
Improve error handling in server startup and response buffering. Validate configuration for reload injections, wrap file watching and TypedSocket initialization in try/catch blocks, enhance client notification and stop procedures, and ensure proper Buffer conversion in the proxy handler.
|
||||
|
||||
- Add validation to throw error if reload script is enabled without a serve directory
|
||||
- Wrap file watching and TypedSocket initialization in try/catch to prevent crashes during startup
|
||||
- Update the reload function to safely notify clients and handle notification errors
|
||||
- Enhance the stop procedure to aggregate cleanup tasks with error handling
|
||||
- Ensure consistent conversion of response bodies to Buffer in HandlerProxy with fallback when undefined
|
||||
- Include fallback hash generation in createServeDirHash for error resilience
|
||||
|
||||
## 2025-03-16 - 3.0.69 - fix(servertools)
|
||||
Fix compression stream creation returns, handler proxy buffer conversion, and sitemap URL concatenation
|
||||
|
||||
- Return compression stream immediately in createCompressionStream for each case instead of using break statements
|
||||
- Convert proxied response to a Buffer in handler proxy rather than throwing an error when it isn't a string
|
||||
- Fix addUrls method in sitemap to correctly concatenate new URLs without duplicating existing entries
|
||||
|
||||
## 2025-02-07 - 3.0.68 - fix(cache-manager)
|
||||
Simplify cache control headers in cache manager
|
||||
|
||||
- Removed unnecessary cache control headers while setting modern Cache-Control.
|
||||
|
||||
## 2025-02-06 - 3.0.67 - fix(serviceworker)
|
||||
Enhance header security for cached resources in service worker
|
||||
|
||||
- Added Cross-Origin-Resource-Policy header management for service worker cached resources.
|
||||
|
||||
## 2025-02-06 - 3.0.66 - fix(serviceworker)
|
||||
Improve error handling and logging in cache manager and update manager.
|
||||
|
||||
- Enhanced error handling and logging in cache management functions.
|
||||
- Corrected network request handling in update manager.
|
||||
- Added missing error handling for fetch events.
|
||||
|
||||
## 2025-02-04 - 3.0.65 - fix(readme)
|
||||
Update documentation with advanced usage and examples
|
||||
|
||||
- Added section on advanced usage including service worker and edge worker setup
|
||||
- Detailed integration examples for type-safe API requests and WebSocket communication
|
||||
- Expanded configuration options and cache strategies
|
||||
|
||||
## 2025-02-04 - 3.0.64 - fix(serviceworker)
|
||||
Improve cache handling and response header management in service worker.
|
||||
|
||||
- Addressed issue preventing caching of certain responses due to missing CORS headers.
|
||||
- Added 'Vary: Origin' header to ensure proper response handling.
|
||||
- Included 'Access-Control-Expose-Headers' for better CORS support.
|
||||
|
||||
## 2025-02-04 - 3.0.63 - fix(core)
|
||||
Refactored caching strategy for service worker to improve compatibility and performance.
|
||||
|
||||
- Removed hard and soft caching distinctions.
|
||||
- Simplified cache setup process.
|
||||
- Improved browser caching control headers.
|
||||
|
||||
## 2025-02-04 - 3.0.62 - fix(Service Worker)
|
||||
Refactor and clean up the cache logic in the Service Worker to improve maintainability and handle Safari-specific cache behavior.
|
||||
|
||||
- Refactored logic for determining cached domains, enhancing the readability and maintainability of the code.
|
||||
- Improved handling of CORS settings in caching requests, notably bypassing caching for soft cached domains in Safari to avoid CORS issues.
|
||||
- Enhanced error response creation for failed resource fetching, maintaining clarity on why and how certain resources were not fetched or cached.
|
||||
- Revised the structure of the caching logic to ensure consistent behavior across all supported browsers.
|
||||
|
||||
## 2025-02-04 - 3.0.61 - fix(ServiceWorkerCacheManager)
|
||||
Fixed caching mechanism to better support Safari's handling of soft-cached domains.
|
||||
|
||||
- Added logic to differentiate between hard and soft cached domains.
|
||||
- Implemented special handling for soft cached domains on Safari by bypassing caching.
|
||||
- Ensured appropriate CORS headers are present in cached responses.
|
||||
- Improved error handling with informative 500 error responses.
|
||||
- Optimized caching logic to prevent redundant caching and potential issues with locked streams on Safari.
|
||||
|
||||
## 2025-02-04 - 3.0.61 - fix(ServiceWorkerCacheManager)
|
||||
Fixed caching mechanism to better support Safari's handling of soft-cached domains.
|
||||
|
||||
- Added logic to differentiate between hard and soft cached domains.
|
||||
- Implemented special handling for soft cached domains on Safari by bypassing caching.
|
||||
- Ensured appropriate CORS headers are present in cached responses.
|
||||
- Improved error handling with informative 500 error responses.
|
||||
- Optimized caching logic to prevent redundant caching and potential issues with locked streams on Safari.
|
||||
|
||||
## 2025-02-04 - 3.0.61 - fix(ServiceWorkerCacheManager)
|
||||
Fixed caching mechanism to better support Safari's handling of soft-cached domains.
|
||||
|
||||
- Added logic to differentiate between hard and soft cached domains.
|
||||
- Implemented special handling for soft cached domains on Safari by bypassing caching.
|
||||
- Ensured appropriate CORS headers are present in cached responses.
|
||||
- Improved error handling with informative 500 error responses.
|
||||
- Optimized caching logic to prevent redundant caching and potential issues with locked streams on Safari.
|
||||
|
||||
## 2025-02-04 - 3.0.60 - fix(cachemanager)
|
||||
Improve cache management and error handling
|
||||
|
||||
- Updated comments for clarity and consistency.
|
||||
- Enhanced error handling in `fetch` event listener.
|
||||
- Optimized cache key management and cleanup process.
|
||||
- Ensured CORS headers are set for cached responses.
|
||||
- Improved logging for caching operations.
|
||||
|
||||
## 2025-02-03 - 3.0.59 - fix(serviceworker)
|
||||
Fixed CORS and Cache Control handling for Service Worker
|
||||
|
||||
- Improved handling of CORS settings for external requests.
|
||||
- Preserved important headers while excluding caching headers.
|
||||
- Ensured the presence of CORS headers in cached responses.
|
||||
- Adjusted Cache-Control headers to prevent browser caching but allow service worker caching.
|
||||
|
||||
## 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
|
@ -6,7 +6,7 @@
|
||||
"projectType": "npm",
|
||||
"module": {
|
||||
"githost": "code.foss.global",
|
||||
"gitscope": "pushrocks",
|
||||
"gitscope": "api.global",
|
||||
"gitrepo": "typedserver",
|
||||
"description": "A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.",
|
||||
"npmPackagename": "@api.global/typedserver",
|
||||
|
87
package.json
87
package.json
@ -1,18 +1,27 @@
|
||||
{
|
||||
"name": "@api.global/typedserver",
|
||||
"version": "3.0.28",
|
||||
"version": "3.0.74",
|
||||
"description": "A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.",
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
"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 --skiplibcheck && 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"
|
||||
"url": "https://code.foss.global/api.global/typedserver.git"
|
||||
},
|
||||
"keywords": [
|
||||
"TypeScript",
|
||||
@ -33,7 +42,7 @@
|
||||
"author": "Lossless GmbH <office@lossless.com> (https://lossless.com)",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/pushrocks/easyserve/issues"
|
||||
"url": "https://code.foss.global/api.global/typedserver/issues"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
@ -47,49 +56,57 @@
|
||||
"npmextra.json",
|
||||
"readme.md"
|
||||
],
|
||||
"homepage": "https://github.com/pushrocks/easyserve",
|
||||
"homepage": "https://code.foss.global/api.global/typedserver",
|
||||
"dependencies": {
|
||||
"@api.global/typedrequest": "^3.0.21",
|
||||
"@api.global/typedrequest-interfaces": "^3.0.18",
|
||||
"@api.global/typedrequest": "^3.1.10",
|
||||
"@api.global/typedrequest-interfaces": "^3.0.19",
|
||||
"@api.global/typedsocket": "^3.0.1",
|
||||
"@push.rocks/lik": "^6.0.15",
|
||||
"@push.rocks/smartchok": "^1.0.33",
|
||||
"@cloudflare/workers-types": "^4.20250410.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.14",
|
||||
"@push.rocks/smartjson": "^5.0.19",
|
||||
"@push.rocks/smartlog": "^3.0.3",
|
||||
"@push.rocks/smartlog-destination-devtools": "^1.0.10",
|
||||
"@push.rocks/smartfile": "^11.2.0",
|
||||
"@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/smartmime": "^1.0.5",
|
||||
"@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.14",
|
||||
"@push.rocks/smartpromise": "^4.0.2",
|
||||
"@push.rocks/smartrequest": "^2.0.22",
|
||||
"@push.rocks/smartpath": "^5.0.18",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartrequest": "^2.1.0",
|
||||
"@push.rocks/smartrx": "^3.0.7",
|
||||
"@push.rocks/smartsitemap": "^2.0.3",
|
||||
"@push.rocks/smartstream": "^3.0.34",
|
||||
"@push.rocks/smarttime": "^4.0.6",
|
||||
"@push.rocks/webstore": "^2.0.14",
|
||||
"@tsclass/tsclass": "^4.0.54",
|
||||
"@types/express": "^4.17.21",
|
||||
"body-parser": "^1.20.2",
|
||||
"@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": "^8.2.0",
|
||||
"@types/express": "^5.0.1",
|
||||
"body-parser": "^1.20.3",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.19.2",
|
||||
"express": "^4.21.2",
|
||||
"express-force-ssl": "^0.3.2",
|
||||
"lit": "^3.1.3"
|
||||
"lit": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.1.72",
|
||||
"@git.zone/tsbundle": "^2.0.15",
|
||||
"@git.zone/tsrun": "^1.2.44",
|
||||
"@git.zone/tstest": "^1.0.90",
|
||||
"@push.rocks/tapbundle": "^5.0.23",
|
||||
"@types/node": "^20.12.7"
|
||||
"@git.zone/tsbuild": "^2.3.2",
|
||||
"@git.zone/tsbundle": "^2.2.5",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@git.zone/tstest": "^1.0.96",
|
||||
"@push.rocks/tapbundle": "^5.6.3",
|
||||
"@types/node": "^22.14.0"
|
||||
},
|
||||
"private": false,
|
||||
"browserslist": [
|
||||
"last 1 chrome versions"
|
||||
]
|
||||
],
|
||||
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
|
||||
}
|
||||
|
11453
pnpm-lock.yaml
generated
11453
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
182
readme.md
182
readme.md
@ -1,52 +1,168 @@
|
||||
```markdown
|
||||
# @api.global/typedserver
|
||||
Easy serving of static files
|
||||
|
||||
## Install
|
||||
To install @api.global/typedserver, run the following command in your terminal:
|
||||
A TypeScript-based framework for serving static files with advanced features including live reloading, compression, and type-safe API requests. Part of the @api.global ecosystem, it integrates seamlessly with @api.global/typedrequest for type-safe HTTP requests and @api.global/typedsocket for WebSocket communication.
|
||||
|
||||
## Features
|
||||
|
||||
- **Type-Safe API Ecosystem**:
|
||||
- HTTP Requests via @api.global/typedrequest
|
||||
- WebSocket Support via @api.global/typedsocket
|
||||
- Full TypeScript support across all endpoints
|
||||
- **Service Worker Integration**: Advanced caching and offline capabilities
|
||||
- **Edge Worker Support**: Optimized edge computing capabilities
|
||||
- **Live Reload**: Automatic browser refresh on file changes
|
||||
- **Compression**: Built-in support for response compression
|
||||
- **CORS Management**: Flexible cross-origin resource sharing
|
||||
- **TypeScript First**: Built with and for TypeScript
|
||||
|
||||
## Components
|
||||
|
||||
### Core Server (`ts/`)
|
||||
- Static file serving with Express
|
||||
- Type-safe request handling
|
||||
- Live reload functionality
|
||||
- Compression middleware
|
||||
|
||||
### Service Worker (`ts_web_serviceworker/`)
|
||||
- `CacheManager`: Advanced caching strategies
|
||||
- `NetworkManager`: Request/response handling
|
||||
- `UpdateManager`: Cache invalidation and updates
|
||||
- `ServiceWorker`: Core service worker implementation
|
||||
|
||||
### Edge Worker (`ts_edgeworker/`)
|
||||
- Edge computing capabilities
|
||||
- Request/response transformation
|
||||
- Edge caching strategies
|
||||
|
||||
### Web Inject (`ts_web_inject/`)
|
||||
- Live reload script injection
|
||||
- Runtime dependency management
|
||||
- Dynamic module loading
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @api.global/typedserver --save
|
||||
npm install @api.global/typedserver
|
||||
```
|
||||
|
||||
This will add `@api.global/typedserver` to your project's dependencies.
|
||||
|
||||
## Usage
|
||||
|
||||
`@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.
|
||||
|
||||
### Setting up a Basic Web Server
|
||||
|
||||
The following example demonstrates how to set up a basic web server serving files from a directory.
|
||||
## Quick Start
|
||||
|
||||
```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 server = new TypedServer({
|
||||
port: 3000,
|
||||
serveDir: './public',
|
||||
watch: true,
|
||||
compression: true
|
||||
});
|
||||
|
||||
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);
|
||||
server.start();
|
||||
```
|
||||
|
||||
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.
|
||||
## Type-Safe API Integration
|
||||
|
||||
### Using Typed Requests and Responses
|
||||
### HTTP Requests with TypedRequest
|
||||
```typescript
|
||||
import { TypedRequest } from '@api.global/typedrequest';
|
||||
|
||||
`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.
|
||||
// Define your request/response interface
|
||||
interface IUserRequest {
|
||||
method: 'getUser';
|
||||
request: { userId: string };
|
||||
response: { username: string; email: string; };
|
||||
}
|
||||
|
||||
First, define the types:
|
||||
// Create and use a typed request
|
||||
const getUserRequest = new TypedRequest<IUserRequest>('/api/users', 'getUser');
|
||||
const user = await getUserRequest.fire({ userId: '123' });
|
||||
```
|
||||
|
||||
### WebSocket Communication
|
||||
```typescript
|
||||
import { TypedSocket } from '@api.global/typedsocket';
|
||||
|
||||
// Server setup
|
||||
const typedRouter = new TypedRouter();
|
||||
const server = await TypedSocket.createServer(typedRouter);
|
||||
|
||||
// Client connection
|
||||
const client = await TypedSocket.createClient(typedRouter, 'ws://localhost:3000');
|
||||
|
||||
// Type-safe real-time messaging
|
||||
interface IChatMessage {
|
||||
method: 'sendMessage';
|
||||
request: { text: string };
|
||||
response: { id: string; timestamp: number; };
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Service Worker Setup
|
||||
|
||||
```typescript
|
||||
import { ServiceWorker } from '@api.global/typedserver/web_serviceworker';
|
||||
|
||||
const sw = new ServiceWorker({
|
||||
cacheStrategy: 'network-first',
|
||||
offlineSupport: true
|
||||
});
|
||||
|
||||
sw.register();
|
||||
```
|
||||
|
||||
### Edge Worker Configuration
|
||||
|
||||
```typescript
|
||||
import { EdgeWorker } from '@api.global/typedserver/edgeworker';
|
||||
|
||||
const edge = new EdgeWorker({
|
||||
transforms: ['compress', 'minify'],
|
||||
caching: true
|
||||
});
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Server Options
|
||||
```typescript
|
||||
interface IServerOptions {
|
||||
port?: number;
|
||||
host?: string;
|
||||
serveDir: string;
|
||||
watch?: boolean;
|
||||
compression?: boolean;
|
||||
cors?: boolean | CorsOptions;
|
||||
cache?: CacheOptions;
|
||||
}
|
||||
```
|
||||
|
||||
### Cache Strategies
|
||||
```typescript
|
||||
type CacheStrategy =
|
||||
| 'network-first'
|
||||
| 'cache-first'
|
||||
| 'stale-while-revalidate';
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
See [API Documentation](https://api.global/docs/typedserver) for detailed API reference.
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create your feature branch
|
||||
3. Commit your changes
|
||||
4. Push to the branch
|
||||
5. Create a Pull Request
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see LICENSE for details.
|
||||
|
||||
Task Venture Capital GmbH © 2024
|
||||
|
||||
```typescript
|
||||
// Define a request type
|
||||
|
@ -1,8 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @pushrocks/commitinfo
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@api.global/typedserver',
|
||||
version: '3.0.28',
|
||||
version: '3.0.74',
|
||||
description: 'A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.'
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as paths from './typedserver.paths.js';
|
||||
import * as interfaces from './interfaces/index.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';
|
||||
|
||||
@ -81,6 +81,7 @@ export class TypedServer {
|
||||
|
||||
public lastReload: number = Date.now();
|
||||
public ended = false;
|
||||
|
||||
constructor(optionsArg: IServerOptions) {
|
||||
const standardOptions: IServerOptions = {
|
||||
port: 3000,
|
||||
@ -103,7 +104,7 @@ export class TypedServer {
|
||||
case 'devtools':
|
||||
res.setHeader('Content-Type', 'text/javascript');
|
||||
res.status(200);
|
||||
res.write(plugins.smartfile.fs.toStringSync(paths.bundlePath));
|
||||
res.write(plugins.smartfile.fs.toStringSync(paths.injectBundlePath));
|
||||
res.end();
|
||||
break;
|
||||
case 'reloadcheck':
|
||||
@ -117,26 +118,39 @@ export class TypedServer {
|
||||
}
|
||||
res.write(this.lastReload.toString());
|
||||
res.end();
|
||||
break;
|
||||
default:
|
||||
res.status(404);
|
||||
res.write('Unknown request type');
|
||||
res.end();
|
||||
break;
|
||||
}
|
||||
})
|
||||
);
|
||||
this.server.addRoute(
|
||||
'/typedrequest',
|
||||
new servertools.HandlerTypedRouter(this.typedrouter)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* inits and starts the server
|
||||
*/
|
||||
public async start() {
|
||||
// Validate essential configuration before starting
|
||||
if (this.options.injectReload && !this.options.serveDir) {
|
||||
throw new Error(
|
||||
'You set to inject the reload script without a serve dir. This is not supported at the moment.'
|
||||
);
|
||||
}
|
||||
|
||||
if (this.options.serveDir) {
|
||||
this.server.addRoute(
|
||||
'/*',
|
||||
new servertools.HandlerStatic(this.options.serveDir, {
|
||||
responseModifier: async (responseArg) => {
|
||||
let fileString = responseArg.responseContent.toString();
|
||||
if (plugins.path.parse(responseArg.path).ext === '.html') {
|
||||
let fileString = responseArg.responseContent.toString();
|
||||
const fileStringArray = fileString.split('<head>');
|
||||
if (this.options.injectReload && fileStringArray.length === 2) {
|
||||
fileStringArray[0] = `${fileStringArray[0]}<head>
|
||||
@ -152,8 +166,9 @@ export class TypedServer {
|
||||
`;
|
||||
fileString = fileStringArray.join('');
|
||||
console.log('injected typedserver script.');
|
||||
responseArg.responseContent = Buffer.from(fileString);
|
||||
} else if (this.options.injectReload) {
|
||||
console.log('Could not insert typedserver script');
|
||||
console.log('Could not insert typedserver script - no <head> tag found');
|
||||
}
|
||||
}
|
||||
const headers = responseArg.headers;
|
||||
@ -164,7 +179,8 @@ export class TypedServer {
|
||||
return {
|
||||
headers,
|
||||
path: responseArg.path,
|
||||
responseContent: Buffer.from(fileString),
|
||||
responseContent: responseArg.responseContent,
|
||||
travelData: responseArg.travelData,
|
||||
};
|
||||
},
|
||||
serveIndexHtmlDefault: true,
|
||||
@ -172,40 +188,44 @@ export class TypedServer {
|
||||
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 () => {
|
||||
try {
|
||||
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();
|
||||
this.reload();
|
||||
});
|
||||
await this.createServeDirHash();
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize file watching:', error);
|
||||
// Continue without file watching rather than crashing
|
||||
}
|
||||
}
|
||||
|
||||
// lets start the server
|
||||
await this.server.start();
|
||||
|
||||
this.typedsocket = await plugins.typedsocket.TypedSocket.createServer(
|
||||
this.typedrouter,
|
||||
this.server
|
||||
);
|
||||
try {
|
||||
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}`);
|
||||
// lets setup typedrouter
|
||||
this.typedrouter.addTypedHandler<interfaces.IReq_GetLatestServerChangeTime>(
|
||||
new plugins.typedrequest.TypedHandler('getLatestServerChangeTime', async () => {
|
||||
return {
|
||||
time: this.lastReload,
|
||||
};
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize TypedSocket:', error);
|
||||
// Continue without WebSocket support rather than crashing
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -213,33 +233,80 @@ export class TypedServer {
|
||||
*/
|
||||
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>(
|
||||
if (!this.typedsocket) {
|
||||
console.warn('TypedSocket not initialized, skipping client notifications');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const connections = await this.typedsocket.findAllTargetConnectionsByTag('typedserver_frontend');
|
||||
for (const connection of connections) {
|
||||
const pushTime = this.typedsocket.createTypedRequest<interfaces.IReq_PushLatestServerChangeTime>(
|
||||
'pushLatestServerChangeTime',
|
||||
connectionArg
|
||||
connection
|
||||
);
|
||||
pushTime.fire({
|
||||
time: this.lastReload,
|
||||
});
|
||||
pushTime.fire({
|
||||
time: this.lastReload,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to notify clients about reload:', error);
|
||||
}
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
/**
|
||||
* Stops the server and cleans up resources
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
this.ended = true;
|
||||
await this.server.stop();
|
||||
await this.typedsocket.stop();
|
||||
if (this.smartchokInstance) {
|
||||
await this.smartchokInstance.stop();
|
||||
|
||||
const stopWithErrorHandling = async (
|
||||
stopFn: () => Promise<unknown>,
|
||||
componentName: string
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await stopFn();
|
||||
} catch (err) {
|
||||
console.error(`Error stopping ${componentName}:`, err);
|
||||
}
|
||||
};
|
||||
|
||||
const tasks: Promise<void>[] = [];
|
||||
|
||||
// Stop server
|
||||
if (this.server) {
|
||||
tasks.push(stopWithErrorHandling(() => this.server.stop(), 'server'));
|
||||
}
|
||||
|
||||
// Stop TypedSocket
|
||||
if (this.typedsocket) {
|
||||
tasks.push(stopWithErrorHandling(() => this.typedsocket.stop(), 'TypedSocket'));
|
||||
}
|
||||
|
||||
// Stop file watcher
|
||||
if (this.smartchokInstance) {
|
||||
tasks.push(stopWithErrorHandling(() => this.smartchokInstance.stop(), 'file watcher'));
|
||||
}
|
||||
|
||||
await Promise.all(tasks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a hash of the served directory for cache busting
|
||||
*/
|
||||
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);
|
||||
try {
|
||||
const serveDirHash = await plugins.smartfile.fs.fileTreeToHash(this.options.serveDir, '**/*');
|
||||
this.serveHash = serveDirHash;
|
||||
console.log('Current ServeDir hash: ' + serveDirHash);
|
||||
this.serveDirHashSubject.next(serveDirHash);
|
||||
} catch (error) {
|
||||
console.error('Failed to create serve directory hash:', error);
|
||||
// Use a timestamp-based hash as fallback
|
||||
const fallbackHash = Date.now().toString(16).slice(-6);
|
||||
this.serveHash = fallbackHash;
|
||||
console.log('Using fallback hash: ' + fallbackHash);
|
||||
this.serveDirHashSubject.next(fallbackHash);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,12 @@ import * as servertools from './servertools/index.js';
|
||||
|
||||
export { servertools };
|
||||
|
||||
export * from './typedserver.classes.typedserver.js';
|
||||
export * from './classes.typedserver.js';
|
||||
// Type helpers
|
||||
export type Request = plugins.express.Request;
|
||||
export type Response = plugins.express.Response;
|
||||
|
||||
|
||||
// lets export utilityservers
|
||||
import * as utilityservers from './utilityservers/index.js';
|
||||
export { utilityservers };
|
||||
|
8
ts/infohtml/00_commitinfo_data.ts
Normal file
8
ts/infohtml/00_commitinfo_data.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @pushrocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@losslessone_private/lole-infohtml',
|
||||
version: '1.0.39',
|
||||
description: 'html for displaying infos at lossless'
|
||||
}
|
44
ts/infohtml/index.ts
Normal file
44
ts/infohtml/index.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import * as plugins from './infohtml.plugins.js';
|
||||
|
||||
import { simpleInfo } from './template.js';
|
||||
|
||||
export interface IHtmlInfoOptions {
|
||||
text: string;
|
||||
heading?: string;
|
||||
title?: string;
|
||||
sentryMessage?: string;
|
||||
sentryDsn?: string;
|
||||
redirectTo?: string;
|
||||
}
|
||||
|
||||
export class InfoHtml {
|
||||
// STATIC
|
||||
public static async fromSimpleText(textArg: string) {
|
||||
const infohtmlInstance = new InfoHtml({
|
||||
text: textArg,
|
||||
heading: null,
|
||||
});
|
||||
await infohtmlInstance.init();
|
||||
return infohtmlInstance;
|
||||
}
|
||||
|
||||
public static async fromOptions(optionsArg: IHtmlInfoOptions) {
|
||||
const infohtmlInstance = new InfoHtml(optionsArg);
|
||||
await infohtmlInstance.init();
|
||||
return infohtmlInstance;
|
||||
}
|
||||
|
||||
// INSTANCE
|
||||
public options: IHtmlInfoOptions;
|
||||
public smartntmlInstance: plugins.smartntml.Smartntml;
|
||||
public htmlString: string;
|
||||
constructor(optionsArg: IHtmlInfoOptions) {
|
||||
this.options = optionsArg;
|
||||
}
|
||||
|
||||
public async init() {
|
||||
this.smartntmlInstance = new plugins.smartntml.Smartntml();
|
||||
this.htmlString = await simpleInfo(this.smartntmlInstance, this.options);
|
||||
return this.htmlString;
|
||||
}
|
||||
}
|
3
ts/infohtml/infohtml.plugins.ts
Normal file
3
ts/infohtml/infohtml.plugins.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import * as smartntml from '@push.rocks/smartntml';
|
||||
|
||||
export { smartntml };
|
132
ts/infohtml/template.ts
Normal file
132
ts/infohtml/template.ts
Normal 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> / © 2014-${new Date().getFullYear()} Task Venture Capital GmbH
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
return smartntmlInstanceArg.renderTemplateResult(htmlTemplate);
|
||||
};
|
11
ts/paths.ts
Normal file
11
ts/paths.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export const packageDir = plugins.path.join(
|
||||
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
||||
'../'
|
||||
);
|
||||
|
||||
export const injectBundleDir = plugins.path.join(packageDir, './dist_ts_web_inject');
|
||||
export const injectBundlePath = plugins.path.join(injectBundleDir, './bundle.js');
|
||||
|
||||
export const serviceworkerBundleDir = plugins.path.join(packageDir, './dist_ts_web_serviceworker');
|
@ -111,6 +111,7 @@ export class Compressor {
|
||||
switch (method) {
|
||||
case 'gzip':
|
||||
compressionStream = plugins.zlib.createGzip();
|
||||
return compressionStream;
|
||||
case 'br':
|
||||
compressionStream = plugins.zlib.createBrotliCompress({
|
||||
chunkSize: 16 * 1024,
|
||||
@ -118,10 +119,13 @@ export class Compressor {
|
||||
|
||||
},
|
||||
});
|
||||
return compressionStream;
|
||||
case 'deflate':
|
||||
compressionStream = plugins.zlib.createDeflate();
|
||||
return compressionStream;
|
||||
default:
|
||||
compressionStream = plugins.smartstream.createPassThrough();
|
||||
return compressionStream;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { Handler } from './classes.handler.js';
|
||||
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||
|
||||
export class HandlerProxy extends Handler {
|
||||
/**
|
||||
@ -40,11 +40,21 @@ export class HandlerProxy extends Handler {
|
||||
}
|
||||
}
|
||||
|
||||
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}`);
|
||||
// Ensure body exists and convert it to Buffer consistently
|
||||
let responseToSend: Buffer;
|
||||
|
||||
if (proxiedResponse.body !== undefined && proxiedResponse.body !== null) {
|
||||
if (Buffer.isBuffer(proxiedResponse.body)) {
|
||||
responseToSend = proxiedResponse.body;
|
||||
} else if (typeof proxiedResponse.body === 'string') {
|
||||
responseToSend = Buffer.from(proxiedResponse.body);
|
||||
} else {
|
||||
// Handle other types (like objects) by JSON stringifying them
|
||||
responseToSend = Buffer.from(JSON.stringify(proxiedResponse.body));
|
||||
}
|
||||
} else {
|
||||
// Provide a default empty buffer if body is undefined/null
|
||||
responseToSend = Buffer.from('');
|
||||
}
|
||||
|
||||
if (optionsArg && optionsArg.responseModifier) {
|
||||
@ -74,4 +84,4 @@ export class HandlerProxy extends Handler {
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as interfaces from '../interfaces/index.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';
|
||||
@ -134,8 +134,10 @@ export class HandlerStatic extends Handler {
|
||||
res.status(200);
|
||||
if (compressionResult?.compressionMethod) {
|
||||
res.header('Content-Encoding', compressionResult.compressionMethod);
|
||||
res.write(compressionResult.result);
|
||||
} else {
|
||||
res.write(fileBuffer);
|
||||
}
|
||||
res.write(compressionResult.result);
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { Handler } from './classes.handler.js';
|
||||
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||
|
||||
export class HandlerTypedRouter extends Handler {
|
||||
/**
|
||||
|
@ -6,7 +6,13 @@ 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) {
|
||||
|
@ -9,7 +9,7 @@ import { setupRobots } from './tools.robots.js';
|
||||
import { setupManifest } from './tools.manifest.js';
|
||||
import { Sitemap } from './classes.sitemap.js';
|
||||
import { Feed } from './classes.feed.js';
|
||||
import { type IServerOptions } from '../typedserver.classes.typedserver.js';
|
||||
import { type IServerOptions } from '../classes.typedserver.js';
|
||||
export type TServerStatus = 'initiated' | 'running' | 'stopped';
|
||||
|
||||
/**
|
||||
@ -77,6 +77,11 @@ export class Server {
|
||||
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();
|
||||
|
||||
|
@ -63,6 +63,6 @@ export class Sitemap {
|
||||
* adds urls to the current set of urls
|
||||
*/
|
||||
public addUrls(urlsArg: IUrlInfo[]) {
|
||||
this.urls = this.urls.concat(this.urls, urlsArg);
|
||||
this.urls = this.urls.concat(urlsArg);
|
||||
}
|
||||
}
|
||||
}
|
@ -4,3 +4,9 @@ export * from './classes.handler.js';
|
||||
export * from './classes.handlerstatic.js';
|
||||
export * from './classes.handlerproxy.js';
|
||||
export * from './classes.handlertypedrouter.js';
|
||||
export * from './classes.compressor.js';
|
||||
import * as serviceworker from './tools.serviceworker.js';
|
||||
|
||||
export {
|
||||
serviceworker,
|
||||
}
|
||||
|
60
ts/servertools/tools.serviceworker.ts
Normal file
60
ts/servertools/tools.serviceworker.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as paths from '../paths.js';
|
||||
|
||||
import * as interfaces from '../../dist_ts_interfaces/index.js'
|
||||
import { Handler } from './classes.handler.js';
|
||||
import type { TypedServer } from '../classes.typedserver.js';
|
||||
import { HandlerTypedRouter } from './classes.handlertypedrouter.js';
|
||||
|
||||
const swBundleJs: string = plugins.smartfile.fs.toStringSync(
|
||||
plugins.path.join(paths.serviceworkerBundleDir, './serviceworker.bundle.js')
|
||||
);
|
||||
const swBundleJsMap: string = plugins.smartfile.fs.toStringSync(
|
||||
plugins.path.join(paths.serviceworkerBundleDir, './serviceworker.bundle.js.map')
|
||||
);
|
||||
let swVersionInfo: interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response'] =
|
||||
null;
|
||||
const serviceworkerHandler = new Handler(
|
||||
'GET',
|
||||
async (req, res) => {
|
||||
if (req.path === '/serviceworker.bundle.js') {
|
||||
res.status(200);
|
||||
res.set('Content-Type', 'text/javascript');
|
||||
res.write(swBundleJs + '\n' + `/** appSemVer: ${swVersionInfo?.appSemVer || 'not set'} */`);
|
||||
} else if (req.path === '/serviceworker.bundle.js.map') {
|
||||
res.status(200);
|
||||
res.set('Content-Type', 'application/json');
|
||||
res.write(swBundleJsMap);
|
||||
}
|
||||
res.end();
|
||||
}
|
||||
);
|
||||
|
||||
export const addServiceWorkerRoute = (
|
||||
typedserverInstance: TypedServer,
|
||||
swDataFunc: () => interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response']
|
||||
) => {
|
||||
// lets the version info as unique string;
|
||||
swVersionInfo = swDataFunc();
|
||||
|
||||
// the basic stuff
|
||||
typedserverInstance.server.addRoute('/serviceworker.*', serviceworkerHandler);
|
||||
|
||||
// the typed stuff
|
||||
const typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo>(
|
||||
'serviceworker_versionInfo',
|
||||
async (req) => {
|
||||
const versionInfoResponse = swDataFunc();
|
||||
return versionInfoResponse;
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
typedserverInstance.server.addRoute(
|
||||
'/sw-typedrequest',
|
||||
new HandlerTypedRouter(typedrouter)
|
||||
);
|
||||
};
|
@ -1,8 +0,0 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export const packageDir = plugins.path.join(
|
||||
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
||||
'../'
|
||||
);
|
||||
|
||||
export const bundlePath = plugins.path.join(packageDir, './dist_ts_web/bundle.js');
|
51
ts/utilityservers/classes.serviceserver.ts
Normal file
51
ts/utilityservers/classes.serviceserver.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { TypedServer } from '../classes.typedserver.js';
|
||||
import * as servertools from '../servertools/index.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
export interface ILoleServiceServerConstructorOptions {
|
||||
addCustomRoutes?: (serverArg: servertools.Server) => Promise<any>;
|
||||
serviceName: string;
|
||||
serviceVersion: string;
|
||||
serviceDomain: string;
|
||||
port?: number;
|
||||
}
|
||||
|
||||
// the main service server
|
||||
export class UtilityServiceServer {
|
||||
public options: ILoleServiceServerConstructorOptions;
|
||||
public typedServer: TypedServer;
|
||||
|
||||
constructor(optionsArg: ILoleServiceServerConstructorOptions) {
|
||||
this.options = optionsArg;
|
||||
}
|
||||
|
||||
public async start() {
|
||||
console.log('starting lole-serviceserver...')
|
||||
this.typedServer = new TypedServer({
|
||||
cors: true,
|
||||
domain: this.options.serviceDomain,
|
||||
forceSsl: false,
|
||||
port: this.options.port || 3000,
|
||||
robots: true,
|
||||
defaultAnswer: async () => {
|
||||
const InfoHtml = (await import('../infohtml/index.js')).InfoHtml;
|
||||
return (
|
||||
await InfoHtml.fromSimpleText(
|
||||
`${this.options.serviceName} (version ${this.options.serviceVersion})`
|
||||
)
|
||||
).htmlString;
|
||||
},
|
||||
});
|
||||
|
||||
// lets add any custom routes
|
||||
if (this.options.addCustomRoutes) {
|
||||
await this.options.addCustomRoutes(this.typedServer.server);
|
||||
}
|
||||
|
||||
await this.typedServer.start();
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
await this.typedServer.stop();
|
||||
}
|
||||
}
|
137
ts/utilityservers/classes.websiteserver.ts
Normal file
137
ts/utilityservers/classes.websiteserver.ts
Normal file
@ -0,0 +1,137 @@
|
||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||
import { type IServerOptions, TypedServer } from '../classes.typedserver.js';
|
||||
import type { Request, Response } from '../index.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as servertools from '../servertools/index.js';
|
||||
|
||||
export interface IUtilityWebsiteServerConstructorOptions {
|
||||
addCustomRoutes?: (serverArg: servertools.Server) => Promise<any>;
|
||||
appSemVer?: string;
|
||||
domain: string;
|
||||
serveDir: string;
|
||||
feedMetadata: IServerOptions['feedMetadata'];
|
||||
}
|
||||
|
||||
/**
|
||||
* the utility website server implements a best practice server for websites
|
||||
* It supports:
|
||||
* * live reload
|
||||
* * compression
|
||||
* * serviceworker
|
||||
* * pwa manifest
|
||||
*/
|
||||
export class UtilityWebsiteServer {
|
||||
public options: IUtilityWebsiteServerConstructorOptions;
|
||||
public typedserver: TypedServer;
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
constructor(optionsArg: IUtilityWebsiteServerConstructorOptions) {
|
||||
this.options = optionsArg;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public async start(portArg = 3000) {
|
||||
this.typedserver = new TypedServer({
|
||||
cors: true,
|
||||
injectReload: true,
|
||||
watch: true,
|
||||
serveDir: this.options.serveDir,
|
||||
enableCompression: true,
|
||||
preferredCompressionMethod: 'gzip',
|
||||
domain: this.options.domain,
|
||||
forceSsl: false,
|
||||
manifest: {
|
||||
name: this.options.domain,
|
||||
short_name: this.options.domain,
|
||||
start_url: '/',
|
||||
display_override: ['window-controls-overlay'],
|
||||
lang: 'en',
|
||||
background_color: '#000000',
|
||||
scope: '/',
|
||||
},
|
||||
port: portArg,
|
||||
|
||||
// features
|
||||
robots: true,
|
||||
sitemap: true,
|
||||
});
|
||||
|
||||
let lswData: interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response'] =
|
||||
{
|
||||
appHash: 'xxxxxx',
|
||||
appSemVer: this.options.appSemVer || 'x.x.x',
|
||||
};
|
||||
|
||||
// -> /lsw* - anything regarding serviceworker
|
||||
servertools.serviceworker.addServiceWorkerRoute(this.typedserver, () => {
|
||||
return lswData;
|
||||
});
|
||||
|
||||
// lets add ads.txt
|
||||
this.typedserver.server.addRoute(
|
||||
'/ads.txt',
|
||||
new servertools.Handler('GET', async (req, res) => {
|
||||
res.type('txt/plain');
|
||||
const adsTxt =
|
||||
['google.com, pub-4104137977476459, DIRECT, f08c47fec0942fa0'].join('\n') + '\n';
|
||||
res.write(adsTxt);
|
||||
res.end();
|
||||
})
|
||||
);
|
||||
|
||||
this.typedserver.server.addRoute(
|
||||
'/assetbroker/manifest/:manifestAsset',
|
||||
new servertools.Handler('GET', async (req, res) => {
|
||||
let manifestAssetName = req.params.manifestAsset;
|
||||
if (manifestAssetName === 'favicon.png') {
|
||||
manifestAssetName = `favicon_${this.options.domain
|
||||
.replace('.', '')
|
||||
.replace('losslesscom', 'lossless')}@2x_transparent.png`;
|
||||
}
|
||||
const fullOriginAssetUrl = `https://assetbroker.lossless.one/brandfiles/00general/${manifestAssetName}`;
|
||||
console.log(`Getting ${manifestAssetName} from ${fullOriginAssetUrl}`);
|
||||
const dataBuffer: Buffer = (await plugins.smartrequest.getBinary(fullOriginAssetUrl)).body;
|
||||
res.type('.png');
|
||||
res.write(dataBuffer);
|
||||
res.end();
|
||||
})
|
||||
);
|
||||
|
||||
// lets add any custom routes
|
||||
if (this.options.addCustomRoutes) {
|
||||
await this.options.addCustomRoutes(this.typedserver.server);
|
||||
}
|
||||
|
||||
// -> /* - serve the files
|
||||
this.typedserver.serveDirHashSubject.subscribe((appHash: string) => {
|
||||
lswData = {
|
||||
appHash,
|
||||
appSemVer: '1.0.0',
|
||||
};
|
||||
});
|
||||
|
||||
// lets setup the typedrouter chain
|
||||
this.typedserver.typedrouter.addTypedRouter(this.typedrouter);
|
||||
|
||||
// lets start everything
|
||||
console.log('routes are all set. Startin up now!');
|
||||
await this.typedserver.start();
|
||||
console.log('typedserver started!');
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
await this.typedserver.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* allows you to hanlde requests from other server instances without the need to listen for yourself
|
||||
* note smartexpress allows you start the instance wuith passing >>false<< as second parameter to .start();
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
public async handleRequest(req: Request, res: Response) {
|
||||
await this.typedserver.server.handleReqRes(req, res);
|
||||
}
|
||||
}
|
2
ts/utilityservers/index.ts
Normal file
2
ts/utilityservers/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './classes.serviceserver.js';
|
||||
export * from './classes.websiteserver.js';
|
8
ts_edgeworker/00_commitinfo_data.ts
Normal file
8
ts_edgeworker/00_commitinfo_data.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @pushrocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: 'cloudflare-workers',
|
||||
version: '1.0.192',
|
||||
description: 'cloudflare-workers'
|
||||
}
|
83
ts_edgeworker/analytics/analyzer.ts
Normal file
83
ts_edgeworker/analytics/analyzer.ts
Normal file
@ -0,0 +1,83 @@
|
||||
|
||||
import type { EdgeWorker } from '../classes.edgeworker.js';
|
||||
import type { WorkerEvent } from '../classes.workerevent.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import { SmartlogDestination } from './smartlog.js';
|
||||
|
||||
export interface IAnalyticsData {
|
||||
requestAgent: string;
|
||||
requestUrl: string;
|
||||
requestMethod: string;
|
||||
requestStartTime: number;
|
||||
responseStatus: number;
|
||||
responseEndTime: number;
|
||||
}
|
||||
|
||||
export class Analyzer {
|
||||
cworkerEventRef: WorkerEvent;
|
||||
|
||||
public data: IAnalyticsData = {
|
||||
requestAgent: 'unknown',
|
||||
requestMethod: 'unknown',
|
||||
requestUrl: 'unknown',
|
||||
requestStartTime: 0,
|
||||
responseStatus: 0,
|
||||
responseEndTime: 0,
|
||||
};
|
||||
|
||||
public finishedDeferred = plugins.smartpromise.defer();
|
||||
|
||||
constructor(cworkerEventRefArg: WorkerEvent) {
|
||||
this.cworkerEventRef = cworkerEventRefArg;
|
||||
this.smartlog.addLogDestination(new SmartlogDestination(this.cworkerEventRef.options.edgeWorkerRef));
|
||||
}
|
||||
public smartlog = new plugins.smartlog.Smartlog({
|
||||
logContext: {
|
||||
environment: 'production',
|
||||
runtime: "cloudflare_workers",
|
||||
zone: 'servezone',
|
||||
company: 'Lossless GmbH',
|
||||
companyunit: 'Lossless Cloud',
|
||||
containerName: 'cloudflare_workers'
|
||||
}
|
||||
});
|
||||
|
||||
public setRequestData (optionsArg: {
|
||||
requestAgent: string;
|
||||
requestUrl: string;
|
||||
requestMethod: string;
|
||||
}) {
|
||||
this.data = {
|
||||
...this.data,
|
||||
...{
|
||||
requestAgent: optionsArg.requestAgent,
|
||||
requestUrl: optionsArg.requestUrl,
|
||||
requestMethod: optionsArg.requestMethod,
|
||||
requestStartTime: Date.now()
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
public setResponseData(optionsArg: {
|
||||
responseStatus: number,
|
||||
responseEndTime: number,
|
||||
}) {
|
||||
this.data = {
|
||||
...this.data,
|
||||
...{
|
||||
responseStatus: optionsArg.responseStatus,
|
||||
responseEndTime: optionsArg.responseEndTime
|
||||
}
|
||||
};
|
||||
this.sendLogs();
|
||||
}
|
||||
|
||||
public async sendLogs() {
|
||||
await this.smartlog.log('info', `
|
||||
Got a ${this.data.requestMethod} request from ${this.data.requestAgent} to
|
||||
${this.data.requestUrl}
|
||||
that took ${this.data.responseEndTime - this.data.requestStartTime}ms to resolve with status ${this.data.responseStatus}.`, this.data);
|
||||
this.finishedDeferred.resolve();
|
||||
}
|
||||
}
|
26
ts_edgeworker/analytics/smartlog.ts
Normal file
26
ts_edgeworker/analytics/smartlog.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import * as smartlogInterfaces from '@push.rocks/smartlog-interfaces';
|
||||
import type { EdgeWorker } from '../classes.edgeworker.js';
|
||||
|
||||
export class SmartlogDestination implements smartlogInterfaces.ILogDestination {
|
||||
public edgeWorkerRef: EdgeWorker;
|
||||
|
||||
constructor(edgeworkerRefArg: EdgeWorker) {
|
||||
this.edgeWorkerRef = edgeworkerRefArg;
|
||||
}
|
||||
|
||||
public async handleLog(logPackageArg: smartlogInterfaces.ILogPackage) {
|
||||
if (this.edgeWorkerRef.options.smartlogConfig) {
|
||||
const requestBody: smartlogInterfaces.ILogPackageAuthenticated = {
|
||||
auth: this.edgeWorkerRef.options.smartlogConfig.token,
|
||||
logPackage: logPackageArg,
|
||||
};
|
||||
await fetch(this.edgeWorkerRef.options.smartlogConfig.endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
67
ts_edgeworker/classes.domainrouter.ts
Normal file
67
ts_edgeworker/classes.domainrouter.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
import * as plugins from './plugins.js';
|
||||
import { WorkerEvent } from './classes.workerevent.js';
|
||||
|
||||
import * as domainInstructions from './domaininstructions/index.js';
|
||||
|
||||
export class DomainRouter {
|
||||
private smartmatches: plugins.smartmatch.SmartMatch[] = [];
|
||||
|
||||
constructor() {
|
||||
for (const key of Object.keys(domainInstructions.instructionObject)) {
|
||||
this.smartmatches.push(new plugins.smartmatch.SmartMatch(key));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param cworkerevent
|
||||
*/
|
||||
public routeToResponder(cworkerevent: WorkerEvent) {
|
||||
const match = this.smartmatches.find(smartmatchArg => {
|
||||
return smartmatchArg.match(cworkerevent.request.url);
|
||||
});
|
||||
cworkerevent.responderInstruction = match
|
||||
? domainInstructions.instructionObject[match.wildcard]
|
||||
: {
|
||||
type: 'cache'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* rendertronRouter
|
||||
*/
|
||||
public checkWetherReRouteToRendertron(cworkerevent: WorkerEvent) {
|
||||
let needsRendertron = false;
|
||||
for (const botAgentIdentifier of domainInstructions.botUserAgents) {
|
||||
if (needsRendertron) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
cworkerevent.request.headers.get('user-agent') &&
|
||||
cworkerevent.request.headers.get('user-agent').toLowerCase().includes(botAgentIdentifier.toLowerCase()) &&
|
||||
!cworkerevent.request.url.includes('lossless.one')
|
||||
) {
|
||||
needsRendertron = true;
|
||||
}
|
||||
}
|
||||
if (needsRendertron) {
|
||||
cworkerevent.routedThroughRendertron = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* check wether this is a preflight request that should be handled
|
||||
*/
|
||||
public checkWetherIsPreflight (cworkerevent: WorkerEvent) {
|
||||
if (
|
||||
cworkerevent.request.method === 'OPTIONS' &&
|
||||
cworkerevent.request.headers.get('Origin') !== null &&
|
||||
cworkerevent.request.headers.get('Access-Control-Request-Method') !== null &&
|
||||
cworkerevent.request.headers.get('Access-Control-Request-Headers') !== null
|
||||
) {
|
||||
cworkerevent.isPreflight = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
73
ts_edgeworker/classes.edgeworker.ts
Normal file
73
ts_edgeworker/classes.edgeworker.ts
Normal file
@ -0,0 +1,73 @@
|
||||
// imports
|
||||
import { WorkerEvent } from './classes.workerevent.js';
|
||||
import { DomainRouter } from './classes.domainrouter.js';
|
||||
import * as plugins from './plugins.js';
|
||||
import * as responders from './responders/index.js';
|
||||
|
||||
export interface IEdgeWorkerOptions {
|
||||
smartlogConfig?: {
|
||||
endpoint: string;
|
||||
token: string;
|
||||
}
|
||||
}
|
||||
|
||||
export class EdgeWorker {
|
||||
public options: IEdgeWorkerOptions;
|
||||
domainRouter: DomainRouter;
|
||||
|
||||
constructor(optionsArg: IEdgeWorkerOptions = {}) {
|
||||
this.options = optionsArg;
|
||||
this.domainRouter = new DomainRouter();
|
||||
addEventListener('fetch', this.fetchFunction as any);
|
||||
}
|
||||
|
||||
public async fetchFunction (eventArg: plugins.cloudflareTypes.FetchEvent) {
|
||||
if (new URL(eventArg.request.url).pathname.startsWith('/socket.io')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cworkerEvent = new WorkerEvent({
|
||||
edgeWorkerRef: this,
|
||||
event: eventArg,
|
||||
passThroughOnException: true
|
||||
});
|
||||
|
||||
// lets answer basic reuest things
|
||||
responders.timeoutResponder(cworkerEvent);
|
||||
cworkerEvent.hasResponse ? null : await responders.urlFormattingResponder(cworkerEvent);
|
||||
|
||||
// lets route the domain
|
||||
this.domainRouter.routeToResponder(cworkerEvent);
|
||||
this.domainRouter.checkWetherReRouteToRendertron(cworkerEvent);
|
||||
this.domainRouter.checkWetherIsPreflight(cworkerEvent);
|
||||
|
||||
// guardresponder
|
||||
cworkerEvent.hasResponse ? null : await responders.guardResponder(cworkerEvent);
|
||||
|
||||
// lets process all requests that need rendertron
|
||||
cworkerEvent.hasResponse ? null : await responders.rendertronResponder(cworkerEvent);
|
||||
|
||||
// lets process all requests that are preflight requests
|
||||
cworkerEvent.hasResponse ? null : await responders.preflightResponder(cworkerEvent);
|
||||
|
||||
switch (cworkerEvent.responderInstruction.type) {
|
||||
case 'cache':
|
||||
cworkerEvent.hasResponse ? null : await responders.cacheResponder(cworkerEvent);
|
||||
break;
|
||||
case 'origin':
|
||||
cworkerEvent.hasResponse ? null : await responders.originResponder(cworkerEvent);
|
||||
break;
|
||||
case 'redirect':
|
||||
cworkerEvent.hasResponse ? null : await responders.adsTxtResponder(cworkerEvent);
|
||||
break;
|
||||
case 'static':
|
||||
cworkerEvent.hasResponse ? null : await responders.staticResponder(cworkerEvent);
|
||||
break;
|
||||
case 'ads.txt':
|
||||
cworkerEvent.hasResponse ? null : await responders.adsTxtResponder(cworkerEvent);
|
||||
break;
|
||||
}
|
||||
// cworkerEvent.hasResponse ? null : await responders.kvResponder(cworkerEvent);
|
||||
cworkerEvent.hasResponse ? null : await responders.errorResponder(cworkerEvent);
|
||||
};
|
||||
}
|
37
ts_edgeworker/classes.kvhandler.ts
Normal file
37
ts_edgeworker/classes.kvhandler.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
declare var lokv: plugins.cloudflareTypes.KVNamespace;
|
||||
|
||||
/**
|
||||
* an abstraction for the workerd KV store
|
||||
*/
|
||||
export class KVHandler {
|
||||
private getSafeIdentifier(urlString: string) {
|
||||
return encodeURI(urlString);
|
||||
}
|
||||
|
||||
async getFromKv(keyIdentifier: string) {
|
||||
const key = this.getSafeIdentifier(keyIdentifier);
|
||||
const valueString = await lokv.get(key);
|
||||
return valueString;
|
||||
}
|
||||
|
||||
async putInKv(keyIdentifier: string, valueForStorage: string) {
|
||||
const key = this.getSafeIdentifier(keyIdentifier);
|
||||
const value = valueForStorage;
|
||||
await lokv.put(key, value);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* deletes a key/value from the cache
|
||||
* @param keyIdentifier
|
||||
*/
|
||||
async deleteInKv(keyIdentifier: string) {
|
||||
const cacheKey = this.getSafeIdentifier(keyIdentifier);
|
||||
await lokv.delete(cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
export const kvHandlerInstance = new KVHandler();
|
46
ts_edgeworker/classes.responsekv.ts
Normal file
46
ts_edgeworker/classes.responsekv.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { kvHandlerInstance } from './classes.kvhandler.js';
|
||||
|
||||
declare var lokv: plugins.cloudflareTypes.KVNamespace;
|
||||
|
||||
interface IKVResponseObject {
|
||||
headers: { [key: string]: string };
|
||||
version: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export class ResponseKv {
|
||||
public async storeResponse(urlIdentifier: string, responseArg: any) {
|
||||
const headers: { [key: string]: string } = {};
|
||||
for (const kv of responseArg.headers.entries()) {
|
||||
headers[kv[0]] = kv[1];
|
||||
}
|
||||
const kvResponseForStorage: IKVResponseObject = {
|
||||
headers,
|
||||
version: '1.0.0',
|
||||
body: await responseArg.text()
|
||||
};
|
||||
await kvHandlerInstance.putInKv(urlIdentifier, JSON.stringify(kvResponseForStorage));
|
||||
}
|
||||
|
||||
public async getResponse(urlIdentifier: string): Promise<Response> {
|
||||
const kvValue = await kvHandlerInstance.getFromKv(urlIdentifier);
|
||||
if (kvValue) {
|
||||
let kvResponse: IKVResponseObject;
|
||||
try {
|
||||
kvResponse = JSON.parse(kvValue);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return null;
|
||||
}
|
||||
const headers = new Headers();
|
||||
for (const key of Object.keys(kvResponse.headers)) {
|
||||
headers.append(key, kvResponse.headers[key]);
|
||||
}
|
||||
headers.append('SERVEZONE_ROUTE', 'CLOUDFLARE_EDGE_LOKV');
|
||||
return new Response(kvResponse.body, { headers: headers });
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
102
ts_edgeworker/classes.workerevent.ts
Normal file
102
ts_edgeworker/classes.workerevent.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
import * as plugins from './plugins.js';
|
||||
import * as helpers from './helpers/index.js';
|
||||
import { DomainRouter } from './classes.domainrouter.js';
|
||||
import { Analyzer } from './analytics/analyzer.js';
|
||||
import type { EdgeWorker } from './classes.edgeworker.js';
|
||||
|
||||
export interface ICworkerEventOptions {
|
||||
event: plugins.cloudflareTypes.FetchEvent
|
||||
edgeWorkerRef: EdgeWorker;
|
||||
passThroughOnException?: boolean;
|
||||
}
|
||||
|
||||
|
||||
export class WorkerEvent {
|
||||
public options: ICworkerEventOptions;
|
||||
|
||||
public analyzer: Analyzer;
|
||||
private responseDeferred: plugins.smartpromise.Deferred<any>;
|
||||
private waitUntilDeferred: plugins.smartpromise.Deferred<any>;
|
||||
|
||||
private response: Response = null;
|
||||
private waitList = [];
|
||||
|
||||
// routing settings
|
||||
public responderInstruction: interfaces.IResponderInstruction;
|
||||
public routedThroughRendertron: boolean = false;
|
||||
public isPreflight: boolean = false;
|
||||
|
||||
public request: plugins.cloudflareTypes.Request;
|
||||
|
||||
public parsedUrl: URL;
|
||||
|
||||
constructor(optionsArg: ICworkerEventOptions) {
|
||||
this.options = optionsArg;
|
||||
|
||||
// lets create an Analyzer for this request
|
||||
this.analyzer = new Analyzer(this);
|
||||
|
||||
// lets make sure we always answer
|
||||
this.options.passThroughOnException ? this.options.event.passThroughOnException() : null;
|
||||
|
||||
// lets set up some better asnyc behaviour
|
||||
this.waitUntilDeferred = plugins.smartpromise.defer();
|
||||
this.responseDeferred = plugins.smartpromise.defer();
|
||||
this.addToWaitList(this.analyzer.finishedDeferred.promise);
|
||||
|
||||
// lets entangle the event with this class instance
|
||||
this.request = this.options.event.request;
|
||||
|
||||
// lets start with analytics
|
||||
this.analyzer.setRequestData({
|
||||
requestAgent: this.request.headers.get('user-agent'),
|
||||
requestMethod: this.request.method,
|
||||
requestUrl: this.request.url
|
||||
});
|
||||
|
||||
|
||||
this.options.event.respondWith(this.responseDeferred.promise);
|
||||
this.options.event.waitUntil(this.waitUntilDeferred.promise);
|
||||
|
||||
// lets parse the url
|
||||
this.parsedUrl = new URL(this.request.url);
|
||||
|
||||
// lets check the waitlist
|
||||
this.checkWaitList();
|
||||
console.log(`Got request for ${this.request.url}`);
|
||||
}
|
||||
|
||||
get hasResponse () {
|
||||
let returnValue: boolean;
|
||||
this.response ? returnValue = true : returnValue = false;
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
public addToWaitList(promiseArg: Promise<any>) {
|
||||
this.waitList.push(promiseArg);
|
||||
}
|
||||
|
||||
private async checkWaitList() {
|
||||
await this.responseDeferred.promise;
|
||||
const currentWaitList = this.waitList;
|
||||
this.waitList = [];
|
||||
await Promise.all(currentWaitList);
|
||||
if (this.waitList.length > 0) {
|
||||
this.checkWaitList();
|
||||
} else {
|
||||
this.waitUntilDeferred.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
public setResponse (responseArg: Response) {
|
||||
this.response = responseArg;
|
||||
this.responseDeferred.resolve(responseArg);
|
||||
this.analyzer.setResponseData({
|
||||
responseStatus: this.response.status,
|
||||
responseEndTime: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
36
ts_edgeworker/domaininstructions/botuseragents.ts
Normal file
36
ts_edgeworker/domaininstructions/botuseragents.ts
Normal file
@ -0,0 +1,36 @@
|
||||
export const botUserAgents = [
|
||||
// Baidu
|
||||
'baiduspider',
|
||||
'embedly',
|
||||
|
||||
// Facebook
|
||||
'facebookexternalhit',
|
||||
|
||||
// Ghost
|
||||
'Ghost',
|
||||
|
||||
// Microsoft
|
||||
'bingbot',
|
||||
'BingPreview',
|
||||
'linkedinbot',
|
||||
'MissinglettrBot',
|
||||
'msnbot',
|
||||
'outbrain',
|
||||
'pinterest',
|
||||
'quora link preview',
|
||||
'rogerbot',
|
||||
'showyoubot',
|
||||
'slackbot',
|
||||
'TelegramBot',
|
||||
|
||||
// Twitter
|
||||
'twitterbot',
|
||||
'vkShare',
|
||||
'W3C_Validator',
|
||||
|
||||
// WhatsApp
|
||||
'whatsapp',
|
||||
|
||||
// woorank
|
||||
'woorank'
|
||||
];
|
7
ts_edgeworker/domaininstructions/domaininstructions.ts
Normal file
7
ts_edgeworker/domaininstructions/domaininstructions.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
|
||||
export const instructionObject: { [key: string]: interfaces.IResponderInstruction } = {
|
||||
'*/ads.txt': {
|
||||
type: 'ads.txt',
|
||||
}
|
||||
};
|
2
ts_edgeworker/domaininstructions/index.ts
Normal file
2
ts_edgeworker/domaininstructions/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './botuseragents.js';
|
||||
export * from './domaininstructions.js';
|
9
ts_edgeworker/helpers/checks.ts
Normal file
9
ts_edgeworker/helpers/checks.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
declare var lokv: plugins.cloudflareTypes.KVNamespace;
|
||||
export const checkLokv = () => {
|
||||
if (!lokv) {
|
||||
throw new Error('lokv not defined!');
|
||||
} else {
|
||||
console.log('lokv present!');
|
||||
}
|
||||
};
|
1
ts_edgeworker/helpers/index.ts
Normal file
1
ts_edgeworker/helpers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './checks.js';
|
1
ts_edgeworker/index.ts
Normal file
1
ts_edgeworker/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './classes.edgeworker.js';
|
9
ts_edgeworker/interfaces/custom.ts
Normal file
9
ts_edgeworker/interfaces/custom.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { WorkerEvent } from "../classes.workerevent.js";
|
||||
|
||||
export interface IResponderInstruction {
|
||||
type: 'origin' | 'cache' | 'static' | 'redirect' | 'ads.txt';
|
||||
cacheClientSideForMin?: number;
|
||||
redirectUrl?: string;
|
||||
}
|
||||
|
||||
export type TRequestResponser = (workerEventArg: WorkerEvent) => Promise<void>;
|
1
ts_edgeworker/interfaces/index.ts
Normal file
1
ts_edgeworker/interfaces/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './custom.js';
|
21
ts_edgeworker/plugins.ts
Normal file
21
ts_edgeworker/plugins.ts
Normal file
@ -0,0 +1,21 @@
|
||||
// @pushrocks scope
|
||||
import * as smartdelay from '@push.rocks/smartdelay';
|
||||
import * as smartlog from '@push.rocks/smartlog';
|
||||
import * as smartlogInterfaces from '@push.rocks/smartlog-interfaces';
|
||||
import * as smartmatch from '@push.rocks/smartmatch';
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
|
||||
export {
|
||||
smartdelay,
|
||||
smartlog,
|
||||
smartlogInterfaces,
|
||||
smartmatch,
|
||||
smartpromise
|
||||
};
|
||||
|
||||
// cloudflarea
|
||||
import * as cloudflareTypes from '@cloudflare/workers-types';
|
||||
|
||||
export {
|
||||
cloudflareTypes
|
||||
}
|
14
ts_edgeworker/responders/adstxt.responder.ts
Normal file
14
ts_edgeworker/responders/adstxt.responder.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import { WorkerEvent } from '../classes.workerevent.js';
|
||||
|
||||
export const adsTxtResponder: interfaces.TRequestResponser = async (cWorkerEventArg: WorkerEvent) => {
|
||||
if (cWorkerEventArg.responderInstruction.type === 'ads.txt') {
|
||||
const response = new Response('google.com, pub-4104137977476459, DIRECT, f08c47fec0942fa0\n', {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain; charset=utf-8'
|
||||
}
|
||||
})
|
||||
cWorkerEventArg.setResponse(response);
|
||||
}
|
||||
|
||||
};
|
92
ts_edgeworker/responders/cache.responder.ts
Normal file
92
ts_edgeworker/responders/cache.responder.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import { WorkerEvent } from '../classes.workerevent.js';
|
||||
import { kvHandlerInstance } from '../classes.kvhandler.js';
|
||||
|
||||
declare const fetch: plugins.cloudflareTypes.Fetcher['fetch'];
|
||||
|
||||
declare var caches: any;
|
||||
export const cacheResponder: interfaces.TRequestResponser = async (cworkerEventArg: WorkerEvent) => {
|
||||
const host = cworkerEventArg.request.headers.get('Host');
|
||||
const appHashKey = `${host.toLowerCase()}_appHash`;
|
||||
const appHash = await kvHandlerInstance.getFromKv(appHashKey);
|
||||
|
||||
const cache = caches.default;
|
||||
let response: Response = await cache.match(cworkerEventArg.request);
|
||||
|
||||
if (
|
||||
response &&
|
||||
response.headers.get('appHash') &&
|
||||
response.headers.get('appHash') !== appHash
|
||||
) {
|
||||
response = null;
|
||||
}
|
||||
|
||||
if (response) {
|
||||
cworkerEventArg.setResponse(response);
|
||||
} else {
|
||||
response = await handleNewRequest(cworkerEventArg.request);
|
||||
if (response) {
|
||||
cworkerEventArg.addToWaitList(new Promise<void>(async (resolve, reject) => {
|
||||
const newAppHash = response.headers.get('appHash');
|
||||
if (newAppHash) {
|
||||
await kvHandlerInstance.putInKv(appHashKey, newAppHash);
|
||||
}
|
||||
resolve();
|
||||
}));
|
||||
cworkerEventArg.addToWaitList(buildCacheResponse(cache, cworkerEventArg.request, response));
|
||||
cworkerEventArg.setResponse(response);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Request} originalRequest
|
||||
*/
|
||||
const handleNewRequest = async (originalRequest: plugins.cloudflareTypes.Request): Promise<Response> => {
|
||||
console.log('answering from origin');
|
||||
const originResponse: any = await fetch(
|
||||
originalRequest
|
||||
);
|
||||
|
||||
// lets capture status
|
||||
if (originResponse.status > 399) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const responseClientPassThroughStream = new TransformStream();
|
||||
originResponse.body.pipeTo(responseClientPassThroughStream.writable);
|
||||
|
||||
// build response for client
|
||||
const clientHeaders = new Headers();
|
||||
for (const kv of originResponse.headers.entries()) {
|
||||
clientHeaders.append(kv[0], kv[1]);
|
||||
}
|
||||
clientHeaders.append('SERVEZONE_ROUTE', 'LOSSLESS_EDGE_ORIGIN_INITIAL');
|
||||
const responseForClient = new Response(responseClientPassThroughStream.readable, {
|
||||
...originResponse,
|
||||
headers: clientHeaders
|
||||
});
|
||||
|
||||
// lets return the responses
|
||||
return responseForClient;
|
||||
};
|
||||
|
||||
const buildCacheResponse = async (cache, matchRequest: plugins.cloudflareTypes.Request, originResponse: any) => {
|
||||
const cacheHeaders = new Headers();
|
||||
for (const kv of originResponse.headers.entries()) {
|
||||
cacheHeaders.append(kv[0], kv[1]);
|
||||
}
|
||||
cacheHeaders.delete('SERVEZONE_ROUTE');
|
||||
cacheHeaders.append('SERVEZONE_ROUTE', 'LOSSLESS_EDGE_CACHE');
|
||||
cacheHeaders.delete('Cache-Control');
|
||||
cacheHeaders.append('Cache-Control', 'public, max-age=60');
|
||||
cacheHeaders.delete('Expires');
|
||||
cacheHeaders.append('Expires', new Date(Date.now() + 60 * 1000).toUTCString());
|
||||
|
||||
const responseForCache = new Response(await originResponse.clone().body, {
|
||||
...originResponse,
|
||||
headers: cacheHeaders
|
||||
});
|
||||
await cache.put(matchRequest, responseForCache);
|
||||
};
|
6
ts_edgeworker/responders/error.responder.ts
Normal file
6
ts_edgeworker/responders/error.responder.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import { WorkerEvent } from '../classes.workerevent.js';
|
||||
export const errorResponder: interfaces.TRequestResponser = async (cWorkerEvent: WorkerEvent) => {
|
||||
const errorResponse = await fetch('https://nullresolve.lossless.one/status/firewall');
|
||||
cWorkerEvent.setResponse(errorResponse);
|
||||
};
|
8
ts_edgeworker/responders/guard.responder.ts
Normal file
8
ts_edgeworker/responders/guard.responder.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import { WorkerEvent } from '../classes.workerevent.js';
|
||||
export const guardResponder: interfaces.TRequestResponser = async (cWorkerEvent: WorkerEvent) => {
|
||||
if (cWorkerEvent.parsedUrl.pathname.endsWith('.map')) {
|
||||
const errorResponse = await fetch('https://nullresolve.lossless.one/status/firewall');
|
||||
cWorkerEvent.setResponse(errorResponse);
|
||||
}
|
||||
};
|
12
ts_edgeworker/responders/index.ts
Normal file
12
ts_edgeworker/responders/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export * from './adstxt.responder.js';
|
||||
export * from './cache.responder.js';
|
||||
export * from './urlformatting.responder.js';
|
||||
export * from './error.responder.js';
|
||||
export * from './guard.responder.js';
|
||||
export * from './kv.responder.js';
|
||||
export * from './origin.responder.js';
|
||||
export * from './preflight.responder.js';
|
||||
export * from './redirect.reponder.js';
|
||||
export * from './rendertron.responder.js';
|
||||
export * from './static.responder.js';
|
||||
export * from './timeout.responder.js';
|
34
ts_edgeworker/responders/kv.responder.ts
Normal file
34
ts_edgeworker/responders/kv.responder.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import { WorkerEvent } from '../classes.workerevent.js';
|
||||
import { ResponseKv } from '../classes.responsekv.js';
|
||||
|
||||
declare const fetch: plugins.cloudflareTypes.Fetcher['fetch'];
|
||||
|
||||
export const kvResponder: interfaces.TRequestResponser = async (cworkerEventArg: WorkerEvent) => {
|
||||
const responseKvInstance = new ResponseKv();
|
||||
let response = await responseKvInstance.getResponse(cworkerEventArg.request.url);
|
||||
if (response) {
|
||||
console.log('Got response from KV');
|
||||
} else {
|
||||
response = await handleNewRequest(cworkerEventArg.request, responseKvInstance);
|
||||
}
|
||||
cworkerEventArg.setResponse(response);
|
||||
};
|
||||
|
||||
const handleNewRequest = async (request: plugins.cloudflareTypes.Request, responseKvInstance: ResponseKv) => {
|
||||
const originResponse: any = await fetch(request);
|
||||
// build response for cache
|
||||
const cacheHeaders = new Headers();
|
||||
for (const kv of originResponse.headers.entries()) {
|
||||
cacheHeaders.append(kv[0], kv[1]);
|
||||
}
|
||||
cacheHeaders.append('SERVEZONE_ROUTE', 'LOSSLESS_EDGE_KVRESPONSE');
|
||||
cacheHeaders.append('Cache-Control', 'max-age=600');
|
||||
const responseForKV = new Response(await originResponse.body, {
|
||||
...originResponse,
|
||||
headers: cacheHeaders
|
||||
});
|
||||
await responseKvInstance.storeResponse(request.url, responseForKV.clone());
|
||||
return responseForKV.clone();
|
||||
};
|
31
ts_edgeworker/responders/origin.responder.ts
Normal file
31
ts_edgeworker/responders/origin.responder.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import { WorkerEvent } from '../classes.workerevent.js';
|
||||
|
||||
declare const fetch: plugins.cloudflareTypes.Fetcher['fetch'];
|
||||
|
||||
export const originResponder: interfaces.TRequestResponser = async (eventArg: WorkerEvent) => {
|
||||
const originResponse: any = await fetch(eventArg.request);
|
||||
// lets capture status
|
||||
if (originResponse.status > 399) {
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
for (const kv of originResponse.headers.entries()) {
|
||||
headers.append(kv[0], kv[1]);
|
||||
}
|
||||
headers.append('SERVEZONE_ROUTE', 'LOSSLESS_EDGE_FASTORIGIN');
|
||||
|
||||
const responsePassThroughStream = new TransformStream();
|
||||
originResponse.body.pipeTo(responsePassThroughStream.writable);
|
||||
|
||||
// response
|
||||
|
||||
const responseForClient = new Response(responsePassThroughStream.readable, {
|
||||
...originResponse,
|
||||
headers,
|
||||
});
|
||||
|
||||
eventArg.setResponse(responseForClient);
|
||||
};
|
18
ts_edgeworker/responders/preflight.responder.ts
Normal file
18
ts_edgeworker/responders/preflight.responder.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import { WorkerEvent } from '../classes.workerevent.js';
|
||||
|
||||
export const preflightResponder: interfaces.TRequestResponser = async (eventArg: WorkerEvent) => {
|
||||
if (eventArg.isPreflight) {
|
||||
const corsHeaders = new Headers();
|
||||
corsHeaders.append('SERVEZONE_ROUTE', 'LOSSLESS_EDGE_PREFLIGHT');
|
||||
corsHeaders.append('Access-Control-Allow-Origin', '*');
|
||||
corsHeaders.append('Access-Control-Allow-Methods', '*');
|
||||
corsHeaders.append('Access-Control-Allow-Headers', '*');
|
||||
|
||||
eventArg.setResponse(
|
||||
new Response(null, {
|
||||
headers: corsHeaders,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
9
ts_edgeworker/responders/redirect.reponder.ts
Normal file
9
ts_edgeworker/responders/redirect.reponder.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import { WorkerEvent } from '../classes.workerevent.js';
|
||||
|
||||
export const redirectResponder: interfaces.TRequestResponser = async (cWorkerEventArg: WorkerEvent) => {
|
||||
if (cWorkerEventArg.responderInstruction.type === 'redirect') {
|
||||
cWorkerEventArg.setResponse(Response.redirect(cWorkerEventArg.responderInstruction.redirectUrl, 302));
|
||||
}
|
||||
|
||||
};
|
22
ts_edgeworker/responders/rendertron.responder.ts
Normal file
22
ts_edgeworker/responders/rendertron.responder.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { WorkerEvent } from '../classes.workerevent.js';
|
||||
|
||||
export const rendertronResponder = async (cworkerevent: WorkerEvent) => {
|
||||
if (cworkerevent.routedThroughRendertron) {
|
||||
const oldHeaders: any = cworkerevent.request.headers;
|
||||
const rendertronHeaders = new Headers();
|
||||
for (const kv of oldHeaders.entries()) {
|
||||
const headerName = kv[0];
|
||||
const headerValue = headerName === 'user-agent' ? 'Lossless Rendertron' : kv[1];
|
||||
rendertronHeaders.append(headerName, headerValue);
|
||||
}
|
||||
const rendertronRequest = new Request(
|
||||
`https://rendertron.lossless.one/render/${cworkerevent.request.url}`,
|
||||
{
|
||||
method: cworkerevent.request.method,
|
||||
headers: rendertronHeaders
|
||||
}
|
||||
);
|
||||
cworkerevent.setResponse(await fetch(rendertronRequest));
|
||||
}
|
||||
};
|
31
ts_edgeworker/responders/static.responder.ts
Normal file
31
ts_edgeworker/responders/static.responder.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import { WorkerEvent } from '../classes.workerevent.js';
|
||||
|
||||
export const staticResponder: interfaces.TRequestResponser = async (cWorkerEventArg: WorkerEvent) => {
|
||||
if (cWorkerEventArg.responderInstruction.type === 'static') {
|
||||
const originResponse: any = await fetch(
|
||||
`https://statichost.lossless.one/resolve?url=${encodeURI(cWorkerEventArg.request.url)}`
|
||||
);
|
||||
|
||||
const cacheHeaders = new Headers();
|
||||
for (const kv of originResponse.headers.entries()) {
|
||||
cacheHeaders.append(kv[0], kv[1]);
|
||||
}
|
||||
cacheHeaders.delete('SERVEZONE_ROUTE');
|
||||
cacheHeaders.append('SERVEZONE_ROUTE', 'LOSSLESS_EDGE_STATICHOST');
|
||||
|
||||
if (cWorkerEventArg.responderInstruction.cacheClientSideForMin) {
|
||||
cacheHeaders.delete('Cache-Control');
|
||||
cacheHeaders.append('Cache-Control', `public, max-age=${cWorkerEventArg.responderInstruction.cacheClientSideForMin * 60}`);
|
||||
cacheHeaders.delete('Expires');
|
||||
cacheHeaders.append('Expires', new Date(Date.now() + cWorkerEventArg.responderInstruction.cacheClientSideForMin * 1000).toUTCString());
|
||||
}
|
||||
|
||||
const responseForClient = new Response(await originResponse.clone().body, {
|
||||
...originResponse,
|
||||
headers: cacheHeaders
|
||||
});
|
||||
|
||||
cWorkerEventArg.setResponse(responseForClient);
|
||||
}
|
||||
};
|
23
ts_edgeworker/responders/timeout.responder.ts
Normal file
23
ts_edgeworker/responders/timeout.responder.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import { WorkerEvent } from '../classes.workerevent.js';
|
||||
|
||||
export const timeoutResponder: interfaces.TRequestResponser = async (cWorkerEvent: WorkerEvent) => {
|
||||
await plugins.smartdelay.delayFor(10000);
|
||||
if (cWorkerEvent.routedThroughRendertron) {
|
||||
await plugins.smartdelay.delayFor(10000);
|
||||
}
|
||||
if (!cWorkerEvent.hasResponse) {
|
||||
const errorResponse = await fetch(
|
||||
`https://nullresolve.lossless.one/custom?title=${encodeURI(
|
||||
`Lossless Network: Request Cancellation!`
|
||||
)}&heading=${encodeURI(`Error: Request Cancellation`)}&text=${encodeURI(
|
||||
`Lossless Network could not decide how to respond to this request within 5 seconds. Therefore it timed out and has been canceled.
|
||||
<p>requestUrl: ${cWorkerEvent.request.url}<br>
|
||||
requestTime: ${Date.now()}<br>
|
||||
referenceNumber: xxxxxx</p>`
|
||||
)}`
|
||||
);
|
||||
cWorkerEvent.setResponse(errorResponse);
|
||||
}
|
||||
};
|
21
ts_edgeworker/responders/urlformatting.responder.ts
Normal file
21
ts_edgeworker/responders/urlformatting.responder.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import * as interfaces from '../interfaces/index.js';
|
||||
import { WorkerEvent } from '../classes.workerevent.js';
|
||||
|
||||
export const urlFormattingResponder: interfaces.TRequestResponser = async (eventArg: WorkerEvent) => {
|
||||
let shouldCorrect = false;
|
||||
const correctedUrl = new URL(eventArg.request.url);
|
||||
if (eventArg.parsedUrl.hostname.startsWith('www.')) {
|
||||
shouldCorrect = true;
|
||||
correctedUrl.hostname = eventArg.parsedUrl.hostname.substring(
|
||||
4,
|
||||
eventArg.parsedUrl.hostname.length
|
||||
);
|
||||
}
|
||||
if (eventArg.parsedUrl.protocol.startsWith('http:')) {
|
||||
shouldCorrect = true;
|
||||
correctedUrl.protocol = 'https:';
|
||||
}
|
||||
if (shouldCorrect) {
|
||||
eventArg.setResponse(Response.redirect(`${correctedUrl.protocol}//${correctedUrl.host}${correctedUrl.pathname}${correctedUrl.search}`, 301));
|
||||
}
|
||||
};
|
7
ts_edgeworker/versionhandler.ts
Normal file
7
ts_edgeworker/versionhandler.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
|
||||
export class VersionHandler {
|
||||
|
||||
}
|
||||
|
||||
export const versionHandlerInstance = new VersionHandler();
|
@ -1,3 +1,9 @@
|
||||
export * from './requestmodifier.js';
|
||||
export * from './responsemodifier.js';
|
||||
export * from './typedrequests.js';
|
||||
|
||||
import * as serviceworker from './serviceworker.js';
|
||||
|
||||
export {
|
||||
serviceworker,
|
||||
}
|
5
ts_interfaces/plugins.ts
Normal file
5
ts_interfaces/plugins.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import * as typedrequestInterfaces from '@api.global/typedrequest-interfaces';
|
||||
|
||||
export {
|
||||
typedrequestInterfaces,
|
||||
}
|
127
ts_interfaces/serviceworker.ts
Normal file
127
ts_interfaces/serviceworker.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export interface CacheStorage {
|
||||
keys: () => Promise<string[]>;
|
||||
match: any;
|
||||
open: any;
|
||||
delete: any;
|
||||
}
|
||||
export declare var caches: CacheStorage;
|
||||
|
||||
|
||||
// =============================
|
||||
// Interfaces for communication
|
||||
// =============================
|
||||
|
||||
export interface IMessage_Serviceworker_Client_UpdateInfo
|
||||
extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IMessage_Serviceworker_Client_UpdateInfo
|
||||
> {
|
||||
method: 'serviceworker_newVersion';
|
||||
request: {
|
||||
appVersion: string;
|
||||
appHash: string;
|
||||
};
|
||||
response: {};
|
||||
}
|
||||
|
||||
export interface IMessage_Serviceworker_Client_RequestReload
|
||||
extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IMessage_Serviceworker_Client_RequestReload
|
||||
> {
|
||||
method: 'serviceworker_requestReload';
|
||||
request: {};
|
||||
response: {};
|
||||
}
|
||||
|
||||
export interface IRequest_Serviceworker_Backend_VersionInfo
|
||||
extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IRequest_Serviceworker_Backend_VersionInfo
|
||||
> {
|
||||
method: 'serviceworker_versionInfo';
|
||||
request: {};
|
||||
response: {
|
||||
appHash: string;
|
||||
appSemVer: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ===============
|
||||
// web
|
||||
// ===============
|
||||
/**
|
||||
* purges the service workers cache
|
||||
*/
|
||||
export interface IRequest_PurgeServiceWorkerCache extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IRequest_PurgeServiceWorkerCache
|
||||
> {
|
||||
method: 'purgeServiceWorkerCache';
|
||||
request: {};
|
||||
response: {};
|
||||
}
|
||||
|
||||
/**
|
||||
* updates the info in all connected tabs
|
||||
*/
|
||||
export interface IMessage_Serviceworker_Client_UpdateInfo
|
||||
extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IMessage_Serviceworker_Client_UpdateInfo
|
||||
> {
|
||||
method: 'serviceworker_newVersion';
|
||||
request: {
|
||||
appVersion: string;
|
||||
appHash: string;
|
||||
};
|
||||
response: {};
|
||||
}
|
||||
|
||||
/**
|
||||
* requests all clients to reload
|
||||
*/
|
||||
export interface IMessage_Serviceworker_Client_RequestReload
|
||||
extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IMessage_Serviceworker_Client_RequestReload
|
||||
> {
|
||||
method: 'serviceworker_requestReload';
|
||||
request: {};
|
||||
response: {};
|
||||
}
|
||||
|
||||
/**
|
||||
* updates version infos
|
||||
*/
|
||||
export interface IRequest_Serviceworker_Backend_VersionInfo
|
||||
extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IRequest_Serviceworker_Backend_VersionInfo
|
||||
> {
|
||||
method: 'serviceworker_versionInfo';
|
||||
request: {};
|
||||
response: {
|
||||
appHash: string;
|
||||
appSemVer: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* ensures a stable connection between clients and the serviceworker
|
||||
*/
|
||||
export interface IRequest_Client_Serviceworker_ConnectionPolling
|
||||
extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IRequest_Client_Serviceworker_ConnectionPolling
|
||||
> {
|
||||
method: 'broadcastConnectionPolling',
|
||||
request: {
|
||||
tabId: string;
|
||||
},
|
||||
response: {
|
||||
serviceworkerId: string;
|
||||
}
|
||||
}
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@api.global/typedserver',
|
||||
version: '3.0.28',
|
||||
version: '3.0.29',
|
||||
description: 'A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.'
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import * as plugins from './typedserver_web.plugins.js';
|
||||
import * as interfaces from '../ts/interfaces/index.js';
|
||||
import * as interfaces from '../dist_ts_interfaces/index.js';
|
||||
import { logger } from './typedserver_web.logger.js';
|
||||
logger.log('info', `TypedServer-Devtools initialized!`);
|
||||
|
8
ts_web_serviceworker/00_commitinfo_data.ts
Normal file
8
ts_web_serviceworker/00_commitinfo_data.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @pushrocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@losslessone_private/lole-serviceworker',
|
||||
version: '1.0.206',
|
||||
description: 'serviceworker implementation for lossless websites'
|
||||
}
|
155
ts_web_serviceworker/classes.backend.ts
Normal file
155
ts_web_serviceworker/classes.backend.ts
Normal file
@ -0,0 +1,155 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as interfaces from '../dist_ts_interfaces/index.js';
|
||||
import { logger } from './logging.js';
|
||||
|
||||
// Add type definitions for ServiceWorker APIs
|
||||
declare global {
|
||||
interface ServiceWorkerGlobalScope extends EventTarget {
|
||||
clients: Clients;
|
||||
registration: ServiceWorkerRegistration;
|
||||
}
|
||||
|
||||
// Define Clients interface
|
||||
interface Clients {
|
||||
matchAll(options?: ClientQueryOptions): Promise<Client[]>;
|
||||
openWindow(url: string): Promise<WindowClient>;
|
||||
claim(): Promise<void>;
|
||||
get(id: string): Promise<Client | undefined>;
|
||||
}
|
||||
|
||||
interface ClientQueryOptions {
|
||||
includeUncontrolled?: boolean;
|
||||
type?: 'window' | 'worker' | 'sharedworker' | 'all';
|
||||
}
|
||||
|
||||
interface Client {
|
||||
id: string;
|
||||
type: 'window' | 'worker' | 'sharedworker';
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface WindowClient extends Client {
|
||||
focused: boolean;
|
||||
visibilityState: 'hidden' | 'visible' | 'prerender' | 'unloaded';
|
||||
focus(): Promise<WindowClient>;
|
||||
navigate(url: string): Promise<WindowClient>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
try {
|
||||
logger.log('info', 'Triggering reload for all clients due to new version');
|
||||
|
||||
// Send update message via DeesComms
|
||||
// This will be picked up by clients that have registered a handler for 'serviceworker_newVersion'
|
||||
await this.deesComms.postMessage({
|
||||
method: 'serviceworker_newVersion',
|
||||
request: {},
|
||||
messageId: `sw_update_${Date.now()}`
|
||||
});
|
||||
|
||||
// As a fallback, also use the clients API to reload clients that might not catch the broadcast
|
||||
// We need to type-cast self since TypeScript doesn't recognize ServiceWorker API
|
||||
const swSelf = self as unknown as ServiceWorkerGlobalScope;
|
||||
const clients = await swSelf.clients.matchAll({ type: 'window' });
|
||||
logger.log('info', `Found ${clients.length} clients to reload`);
|
||||
|
||||
for (const client of clients) {
|
||||
if ('navigate' in client) {
|
||||
// For modern browsers, navigate to the same URL to trigger reload
|
||||
(client as any).navigate(client.url);
|
||||
logger.log('info', `Navigated client to: ${client.url}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log('error', `Failed to reload clients: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* display notification
|
||||
*/
|
||||
public async addNotification(notificationArg: {
|
||||
title: string;
|
||||
body: string;
|
||||
}) {
|
||||
try {
|
||||
// Check if we have permission to show notifications
|
||||
const permission = self.Notification?.permission;
|
||||
if (permission !== 'granted') {
|
||||
logger.log('warn', `Cannot show notification: permission is ${permission}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Type-cast self to ServiceWorkerGlobalScope
|
||||
const swSelf = self as unknown as ServiceWorkerGlobalScope;
|
||||
|
||||
// Use type assertion for notification options to include vibrate
|
||||
const options = {
|
||||
body: notificationArg.body,
|
||||
icon: '/favicon.ico', // Assuming there's a favicon
|
||||
badge: '/favicon.ico',
|
||||
vibrate: [200, 100, 200]
|
||||
} as NotificationOptions;
|
||||
|
||||
await swSelf.registration.showNotification(notificationArg.title, options);
|
||||
|
||||
logger.log('info', `Notification shown: ${notificationArg.title}`);
|
||||
} catch (error) {
|
||||
logger.log('error', `Failed to show notification: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async alert(alertText: string) {
|
||||
// Since we can't directly show alerts from service worker context,
|
||||
// we'll use notifications as a fallback
|
||||
await this.addNotification({
|
||||
title: 'Alert',
|
||||
body: alertText
|
||||
});
|
||||
|
||||
// Send message to clients who might be able to show an actual alert
|
||||
try {
|
||||
await this.deesComms.postMessage({
|
||||
method: 'serviceworker_alert',
|
||||
request: { message: alertText },
|
||||
messageId: `sw_alert_${Date.now()}`
|
||||
});
|
||||
logger.log('info', `Alert message sent to clients: ${alertText}`);
|
||||
} catch (error) {
|
||||
logger.log('error', `Failed to send alert to clients: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
}
|
265
ts_web_serviceworker/classes.cachemanager.ts
Normal file
265
ts_web_serviceworker/classes.cachemanager.ts
Normal file
@ -0,0 +1,265 @@
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the service worker's fetch event to intercept and cache responses.
|
||||
*/
|
||||
private _setupCache = () => {
|
||||
// Create a matching request. For internal requests, reuse the original; for external requests, create one with CORS settings.
|
||||
const createMatchRequest = (requestArg: Request): Request => {
|
||||
let matchRequest: Request;
|
||||
try {
|
||||
if (
|
||||
requestArg.url.startsWith(
|
||||
this.losslessServiceWorkerRef.serviceWindowRef.location.origin
|
||||
)
|
||||
) {
|
||||
// Internal request
|
||||
matchRequest = requestArg;
|
||||
} else {
|
||||
// External request: create a new Request with appropriate CORS settings.
|
||||
matchRequest = new Request(requestArg.url, {
|
||||
method: requestArg.method,
|
||||
headers: requestArg.headers,
|
||||
mode: 'cors',
|
||||
credentials: 'same-origin',
|
||||
redirect: 'follow'
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.log('error', `Error creating match request for ${requestArg.url}: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
return matchRequest;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a 500 error response.
|
||||
*/
|
||||
const create500Response = async (requestArg: Request, responseArg: Response): Promise<Response> => {
|
||||
try {
|
||||
const responseText = await responseArg.clone().text();
|
||||
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 error 500</strong><br>
|
||||
</div>
|
||||
ServiceWorker is unable to fetch this request.<br>
|
||||
<br>
|
||||
<strong>Request URL:</strong> ${requestArg.url}<br>
|
||||
<strong>Response Type:</strong> ${responseArg.type}<br>
|
||||
<strong>Response Body:</strong> ${responseText}<br>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
{
|
||||
headers: { 'Content-Type': 'text/html' },
|
||||
status: 500
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
logger.log('error', `Error creating 500 response for ${requestArg.url}: ${err}`);
|
||||
return new Response('Internal error', { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for fetch events on the service worker's controlled window.
|
||||
this.losslessServiceWorkerRef.serviceWindowRef.addEventListener('fetch', async (fetchEventArg: any) => {
|
||||
try {
|
||||
const originalRequest: Request = fetchEventArg.request;
|
||||
const parsedUrl = new URL(originalRequest.url);
|
||||
|
||||
// Block requests that we don't want the service worker to handle.
|
||||
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;
|
||||
}
|
||||
|
||||
// Create a deferred promise for the fetch event's response.
|
||||
const done = plugins.smartpromise.defer<Response>();
|
||||
fetchEventArg.respondWith(done.promise);
|
||||
|
||||
// Determine whether this request should be cached.
|
||||
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')
|
||||
) {
|
||||
// Kick off an asynchronous update check.
|
||||
this.losslessServiceWorkerRef.updateManager.checkUpdate(this);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
logger.log('info', `NOTYETCACHED: Trying to cache ${matchRequest.url}`);
|
||||
let newResponse: Response;
|
||||
try {
|
||||
newResponse = await fetch(matchRequest);
|
||||
} catch (err: any) {
|
||||
logger.log('error', `Fetch error for ${matchRequest.url}: ${err}`);
|
||||
newResponse = await create500Response(matchRequest, new Response(err.message));
|
||||
}
|
||||
|
||||
// Check if the response should be cached. In this version, if the response status is >299 or the response is opaque, we do not cache.
|
||||
if (newResponse.status > 299 || newResponse.type === 'opaque') {
|
||||
logger.log(
|
||||
'error',
|
||||
`NOTCACHED: Can't cache response for ${matchRequest.url} (status: ${newResponse.status}, type: ${newResponse.type})`
|
||||
);
|
||||
// Optionally, you can force a 500 response so errors are clearly visible.
|
||||
done.resolve(await create500Response(matchRequest, newResponse));
|
||||
} else {
|
||||
try {
|
||||
const cache = await caches.open(this.usedCacheNames.runtimeCacheName);
|
||||
const responseToPutToCache = newResponse.clone();
|
||||
|
||||
// Create new headers preserving all except caching-related ones.
|
||||
const headers = new Headers();
|
||||
responseToPutToCache.headers.forEach((value, key) => {
|
||||
if (!['Cache-Control', 'cache-control', 'Expires', 'expires', 'Pragma', 'pragma'].includes(key)) {
|
||||
headers.set(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure that CORS-related headers are present.
|
||||
if (!headers.has('Access-Control-Allow-Origin')) {
|
||||
headers.set('Access-Control-Allow-Origin', '*');
|
||||
}
|
||||
if (!headers.has('Access-Control-Allow-Methods')) {
|
||||
headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
}
|
||||
if (!headers.has('Access-Control-Allow-Headers')) {
|
||||
headers.set('Access-Control-Allow-Headers', 'Content-Type');
|
||||
}
|
||||
|
||||
// Set Cross-Origin-Resource-Policy
|
||||
if (matchRequest.url.startsWith(this.losslessServiceWorkerRef.serviceWindowRef.location.origin)) {
|
||||
// For same-origin resources
|
||||
headers.set('Cross-Origin-Resource-Policy', 'same-origin');
|
||||
} else {
|
||||
// For cross-origin resources that we explicitly allow
|
||||
headers.set('Cross-Origin-Resource-Policy', 'cross-origin');
|
||||
}
|
||||
|
||||
// Set caching headers - use modern Cache-Control only
|
||||
headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
||||
|
||||
// IMPORTANT: Read the full response body as a blob to avoid issues (e.g., Safari locked streams).
|
||||
const bodyBlob = await responseToPutToCache.blob();
|
||||
const newCachedResponse = new Response(bodyBlob, {
|
||||
status: responseToPutToCache.status,
|
||||
statusText: responseToPutToCache.statusText,
|
||||
headers
|
||||
});
|
||||
|
||||
await cache.put(matchRequest, newCachedResponse);
|
||||
logger.log('ok', `NOWCACHED: Cached response for ${matchRequest.url} for subsequent requests!`);
|
||||
done.resolve(newResponse);
|
||||
} catch (err) {
|
||||
logger.log('error', `Error caching response for ${matchRequest.url}: ${err}`);
|
||||
done.resolve(await create500Response(matchRequest, newResponse));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For requests not intended for caching, simply fetch from the origin.
|
||||
logger.log('ok', `NOTCACHED: Not caching ${originalRequest.url}. Fetching from origin...`);
|
||||
try {
|
||||
const originResponse = await fetch(originalRequest);
|
||||
done.resolve(originResponse);
|
||||
} catch (err: any) {
|
||||
logger.log('error', `Fetch error for ${originalRequest.url}: ${err}`);
|
||||
done.resolve(await create500Response(originalRequest, new Response(err.message)));
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.log('error', `Unhandled fetch event error: ${err}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Cleans all caches.
|
||||
* Should only be run when a new ServiceWorker is activated.
|
||||
*/
|
||||
public cleanCaches = async (reasonArg = 'no reason given'): Promise<void> => {
|
||||
try {
|
||||
logger.log('info', `MAJOR CACHEEVENT: Cleaning caches now! Reason: ${reasonArg}`);
|
||||
const cacheNames = await caches.keys();
|
||||
const deletePromises = cacheNames.map((cacheToDelete) =>
|
||||
caches.delete(cacheToDelete).then(() => {
|
||||
logger.log('ok', `Deleted cache ${cacheToDelete}`);
|
||||
})
|
||||
);
|
||||
await Promise.all(deletePromises);
|
||||
} catch (err) {
|
||||
logger.log('error', `Error cleaning caches: ${err}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Revalidates the runtime cache by fetching fresh responses and updating the cache.
|
||||
*/
|
||||
public async revalidateCache(): Promise<void> {
|
||||
try {
|
||||
const runtimeCache = await caches.open(this.usedCacheNames.runtimeCacheName);
|
||||
const cacheKeys = await runtimeCache.keys();
|
||||
for (const requestArg of cacheKeys) {
|
||||
try {
|
||||
const clonedRequest = requestArg.clone();
|
||||
const response = await plugins.smartpromise.timeoutWrap(fetch(clonedRequest), 5000);
|
||||
if (response && response.status >= 200 && response.status < 300) {
|
||||
await runtimeCache.delete(requestArg);
|
||||
await runtimeCache.put(requestArg, response);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.log('error', `Error revalidating cache for ${requestArg.url}: ${err}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.log('error', `Error revalidating runtime cache: ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
151
ts_web_serviceworker/classes.networkmanager.ts
Normal file
151
ts_web_serviceworker/classes.networkmanager.ts
Normal file
@ -0,0 +1,151 @@
|
||||
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 | unknown;
|
||||
for (let i = 0; i <= retries; i++) {
|
||||
let timeoutId: number | undefined;
|
||||
const controller = new AbortController();
|
||||
|
||||
try {
|
||||
const isOnline = await this.checkOnlineStatus();
|
||||
if (!isOnline) {
|
||||
throw new Error('Device is offline');
|
||||
}
|
||||
|
||||
// Set up timeout
|
||||
timeoutId = setTimeout(() => controller.abort(), timeoutMs) as unknown as number;
|
||||
|
||||
const response = await fetch(request, {
|
||||
...typeof request === 'string' ? {} : request,
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
// Clear timeout on successful response
|
||||
clearTimeout(timeoutId);
|
||||
return response;
|
||||
} catch (error) {
|
||||
// Always clear timeout, even on error
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
lastError = error;
|
||||
logger.log('warn', `Request attempt ${i+1}/${retries+1} failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
|
||||
// Check if this was an abort error (timeout)
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
logger.log('warn', `Request timed out after ${timeoutMs}ms`);
|
||||
}
|
||||
|
||||
// Retry with backoff if we have retries left
|
||||
if (i < retries) {
|
||||
const backoffTime = backoffMs * (i + 1);
|
||||
logger.log('info', `Retrying in ${backoffTime}ms...`);
|
||||
await new Promise(resolve => setTimeout(resolve, backoffTime));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert lastError to Error if it isn't already
|
||||
const finalError = lastError instanceof Error
|
||||
? lastError
|
||||
: new Error(typeof lastError === 'string' ? lastError : 'Unknown error during request');
|
||||
|
||||
throw finalError;
|
||||
}
|
||||
}
|
91
ts_web_serviceworker/classes.serviceworker.ts
Normal file
91
ts_web_serviceworker/classes.serviceworker.ts
Normal file
@ -0,0 +1,91 @@
|
||||
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
|
||||
try {
|
||||
logger.log('success', `service worker installed! TimeStamp = ${new Date().toISOString()}`);
|
||||
selfArg.skipWaiting();
|
||||
logger.log('note', `Called skip waiting!`);
|
||||
done.resolve();
|
||||
} catch (error) {
|
||||
logger.log('error', `Service worker installation error: ${error}`);
|
||||
done.reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
try {
|
||||
await selfArg.clients.claim();
|
||||
logger.log('ok', 'Clients claimed successfully');
|
||||
|
||||
await this.cacheManager.cleanCaches('new service worker loaded! :)');
|
||||
logger.log('ok', 'Caches cleaned successfully');
|
||||
|
||||
done.resolve();
|
||||
logger.log('success', `Service worker activated at ${new Date().toISOString()}`);
|
||||
} catch (error) {
|
||||
logger.log('error', `Service worker activation error: ${error}`);
|
||||
done.reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
24
ts_web_serviceworker/classes.taskmanager.ts
Normal file
24
ts_web_serviceworker/classes.taskmanager.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { ServiceWorker } from './classes.serviceworker.js';
|
||||
|
||||
/**
|
||||
* Taskmanager
|
||||
* should use times allocated by browser
|
||||
*/
|
||||
export class TaskManager {
|
||||
public serviceworkerRef: ServiceWorker;
|
||||
public taskmanager = new plugins.taskbuffer.TaskManager();
|
||||
|
||||
constructor(serviceWorkerRefArg: ServiceWorker) {
|
||||
this.serviceworkerRef = serviceWorkerRefArg;
|
||||
this.taskmanager.start();
|
||||
}
|
||||
|
||||
public updateTask = new plugins.taskbuffer.Task({
|
||||
name: 'updateTask',
|
||||
taskFunction: async () => {
|
||||
await this.serviceworkerRef.cacheManager.cleanCaches('a new app version has been communicated by the server.');
|
||||
}
|
||||
})
|
||||
|
||||
}
|
1
ts_web_serviceworker/classes.typedrequestmanager.ts
Normal file
1
ts_web_serviceworker/classes.typedrequestmanager.ts
Normal file
@ -0,0 +1 @@
|
||||
import * as plugins from './plugins.js';
|
157
ts_web_serviceworker/classes.updatemanager.ts
Normal file
157
ts_web_serviceworker/classes.updatemanager.ts
Normal file
@ -0,0 +1,157 @@
|
||||
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 getAppHashRequest.fire({});
|
||||
|
||||
return response;
|
||||
} 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
|
||||
});
|
||||
}
|
20
ts_web_serviceworker/env.ts
Normal file
20
ts_web_serviceworker/env.ts
Normal file
@ -0,0 +1,20 @@
|
||||
export * from '../dist_ts_interfaces/index.js';
|
||||
|
||||
// =============================
|
||||
// Interfaces for the service worker
|
||||
// =============================
|
||||
// tslint:disable-next-line: interface-name
|
||||
export interface ServiceEvent extends Event {
|
||||
request: any;
|
||||
respondWith: any;
|
||||
waitUntil: any;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: interface-name
|
||||
export interface ServiceWindow extends Window {
|
||||
addEventListener: any;
|
||||
location: any;
|
||||
skipWaiting: any;
|
||||
clients: any;
|
||||
}
|
||||
declare var self: Window;
|
7
ts_web_serviceworker/index.ts
Normal file
7
ts_web_serviceworker/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
// TypeScript declatations
|
||||
import * as env from './env.js';
|
||||
declare var self: env.ServiceWindow;
|
||||
|
||||
import { ServiceWorker } from './classes.serviceworker.js';
|
||||
|
||||
const sw = new ServiceWorker(self);
|
17
ts_web_serviceworker/logging.ts
Normal file
17
ts_web_serviceworker/logging.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import * as smartlog from '@push.rocks/smartlog';
|
||||
import { SmartlogDestinationDevtools } from '@push.rocks/smartlog-destination-devtools';
|
||||
|
||||
export const logger = new smartlog.Smartlog({
|
||||
logContext: {
|
||||
company: 'Lossless GmbH',
|
||||
companyunit: 'Lossless Cloud',
|
||||
containerName: 'web',
|
||||
environment: 'production',
|
||||
runtime: 'chrome',
|
||||
zone: 'servezone'
|
||||
},
|
||||
minimumLogLevel: 'info'
|
||||
});
|
||||
|
||||
logger.addLogDestination(new SmartlogDestinationDevtools());
|
||||
logger.log('note', 'serviceworker console initialized!');
|
25
ts_web_serviceworker/plugins.ts
Normal file
25
ts_web_serviceworker/plugins.ts
Normal file
@ -0,0 +1,25 @@
|
||||
// @losslessone_private scope
|
||||
import * as interfaces from '../dist_ts_interfaces/index.js';
|
||||
|
||||
export { interfaces };
|
||||
|
||||
// @apiglobal scope
|
||||
import * as typedrequest from '@api.global/typedrequest';
|
||||
|
||||
export { typedrequest };
|
||||
|
||||
// @pushrocks scope
|
||||
import * as smartdelay from '@push.rocks/smartdelay';
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
import * as webrequest from '@push.rocks/webrequest';
|
||||
import * as webstore from '@push.rocks/webstore';
|
||||
import * as taskbuffer from '@push.rocks/taskbuffer';
|
||||
|
||||
export { smartdelay, smartpromise, webrequest, webstore, taskbuffer };
|
||||
|
||||
// @design.estate scope
|
||||
import * as deesComms from '@design.estate/dees-comms';
|
||||
|
||||
export {
|
||||
deesComms,
|
||||
}
|
8
ts_web_serviceworker_client/00_commitinfo_data.ts
Normal file
8
ts_web_serviceworker_client/00_commitinfo_data.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @pushrocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@losslessone_private/lele-serviceworker',
|
||||
version: '1.0.58',
|
||||
description: 'the mainthread of the serviceworker'
|
||||
}
|
58
ts_web_serviceworker_client/classes.actionmanager.ts
Normal file
58
ts_web_serviceworker_client/classes.actionmanager.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as interfaces from '../dist_ts_interfaces/index.js';
|
||||
import { logger } from './logging.js';
|
||||
|
||||
/**
|
||||
* MessageManager implements two ways of serviceworker communication
|
||||
* * the serviceWorker method
|
||||
* * the deesComms method using BroadcastChannel
|
||||
*/
|
||||
export class ActionManager {
|
||||
public deesComms = new plugins.deesComms.DeesComms();
|
||||
|
||||
constructor() {
|
||||
// lets define handlers on the client/tab side
|
||||
this.deesComms.createTypedHandler<interfaces.serviceworker.IMessage_Serviceworker_Client_UpdateInfo>('serviceworker_newVersion', async req => {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 200);
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
public async waitForServiceWorkerConnection () {
|
||||
console.log('waiting for service worker connection...')
|
||||
const tr = this.deesComms.createTypedRequest<interfaces.serviceworker.IRequest_Client_Serviceworker_ConnectionPolling>('broadcastConnectionPolling');
|
||||
let connected = false;
|
||||
while (!connected) {
|
||||
tr.fire({
|
||||
tabId: '123'
|
||||
}).then(response => {
|
||||
if (response.serviceworkerId) {
|
||||
console.log('connected to serviceworker!');
|
||||
connected = true;
|
||||
}
|
||||
}).catch();
|
||||
await plugins.smartdelay.delayFor(777);
|
||||
if (!connected) {
|
||||
// lets wake it up.
|
||||
navigator.serviceWorker.controller.postMessage({
|
||||
type: 'wakeUpCall',
|
||||
});
|
||||
}
|
||||
}
|
||||
console.log('ok, got serviceworker connection.')
|
||||
}
|
||||
|
||||
public async purgeServiceWorkerCache () {
|
||||
const tr = this.deesComms.createTypedRequest<interfaces.serviceworker.IRequest_PurgeServiceWorkerCache>('purgeServiceWorkerCache');
|
||||
const response = await tr.fire({});
|
||||
return response;
|
||||
}
|
||||
|
||||
public async getVersionInfo () {
|
||||
const tr = this.deesComms.createTypedRequest<interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo>('serviceworker_versionInfo');
|
||||
const response = await tr.fire({});
|
||||
return response;
|
||||
}
|
||||
}
|
29
ts_web_serviceworker_client/classes.globalsw.ts
Normal file
29
ts_web_serviceworker_client/classes.globalsw.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { ServiceworkerClient } from './classes.serviceworkerclient.js';
|
||||
|
||||
export class GlobalSW {
|
||||
losslessSw: ServiceworkerClient;
|
||||
constructor(losslessServiceWorkerInstanceArg: ServiceworkerClient) {
|
||||
this.losslessSw = losslessServiceWorkerInstanceArg;
|
||||
globalThis.globalSw = this;
|
||||
};
|
||||
|
||||
/**
|
||||
* purges the cache of the app's serviceworker
|
||||
* @returns
|
||||
*/
|
||||
public async purgeCache() {
|
||||
await this.losslessSw.actionManager.waitForServiceWorkerConnection();
|
||||
console.log(`purgeCache() was executed via globalThis.globalSw`);
|
||||
const result = await this.losslessSw.actionManager.purgeServiceWorkerCache();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* attempts to reload the app
|
||||
*/
|
||||
public async reloadApp() {
|
||||
await this.purgeCache();
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
16
ts_web_serviceworker_client/classes.notificationmanager.ts
Normal file
16
ts_web_serviceworker_client/classes.notificationmanager.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as interfaces from '../dist_ts_interfaces/index.js';
|
||||
import { logger } from "./logging.js";
|
||||
|
||||
export class NotificationManager {
|
||||
|
||||
constructor() {
|
||||
// this.askPermission();
|
||||
}
|
||||
|
||||
public askPermission () {
|
||||
Notification.requestPermission((status) => {
|
||||
console.log('Notification permission status:', status);
|
||||
});
|
||||
}
|
||||
}
|
69
ts_web_serviceworker_client/classes.serviceworkerclient.ts
Normal file
69
ts_web_serviceworker_client/classes.serviceworkerclient.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as interfaces from '../dist_ts_interfaces/index.js';
|
||||
import { logger } from "./logging.js";
|
||||
import { NotificationManager } from './classes.notificationmanager.js';
|
||||
import { ActionManager } from './classes.actionmanager.js';
|
||||
import { GlobalSW } from './classes.globalsw.js'
|
||||
|
||||
export class ServiceworkerClient {
|
||||
// STATIC
|
||||
public static async createServiceWorker(): Promise<ServiceworkerClient> {
|
||||
if ('serviceWorker' in navigator) {
|
||||
try {
|
||||
logger.log('info', 'trying to register serviceworker');
|
||||
// this is some magic for Parcel to not pick up the serviceworker
|
||||
const serviceworkerInNavigator: ServiceWorkerContainer = navigator.serviceWorker;
|
||||
const swRegistration: ServiceWorkerRegistration = await serviceworkerInNavigator.register('/serviceworker.bundle.js', {
|
||||
scope: '/',
|
||||
updateViaCache: 'none'
|
||||
});
|
||||
plugins.smartdelay.delayFor(2000).then(async () => {
|
||||
swRegistration.onupdatefound = () => {
|
||||
logger.log('info', 'update found for service worker!');
|
||||
logger.log('warn', 'trying to find convenient time to update');
|
||||
};
|
||||
while(true) {
|
||||
await plugins.smartdelay.delayFor(60000);
|
||||
swRegistration.update();
|
||||
}
|
||||
});
|
||||
logger.log('ok', 'serviceworker registered');
|
||||
await navigator.serviceWorker.ready;
|
||||
logger.log('ok', 'serviceworker is ready!');
|
||||
await this.waitForController();
|
||||
const losslessServiceWorkerInstance = new ServiceworkerClient();
|
||||
return losslessServiceWorkerInstance;
|
||||
} catch (err) {
|
||||
// sentry integration here
|
||||
console.log(err);
|
||||
console.log(err.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async waitForController() {
|
||||
const done = new plugins.smartpromise.Deferred();
|
||||
const checkReady = () => {
|
||||
if(navigator.serviceWorker.controller) {
|
||||
logger.log('ok', 'controller is ready');
|
||||
done.resolve();
|
||||
} else {
|
||||
logger.log('warn', 'controller not ready');
|
||||
}
|
||||
};
|
||||
navigator.serviceWorker.oncontrollerchange = checkReady;
|
||||
checkReady();
|
||||
await done.promise;
|
||||
}
|
||||
|
||||
// INSTANCE
|
||||
public notificationManager: NotificationManager;
|
||||
public actionManager: ActionManager;
|
||||
public globalSw: GlobalSW;
|
||||
|
||||
constructor() {
|
||||
this.notificationManager = new NotificationManager();
|
||||
this.actionManager = new ActionManager();
|
||||
this.globalSw = new GlobalSW(this);
|
||||
}
|
||||
}
|
24
ts_web_serviceworker_client/index.ts
Normal file
24
ts_web_serviceworker_client/index.ts
Normal file
@ -0,0 +1,24 @@
|
||||
// types
|
||||
import type * as interfaces from '../dist_ts_interfaces/index.js';
|
||||
export type {
|
||||
interfaces
|
||||
}
|
||||
|
||||
// ====================================
|
||||
// imports
|
||||
// ====================================
|
||||
|
||||
import { logger } from './logging.js';
|
||||
logger.log('note', 'mainthread console initialized!');
|
||||
|
||||
import { ServiceworkerClient } from './classes.serviceworkerclient.js';
|
||||
|
||||
export type {
|
||||
ServiceworkerClient
|
||||
}
|
||||
|
||||
export const getServiceworkerClient = async () => {
|
||||
const swClient = await ServiceworkerClient.createServiceWorker(); // lets setup the service worker
|
||||
logger.log('ok', 'service worker ready!'); // and wait for it to be ready
|
||||
return swClient;
|
||||
};
|
16
ts_web_serviceworker_client/logging.ts
Normal file
16
ts_web_serviceworker_client/logging.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import * as smartlog from '@push.rocks/smartlog';
|
||||
import { SmartlogDestinationDevtools } from '@push.rocks/smartlog-destination-devtools';
|
||||
|
||||
export const logger = new smartlog.Smartlog({
|
||||
logContext: {
|
||||
company: 'Lossless GmbH',
|
||||
companyunit: 'Lossless Cloud',
|
||||
containerName: 'web',
|
||||
environment: 'production',
|
||||
runtime: 'chrome',
|
||||
zone: 'servezone'
|
||||
},
|
||||
minimumLogLevel: 'info'
|
||||
});
|
||||
|
||||
logger.addLogDestination(new SmartlogDestinationDevtools());
|
22
ts_web_serviceworker_client/plugins.ts
Normal file
22
ts_web_serviceworker_client/plugins.ts
Normal file
@ -0,0 +1,22 @@
|
||||
// @apiglobal scope
|
||||
import * as typedrequestInterfaces from '@api.global/typedrequest-interfaces';
|
||||
|
||||
export { typedrequestInterfaces };
|
||||
|
||||
// designestate
|
||||
import * as deesComms from '@design.estate/dees-comms';
|
||||
|
||||
export {
|
||||
deesComms
|
||||
};
|
||||
|
||||
// @pushrocks scope
|
||||
import * as smartdelay from '@push.rocks/smartdelay';
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
import * as webstore from '@push.rocks/webstore';
|
||||
|
||||
export {
|
||||
smartdelay,
|
||||
smartpromise,
|
||||
webstore
|
||||
};
|
Reference in New Issue
Block a user